@kolisachint/hoocode-agent 0.4.113 → 0.4.115
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 +16 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/plugins/authoring.d.ts +51 -0
- package/dist/core/extensions/plugins/authoring.d.ts.map +1 -0
- package/dist/core/extensions/plugins/authoring.js +91 -0
- package/dist/core/extensions/plugins/authoring.js.map +1 -0
- package/dist/core/extensions/plugins/default-marketplace/.agents-plugin/marketplace.json +12 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/.agents-plugin/plugin.json +6 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/commands/hello.md +5 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/skills/greet/SKILL.md +7 -0
- package/dist/core/extensions/plugins/formats/agents.d.ts +7 -0
- package/dist/core/extensions/plugins/formats/agents.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/agents.js +14 -0
- package/dist/core/extensions/plugins/formats/agents.js.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts +10 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.js +17 -0
- package/dist/core/extensions/plugins/formats/claude.js.map +1 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts +23 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/copilot.js +130 -0
- package/dist/core/extensions/plugins/formats/copilot.js.map +1 -0
- package/dist/core/extensions/plugins/formats/index.d.ts +41 -0
- package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/index.js +76 -0
- package/dist/core/extensions/plugins/formats/index.js.map +1 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +21 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.js +97 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts +44 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/shared.js +107 -0
- package/dist/core/extensions/plugins/formats/shared.js.map +1 -0
- package/dist/core/extensions/plugins/formats/types.d.ts +118 -0
- package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/types.js +19 -0
- package/dist/core/extensions/plugins/formats/types.js.map +1 -0
- package/dist/core/extensions/plugins/install.d.ts +73 -0
- package/dist/core/extensions/plugins/install.d.ts.map +1 -0
- package/dist/core/extensions/plugins/install.js +173 -0
- package/dist/core/extensions/plugins/install.js.map +1 -0
- package/dist/core/extensions/plugins/manifest.d.ts +25 -7
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/manifest.js +16 -91
- package/dist/core/extensions/plugins/manifest.js.map +1 -1
- package/dist/core/extensions/plugins/marketplace.d.ts +5 -4
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
- package/dist/core/extensions/plugins/marketplace.js +8 -13
- package/dist/core/extensions/plugins/marketplace.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +2 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +2 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +16 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +2 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/subagent-depth.d.ts +13 -0
- package/dist/core/subagent-depth.d.ts.map +1 -1
- package/dist/core/subagent-depth.js +15 -0
- package/dist/core/subagent-depth.js.map +1 -1
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +4 -1
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/tools/find.d.ts +8 -3
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +189 -122
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/index.d.ts +0 -4
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +2 -5
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/plugin-tool-names.d.ts +21 -0
- package/dist/core/tools/plugin-tool-names.d.ts.map +1 -0
- package/dist/core/tools/plugin-tool-names.js +31 -0
- package/dist/core/tools/plugin-tool-names.js.map +1 -0
- package/dist/core/tools/plugins.d.ts +49 -0
- package/dist/core/tools/plugins.d.ts.map +1 -0
- package/dist/core/tools/plugins.js +186 -0
- package/dist/core/tools/plugins.js.map +1 -0
- package/dist/core/tools/propose-plugin.d.ts +36 -0
- package/dist/core/tools/propose-plugin.d.ts.map +1 -0
- package/dist/core/tools/propose-plugin.js +219 -0
- package/dist/core/tools/propose-plugin.js.map +1 -0
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +29 -3
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/warm-subagent-pool.d.ts.map +1 -1
- package/dist/core/warm-subagent-pool.js +4 -1
- package/dist/core/warm-subagent-pool.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +7 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +23 -67
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/extensions/core/mcp-deferred.d.ts +28 -0
- package/dist/extensions/core/mcp-deferred.d.ts.map +1 -0
- package/dist/extensions/core/mcp-deferred.js +53 -0
- package/dist/extensions/core/mcp-deferred.js.map +1 -0
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +132 -78
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +19 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/brand.d.ts +41 -0
- package/dist/modes/interactive/brand.d.ts.map +1 -0
- package/dist/modes/interactive/brand.js +40 -0
- package/dist/modes/interactive/brand.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +102 -131
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +6 -5
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +13 -9
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +131 -86
- package/dist/modes/interactive/resource-display.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -1
- package/dist/modes/interactive/theme/light.json +1 -1
- package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-controller.js +15 -7
- package/dist/modes/interactive/voice/voice-controller.js.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts +27 -8
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.js +89 -27
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +8 -2
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +15 -6
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -5
- package/dist/core/tools/glob.d.ts +0 -42
- package/dist/core/tools/glob.d.ts.map +0 -1
- package/dist/core/tools/glob.js +0 -432
- package/dist/core/tools/glob.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/install.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAQH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EACN,KAAK,iBAAiB,EAItB,MAAM,kBAAkB,CAAC;AAE1B,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAUD,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED,4GAA0G;AAC1G,wBAAgB,wBAAwB,IAAI,iBAAiB,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAcvE;AAED,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,UAAU,EAAE,OAAO,GAAG,KAAK,GAAG,KAAK,CAAC;IACpC,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,eAAe,EAAE,mBAAmB,EAAE,CAAC;CACvC;AAED,sGAAsG;AACtG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CAqBnE;AAED,oDAAoD;AACpD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE1F;AAED,sEAAsE;AACtE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,gBAAgB,EAAE,CAEtG;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,OAAO,CAEtG;AAkBD,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACxC,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAsC/F;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAS3E","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, rmSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../../../config.js\";\nimport { defaultPluginDirs } from \"../loader.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\nimport { discoverPlugins } from \"./index.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { parsePluginDir } from \"./manifest.js\";\nimport {\n\ttype MarketplaceRecord,\n\tparseMarketplaceDir,\n\treadMarketplaceStore,\n\tresolvePluginSource,\n} from \"./marketplace.js\";\n\n/** `.agents/` is the primary, cross-vendor home for installed plugins and the added-marketplace registry. */\nexport function installedPluginsDir(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"plugins\");\n}\n\nfunction marketplaceStorePath(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"marketplaces.json\");\n}\n\nfunction legacyStorePath(cwd: string): string {\n\treturn path.join(cwd, \".hoocode\", \"marketplaces.json\");\n}\n\n/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */\nexport function sanitizeForDir(s: string): string {\n\treturn s.replace(/[^a-zA-Z0-9._-]+/g, \"_\").slice(0, 80);\n}\n\n/** Absolute path to the curated default marketplace bundled with hoocode. */\nexport function 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 * 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): MarketplaceRecord[] {\n\tconst records: MarketplaceRecord[] = [];\n\tconst def = defaultMarketplaceRecord();\n\tif (existsSync(def.dir)) records.push(def);\n\n\tconst primary = readMarketplaceStore(marketplaceStorePath(cwd));\n\tconst user =\n\t\tprimary.length > 0 || existsSync(marketplaceStorePath(cwd))\n\t\t\t? primary\n\t\t\t: readMarketplaceStore(legacyStorePath(cwd));\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, or `npm:<spec>`. */\n\tsource: string;\n\t/** Resolved source kind, for display and gating. */\n\tsourceKind: \"local\" | \"git\" | \"npm\";\n\tmarketplaceName: string;\n\tmarketplaceRoot: string;\n\t/** Platforms this entry targets (per-entry hint, else the marketplace's). */\n\tsupportPlatform: MarketplacePlatform[];\n}\n\n/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */\nexport function listAvailablePlugins(cwd: string): AvailablePlugin[] {\n\tconst out: AvailablePlugin[] = [];\n\tconst seen = new Set<string>();\n\tfor (const rec of readMarketplaceRecords(cwd)) {\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(cwd: string, name: string): AvailablePlugin | undefined {\n\treturn listAvailablePlugins(cwd).find((p) => p.name === name);\n}\n\n/** All currently installed plugins (project + global plugin dirs). */\nexport function listInstalledPlugins(cwd: string, agentDir: string = getAgentDir()): NormalizedPlugin[] {\n\treturn discoverPlugins(defaultPluginDirs(cwd, agentDir));\n}\n\n/** Whether a plugin with `name` (by id) is already installed. */\nexport function isPluginInstalled(cwd: string, name: string, agentDir: string = getAgentDir()): boolean {\n\treturn listInstalledPlugins(cwd, agentDir).some((p) => p.id === name);\n}\n\nfunction execGit(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", args);\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));\n\t\tchild.on(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t});\n}\n\nexport interface InstallOutcome {\n\tinstalled: boolean;\n\t/** Install destination directory (when installed). */\n\tdest?: string;\n\t/** Platforms the installed plugin supports (read back from disk). */\n\tsupportPlatform?: MarketplacePlatform[];\n\t/** Human-readable summary suitable for a tool result or notification. */\n\tmessage: string;\n}\n\n/**\n * Install an available plugin by name into `.agents/plugins`. Copies local\n * sources; clones git sources. Transparent + reversible by construction — the\n * plugin lands in a named directory and {@link uninstallPlugin} removes it.\n * Active after the next reload.\n */\nexport async function installAvailablePlugin(cwd: string, name: string): Promise<InstallOutcome> {\n\tconst found = findAvailablePlugin(cwd, name);\n\tif (!found) return { installed: false, message: `Plugin \"${name}\" not found in any registered marketplace.` };\n\n\tconst resolved = resolvePluginSource(found.source, found.marketplaceRoot);\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(name));\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(installedPluginsDir(cwd), { 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 execGit([\"clone\", \"--depth\", \"1\", resolved.url, dest]);\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 {\n\t\treturn { installed: false, message: `npm plugin sources are not supported yet (${resolved.spec}).` };\n\t}\n\n\tconst parsed = parsePluginDir(dest);\n\tif (!parsed) {\n\t\trmSync(dest, { recursive: true, force: true });\n\t\treturn { installed: false, message: `Installed source for \"${name}\" has no recognizable plugin manifest.` };\n\t}\n\treturn {\n\t\tinstalled: true,\n\t\tdest,\n\t\tsupportPlatform: parsed.supportPlatform,\n\t\tmessage:\n\t\t\t`Installed \"${name}\" from marketplace \"${found.marketplaceName}\" ` +\n\t\t\t`(${parsed.supportPlatform.join(\", \")}) to ${path.relative(cwd, dest) || dest}. ` +\n\t\t\t`Active after the next reload; remove it with UninstallPlugin.`,\n\t};\n}\n\nexport interface UninstallOutcome {\n\tremoved: boolean;\n\tmessage: string;\n}\n\n/** Remove an installed plugin from `.agents/plugins` (and the legacy `.hoocode/plugins`). */\nexport function uninstallPlugin(cwd: string, name: string): UninstallOutcome {\n\tconst candidates = [\n\t\tpath.join(installedPluginsDir(cwd), sanitizeForDir(name)),\n\t\tpath.join(cwd, \".hoocode\", \"plugins\", sanitizeForDir(name)),\n\t];\n\tconst present = candidates.filter((p) => existsSync(p));\n\tif (present.length === 0) return { removed: false, message: `Plugin \"${name}\" is not installed.` };\n\tfor (const p of present) rmSync(p, { recursive: true, force: true });\n\treturn { removed: true, message: `Removed \"${name}\". Effective after the next reload.` };\n}\n"]}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin install/uninstall/discovery — the shared engine behind both the
|
|
3
|
+
* `/plugin` slash command (human-driven) and the model-facing lifecycle tools
|
|
4
|
+
* (`SearchPlugins`, `InstallPlugin`, ...). Keeping it in one place means the two
|
|
5
|
+
* surfaces can never drift on where plugins live or how sources resolve.
|
|
6
|
+
*
|
|
7
|
+
* Trust model (see docs/plugin-system-spec.md): *adding* a marketplace is the
|
|
8
|
+
* human trust boundary; *installing* from an already-trusted marketplace is the
|
|
9
|
+
* model's discretion (package-manager model), and must stay transparent and
|
|
10
|
+
* reversible. This module performs the mechanical install/remove; the gating
|
|
11
|
+
* (announce, injection carve-out) lives with the callers.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { getAgentDir } from "../../../config.js";
|
|
18
|
+
import { defaultPluginDirs } from "../loader.js";
|
|
19
|
+
import { discoverPlugins } from "./index.js";
|
|
20
|
+
import { parsePluginDir } from "./manifest.js";
|
|
21
|
+
import { parseMarketplaceDir, readMarketplaceStore, resolvePluginSource, } from "./marketplace.js";
|
|
22
|
+
/** `.agents/` is the primary, cross-vendor home for installed plugins and the added-marketplace registry. */
|
|
23
|
+
export function installedPluginsDir(cwd) {
|
|
24
|
+
return path.join(cwd, ".agents", "plugins");
|
|
25
|
+
}
|
|
26
|
+
function marketplaceStorePath(cwd) {
|
|
27
|
+
return path.join(cwd, ".agents", "marketplaces.json");
|
|
28
|
+
}
|
|
29
|
+
function legacyStorePath(cwd) {
|
|
30
|
+
return path.join(cwd, ".hoocode", "marketplaces.json");
|
|
31
|
+
}
|
|
32
|
+
/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */
|
|
33
|
+
export function sanitizeForDir(s) {
|
|
34
|
+
return s.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 80);
|
|
35
|
+
}
|
|
36
|
+
/** Absolute path to the curated default marketplace bundled with hoocode. */
|
|
37
|
+
export function defaultMarketplaceDir() {
|
|
38
|
+
return fileURLToPath(new URL("./default-marketplace/", import.meta.url));
|
|
39
|
+
}
|
|
40
|
+
/** The default marketplace record — always present so source-level trust is meaningful out of the box. */
|
|
41
|
+
export function defaultMarketplaceRecord() {
|
|
42
|
+
return { location: "hoocode-default", dir: defaultMarketplaceDir() };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* All marketplace records in effect: the bundled default first (curated,
|
|
46
|
+
* trusted), then user-added ones from `.agents/` (falling back to legacy
|
|
47
|
+
* `.hoocode/`). Deduplicated by directory.
|
|
48
|
+
*/
|
|
49
|
+
export function readMarketplaceRecords(cwd) {
|
|
50
|
+
const records = [];
|
|
51
|
+
const def = defaultMarketplaceRecord();
|
|
52
|
+
if (existsSync(def.dir))
|
|
53
|
+
records.push(def);
|
|
54
|
+
const primary = readMarketplaceStore(marketplaceStorePath(cwd));
|
|
55
|
+
const user = primary.length > 0 || existsSync(marketplaceStorePath(cwd))
|
|
56
|
+
? primary
|
|
57
|
+
: readMarketplaceStore(legacyStorePath(cwd));
|
|
58
|
+
for (const r of user) {
|
|
59
|
+
if (!records.some((x) => x.dir === r.dir))
|
|
60
|
+
records.push(r);
|
|
61
|
+
}
|
|
62
|
+
return records;
|
|
63
|
+
}
|
|
64
|
+
/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */
|
|
65
|
+
export function listAvailablePlugins(cwd) {
|
|
66
|
+
const out = [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const rec of readMarketplaceRecords(cwd)) {
|
|
69
|
+
const market = parseMarketplaceDir(rec.dir);
|
|
70
|
+
if (!market)
|
|
71
|
+
continue;
|
|
72
|
+
for (const entry of market.plugins) {
|
|
73
|
+
if (seen.has(entry.name))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(entry.name);
|
|
76
|
+
out.push({
|
|
77
|
+
name: entry.name,
|
|
78
|
+
description: entry.description,
|
|
79
|
+
source: entry.source,
|
|
80
|
+
sourceKind: resolvePluginSource(entry.source, market.root).kind,
|
|
81
|
+
marketplaceName: market.name,
|
|
82
|
+
marketplaceRoot: market.root,
|
|
83
|
+
supportPlatform: entry.supportPlatform ?? market.supportPlatform,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/** Find a single available plugin by exact name. */
|
|
90
|
+
export function findAvailablePlugin(cwd, name) {
|
|
91
|
+
return listAvailablePlugins(cwd).find((p) => p.name === name);
|
|
92
|
+
}
|
|
93
|
+
/** All currently installed plugins (project + global plugin dirs). */
|
|
94
|
+
export function listInstalledPlugins(cwd, agentDir = getAgentDir()) {
|
|
95
|
+
return discoverPlugins(defaultPluginDirs(cwd, agentDir));
|
|
96
|
+
}
|
|
97
|
+
/** Whether a plugin with `name` (by id) is already installed. */
|
|
98
|
+
export function isPluginInstalled(cwd, name, agentDir = getAgentDir()) {
|
|
99
|
+
return listInstalledPlugins(cwd, agentDir).some((p) => p.id === name);
|
|
100
|
+
}
|
|
101
|
+
function execGit(args) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const child = spawn("git", args);
|
|
104
|
+
let stdout = "";
|
|
105
|
+
let stderr = "";
|
|
106
|
+
child.stdout?.on("data", (d) => {
|
|
107
|
+
stdout += d.toString();
|
|
108
|
+
});
|
|
109
|
+
child.stderr?.on("data", (d) => {
|
|
110
|
+
stderr += d.toString();
|
|
111
|
+
});
|
|
112
|
+
child.on("error", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));
|
|
113
|
+
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Install an available plugin by name into `.agents/plugins`. Copies local
|
|
118
|
+
* sources; clones git sources. Transparent + reversible by construction — the
|
|
119
|
+
* plugin lands in a named directory and {@link uninstallPlugin} removes it.
|
|
120
|
+
* Active after the next reload.
|
|
121
|
+
*/
|
|
122
|
+
export async function installAvailablePlugin(cwd, name) {
|
|
123
|
+
const found = findAvailablePlugin(cwd, name);
|
|
124
|
+
if (!found)
|
|
125
|
+
return { installed: false, message: `Plugin "${name}" not found in any registered marketplace.` };
|
|
126
|
+
const resolved = resolvePluginSource(found.source, found.marketplaceRoot);
|
|
127
|
+
const dest = path.join(installedPluginsDir(cwd), sanitizeForDir(name));
|
|
128
|
+
rmSync(dest, { recursive: true, force: true });
|
|
129
|
+
mkdirSync(installedPluginsDir(cwd), { recursive: true });
|
|
130
|
+
if (resolved.kind === "local") {
|
|
131
|
+
if (!existsSync(resolved.path)) {
|
|
132
|
+
return { installed: false, message: `Plugin source path not found: ${resolved.path}` };
|
|
133
|
+
}
|
|
134
|
+
cpSync(resolved.path, dest, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
else if (resolved.kind === "git") {
|
|
137
|
+
const res = await execGit(["clone", "--depth", "1", resolved.url, dest]);
|
|
138
|
+
if (res.code !== 0) {
|
|
139
|
+
rmSync(dest, { recursive: true, force: true });
|
|
140
|
+
return { installed: false, message: `git clone failed: ${res.stderr || res.stdout}`.trim() };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
return { installed: false, message: `npm plugin sources are not supported yet (${resolved.spec}).` };
|
|
145
|
+
}
|
|
146
|
+
const parsed = parsePluginDir(dest);
|
|
147
|
+
if (!parsed) {
|
|
148
|
+
rmSync(dest, { recursive: true, force: true });
|
|
149
|
+
return { installed: false, message: `Installed source for "${name}" has no recognizable plugin manifest.` };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
installed: true,
|
|
153
|
+
dest,
|
|
154
|
+
supportPlatform: parsed.supportPlatform,
|
|
155
|
+
message: `Installed "${name}" from marketplace "${found.marketplaceName}" ` +
|
|
156
|
+
`(${parsed.supportPlatform.join(", ")}) to ${path.relative(cwd, dest) || dest}. ` +
|
|
157
|
+
`Active after the next reload; remove it with UninstallPlugin.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** Remove an installed plugin from `.agents/plugins` (and the legacy `.hoocode/plugins`). */
|
|
161
|
+
export function uninstallPlugin(cwd, name) {
|
|
162
|
+
const candidates = [
|
|
163
|
+
path.join(installedPluginsDir(cwd), sanitizeForDir(name)),
|
|
164
|
+
path.join(cwd, ".hoocode", "plugins", sanitizeForDir(name)),
|
|
165
|
+
];
|
|
166
|
+
const present = candidates.filter((p) => existsSync(p));
|
|
167
|
+
if (present.length === 0)
|
|
168
|
+
return { removed: false, message: `Plugin "${name}" is not installed.` };
|
|
169
|
+
for (const p of present)
|
|
170
|
+
rmSync(p, { recursive: true, force: true });
|
|
171
|
+
return { removed: true, message: `Removed "${name}". Effective after the next reload.` };
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +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, rmSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getAgentDir } from \"../../../config.js\";\nimport { defaultPluginDirs } from \"../loader.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\nimport { discoverPlugins } from \"./index.js\";\nimport type { NormalizedPlugin } from \"./manifest.js\";\nimport { parsePluginDir } from \"./manifest.js\";\nimport {\n\ttype MarketplaceRecord,\n\tparseMarketplaceDir,\n\treadMarketplaceStore,\n\tresolvePluginSource,\n} from \"./marketplace.js\";\n\n/** `.agents/` is the primary, cross-vendor home for installed plugins and the added-marketplace registry. */\nexport function installedPluginsDir(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"plugins\");\n}\n\nfunction marketplaceStorePath(cwd: string): string {\n\treturn path.join(cwd, \".agents\", \"marketplaces.json\");\n}\n\nfunction legacyStorePath(cwd: string): string {\n\treturn path.join(cwd, \".hoocode\", \"marketplaces.json\");\n}\n\n/** Filesystem-safe directory name derived from a plugin name (matches the `/plugin` command). */\nexport function sanitizeForDir(s: string): string {\n\treturn s.replace(/[^a-zA-Z0-9._-]+/g, \"_\").slice(0, 80);\n}\n\n/** Absolute path to the curated default marketplace bundled with hoocode. */\nexport function 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 * 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): MarketplaceRecord[] {\n\tconst records: MarketplaceRecord[] = [];\n\tconst def = defaultMarketplaceRecord();\n\tif (existsSync(def.dir)) records.push(def);\n\n\tconst primary = readMarketplaceStore(marketplaceStorePath(cwd));\n\tconst user =\n\t\tprimary.length > 0 || existsSync(marketplaceStorePath(cwd))\n\t\t\t? primary\n\t\t\t: readMarketplaceStore(legacyStorePath(cwd));\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, or `npm:<spec>`. */\n\tsource: string;\n\t/** Resolved source kind, for display and gating. */\n\tsourceKind: \"local\" | \"git\" | \"npm\";\n\tmarketplaceName: string;\n\tmarketplaceRoot: string;\n\t/** Platforms this entry targets (per-entry hint, else the marketplace's). */\n\tsupportPlatform: MarketplacePlatform[];\n}\n\n/** Every plugin offered across all registered marketplaces (first marketplace wins on name clash). */\nexport function listAvailablePlugins(cwd: string): AvailablePlugin[] {\n\tconst out: AvailablePlugin[] = [];\n\tconst seen = new Set<string>();\n\tfor (const rec of readMarketplaceRecords(cwd)) {\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(cwd: string, name: string): AvailablePlugin | undefined {\n\treturn listAvailablePlugins(cwd).find((p) => p.name === name);\n}\n\n/** All currently installed plugins (project + global plugin dirs). */\nexport function listInstalledPlugins(cwd: string, agentDir: string = getAgentDir()): NormalizedPlugin[] {\n\treturn discoverPlugins(defaultPluginDirs(cwd, agentDir));\n}\n\n/** Whether a plugin with `name` (by id) is already installed. */\nexport function isPluginInstalled(cwd: string, name: string, agentDir: string = getAgentDir()): boolean {\n\treturn listInstalledPlugins(cwd, agentDir).some((p) => p.id === name);\n}\n\nfunction execGit(args: string[]): Promise<{ code: number; stdout: string; stderr: string }> {\n\treturn new Promise((resolve) => {\n\t\tconst child = spawn(\"git\", args);\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tchild.stdout?.on(\"data\", (d) => {\n\t\t\tstdout += d.toString();\n\t\t});\n\t\tchild.stderr?.on(\"data\", (d) => {\n\t\t\tstderr += d.toString();\n\t\t});\n\t\tchild.on(\"error\", (e) => resolve({ code: 1, stdout, stderr: stderr || String(e) }));\n\t\tchild.on(\"close\", (code) => resolve({ code: code ?? 1, stdout, stderr }));\n\t});\n}\n\nexport interface InstallOutcome {\n\tinstalled: boolean;\n\t/** Install destination directory (when installed). */\n\tdest?: string;\n\t/** Platforms the installed plugin supports (read back from disk). */\n\tsupportPlatform?: MarketplacePlatform[];\n\t/** Human-readable summary suitable for a tool result or notification. */\n\tmessage: string;\n}\n\n/**\n * Install an available plugin by name into `.agents/plugins`. Copies local\n * sources; clones git sources. Transparent + reversible by construction — the\n * plugin lands in a named directory and {@link uninstallPlugin} removes it.\n * Active after the next reload.\n */\nexport async function installAvailablePlugin(cwd: string, name: string): Promise<InstallOutcome> {\n\tconst found = findAvailablePlugin(cwd, name);\n\tif (!found) return { installed: false, message: `Plugin \"${name}\" not found in any registered marketplace.` };\n\n\tconst resolved = resolvePluginSource(found.source, found.marketplaceRoot);\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(name));\n\trmSync(dest, { recursive: true, force: true });\n\tmkdirSync(installedPluginsDir(cwd), { 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 execGit([\"clone\", \"--depth\", \"1\", resolved.url, dest]);\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 {\n\t\treturn { installed: false, message: `npm plugin sources are not supported yet (${resolved.spec}).` };\n\t}\n\n\tconst parsed = parsePluginDir(dest);\n\tif (!parsed) {\n\t\trmSync(dest, { recursive: true, force: true });\n\t\treturn { installed: false, message: `Installed source for \"${name}\" has no recognizable plugin manifest.` };\n\t}\n\treturn {\n\t\tinstalled: true,\n\t\tdest,\n\t\tsupportPlatform: parsed.supportPlatform,\n\t\tmessage:\n\t\t\t`Installed \"${name}\" from marketplace \"${found.marketplaceName}\" ` +\n\t\t\t`(${parsed.supportPlatform.join(\", \")}) to ${path.relative(cwd, dest) || dest}. ` +\n\t\t\t`Active after the next reload; remove it with UninstallPlugin.`,\n\t};\n}\n\nexport interface UninstallOutcome {\n\tremoved: boolean;\n\tmessage: string;\n}\n\n/** Remove an installed plugin from `.agents/plugins` (and the legacy `.hoocode/plugins`). */\nexport function uninstallPlugin(cwd: string, name: string): UninstallOutcome {\n\tconst candidates = [\n\t\tpath.join(installedPluginsDir(cwd), sanitizeForDir(name)),\n\t\tpath.join(cwd, \".hoocode\", \"plugins\", sanitizeForDir(name)),\n\t];\n\tconst present = candidates.filter((p) => existsSync(p));\n\tif (present.length === 0) return { removed: false, message: `Plugin \"${name}\" is not installed.` };\n\tfor (const p of present) rmSync(p, { recursive: true, force: true });\n\treturn { removed: true, message: `Removed \"${name}\". Effective after the next reload.` };\n}\n"]}
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin manifest
|
|
2
|
+
* Plugin manifest types and the format-dispatching parse entry point.
|
|
3
3
|
*
|
|
4
4
|
* A "plugin" is a directory that bundles agent capabilities (skills, commands,
|
|
5
5
|
* agents/modes, themes, providers, hooks, MCP servers) behind a single manifest.
|
|
6
|
-
*
|
|
6
|
+
* Several vendor formats are supported and parsed into one {@link NormalizedPlugin}:
|
|
7
7
|
*
|
|
8
|
-
* - `.agents-plugin/plugin.json`
|
|
9
|
-
* - `.claude-plugin/plugin.json`
|
|
8
|
+
* - `.agents-plugin/plugin.json` — native format (a strict superset)
|
|
9
|
+
* - `.claude-plugin/plugin.json` — Claude Code compatible format
|
|
10
|
+
* - `.github/copilot-plugin.json` — GitHub Copilot format (`.github/` layout)
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
+
* The per-format detect/parse/emit logic lives in {@link ./formats}, one adapter
|
|
13
|
+
* per vendor, behind the {@link PluginFormatAdapter} contract — so this module
|
|
14
|
+
* carries only the shared types and delegates directory parsing to the registry.
|
|
15
|
+
* When a directory carries more than one format, the highest-precedence one wins
|
|
16
|
+
* (native > Claude > Copilot; no merge).
|
|
12
17
|
*/
|
|
13
18
|
import type { ProviderConfig } from "../types.js";
|
|
19
|
+
import type { MarketplacePlatform } from "./formats/types.js";
|
|
14
20
|
/** Marker subdirectory + manifest filename for each supported format. */
|
|
15
21
|
export declare const NATIVE_MANIFEST_DIR = ".agents-plugin";
|
|
16
22
|
export declare const CLAUDE_MANIFEST_DIR = ".claude-plugin";
|
|
23
|
+
export declare const COPILOT_MANIFEST_DIR = ".github";
|
|
17
24
|
export declare const MANIFEST_FILE = "plugin.json";
|
|
25
|
+
export declare const COPILOT_MANIFEST_FILE = "copilot-plugin.json";
|
|
18
26
|
/** A single shell command invoked by a hook. */
|
|
19
27
|
export interface PluginHookCommand {
|
|
20
28
|
type?: "command";
|
|
@@ -49,8 +57,16 @@ export interface NormalizedPlugin {
|
|
|
49
57
|
root: string;
|
|
50
58
|
/** Absolute path to the parsed manifest file. */
|
|
51
59
|
manifestPath: string;
|
|
52
|
-
/** Which manifest format produced this plugin. */
|
|
53
|
-
format: "agents" | "claude";
|
|
60
|
+
/** Which manifest format produced this plugin (the precedence winner). */
|
|
61
|
+
format: "agents" | "claude" | "copilot";
|
|
62
|
+
/**
|
|
63
|
+
* Every platform this plugin directory offers. When a directory carries more
|
|
64
|
+
* than one format manifest (e.g. both `.claude-plugin/` and `.github/`), all
|
|
65
|
+
* are recorded here — the precedence winner's platform first — rather than
|
|
66
|
+
* hidden behind the single `format`. Always non-empty (at least `format`'s
|
|
67
|
+
* platform), mirroring the marketplace's `supportPlatform`.
|
|
68
|
+
*/
|
|
69
|
+
supportPlatform: MarketplacePlatform[];
|
|
54
70
|
/** Resolved capability directories (only set when they exist on disk). */
|
|
55
71
|
skillsDir?: string;
|
|
56
72
|
commandsDir?: string;
|
|
@@ -66,6 +82,8 @@ export interface NormalizedPlugin {
|
|
|
66
82
|
/**
|
|
67
83
|
* Parse and normalize a single plugin directory.
|
|
68
84
|
* Returns null if no recognized manifest is present or it is invalid.
|
|
85
|
+
*
|
|
86
|
+
* Delegates to the format registry; see {@link ./formats/index.ts}.
|
|
69
87
|
*/
|
|
70
88
|
export declare function parsePluginDir(root: string): NormalizedPlugin | null;
|
|
71
89
|
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/manifest.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,yEAAyE;AACzE,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,oBAAoB,YAAY,CAAC;AAC9C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAC3C,eAAO,MAAM,qBAAqB,wBAAwB,CAAC;AAE3D,gDAAgD;AAChD,MAAM,WAAW,iBAAiB;IACjC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACtC,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,EAAE,CAAC,CAAC;AAEzE,4FAA4F;AAC5F,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;CACvB;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAChC,2CAA2C;IAC3C,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACxC;;;;;;OAMG;IACH,eAAe,EAAE,mBAAmB,EAAE,CAAC;IACvC,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,oGAAoG;IACpG,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,0CAA0C;IAC1C,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAEpE","sourcesContent":["/**\n * Plugin manifest types and the format-dispatching parse entry point.\n *\n * A \"plugin\" is a directory that bundles agent capabilities (skills, commands,\n * agents/modes, themes, providers, hooks, MCP servers) behind a single manifest.\n * Several vendor formats are supported and parsed into one {@link NormalizedPlugin}:\n *\n * - `.agents-plugin/plugin.json` — native format (a strict superset)\n * - `.claude-plugin/plugin.json` — Claude Code compatible format\n * - `.github/copilot-plugin.json` — GitHub Copilot format (`.github/` layout)\n *\n * The per-format detect/parse/emit logic lives in {@link ./formats}, one adapter\n * per vendor, behind the {@link PluginFormatAdapter} contract — so this module\n * carries only the shared types and delegates directory parsing to the registry.\n * When a directory carries more than one format, the highest-precedence one wins\n * (native > Claude > Copilot; no merge).\n */\n\nimport type { ProviderConfig } from \"../types.js\";\nimport { parsePluginWithFormats } from \"./formats/index.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/** Marker subdirectory + manifest filename for each supported format. */\nexport const NATIVE_MANIFEST_DIR = \".agents-plugin\";\nexport const CLAUDE_MANIFEST_DIR = \".claude-plugin\";\nexport const COPILOT_MANIFEST_DIR = \".github\";\nexport const MANIFEST_FILE = \"plugin.json\";\nexport const COPILOT_MANIFEST_FILE = \"copilot-plugin.json\";\n\n/** A single shell command invoked by a hook. */\nexport interface PluginHookCommand {\n\ttype?: \"command\";\n\tcommand: string;\n\t/** Timeout in seconds (Claude Code convention). */\n\ttimeout?: number;\n}\n\n/** A matcher group: run these commands when `matcher` matches. */\nexport interface PluginHookMatcherGroup {\n\t/** Regex string matched against the tool name. Empty / \"*\" matches everything. */\n\tmatcher?: string;\n\thooks: PluginHookCommand[];\n}\n\n/**\n * Hook event map keyed by Claude Code event name\n * (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ...).\n */\nexport type PluginHooksConfig = Record<string, PluginHookMatcherGroup[]>;\n\n/** A provider contributed by a native plugin (config-only; uses a built-in API handler). */\nexport interface PluginProvider {\n\tname: string;\n\tconfig: ProviderConfig;\n}\n\n/** Normalized, format-agnostic representation of a plugin. */\nexport interface NormalizedPlugin {\n\t/** Stable identifier (manifest `name`). */\n\tid: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string;\n\t/** Absolute path to the plugin root directory. */\n\troot: string;\n\t/** Absolute path to the parsed manifest file. */\n\tmanifestPath: string;\n\t/** Which manifest format produced this plugin (the precedence winner). */\n\tformat: \"agents\" | \"claude\" | \"copilot\";\n\t/**\n\t * Every platform this plugin directory offers. When a directory carries more\n\t * than one format manifest (e.g. both `.claude-plugin/` and `.github/`), all\n\t * are recorded here — the precedence winner's platform first — rather than\n\t * hidden behind the single `format`. Always non-empty (at least `format`'s\n\t * platform), mirroring the marketplace's `supportPlatform`.\n\t */\n\tsupportPlatform: MarketplacePlatform[];\n\t/** Resolved capability directories (only set when they exist on disk). */\n\tskillsDir?: string;\n\tcommandsDir?: string;\n\tagentsDir?: string;\n\tthemesDir?: string;\n\t/** Parsed hooks (from `hooks/hooks.json` or inline `hooks`). */\n\thooks?: PluginHooksConfig;\n\t/** Parsed MCP servers (from `.mcp.json` or inline `mcpServers`); connected via the MCP registry. */\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only: providers to register. */\n\tproviders?: PluginProvider[];\n}\n\n/**\n * Parse and normalize a single plugin directory.\n * Returns null if no recognized manifest is present or it is invalid.\n *\n * Delegates to the format registry; see {@link ./formats/index.ts}.\n */\nexport function parsePluginDir(root: string): NormalizedPlugin | null {\n\treturn parsePluginWithFormats(root);\n}\n"]}
|
|
@@ -1,109 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin manifest
|
|
2
|
+
* Plugin manifest types and the format-dispatching parse entry point.
|
|
3
3
|
*
|
|
4
4
|
* A "plugin" is a directory that bundles agent capabilities (skills, commands,
|
|
5
5
|
* agents/modes, themes, providers, hooks, MCP servers) behind a single manifest.
|
|
6
|
-
*
|
|
6
|
+
* Several vendor formats are supported and parsed into one {@link NormalizedPlugin}:
|
|
7
7
|
*
|
|
8
|
-
* - `.agents-plugin/plugin.json`
|
|
9
|
-
* - `.claude-plugin/plugin.json`
|
|
8
|
+
* - `.agents-plugin/plugin.json` — native format (a strict superset)
|
|
9
|
+
* - `.claude-plugin/plugin.json` — Claude Code compatible format
|
|
10
|
+
* - `.github/copilot-plugin.json` — GitHub Copilot format (`.github/` layout)
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
+
* The per-format detect/parse/emit logic lives in {@link ./formats}, one adapter
|
|
13
|
+
* per vendor, behind the {@link PluginFormatAdapter} contract — so this module
|
|
14
|
+
* carries only the shared types and delegates directory parsing to the registry.
|
|
15
|
+
* When a directory carries more than one format, the highest-precedence one wins
|
|
16
|
+
* (native > Claude > Copilot; no merge).
|
|
12
17
|
*/
|
|
13
|
-
import
|
|
14
|
-
import * as path from "node:path";
|
|
18
|
+
import { parsePluginWithFormats } from "./formats/index.js";
|
|
15
19
|
/** Marker subdirectory + manifest filename for each supported format. */
|
|
16
20
|
export const NATIVE_MANIFEST_DIR = ".agents-plugin";
|
|
17
21
|
export const CLAUDE_MANIFEST_DIR = ".claude-plugin";
|
|
22
|
+
export const COPILOT_MANIFEST_DIR = ".github";
|
|
18
23
|
export const MANIFEST_FILE = "plugin.json";
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function dirIfExists(root, name) {
|
|
28
|
-
const p = path.join(root, name);
|
|
29
|
-
return fs.existsSync(p) && fs.statSync(p).isDirectory() ? p : undefined;
|
|
30
|
-
}
|
|
31
|
-
/** Accept either `{ hooks: {...} }` or a bare event map. */
|
|
32
|
-
function normalizeHooks(raw, root) {
|
|
33
|
-
let config;
|
|
34
|
-
if (raw && typeof raw === "object") {
|
|
35
|
-
config = "hooks" in raw && raw.hooks ? raw.hooks : raw;
|
|
36
|
-
}
|
|
37
|
-
// Fall back to the conventional hooks/hooks.json file.
|
|
38
|
-
if (!config) {
|
|
39
|
-
const hooksFile = path.join(root, "hooks", "hooks.json");
|
|
40
|
-
if (fs.existsSync(hooksFile)) {
|
|
41
|
-
const fileRaw = readJson(hooksFile);
|
|
42
|
-
if (fileRaw && typeof fileRaw === "object") {
|
|
43
|
-
config =
|
|
44
|
-
"hooks" in fileRaw && fileRaw.hooks
|
|
45
|
-
? fileRaw.hooks
|
|
46
|
-
: fileRaw;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return config && Object.keys(config).length > 0 ? config : undefined;
|
|
51
|
-
}
|
|
52
|
-
function normalizeMcp(raw, root) {
|
|
53
|
-
if (raw && Object.keys(raw).length > 0)
|
|
54
|
-
return raw;
|
|
55
|
-
const mcpFile = path.join(root, ".mcp.json");
|
|
56
|
-
if (fs.existsSync(mcpFile)) {
|
|
57
|
-
const fileRaw = readJson(mcpFile);
|
|
58
|
-
if (fileRaw?.mcpServers && Object.keys(fileRaw.mcpServers).length > 0)
|
|
59
|
-
return fileRaw.mcpServers;
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
24
|
+
export const COPILOT_MANIFEST_FILE = "copilot-plugin.json";
|
|
63
25
|
/**
|
|
64
26
|
* Parse and normalize a single plugin directory.
|
|
65
27
|
* Returns null if no recognized manifest is present or it is invalid.
|
|
28
|
+
*
|
|
29
|
+
* Delegates to the format registry; see {@link ./formats/index.ts}.
|
|
66
30
|
*/
|
|
67
31
|
export function parsePluginDir(root) {
|
|
68
|
-
|
|
69
|
-
const nativePath = path.join(root, NATIVE_MANIFEST_DIR, MANIFEST_FILE);
|
|
70
|
-
const claudePath = path.join(root, CLAUDE_MANIFEST_DIR, MANIFEST_FILE);
|
|
71
|
-
let manifestPath;
|
|
72
|
-
let format;
|
|
73
|
-
if (fs.existsSync(nativePath)) {
|
|
74
|
-
manifestPath = nativePath;
|
|
75
|
-
format = "agents";
|
|
76
|
-
}
|
|
77
|
-
else if (fs.existsSync(claudePath)) {
|
|
78
|
-
manifestPath = claudePath;
|
|
79
|
-
format = "claude";
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
const raw = readJson(manifestPath);
|
|
85
|
-
if (!raw)
|
|
86
|
-
return null;
|
|
87
|
-
const id = (raw.name ?? path.basename(root)).trim();
|
|
88
|
-
if (!id)
|
|
89
|
-
return null;
|
|
90
|
-
const author = typeof raw.author === "string" ? raw.author : raw.author?.name;
|
|
91
|
-
return {
|
|
92
|
-
id,
|
|
93
|
-
version: raw.version,
|
|
94
|
-
description: raw.description,
|
|
95
|
-
author,
|
|
96
|
-
root,
|
|
97
|
-
manifestPath,
|
|
98
|
-
format,
|
|
99
|
-
skillsDir: dirIfExists(root, "skills"),
|
|
100
|
-
commandsDir: dirIfExists(root, "commands"),
|
|
101
|
-
agentsDir: dirIfExists(root, "agents"),
|
|
102
|
-
themesDir: dirIfExists(root, "themes"),
|
|
103
|
-
hooks: normalizeHooks(raw.hooks, root),
|
|
104
|
-
mcpServers: normalizeMcp(raw.mcpServers, root),
|
|
105
|
-
// Providers are native-only; ignored on the Claude-compat path.
|
|
106
|
-
providers: format === "agents" && Array.isArray(raw.providers) ? raw.providers : undefined,
|
|
107
|
-
};
|
|
32
|
+
return parsePluginWithFormats(root);
|
|
108
33
|
}
|
|
109
34
|
//# sourceMappingURL=manifest.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/manifest.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,yEAAyE;AACzE,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AACpD,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AACpD,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAC;AAC9C,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAC3C,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAC;AA+D3D;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAA2B;IACrE,OAAO,sBAAsB,CAAC,IAAI,CAAC,CAAC;AAAA,CACpC","sourcesContent":["/**\n * Plugin manifest types and the format-dispatching parse entry point.\n *\n * A \"plugin\" is a directory that bundles agent capabilities (skills, commands,\n * agents/modes, themes, providers, hooks, MCP servers) behind a single manifest.\n * Several vendor formats are supported and parsed into one {@link NormalizedPlugin}:\n *\n * - `.agents-plugin/plugin.json` — native format (a strict superset)\n * - `.claude-plugin/plugin.json` — Claude Code compatible format\n * - `.github/copilot-plugin.json` — GitHub Copilot format (`.github/` layout)\n *\n * The per-format detect/parse/emit logic lives in {@link ./formats}, one adapter\n * per vendor, behind the {@link PluginFormatAdapter} contract — so this module\n * carries only the shared types and delegates directory parsing to the registry.\n * When a directory carries more than one format, the highest-precedence one wins\n * (native > Claude > Copilot; no merge).\n */\n\nimport type { ProviderConfig } from \"../types.js\";\nimport { parsePluginWithFormats } from \"./formats/index.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/** Marker subdirectory + manifest filename for each supported format. */\nexport const NATIVE_MANIFEST_DIR = \".agents-plugin\";\nexport const CLAUDE_MANIFEST_DIR = \".claude-plugin\";\nexport const COPILOT_MANIFEST_DIR = \".github\";\nexport const MANIFEST_FILE = \"plugin.json\";\nexport const COPILOT_MANIFEST_FILE = \"copilot-plugin.json\";\n\n/** A single shell command invoked by a hook. */\nexport interface PluginHookCommand {\n\ttype?: \"command\";\n\tcommand: string;\n\t/** Timeout in seconds (Claude Code convention). */\n\ttimeout?: number;\n}\n\n/** A matcher group: run these commands when `matcher` matches. */\nexport interface PluginHookMatcherGroup {\n\t/** Regex string matched against the tool name. Empty / \"*\" matches everything. */\n\tmatcher?: string;\n\thooks: PluginHookCommand[];\n}\n\n/**\n * Hook event map keyed by Claude Code event name\n * (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, ...).\n */\nexport type PluginHooksConfig = Record<string, PluginHookMatcherGroup[]>;\n\n/** A provider contributed by a native plugin (config-only; uses a built-in API handler). */\nexport interface PluginProvider {\n\tname: string;\n\tconfig: ProviderConfig;\n}\n\n/** Normalized, format-agnostic representation of a plugin. */\nexport interface NormalizedPlugin {\n\t/** Stable identifier (manifest `name`). */\n\tid: string;\n\tversion?: string;\n\tdescription?: string;\n\tauthor?: string;\n\t/** Absolute path to the plugin root directory. */\n\troot: string;\n\t/** Absolute path to the parsed manifest file. */\n\tmanifestPath: string;\n\t/** Which manifest format produced this plugin (the precedence winner). */\n\tformat: \"agents\" | \"claude\" | \"copilot\";\n\t/**\n\t * Every platform this plugin directory offers. When a directory carries more\n\t * than one format manifest (e.g. both `.claude-plugin/` and `.github/`), all\n\t * are recorded here — the precedence winner's platform first — rather than\n\t * hidden behind the single `format`. Always non-empty (at least `format`'s\n\t * platform), mirroring the marketplace's `supportPlatform`.\n\t */\n\tsupportPlatform: MarketplacePlatform[];\n\t/** Resolved capability directories (only set when they exist on disk). */\n\tskillsDir?: string;\n\tcommandsDir?: string;\n\tagentsDir?: string;\n\tthemesDir?: string;\n\t/** Parsed hooks (from `hooks/hooks.json` or inline `hooks`). */\n\thooks?: PluginHooksConfig;\n\t/** Parsed MCP servers (from `.mcp.json` or inline `mcpServers`); connected via the MCP registry. */\n\tmcpServers?: Record<string, unknown>;\n\t/** Native-only: providers to register. */\n\tproviders?: PluginProvider[];\n}\n\n/**\n * Parse and normalize a single plugin directory.\n * Returns null if no recognized manifest is present or it is invalid.\n *\n * Delegates to the format registry; see {@link ./formats/index.ts}.\n */\nexport function parsePluginDir(root: string): NormalizedPlugin | null {\n\treturn parsePluginWithFormats(root);\n}\n"]}
|
|
@@ -15,16 +15,17 @@
|
|
|
15
15
|
* Claude, then Copilot (no merge) — matching the "`.agents/` first" policy used
|
|
16
16
|
* across hoocode's resource surfaces.
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
export declare const CLAUDE_MARKETPLACE_FILE: string;
|
|
20
|
-
export declare const COPILOT_MARKETPLACE_FILE: string;
|
|
18
|
+
import type { MarketplacePlatform } from "./formats/types.js";
|
|
21
19
|
/**
|
|
22
20
|
* Canonical platform token used by the optional `supportPlatform` field. The
|
|
23
21
|
* `.github/marketplace.json` (Copilot-style) format is surfaced as `"github"` —
|
|
24
22
|
* friendlier than the internal `format: "copilot"` and matching where the file
|
|
25
23
|
* lives. Authored aliases (`copilot`, `gh`) normalize to `github`.
|
|
24
|
+
*
|
|
25
|
+
* Re-exported from the format registry so the platform vocabulary has a single
|
|
26
|
+
* source of truth.
|
|
26
27
|
*/
|
|
27
|
-
export type MarketplacePlatform
|
|
28
|
+
export type { MarketplacePlatform } from "./formats/types.js";
|
|
28
29
|
/** Normalize an authored `supportPlatform` (string | string[]) to canonical tokens. */
|
|
29
30
|
export declare function normalizePlatforms(value: unknown): MarketplacePlatform[];
|
|
30
31
|
export interface MarketplacePluginEntry {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"marketplace.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/marketplace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,
|
|
1
|
+
{"version":3,"file":"marketplace.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/marketplace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;;;;;GAQG;AACH,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAO9D,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,EAAE,CAiBxE;AAED,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACxC;;;;;;OAMG;IACH,eAAe,EAAE,mBAAmB,EAAE,CAAC;IACvC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,sBAAsB,EAAE,CAAC;CAClC;AAUD,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAC7B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAUjC;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CA6C7E;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,oBAAoB,CAMjG;AAMD,MAAM,WAAW,iBAAiB;IACjC,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,GAAG,EAAE,MAAM,CAAC;CACZ;AAMD,8DAA8D;AAC9D,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAG3E;AAED,mDAAmD;AACnD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI,CAG3F","sourcesContent":["/**\n * Plugin marketplaces — install plugins from a curated index hosted in a git repo\n * (or a local directory).\n *\n * A marketplace is a directory containing an index manifest in one of three formats:\n * - Native: `.agents-plugin/marketplace.json` (preferred; the `.agents` surface)\n * - Claude: `.claude-plugin/marketplace.json`\n * - Copilot: `.github/marketplace.json` (Copilot-style git index)\n *\n * All three use the same shape: `{ name?, owner?, plugins: [{ name, source, description? }] }`.\n * A plugin `source` is either a path relative to the marketplace root (a plugin\n * directory inside the repo), a git URL, or an `npm:<spec>` reference.\n *\n * When more than one is present the native `.agents-plugin` format wins, then\n * Claude, then Copilot (no merge) — matching the \"`.agents/` first\" policy used\n * across hoocode's resource surfaces.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { PLUGIN_FORMATS } from \"./formats/index.js\";\nimport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/**\n * Canonical platform token used by the optional `supportPlatform` field. The\n * `.github/marketplace.json` (Copilot-style) format is surfaced as `\"github\"` —\n * friendlier than the internal `format: \"copilot\"` and matching where the file\n * lives. Authored aliases (`copilot`, `gh`) normalize to `github`.\n *\n * Re-exported from the format registry so the platform vocabulary has a single\n * source of truth.\n */\nexport type { MarketplacePlatform } from \"./formats/types.js\";\n\n/** Map an internal marketplace `format` to its public platform token. */\nfunction formatToPlatform(format: NormalizedMarketplace[\"format\"]): MarketplacePlatform {\n\treturn format === \"copilot\" ? \"github\" : format;\n}\n\n/** Normalize an authored `supportPlatform` (string | string[]) to canonical tokens. */\nexport function normalizePlatforms(value: unknown): MarketplacePlatform[] {\n\tconst raw = Array.isArray(value) ? value : value == null ? [] : [value];\n\tconst out: MarketplacePlatform[] = [];\n\tfor (const v of raw) {\n\t\tif (typeof v !== \"string\") continue;\n\t\tconst t = v.trim().toLowerCase();\n\t\tconst canonical: MarketplacePlatform | undefined =\n\t\t\tt === \"agents\" || t === \"native\"\n\t\t\t\t? \"agents\"\n\t\t\t\t: t === \"claude\"\n\t\t\t\t\t? \"claude\"\n\t\t\t\t\t: t === \"github\" || t === \"copilot\" || t === \"gh\"\n\t\t\t\t\t\t? \"github\"\n\t\t\t\t\t\t: undefined;\n\t\tif (canonical && !out.includes(canonical)) out.push(canonical);\n\t}\n\treturn out;\n}\n\nexport interface MarketplacePluginEntry {\n\tname: string;\n\t/** Relative path, git URL, or `npm:<spec>`. */\n\tsource: string;\n\tdescription?: string;\n\t/**\n\t * Optional platform(s) this entry targets. Only set when authored on the\n\t * entry; absent means \"no per-entry restriction\" (the marketplace's\n\t * platforms apply). Purely informational today — nothing filters on it.\n\t */\n\tsupportPlatform?: MarketplacePlatform[];\n}\n\nexport interface NormalizedMarketplace {\n\tname: string;\n\t/** Precedence winner when several index files conflict (agents > claude > copilot). */\n\tformat: \"agents\" | \"claude\" | \"copilot\";\n\t/**\n\t * Every platform this marketplace directory offers, so a conflict between\n\t * multiple index files (e.g. both `.github/` and `.claude-plugin/`) is\n\t * recorded rather than hidden. Always includes the resolved `format`'s\n\t * platform; also folds in any authored top-level `supportPlatform`. Never\n\t * empty.\n\t */\n\tsupportPlatform: MarketplacePlatform[];\n\t/** Absolute path to the marketplace directory. */\n\troot: string;\n\tmanifestPath: string;\n\tplugins: MarketplacePluginEntry[];\n}\n\ninterface RawMarketplace {\n\tname?: string;\n\towner?: string;\n\t/** Optional authored platform hint (string | string[]); folded into supportPlatform. */\n\tsupportPlatform?: unknown;\n\tplugins?: Array<{ name?: string; source?: string; description?: string; supportPlatform?: unknown }>;\n}\n\n/** A resolved, installable plugin source. */\nexport type ResolvedPluginSource =\n\t| { kind: \"local\"; path: string }\n\t| { kind: \"git\"; url: string }\n\t| { kind: \"npm\"; spec: string };\n\nfunction readJson<T>(file: string): T | null {\n\ttry {\n\t\treturn JSON.parse(fs.readFileSync(file, \"utf8\")) as T;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Parse a marketplace directory. Native `.agents-plugin` wins, then Claude, then\n * Copilot when more than one file is present. Returns null if no manifest exists\n * or it is invalid.\n */\nexport function parseMarketplaceDir(dir: string): NormalizedMarketplace | null {\n\t// Record every index format present so a conflict (more than one file) is\n\t// visible via supportPlatform, not silently dropped. PLUGIN_FORMATS is\n\t// precedence-ordered (native > Claude > Copilot), so `present` inherits it.\n\tconst present: Array<{ format: NormalizedMarketplace[\"format\"]; path: string }> = [];\n\tfor (const fmt of PLUGIN_FORMATS) {\n\t\tconst p = path.join(dir, fmt.marketplaceFile);\n\t\tif (fs.existsSync(p)) present.push({ format: fmt.id, path: p });\n\t}\n\tif (present.length === 0) return null;\n\n\t// Precedence winner (present is built in agents > claude > copilot order).\n\tconst { format, path: manifestPath } = present[0];\n\n\tconst raw = readJson<RawMarketplace>(manifestPath);\n\tif (!raw) return null;\n\n\tconst plugins: MarketplacePluginEntry[] = [];\n\tfor (const entry of raw.plugins ?? []) {\n\t\tif (entry?.name && entry.source) {\n\t\t\tconst entryPlatforms = normalizePlatforms(entry.supportPlatform);\n\t\t\tplugins.push({\n\t\t\t\tname: entry.name,\n\t\t\t\tsource: entry.source,\n\t\t\t\tdescription: entry.description,\n\t\t\t\t...(entryPlatforms.length > 0 ? { supportPlatform: entryPlatforms } : {}),\n\t\t\t});\n\t\t}\n\t}\n\n\t// supportPlatform = platforms of all present index files, plus any authored\n\t// top-level hint, deduped. Always non-empty (at least the resolved format).\n\tconst supportPlatform: MarketplacePlatform[] = [];\n\tfor (const p of [...present.map((e) => formatToPlatform(e.format)), ...normalizePlatforms(raw.supportPlatform)]) {\n\t\tif (!supportPlatform.includes(p)) supportPlatform.push(p);\n\t}\n\n\treturn {\n\t\tname: (raw.name ?? path.basename(dir)).trim() || path.basename(dir),\n\t\tformat,\n\t\tsupportPlatform,\n\t\troot: dir,\n\t\tmanifestPath,\n\t\tplugins,\n\t};\n}\n\n/** Classify and resolve a plugin `source` against its marketplace root. */\nexport function resolvePluginSource(source: string, marketplaceRoot: string): ResolvedPluginSource {\n\tconst s = source.trim();\n\tif (s.startsWith(\"npm:\")) return { kind: \"npm\", spec: s.slice(4) };\n\tif (/^https?:\\/\\//.test(s) || s.startsWith(\"git@\") || s.endsWith(\".git\")) return { kind: \"git\", url: s };\n\t// Otherwise a path relative to (or absolute within) the marketplace repo.\n\treturn { kind: \"local\", path: path.isAbsolute(s) ? s : path.resolve(marketplaceRoot, s) };\n}\n\n// ============================================================================\n// Marketplace registry (persisted list of added marketplaces)\n// ============================================================================\n\nexport interface MarketplaceRecord {\n\t/** Original location the user added (git URL or local path). */\n\tlocation: string;\n\t/** Local directory to read the manifest from (clone dir for git, else location). */\n\tdir: string;\n}\n\ninterface MarketplaceStoreFile {\n\tmarketplaces?: MarketplaceRecord[];\n}\n\n/** Read the persisted marketplace list from `<storePath>`. */\nexport function readMarketplaceStore(storePath: string): MarketplaceRecord[] {\n\tconst parsed = readJson<MarketplaceStoreFile>(storePath);\n\treturn Array.isArray(parsed?.marketplaces) ? parsed.marketplaces : [];\n}\n\n/** Write the marketplace list to `<storePath>`. */\nexport function writeMarketplaceStore(storePath: string, records: MarketplaceRecord[]): void {\n\tfs.mkdirSync(path.dirname(storePath), { recursive: true });\n\tfs.writeFileSync(storePath, `${JSON.stringify({ marketplaces: records }, null, 2)}\\n`, \"utf8\");\n}\n"]}
|