@kolisachint/hoocode-agent 0.5.36 → 0.5.37

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.37] - 2026-08-25
4
+
5
+ ### Fixed
6
+
7
+ - The marketplace TTL tests no longer clone three real GitHub repositories on
8
+ every call. `refreshMarketplaces` has always meant the well-known indices as
9
+ well as the user-added ones, so a test covering a *local* git remote still
10
+ reached `github.com` three times per call — six to eight seconds a test
11
+ locally, and past the 30-second budget on a loaded CI runner. That is what
12
+ made this file the most reliable way to turn `main` red: a different test in
13
+ it timed out on each run, which is the signature of a clock rather than a
14
+ defect.
15
+
16
+ `ensureWellKnownMarketplaces` already documented a seam for exactly this — "a
17
+ check that needs the network is a check that rots" — but `refreshMarketplaces`
18
+ did not carry it up to its own callers. It does now, and the tests pass an
19
+ empty list, which is what their own docstring always claimed they did. The
20
+ three tests drop from 20.3s to 0.6s combined. Production behaviour is
21
+ unchanged: with no options the default is still the well-known list.
22
+
3
23
  ## [0.5.36] - 2026-08-25
4
24
 
5
25
  ### Added
@@ -79,7 +79,7 @@ export declare function ensureWellKnownMarketplaces(agentDir?: string, options?:
79
79
  * The explicit half of the freshness story, for when a user knows the upstream
80
80
  * changed and does not want to wait out the interval.
81
81
  */
82
- export declare function refreshMarketplaces(cwd: string, agentDir?: string): Promise<{
82
+ export declare function refreshMarketplaces(cwd: string, agentDir?: string, options?: EnsureMarketplacesOptions): Promise<{
83
83
  refreshed: string[];
84
84
  errors: string[];
85
85
  }>;
@@ -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\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; ref?: 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.\n\t//\n\t// Pinned to the `marketplace` branch, and that is load-bearing. On `main` a\n\t// `plugins/<name>/` directory holds only `plugin.json` and `README.md`; the\n\t// real content lives in the repository's top-level `agents/`, `skills/` and\n\t// `extensions/` trees and is named from the manifest under the repo's own\n\t// `extensions[\"com.github.awesome-copilot\"]` namespace. CI *materializes* those\n\t// references into each plugin directory and publishes the result on\n\t// `marketplace` (`eng/materialize-plugins.mjs`), which is the branch the\n\t// vendor's own `copilot plugin install` consumes. Cloning the default branch\n\t// installed the stub — every entry landed with no capabilities and said so,\n\t// which read as \"hoocode does not model this surface\" when it was really\n\t// \"hoocode fetched the wrong ref\".\n\t{ name: \"awesome-copilot\", url: \"https://github.com/github/awesome-copilot\", ref: \"marketplace\" },\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(\n\turl: string,\n\tdir: string,\n\tref?: string,\n): 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\t// The re-clone must land on the same ref the cache was created from, or a\n\t// pinned marketplace silently falls back to the default branch on its first\n\t// non-fast-forward refresh.\n\tconst cloned = await cloneGitRepo(url, target, ref);\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\n/**\n * Branch name a cached clone is sitting on, or undefined when it is detached or\n * unreadable (a corrupt cache answers \"not the ref I want\", which is correct).\n */\nasync function checkedOutRef(dir: string): Promise<string | undefined> {\n\tconst res = await execGit([\"-C\", dir, \"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n\tif (res.code !== 0) return undefined;\n\tconst name = res.stdout.trim();\n\treturn name && name !== \"HEAD\" ? name : undefined;\n}\n\n/** Whether `ref` is still a branch on the remote. False on any network failure. */\nasync function remoteHasRef(url: string, ref: string): Promise<boolean> {\n\tconst res = await execGit([\"ls-remote\", \"--heads\", \"--\", url, ref]);\n\treturn res.code === 0 && res.stdout.trim().length > 0;\n}\n\n/**\n * Whether {@link ensureWellKnownMarketplaces} would actually reach the network.\n *\n * Exists so an interactive caller can say \"Refreshing marketplace indices…\"\n * before a multi-second clone and stay silent when the answer is already on\n * disk. Deliberately does not probe the remote — the ref-mismatch case needs\n * network to detect, so this under-reports rather than paying for a round trip\n * to decide whether to print a line.\n */\nexport function wellKnownMarketplacesAreStale(agentDir: string = getAgentDir()): boolean {\n\treturn WELL_KNOWN_MARKETPLACES.some(\n\t\t(wk) => !existsSync(marketplaceCacheDir(wk.url, agentDir)) || isStale(agentDir, wk.url),\n\t);\n}\n\nexport interface EnsureMarketplacesOptions {\n\t/** Refresh regardless of the TTL. */\n\tforce?: boolean;\n\t/**\n\t * Marketplaces to ensure; defaults to {@link WELL_KNOWN_MARKETPLACES}.\n\t *\n\t * A seam, and a deliberate one. The ref-pinning rules below — discard a cache\n\t * on the wrong branch, fall back when a pin is retired, and do not loop\n\t * between those two — are only exercised against a remote whose branches can\n\t * be moved and deleted, which no real marketplace will do on demand. Without\n\t * this the whole path could only be checked by hand against GitHub, and a\n\t * check that needs the network is a check that rots.\n\t */\n\tmarketplaces?: ReadonlyArray<{ name: string; url: string; ref?: string }>;\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 *\n * The returned strings are notes for the caller to surface, not fatal errors —\n * a retired ref that fell back to the default branch reports here too, because\n * the consequence (entries that install with no capabilities) is otherwise\n * invisible and inexplicable.\n */\nexport async function ensureWellKnownMarketplaces(\n\tagentDir: string = getAgentDir(),\n\toptions: EnsureMarketplacesOptions = {},\n): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of options.marketplaces ?? WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tlet present = existsSync(dir);\n\t\t// A cache is keyed by URL, not by ref, so a marketplace that gains (or\n\t\t// changes) a pin finds a clone of the wrong branch sitting in its slot. A\n\t\t// `pull --ff-only` there would faithfully keep updating the wrong branch\n\t\t// forever, so the mismatch is resolved the only way it can be: discard and\n\t\t// clone the ref that is asked for.\n\t\t//\n\t\t// Guarded on the ref still existing upstream, which is not paranoia — it is\n\t\t// what stops a loop. When a pin is retired, the fallback below leaves a\n\t\t// default-branch clone in the slot; that clone mismatches the pin forever,\n\t\t// so an unguarded check would discard and re-download it on every single\n\t\t// run. Probing costs one `ls-remote` and only on the mismatch path.\n\t\tif (present && wk.ref && (await checkedOutRef(dir)) !== wk.ref && (await remoteHasRef(wk.url, wk.ref))) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\tpresent = false;\n\t\t}\n\t\tif (present && !options.force && !isStale(agentDir, wk.url)) continue;\n\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tlet res = present ? await refreshClone(wk.url, dir, wk.ref) : await cloneGitRepo(wk.url, dir, wk.ref);\n\t\t// A pinned ref that cannot be cloned is the one failure worth retrying\n\t\t// differently. The pin names someone else's distribution branch — for\n\t\t// awesome-copilot, a name held in a workflow env var — so it can be renamed\n\t\t// or retired without warning, and a cold cache would then lose that\n\t\t// marketplace outright rather than degrade. A warm cache already degrades\n\t\t// correctly (the stale copy is kept below, and it is *built* content, which\n\t\t// beats an unbuilt default branch), so this applies to the cold case only.\n\t\tlet degraded: string | undefined;\n\t\tif (res.code !== 0 && !present && wk.ref) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\tconst fallback = await cloneGitRepo(wk.url, dir);\n\t\t\tif (fallback.code === 0) {\n\t\t\t\tdegraded =\n\t\t\t\t\t`${wk.name}: branch \"${wk.ref}\" is unavailable, so the default branch was used instead. ` +\n\t\t\t\t\t\"Plugin entries there may be unbuilt and install with no capabilities.\";\n\t\t\t\tres = fallback;\n\t\t\t}\n\t\t}\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\tif (degraded) errors.push(degraded);\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\t// Not loaded at session start like the rest — a canvas has no passive half —\n\t\t// but it is unquestionably something the install brought, and reporting it\n\t\t// is what stops a canvas plugin reading as an install that did nothing.\n\t\tplugin.canvasExtensions?.length && \"canvases\",\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 (parsed.canvasExtensions?.length) {\n\t\t// A canvas is the one capability that is not loaded on activation: it has no\n\t\t// passive half, so it waits for someone to open it. Saying so here is the\n\t\t// difference between \"installed and inert\" and \"installed, run /canvas\".\n\t\tconst ids = parsed.canvasExtensions.map((extension) => extension.id).join(\", \");\n\t\tmessage += ` Canvases: ${ids} — open one with /canvas open <id>.`;\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\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, canvases, 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"]}
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; ref?: 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.\n\t//\n\t// Pinned to the `marketplace` branch, and that is load-bearing. On `main` a\n\t// `plugins/<name>/` directory holds only `plugin.json` and `README.md`; the\n\t// real content lives in the repository's top-level `agents/`, `skills/` and\n\t// `extensions/` trees and is named from the manifest under the repo's own\n\t// `extensions[\"com.github.awesome-copilot\"]` namespace. CI *materializes* those\n\t// references into each plugin directory and publishes the result on\n\t// `marketplace` (`eng/materialize-plugins.mjs`), which is the branch the\n\t// vendor's own `copilot plugin install` consumes. Cloning the default branch\n\t// installed the stub — every entry landed with no capabilities and said so,\n\t// which read as \"hoocode does not model this surface\" when it was really\n\t// \"hoocode fetched the wrong ref\".\n\t{ name: \"awesome-copilot\", url: \"https://github.com/github/awesome-copilot\", ref: \"marketplace\" },\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(\n\turl: string,\n\tdir: string,\n\tref?: string,\n): 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\t// The re-clone must land on the same ref the cache was created from, or a\n\t// pinned marketplace silently falls back to the default branch on its first\n\t// non-fast-forward refresh.\n\tconst cloned = await cloneGitRepo(url, target, ref);\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\n/**\n * Branch name a cached clone is sitting on, or undefined when it is detached or\n * unreadable (a corrupt cache answers \"not the ref I want\", which is correct).\n */\nasync function checkedOutRef(dir: string): Promise<string | undefined> {\n\tconst res = await execGit([\"-C\", dir, \"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n\tif (res.code !== 0) return undefined;\n\tconst name = res.stdout.trim();\n\treturn name && name !== \"HEAD\" ? name : undefined;\n}\n\n/** Whether `ref` is still a branch on the remote. False on any network failure. */\nasync function remoteHasRef(url: string, ref: string): Promise<boolean> {\n\tconst res = await execGit([\"ls-remote\", \"--heads\", \"--\", url, ref]);\n\treturn res.code === 0 && res.stdout.trim().length > 0;\n}\n\n/**\n * Whether {@link ensureWellKnownMarketplaces} would actually reach the network.\n *\n * Exists so an interactive caller can say \"Refreshing marketplace indices…\"\n * before a multi-second clone and stay silent when the answer is already on\n * disk. Deliberately does not probe the remote — the ref-mismatch case needs\n * network to detect, so this under-reports rather than paying for a round trip\n * to decide whether to print a line.\n */\nexport function wellKnownMarketplacesAreStale(agentDir: string = getAgentDir()): boolean {\n\treturn WELL_KNOWN_MARKETPLACES.some(\n\t\t(wk) => !existsSync(marketplaceCacheDir(wk.url, agentDir)) || isStale(agentDir, wk.url),\n\t);\n}\n\nexport interface EnsureMarketplacesOptions {\n\t/** Refresh regardless of the TTL. */\n\tforce?: boolean;\n\t/**\n\t * Marketplaces to ensure; defaults to {@link WELL_KNOWN_MARKETPLACES}.\n\t *\n\t * A seam, and a deliberate one. The ref-pinning rules below — discard a cache\n\t * on the wrong branch, fall back when a pin is retired, and do not loop\n\t * between those two — are only exercised against a remote whose branches can\n\t * be moved and deleted, which no real marketplace will do on demand. Without\n\t * this the whole path could only be checked by hand against GitHub, and a\n\t * check that needs the network is a check that rots.\n\t */\n\tmarketplaces?: ReadonlyArray<{ name: string; url: string; ref?: string }>;\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 *\n * The returned strings are notes for the caller to surface, not fatal errors —\n * a retired ref that fell back to the default branch reports here too, because\n * the consequence (entries that install with no capabilities) is otherwise\n * invisible and inexplicable.\n */\nexport async function ensureWellKnownMarketplaces(\n\tagentDir: string = getAgentDir(),\n\toptions: EnsureMarketplacesOptions = {},\n): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of options.marketplaces ?? WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tlet present = existsSync(dir);\n\t\t// A cache is keyed by URL, not by ref, so a marketplace that gains (or\n\t\t// changes) a pin finds a clone of the wrong branch sitting in its slot. A\n\t\t// `pull --ff-only` there would faithfully keep updating the wrong branch\n\t\t// forever, so the mismatch is resolved the only way it can be: discard and\n\t\t// clone the ref that is asked for.\n\t\t//\n\t\t// Guarded on the ref still existing upstream, which is not paranoia — it is\n\t\t// what stops a loop. When a pin is retired, the fallback below leaves a\n\t\t// default-branch clone in the slot; that clone mismatches the pin forever,\n\t\t// so an unguarded check would discard and re-download it on every single\n\t\t// run. Probing costs one `ls-remote` and only on the mismatch path.\n\t\tif (present && wk.ref && (await checkedOutRef(dir)) !== wk.ref && (await remoteHasRef(wk.url, wk.ref))) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\tpresent = false;\n\t\t}\n\t\tif (present && !options.force && !isStale(agentDir, wk.url)) continue;\n\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tlet res = present ? await refreshClone(wk.url, dir, wk.ref) : await cloneGitRepo(wk.url, dir, wk.ref);\n\t\t// A pinned ref that cannot be cloned is the one failure worth retrying\n\t\t// differently. The pin names someone else's distribution branch — for\n\t\t// awesome-copilot, a name held in a workflow env var — so it can be renamed\n\t\t// or retired without warning, and a cold cache would then lose that\n\t\t// marketplace outright rather than degrade. A warm cache already degrades\n\t\t// correctly (the stale copy is kept below, and it is *built* content, which\n\t\t// beats an unbuilt default branch), so this applies to the cold case only.\n\t\tlet degraded: string | undefined;\n\t\tif (res.code !== 0 && !present && wk.ref) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\tconst fallback = await cloneGitRepo(wk.url, dir);\n\t\t\tif (fallback.code === 0) {\n\t\t\t\tdegraded =\n\t\t\t\t\t`${wk.name}: branch \"${wk.ref}\" is unavailable, so the default branch was used instead. ` +\n\t\t\t\t\t\"Plugin entries there may be unbuilt and install with no capabilities.\";\n\t\t\t\tres = fallback;\n\t\t\t}\n\t\t}\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\tif (degraded) errors.push(degraded);\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\toptions: EnsureMarketplacesOptions = {},\n): Promise<{ refreshed: string[]; errors: string[] }> {\n\t// The same seam `ensureWellKnownMarketplaces` documents, carried up to the\n\t// caller that needs it. Without it a test covering *user-added* marketplaces\n\t// still cloned three real GitHub repositories on every call, because\n\t// refreshing \"the marketplaces\" has always meant the well-known list too —\n\t// which is a check that needs the network, and so a check that rots.\n\tconst wellKnown = options.marketplaces ?? WELL_KNOWN_MARKETPLACES;\n\tconst errors = await ensureWellKnownMarketplaces(agentDir, { ...options, force: true });\n\tconst refreshed = wellKnown.filter((wk) => existsSync(marketplaceCacheDir(wk.url, agentDir))).map((wk) => wk.name);\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 || wellKnown.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\t// Not loaded at session start like the rest — a canvas has no passive half —\n\t\t// but it is unquestionably something the install brought, and reporting it\n\t\t// is what stops a canvas plugin reading as an install that did nothing.\n\t\tplugin.canvasExtensions?.length && \"canvases\",\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 (parsed.canvasExtensions?.length) {\n\t\t// A canvas is the one capability that is not loaded on activation: it has no\n\t\t// passive half, so it waits for someone to open it. Saying so here is the\n\t\t// difference between \"installed and inert\" and \"installed, run /canvas\".\n\t\tconst ids = parsed.canvasExtensions.map((extension) => extension.id).join(\", \");\n\t\tmessage += ` Canvases: ${ids} — open one with /canvas open <id>.`;\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\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, canvases, 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"]}
@@ -249,14 +249,20 @@ export async function ensureWellKnownMarketplaces(agentDir = getAgentDir(), opti
249
249
  * The explicit half of the freshness story, for when a user knows the upstream
250
250
  * changed and does not want to wait out the interval.
251
251
  */
252
- export async function refreshMarketplaces(cwd, agentDir = getAgentDir()) {
253
- const errors = await ensureWellKnownMarketplaces(agentDir, { force: true });
254
- const refreshed = WELL_KNOWN_MARKETPLACES.filter((wk) => existsSync(marketplaceCacheDir(wk.url, agentDir))).map((wk) => wk.name);
252
+ export async function refreshMarketplaces(cwd, agentDir = getAgentDir(), options = {}) {
253
+ // The same seam `ensureWellKnownMarketplaces` documents, carried up to the
254
+ // caller that needs it. Without it a test covering *user-added* marketplaces
255
+ // still cloned three real GitHub repositories on every call, because
256
+ // refreshing "the marketplaces" has always meant the well-known list too —
257
+ // which is a check that needs the network, and so a check that rots.
258
+ const wellKnown = options.marketplaces ?? WELL_KNOWN_MARKETPLACES;
259
+ const errors = await ensureWellKnownMarketplaces(agentDir, { ...options, force: true });
260
+ const refreshed = wellKnown.filter((wk) => existsSync(marketplaceCacheDir(wk.url, agentDir))).map((wk) => wk.name);
255
261
  // User-added marketplaces are cached the same way when they came from git; a
256
262
  // local-path marketplace is read in place and has nothing to refresh.
257
263
  for (const record of readMarketplaceRecords(cwd, agentDir)) {
258
264
  const isCachedClone = record.dir.startsWith(marketplaceCacheRoot(agentDir));
259
- if (!isCachedClone || WELL_KNOWN_MARKETPLACES.some((wk) => wk.url === record.location))
265
+ if (!isCachedClone || wellKnown.some((wk) => wk.url === record.location))
260
266
  continue;
261
267
  const res = await refreshClone(record.location, record.dir);
262
268
  if (res.code !== 0)
@@ -1 +1 @@
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; ref?: 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.\n\t//\n\t// Pinned to the `marketplace` branch, and that is load-bearing. On `main` a\n\t// `plugins/<name>/` directory holds only `plugin.json` and `README.md`; the\n\t// real content lives in the repository's top-level `agents/`, `skills/` and\n\t// `extensions/` trees and is named from the manifest under the repo's own\n\t// `extensions[\"com.github.awesome-copilot\"]` namespace. CI *materializes* those\n\t// references into each plugin directory and publishes the result on\n\t// `marketplace` (`eng/materialize-plugins.mjs`), which is the branch the\n\t// vendor's own `copilot plugin install` consumes. Cloning the default branch\n\t// installed the stub — every entry landed with no capabilities and said so,\n\t// which read as \"hoocode does not model this surface\" when it was really\n\t// \"hoocode fetched the wrong ref\".\n\t{ name: \"awesome-copilot\", url: \"https://github.com/github/awesome-copilot\", ref: \"marketplace\" },\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(\n\turl: string,\n\tdir: string,\n\tref?: string,\n): 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\t// The re-clone must land on the same ref the cache was created from, or a\n\t// pinned marketplace silently falls back to the default branch on its first\n\t// non-fast-forward refresh.\n\tconst cloned = await cloneGitRepo(url, target, ref);\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\n/**\n * Branch name a cached clone is sitting on, or undefined when it is detached or\n * unreadable (a corrupt cache answers \"not the ref I want\", which is correct).\n */\nasync function checkedOutRef(dir: string): Promise<string | undefined> {\n\tconst res = await execGit([\"-C\", dir, \"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n\tif (res.code !== 0) return undefined;\n\tconst name = res.stdout.trim();\n\treturn name && name !== \"HEAD\" ? name : undefined;\n}\n\n/** Whether `ref` is still a branch on the remote. False on any network failure. */\nasync function remoteHasRef(url: string, ref: string): Promise<boolean> {\n\tconst res = await execGit([\"ls-remote\", \"--heads\", \"--\", url, ref]);\n\treturn res.code === 0 && res.stdout.trim().length > 0;\n}\n\n/**\n * Whether {@link ensureWellKnownMarketplaces} would actually reach the network.\n *\n * Exists so an interactive caller can say \"Refreshing marketplace indices…\"\n * before a multi-second clone and stay silent when the answer is already on\n * disk. Deliberately does not probe the remote — the ref-mismatch case needs\n * network to detect, so this under-reports rather than paying for a round trip\n * to decide whether to print a line.\n */\nexport function wellKnownMarketplacesAreStale(agentDir: string = getAgentDir()): boolean {\n\treturn WELL_KNOWN_MARKETPLACES.some(\n\t\t(wk) => !existsSync(marketplaceCacheDir(wk.url, agentDir)) || isStale(agentDir, wk.url),\n\t);\n}\n\nexport interface EnsureMarketplacesOptions {\n\t/** Refresh regardless of the TTL. */\n\tforce?: boolean;\n\t/**\n\t * Marketplaces to ensure; defaults to {@link WELL_KNOWN_MARKETPLACES}.\n\t *\n\t * A seam, and a deliberate one. The ref-pinning rules below — discard a cache\n\t * on the wrong branch, fall back when a pin is retired, and do not loop\n\t * between those two — are only exercised against a remote whose branches can\n\t * be moved and deleted, which no real marketplace will do on demand. Without\n\t * this the whole path could only be checked by hand against GitHub, and a\n\t * check that needs the network is a check that rots.\n\t */\n\tmarketplaces?: ReadonlyArray<{ name: string; url: string; ref?: string }>;\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 *\n * The returned strings are notes for the caller to surface, not fatal errors —\n * a retired ref that fell back to the default branch reports here too, because\n * the consequence (entries that install with no capabilities) is otherwise\n * invisible and inexplicable.\n */\nexport async function ensureWellKnownMarketplaces(\n\tagentDir: string = getAgentDir(),\n\toptions: EnsureMarketplacesOptions = {},\n): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of options.marketplaces ?? WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tlet present = existsSync(dir);\n\t\t// A cache is keyed by URL, not by ref, so a marketplace that gains (or\n\t\t// changes) a pin finds a clone of the wrong branch sitting in its slot. A\n\t\t// `pull --ff-only` there would faithfully keep updating the wrong branch\n\t\t// forever, so the mismatch is resolved the only way it can be: discard and\n\t\t// clone the ref that is asked for.\n\t\t//\n\t\t// Guarded on the ref still existing upstream, which is not paranoia — it is\n\t\t// what stops a loop. When a pin is retired, the fallback below leaves a\n\t\t// default-branch clone in the slot; that clone mismatches the pin forever,\n\t\t// so an unguarded check would discard and re-download it on every single\n\t\t// run. Probing costs one `ls-remote` and only on the mismatch path.\n\t\tif (present && wk.ref && (await checkedOutRef(dir)) !== wk.ref && (await remoteHasRef(wk.url, wk.ref))) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\tpresent = false;\n\t\t}\n\t\tif (present && !options.force && !isStale(agentDir, wk.url)) continue;\n\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tlet res = present ? await refreshClone(wk.url, dir, wk.ref) : await cloneGitRepo(wk.url, dir, wk.ref);\n\t\t// A pinned ref that cannot be cloned is the one failure worth retrying\n\t\t// differently. The pin names someone else's distribution branch — for\n\t\t// awesome-copilot, a name held in a workflow env var — so it can be renamed\n\t\t// or retired without warning, and a cold cache would then lose that\n\t\t// marketplace outright rather than degrade. A warm cache already degrades\n\t\t// correctly (the stale copy is kept below, and it is *built* content, which\n\t\t// beats an unbuilt default branch), so this applies to the cold case only.\n\t\tlet degraded: string | undefined;\n\t\tif (res.code !== 0 && !present && wk.ref) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\tconst fallback = await cloneGitRepo(wk.url, dir);\n\t\t\tif (fallback.code === 0) {\n\t\t\t\tdegraded =\n\t\t\t\t\t`${wk.name}: branch \"${wk.ref}\" is unavailable, so the default branch was used instead. ` +\n\t\t\t\t\t\"Plugin entries there may be unbuilt and install with no capabilities.\";\n\t\t\t\tres = fallback;\n\t\t\t}\n\t\t}\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\tif (degraded) errors.push(degraded);\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\t// Not loaded at session start like the rest — a canvas has no passive half —\n\t\t// but it is unquestionably something the install brought, and reporting it\n\t\t// is what stops a canvas plugin reading as an install that did nothing.\n\t\tplugin.canvasExtensions?.length && \"canvases\",\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 (parsed.canvasExtensions?.length) {\n\t\t// A canvas is the one capability that is not loaded on activation: it has no\n\t\t// passive half, so it waits for someone to open it. Saying so here is the\n\t\t// difference between \"installed and inert\" and \"installed, run /canvas\".\n\t\tconst ids = parsed.canvasExtensions.map((extension) => extension.id).join(\", \");\n\t\tmessage += ` Canvases: ${ids} — open one with /canvas open <id>.`;\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\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, canvases, 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"]}
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; ref?: 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.\n\t//\n\t// Pinned to the `marketplace` branch, and that is load-bearing. On `main` a\n\t// `plugins/<name>/` directory holds only `plugin.json` and `README.md`; the\n\t// real content lives in the repository's top-level `agents/`, `skills/` and\n\t// `extensions/` trees and is named from the manifest under the repo's own\n\t// `extensions[\"com.github.awesome-copilot\"]` namespace. CI *materializes* those\n\t// references into each plugin directory and publishes the result on\n\t// `marketplace` (`eng/materialize-plugins.mjs`), which is the branch the\n\t// vendor's own `copilot plugin install` consumes. Cloning the default branch\n\t// installed the stub — every entry landed with no capabilities and said so,\n\t// which read as \"hoocode does not model this surface\" when it was really\n\t// \"hoocode fetched the wrong ref\".\n\t{ name: \"awesome-copilot\", url: \"https://github.com/github/awesome-copilot\", ref: \"marketplace\" },\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(\n\turl: string,\n\tdir: string,\n\tref?: string,\n): 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\t// The re-clone must land on the same ref the cache was created from, or a\n\t// pinned marketplace silently falls back to the default branch on its first\n\t// non-fast-forward refresh.\n\tconst cloned = await cloneGitRepo(url, target, ref);\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\n/**\n * Branch name a cached clone is sitting on, or undefined when it is detached or\n * unreadable (a corrupt cache answers \"not the ref I want\", which is correct).\n */\nasync function checkedOutRef(dir: string): Promise<string | undefined> {\n\tconst res = await execGit([\"-C\", dir, \"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n\tif (res.code !== 0) return undefined;\n\tconst name = res.stdout.trim();\n\treturn name && name !== \"HEAD\" ? name : undefined;\n}\n\n/** Whether `ref` is still a branch on the remote. False on any network failure. */\nasync function remoteHasRef(url: string, ref: string): Promise<boolean> {\n\tconst res = await execGit([\"ls-remote\", \"--heads\", \"--\", url, ref]);\n\treturn res.code === 0 && res.stdout.trim().length > 0;\n}\n\n/**\n * Whether {@link ensureWellKnownMarketplaces} would actually reach the network.\n *\n * Exists so an interactive caller can say \"Refreshing marketplace indices…\"\n * before a multi-second clone and stay silent when the answer is already on\n * disk. Deliberately does not probe the remote — the ref-mismatch case needs\n * network to detect, so this under-reports rather than paying for a round trip\n * to decide whether to print a line.\n */\nexport function wellKnownMarketplacesAreStale(agentDir: string = getAgentDir()): boolean {\n\treturn WELL_KNOWN_MARKETPLACES.some(\n\t\t(wk) => !existsSync(marketplaceCacheDir(wk.url, agentDir)) || isStale(agentDir, wk.url),\n\t);\n}\n\nexport interface EnsureMarketplacesOptions {\n\t/** Refresh regardless of the TTL. */\n\tforce?: boolean;\n\t/**\n\t * Marketplaces to ensure; defaults to {@link WELL_KNOWN_MARKETPLACES}.\n\t *\n\t * A seam, and a deliberate one. The ref-pinning rules below — discard a cache\n\t * on the wrong branch, fall back when a pin is retired, and do not loop\n\t * between those two — are only exercised against a remote whose branches can\n\t * be moved and deleted, which no real marketplace will do on demand. Without\n\t * this the whole path could only be checked by hand against GitHub, and a\n\t * check that needs the network is a check that rots.\n\t */\n\tmarketplaces?: ReadonlyArray<{ name: string; url: string; ref?: string }>;\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 *\n * The returned strings are notes for the caller to surface, not fatal errors —\n * a retired ref that fell back to the default branch reports here too, because\n * the consequence (entries that install with no capabilities) is otherwise\n * invisible and inexplicable.\n */\nexport async function ensureWellKnownMarketplaces(\n\tagentDir: string = getAgentDir(),\n\toptions: EnsureMarketplacesOptions = {},\n): Promise<string[]> {\n\tconst errors: string[] = [];\n\tfor (const wk of options.marketplaces ?? WELL_KNOWN_MARKETPLACES) {\n\t\tconst dir = marketplaceCacheDir(wk.url, agentDir);\n\t\tlet present = existsSync(dir);\n\t\t// A cache is keyed by URL, not by ref, so a marketplace that gains (or\n\t\t// changes) a pin finds a clone of the wrong branch sitting in its slot. A\n\t\t// `pull --ff-only` there would faithfully keep updating the wrong branch\n\t\t// forever, so the mismatch is resolved the only way it can be: discard and\n\t\t// clone the ref that is asked for.\n\t\t//\n\t\t// Guarded on the ref still existing upstream, which is not paranoia — it is\n\t\t// what stops a loop. When a pin is retired, the fallback below leaves a\n\t\t// default-branch clone in the slot; that clone mismatches the pin forever,\n\t\t// so an unguarded check would discard and re-download it on every single\n\t\t// run. Probing costs one `ls-remote` and only on the mismatch path.\n\t\tif (present && wk.ref && (await checkedOutRef(dir)) !== wk.ref && (await remoteHasRef(wk.url, wk.ref))) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\tpresent = false;\n\t\t}\n\t\tif (present && !options.force && !isStale(agentDir, wk.url)) continue;\n\n\t\tmkdirSync(path.dirname(dir), { recursive: true });\n\t\tlet res = present ? await refreshClone(wk.url, dir, wk.ref) : await cloneGitRepo(wk.url, dir, wk.ref);\n\t\t// A pinned ref that cannot be cloned is the one failure worth retrying\n\t\t// differently. The pin names someone else's distribution branch — for\n\t\t// awesome-copilot, a name held in a workflow env var — so it can be renamed\n\t\t// or retired without warning, and a cold cache would then lose that\n\t\t// marketplace outright rather than degrade. A warm cache already degrades\n\t\t// correctly (the stale copy is kept below, and it is *built* content, which\n\t\t// beats an unbuilt default branch), so this applies to the cold case only.\n\t\tlet degraded: string | undefined;\n\t\tif (res.code !== 0 && !present && wk.ref) {\n\t\t\trmSync(dir, { recursive: true, force: true });\n\t\t\tconst fallback = await cloneGitRepo(wk.url, dir);\n\t\t\tif (fallback.code === 0) {\n\t\t\t\tdegraded =\n\t\t\t\t\t`${wk.name}: branch \"${wk.ref}\" is unavailable, so the default branch was used instead. ` +\n\t\t\t\t\t\"Plugin entries there may be unbuilt and install with no capabilities.\";\n\t\t\t\tres = fallback;\n\t\t\t}\n\t\t}\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\tif (degraded) errors.push(degraded);\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\toptions: EnsureMarketplacesOptions = {},\n): Promise<{ refreshed: string[]; errors: string[] }> {\n\t// The same seam `ensureWellKnownMarketplaces` documents, carried up to the\n\t// caller that needs it. Without it a test covering *user-added* marketplaces\n\t// still cloned three real GitHub repositories on every call, because\n\t// refreshing \"the marketplaces\" has always meant the well-known list too —\n\t// which is a check that needs the network, and so a check that rots.\n\tconst wellKnown = options.marketplaces ?? WELL_KNOWN_MARKETPLACES;\n\tconst errors = await ensureWellKnownMarketplaces(agentDir, { ...options, force: true });\n\tconst refreshed = wellKnown.filter((wk) => existsSync(marketplaceCacheDir(wk.url, agentDir))).map((wk) => wk.name);\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 || wellKnown.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\t// Not loaded at session start like the rest — a canvas has no passive half —\n\t\t// but it is unquestionably something the install brought, and reporting it\n\t\t// is what stops a canvas plugin reading as an install that did nothing.\n\t\tplugin.canvasExtensions?.length && \"canvases\",\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 (parsed.canvasExtensions?.length) {\n\t\t// A canvas is the one capability that is not loaded on activation: it has no\n\t\t// passive half, so it waits for someone to open it. Saying so here is the\n\t\t// difference between \"installed and inert\" and \"installed, run /canvas\".\n\t\tconst ids = parsed.canvasExtensions.map((extension) => extension.id).join(\", \");\n\t\tmessage += ` Canvases: ${ids} — open one with /canvas open <id>.`;\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\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, canvases, 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"]}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.3.36",
4
+ "version": "0.3.37",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.3.36",
4
+ "version": "0.3.37",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.3.36",
4
+ "version": "0.3.37",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.3.36",
4
+ "version": "0.3.37",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-agent",
3
- "version": "0.5.36",
3
+ "version": "0.5.37",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "hoocodeConfig": {
@@ -50,9 +50,9 @@
50
50
  "prepublishOnly": "npm run clean && npm run build"
51
51
  },
52
52
  "dependencies": {
53
- "@kolisachint/hoocode-agent-core": "^0.5.36",
54
- "@kolisachint/hoocode-ai": "^0.5.36",
55
- "@kolisachint/hoocode-tui": "^0.5.36",
53
+ "@kolisachint/hoocode-agent-core": "^0.5.37",
54
+ "@kolisachint/hoocode-ai": "^0.5.37",
55
+ "@kolisachint/hoocode-tui": "^0.5.37",
56
56
  "@silvia-odwyer/photon-node": "^0.3.4",
57
57
  "chalk": "^5.5.0",
58
58
  "cli-highlight": "^2.1.11",