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