Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
8.3 kB · 265 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266/** * Marketplace reconciler — makes known_marketplaces.json consistent with * declared intent in settings. * * Two layers: * - diffMarketplaces(): comparison (reads .git for worktree canonicalization, memoized) * - reconcileMarketplaces(): bundled diff + install (I/O, idempotent, additive) */
import isEqual from 'lodash-es/isEqual.js'import { isAbsolute, resolve } from 'path'import { getOriginalCwd } from '../../bootstrap/state.js'import { logForDebugging } from '../debug.js'import { errorMessage } from '../errors.js'import { pathExists } from '../file.js'import { findCanonicalGitRoot } from '../git.js'import { logError } from '../log.js'import { addMarketplaceSource, type DeclaredMarketplace, getDeclaredMarketplaces, loadKnownMarketplacesConfig,} from './marketplaceManager.js'import { isLocalMarketplaceSource, type KnownMarketplacesFile, type MarketplaceSource,} from './schemas.js'
export type MarketplaceDiff = { /** Declared in settings, absent from known_marketplaces.json */ missing: string[] /** Present in both, but settings source ≠ JSON source (settings wins) */ sourceChanged: Array<{ name: string declaredSource: MarketplaceSource materializedSource: MarketplaceSource }> /** Present in both, sources match */ upToDate: string[]}
/** * Compare declared intent (settings) against materialized state (JSON). * * Resolves relative directory/file paths in `declared` before comparing, * so project settings with `./path` match JSON's absolute path. Path * resolution reads `.git` to canonicalize worktree paths (memoized). */export function diffMarketplaces( declared: Record<string, DeclaredMarketplace>, materialized: KnownMarketplacesFile, opts?: { projectRoot?: string },): MarketplaceDiff { const missing: string[] = [] const sourceChanged: MarketplaceDiff['sourceChanged'] = [] const upToDate: string[] = []
for (const [name, intent] of Object.entries(declared)) { const state = materialized[name] const normalizedIntent = normalizeSource(intent.source, opts?.projectRoot)
if (!state) { missing.push(name) } else if (intent.sourceIsFallback) { // Fallback: presence suffices. Don't compare sources — the declared source // is only a default for the `missing` branch. If seed/prior-install/mirror // materialized this marketplace under ANY source, leave it alone. Comparing // would report sourceChanged → re-clone → stomp the materialized content. upToDate.push(name) } else if (!isEqual(normalizedIntent, state.source)) { sourceChanged.push({ name, declaredSource: normalizedIntent, materializedSource: state.source, }) } else { upToDate.push(name) } }
return { missing, sourceChanged, upToDate }}
export type ReconcileOptions = { /** Skip a declared marketplace. Used by zip-cache mode for unsupported source types. */ skip?: (name: string, source: MarketplaceSource) => boolean onProgress?: (event: ReconcileProgressEvent) => void}
export type ReconcileProgressEvent = | { type: 'installing' name: string action: 'install' | 'update' index: number total: number } | { type: 'installed'; name: string; alreadyMaterialized: boolean } | { type: 'failed'; name: string; error: string }
export type ReconcileResult = { installed: string[] updated: string[] failed: Array<{ name: string; error: string }> upToDate: string[] skipped: string[]}
/** * Make known_marketplaces.json consistent with declared intent. * Idempotent. Additive only (never deletes). Does not touch AppState. */export async function reconcileMarketplaces( opts?: ReconcileOptions,): Promise<ReconcileResult> { const declared = getDeclaredMarketplaces() if (Object.keys(declared).length === 0) { return { installed: [], updated: [], failed: [], upToDate: [], skipped: [] } }
let materialized: KnownMarketplacesFile try { materialized = await loadKnownMarketplacesConfig() } catch (e) { logError(e) materialized = {} }
const diff = diffMarketplaces(declared, materialized, { projectRoot: getOriginalCwd(), })
type WorkItem = { name: string source: MarketplaceSource action: 'install' | 'update' } const work: WorkItem[] = [ ...diff.missing.map( (name): WorkItem => ({ name, source: normalizeSource(declared[name]!.source), action: 'install', }), ), ...diff.sourceChanged.map( ({ name, declaredSource }): WorkItem => ({ name, source: declaredSource, action: 'update', }), ), ]
const skipped: string[] = [] const toProcess: WorkItem[] = [] for (const item of work) { if (opts?.skip?.(item.name, item.source)) { skipped.push(item.name) continue } // For sourceChanged local-path entries, skip if the declared path doesn't // exist. Guards multi-checkout scenarios where normalizeSource can't // canonicalize and produces a dead path — the materialized entry may still // be valid; addMarketplaceSource would fail anyway, so skipping avoids a // noisy "failed" event and preserves the working entry. Missing entries // are NOT skipped (nothing to preserve; the user should see the error). if ( item.action === 'update' && isLocalMarketplaceSource(item.source) && !(await pathExists(item.source.path)) ) { logForDebugging( `[reconcile] '${item.name}' declared path does not exist; keeping materialized entry`, ) skipped.push(item.name) continue } toProcess.push(item) }
if (toProcess.length === 0) { return { installed: [], updated: [], failed: [], upToDate: diff.upToDate, skipped, } }
logForDebugging( `[reconcile] ${toProcess.length} marketplace(s): ${toProcess.map(w => `${w.name}(${w.action})`).join(', ')}`, )
const installed: string[] = [] const updated: string[] = [] const failed: ReconcileResult['failed'] = []
for (let i = 0; i < toProcess.length; i++) { const { name, source, action } = toProcess[i]! opts?.onProgress?.({ type: 'installing', name, action, index: i + 1, total: toProcess.length, })
try { // addMarketplaceSource is source-idempotent — same source returns // alreadyMaterialized:true without cloning. For 'update' (source // changed), the new source won't match existing → proceeds with clone // and overwrites the old JSON entry. const result = await addMarketplaceSource(source)
if (action === 'install') installed.push(name) else updated.push(name) opts?.onProgress?.({ type: 'installed', name, alreadyMaterialized: result.alreadyMaterialized, }) } catch (e) { const error = errorMessage(e) failed.push({ name, error }) opts?.onProgress?.({ type: 'failed', name, error }) logError(e) } }
return { installed, updated, failed, upToDate: diff.upToDate, skipped }}
/** * Resolve relative directory/file paths for stable comparison. * Settings declared at project scope may use project-relative paths; * JSON stores absolute paths. * * For git worktrees, resolve against the main checkout (canonical root) * instead of the worktree cwd. Project settings are checked into git, * so `./foo` means "relative to this repo" — but known_marketplaces.json is * user-global with one entry per marketplace name. Resolving against the * worktree cwd means each worktree session overwrites the shared entry with * its own absolute path, and deleting the worktree leaves a dead * installLocation. The canonical root is stable across all worktrees. */function normalizeSource( source: MarketplaceSource, projectRoot?: string,): MarketplaceSource { if ( (source.source === 'directory' || source.source === 'file') && !isAbsolute(source.path) ) { const base = projectRoot ?? getOriginalCwd() const canonicalRoot = findCanonicalGitRoot(base) return { ...source, path: resolve(canonicalRoot ?? base, source.path), } } return source}