source dump of claude code
at main 164 lines 5.3 kB view raw
1/** 2 * Zip Cache Adapters 3 * 4 * I/O helpers for the plugin zip cache. These functions handle reading/writing 5 * zip-cache-local metadata files, extracting ZIPs to session directories, 6 * and creating ZIPs for newly installed plugins. 7 * 8 * The zip cache stores data on a mounted volume (e.g., Filestore) that persists 9 * across ephemeral container lifetimes. The session cache is a local temp dir 10 * for extracted plugins used during a single session. 11 */ 12 13import { readFile } from 'fs/promises' 14import { join } from 'path' 15import { logForDebugging } from '../debug.js' 16import { jsonParse, jsonStringify } from '../slowOperations.js' 17import { loadKnownMarketplacesConfigSafe } from './marketplaceManager.js' 18import { 19 type KnownMarketplacesFile, 20 KnownMarketplacesFileSchema, 21 type PluginMarketplace, 22 PluginMarketplaceSchema, 23} from './schemas.js' 24import { 25 atomicWriteToZipCache, 26 getMarketplaceJsonRelativePath, 27 getPluginZipCachePath, 28 getZipCacheKnownMarketplacesPath, 29} from './zipCache.js' 30 31// ── Metadata I/O ── 32 33/** 34 * Read known_marketplaces.json from the zip cache. 35 * Returns empty object if file doesn't exist, can't be parsed, or fails schema 36 * validation (data comes from a shared mounted volume — other containers may write). 37 */ 38export async function readZipCacheKnownMarketplaces(): Promise<KnownMarketplacesFile> { 39 try { 40 const content = await readFile(getZipCacheKnownMarketplacesPath(), 'utf-8') 41 const parsed = KnownMarketplacesFileSchema().safeParse(jsonParse(content)) 42 if (!parsed.success) { 43 logForDebugging( 44 `Invalid known_marketplaces.json in zip cache: ${parsed.error.message}`, 45 { level: 'error' }, 46 ) 47 return {} 48 } 49 return parsed.data 50 } catch { 51 return {} 52 } 53} 54 55/** 56 * Write known_marketplaces.json to the zip cache atomically. 57 */ 58export async function writeZipCacheKnownMarketplaces( 59 data: KnownMarketplacesFile, 60): Promise<void> { 61 await atomicWriteToZipCache( 62 getZipCacheKnownMarketplacesPath(), 63 jsonStringify(data, null, 2), 64 ) 65} 66 67// ── Marketplace JSON ── 68 69/** 70 * Read a marketplace JSON file from the zip cache. 71 */ 72export async function readMarketplaceJson( 73 marketplaceName: string, 74): Promise<PluginMarketplace | null> { 75 const zipCachePath = getPluginZipCachePath() 76 if (!zipCachePath) { 77 return null 78 } 79 const relPath = getMarketplaceJsonRelativePath(marketplaceName) 80 const fullPath = join(zipCachePath, relPath) 81 try { 82 const content = await readFile(fullPath, 'utf-8') 83 const parsed = jsonParse(content) 84 const result = PluginMarketplaceSchema().safeParse(parsed) 85 if (result.success) { 86 return result.data 87 } 88 logForDebugging( 89 `Invalid marketplace JSON for ${marketplaceName}: ${result.error}`, 90 ) 91 return null 92 } catch { 93 return null 94 } 95} 96 97/** 98 * Save a marketplace JSON to the zip cache from its install location. 99 */ 100export async function saveMarketplaceJsonToZipCache( 101 marketplaceName: string, 102 installLocation: string, 103): Promise<void> { 104 const zipCachePath = getPluginZipCachePath() 105 if (!zipCachePath) { 106 return 107 } 108 const content = await readMarketplaceJsonContent(installLocation) 109 if (content !== null) { 110 const relPath = getMarketplaceJsonRelativePath(marketplaceName) 111 await atomicWriteToZipCache(join(zipCachePath, relPath), content) 112 } 113} 114 115/** 116 * Read marketplace.json content from a cloned marketplace directory or file. 117 * For directory sources: checks .claude-plugin/marketplace.json, marketplace.json 118 * For URL sources: the installLocation IS the marketplace JSON file itself. 119 */ 120async function readMarketplaceJsonContent(dir: string): Promise<string | null> { 121 const candidates = [ 122 join(dir, '.claude-plugin', 'marketplace.json'), 123 join(dir, 'marketplace.json'), 124 dir, // For URL sources, installLocation IS the marketplace JSON file 125 ] 126 for (const candidate of candidates) { 127 try { 128 return await readFile(candidate, 'utf-8') 129 } catch { 130 // ENOENT (doesn't exist) or EISDIR (directory) — try next 131 } 132 } 133 return null 134} 135 136/** 137 * Sync marketplace data to zip cache for offline access. 138 * Saves marketplace JSONs and merges with previously cached data 139 * so ephemeral containers can access marketplaces without re-cloning. 140 */ 141export async function syncMarketplacesToZipCache(): Promise<void> { 142 // Read-only iteration — Safe variant so a corrupted config doesn't throw. 143 // This runs during startup paths; a throw here cascades to the same 144 // try-block that catches loadAllPlugins failures. 145 const knownMarketplaces = await loadKnownMarketplacesConfigSafe() 146 147 // Save marketplace JSONs to zip cache 148 for (const [name, entry] of Object.entries(knownMarketplaces)) { 149 if (!entry.installLocation) continue 150 try { 151 await saveMarketplaceJsonToZipCache(name, entry.installLocation) 152 } catch (error) { 153 logForDebugging(`Failed to save marketplace JSON for ${name}: ${error}`) 154 } 155 } 156 157 // Merge with previously cached data (ephemeral containers lose global config) 158 const zipCacheKnownMarketplaces = await readZipCacheKnownMarketplaces() 159 const mergedKnownMarketplaces: KnownMarketplacesFile = { 160 ...zipCacheKnownMarketplaces, 161 ...knownMarketplaces, 162 } 163 await writeZipCacheKnownMarketplaces(mergedKnownMarketplaces) 164}