@kolisachint/hoocode-agent 0.5.12 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/dist/core/extensions/loader.d.ts +18 -6
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +35 -19
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/install.d.ts +51 -10
- package/dist/core/extensions/plugins/install.d.ts.map +1 -1
- package/dist/core/extensions/plugins/install.js +178 -31
- package/dist/core/extensions/plugins/install.js.map +1 -1
- package/dist/core/extensions/plugins/listing.d.ts +8 -2
- package/dist/core/extensions/plugins/listing.d.ts.map +1 -1
- package/dist/core/extensions/plugins/listing.js +29 -5
- package/dist/core/extensions/plugins/listing.js.map +1 -1
- package/dist/core/extensions/plugins/locations.d.ts +62 -12
- package/dist/core/extensions/plugins/locations.d.ts.map +1 -1
- package/dist/core/extensions/plugins/locations.js +74 -18
- package/dist/core/extensions/plugins/locations.js.map +1 -1
- package/dist/core/extensions/plugins/marketplace.d.ts +29 -3
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
- package/dist/core/extensions/plugins/marketplace.js +40 -7
- package/dist/core/extensions/plugins/marketplace.js.map +1 -1
- package/dist/core/extensions/plugins/trust.d.ts +56 -0
- package/dist/core/extensions/plugins/trust.d.ts.map +1 -0
- package/dist/core/extensions/plugins/trust.js +88 -0
- package/dist/core/extensions/plugins/trust.js.map +1 -0
- package/dist/core/settings-defaults.d.ts +1 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +14 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/tools/plugins.d.ts.map +1 -1
- package/dist/core/tools/plugins.js +18 -3
- package/dist/core/tools/plugins.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +7 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +103 -11
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +15 -2
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +6 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/install.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin install/uninstall/discovery — the shared engine behind both the\n * `/plugin` slash command (human-driven) and the model-facing lifecycle tools\n * (`SearchPlugins`, `InstallPlugin`, ...). Keeping it in one place means the two\n * surfaces can never drift on where plugins live or how sources resolve.\n *\n * Trust model (see docs/plugin-system-spec.md): *adding* a marketplace is the\n * human trust boundary; *installing* from an already-trusted marketplace is the\n * model's discretion (package-manager model), and must stay transparent and\n * reversible. This module performs the mechanical install/remove; the gating\n * (announce, injection carve-out) lives with the callers.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../../../config.js\";\nimport { defaultPluginDirs } from \"../loader.js\";\nimport { hasAnyManifest } from \"./formats/index.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\nimport { discoverPlugins } from \"./index.js\";\nimport {\n\tcandidatePluginDirs,\n\tconsumptionPluginsDir,\n\tmarketplaceCacheDir,\n\tmarketplaceCacheMetaPath,\n\tmarketplaceCacheRoot,\n\tmarketplaceStorePath,\n\tsanitizeForDir,\n} from \"./locations.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { parsePluginDir } from \"./manifest.js\";\nimport {\n\ttype MarketplacePluginSource,\n\ttype MarketplaceRecord,\n\tparseMarketplaceDir,\n\treadMarketplaceStore,\n\tresolvePluginSource,\n} from \"./marketplace.js\";\n\n/**\n * Marketplace registries older versions wrote into the working tree, newest\n * convention first. Still read so a user who added a marketplace before the\n * registry moved to the agent dir does not silently lose it; nothing writes here.\n */\nfunction legacyStorePaths(cwd: string): string[] {\n\treturn [path.join(cwd, \".agents\", \"marketplaces.json\"), path.join(cwd, \".hoocode\", \"marketplaces.json\")];\n}\n\n/** Absolute path to the curated default marketplace bundled with hoocode. */\nfunction defaultMarketplaceDir(): string {\n\treturn fileURLToPath(new URL(\"./default-marketplace/\", import.meta.url));\n}\n\n/** The default marketplace record — always present so source-level trust is meaningful out of the box. */\nexport function defaultMarketplaceRecord(): MarketplaceRecord {\n\treturn { location: \"hoocode-default\", dir: defaultMarketplaceDir() };\n}\n\n/**\n * Curated well-known marketplaces, trusted out of the box. Shipping an entry\n * here is a maintainer-level trust decision (the human half of the \"adding a\n * marketplace is the human trust boundary\" rule) — keep this list short and\n * high-trust. Indices are cloned lazily into the marketplace cache on first\n * search and refreshed on a TTL thereafter. Refreshing the *index* only changes\n * what is discoverable; installed plugins are still never auto-updated, and\n * plugin sources inside the official index are sha-pinned.\n */\nexport const WELL_KNOWN_MARKETPLACES: ReadonlyArray<{ name: string; url: string }> = [\n\t{ name: \"claude-plugins-official\", url: \"https://github.com/anthropics/claude-plugins-official\" },\n\t// The Copilot plugin directory ships both a `.github/plugin/marketplace.json`\n\t// and a `.claude-plugin/marketplace.json`, so it parses with\n\t// supportPlatform [\"claude\", \"github\"] — the default `github` source.\n\t{ name: \"copilot-plugins\", url: \"https://github.com/github/copilot-plugins\" },\n];\n\n/**\n * How long a cached marketplace index is considered fresh.\n *\n * Refreshing an *index* is safe to automate: it changes what is discoverable,\n * not what is installed. The rule that an installed plugin is never auto-updated\n * from a remote is untouched — that is the supply-chain boundary, and this is a\n * catalog listing.\n */\nconst MARKETPLACE_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction marketplaceTtlMs(): number {\n\tconst raw = Number(process.env.HOOCODE_MARKETPLACE_TTL_MS);\n\treturn Number.isFinite(raw) && raw >= 0 ? raw : MARKETPLACE_TTL_MS;\n}\n\ntype CacheMeta = Record<string, string>;\n\nfunction readCacheMeta(agentDir: string): CacheMeta {\n\tconst parsed = readJsonFile<CacheMeta>(marketplaceCacheMetaPath(agentDir));\n\treturn parsed && typeof parsed === \"object\" ? parsed : {};\n}\n\nfunction recordFetch(agentDir: string, url: string): void {\n\tconst meta = readCacheMeta(agentDir);\n\tmeta[url] = new Date().toISOString();\n\tconst file = marketplaceCacheMetaPath(agentDir);\n\tmkdirSync(path.dirname(file), { recursive: true });\n\twriteFileSync(file, `${JSON.stringify(meta, null, 2)}\\n`, \"utf8\");\n}\n\nfunction isStale(agentDir: string, url: string): boolean {\n\tconst ttl = marketplaceTtlMs();\n\tif (ttl === 0) return true;\n\tconst at = readCacheMeta(agentDir)[url];\n\tif (!at) return true;\n\tconst age = Date.now() - Date.parse(at);\n\treturn !Number.isFinite(age) || age > ttl;\n}\n\nfunction readJsonFile<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Bring an existing clone up to date, falling back to a fresh clone when the pull\n * cannot fast-forward (a force-pushed remote, or a corrupt cache).\n *\n * The re-clone goes to a temp directory and is swapped in only on success. An\n * earlier version deleted the cache first, which meant a refresh attempted while\n * offline destroyed a perfectly good index — the opposite of the \"offline is\n * non-fatal\" property the caller depends on.\n */\nasync function refreshClone(url: string, dir: string): Promise<{ code: number; stdout: string; stderr: string }> {\n\tconst pulled = await execGit([\"-C\", dir, \"pull\", \"--ff-only\"]);\n\tif (pulled.code === 0) return pulled;\n\n\tconst staging = mkdtempSync(path.join(os.tmpdir(), \"hoo-market-refresh-\"));\n\tconst target = path.join(staging, \"clone\");\n\tconst cloned = await cloneGitRepo(url, target);\n\tif (cloned.code !== 0) {\n\t\trmSync(staging, { recursive: true, force: true });\n\t\treturn cloned;\n\t}\n\trmSync(dir, { recursive: true, force: true });\n\tmkdirSync(path.dirname(dir), { recursive: true });\n\ttry {\n\t\trenameSync(target, dir);\n\t} catch {\n\t\tcpSync(target, dir, { recursive: true });\n\t}\n\trmSync(staging, { recursive: true, force: true });\n\treturn cloned;\n}\n\nexport interface EnsureMarketplacesOptions {\n\t/** Refresh regardless of the TTL. */\n\tforce?: boolean;\n}\n\n/**\n * Make sure every well-known marketplace index is cached and reasonably fresh.\n *\n * Clones what is missing and pulls what has gone stale. Without the TTL the\n * indices were fetched exactly once and never again, so a plugin added upstream\n * stayed permanently invisible — the inherit half of the system quietly stopped\n * inheriting. Offline is non-fatal: each failure is returned and search degrades\n * to whatever is already on disk.\n */\nexport async function ensureWellKnownMarketplaces(\n\tagentDir: string = getAgentDir(),\n\toptions: EnsureMarketplacesOptions = {},\n): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tconst present = existsSync(dir);\n\t\tif (present && !options.force && !isStale(agentDir, wk.url)) continue;\n\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tconst res = present ? await refreshClone(wk.url, dir) : await cloneGitRepo(wk.url, dir);\n\t\tif (res.code !== 0) {\n\t\t\t// A failed *refresh* keeps the stale copy: an out-of-date index is far\n\t\t\t// more useful than none, and the network may simply be down.\n\t\t\tif (!present) rmSync(dir, { recursive: true, force: true });\n\t\t\terrors.push(`${wk.name}: ${(res.stderr || res.stdout).trim()}`);\n\t\t\tcontinue;\n\t\t}\n\t\trecordFetch(agentDir, wk.url);\n\t}\n\treturn errors;\n}\n\n/**\n * Refresh every cached index — well-known and user-added — ignoring the TTL.\n * The explicit half of the freshness story, for when a user knows the upstream\n * changed and does not want to wait out the interval.\n */\nexport async function refreshMarketplaces(\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<{ refreshed: string[]; errors: string[] }> {\n\tconst errors = await ensureWellKnownMarketplaces(agentDir, { force: true });\n\tconst refreshed = WELL_KNOWN_MARKETPLACES.filter((wk) => existsSync(marketplaceCacheDir(wk.url, agentDir))).map(\n\t\t(wk) => wk.name,\n\t);\n\n\t// User-added marketplaces are cached the same way when they came from git; a\n\t// local-path marketplace is read in place and has nothing to refresh.\n\tfor (const record of readMarketplaceRecords(cwd, agentDir)) {\n\t\tconst isCachedClone = record.dir.startsWith(marketplaceCacheRoot(agentDir));\n\t\tif (!isCachedClone || WELL_KNOWN_MARKETPLACES.some((wk) => wk.url === record.location)) continue;\n\t\tconst res = await refreshClone(record.location, record.dir);\n\t\tif (res.code !== 0) errors.push(`${record.location}: ${(res.stderr || res.stdout).trim()}`);\n\t\telse {\n\t\t\trecordFetch(agentDir, record.location);\n\t\t\trefreshed.push(record.location);\n\t\t}\n\t}\n\treturn { refreshed, errors };\n}\n\n/**\n * All marketplace records in effect: the bundled default first (curated,\n * trusted), then user-added ones from `.agents/` (falling back to legacy\n * `.hoocode/`). Deduplicated by directory.\n */\nexport function readMarketplaceRecords(cwd: string, agentDir: string = getAgentDir()): MarketplaceRecord[] {\n\tconst records: MarketplaceRecord[] = [];\n\tconst def = defaultMarketplaceRecord();\n\tif (existsSync(def.dir)) records.push(def);\n\n\t// Well-known marketplaces participate once their index is cached locally\n\t// (see ensureWellKnownMarketplaces; SearchPlugins fetches lazily).\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tif (existsSync(dir) && !records.some((x) => x.dir === dir)) {\n\t\t\trecords.push({ location: wk.url, dir });\n\t\t}\n\t}\n\n\t// The registry lives in the agent dir. Project-local ones are a migration\n\t// read-path: merged in, never written back, so an older setup keeps working.\n\tconst user = [\n\t\t...readMarketplaceStore(marketplaceStorePath(agentDir)),\n\t\t...legacyStorePaths(cwd).flatMap((p) => readMarketplaceStore(p)),\n\t];\n\tfor (const r of user) {\n\t\tif (!records.some((x) => x.dir === r.dir)) records.push(r);\n\t}\n\treturn records;\n}\n\n/** A plugin offered by some registered marketplace (not necessarily installed). */\nexport interface AvailablePlugin {\n\tname: string;\n\tdescription?: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\t/** Resolved source kind, for display and gating. */\n\tsourceKind: \"local\" | \"git\" | \"git-subdir\" | \"npm\";\n\tmarketplaceName: string;\n\tmarketplaceRoot: string;\n\t/** Platforms this entry targets (per-entry hint, else the marketplace's). */\n\tsupportPlatform: MarketplacePlatform[];\n}\n\n/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */\nexport function listAvailablePlugins(cwd: string, agentDir: string = getAgentDir()): AvailablePlugin[] {\n\tconst out: AvailablePlugin[] = [];\n\tconst seen = new Set<string>();\n\tfor (const rec of readMarketplaceRecords(cwd, agentDir)) {\n\t\tconst market = parseMarketplaceDir(rec.dir);\n\t\tif (!market) continue;\n\t\tfor (const entry of market.plugins) {\n\t\t\tif (seen.has(entry.name)) continue;\n\t\t\tseen.add(entry.name);\n\t\t\tout.push({\n\t\t\t\tname: entry.name,\n\t\t\t\tdescription: entry.description,\n\t\t\t\tsource: entry.source,\n\t\t\t\tsourceKind: resolvePluginSource(entry.source, market.root).kind,\n\t\t\t\tmarketplaceName: market.name,\n\t\t\t\tmarketplaceRoot: market.root,\n\t\t\t\tsupportPlatform: entry.supportPlatform ?? market.supportPlatform,\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Find a single available plugin by exact name. */\nexport function findAvailablePlugin(\n\tcwd: string,\n\tname: string,\n\tagentDir: string = getAgentDir(),\n): AvailablePlugin | undefined {\n\treturn listAvailablePlugins(cwd, agentDir).find((p) => p.name === name);\n}\n\n/** All currently installed plugins (project + global plugin dirs). */\nexport function listInstalledPlugins(cwd: string, agentDir: string = getAgentDir()): NormalizedPlugin[] {\n\treturn discoverPlugins(defaultPluginDirs(cwd, agentDir));\n}\n\n/** Whether a plugin with `name` (by id) is already installed. */\nexport function isPluginInstalled(cwd: string, name: string, agentDir: string = getAgentDir()): boolean {\n\treturn listInstalledPlugins(cwd, agentDir).some((p) => p.id === name);\n}\n\nfunction execGit(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", args);\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));\n\t\tchild.on(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t});\n}\n\n/**\n * Clone a git repository into `dest`.\n * If `ref` is provided, does a shallow clone of that branch/tag.\n * If only `sha` is provided, does a full clone and checks out the commit.\n * Returns the clone result; on failure `dest` may be partially created.\n */\nasync function cloneGitRepo(\n\turl: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n\tif (ref) {\n\t\treturn execGit([\"clone\", \"--branch\", ref, \"--depth\", \"1\", \"--\", url, dest]);\n\t}\n\tif (sha) {\n\t\tconst cloneRes = await execGit([\"clone\", \"--\", url, dest]);\n\t\tif (cloneRes.code !== 0) return cloneRes;\n\t\treturn execGit([\"-C\", dest, \"checkout\", \"--quiet\", sha]);\n\t}\n\treturn execGit([\"clone\", \"--depth\", \"1\", \"--\", url, dest]);\n}\n\n/**\n * Resolve a git-subdir source: clone the repo, checkout the requested ref/sha,\n * and copy the subdirectory to `dest`. Cleans up the temporary clone on failure.\n */\nasync function installGitSubdir(\n\turl: string,\n\tsubdir: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ ok: true } | { ok: false; message: string }> {\n\tconst tmpDir = mkdtempSync(path.join(os.tmpdir(), \"hoo-plugin-clone-\"));\n\ttry {\n\t\tconst cloneRes = await cloneGitRepo(url, tmpDir, ref, sha);\n\t\tif (cloneRes.code !== 0) {\n\t\t\treturn { ok: false, message: `git clone failed: ${cloneRes.stderr || cloneRes.stdout}`.trim() };\n\t\t}\n\t\tconst src = path.resolve(tmpDir, subdir);\n\t\tif (!existsSync(src)) {\n\t\t\treturn { ok: false, message: `Plugin subdirectory not found in cloned repo: ${subdir}` };\n\t\t}\n\t\tcpSync(src, dest, { recursive: true });\n\t\treturn { ok: true };\n\t} finally {\n\t\trmSync(tmpDir, { recursive: true, force: true });\n\t}\n}\n\nexport interface InstallOutcome {\n\tinstalled: boolean;\n\t/** Install destination directory (when installed). */\n\tdest?: string;\n\t/** Platforms the installed plugin supports (read back from disk). */\n\tsupportPlatform?: MarketplacePlatform[];\n\t/** Human-readable summary suitable for a tool result or notification. */\n\tmessage: string;\n}\n\n/**\n * Install an available plugin by name into the **consumption home** — the global\n * `~/.agents/plugins/`, not the working tree. A plugin is portable and reusable\n * across projects, so installing into the repo would both dirty `git status` and\n * hide the capability from every other checkout.\n *\n * Copies local sources; clones git sources. Transparent + reversible by\n * construction — the plugin lands in a named directory and\n * {@link uninstallPlugin} removes it. Callers activate the result (live\n * activation via AgentSession.activatePlugin, or a reload).\n */\nexport async function installAvailablePlugin(\n\tcwd: string,\n\tname: string,\n\tagentDir: string = getAgentDir(),\n): Promise<InstallOutcome> {\n\tconst found = findAvailablePlugin(cwd, name, agentDir);\n\tif (!found) return { installed: false, message: `Plugin \"${name}\" not found in any registered marketplace.` };\n\n\tconst resolved = resolvePluginSource(found.source, found.marketplaceRoot);\n\tconst home = consumptionPluginsDir(agentDir);\n\tconst dest = path.join(home, sanitizeForDir(name));\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(home, { recursive: true });\n\n\tif (resolved.kind === \"local\") {\n\t\tif (!existsSync(resolved.path)) {\n\t\t\treturn { installed: false, message: `Plugin source path not found: ${resolved.path}` };\n\t\t}\n\t\tcpSync(resolved.path, dest, { recursive: true });\n\t} else if (resolved.kind === \"git\") {\n\t\tconst res = await cloneGitRepo(resolved.url, dest, resolved.ref, resolved.sha);\n\t\tif (res.code !== 0) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: `git clone failed: ${res.stderr || res.stdout}`.trim() };\n\t\t}\n\t} else if (resolved.kind === \"git-subdir\") {\n\t\tconst res = await installGitSubdir(resolved.url, resolved.path, dest, resolved.ref, resolved.sha);\n\t\tif (!res.ok) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: res.message };\n\t\t}\n\t} else {\n\t\treturn { installed: false, message: `npm plugin sources are not supported yet (${resolved.spec}).` };\n\t}\n\n\tlet parsed = parsePluginDir(dest);\n\tif (!hasAnyManifest(dest)) {\n\t\t// Manifest-less plugin dir (some marketplaces index bare capability trees,\n\t\t// e.g. a plugin that is just a `skills/` directory). Such a directory now\n\t\t// *parses* on its own — the vendors make the manifest optional — but the\n\t\t// entry's name and description live only in the marketplace index, and the\n\t\t// derived id would be the sanitized directory name. Synthesize a native\n\t\t// manifest so both survive the install.\n\t\tconst manifestDir = path.join(dest, \".agents-plugin\");\n\t\tmkdirSync(manifestDir, { recursive: true });\n\t\twriteFileSync(\n\t\t\tpath.join(manifestDir, \"plugin.json\"),\n\t\t\t`${JSON.stringify(\n\t\t\t\t{ name: found.name, ...(found.description ? { description: found.description } : {}) },\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t)}\\n`,\n\t\t\t\"utf8\",\n\t\t);\n\t\tparsed = parsePluginDir(dest);\n\t}\n\tif (!parsed) {\n\t\trmSync(dest, { recursive: true, force: true });\n\t\treturn { installed: false, message: `Installed source for \"${name}\" has no recognizable plugin manifest.` };\n\t}\n\treturn {\n\t\tinstalled: true,\n\t\tdest,\n\t\tsupportPlatform: parsed.supportPlatform,\n\t\tmessage:\n\t\t\t`Installed \"${name}\" from marketplace \"${found.marketplaceName}\" ` +\n\t\t\t`(${parsed.supportPlatform.join(\", \")}) to ${dest}. ` +\n\t\t\t`Remove it with UninstallPlugin.`,\n\t};\n}\n\nexport interface UninstallOutcome {\n\tremoved: boolean;\n\tmessage: string;\n}\n\n/**\n * Remove an installed or authored plugin from every location it could occupy:\n * the consumption home, both production homes, and the legacy project-local\n * directories that older versions wrote into.\n */\nexport function uninstallPlugin(cwd: string, name: string, agentDir: string = getAgentDir()): UninstallOutcome {\n\tconst candidates = [\n\t\t...candidatePluginDirs(cwd, name, agentDir),\n\t\tpath.join(cwd, \".hoocode\", \"plugins\", sanitizeForDir(name)),\n\t];\n\tconst present = candidates.filter((p) => existsSync(p));\n\tif (present.length === 0) return { removed: false, message: `Plugin \"${name}\" is not installed.` };\n\tfor (const p of present) rmSync(p, { recursive: true, force: true });\n\treturn { removed: true, message: `Removed \"${name}\".` };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/install.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin install/uninstall/discovery — the shared engine behind both the\n * `/plugin` slash command (human-driven) and the model-facing lifecycle tools\n * (`SearchPlugins`, `InstallPlugin`, ...). Keeping it in one place means the two\n * surfaces can never drift on where plugins live or how sources resolve.\n *\n * Trust model (see docs/plugin-system-spec.md): *adding* a marketplace is the\n * human trust boundary; *installing* from an already-trusted marketplace is the\n * model's discretion (package-manager model), and must stay transparent and\n * reversible. This module performs the mechanical install/remove; the gating\n * (announce, injection carve-out) lives with the callers.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../../../config.js\";\nimport { defaultPluginDirs } from \"../loader.js\";\nimport { hasAnyManifest } from \"./formats/index.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\nimport { discoverPlugins } from \"./index.js\";\nimport {\n\tcandidatePluginDirs,\n\tinstallHomeForScope,\n\tmarketplaceCacheDir,\n\tmarketplaceCacheMetaPath,\n\tmarketplaceCacheRoot,\n\tmarketplaceStorePath,\n\ttype PluginInstallScope,\n\tpluginHomeRoots,\n\tsanitizeForDir,\n} from \"./locations.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { parsePluginDir } from \"./manifest.js\";\nimport {\n\ttype MarketplacePluginSource,\n\ttype MarketplaceRecord,\n\tparseMarketplaceDir,\n\treadMarketplaceStore,\n\tresolvePluginSource,\n} from \"./marketplace.js\";\n\n/**\n * Marketplace registries older versions wrote into the working tree, newest\n * convention first. Still read so a user who added a marketplace before the\n * registry moved to the agent dir does not silently lose it; nothing writes here.\n */\nfunction legacyStorePaths(cwd: string): string[] {\n\treturn [path.join(cwd, \".agents\", \"marketplaces.json\"), path.join(cwd, \".hoocode\", \"marketplaces.json\")];\n}\n\n/** Absolute path to the curated default marketplace bundled with hoocode. */\nfunction defaultMarketplaceDir(): string {\n\treturn fileURLToPath(new URL(\"./default-marketplace/\", import.meta.url));\n}\n\n/** The default marketplace record — always present so source-level trust is meaningful out of the box. */\nexport function defaultMarketplaceRecord(): MarketplaceRecord {\n\treturn { location: \"hoocode-default\", dir: defaultMarketplaceDir() };\n}\n\n/**\n * Curated well-known marketplaces, trusted out of the box. Shipping an entry\n * here is a maintainer-level trust decision (the human half of the \"adding a\n * marketplace is the human trust boundary\" rule) — keep this list short and\n * high-trust. Indices are cloned lazily into the marketplace cache on first\n * search and refreshed on a TTL thereafter. Refreshing the *index* only changes\n * what is discoverable; installed plugins are still never auto-updated, and\n * plugin sources inside the official index are sha-pinned.\n */\nexport const WELL_KNOWN_MARKETPLACES: ReadonlyArray<{ name: string; url: string }> = [\n\t{ name: \"claude-plugins-official\", url: \"https://github.com/anthropics/claude-plugins-official\" },\n\t// The Copilot plugin directory ships both a `.github/plugin/marketplace.json`\n\t// and a `.claude-plugin/marketplace.json`, so it parses with\n\t// supportPlatform [\"claude\", \"github\"] — the default `github` source.\n\t{ name: \"copilot-plugins\", url: \"https://github.com/github/copilot-plugins\" },\n\t// GitHub's community index, `.github/plugin/marketplace.json`. Same owner as\n\t// copilot-plugins, which is what makes it a source-trust decision of the same\n\t// kind. Note that most of its in-repo entries carry their content in Copilot\n\t// UI `extensions/`, a surface hoocode does not model — those install and load\n\t// nothing, and say so.\n\t{ name: \"awesome-copilot\", url: \"https://github.com/github/awesome-copilot\" },\n];\n\n/**\n * How long a cached marketplace index is considered fresh.\n *\n * Refreshing an *index* is safe to automate: it changes what is discoverable,\n * not what is installed. The rule that an installed plugin is never auto-updated\n * from a remote is untouched — that is the supply-chain boundary, and this is a\n * catalog listing.\n */\nconst MARKETPLACE_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction marketplaceTtlMs(): number {\n\tconst raw = Number(process.env.HOOCODE_MARKETPLACE_TTL_MS);\n\treturn Number.isFinite(raw) && raw >= 0 ? raw : MARKETPLACE_TTL_MS;\n}\n\ntype CacheMeta = Record<string, string>;\n\nfunction readCacheMeta(agentDir: string): CacheMeta {\n\tconst parsed = readJsonFile<CacheMeta>(marketplaceCacheMetaPath(agentDir));\n\treturn parsed && typeof parsed === \"object\" ? parsed : {};\n}\n\nfunction recordFetch(agentDir: string, url: string): void {\n\tconst meta = readCacheMeta(agentDir);\n\tmeta[url] = new Date().toISOString();\n\tconst file = marketplaceCacheMetaPath(agentDir);\n\tmkdirSync(path.dirname(file), { recursive: true });\n\twriteFileSync(file, `${JSON.stringify(meta, null, 2)}\\n`, \"utf8\");\n}\n\nfunction isStale(agentDir: string, url: string): boolean {\n\tconst ttl = marketplaceTtlMs();\n\tif (ttl === 0) return true;\n\tconst at = readCacheMeta(agentDir)[url];\n\tif (!at) return true;\n\tconst age = Date.now() - Date.parse(at);\n\treturn !Number.isFinite(age) || age > ttl;\n}\n\nfunction readJsonFile<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Bring an existing clone up to date, falling back to a fresh clone when the pull\n * cannot fast-forward (a force-pushed remote, or a corrupt cache).\n *\n * The re-clone goes to a temp directory and is swapped in only on success. An\n * earlier version deleted the cache first, which meant a refresh attempted while\n * offline destroyed a perfectly good index — the opposite of the \"offline is\n * non-fatal\" property the caller depends on.\n */\nasync function refreshClone(url: string, dir: string): Promise<{ code: number; stdout: string; stderr: string }> {\n\tconst pulled = await execGit([\"-C\", dir, \"pull\", \"--ff-only\"]);\n\tif (pulled.code === 0) return pulled;\n\n\tconst staging = mkdtempSync(path.join(os.tmpdir(), \"hoo-market-refresh-\"));\n\tconst target = path.join(staging, \"clone\");\n\tconst cloned = await cloneGitRepo(url, target);\n\tif (cloned.code !== 0) {\n\t\trmSync(staging, { recursive: true, force: true });\n\t\treturn cloned;\n\t}\n\trmSync(dir, { recursive: true, force: true });\n\tmkdirSync(path.dirname(dir), { recursive: true });\n\ttry {\n\t\trenameSync(target, dir);\n\t} catch {\n\t\tcpSync(target, dir, { recursive: true });\n\t}\n\trmSync(staging, { recursive: true, force: true });\n\treturn cloned;\n}\n\nexport interface EnsureMarketplacesOptions {\n\t/** Refresh regardless of the TTL. */\n\tforce?: boolean;\n}\n\n/**\n * Make sure every well-known marketplace index is cached and reasonably fresh.\n *\n * Clones what is missing and pulls what has gone stale. Without the TTL the\n * indices were fetched exactly once and never again, so a plugin added upstream\n * stayed permanently invisible — the inherit half of the system quietly stopped\n * inheriting. Offline is non-fatal: each failure is returned and search degrades\n * to whatever is already on disk.\n */\nexport async function ensureWellKnownMarketplaces(\n\tagentDir: string = getAgentDir(),\n\toptions: EnsureMarketplacesOptions = {},\n): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tconst present = existsSync(dir);\n\t\tif (present && !options.force && !isStale(agentDir, wk.url)) continue;\n\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tconst res = present ? await refreshClone(wk.url, dir) : await cloneGitRepo(wk.url, dir);\n\t\tif (res.code !== 0) {\n\t\t\t// A failed *refresh* keeps the stale copy: an out-of-date index is far\n\t\t\t// more useful than none, and the network may simply be down.\n\t\t\tif (!present) rmSync(dir, { recursive: true, force: true });\n\t\t\terrors.push(`${wk.name}: ${(res.stderr || res.stdout).trim()}`);\n\t\t\tcontinue;\n\t\t}\n\t\trecordFetch(agentDir, wk.url);\n\t}\n\treturn errors;\n}\n\n/**\n * Refresh every cached index — well-known and user-added — ignoring the TTL.\n * The explicit half of the freshness story, for when a user knows the upstream\n * changed and does not want to wait out the interval.\n */\nexport async function refreshMarketplaces(\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<{ refreshed: string[]; errors: string[] }> {\n\tconst errors = await ensureWellKnownMarketplaces(agentDir, { force: true });\n\tconst refreshed = WELL_KNOWN_MARKETPLACES.filter((wk) => existsSync(marketplaceCacheDir(wk.url, agentDir))).map(\n\t\t(wk) => wk.name,\n\t);\n\n\t// User-added marketplaces are cached the same way when they came from git; a\n\t// local-path marketplace is read in place and has nothing to refresh.\n\tfor (const record of readMarketplaceRecords(cwd, agentDir)) {\n\t\tconst isCachedClone = record.dir.startsWith(marketplaceCacheRoot(agentDir));\n\t\tif (!isCachedClone || WELL_KNOWN_MARKETPLACES.some((wk) => wk.url === record.location)) continue;\n\t\tconst res = await refreshClone(record.location, record.dir);\n\t\tif (res.code !== 0) errors.push(`${record.location}: ${(res.stderr || res.stdout).trim()}`);\n\t\telse {\n\t\t\trecordFetch(agentDir, record.location);\n\t\t\trefreshed.push(record.location);\n\t\t}\n\t}\n\treturn { refreshed, errors };\n}\n\n/**\n * All marketplace records in effect: the bundled default first (curated,\n * trusted), then user-added ones from `.agents/` (falling back to legacy\n * `.hoocode/`). Deduplicated by directory.\n */\nexport function readMarketplaceRecords(cwd: string, agentDir: string = getAgentDir()): MarketplaceRecord[] {\n\tconst records: MarketplaceRecord[] = [];\n\tconst def = defaultMarketplaceRecord();\n\tif (existsSync(def.dir)) records.push(def);\n\n\t// Well-known marketplaces participate once their index is cached locally\n\t// (see ensureWellKnownMarketplaces; SearchPlugins fetches lazily).\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tif (existsSync(dir) && !records.some((x) => x.dir === dir)) {\n\t\t\trecords.push({ location: wk.url, dir });\n\t\t}\n\t}\n\n\t// The registry lives in the agent dir. Project-local ones are a migration\n\t// read-path: merged in, never written back, so an older setup keeps working.\n\tconst user = [\n\t\t...readMarketplaceStore(marketplaceStorePath(agentDir)),\n\t\t...legacyStorePaths(cwd).flatMap((p) => readMarketplaceStore(p)),\n\t];\n\tfor (const r of user) {\n\t\tif (!records.some((x) => x.dir === r.dir)) records.push(r);\n\t}\n\treturn records;\n}\n\n/** A plugin offered by some registered marketplace (not necessarily installed). */\nexport interface AvailablePlugin {\n\tname: string;\n\tdescription?: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\t/** Resolved source kind, for display and gating. `npm`/`archive` are listed but not installable. */\n\tsourceKind: \"local\" | \"git\" | \"git-subdir\" | \"npm\" | \"archive\";\n\tmarketplaceName: string;\n\tmarketplaceRoot: string;\n\t/** Platforms this entry targets (per-entry hint, else the marketplace's). */\n\tsupportPlatform: MarketplacePlatform[];\n}\n\n/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */\nexport function listAvailablePlugins(cwd: string, agentDir: string = getAgentDir()): AvailablePlugin[] {\n\tconst out: AvailablePlugin[] = [];\n\tconst seen = new Set<string>();\n\tfor (const rec of readMarketplaceRecords(cwd, agentDir)) {\n\t\tconst market = parseMarketplaceDir(rec.dir);\n\t\tif (!market) continue;\n\t\tfor (const entry of market.plugins) {\n\t\t\tif (seen.has(entry.name)) continue;\n\t\t\tseen.add(entry.name);\n\t\t\tout.push({\n\t\t\t\tname: entry.name,\n\t\t\t\tdescription: entry.description,\n\t\t\t\tsource: entry.source,\n\t\t\t\tsourceKind: resolvePluginSource(entry.source, market.root).kind,\n\t\t\t\tmarketplaceName: market.name,\n\t\t\t\tmarketplaceRoot: market.root,\n\t\t\t\tsupportPlatform: entry.supportPlatform ?? market.supportPlatform,\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Find a single available plugin by exact name. */\nexport function findAvailablePlugin(\n\tcwd: string,\n\tname: string,\n\tagentDir: string = getAgentDir(),\n): AvailablePlugin | undefined {\n\treturn listAvailablePlugins(cwd, agentDir).find((p) => p.name === name);\n}\n\n/** All currently installed plugins (project + global plugin dirs). */\nexport function listInstalledPlugins(cwd: string, agentDir: string = getAgentDir()): NormalizedPlugin[] {\n\treturn discoverPlugins(defaultPluginDirs(cwd, agentDir));\n}\n\n/**\n * Whether `name` is already installed, by either name it can go by.\n *\n * A marketplace entry name and the plugin's own manifest `name` are allowed to\n * differ — both vendors say so outright — and installs land in a directory named\n * for the *entry*, while discovery reports the *manifest* id. Matching ids alone\n * meant a plugin installed as `42crunch-api-security-testing` (manifest id\n * `api-security-testing`) never counted as installed, so every InstallPlugin\n * call for the catalog name re-cloned it.\n */\nexport function isPluginInstalled(cwd: string, name: string, agentDir: string = getAgentDir()): boolean {\n\tif (listInstalledPlugins(cwd, agentDir).some((p) => p.id === name)) return true;\n\treturn candidatePluginDirs(cwd, name, agentDir).some((p) => existsSync(p));\n}\n\nfunction execGit(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", args);\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));\n\t\tchild.on(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t});\n}\n\n/**\n * Materialize exactly `sha` at `dest`, without cloning the repository's history.\n *\n * `git fetch --depth 1 <sha>` is what makes honoring a pin affordable: a pinned\n * commit is usually not the tip of any branch, so the alternative is a full\n * clone — 55s on a large plugin repo against ~1s here, on sources where two\n * thirds of the official Claude marketplace is pinned. Servers that refuse a\n * by-sha fetch (AWS CodeCommit and other non-GitHub hosts) fall back to a clone\n * deep enough to reach the commit, then check it out.\n */\nasync function fetchGitCommit(\n\turl: string,\n\tdest: string,\n\tsha: string,\n\tref?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n\tmkdirSync(dest, { recursive: true });\n\tconst init = await execGit([\"init\", \"--quiet\", dest]);\n\tif (init.code === 0) {\n\t\tconst remote = await execGit([\"-C\", dest, \"remote\", \"add\", \"origin\", url]);\n\t\tif (remote.code === 0) {\n\t\t\tconst fetched = await execGit([\"-C\", dest, \"fetch\", \"--quiet\", \"--depth\", \"1\", \"origin\", sha]);\n\t\t\tif (fetched.code === 0) {\n\t\t\t\tconst checkout = await execGit([\"-C\", dest, \"checkout\", \"--quiet\", \"FETCH_HEAD\"]);\n\t\t\t\tif (checkout.code === 0) return checkout;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback: a full clone (shallow cannot reach an arbitrary commit), then the\n\t// pin. `ref` narrows it when the entry named one.\n\trmSync(dest, { recursive: true, force: true });\n\tconst cloned = ref\n\t\t? await execGit([\"clone\", \"--branch\", ref, \"--\", url, dest])\n\t\t: await execGit([\"clone\", \"--\", url, dest]);\n\tif (cloned.code !== 0) return cloned;\n\treturn execGit([\"-C\", dest, \"checkout\", \"--quiet\", sha]);\n}\n\n/**\n * Clone a git repository into `dest` at the requested pin.\n *\n * **`sha` outranks `ref`.** Both vendors define it that way (\"when both `ref`\n * and `sha` are set, the `sha` is the effective pin\"), and a marketplace that\n * ships both is stating the exact commit it vouches for — a tag can be moved or\n * re-pointed after the catalog pinned it, so preferring `ref` installed a\n * different commit than the index promised. It is not hypothetical: the official\n * catalog's `42crunch-api-security-testing` pins `30287f5` while its `v1.5.5`\n * tag now resolves to `faf5305`.\n *\n * Returns the git result; on failure `dest` may be partially created.\n */\nasync function cloneGitRepo(\n\turl: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n\tif (sha) return fetchGitCommit(url, dest, sha, ref);\n\tif (ref) return execGit([\"clone\", \"--branch\", ref, \"--depth\", \"1\", \"--\", url, dest]);\n\treturn execGit([\"clone\", \"--depth\", \"1\", \"--\", url, dest]);\n}\n\n/**\n * Resolve a git-subdir source: clone the repo, checkout the requested ref/sha,\n * and copy the subdirectory to `dest`. Cleans up the temporary clone on failure.\n */\nasync function installGitSubdir(\n\turl: string,\n\tsubdir: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ ok: true } | { ok: false; message: string }> {\n\tconst tmpDir = mkdtempSync(path.join(os.tmpdir(), \"hoo-plugin-clone-\"));\n\ttry {\n\t\tconst cloneRes = await cloneGitRepo(url, tmpDir, ref, sha);\n\t\tif (cloneRes.code !== 0) {\n\t\t\treturn { ok: false, message: `git clone failed: ${cloneRes.stderr || cloneRes.stdout}`.trim() };\n\t\t}\n\t\tconst src = path.resolve(tmpDir, subdir);\n\t\tif (!existsSync(src)) {\n\t\t\treturn { ok: false, message: `Plugin subdirectory not found in cloned repo: ${subdir}` };\n\t\t}\n\t\tcpSync(src, dest, { recursive: true });\n\t\treturn { ok: true };\n\t} finally {\n\t\trmSync(tmpDir, { recursive: true, force: true });\n\t}\n}\n\nexport interface InstallOptions {\n\t/**\n\t * Where the plugin lands. Defaults to `user`. The human path (`/plugin\n\t * install`) asks; the autonomous path passes the `pluginInstallScope` setting.\n\t */\n\tscope?: PluginInstallScope;\n}\n\nexport interface InstallOutcome {\n\tinstalled: boolean;\n\t/** Install destination directory (when installed). */\n\tdest?: string;\n\t/** The scope the plugin was installed at. */\n\tscope?: PluginInstallScope;\n\t/**\n\t * The plugin's own manifest id, when it differs from the marketplace entry\n\t * name it was installed by. This is the id `ListPlugins` reports, so the\n\t * caller can name both rather than leaving the model to reconcile them.\n\t */\n\tid?: string;\n\t/** Platforms the installed plugin supports (read back from disk). */\n\tsupportPlatform?: MarketplacePlatform[];\n\t/** Human-readable summary suitable for a tool result or notification. */\n\tmessage: string;\n}\n\n/** Capability surfaces hoocode can actually load out of an installed plugin. */\nfunction loadableCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [\n\t\tplugin.skillsDir && \"skills\",\n\t\tplugin.commandsDir && \"commands\",\n\t\tplugin.agentsDir && \"subagents\",\n\t\tplugin.themesDir && \"themes\",\n\t\tplugin.hooks && \"hooks\",\n\t\tplugin.mcpServers && Object.keys(plugin.mcpServers).length > 0 && \"MCP servers\",\n\t\tplugin.providers?.length && \"providers\",\n\t].filter((c): c is string => typeof c === \"string\");\n}\n\n/**\n * Install an available plugin by name into the consumption home for\n * `options.scope` — `~/.agents/plugins/` for `user` (the default), or\n * `<cwd>/.agents/plugins/` for `project`.\n *\n * `user` is the default for a reason worth keeping in view: a plugin is portable\n * and reusable across projects, so installing into the repo hides the capability\n * from every other checkout and puts content into `git status` that has nothing\n * to do with the change being made. `project` inverts both of those on purpose —\n * it is how a team pins a plugin to a repository — so it is chosen, never\n * defaulted into.\n *\n * Copies local sources; clones git sources. Transparent + reversible by\n * construction — the plugin lands in a named directory and\n * {@link uninstallPlugin} removes it from either scope. Callers activate the\n * result (live activation via AgentSession.activatePlugin, or a reload).\n */\nexport async function installAvailablePlugin(\n\tcwd: string,\n\tname: string,\n\tagentDir: string = getAgentDir(),\n\toptions: InstallOptions = {},\n): Promise<InstallOutcome> {\n\tconst found = findAvailablePlugin(cwd, name, agentDir);\n\tif (!found) return { installed: false, message: `Plugin \"${name}\" not found in any registered marketplace.` };\n\n\tconst scope = options.scope ?? \"user\";\n\tconst resolved = resolvePluginSource(found.source, found.marketplaceRoot);\n\tconst home = installHomeForScope(scope, cwd, agentDir);\n\tconst dest = path.join(home, sanitizeForDir(name));\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(home, { recursive: true });\n\n\tif (resolved.kind === \"local\") {\n\t\tif (!existsSync(resolved.path)) {\n\t\t\treturn { installed: false, message: `Plugin source path not found: ${resolved.path}` };\n\t\t}\n\t\tcpSync(resolved.path, dest, { recursive: true });\n\t} else if (resolved.kind === \"git\") {\n\t\tconst res = await cloneGitRepo(resolved.url, dest, resolved.ref, resolved.sha);\n\t\tif (res.code !== 0) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: `git clone failed: ${res.stderr || res.stdout}`.trim() };\n\t\t}\n\t} else if (resolved.kind === \"git-subdir\") {\n\t\tconst res = await installGitSubdir(resolved.url, resolved.path, dest, resolved.ref, resolved.sha);\n\t\tif (!res.ok) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: res.message };\n\t\t}\n\t} else if (resolved.kind === \"npm\") {\n\t\treturn {\n\t\t\tinstalled: false,\n\t\t\tmessage:\n\t\t\t\t`\"${name}\" is published as an npm package (${resolved.spec}), a source type hoocode cannot install yet. ` +\n\t\t\t\t\"The entry is listed so you know it exists; install it through the vendor CLI, or ask the marketplace \" +\n\t\t\t\t\"for a git source.\",\n\t\t};\n\t} else {\n\t\treturn {\n\t\t\tinstalled: false,\n\t\t\tmessage:\n\t\t\t\t`\"${name}\" is published as a zip archive (${resolved.url}), a source type hoocode cannot install yet. ` +\n\t\t\t\t\"The entry is listed so you know it exists; install it through the vendor CLI, or ask the marketplace \" +\n\t\t\t\t\"for a git source.\",\n\t\t};\n\t}\n\n\t// Drop the clone's `.git`. Nothing reads it — an installed plugin is never\n\t// updated from its remote, by design (that is the supply-chain boundary) — and\n\t// keeping it actively breaks project scope: a repository nested inside the\n\t// user's repository is an embedded repo, so `git add` writes a gitlink instead\n\t// of the plugin's files and the install cannot actually be committed. Which is\n\t// the one thing project scope is for. Cheaper installs at user scope too.\n\trmSync(path.join(dest, \".git\"), { recursive: true, force: true });\n\n\tlet parsed = parsePluginDir(dest);\n\tif (!hasAnyManifest(dest)) {\n\t\t// Manifest-less plugin dir (some marketplaces index bare capability trees,\n\t\t// e.g. a plugin that is just a `skills/` directory). Such a directory now\n\t\t// *parses* on its own — the vendors make the manifest optional — but the\n\t\t// entry's name and description live only in the marketplace index, and the\n\t\t// derived id would be the sanitized directory name. Synthesize a native\n\t\t// manifest so both survive the install.\n\t\tconst manifestDir = path.join(dest, \".agents-plugin\");\n\t\tmkdirSync(manifestDir, { recursive: true });\n\t\twriteFileSync(\n\t\t\tpath.join(manifestDir, \"plugin.json\"),\n\t\t\t`${JSON.stringify(\n\t\t\t\t{ name: found.name, ...(found.description ? { description: found.description } : {}) },\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t)}\\n`,\n\t\t\t\"utf8\",\n\t\t);\n\t\tparsed = parsePluginDir(dest);\n\t}\n\tif (!parsed) {\n\t\trmSync(dest, { recursive: true, force: true });\n\t\treturn { installed: false, message: `Installed source for \"${name}\" has no recognizable plugin manifest.` };\n\t}\n\tconst capabilities = loadableCapabilities(parsed);\n\tlet message =\n\t\t`Installed \"${name}\" from marketplace \"${found.marketplaceName}\" ` +\n\t\t`(${parsed.supportPlatform.join(\", \")}) to ${dest} [${scope} scope]. ` +\n\t\t`Remove it with UninstallPlugin.`;\n\tif (scope === \"project\") {\n\t\t// Project scope puts the plugin in the working tree, which is the point —\n\t\t// but it is also a `git status` entry and, once committed, code that runs\n\t\t// for whoever clones next. Executable capabilities are the half that\n\t\t// matters: hoocode has no workspace-trust gate for `<cwd>/.agents/plugins`\n\t\t// (see docs/plugin-system-architecture.md §5.9), so a committed hook or MCP\n\t\t// server loads for collaborators without a prompt.\n\t\tconst executables = [parsed.hooks && \"hooks\", parsed.mcpServers && \"MCP servers\"].filter(Boolean);\n\t\tmessage += ` It is now part of the working tree — commit it to share it with collaborators.`;\n\t\tif (executables.length > 0) {\n\t\t\tmessage +=\n\t\t\t\t` It carries ${executables.join(\" and \")}, which run for anyone who clones the repository;` +\n\t\t\t\t` keep it out of version control if that is not what you want.`;\n\t\t}\n\t}\n\tif (parsed.id !== name) {\n\t\t// The entry name and the manifest name are allowed to differ, and the\n\t\t// difference is otherwise invisible until ListPlugins reports an id the\n\t\t// caller never asked for.\n\t\tmessage += ` Listed by ListPlugins as \"${parsed.id}\"; either name uninstalls it.`;\n\t}\n\tif (capabilities.length === 0) {\n\t\t// An install that adds nothing should not read as an install that worked.\n\t\t// Common in github/awesome-copilot, where most entries carry their content\n\t\t// in Copilot UI `extensions/` that hoocode has no analog for.\n\t\tconst surfaces = parsed.unsupportedSurfaces?.length\n\t\t\t? ` Unsupported surfaces present: ${parsed.unsupportedSurfaces.join(\", \")}.`\n\t\t\t: \"\";\n\t\tmessage +=\n\t\t\t\" Note: it contributes no capabilities hoocode can load\" +\n\t\t\t` (no skills, commands, subagents, hooks or MCP servers).${surfaces}`;\n\t}\n\treturn {\n\t\tinstalled: true,\n\t\tdest,\n\t\tscope,\n\t\t...(parsed.id !== name ? { id: parsed.id } : {}),\n\t\tsupportPlatform: parsed.supportPlatform,\n\t\tmessage,\n\t};\n}\n\nexport interface UninstallOutcome {\n\tremoved: boolean;\n\tmessage: string;\n}\n\n/** True when `target` is `root` or sits inside it. */\nfunction isUnderDir(target: string, root: string): boolean {\n\tconst normalized = path.resolve(root);\n\tif (path.resolve(target) === normalized) return true;\n\treturn path.resolve(target).startsWith(normalized.endsWith(path.sep) ? normalized : `${normalized}${path.sep}`);\n}\n\n/**\n * Remove an installed or authored plugin from every location it could occupy:\n * the consumption home, both production homes, and the legacy project-local\n * directories that older versions wrote into.\n *\n * `name` is matched two ways, because a plugin has two names: the marketplace\n * entry name it was installed under (which is what the directory is called) and\n * the manifest id `ListPlugins` reports. Resolving only the first left a plugin\n * whose names differ impossible to remove through the tool surface — the id the\n * model was shown was the one uninstall did not accept.\n *\n * Removal by id stays inside {@link pluginHomeRoots}, so a plugin discovered in\n * `<cwd>/.claude/skills` — repository content the team committed, which hoocode\n * did not install — is never deleted out of the working tree.\n */\nexport function uninstallPlugin(cwd: string, name: string, agentDir: string = getAgentDir()): UninstallOutcome {\n\tconst homes = pluginHomeRoots(cwd, agentDir);\n\tconst byManifestId = listInstalledPlugins(cwd, agentDir)\n\t\t.filter((p) => p.id === name)\n\t\t.map((p) => p.root)\n\t\t.filter((root) => homes.some((home) => isUnderDir(root, home)));\n\tconst candidates = [...new Set([...candidatePluginDirs(cwd, name, agentDir), ...byManifestId])];\n\tconst present = candidates.filter((p) => existsSync(p));\n\tif (present.length === 0) return { removed: false, message: `Plugin \"${name}\" is not installed.` };\n\tfor (const p of present) rmSync(p, { recursive: true, force: true });\n\treturn { removed: true, message: `Removed \"${name}\".` };\n}\n"]}
|
|
@@ -19,7 +19,7 @@ import { getAgentDir } from "../../../config.js";
|
|
|
19
19
|
import { defaultPluginDirs } from "../loader.js";
|
|
20
20
|
import { hasAnyManifest } from "./formats/index.js";
|
|
21
21
|
import { discoverPlugins } from "./index.js";
|
|
22
|
-
import { candidatePluginDirs,
|
|
22
|
+
import { candidatePluginDirs, installHomeForScope, marketplaceCacheDir, marketplaceCacheMetaPath, marketplaceCacheRoot, marketplaceStorePath, pluginHomeRoots, sanitizeForDir, } from "./locations.js";
|
|
23
23
|
import { parsePluginDir } from "./manifest.js";
|
|
24
24
|
import { parseMarketplaceDir, readMarketplaceStore, resolvePluginSource, } from "./marketplace.js";
|
|
25
25
|
/**
|
|
@@ -53,6 +53,12 @@ export const WELL_KNOWN_MARKETPLACES = [
|
|
|
53
53
|
// and a `.claude-plugin/marketplace.json`, so it parses with
|
|
54
54
|
// supportPlatform ["claude", "github"] — the default `github` source.
|
|
55
55
|
{ name: "copilot-plugins", url: "https://github.com/github/copilot-plugins" },
|
|
56
|
+
// GitHub's community index, `.github/plugin/marketplace.json`. Same owner as
|
|
57
|
+
// copilot-plugins, which is what makes it a source-trust decision of the same
|
|
58
|
+
// kind. Note that most of its in-repo entries carry their content in Copilot
|
|
59
|
+
// UI `extensions/`, a surface hoocode does not model — those install and load
|
|
60
|
+
// nothing, and say so.
|
|
61
|
+
{ name: "awesome-copilot", url: "https://github.com/github/awesome-copilot" },
|
|
56
62
|
];
|
|
57
63
|
/**
|
|
58
64
|
* How long a cached marketplace index is considered fresh.
|
|
@@ -244,9 +250,20 @@ export function findAvailablePlugin(cwd, name, agentDir = getAgentDir()) {
|
|
|
244
250
|
export function listInstalledPlugins(cwd, agentDir = getAgentDir()) {
|
|
245
251
|
return discoverPlugins(defaultPluginDirs(cwd, agentDir));
|
|
246
252
|
}
|
|
247
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* Whether `name` is already installed, by either name it can go by.
|
|
255
|
+
*
|
|
256
|
+
* A marketplace entry name and the plugin's own manifest `name` are allowed to
|
|
257
|
+
* differ — both vendors say so outright — and installs land in a directory named
|
|
258
|
+
* for the *entry*, while discovery reports the *manifest* id. Matching ids alone
|
|
259
|
+
* meant a plugin installed as `42crunch-api-security-testing` (manifest id
|
|
260
|
+
* `api-security-testing`) never counted as installed, so every InstallPlugin
|
|
261
|
+
* call for the catalog name re-cloned it.
|
|
262
|
+
*/
|
|
248
263
|
export function isPluginInstalled(cwd, name, agentDir = getAgentDir()) {
|
|
249
|
-
|
|
264
|
+
if (listInstalledPlugins(cwd, agentDir).some((p) => p.id === name))
|
|
265
|
+
return true;
|
|
266
|
+
return candidatePluginDirs(cwd, name, agentDir).some((p) => existsSync(p));
|
|
250
267
|
}
|
|
251
268
|
function execGit(args) {
|
|
252
269
|
return new Promise((resolve) => {
|
|
@@ -264,21 +281,57 @@ function execGit(args) {
|
|
|
264
281
|
});
|
|
265
282
|
}
|
|
266
283
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
284
|
+
* Materialize exactly `sha` at `dest`, without cloning the repository's history.
|
|
285
|
+
*
|
|
286
|
+
* `git fetch --depth 1 <sha>` is what makes honoring a pin affordable: a pinned
|
|
287
|
+
* commit is usually not the tip of any branch, so the alternative is a full
|
|
288
|
+
* clone — 55s on a large plugin repo against ~1s here, on sources where two
|
|
289
|
+
* thirds of the official Claude marketplace is pinned. Servers that refuse a
|
|
290
|
+
* by-sha fetch (AWS CodeCommit and other non-GitHub hosts) fall back to a clone
|
|
291
|
+
* deep enough to reach the commit, then check it out.
|
|
292
|
+
*/
|
|
293
|
+
async function fetchGitCommit(url, dest, sha, ref) {
|
|
294
|
+
mkdirSync(dest, { recursive: true });
|
|
295
|
+
const init = await execGit(["init", "--quiet", dest]);
|
|
296
|
+
if (init.code === 0) {
|
|
297
|
+
const remote = await execGit(["-C", dest, "remote", "add", "origin", url]);
|
|
298
|
+
if (remote.code === 0) {
|
|
299
|
+
const fetched = await execGit(["-C", dest, "fetch", "--quiet", "--depth", "1", "origin", sha]);
|
|
300
|
+
if (fetched.code === 0) {
|
|
301
|
+
const checkout = await execGit(["-C", dest, "checkout", "--quiet", "FETCH_HEAD"]);
|
|
302
|
+
if (checkout.code === 0)
|
|
303
|
+
return checkout;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Fallback: a full clone (shallow cannot reach an arbitrary commit), then the
|
|
308
|
+
// pin. `ref` narrows it when the entry named one.
|
|
309
|
+
rmSync(dest, { recursive: true, force: true });
|
|
310
|
+
const cloned = ref
|
|
311
|
+
? await execGit(["clone", "--branch", ref, "--", url, dest])
|
|
312
|
+
: await execGit(["clone", "--", url, dest]);
|
|
313
|
+
if (cloned.code !== 0)
|
|
314
|
+
return cloned;
|
|
315
|
+
return execGit(["-C", dest, "checkout", "--quiet", sha]);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Clone a git repository into `dest` at the requested pin.
|
|
319
|
+
*
|
|
320
|
+
* **`sha` outranks `ref`.** Both vendors define it that way ("when both `ref`
|
|
321
|
+
* and `sha` are set, the `sha` is the effective pin"), and a marketplace that
|
|
322
|
+
* ships both is stating the exact commit it vouches for — a tag can be moved or
|
|
323
|
+
* re-pointed after the catalog pinned it, so preferring `ref` installed a
|
|
324
|
+
* different commit than the index promised. It is not hypothetical: the official
|
|
325
|
+
* catalog's `42crunch-api-security-testing` pins `30287f5` while its `v1.5.5`
|
|
326
|
+
* tag now resolves to `faf5305`.
|
|
327
|
+
*
|
|
328
|
+
* Returns the git result; on failure `dest` may be partially created.
|
|
271
329
|
*/
|
|
272
330
|
async function cloneGitRepo(url, dest, ref, sha) {
|
|
273
|
-
if (
|
|
331
|
+
if (sha)
|
|
332
|
+
return fetchGitCommit(url, dest, sha, ref);
|
|
333
|
+
if (ref)
|
|
274
334
|
return execGit(["clone", "--branch", ref, "--depth", "1", "--", url, dest]);
|
|
275
|
-
}
|
|
276
|
-
if (sha) {
|
|
277
|
-
const cloneRes = await execGit(["clone", "--", url, dest]);
|
|
278
|
-
if (cloneRes.code !== 0)
|
|
279
|
-
return cloneRes;
|
|
280
|
-
return execGit(["-C", dest, "checkout", "--quiet", sha]);
|
|
281
|
-
}
|
|
282
335
|
return execGit(["clone", "--depth", "1", "--", url, dest]);
|
|
283
336
|
}
|
|
284
337
|
/**
|
|
@@ -303,23 +356,42 @@ async function installGitSubdir(url, subdir, dest, ref, sha) {
|
|
|
303
356
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
304
357
|
}
|
|
305
358
|
}
|
|
359
|
+
/** Capability surfaces hoocode can actually load out of an installed plugin. */
|
|
360
|
+
function loadableCapabilities(plugin) {
|
|
361
|
+
return [
|
|
362
|
+
plugin.skillsDir && "skills",
|
|
363
|
+
plugin.commandsDir && "commands",
|
|
364
|
+
plugin.agentsDir && "subagents",
|
|
365
|
+
plugin.themesDir && "themes",
|
|
366
|
+
plugin.hooks && "hooks",
|
|
367
|
+
plugin.mcpServers && Object.keys(plugin.mcpServers).length > 0 && "MCP servers",
|
|
368
|
+
plugin.providers?.length && "providers",
|
|
369
|
+
].filter((c) => typeof c === "string");
|
|
370
|
+
}
|
|
306
371
|
/**
|
|
307
|
-
* Install an available plugin by name into the
|
|
308
|
-
* `~/.agents/plugins
|
|
309
|
-
*
|
|
310
|
-
*
|
|
372
|
+
* Install an available plugin by name into the consumption home for
|
|
373
|
+
* `options.scope` — `~/.agents/plugins/` for `user` (the default), or
|
|
374
|
+
* `<cwd>/.agents/plugins/` for `project`.
|
|
375
|
+
*
|
|
376
|
+
* `user` is the default for a reason worth keeping in view: a plugin is portable
|
|
377
|
+
* and reusable across projects, so installing into the repo hides the capability
|
|
378
|
+
* from every other checkout and puts content into `git status` that has nothing
|
|
379
|
+
* to do with the change being made. `project` inverts both of those on purpose —
|
|
380
|
+
* it is how a team pins a plugin to a repository — so it is chosen, never
|
|
381
|
+
* defaulted into.
|
|
311
382
|
*
|
|
312
383
|
* Copies local sources; clones git sources. Transparent + reversible by
|
|
313
384
|
* construction — the plugin lands in a named directory and
|
|
314
|
-
* {@link uninstallPlugin} removes it. Callers activate the
|
|
315
|
-
* activation via AgentSession.activatePlugin, or a reload).
|
|
385
|
+
* {@link uninstallPlugin} removes it from either scope. Callers activate the
|
|
386
|
+
* result (live activation via AgentSession.activatePlugin, or a reload).
|
|
316
387
|
*/
|
|
317
|
-
export async function installAvailablePlugin(cwd, name, agentDir = getAgentDir()) {
|
|
388
|
+
export async function installAvailablePlugin(cwd, name, agentDir = getAgentDir(), options = {}) {
|
|
318
389
|
const found = findAvailablePlugin(cwd, name, agentDir);
|
|
319
390
|
if (!found)
|
|
320
391
|
return { installed: false, message: `Plugin "${name}" not found in any registered marketplace.` };
|
|
392
|
+
const scope = options.scope ?? "user";
|
|
321
393
|
const resolved = resolvePluginSource(found.source, found.marketplaceRoot);
|
|
322
|
-
const home =
|
|
394
|
+
const home = installHomeForScope(scope, cwd, agentDir);
|
|
323
395
|
const dest = path.join(home, sanitizeForDir(name));
|
|
324
396
|
rmSync(dest, { recursive: true, force: true });
|
|
325
397
|
mkdirSync(home, { recursive: true });
|
|
@@ -343,9 +415,29 @@ export async function installAvailablePlugin(cwd, name, agentDir = getAgentDir()
|
|
|
343
415
|
return { installed: false, message: res.message };
|
|
344
416
|
}
|
|
345
417
|
}
|
|
418
|
+
else if (resolved.kind === "npm") {
|
|
419
|
+
return {
|
|
420
|
+
installed: false,
|
|
421
|
+
message: `"${name}" is published as an npm package (${resolved.spec}), a source type hoocode cannot install yet. ` +
|
|
422
|
+
"The entry is listed so you know it exists; install it through the vendor CLI, or ask the marketplace " +
|
|
423
|
+
"for a git source.",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
346
426
|
else {
|
|
347
|
-
return {
|
|
427
|
+
return {
|
|
428
|
+
installed: false,
|
|
429
|
+
message: `"${name}" is published as a zip archive (${resolved.url}), a source type hoocode cannot install yet. ` +
|
|
430
|
+
"The entry is listed so you know it exists; install it through the vendor CLI, or ask the marketplace " +
|
|
431
|
+
"for a git source.",
|
|
432
|
+
};
|
|
348
433
|
}
|
|
434
|
+
// Drop the clone's `.git`. Nothing reads it — an installed plugin is never
|
|
435
|
+
// updated from its remote, by design (that is the supply-chain boundary) — and
|
|
436
|
+
// keeping it actively breaks project scope: a repository nested inside the
|
|
437
|
+
// user's repository is an embedded repo, so `git add` writes a gitlink instead
|
|
438
|
+
// of the plugin's files and the install cannot actually be committed. Which is
|
|
439
|
+
// the one thing project scope is for. Cheaper installs at user scope too.
|
|
440
|
+
rmSync(path.join(dest, ".git"), { recursive: true, force: true });
|
|
349
441
|
let parsed = parsePluginDir(dest);
|
|
350
442
|
if (!hasAnyManifest(dest)) {
|
|
351
443
|
// Manifest-less plugin dir (some marketplaces index bare capability trees,
|
|
@@ -363,25 +455,80 @@ export async function installAvailablePlugin(cwd, name, agentDir = getAgentDir()
|
|
|
363
455
|
rmSync(dest, { recursive: true, force: true });
|
|
364
456
|
return { installed: false, message: `Installed source for "${name}" has no recognizable plugin manifest.` };
|
|
365
457
|
}
|
|
458
|
+
const capabilities = loadableCapabilities(parsed);
|
|
459
|
+
let message = `Installed "${name}" from marketplace "${found.marketplaceName}" ` +
|
|
460
|
+
`(${parsed.supportPlatform.join(", ")}) to ${dest} [${scope} scope]. ` +
|
|
461
|
+
`Remove it with UninstallPlugin.`;
|
|
462
|
+
if (scope === "project") {
|
|
463
|
+
// Project scope puts the plugin in the working tree, which is the point —
|
|
464
|
+
// but it is also a `git status` entry and, once committed, code that runs
|
|
465
|
+
// for whoever clones next. Executable capabilities are the half that
|
|
466
|
+
// matters: hoocode has no workspace-trust gate for `<cwd>/.agents/plugins`
|
|
467
|
+
// (see docs/plugin-system-architecture.md §5.9), so a committed hook or MCP
|
|
468
|
+
// server loads for collaborators without a prompt.
|
|
469
|
+
const executables = [parsed.hooks && "hooks", parsed.mcpServers && "MCP servers"].filter(Boolean);
|
|
470
|
+
message += ` It is now part of the working tree — commit it to share it with collaborators.`;
|
|
471
|
+
if (executables.length > 0) {
|
|
472
|
+
message +=
|
|
473
|
+
` It carries ${executables.join(" and ")}, which run for anyone who clones the repository;` +
|
|
474
|
+
` keep it out of version control if that is not what you want.`;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (parsed.id !== name) {
|
|
478
|
+
// The entry name and the manifest name are allowed to differ, and the
|
|
479
|
+
// difference is otherwise invisible until ListPlugins reports an id the
|
|
480
|
+
// caller never asked for.
|
|
481
|
+
message += ` Listed by ListPlugins as "${parsed.id}"; either name uninstalls it.`;
|
|
482
|
+
}
|
|
483
|
+
if (capabilities.length === 0) {
|
|
484
|
+
// An install that adds nothing should not read as an install that worked.
|
|
485
|
+
// Common in github/awesome-copilot, where most entries carry their content
|
|
486
|
+
// in Copilot UI `extensions/` that hoocode has no analog for.
|
|
487
|
+
const surfaces = parsed.unsupportedSurfaces?.length
|
|
488
|
+
? ` Unsupported surfaces present: ${parsed.unsupportedSurfaces.join(", ")}.`
|
|
489
|
+
: "";
|
|
490
|
+
message +=
|
|
491
|
+
" Note: it contributes no capabilities hoocode can load" +
|
|
492
|
+
` (no skills, commands, subagents, hooks or MCP servers).${surfaces}`;
|
|
493
|
+
}
|
|
366
494
|
return {
|
|
367
495
|
installed: true,
|
|
368
496
|
dest,
|
|
497
|
+
scope,
|
|
498
|
+
...(parsed.id !== name ? { id: parsed.id } : {}),
|
|
369
499
|
supportPlatform: parsed.supportPlatform,
|
|
370
|
-
message
|
|
371
|
-
`(${parsed.supportPlatform.join(", ")}) to ${dest}. ` +
|
|
372
|
-
`Remove it with UninstallPlugin.`,
|
|
500
|
+
message,
|
|
373
501
|
};
|
|
374
502
|
}
|
|
503
|
+
/** True when `target` is `root` or sits inside it. */
|
|
504
|
+
function isUnderDir(target, root) {
|
|
505
|
+
const normalized = path.resolve(root);
|
|
506
|
+
if (path.resolve(target) === normalized)
|
|
507
|
+
return true;
|
|
508
|
+
return path.resolve(target).startsWith(normalized.endsWith(path.sep) ? normalized : `${normalized}${path.sep}`);
|
|
509
|
+
}
|
|
375
510
|
/**
|
|
376
511
|
* Remove an installed or authored plugin from every location it could occupy:
|
|
377
512
|
* the consumption home, both production homes, and the legacy project-local
|
|
378
513
|
* directories that older versions wrote into.
|
|
514
|
+
*
|
|
515
|
+
* `name` is matched two ways, because a plugin has two names: the marketplace
|
|
516
|
+
* entry name it was installed under (which is what the directory is called) and
|
|
517
|
+
* the manifest id `ListPlugins` reports. Resolving only the first left a plugin
|
|
518
|
+
* whose names differ impossible to remove through the tool surface — the id the
|
|
519
|
+
* model was shown was the one uninstall did not accept.
|
|
520
|
+
*
|
|
521
|
+
* Removal by id stays inside {@link pluginHomeRoots}, so a plugin discovered in
|
|
522
|
+
* `<cwd>/.claude/skills` — repository content the team committed, which hoocode
|
|
523
|
+
* did not install — is never deleted out of the working tree.
|
|
379
524
|
*/
|
|
380
525
|
export function uninstallPlugin(cwd, name, agentDir = getAgentDir()) {
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
526
|
+
const homes = pluginHomeRoots(cwd, agentDir);
|
|
527
|
+
const byManifestId = listInstalledPlugins(cwd, agentDir)
|
|
528
|
+
.filter((p) => p.id === name)
|
|
529
|
+
.map((p) => p.root)
|
|
530
|
+
.filter((root) => homes.some((home) => isUnderDir(root, home)));
|
|
531
|
+
const candidates = [...new Set([...candidatePluginDirs(cwd, name, agentDir), ...byManifestId])];
|
|
385
532
|
const present = candidates.filter((p) => existsSync(p));
|
|
386
533
|
if (present.length === 0)
|
|
387
534
|
return { removed: false, message: `Plugin "${name}" is not installed.` };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACN,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,GACd,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAGN,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,GACnB,MAAM,kBAAkB,CAAC;AAE1B;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,GAAW,EAAY;IAChD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,mBAAmB,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAAA,CACzG;AAED,6EAA6E;AAC7E,SAAS,qBAAqB,GAAW;IACxC,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,wBAAwB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAAA,CACzE;AAED,4GAA0G;AAC1G,MAAM,UAAU,wBAAwB,GAAsB;IAC7D,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,GAAG,EAAE,qBAAqB,EAAE,EAAE,CAAC;AAAA,CACrE;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAiD;IACpF,EAAE,IAAI,EAAE,yBAAyB,EAAE,GAAG,EAAE,uDAAuD,EAAE;IACjG,8EAA8E;IAC9E,6DAA6D;IAC7D,wEAAsE;IACtE,EAAE,IAAI,EAAE,iBAAiB,EAAE,GAAG,EAAE,2CAA2C,EAAE;CAC7E,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE/C,SAAS,gBAAgB,GAAW;IACnC,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;IAC3D,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC;AAAA,CACnE;AAID,SAAS,aAAa,CAAC,QAAgB,EAAa;IACnD,MAAM,MAAM,GAAG,YAAY,CAAY,wBAAwB,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC3E,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CAC1D;AAED,SAAS,WAAW,CAAC,QAAgB,EAAE,GAAW,EAAQ;IACzD,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACrC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAChD,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACnD,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAAA,CAClE;AAED,SAAS,OAAO,CAAC,QAAgB,EAAE,GAAW,EAAW;IACxD,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAC/B,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,EAAE,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACrB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACxC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,GAAG,CAAC;AAAA,CAC1C;AAED,SAAS,YAAY,CAAI,IAAY,EAAY;IAChD,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAM,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AAAA,CACD;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,YAAY,CAAC,GAAW,EAAE,GAAW,EAA6D;IAChH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAC/D,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAErC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,OAAO,MAAM,CAAC;IACf,CAAC;IACD,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,IAAI,CAAC;QACJ,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,OAAO,MAAM,CAAC;AAAA,CACd;AAOD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAChD,QAAQ,GAAW,WAAW,EAAE,EAChC,OAAO,GAA8B,EAAE,EACnB;IACpB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,EAAE,IAAI,uBAAuB,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAClD,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC;YAAE,SAAS;QAEtE,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACxF,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACpB,uEAAuE;YACvE,6DAA6D;YAC7D,IAAI,CAAC,OAAO;gBAAE,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC5D,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YAChE,SAAS;QACV,CAAC;QACD,WAAW,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,GAAW,EACX,QAAQ,GAAW,WAAW,EAAE,EACqB;IACrD,MAAM,MAAM,GAAG,MAAM,2BAA2B,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5E,MAAM,SAAS,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAC9G,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CACf,CAAC;IAEF,6EAA6E;IAC7E,sEAAsE;IACtE,KAAK,MAAM,MAAM,IAAI,sBAAsB,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;QAC5D,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC5E,IAAI,CAAC,aAAa,IAAI,uBAAuB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,QAAQ,CAAC;YAAE,SAAS;QACjG,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5D,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,QAAQ,KAAK,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;aACvF,CAAC;YACL,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;YACvC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;IACF,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AAAA,CAC7B;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAW,EAAE,QAAQ,GAAW,WAAW,EAAE,EAAuB;IAC1G,MAAM,OAAO,GAAwB,EAAE,CAAC;IACxC,MAAM,GAAG,GAAG,wBAAwB,EAAE,CAAC;IACvC,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE3C,yEAAyE;IACzE,mEAAmE;IACnE,KAAK,MAAM,EAAE,IAAI,uBAAuB,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAClD,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC;YAC5D,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QACzC,CAAC;IACF,CAAC;IAED,0EAA0E;IAC1E,6EAA6E;IAC7E,MAAM,IAAI,GAAG;QACZ,GAAG,oBAAoB,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QACvD,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC;KAChE,CAAC;IACF,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACtB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,OAAO,CAAC;AAAA,CACf;AAgBD,sGAAsG;AACtG,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,QAAQ,GAAW,WAAW,EAAE,EAAqB;IACtG,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,sBAAsB,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;QACzD,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM;YAAE,SAAS;QACtB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACnC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACrB,GAAG,CAAC,IAAI,CAAC;gBACR,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,WAAW,EAAE,KAAK,CAAC,WAAW;gBAC9B,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,UAAU,EAAE,mBAAmB,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI;gBAC/D,eAAe,EAAE,MAAM,CAAC,IAAI;gBAC5B,eAAe,EAAE,MAAM,CAAC,IAAI;gBAC5B,eAAe,EAAE,KAAK,CAAC,eAAe,IAAI,MAAM,CAAC,eAAe;aAChE,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IACD,OAAO,GAAG,CAAC;AAAA,CACX;AAED,oDAAoD;AACpD,MAAM,UAAU,mBAAmB,CAClC,GAAW,EACX,IAAY,EACZ,QAAQ,GAAW,WAAW,EAAE,EACF;IAC9B,OAAO,oBAAoB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAAA,CACxE;AAED,sEAAsE;AACtE,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,QAAQ,GAAW,WAAW,EAAE,EAAsB;IACvG,OAAO,eAAe,CAAC,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;AAAA,CACzD;AAED,iEAAiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,IAAY,EAAE,QAAQ,GAAW,WAAW,EAAE,EAAW;IACvG,OAAO,oBAAoB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC;AAAA,CACtE;AAED,SAAS,OAAO,CAAC,IAAc,EAA6D;IAC3F,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACjC,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QAAA,CACvB,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QAAA,CACvB,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAAA,CAC1E,CAAC,CAAC;AAAA,CACH;AAED;;;;;GAKG;AACH,KAAK,UAAU,YAAY,CAC1B,GAAW,EACX,IAAY,EACZ,GAAY,EACZ,GAAY,EACgD;IAC5D,IAAI,GAAG,EAAE,CAAC;QACT,OAAO,OAAO,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,GAAG,EAAE,CAAC;QACT,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;QAC3D,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC;QACzC,OAAO,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,OAAO,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;AAAA,CAC3D;AAED;;;GAGG;AACH,KAAK,UAAU,gBAAgB,CAC9B,GAAW,EACX,MAAc,EACd,IAAY,EACZ,GAAY,EACZ,GAAY,EAC6C;IACzD,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC;IACxE,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAC3D,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;QACjG,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,iDAAiD,MAAM,EAAE,EAAE,CAAC;QAC1F,CAAC;QACD,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACrB,CAAC;YAAS,CAAC;QACV,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;AAAA,CACD;AAYD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC3C,GAAW,EACX,IAAY,EACZ,QAAQ,GAAW,WAAW,EAAE,EACN;IAC1B,MAAM,KAAK,GAAG,mBAAmB,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IACvD,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,IAAI,4CAA4C,EAAE,CAAC;IAE9G,MAAM,QAAQ,GAAG,mBAAmB,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;IAC1E,MAAM,IAAI,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;IACnD,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAErC,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC/B,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,iCAAiC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QACxF,CAAC;QACD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;SAAM,IAAI,QAAQ,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACpC,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC/E,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;QAC9F,CAAC;IACF,CAAC;SAAM,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC3C,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;QAClG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC;QACnD,CAAC;IACF,CAAC;SAAM,CAAC;QACP,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,6CAA6C,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC;IACtG,CAAC;IAED,IAAI,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,2EAA2E;QAC3E,0EAA0E;QAC1E,6EAAyE;QACzE,2EAA2E;QAC3E,wEAAwE;QACxE,wCAAwC;QACxC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;QACtD,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5C,aAAa,CACZ,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,EACrC,GAAG,IAAI,CAAC,SAAS,CAChB,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EACtF,IAAI,EACJ,CAAC,CACD,IAAI,EACL,MAAM,CACN,CAAC;QACF,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,yBAAyB,IAAI,wCAAwC,EAAE,CAAC;IAC7G,CAAC;IACD,OAAO;QACN,SAAS,EAAE,IAAI;QACf,IAAI;QACJ,eAAe,EAAE,MAAM,CAAC,eAAe;QACvC,OAAO,EACN,cAAc,IAAI,uBAAuB,KAAK,CAAC,eAAe,IAAI;YAClE,IAAI,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI;YACrD,iCAAiC;KAClC,CAAC;AAAA,CACF;AAOD;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,IAAY,EAAE,QAAQ,GAAW,WAAW,EAAE,EAAoB;IAC9G,MAAM,UAAU,GAAG;QAClB,GAAG,mBAAmB,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC;KAC3D,CAAC;IACF,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,IAAI,qBAAqB,EAAE,CAAC;IACnG,KAAK,MAAM,CAAC,IAAI,OAAO;QAAE,MAAM,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,IAAI,IAAI,EAAE,CAAC;AAAA,CACxD","sourcesContent":["/**\n * Plugin install/uninstall/discovery — the shared engine behind both the\n * `/plugin` slash command (human-driven) and the model-facing lifecycle tools\n * (`SearchPlugins`, `InstallPlugin`, ...). Keeping it in one place means the two\n * surfaces can never drift on where plugins live or how sources resolve.\n *\n * Trust model (see docs/plugin-system-spec.md): *adding* a marketplace is the\n * human trust boundary; *installing* from an already-trusted marketplace is the\n * model's discretion (package-manager model), and must stay transparent and\n * reversible. This module performs the mechanical install/remove; the gating\n * (announce, injection carve-out) lives with the callers.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../../../config.js\";\nimport { defaultPluginDirs } from \"../loader.js\";\nimport { hasAnyManifest } from \"./formats/index.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\nimport { discoverPlugins } from \"./index.js\";\nimport {\n\tcandidatePluginDirs,\n\tconsumptionPluginsDir,\n\tmarketplaceCacheDir,\n\tmarketplaceCacheMetaPath,\n\tmarketplaceCacheRoot,\n\tmarketplaceStorePath,\n\tsanitizeForDir,\n} from \"./locations.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { parsePluginDir } from \"./manifest.js\";\nimport {\n\ttype MarketplacePluginSource,\n\ttype MarketplaceRecord,\n\tparseMarketplaceDir,\n\treadMarketplaceStore,\n\tresolvePluginSource,\n} from \"./marketplace.js\";\n\n/**\n * Marketplace registries older versions wrote into the working tree, newest\n * convention first. Still read so a user who added a marketplace before the\n * registry moved to the agent dir does not silently lose it; nothing writes here.\n */\nfunction legacyStorePaths(cwd: string): string[] {\n\treturn [path.join(cwd, \".agents\", \"marketplaces.json\"), path.join(cwd, \".hoocode\", \"marketplaces.json\")];\n}\n\n/** Absolute path to the curated default marketplace bundled with hoocode. */\nfunction defaultMarketplaceDir(): string {\n\treturn fileURLToPath(new URL(\"./default-marketplace/\", import.meta.url));\n}\n\n/** The default marketplace record — always present so source-level trust is meaningful out of the box. */\nexport function defaultMarketplaceRecord(): MarketplaceRecord {\n\treturn { location: \"hoocode-default\", dir: defaultMarketplaceDir() };\n}\n\n/**\n * Curated well-known marketplaces, trusted out of the box. Shipping an entry\n * here is a maintainer-level trust decision (the human half of the \"adding a\n * marketplace is the human trust boundary\" rule) — keep this list short and\n * high-trust. Indices are cloned lazily into the marketplace cache on first\n * search and refreshed on a TTL thereafter. Refreshing the *index* only changes\n * what is discoverable; installed plugins are still never auto-updated, and\n * plugin sources inside the official index are sha-pinned.\n */\nexport const WELL_KNOWN_MARKETPLACES: ReadonlyArray<{ name: string; url: string }> = [\n\t{ name: \"claude-plugins-official\", url: \"https://github.com/anthropics/claude-plugins-official\" },\n\t// The Copilot plugin directory ships both a `.github/plugin/marketplace.json`\n\t// and a `.claude-plugin/marketplace.json`, so it parses with\n\t// supportPlatform [\"claude\", \"github\"] — the default `github` source.\n\t{ name: \"copilot-plugins\", url: \"https://github.com/github/copilot-plugins\" },\n];\n\n/**\n * How long a cached marketplace index is considered fresh.\n *\n * Refreshing an *index* is safe to automate: it changes what is discoverable,\n * not what is installed. The rule that an installed plugin is never auto-updated\n * from a remote is untouched — that is the supply-chain boundary, and this is a\n * catalog listing.\n */\nconst MARKETPLACE_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction marketplaceTtlMs(): number {\n\tconst raw = Number(process.env.HOOCODE_MARKETPLACE_TTL_MS);\n\treturn Number.isFinite(raw) && raw >= 0 ? raw : MARKETPLACE_TTL_MS;\n}\n\ntype CacheMeta = Record<string, string>;\n\nfunction readCacheMeta(agentDir: string): CacheMeta {\n\tconst parsed = readJsonFile<CacheMeta>(marketplaceCacheMetaPath(agentDir));\n\treturn parsed && typeof parsed === \"object\" ? parsed : {};\n}\n\nfunction recordFetch(agentDir: string, url: string): void {\n\tconst meta = readCacheMeta(agentDir);\n\tmeta[url] = new Date().toISOString();\n\tconst file = marketplaceCacheMetaPath(agentDir);\n\tmkdirSync(path.dirname(file), { recursive: true });\n\twriteFileSync(file, `${JSON.stringify(meta, null, 2)}\\n`, \"utf8\");\n}\n\nfunction isStale(agentDir: string, url: string): boolean {\n\tconst ttl = marketplaceTtlMs();\n\tif (ttl === 0) return true;\n\tconst at = readCacheMeta(agentDir)[url];\n\tif (!at) return true;\n\tconst age = Date.now() - Date.parse(at);\n\treturn !Number.isFinite(age) || age > ttl;\n}\n\nfunction readJsonFile<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Bring an existing clone up to date, falling back to a fresh clone when the pull\n * cannot fast-forward (a force-pushed remote, or a corrupt cache).\n *\n * The re-clone goes to a temp directory and is swapped in only on success. An\n * earlier version deleted the cache first, which meant a refresh attempted while\n * offline destroyed a perfectly good index — the opposite of the \"offline is\n * non-fatal\" property the caller depends on.\n */\nasync function refreshClone(url: string, dir: string): Promise<{ code: number; stdout: string; stderr: string }> {\n\tconst pulled = await execGit([\"-C\", dir, \"pull\", \"--ff-only\"]);\n\tif (pulled.code === 0) return pulled;\n\n\tconst staging = mkdtempSync(path.join(os.tmpdir(), \"hoo-market-refresh-\"));\n\tconst target = path.join(staging, \"clone\");\n\tconst cloned = await cloneGitRepo(url, target);\n\tif (cloned.code !== 0) {\n\t\trmSync(staging, { recursive: true, force: true });\n\t\treturn cloned;\n\t}\n\trmSync(dir, { recursive: true, force: true });\n\tmkdirSync(path.dirname(dir), { recursive: true });\n\ttry {\n\t\trenameSync(target, dir);\n\t} catch {\n\t\tcpSync(target, dir, { recursive: true });\n\t}\n\trmSync(staging, { recursive: true, force: true });\n\treturn cloned;\n}\n\nexport interface EnsureMarketplacesOptions {\n\t/** Refresh regardless of the TTL. */\n\tforce?: boolean;\n}\n\n/**\n * Make sure every well-known marketplace index is cached and reasonably fresh.\n *\n * Clones what is missing and pulls what has gone stale. Without the TTL the\n * indices were fetched exactly once and never again, so a plugin added upstream\n * stayed permanently invisible — the inherit half of the system quietly stopped\n * inheriting. Offline is non-fatal: each failure is returned and search degrades\n * to whatever is already on disk.\n */\nexport async function ensureWellKnownMarketplaces(\n\tagentDir: string = getAgentDir(),\n\toptions: EnsureMarketplacesOptions = {},\n): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tconst present = existsSync(dir);\n\t\tif (present && !options.force && !isStale(agentDir, wk.url)) continue;\n\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tconst res = present ? await refreshClone(wk.url, dir) : await cloneGitRepo(wk.url, dir);\n\t\tif (res.code !== 0) {\n\t\t\t// A failed *refresh* keeps the stale copy: an out-of-date index is far\n\t\t\t// more useful than none, and the network may simply be down.\n\t\t\tif (!present) rmSync(dir, { recursive: true, force: true });\n\t\t\terrors.push(`${wk.name}: ${(res.stderr || res.stdout).trim()}`);\n\t\t\tcontinue;\n\t\t}\n\t\trecordFetch(agentDir, wk.url);\n\t}\n\treturn errors;\n}\n\n/**\n * Refresh every cached index — well-known and user-added — ignoring the TTL.\n * The explicit half of the freshness story, for when a user knows the upstream\n * changed and does not want to wait out the interval.\n */\nexport async function refreshMarketplaces(\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<{ refreshed: string[]; errors: string[] }> {\n\tconst errors = await ensureWellKnownMarketplaces(agentDir, { force: true });\n\tconst refreshed = WELL_KNOWN_MARKETPLACES.filter((wk) => existsSync(marketplaceCacheDir(wk.url, agentDir))).map(\n\t\t(wk) => wk.name,\n\t);\n\n\t// User-added marketplaces are cached the same way when they came from git; a\n\t// local-path marketplace is read in place and has nothing to refresh.\n\tfor (const record of readMarketplaceRecords(cwd, agentDir)) {\n\t\tconst isCachedClone = record.dir.startsWith(marketplaceCacheRoot(agentDir));\n\t\tif (!isCachedClone || WELL_KNOWN_MARKETPLACES.some((wk) => wk.url === record.location)) continue;\n\t\tconst res = await refreshClone(record.location, record.dir);\n\t\tif (res.code !== 0) errors.push(`${record.location}: ${(res.stderr || res.stdout).trim()}`);\n\t\telse {\n\t\t\trecordFetch(agentDir, record.location);\n\t\t\trefreshed.push(record.location);\n\t\t}\n\t}\n\treturn { refreshed, errors };\n}\n\n/**\n * All marketplace records in effect: the bundled default first (curated,\n * trusted), then user-added ones from `.agents/` (falling back to legacy\n * `.hoocode/`). Deduplicated by directory.\n */\nexport function readMarketplaceRecords(cwd: string, agentDir: string = getAgentDir()): MarketplaceRecord[] {\n\tconst records: MarketplaceRecord[] = [];\n\tconst def = defaultMarketplaceRecord();\n\tif (existsSync(def.dir)) records.push(def);\n\n\t// Well-known marketplaces participate once their index is cached locally\n\t// (see ensureWellKnownMarketplaces; SearchPlugins fetches lazily).\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tif (existsSync(dir) && !records.some((x) => x.dir === dir)) {\n\t\t\trecords.push({ location: wk.url, dir });\n\t\t}\n\t}\n\n\t// The registry lives in the agent dir. Project-local ones are a migration\n\t// read-path: merged in, never written back, so an older setup keeps working.\n\tconst user = [\n\t\t...readMarketplaceStore(marketplaceStorePath(agentDir)),\n\t\t...legacyStorePaths(cwd).flatMap((p) => readMarketplaceStore(p)),\n\t];\n\tfor (const r of user) {\n\t\tif (!records.some((x) => x.dir === r.dir)) records.push(r);\n\t}\n\treturn records;\n}\n\n/** A plugin offered by some registered marketplace (not necessarily installed). */\nexport interface AvailablePlugin {\n\tname: string;\n\tdescription?: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\t/** Resolved source kind, for display and gating. */\n\tsourceKind: \"local\" | \"git\" | \"git-subdir\" | \"npm\";\n\tmarketplaceName: string;\n\tmarketplaceRoot: string;\n\t/** Platforms this entry targets (per-entry hint, else the marketplace's). */\n\tsupportPlatform: MarketplacePlatform[];\n}\n\n/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */\nexport function listAvailablePlugins(cwd: string, agentDir: string = getAgentDir()): AvailablePlugin[] {\n\tconst out: AvailablePlugin[] = [];\n\tconst seen = new Set<string>();\n\tfor (const rec of readMarketplaceRecords(cwd, agentDir)) {\n\t\tconst market = parseMarketplaceDir(rec.dir);\n\t\tif (!market) continue;\n\t\tfor (const entry of market.plugins) {\n\t\t\tif (seen.has(entry.name)) continue;\n\t\t\tseen.add(entry.name);\n\t\t\tout.push({\n\t\t\t\tname: entry.name,\n\t\t\t\tdescription: entry.description,\n\t\t\t\tsource: entry.source,\n\t\t\t\tsourceKind: resolvePluginSource(entry.source, market.root).kind,\n\t\t\t\tmarketplaceName: market.name,\n\t\t\t\tmarketplaceRoot: market.root,\n\t\t\t\tsupportPlatform: entry.supportPlatform ?? market.supportPlatform,\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Find a single available plugin by exact name. */\nexport function findAvailablePlugin(\n\tcwd: string,\n\tname: string,\n\tagentDir: string = getAgentDir(),\n): AvailablePlugin | undefined {\n\treturn listAvailablePlugins(cwd, agentDir).find((p) => p.name === name);\n}\n\n/** All currently installed plugins (project + global plugin dirs). */\nexport function listInstalledPlugins(cwd: string, agentDir: string = getAgentDir()): NormalizedPlugin[] {\n\treturn discoverPlugins(defaultPluginDirs(cwd, agentDir));\n}\n\n/** Whether a plugin with `name` (by id) is already installed. */\nexport function isPluginInstalled(cwd: string, name: string, agentDir: string = getAgentDir()): boolean {\n\treturn listInstalledPlugins(cwd, agentDir).some((p) => p.id === name);\n}\n\nfunction execGit(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", args);\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));\n\t\tchild.on(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t});\n}\n\n/**\n * Clone a git repository into `dest`.\n * If `ref` is provided, does a shallow clone of that branch/tag.\n * If only `sha` is provided, does a full clone and checks out the commit.\n * Returns the clone result; on failure `dest` may be partially created.\n */\nasync function cloneGitRepo(\n\turl: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n\tif (ref) {\n\t\treturn execGit([\"clone\", \"--branch\", ref, \"--depth\", \"1\", \"--\", url, dest]);\n\t}\n\tif (sha) {\n\t\tconst cloneRes = await execGit([\"clone\", \"--\", url, dest]);\n\t\tif (cloneRes.code !== 0) return cloneRes;\n\t\treturn execGit([\"-C\", dest, \"checkout\", \"--quiet\", sha]);\n\t}\n\treturn execGit([\"clone\", \"--depth\", \"1\", \"--\", url, dest]);\n}\n\n/**\n * Resolve a git-subdir source: clone the repo, checkout the requested ref/sha,\n * and copy the subdirectory to `dest`. Cleans up the temporary clone on failure.\n */\nasync function installGitSubdir(\n\turl: string,\n\tsubdir: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ ok: true } | { ok: false; message: string }> {\n\tconst tmpDir = mkdtempSync(path.join(os.tmpdir(), \"hoo-plugin-clone-\"));\n\ttry {\n\t\tconst cloneRes = await cloneGitRepo(url, tmpDir, ref, sha);\n\t\tif (cloneRes.code !== 0) {\n\t\t\treturn { ok: false, message: `git clone failed: ${cloneRes.stderr || cloneRes.stdout}`.trim() };\n\t\t}\n\t\tconst src = path.resolve(tmpDir, subdir);\n\t\tif (!existsSync(src)) {\n\t\t\treturn { ok: false, message: `Plugin subdirectory not found in cloned repo: ${subdir}` };\n\t\t}\n\t\tcpSync(src, dest, { recursive: true });\n\t\treturn { ok: true };\n\t} finally {\n\t\trmSync(tmpDir, { recursive: true, force: true });\n\t}\n}\n\nexport interface InstallOutcome {\n\tinstalled: boolean;\n\t/** Install destination directory (when installed). */\n\tdest?: string;\n\t/** Platforms the installed plugin supports (read back from disk). */\n\tsupportPlatform?: MarketplacePlatform[];\n\t/** Human-readable summary suitable for a tool result or notification. */\n\tmessage: string;\n}\n\n/**\n * Install an available plugin by name into the **consumption home** — the global\n * `~/.agents/plugins/`, not the working tree. A plugin is portable and reusable\n * across projects, so installing into the repo would both dirty `git status` and\n * hide the capability from every other checkout.\n *\n * Copies local sources; clones git sources. Transparent + reversible by\n * construction — the plugin lands in a named directory and\n * {@link uninstallPlugin} removes it. Callers activate the result (live\n * activation via AgentSession.activatePlugin, or a reload).\n */\nexport async function installAvailablePlugin(\n\tcwd: string,\n\tname: string,\n\tagentDir: string = getAgentDir(),\n): Promise<InstallOutcome> {\n\tconst found = findAvailablePlugin(cwd, name, agentDir);\n\tif (!found) return { installed: false, message: `Plugin \"${name}\" not found in any registered marketplace.` };\n\n\tconst resolved = resolvePluginSource(found.source, found.marketplaceRoot);\n\tconst home = consumptionPluginsDir(agentDir);\n\tconst dest = path.join(home, sanitizeForDir(name));\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(home, { recursive: true });\n\n\tif (resolved.kind === \"local\") {\n\t\tif (!existsSync(resolved.path)) {\n\t\t\treturn { installed: false, message: `Plugin source path not found: ${resolved.path}` };\n\t\t}\n\t\tcpSync(resolved.path, dest, { recursive: true });\n\t} else if (resolved.kind === \"git\") {\n\t\tconst res = await cloneGitRepo(resolved.url, dest, resolved.ref, resolved.sha);\n\t\tif (res.code !== 0) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: `git clone failed: ${res.stderr || res.stdout}`.trim() };\n\t\t}\n\t} else if (resolved.kind === \"git-subdir\") {\n\t\tconst res = await installGitSubdir(resolved.url, resolved.path, dest, resolved.ref, resolved.sha);\n\t\tif (!res.ok) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: res.message };\n\t\t}\n\t} else {\n\t\treturn { installed: false, message: `npm plugin sources are not supported yet (${resolved.spec}).` };\n\t}\n\n\tlet parsed = parsePluginDir(dest);\n\tif (!hasAnyManifest(dest)) {\n\t\t// Manifest-less plugin dir (some marketplaces index bare capability trees,\n\t\t// e.g. a plugin that is just a `skills/` directory). Such a directory now\n\t\t// *parses* on its own — the vendors make the manifest optional — but the\n\t\t// entry's name and description live only in the marketplace index, and the\n\t\t// derived id would be the sanitized directory name. Synthesize a native\n\t\t// manifest so both survive the install.\n\t\tconst manifestDir = path.join(dest, \".agents-plugin\");\n\t\tmkdirSync(manifestDir, { recursive: true });\n\t\twriteFileSync(\n\t\t\tpath.join(manifestDir, \"plugin.json\"),\n\t\t\t`${JSON.stringify(\n\t\t\t\t{ name: found.name, ...(found.description ? { description: found.description } : {}) },\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t)}\\n`,\n\t\t\t\"utf8\",\n\t\t);\n\t\tparsed = parsePluginDir(dest);\n\t}\n\tif (!parsed) {\n\t\trmSync(dest, { recursive: true, force: true });\n\t\treturn { installed: false, message: `Installed source for \"${name}\" has no recognizable plugin manifest.` };\n\t}\n\treturn {\n\t\tinstalled: true,\n\t\tdest,\n\t\tsupportPlatform: parsed.supportPlatform,\n\t\tmessage:\n\t\t\t`Installed \"${name}\" from marketplace \"${found.marketplaceName}\" ` +\n\t\t\t`(${parsed.supportPlatform.join(\", \")}) to ${dest}. ` +\n\t\t\t`Remove it with UninstallPlugin.`,\n\t};\n}\n\nexport interface UninstallOutcome {\n\tremoved: boolean;\n\tmessage: string;\n}\n\n/**\n * Remove an installed or authored plugin from every location it could occupy:\n * the consumption home, both production homes, and the legacy project-local\n * directories that older versions wrote into.\n */\nexport function uninstallPlugin(cwd: string, name: string, agentDir: string = getAgentDir()): UninstallOutcome {\n\tconst candidates = [\n\t\t...candidatePluginDirs(cwd, name, agentDir),\n\t\tpath.join(cwd, \".hoocode\", \"plugins\", sanitizeForDir(name)),\n\t];\n\tconst present = candidates.filter((p) => existsSync(p));\n\tif (present.length === 0) return { removed: false, message: `Plugin \"${name}\" is not installed.` };\n\tfor (const p of present) rmSync(p, { recursive: true, force: true });\n\treturn { removed: true, message: `Removed \"${name}\".` };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/install.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin install/uninstall/discovery — the shared engine behind both the\n * `/plugin` slash command (human-driven) and the model-facing lifecycle tools\n * (`SearchPlugins`, `InstallPlugin`, ...). Keeping it in one place means the two\n * surfaces can never drift on where plugins live or how sources resolve.\n *\n * Trust model (see docs/plugin-system-spec.md): *adding* a marketplace is the\n * human trust boundary; *installing* from an already-trusted marketplace is the\n * model's discretion (package-manager model), and must stay transparent and\n * reversible. This module performs the mechanical install/remove; the gating\n * (announce, injection carve-out) lives with the callers.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../../../config.js\";\nimport { defaultPluginDirs } from \"../loader.js\";\nimport { hasAnyManifest } from \"./formats/index.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\nimport { discoverPlugins } from \"./index.js\";\nimport {\n\tcandidatePluginDirs,\n\tinstallHomeForScope,\n\tmarketplaceCacheDir,\n\tmarketplaceCacheMetaPath,\n\tmarketplaceCacheRoot,\n\tmarketplaceStorePath,\n\ttype PluginInstallScope,\n\tpluginHomeRoots,\n\tsanitizeForDir,\n} from \"./locations.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { parsePluginDir } from \"./manifest.js\";\nimport {\n\ttype MarketplacePluginSource,\n\ttype MarketplaceRecord,\n\tparseMarketplaceDir,\n\treadMarketplaceStore,\n\tresolvePluginSource,\n} from \"./marketplace.js\";\n\n/**\n * Marketplace registries older versions wrote into the working tree, newest\n * convention first. Still read so a user who added a marketplace before the\n * registry moved to the agent dir does not silently lose it; nothing writes here.\n */\nfunction legacyStorePaths(cwd: string): string[] {\n\treturn [path.join(cwd, \".agents\", \"marketplaces.json\"), path.join(cwd, \".hoocode\", \"marketplaces.json\")];\n}\n\n/** Absolute path to the curated default marketplace bundled with hoocode. */\nfunction defaultMarketplaceDir(): string {\n\treturn fileURLToPath(new URL(\"./default-marketplace/\", import.meta.url));\n}\n\n/** The default marketplace record — always present so source-level trust is meaningful out of the box. */\nexport function defaultMarketplaceRecord(): MarketplaceRecord {\n\treturn { location: \"hoocode-default\", dir: defaultMarketplaceDir() };\n}\n\n/**\n * Curated well-known marketplaces, trusted out of the box. Shipping an entry\n * here is a maintainer-level trust decision (the human half of the \"adding a\n * marketplace is the human trust boundary\" rule) — keep this list short and\n * high-trust. Indices are cloned lazily into the marketplace cache on first\n * search and refreshed on a TTL thereafter. Refreshing the *index* only changes\n * what is discoverable; installed plugins are still never auto-updated, and\n * plugin sources inside the official index are sha-pinned.\n */\nexport const WELL_KNOWN_MARKETPLACES: ReadonlyArray<{ name: string; url: string }> = [\n\t{ name: \"claude-plugins-official\", url: \"https://github.com/anthropics/claude-plugins-official\" },\n\t// The Copilot plugin directory ships both a `.github/plugin/marketplace.json`\n\t// and a `.claude-plugin/marketplace.json`, so it parses with\n\t// supportPlatform [\"claude\", \"github\"] — the default `github` source.\n\t{ name: \"copilot-plugins\", url: \"https://github.com/github/copilot-plugins\" },\n\t// GitHub's community index, `.github/plugin/marketplace.json`. Same owner as\n\t// copilot-plugins, which is what makes it a source-trust decision of the same\n\t// kind. Note that most of its in-repo entries carry their content in Copilot\n\t// UI `extensions/`, a surface hoocode does not model — those install and load\n\t// nothing, and say so.\n\t{ name: \"awesome-copilot\", url: \"https://github.com/github/awesome-copilot\" },\n];\n\n/**\n * How long a cached marketplace index is considered fresh.\n *\n * Refreshing an *index* is safe to automate: it changes what is discoverable,\n * not what is installed. The rule that an installed plugin is never auto-updated\n * from a remote is untouched — that is the supply-chain boundary, and this is a\n * catalog listing.\n */\nconst MARKETPLACE_TTL_MS = 24 * 60 * 60 * 1000;\n\nfunction marketplaceTtlMs(): number {\n\tconst raw = Number(process.env.HOOCODE_MARKETPLACE_TTL_MS);\n\treturn Number.isFinite(raw) && raw >= 0 ? raw : MARKETPLACE_TTL_MS;\n}\n\ntype CacheMeta = Record<string, string>;\n\nfunction readCacheMeta(agentDir: string): CacheMeta {\n\tconst parsed = readJsonFile<CacheMeta>(marketplaceCacheMetaPath(agentDir));\n\treturn parsed && typeof parsed === \"object\" ? parsed : {};\n}\n\nfunction recordFetch(agentDir: string, url: string): void {\n\tconst meta = readCacheMeta(agentDir);\n\tmeta[url] = new Date().toISOString();\n\tconst file = marketplaceCacheMetaPath(agentDir);\n\tmkdirSync(path.dirname(file), { recursive: true });\n\twriteFileSync(file, `${JSON.stringify(meta, null, 2)}\\n`, \"utf8\");\n}\n\nfunction isStale(agentDir: string, url: string): boolean {\n\tconst ttl = marketplaceTtlMs();\n\tif (ttl === 0) return true;\n\tconst at = readCacheMeta(agentDir)[url];\n\tif (!at) return true;\n\tconst age = Date.now() - Date.parse(at);\n\treturn !Number.isFinite(age) || age > ttl;\n}\n\nfunction readJsonFile<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Bring an existing clone up to date, falling back to a fresh clone when the pull\n * cannot fast-forward (a force-pushed remote, or a corrupt cache).\n *\n * The re-clone goes to a temp directory and is swapped in only on success. An\n * earlier version deleted the cache first, which meant a refresh attempted while\n * offline destroyed a perfectly good index — the opposite of the \"offline is\n * non-fatal\" property the caller depends on.\n */\nasync function refreshClone(url: string, dir: string): Promise<{ code: number; stdout: string; stderr: string }> {\n\tconst pulled = await execGit([\"-C\", dir, \"pull\", \"--ff-only\"]);\n\tif (pulled.code === 0) return pulled;\n\n\tconst staging = mkdtempSync(path.join(os.tmpdir(), \"hoo-market-refresh-\"));\n\tconst target = path.join(staging, \"clone\");\n\tconst cloned = await cloneGitRepo(url, target);\n\tif (cloned.code !== 0) {\n\t\trmSync(staging, { recursive: true, force: true });\n\t\treturn cloned;\n\t}\n\trmSync(dir, { recursive: true, force: true });\n\tmkdirSync(path.dirname(dir), { recursive: true });\n\ttry {\n\t\trenameSync(target, dir);\n\t} catch {\n\t\tcpSync(target, dir, { recursive: true });\n\t}\n\trmSync(staging, { recursive: true, force: true });\n\treturn cloned;\n}\n\nexport interface EnsureMarketplacesOptions {\n\t/** Refresh regardless of the TTL. */\n\tforce?: boolean;\n}\n\n/**\n * Make sure every well-known marketplace index is cached and reasonably fresh.\n *\n * Clones what is missing and pulls what has gone stale. Without the TTL the\n * indices were fetched exactly once and never again, so a plugin added upstream\n * stayed permanently invisible — the inherit half of the system quietly stopped\n * inheriting. Offline is non-fatal: each failure is returned and search degrades\n * to whatever is already on disk.\n */\nexport async function ensureWellKnownMarketplaces(\n\tagentDir: string = getAgentDir(),\n\toptions: EnsureMarketplacesOptions = {},\n): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tconst present = existsSync(dir);\n\t\tif (present && !options.force && !isStale(agentDir, wk.url)) continue;\n\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tconst res = present ? await refreshClone(wk.url, dir) : await cloneGitRepo(wk.url, dir);\n\t\tif (res.code !== 0) {\n\t\t\t// A failed *refresh* keeps the stale copy: an out-of-date index is far\n\t\t\t// more useful than none, and the network may simply be down.\n\t\t\tif (!present) rmSync(dir, { recursive: true, force: true });\n\t\t\terrors.push(`${wk.name}: ${(res.stderr || res.stdout).trim()}`);\n\t\t\tcontinue;\n\t\t}\n\t\trecordFetch(agentDir, wk.url);\n\t}\n\treturn errors;\n}\n\n/**\n * Refresh every cached index — well-known and user-added — ignoring the TTL.\n * The explicit half of the freshness story, for when a user knows the upstream\n * changed and does not want to wait out the interval.\n */\nexport async function refreshMarketplaces(\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): Promise<{ refreshed: string[]; errors: string[] }> {\n\tconst errors = await ensureWellKnownMarketplaces(agentDir, { force: true });\n\tconst refreshed = WELL_KNOWN_MARKETPLACES.filter((wk) => existsSync(marketplaceCacheDir(wk.url, agentDir))).map(\n\t\t(wk) => wk.name,\n\t);\n\n\t// User-added marketplaces are cached the same way when they came from git; a\n\t// local-path marketplace is read in place and has nothing to refresh.\n\tfor (const record of readMarketplaceRecords(cwd, agentDir)) {\n\t\tconst isCachedClone = record.dir.startsWith(marketplaceCacheRoot(agentDir));\n\t\tif (!isCachedClone || WELL_KNOWN_MARKETPLACES.some((wk) => wk.url === record.location)) continue;\n\t\tconst res = await refreshClone(record.location, record.dir);\n\t\tif (res.code !== 0) errors.push(`${record.location}: ${(res.stderr || res.stdout).trim()}`);\n\t\telse {\n\t\t\trecordFetch(agentDir, record.location);\n\t\t\trefreshed.push(record.location);\n\t\t}\n\t}\n\treturn { refreshed, errors };\n}\n\n/**\n * All marketplace records in effect: the bundled default first (curated,\n * trusted), then user-added ones from `.agents/` (falling back to legacy\n * `.hoocode/`). Deduplicated by directory.\n */\nexport function readMarketplaceRecords(cwd: string, agentDir: string = getAgentDir()): MarketplaceRecord[] {\n\tconst records: MarketplaceRecord[] = [];\n\tconst def = defaultMarketplaceRecord();\n\tif (existsSync(def.dir)) records.push(def);\n\n\t// Well-known marketplaces participate once their index is cached locally\n\t// (see ensureWellKnownMarketplaces; SearchPlugins fetches lazily).\n\tfor (const wk of WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tif (existsSync(dir) && !records.some((x) => x.dir === dir)) {\n\t\t\trecords.push({ location: wk.url, dir });\n\t\t}\n\t}\n\n\t// The registry lives in the agent dir. Project-local ones are a migration\n\t// read-path: merged in, never written back, so an older setup keeps working.\n\tconst user = [\n\t\t...readMarketplaceStore(marketplaceStorePath(agentDir)),\n\t\t...legacyStorePaths(cwd).flatMap((p) => readMarketplaceStore(p)),\n\t];\n\tfor (const r of user) {\n\t\tif (!records.some((x) => x.dir === r.dir)) records.push(r);\n\t}\n\treturn records;\n}\n\n/** A plugin offered by some registered marketplace (not necessarily installed). */\nexport interface AvailablePlugin {\n\tname: string;\n\tdescription?: string;\n\t/** Relative path, git URL, `npm:<spec>`, or structured source object. */\n\tsource: MarketplacePluginSource;\n\t/** Resolved source kind, for display and gating. `npm`/`archive` are listed but not installable. */\n\tsourceKind: \"local\" | \"git\" | \"git-subdir\" | \"npm\" | \"archive\";\n\tmarketplaceName: string;\n\tmarketplaceRoot: string;\n\t/** Platforms this entry targets (per-entry hint, else the marketplace's). */\n\tsupportPlatform: MarketplacePlatform[];\n}\n\n/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */\nexport function listAvailablePlugins(cwd: string, agentDir: string = getAgentDir()): AvailablePlugin[] {\n\tconst out: AvailablePlugin[] = [];\n\tconst seen = new Set<string>();\n\tfor (const rec of readMarketplaceRecords(cwd, agentDir)) {\n\t\tconst market = parseMarketplaceDir(rec.dir);\n\t\tif (!market) continue;\n\t\tfor (const entry of market.plugins) {\n\t\t\tif (seen.has(entry.name)) continue;\n\t\t\tseen.add(entry.name);\n\t\t\tout.push({\n\t\t\t\tname: entry.name,\n\t\t\t\tdescription: entry.description,\n\t\t\t\tsource: entry.source,\n\t\t\t\tsourceKind: resolvePluginSource(entry.source, market.root).kind,\n\t\t\t\tmarketplaceName: market.name,\n\t\t\t\tmarketplaceRoot: market.root,\n\t\t\t\tsupportPlatform: entry.supportPlatform ?? market.supportPlatform,\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Find a single available plugin by exact name. */\nexport function findAvailablePlugin(\n\tcwd: string,\n\tname: string,\n\tagentDir: string = getAgentDir(),\n): AvailablePlugin | undefined {\n\treturn listAvailablePlugins(cwd, agentDir).find((p) => p.name === name);\n}\n\n/** All currently installed plugins (project + global plugin dirs). */\nexport function listInstalledPlugins(cwd: string, agentDir: string = getAgentDir()): NormalizedPlugin[] {\n\treturn discoverPlugins(defaultPluginDirs(cwd, agentDir));\n}\n\n/**\n * Whether `name` is already installed, by either name it can go by.\n *\n * A marketplace entry name and the plugin's own manifest `name` are allowed to\n * differ — both vendors say so outright — and installs land in a directory named\n * for the *entry*, while discovery reports the *manifest* id. Matching ids alone\n * meant a plugin installed as `42crunch-api-security-testing` (manifest id\n * `api-security-testing`) never counted as installed, so every InstallPlugin\n * call for the catalog name re-cloned it.\n */\nexport function isPluginInstalled(cwd: string, name: string, agentDir: string = getAgentDir()): boolean {\n\tif (listInstalledPlugins(cwd, agentDir).some((p) => p.id === name)) return true;\n\treturn candidatePluginDirs(cwd, name, agentDir).some((p) => existsSync(p));\n}\n\nfunction execGit(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", args);\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));\n\t\tchild.on(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t});\n}\n\n/**\n * Materialize exactly `sha` at `dest`, without cloning the repository's history.\n *\n * `git fetch --depth 1 <sha>` is what makes honoring a pin affordable: a pinned\n * commit is usually not the tip of any branch, so the alternative is a full\n * clone — 55s on a large plugin repo against ~1s here, on sources where two\n * thirds of the official Claude marketplace is pinned. Servers that refuse a\n * by-sha fetch (AWS CodeCommit and other non-GitHub hosts) fall back to a clone\n * deep enough to reach the commit, then check it out.\n */\nasync function fetchGitCommit(\n\turl: string,\n\tdest: string,\n\tsha: string,\n\tref?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n\tmkdirSync(dest, { recursive: true });\n\tconst init = await execGit([\"init\", \"--quiet\", dest]);\n\tif (init.code === 0) {\n\t\tconst remote = await execGit([\"-C\", dest, \"remote\", \"add\", \"origin\", url]);\n\t\tif (remote.code === 0) {\n\t\t\tconst fetched = await execGit([\"-C\", dest, \"fetch\", \"--quiet\", \"--depth\", \"1\", \"origin\", sha]);\n\t\t\tif (fetched.code === 0) {\n\t\t\t\tconst checkout = await execGit([\"-C\", dest, \"checkout\", \"--quiet\", \"FETCH_HEAD\"]);\n\t\t\t\tif (checkout.code === 0) return checkout;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback: a full clone (shallow cannot reach an arbitrary commit), then the\n\t// pin. `ref` narrows it when the entry named one.\n\trmSync(dest, { recursive: true, force: true });\n\tconst cloned = ref\n\t\t? await execGit([\"clone\", \"--branch\", ref, \"--\", url, dest])\n\t\t: await execGit([\"clone\", \"--\", url, dest]);\n\tif (cloned.code !== 0) return cloned;\n\treturn execGit([\"-C\", dest, \"checkout\", \"--quiet\", sha]);\n}\n\n/**\n * Clone a git repository into `dest` at the requested pin.\n *\n * **`sha` outranks `ref`.** Both vendors define it that way (\"when both `ref`\n * and `sha` are set, the `sha` is the effective pin\"), and a marketplace that\n * ships both is stating the exact commit it vouches for — a tag can be moved or\n * re-pointed after the catalog pinned it, so preferring `ref` installed a\n * different commit than the index promised. It is not hypothetical: the official\n * catalog's `42crunch-api-security-testing` pins `30287f5` while its `v1.5.5`\n * tag now resolves to `faf5305`.\n *\n * Returns the git result; on failure `dest` may be partially created.\n */\nasync function cloneGitRepo(\n\turl: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ code: number; stdout: string; stderr: string }> {\n\tif (sha) return fetchGitCommit(url, dest, sha, ref);\n\tif (ref) return execGit([\"clone\", \"--branch\", ref, \"--depth\", \"1\", \"--\", url, dest]);\n\treturn execGit([\"clone\", \"--depth\", \"1\", \"--\", url, dest]);\n}\n\n/**\n * Resolve a git-subdir source: clone the repo, checkout the requested ref/sha,\n * and copy the subdirectory to `dest`. Cleans up the temporary clone on failure.\n */\nasync function installGitSubdir(\n\turl: string,\n\tsubdir: string,\n\tdest: string,\n\tref?: string,\n\tsha?: string,\n): Promise<{ ok: true } | { ok: false; message: string }> {\n\tconst tmpDir = mkdtempSync(path.join(os.tmpdir(), \"hoo-plugin-clone-\"));\n\ttry {\n\t\tconst cloneRes = await cloneGitRepo(url, tmpDir, ref, sha);\n\t\tif (cloneRes.code !== 0) {\n\t\t\treturn { ok: false, message: `git clone failed: ${cloneRes.stderr || cloneRes.stdout}`.trim() };\n\t\t}\n\t\tconst src = path.resolve(tmpDir, subdir);\n\t\tif (!existsSync(src)) {\n\t\t\treturn { ok: false, message: `Plugin subdirectory not found in cloned repo: ${subdir}` };\n\t\t}\n\t\tcpSync(src, dest, { recursive: true });\n\t\treturn { ok: true };\n\t} finally {\n\t\trmSync(tmpDir, { recursive: true, force: true });\n\t}\n}\n\nexport interface InstallOptions {\n\t/**\n\t * Where the plugin lands. Defaults to `user`. The human path (`/plugin\n\t * install`) asks; the autonomous path passes the `pluginInstallScope` setting.\n\t */\n\tscope?: PluginInstallScope;\n}\n\nexport interface InstallOutcome {\n\tinstalled: boolean;\n\t/** Install destination directory (when installed). */\n\tdest?: string;\n\t/** The scope the plugin was installed at. */\n\tscope?: PluginInstallScope;\n\t/**\n\t * The plugin's own manifest id, when it differs from the marketplace entry\n\t * name it was installed by. This is the id `ListPlugins` reports, so the\n\t * caller can name both rather than leaving the model to reconcile them.\n\t */\n\tid?: string;\n\t/** Platforms the installed plugin supports (read back from disk). */\n\tsupportPlatform?: MarketplacePlatform[];\n\t/** Human-readable summary suitable for a tool result or notification. */\n\tmessage: string;\n}\n\n/** Capability surfaces hoocode can actually load out of an installed plugin. */\nfunction loadableCapabilities(plugin: NormalizedPlugin): string[] {\n\treturn [\n\t\tplugin.skillsDir && \"skills\",\n\t\tplugin.commandsDir && \"commands\",\n\t\tplugin.agentsDir && \"subagents\",\n\t\tplugin.themesDir && \"themes\",\n\t\tplugin.hooks && \"hooks\",\n\t\tplugin.mcpServers && Object.keys(plugin.mcpServers).length > 0 && \"MCP servers\",\n\t\tplugin.providers?.length && \"providers\",\n\t].filter((c): c is string => typeof c === \"string\");\n}\n\n/**\n * Install an available plugin by name into the consumption home for\n * `options.scope` — `~/.agents/plugins/` for `user` (the default), or\n * `<cwd>/.agents/plugins/` for `project`.\n *\n * `user` is the default for a reason worth keeping in view: a plugin is portable\n * and reusable across projects, so installing into the repo hides the capability\n * from every other checkout and puts content into `git status` that has nothing\n * to do with the change being made. `project` inverts both of those on purpose —\n * it is how a team pins a plugin to a repository — so it is chosen, never\n * defaulted into.\n *\n * Copies local sources; clones git sources. Transparent + reversible by\n * construction — the plugin lands in a named directory and\n * {@link uninstallPlugin} removes it from either scope. Callers activate the\n * result (live activation via AgentSession.activatePlugin, or a reload).\n */\nexport async function installAvailablePlugin(\n\tcwd: string,\n\tname: string,\n\tagentDir: string = getAgentDir(),\n\toptions: InstallOptions = {},\n): Promise<InstallOutcome> {\n\tconst found = findAvailablePlugin(cwd, name, agentDir);\n\tif (!found) return { installed: false, message: `Plugin \"${name}\" not found in any registered marketplace.` };\n\n\tconst scope = options.scope ?? \"user\";\n\tconst resolved = resolvePluginSource(found.source, found.marketplaceRoot);\n\tconst home = installHomeForScope(scope, cwd, agentDir);\n\tconst dest = path.join(home, sanitizeForDir(name));\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(home, { recursive: true });\n\n\tif (resolved.kind === \"local\") {\n\t\tif (!existsSync(resolved.path)) {\n\t\t\treturn { installed: false, message: `Plugin source path not found: ${resolved.path}` };\n\t\t}\n\t\tcpSync(resolved.path, dest, { recursive: true });\n\t} else if (resolved.kind === \"git\") {\n\t\tconst res = await cloneGitRepo(resolved.url, dest, resolved.ref, resolved.sha);\n\t\tif (res.code !== 0) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: `git clone failed: ${res.stderr || res.stdout}`.trim() };\n\t\t}\n\t} else if (resolved.kind === \"git-subdir\") {\n\t\tconst res = await installGitSubdir(resolved.url, resolved.path, dest, resolved.ref, resolved.sha);\n\t\tif (!res.ok) {\n\t\t\trmSync(dest, { recursive: true, force: true });\n\t\t\treturn { installed: false, message: res.message };\n\t\t}\n\t} else if (resolved.kind === \"npm\") {\n\t\treturn {\n\t\t\tinstalled: false,\n\t\t\tmessage:\n\t\t\t\t`\"${name}\" is published as an npm package (${resolved.spec}), a source type hoocode cannot install yet. ` +\n\t\t\t\t\"The entry is listed so you know it exists; install it through the vendor CLI, or ask the marketplace \" +\n\t\t\t\t\"for a git source.\",\n\t\t};\n\t} else {\n\t\treturn {\n\t\t\tinstalled: false,\n\t\t\tmessage:\n\t\t\t\t`\"${name}\" is published as a zip archive (${resolved.url}), a source type hoocode cannot install yet. ` +\n\t\t\t\t\"The entry is listed so you know it exists; install it through the vendor CLI, or ask the marketplace \" +\n\t\t\t\t\"for a git source.\",\n\t\t};\n\t}\n\n\t// Drop the clone's `.git`. Nothing reads it — an installed plugin is never\n\t// updated from its remote, by design (that is the supply-chain boundary) — and\n\t// keeping it actively breaks project scope: a repository nested inside the\n\t// user's repository is an embedded repo, so `git add` writes a gitlink instead\n\t// of the plugin's files and the install cannot actually be committed. Which is\n\t// the one thing project scope is for. Cheaper installs at user scope too.\n\trmSync(path.join(dest, \".git\"), { recursive: true, force: true });\n\n\tlet parsed = parsePluginDir(dest);\n\tif (!hasAnyManifest(dest)) {\n\t\t// Manifest-less plugin dir (some marketplaces index bare capability trees,\n\t\t// e.g. a plugin that is just a `skills/` directory). Such a directory now\n\t\t// *parses* on its own — the vendors make the manifest optional — but the\n\t\t// entry's name and description live only in the marketplace index, and the\n\t\t// derived id would be the sanitized directory name. Synthesize a native\n\t\t// manifest so both survive the install.\n\t\tconst manifestDir = path.join(dest, \".agents-plugin\");\n\t\tmkdirSync(manifestDir, { recursive: true });\n\t\twriteFileSync(\n\t\t\tpath.join(manifestDir, \"plugin.json\"),\n\t\t\t`${JSON.stringify(\n\t\t\t\t{ name: found.name, ...(found.description ? { description: found.description } : {}) },\n\t\t\t\tnull,\n\t\t\t\t2,\n\t\t\t)}\\n`,\n\t\t\t\"utf8\",\n\t\t);\n\t\tparsed = parsePluginDir(dest);\n\t}\n\tif (!parsed) {\n\t\trmSync(dest, { recursive: true, force: true });\n\t\treturn { installed: false, message: `Installed source for \"${name}\" has no recognizable plugin manifest.` };\n\t}\n\tconst capabilities = loadableCapabilities(parsed);\n\tlet message =\n\t\t`Installed \"${name}\" from marketplace \"${found.marketplaceName}\" ` +\n\t\t`(${parsed.supportPlatform.join(\", \")}) to ${dest} [${scope} scope]. ` +\n\t\t`Remove it with UninstallPlugin.`;\n\tif (scope === \"project\") {\n\t\t// Project scope puts the plugin in the working tree, which is the point —\n\t\t// but it is also a `git status` entry and, once committed, code that runs\n\t\t// for whoever clones next. Executable capabilities are the half that\n\t\t// matters: hoocode has no workspace-trust gate for `<cwd>/.agents/plugins`\n\t\t// (see docs/plugin-system-architecture.md §5.9), so a committed hook or MCP\n\t\t// server loads for collaborators without a prompt.\n\t\tconst executables = [parsed.hooks && \"hooks\", parsed.mcpServers && \"MCP servers\"].filter(Boolean);\n\t\tmessage += ` It is now part of the working tree — commit it to share it with collaborators.`;\n\t\tif (executables.length > 0) {\n\t\t\tmessage +=\n\t\t\t\t` It carries ${executables.join(\" and \")}, which run for anyone who clones the repository;` +\n\t\t\t\t` keep it out of version control if that is not what you want.`;\n\t\t}\n\t}\n\tif (parsed.id !== name) {\n\t\t// The entry name and the manifest name are allowed to differ, and the\n\t\t// difference is otherwise invisible until ListPlugins reports an id the\n\t\t// caller never asked for.\n\t\tmessage += ` Listed by ListPlugins as \"${parsed.id}\"; either name uninstalls it.`;\n\t}\n\tif (capabilities.length === 0) {\n\t\t// An install that adds nothing should not read as an install that worked.\n\t\t// Common in github/awesome-copilot, where most entries carry their content\n\t\t// in Copilot UI `extensions/` that hoocode has no analog for.\n\t\tconst surfaces = parsed.unsupportedSurfaces?.length\n\t\t\t? ` Unsupported surfaces present: ${parsed.unsupportedSurfaces.join(\", \")}.`\n\t\t\t: \"\";\n\t\tmessage +=\n\t\t\t\" Note: it contributes no capabilities hoocode can load\" +\n\t\t\t` (no skills, commands, subagents, hooks or MCP servers).${surfaces}`;\n\t}\n\treturn {\n\t\tinstalled: true,\n\t\tdest,\n\t\tscope,\n\t\t...(parsed.id !== name ? { id: parsed.id } : {}),\n\t\tsupportPlatform: parsed.supportPlatform,\n\t\tmessage,\n\t};\n}\n\nexport interface UninstallOutcome {\n\tremoved: boolean;\n\tmessage: string;\n}\n\n/** True when `target` is `root` or sits inside it. */\nfunction isUnderDir(target: string, root: string): boolean {\n\tconst normalized = path.resolve(root);\n\tif (path.resolve(target) === normalized) return true;\n\treturn path.resolve(target).startsWith(normalized.endsWith(path.sep) ? normalized : `${normalized}${path.sep}`);\n}\n\n/**\n * Remove an installed or authored plugin from every location it could occupy:\n * the consumption home, both production homes, and the legacy project-local\n * directories that older versions wrote into.\n *\n * `name` is matched two ways, because a plugin has two names: the marketplace\n * entry name it was installed under (which is what the directory is called) and\n * the manifest id `ListPlugins` reports. Resolving only the first left a plugin\n * whose names differ impossible to remove through the tool surface — the id the\n * model was shown was the one uninstall did not accept.\n *\n * Removal by id stays inside {@link pluginHomeRoots}, so a plugin discovered in\n * `<cwd>/.claude/skills` — repository content the team committed, which hoocode\n * did not install — is never deleted out of the working tree.\n */\nexport function uninstallPlugin(cwd: string, name: string, agentDir: string = getAgentDir()): UninstallOutcome {\n\tconst homes = pluginHomeRoots(cwd, agentDir);\n\tconst byManifestId = listInstalledPlugins(cwd, agentDir)\n\t\t.filter((p) => p.id === name)\n\t\t.map((p) => p.root)\n\t\t.filter((root) => homes.some((home) => isUnderDir(root, home)));\n\tconst candidates = [...new Set([...candidatePluginDirs(cwd, name, agentDir), ...byManifestId])];\n\tconst present = candidates.filter((p) => existsSync(p));\n\tif (present.length === 0) return { removed: false, message: `Plugin \"${name}\" is not installed.` };\n\tfor (const p of present) rmSync(p, { recursive: true, force: true });\n\treturn { removed: true, message: `Removed \"${name}\".` };\n}\n"]}
|