@hashicorp/kits 0.1.0
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/README.md +619 -0
- package/bin/kits.js +8 -0
- package/dist/adapters/base-adapter.d.ts +231 -0
- package/dist/adapters/base-adapter.d.ts.map +1 -0
- package/dist/adapters/base-adapter.js +703 -0
- package/dist/adapters/base-adapter.js.map +1 -0
- package/dist/adapters/claude-code/detection.d.ts +63 -0
- package/dist/adapters/claude-code/detection.d.ts.map +1 -0
- package/dist/adapters/claude-code/detection.js +154 -0
- package/dist/adapters/claude-code/detection.js.map +1 -0
- package/dist/adapters/claude-code/index.d.ts +178 -0
- package/dist/adapters/claude-code/index.d.ts.map +1 -0
- package/dist/adapters/claude-code/index.js +643 -0
- package/dist/adapters/claude-code/index.js.map +1 -0
- package/dist/adapters/claude-code/installer.d.ts +161 -0
- package/dist/adapters/claude-code/installer.d.ts.map +1 -0
- package/dist/adapters/claude-code/installer.js +413 -0
- package/dist/adapters/claude-code/installer.js.map +1 -0
- package/dist/adapters/claude-code/model-mapping.d.ts +7 -0
- package/dist/adapters/claude-code/model-mapping.d.ts.map +1 -0
- package/dist/adapters/claude-code/model-mapping.js +14 -0
- package/dist/adapters/claude-code/model-mapping.js.map +1 -0
- package/dist/adapters/claude-code/tool-mapping.d.ts +18 -0
- package/dist/adapters/claude-code/tool-mapping.d.ts.map +1 -0
- package/dist/adapters/claude-code/tool-mapping.js +31 -0
- package/dist/adapters/claude-code/tool-mapping.js.map +1 -0
- package/dist/adapters/codex/detection.d.ts +60 -0
- package/dist/adapters/codex/detection.d.ts.map +1 -0
- package/dist/adapters/codex/detection.js +146 -0
- package/dist/adapters/codex/detection.js.map +1 -0
- package/dist/adapters/codex/index.d.ts +167 -0
- package/dist/adapters/codex/index.d.ts.map +1 -0
- package/dist/adapters/codex/index.js +344 -0
- package/dist/adapters/codex/index.js.map +1 -0
- package/dist/adapters/codex/installer.d.ts +147 -0
- package/dist/adapters/codex/installer.d.ts.map +1 -0
- package/dist/adapters/codex/installer.js +229 -0
- package/dist/adapters/codex/installer.js.map +1 -0
- package/dist/adapters/codex/model-mapping.d.ts +7 -0
- package/dist/adapters/codex/model-mapping.d.ts.map +1 -0
- package/dist/adapters/codex/model-mapping.js +14 -0
- package/dist/adapters/codex/model-mapping.js.map +1 -0
- package/dist/adapters/codex/tool-mapping.d.ts +19 -0
- package/dist/adapters/codex/tool-mapping.d.ts.map +1 -0
- package/dist/adapters/codex/tool-mapping.js +32 -0
- package/dist/adapters/codex/tool-mapping.js.map +1 -0
- package/dist/adapters/command-parser.d.ts +72 -0
- package/dist/adapters/command-parser.d.ts.map +1 -0
- package/dist/adapters/command-parser.js +222 -0
- package/dist/adapters/command-parser.js.map +1 -0
- package/dist/adapters/file-operations.d.ts +164 -0
- package/dist/adapters/file-operations.d.ts.map +1 -0
- package/dist/adapters/file-operations.js +526 -0
- package/dist/adapters/file-operations.js.map +1 -0
- package/dist/adapters/gemini-cli/detection.d.ts +57 -0
- package/dist/adapters/gemini-cli/detection.d.ts.map +1 -0
- package/dist/adapters/gemini-cli/detection.js +143 -0
- package/dist/adapters/gemini-cli/detection.js.map +1 -0
- package/dist/adapters/gemini-cli/index.d.ts +182 -0
- package/dist/adapters/gemini-cli/index.d.ts.map +1 -0
- package/dist/adapters/gemini-cli/index.js +598 -0
- package/dist/adapters/gemini-cli/index.js.map +1 -0
- package/dist/adapters/gemini-cli/installer.d.ts +158 -0
- package/dist/adapters/gemini-cli/installer.d.ts.map +1 -0
- package/dist/adapters/gemini-cli/installer.js +457 -0
- package/dist/adapters/gemini-cli/installer.js.map +1 -0
- package/dist/adapters/gemini-cli/model-mapping.d.ts +7 -0
- package/dist/adapters/gemini-cli/model-mapping.d.ts.map +1 -0
- package/dist/adapters/gemini-cli/model-mapping.js +14 -0
- package/dist/adapters/gemini-cli/model-mapping.js.map +1 -0
- package/dist/adapters/gemini-cli/tool-mapping.d.ts +18 -0
- package/dist/adapters/gemini-cli/tool-mapping.d.ts.map +1 -0
- package/dist/adapters/gemini-cli/tool-mapping.js +31 -0
- package/dist/adapters/gemini-cli/tool-mapping.js.map +1 -0
- package/dist/adapters/github-copilot/detection.d.ts +58 -0
- package/dist/adapters/github-copilot/detection.d.ts.map +1 -0
- package/dist/adapters/github-copilot/detection.js +144 -0
- package/dist/adapters/github-copilot/detection.js.map +1 -0
- package/dist/adapters/github-copilot/index.d.ts +203 -0
- package/dist/adapters/github-copilot/index.d.ts.map +1 -0
- package/dist/adapters/github-copilot/index.js +595 -0
- package/dist/adapters/github-copilot/index.js.map +1 -0
- package/dist/adapters/github-copilot/installer.d.ts +124 -0
- package/dist/adapters/github-copilot/installer.d.ts.map +1 -0
- package/dist/adapters/github-copilot/installer.js +343 -0
- package/dist/adapters/github-copilot/installer.js.map +1 -0
- package/dist/adapters/github-copilot/model-mapping.d.ts +7 -0
- package/dist/adapters/github-copilot/model-mapping.d.ts.map +1 -0
- package/dist/adapters/github-copilot/model-mapping.js +14 -0
- package/dist/adapters/github-copilot/model-mapping.js.map +1 -0
- package/dist/adapters/github-copilot/tool-mapping.d.ts +18 -0
- package/dist/adapters/github-copilot/tool-mapping.d.ts.map +1 -0
- package/dist/adapters/github-copilot/tool-mapping.js +31 -0
- package/dist/adapters/github-copilot/tool-mapping.js.map +1 -0
- package/dist/adapters/index.d.ts +39 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +76 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/interface.d.ts +9 -0
- package/dist/adapters/interface.d.ts.map +1 -0
- package/dist/adapters/interface.js +8 -0
- package/dist/adapters/interface.js.map +1 -0
- package/dist/adapters/model-templating.d.ts +16 -0
- package/dist/adapters/model-templating.d.ts.map +1 -0
- package/dist/adapters/model-templating.js +52 -0
- package/dist/adapters/model-templating.js.map +1 -0
- package/dist/adapters/opencode/detection.d.ts +57 -0
- package/dist/adapters/opencode/detection.d.ts.map +1 -0
- package/dist/adapters/opencode/detection.js +140 -0
- package/dist/adapters/opencode/detection.js.map +1 -0
- package/dist/adapters/opencode/index.d.ts +168 -0
- package/dist/adapters/opencode/index.d.ts.map +1 -0
- package/dist/adapters/opencode/index.js +494 -0
- package/dist/adapters/opencode/index.js.map +1 -0
- package/dist/adapters/opencode/installer.d.ts +91 -0
- package/dist/adapters/opencode/installer.d.ts.map +1 -0
- package/dist/adapters/opencode/installer.js +290 -0
- package/dist/adapters/opencode/installer.js.map +1 -0
- package/dist/adapters/opencode/model-mapping.d.ts +7 -0
- package/dist/adapters/opencode/model-mapping.d.ts.map +1 -0
- package/dist/adapters/opencode/model-mapping.js +14 -0
- package/dist/adapters/opencode/model-mapping.js.map +1 -0
- package/dist/adapters/opencode/tool-mapping.d.ts +18 -0
- package/dist/adapters/opencode/tool-mapping.d.ts.map +1 -0
- package/dist/adapters/opencode/tool-mapping.js +31 -0
- package/dist/adapters/opencode/tool-mapping.js.map +1 -0
- package/dist/adapters/registry.d.ts +154 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +223 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/skill-frontmatter.d.ts +34 -0
- package/dist/adapters/skill-frontmatter.d.ts.map +1 -0
- package/dist/adapters/skill-frontmatter.js +110 -0
- package/dist/adapters/skill-frontmatter.js.map +1 -0
- package/dist/adapters/subagent-frontmatter.d.ts +22 -0
- package/dist/adapters/subagent-frontmatter.d.ts.map +1 -0
- package/dist/adapters/subagent-frontmatter.js +80 -0
- package/dist/adapters/subagent-frontmatter.js.map +1 -0
- package/dist/adapters/tool-templating.d.ts +162 -0
- package/dist/adapters/tool-templating.d.ts.map +1 -0
- package/dist/adapters/tool-templating.js +273 -0
- package/dist/adapters/tool-templating.js.map +1 -0
- package/dist/adapters/types.d.ts +347 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +33 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +261 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/info.d.ts +20 -0
- package/dist/cli/info.d.ts.map +1 -0
- package/dist/cli/info.js +194 -0
- package/dist/cli/info.js.map +1 -0
- package/dist/cli/install.d.ts +21 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/install.js +1624 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/list.d.ts +19 -0
- package/dist/cli/list.d.ts.map +1 -0
- package/dist/cli/list.js +216 -0
- package/dist/cli/list.js.map +1 -0
- package/dist/cli/types.d.ts +246 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +25 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/uninstall.d.ts +20 -0
- package/dist/cli/uninstall.d.ts.map +1 -0
- package/dist/cli/uninstall.js +393 -0
- package/dist/cli/uninstall.js.map +1 -0
- package/dist/cli/upgrade.d.ts +20 -0
- package/dist/cli/upgrade.d.ts.map +1 -0
- package/dist/cli/upgrade.js +372 -0
- package/dist/cli/upgrade.js.map +1 -0
- package/dist/cli/validate.d.ts +14 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +307 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/core/debug.d.ts +23 -0
- package/dist/core/debug.d.ts.map +1 -0
- package/dist/core/debug.js +69 -0
- package/dist/core/debug.js.map +1 -0
- package/dist/core/hook-instance.d.ts +8 -0
- package/dist/core/hook-instance.d.ts.map +1 -0
- package/dist/core/hook-instance.js +62 -0
- package/dist/core/hook-instance.js.map +1 -0
- package/dist/core/mcp-instance.d.ts +13 -0
- package/dist/core/mcp-instance.d.ts.map +1 -0
- package/dist/core/mcp-instance.js +80 -0
- package/dist/core/mcp-instance.js.map +1 -0
- package/dist/core/types.d.ts +461 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +42 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/upgrade-executor.d.ts +70 -0
- package/dist/core/upgrade-executor.d.ts.map +1 -0
- package/dist/core/upgrade-executor.js +368 -0
- package/dist/core/upgrade-executor.js.map +1 -0
- package/dist/discovery/fetcher-registry.d.ts +87 -0
- package/dist/discovery/fetcher-registry.d.ts.map +1 -0
- package/dist/discovery/fetcher-registry.js +119 -0
- package/dist/discovery/fetcher-registry.js.map +1 -0
- package/dist/discovery/git-fetcher.d.ts +61 -0
- package/dist/discovery/git-fetcher.d.ts.map +1 -0
- package/dist/discovery/git-fetcher.js +150 -0
- package/dist/discovery/git-fetcher.js.map +1 -0
- package/dist/discovery/index.d.ts +13 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +15 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/kit-scanner.d.ts +55 -0
- package/dist/discovery/kit-scanner.d.ts.map +1 -0
- package/dist/discovery/kit-scanner.js +305 -0
- package/dist/discovery/kit-scanner.js.map +1 -0
- package/dist/discovery/local-fetcher.d.ts +38 -0
- package/dist/discovery/local-fetcher.d.ts.map +1 -0
- package/dist/discovery/local-fetcher.js +100 -0
- package/dist/discovery/local-fetcher.js.map +1 -0
- package/dist/discovery/source-parser.d.ts +33 -0
- package/dist/discovery/source-parser.d.ts.map +1 -0
- package/dist/discovery/source-parser.js +136 -0
- package/dist/discovery/source-parser.js.map +1 -0
- package/dist/discovery/types.d.ts +145 -0
- package/dist/discovery/types.d.ts.map +1 -0
- package/dist/discovery/types.js +18 -0
- package/dist/discovery/types.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/index.d.ts +79 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +200 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/read.d.ts +32 -0
- package/dist/manifest/read.d.ts.map +1 -0
- package/dist/manifest/read.js +88 -0
- package/dist/manifest/read.js.map +1 -0
- package/dist/manifest/types.d.ts +119 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +44 -0
- package/dist/manifest/types.js.map +1 -0
- package/dist/manifest/upgrade-check.d.ts +72 -0
- package/dist/manifest/upgrade-check.d.ts.map +1 -0
- package/dist/manifest/upgrade-check.js +215 -0
- package/dist/manifest/upgrade-check.js.map +1 -0
- package/dist/manifest/utils.d.ts +35 -0
- package/dist/manifest/utils.d.ts.map +1 -0
- package/dist/manifest/utils.js +57 -0
- package/dist/manifest/utils.js.map +1 -0
- package/dist/manifest/write.d.ts +44 -0
- package/dist/manifest/write.d.ts.map +1 -0
- package/dist/manifest/write.js +77 -0
- package/dist/manifest/write.js.map +1 -0
- package/dist/resolution/env-resolver.d.ts +81 -0
- package/dist/resolution/env-resolver.d.ts.map +1 -0
- package/dist/resolution/env-resolver.js +233 -0
- package/dist/resolution/env-resolver.js.map +1 -0
- package/dist/resolution/index.d.ts +55 -0
- package/dist/resolution/index.d.ts.map +1 -0
- package/dist/resolution/index.js +412 -0
- package/dist/resolution/index.js.map +1 -0
- package/dist/resolution/multi-kit-resolver.d.ts +43 -0
- package/dist/resolution/multi-kit-resolver.d.ts.map +1 -0
- package/dist/resolution/multi-kit-resolver.js +258 -0
- package/dist/resolution/multi-kit-resolver.js.map +1 -0
- package/dist/resolution/primitive-paths.d.ts +17 -0
- package/dist/resolution/primitive-paths.d.ts.map +1 -0
- package/dist/resolution/primitive-paths.js +59 -0
- package/dist/resolution/primitive-paths.js.map +1 -0
- package/dist/resolution/primitives-registry.d.ts +137 -0
- package/dist/resolution/primitives-registry.d.ts.map +1 -0
- package/dist/resolution/primitives-registry.js +295 -0
- package/dist/resolution/primitives-registry.js.map +1 -0
- package/dist/resolution/reference-parser.d.ts +62 -0
- package/dist/resolution/reference-parser.d.ts.map +1 -0
- package/dist/resolution/reference-parser.js +182 -0
- package/dist/resolution/reference-parser.js.map +1 -0
- package/dist/resolution/types.d.ts +77 -0
- package/dist/resolution/types.d.ts.map +1 -0
- package/dist/resolution/types.js +13 -0
- package/dist/resolution/types.js.map +1 -0
- package/dist/resolution/version-resolver.d.ts +76 -0
- package/dist/resolution/version-resolver.d.ts.map +1 -0
- package/dist/resolution/version-resolver.js +269 -0
- package/dist/resolution/version-resolver.js.map +1 -0
- package/dist/tui/compatibility.d.ts +80 -0
- package/dist/tui/compatibility.d.ts.map +1 -0
- package/dist/tui/compatibility.js +355 -0
- package/dist/tui/compatibility.js.map +1 -0
- package/dist/tui/env-prompt.d.ts +129 -0
- package/dist/tui/env-prompt.d.ts.map +1 -0
- package/dist/tui/env-prompt.js +488 -0
- package/dist/tui/env-prompt.js.map +1 -0
- package/dist/tui/harness-select.d.ts +54 -0
- package/dist/tui/harness-select.d.ts.map +1 -0
- package/dist/tui/harness-select.js +171 -0
- package/dist/tui/harness-select.js.map +1 -0
- package/dist/tui/index.d.ts +112 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +213 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/kit-select.d.ts +72 -0
- package/dist/tui/kit-select.d.ts.map +1 -0
- package/dist/tui/kit-select.js +209 -0
- package/dist/tui/kit-select.js.map +1 -0
- package/dist/tui/progress.d.ts +75 -0
- package/dist/tui/progress.d.ts.map +1 -0
- package/dist/tui/progress.js +267 -0
- package/dist/tui/progress.js.map +1 -0
- package/dist/tui/resolution.d.ts +62 -0
- package/dist/tui/resolution.d.ts.map +1 -0
- package/dist/tui/resolution.js +261 -0
- package/dist/tui/resolution.js.map +1 -0
- package/dist/tui/scope-compatibility.d.ts +139 -0
- package/dist/tui/scope-compatibility.d.ts.map +1 -0
- package/dist/tui/scope-compatibility.js +230 -0
- package/dist/tui/scope-compatibility.js.map +1 -0
- package/dist/tui/scope-select.d.ts +67 -0
- package/dist/tui/scope-select.d.ts.map +1 -0
- package/dist/tui/scope-select.js +134 -0
- package/dist/tui/scope-select.js.map +1 -0
- package/dist/tui/spinner.d.ts +114 -0
- package/dist/tui/spinner.d.ts.map +1 -0
- package/dist/tui/spinner.js +186 -0
- package/dist/tui/spinner.js.map +1 -0
- package/dist/tui/summary.d.ts +71 -0
- package/dist/tui/summary.d.ts.map +1 -0
- package/dist/tui/summary.js +343 -0
- package/dist/tui/summary.js.map +1 -0
- package/dist/tui/types.d.ts +234 -0
- package/dist/tui/types.d.ts.map +1 -0
- package/dist/tui/types.js +7 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/tui/upgrade-select.d.ts +73 -0
- package/dist/tui/upgrade-select.d.ts.map +1 -0
- package/dist/tui/upgrade-select.js +324 -0
- package/dist/tui/upgrade-select.js.map +1 -0
- package/dist/validation/index.d.ts +13 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +13 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/source.d.ts +14 -0
- package/dist/validation/source.d.ts.map +1 -0
- package/dist/validation/source.js +51 -0
- package/dist/validation/source.js.map +1 -0
- package/dist/validation/utils.d.ts +29 -0
- package/dist/validation/utils.d.ts.map +1 -0
- package/dist/validation/utils.js +89 -0
- package/dist/validation/utils.js.map +1 -0
- package/dist/validation/validate-commands.d.ts +28 -0
- package/dist/validation/validate-commands.d.ts.map +1 -0
- package/dist/validation/validate-commands.js +151 -0
- package/dist/validation/validate-commands.js.map +1 -0
- package/dist/validation/validate-hooks.d.ts +13 -0
- package/dist/validation/validate-hooks.d.ts.map +1 -0
- package/dist/validation/validate-hooks.js +272 -0
- package/dist/validation/validate-hooks.js.map +1 -0
- package/dist/validation/validate-kits.d.ts +15 -0
- package/dist/validation/validate-kits.d.ts.map +1 -0
- package/dist/validation/validate-kits.js +185 -0
- package/dist/validation/validate-kits.js.map +1 -0
- package/dist/validation/validate-mcp.d.ts +12 -0
- package/dist/validation/validate-mcp.d.ts.map +1 -0
- package/dist/validation/validate-mcp.js +132 -0
- package/dist/validation/validate-mcp.js.map +1 -0
- package/dist/validation/validate-skills.d.ts +24 -0
- package/dist/validation/validate-skills.d.ts.map +1 -0
- package/dist/validation/validate-skills.js +223 -0
- package/dist/validation/validate-skills.js.map +1 -0
- package/dist/validation/validate-subagents.d.ts +27 -0
- package/dist/validation/validate-subagents.d.ts.map +1 -0
- package/dist/validation/validate-subagents.js +269 -0
- package/dist/validation/validate-subagents.js.map +1 -0
- package/package.json +91 -0
- package/schemas/command.schema.json +23 -0
- package/schemas/examples/hook-binding-valid.json +20 -0
- package/schemas/examples/hook-program-valid.json +25 -0
- package/schemas/examples/http-server-valid.json +82 -0
- package/schemas/examples/invalid-sensitive-header-no-envvar.json +16 -0
- package/schemas/examples/invalid-sensitive-header-with-value.json +17 -0
- package/schemas/examples/invalid-sensitive-var-with-default.json +19 -0
- package/schemas/examples/stdio-server-valid.json +55 -0
- package/schemas/hook-binding.schema.json +63 -0
- package/schemas/hook-program.schema.json +104 -0
- package/schemas/kit.schema.json +338 -0
- package/schemas/kits.schema.json +117 -0
- package/schemas/manifest.schema.json +200 -0
- package/schemas/mcp-server.schema.json +305 -0
- package/schemas/primitives.schema.json +118 -0
- package/schemas/skill.schema.json +96 -0
- package/schemas/subagent.schema.json +107 -0
|
@@ -0,0 +1,1624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install command implementation.
|
|
3
|
+
*
|
|
4
|
+
* Handles fetching sources, discovering kits, and installing to harnesses.
|
|
5
|
+
* Uses the TUI layer for interactive user interaction.
|
|
6
|
+
*/
|
|
7
|
+
import { ExitCode } from "./types.js";
|
|
8
|
+
import * as clack from "@clack/prompts";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import { fetchSource, scanKits, filterKitsByName as filterDiscoveryKitsByName, getMissingKitNames as getDiscoveryMissingKitNames, NoFetcherError, SourceParseError, } from "../discovery/index.js";
|
|
12
|
+
import { resolvePrimitiveReferences, PrimitivesRegistryLoader, validateCliEnvFlags, mergeEnvDefs, resolveEnvVarsFromConfig, } from "../resolution/index.js";
|
|
13
|
+
import { hashMcpConfig } from "../core/mcp-instance.js";
|
|
14
|
+
import { hashHookConfig } from "../core/hook-instance.js";
|
|
15
|
+
import { readManifest } from "../manifest/read.js";
|
|
16
|
+
import { writeManifest } from "../manifest/write.js";
|
|
17
|
+
import { createEmptyManifest, } from "../manifest/types.js";
|
|
18
|
+
import { getAdapterRegistry } from "../adapters/index.js";
|
|
19
|
+
import { debugLog, enableDebugLogging, getDebugLogPath, checkDebugLogWritable, } from "../core/debug.js";
|
|
20
|
+
import { expandPath } from "../adapters/file-operations.js";
|
|
21
|
+
import {
|
|
22
|
+
// TUI components
|
|
23
|
+
intro, outroSuccess, outroError, outroCancel, logInfo, logError, confirmInstallPlan, displayInstallPlanSummary,
|
|
24
|
+
// Spinner utilities
|
|
25
|
+
startSpinner, stopSpinnerSuccess, stopSpinnerError, formatSource, formatCount, formatKit,
|
|
26
|
+
// Harness selection
|
|
27
|
+
selectHarnesses, warnMissingHarnesses, displayNoHarnessesDetected, filterHarnessesByName, getMissingHarnesses,
|
|
28
|
+
// Kit selection
|
|
29
|
+
selectKits, displayAvailableKits, displayKitsJson, displayNoKitsFound, displayMissingKits, filterKitsByName,
|
|
30
|
+
// Compatibility
|
|
31
|
+
runCompatibilityCheck, buildCompatibilityMatrix, hasIncompatibilities,
|
|
32
|
+
// Scope selection
|
|
33
|
+
selectScope,
|
|
34
|
+
// Summary
|
|
35
|
+
displayDryRunSummary,
|
|
36
|
+
// Env prompts (Phase 6)
|
|
37
|
+
promptForEnvVars, promptForSharedMcpInstances, } from "../tui/index.js";
|
|
38
|
+
/**
|
|
39
|
+
* Detect installed harnesses.
|
|
40
|
+
*/
|
|
41
|
+
async function detectAllHarnesses() {
|
|
42
|
+
const registry = getAdapterRegistry();
|
|
43
|
+
const adapters = registry.list();
|
|
44
|
+
const results = [];
|
|
45
|
+
for (const adapter of adapters) {
|
|
46
|
+
const detection = await adapter.detect();
|
|
47
|
+
const info = {
|
|
48
|
+
name: adapter.name,
|
|
49
|
+
displayName: adapter.displayName,
|
|
50
|
+
detected: detection.detected,
|
|
51
|
+
supportedPrimitives: adapter.getSupportedPrimitives(),
|
|
52
|
+
adapter,
|
|
53
|
+
};
|
|
54
|
+
if (detection.version) {
|
|
55
|
+
info.version = detection.version;
|
|
56
|
+
}
|
|
57
|
+
if (detection.configPath) {
|
|
58
|
+
info.configPath = detection.configPath;
|
|
59
|
+
}
|
|
60
|
+
results.push(info);
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Run the install command.
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Determine scope from CLI options.
|
|
69
|
+
* Returns null if scope needs to be determined via TUI.
|
|
70
|
+
*/
|
|
71
|
+
function getScopeFromOptions(options) {
|
|
72
|
+
if (options.global) {
|
|
73
|
+
return "global";
|
|
74
|
+
}
|
|
75
|
+
if (options.project) {
|
|
76
|
+
return "project";
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Run the install command.
|
|
82
|
+
*/
|
|
83
|
+
export async function runInstall(source, options) {
|
|
84
|
+
const isInteractive = !options.yes && !options.json;
|
|
85
|
+
let fetchResult;
|
|
86
|
+
enableDebugLogging(options.debug ?? false);
|
|
87
|
+
if (options.debug) {
|
|
88
|
+
const debugCheck = checkDebugLogWritable();
|
|
89
|
+
if (!debugCheck.ok) {
|
|
90
|
+
console.error(`Warning: Debug logging enabled but cannot write to ${debugCheck.path}: ${debugCheck.error}`);
|
|
91
|
+
}
|
|
92
|
+
void debugLog({
|
|
93
|
+
level: "info",
|
|
94
|
+
event: "debug.enabled",
|
|
95
|
+
message: "Debug logging enabled",
|
|
96
|
+
data: { logPath: getDebugLogPath() },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
void debugLog({
|
|
100
|
+
level: "info",
|
|
101
|
+
event: "install.start",
|
|
102
|
+
data: {
|
|
103
|
+
source,
|
|
104
|
+
options: {
|
|
105
|
+
agent: options.agent,
|
|
106
|
+
kit: options.kit,
|
|
107
|
+
list: options.list ?? false,
|
|
108
|
+
yes: options.yes ?? false,
|
|
109
|
+
dryRun: options.dryRun ?? false,
|
|
110
|
+
json: options.json ?? false,
|
|
111
|
+
verbose: options.verbose ?? false,
|
|
112
|
+
global: options.global ?? false,
|
|
113
|
+
project: options.project ?? false,
|
|
114
|
+
envKeys: options.env ? Object.keys(options.env) : [],
|
|
115
|
+
mcpInstanceKits: options.mcpInstance ? Object.keys(options.mcpInstance) : [],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// Parse scope from options
|
|
120
|
+
let scope = getScopeFromOptions(options);
|
|
121
|
+
const projectRoot = process.cwd();
|
|
122
|
+
// In non-interactive mode, scope must be specified
|
|
123
|
+
// Exception: --list flag doesn't require scope
|
|
124
|
+
if (!isInteractive && !scope && !options.list) {
|
|
125
|
+
const message = "Scope is required in non-interactive mode. Use --global (-g) or --project (-p).";
|
|
126
|
+
if (options.json) {
|
|
127
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.error(`Error: ${message}`);
|
|
131
|
+
}
|
|
132
|
+
return { success: false, exitCode: ExitCode.ScopeRequired, error: message };
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
// Start interactive UI if applicable
|
|
136
|
+
if (isInteractive) {
|
|
137
|
+
intro("@hashicorp/kits");
|
|
138
|
+
}
|
|
139
|
+
// Fetch source
|
|
140
|
+
const fetchSpinner = isInteractive
|
|
141
|
+
? startSpinner(`Fetching ${formatSource(source)}...`)
|
|
142
|
+
: undefined;
|
|
143
|
+
if (!fetchSpinner && options.verbose) {
|
|
144
|
+
console.error(`Fetching ${source}...`);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
fetchResult = await fetchSource(source);
|
|
148
|
+
if (fetchSpinner) {
|
|
149
|
+
stopSpinnerSuccess(fetchSpinner, `Fetched ${formatSource(source)}`);
|
|
150
|
+
}
|
|
151
|
+
else if (options.verbose) {
|
|
152
|
+
console.error(`Fetched to ${fetchResult.localPath}`);
|
|
153
|
+
}
|
|
154
|
+
void debugLog({
|
|
155
|
+
level: "info",
|
|
156
|
+
event: "install.fetch.success",
|
|
157
|
+
data: { localPath: fetchResult.localPath },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (fetchSpinner) {
|
|
162
|
+
stopSpinnerError(fetchSpinner, "Failed to fetch source");
|
|
163
|
+
}
|
|
164
|
+
if (error instanceof NoFetcherError || error instanceof SourceParseError) {
|
|
165
|
+
const message = error.message;
|
|
166
|
+
if (isInteractive) {
|
|
167
|
+
logError(message);
|
|
168
|
+
outroError("Installation failed");
|
|
169
|
+
}
|
|
170
|
+
else if (options.json) {
|
|
171
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.error(`Error: ${message}`);
|
|
175
|
+
}
|
|
176
|
+
return { success: false, exitCode: ExitCode.SourceNotFound, error: message };
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
// Scan for kits
|
|
181
|
+
const scanSpinner = isInteractive ? startSpinner("Scanning for kits...") : undefined;
|
|
182
|
+
let discovery = await scanKits(fetchResult.localPath, source);
|
|
183
|
+
if (scanSpinner) {
|
|
184
|
+
const kitCount = discovery.kits.length;
|
|
185
|
+
stopSpinnerSuccess(scanSpinner, `Found ${formatCount(kitCount, "kit")}`);
|
|
186
|
+
}
|
|
187
|
+
void debugLog({
|
|
188
|
+
level: "info",
|
|
189
|
+
event: "install.scan.success",
|
|
190
|
+
data: { kits: discovery.kits.map((kit) => kit.name) },
|
|
191
|
+
});
|
|
192
|
+
// Filter by requested kit names
|
|
193
|
+
if (options.kit && options.kit.length > 0) {
|
|
194
|
+
const missing = getDiscoveryMissingKitNames(discovery, options.kit);
|
|
195
|
+
if (missing.length > 0) {
|
|
196
|
+
const message = `Kit${missing.length === 1 ? "" : "s"} not found: ${missing.join(", ")}`;
|
|
197
|
+
if (isInteractive) {
|
|
198
|
+
displayMissingKits(missing, discovery.kits);
|
|
199
|
+
outroError("Installation failed");
|
|
200
|
+
}
|
|
201
|
+
else if (options.json) {
|
|
202
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.error(`Error: ${message}`);
|
|
206
|
+
console.error("Available kits: " + discovery.kits.map((k) => k.name).join(", "));
|
|
207
|
+
}
|
|
208
|
+
return { success: false, exitCode: ExitCode.KitNotFound, error: message };
|
|
209
|
+
}
|
|
210
|
+
discovery = filterDiscoveryKitsByName(discovery, options.kit);
|
|
211
|
+
}
|
|
212
|
+
// Handle --list flag
|
|
213
|
+
if (options.list) {
|
|
214
|
+
if (options.json) {
|
|
215
|
+
displayKitsJson(discovery.kits, discovery.source, discovery.repository);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
displayAvailableKits(discovery.kits);
|
|
219
|
+
}
|
|
220
|
+
return { success: true, exitCode: ExitCode.Success };
|
|
221
|
+
}
|
|
222
|
+
// Check if any kits were found
|
|
223
|
+
if (discovery.kits.length === 0) {
|
|
224
|
+
const message = "No kits found in repository";
|
|
225
|
+
if (isInteractive) {
|
|
226
|
+
displayNoKitsFound();
|
|
227
|
+
outroError("Installation failed");
|
|
228
|
+
}
|
|
229
|
+
else if (options.json) {
|
|
230
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.error(`Error: ${message}`);
|
|
234
|
+
}
|
|
235
|
+
return { success: false, exitCode: ExitCode.KitNotFound, error: message };
|
|
236
|
+
}
|
|
237
|
+
// Interactive kit selection (stage 1)
|
|
238
|
+
let selectedKits = discovery.kits;
|
|
239
|
+
if (isInteractive && !options.kit) {
|
|
240
|
+
const result = await selectKits(discovery.kits, {
|
|
241
|
+
showAvailable: false,
|
|
242
|
+
});
|
|
243
|
+
if (result.cancelled) {
|
|
244
|
+
outroCancel("Installation cancelled");
|
|
245
|
+
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
246
|
+
}
|
|
247
|
+
selectedKits = filterKitsByName(discovery.kits, result.selected);
|
|
248
|
+
}
|
|
249
|
+
void debugLog({
|
|
250
|
+
level: "info",
|
|
251
|
+
event: "install.kits.selected",
|
|
252
|
+
data: { kits: selectedKits.map((kit) => kit.name) },
|
|
253
|
+
});
|
|
254
|
+
// Detect harnesses
|
|
255
|
+
const harnessSpinner = isInteractive ? startSpinner("Detecting AI harnesses...") : undefined;
|
|
256
|
+
const allHarnesses = await detectAllHarnesses();
|
|
257
|
+
const detectedHarnesses = allHarnesses.filter((h) => h.detected);
|
|
258
|
+
if (harnessSpinner) {
|
|
259
|
+
const count = detectedHarnesses.length;
|
|
260
|
+
const message = isInteractive && !options.agent
|
|
261
|
+
? "Detected harnesses"
|
|
262
|
+
: `Found ${formatCount(count, "harness", "harnesses")}`;
|
|
263
|
+
stopSpinnerSuccess(harnessSpinner, message);
|
|
264
|
+
}
|
|
265
|
+
void debugLog({
|
|
266
|
+
level: "info",
|
|
267
|
+
event: "install.harnesses.detected",
|
|
268
|
+
data: { harnesses: detectedHarnesses.map((h) => h.name) },
|
|
269
|
+
});
|
|
270
|
+
// Filter by requested harnesses
|
|
271
|
+
let targetHarnesses = detectedHarnesses;
|
|
272
|
+
if (options.agent && options.agent.length > 0) {
|
|
273
|
+
const missing = getMissingHarnesses(detectedHarnesses, options.agent);
|
|
274
|
+
if (missing.length > 0) {
|
|
275
|
+
if (isInteractive) {
|
|
276
|
+
warnMissingHarnesses(missing);
|
|
277
|
+
}
|
|
278
|
+
else if (options.verbose) {
|
|
279
|
+
const label = missing.length === 1 ? "Harness" : "Harnesses";
|
|
280
|
+
console.error(`Warning: ${label} not detected: ${missing.join(", ")}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
targetHarnesses = filterHarnessesByName(detectedHarnesses, options.agent);
|
|
284
|
+
}
|
|
285
|
+
// Check if any harnesses are available
|
|
286
|
+
if (targetHarnesses.length === 0) {
|
|
287
|
+
const message = "No compatible AI harnesses detected";
|
|
288
|
+
if (isInteractive) {
|
|
289
|
+
displayNoHarnessesDetected();
|
|
290
|
+
outroError("Installation failed");
|
|
291
|
+
}
|
|
292
|
+
else if (options.json) {
|
|
293
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
console.error(`Error: ${message}`);
|
|
297
|
+
}
|
|
298
|
+
return { success: false, exitCode: ExitCode.NoHarnessesDetected, error: message };
|
|
299
|
+
}
|
|
300
|
+
// Interactive harness selection
|
|
301
|
+
let selectedHarnesses = targetHarnesses;
|
|
302
|
+
if (isInteractive && !options.agent) {
|
|
303
|
+
const result = await selectHarnesses(detectedHarnesses, {
|
|
304
|
+
showDetected: true,
|
|
305
|
+
includeAllOption: true,
|
|
306
|
+
});
|
|
307
|
+
if (result.cancelled) {
|
|
308
|
+
outroCancel("Installation cancelled");
|
|
309
|
+
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
310
|
+
}
|
|
311
|
+
selectedHarnesses = filterHarnessesByName(detectedHarnesses, result.selected);
|
|
312
|
+
}
|
|
313
|
+
void debugLog({
|
|
314
|
+
level: "info",
|
|
315
|
+
event: "install.harnesses.selected",
|
|
316
|
+
data: { harnesses: selectedHarnesses.map((h) => h.name) },
|
|
317
|
+
});
|
|
318
|
+
// Interactive scope selection
|
|
319
|
+
let effectiveScope;
|
|
320
|
+
if (scope) {
|
|
321
|
+
// Scope specified via CLI flag
|
|
322
|
+
effectiveScope = scope;
|
|
323
|
+
}
|
|
324
|
+
else if (isInteractive) {
|
|
325
|
+
// Prompt for scope selection
|
|
326
|
+
const scopeResult = await selectScope({ showInfo: true });
|
|
327
|
+
if (scopeResult.cancelled) {
|
|
328
|
+
outroCancel("Installation cancelled");
|
|
329
|
+
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
330
|
+
}
|
|
331
|
+
effectiveScope = scopeResult.scope;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// Default to global scope in non-interactive mode without flag
|
|
335
|
+
// (This shouldn't happen due to earlier check, but handle gracefully)
|
|
336
|
+
effectiveScope = "global";
|
|
337
|
+
}
|
|
338
|
+
void debugLog({
|
|
339
|
+
level: "info",
|
|
340
|
+
event: "install.scope.selected",
|
|
341
|
+
data: { scope: effectiveScope, projectRoot },
|
|
342
|
+
});
|
|
343
|
+
// Compatibility check (after scope selection)
|
|
344
|
+
const compatibilityHarnesses = selectedHarnesses.map((h) => ({
|
|
345
|
+
...h,
|
|
346
|
+
supportedPrimitives: h.adapter.getSupportedPrimitives(effectiveScope),
|
|
347
|
+
}));
|
|
348
|
+
let matrices = buildCompatibilityMatrix(selectedKits, compatibilityHarnesses);
|
|
349
|
+
let compatResult = {
|
|
350
|
+
cancelled: false,
|
|
351
|
+
choice: "proceed",
|
|
352
|
+
};
|
|
353
|
+
let compatiblePairs = [];
|
|
354
|
+
let allowCompatibleOnly = false;
|
|
355
|
+
if (isInteractive) {
|
|
356
|
+
const compat = await runCompatibilityCheck(selectedKits, compatibilityHarnesses, effectiveScope, {
|
|
357
|
+
showMatrix: options.verbose ?? false,
|
|
358
|
+
promptOnIncompatible: true,
|
|
359
|
+
});
|
|
360
|
+
matrices = compat.matrices;
|
|
361
|
+
compatResult = compat.result;
|
|
362
|
+
compatiblePairs = compat.compatiblePairs;
|
|
363
|
+
allowCompatibleOnly = compat.result.choice === "install-compatible";
|
|
364
|
+
}
|
|
365
|
+
if (hasIncompatibilities(matrices)) {
|
|
366
|
+
void debugLog({
|
|
367
|
+
level: "warn",
|
|
368
|
+
event: "install.compatibility.failed",
|
|
369
|
+
data: { scope: effectiveScope },
|
|
370
|
+
});
|
|
371
|
+
if (isInteractive) {
|
|
372
|
+
if (compatResult.cancelled || compatResult.choice === "cancel") {
|
|
373
|
+
outroCancel("Installation cancelled");
|
|
374
|
+
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
375
|
+
}
|
|
376
|
+
if (compatResult.choice === "go-back") {
|
|
377
|
+
outroCancel("Installation cancelled - please run again with different selections");
|
|
378
|
+
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
379
|
+
}
|
|
380
|
+
if (compatResult.choice === "install-compatible") {
|
|
381
|
+
void debugLog({
|
|
382
|
+
level: "info",
|
|
383
|
+
event: "install.compatibility.proceed",
|
|
384
|
+
data: { mode: "compatible-only", pairs: compatiblePairs.length },
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (!isInteractive || !allowCompatibleOnly) {
|
|
389
|
+
const message = "No compatible kit-harness combinations for the selected scope";
|
|
390
|
+
if (options.json) {
|
|
391
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
392
|
+
}
|
|
393
|
+
else if (!isInteractive) {
|
|
394
|
+
console.error(`Error: ${message}`);
|
|
395
|
+
}
|
|
396
|
+
return { success: false, exitCode: ExitCode.KitIncompatible, error: message };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
void debugLog({
|
|
400
|
+
level: "info",
|
|
401
|
+
event: "install.compatibility.ok",
|
|
402
|
+
data: { scope: effectiveScope },
|
|
403
|
+
});
|
|
404
|
+
// Installation summary (interactive only)
|
|
405
|
+
if (isInteractive && !options.dryRun) {
|
|
406
|
+
const planKitNames = allowCompatibleOnly
|
|
407
|
+
? new Set(compatiblePairs.map((pair) => pair.kit))
|
|
408
|
+
: new Set(selectedKits.map((kit) => kit.name));
|
|
409
|
+
const planHarnessNames = allowCompatibleOnly
|
|
410
|
+
? new Set(compatiblePairs.map((pair) => pair.harness))
|
|
411
|
+
: new Set(selectedHarnesses.map((h) => h.name));
|
|
412
|
+
const planKits = selectedKits.filter((kit) => planKitNames.has(kit.name));
|
|
413
|
+
const planHarnesses = selectedHarnesses.filter((h) => planHarnessNames.has(h.name));
|
|
414
|
+
displayInstallPlanSummary(planKits.map((kit) => ({ name: kit.name, version: kit.manifest.version })), planHarnesses.map((h) => ({ displayName: h.displayName })), effectiveScope);
|
|
415
|
+
const proceed = await confirmInstallPlan();
|
|
416
|
+
if (proceed.cancelled || !proceed.proceed) {
|
|
417
|
+
outroCancel("Installation cancelled");
|
|
418
|
+
return { success: false, exitCode: ExitCode.InstallationFailed };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Dry run - show what would be installed
|
|
422
|
+
if (options.dryRun) {
|
|
423
|
+
if (options.json) {
|
|
424
|
+
const output = {
|
|
425
|
+
success: true,
|
|
426
|
+
source,
|
|
427
|
+
scope: effectiveScope,
|
|
428
|
+
harnesses: selectedHarnesses.map((h) => ({
|
|
429
|
+
name: h.name,
|
|
430
|
+
configPath: h.configPath || "",
|
|
431
|
+
kits: selectedKits.map((kit) => ({
|
|
432
|
+
name: kit.name,
|
|
433
|
+
version: kit.manifest.version,
|
|
434
|
+
installed: [],
|
|
435
|
+
})),
|
|
436
|
+
})),
|
|
437
|
+
skipped: [],
|
|
438
|
+
};
|
|
439
|
+
if (effectiveScope === "project") {
|
|
440
|
+
output.projectRoot = projectRoot;
|
|
441
|
+
}
|
|
442
|
+
console.log(JSON.stringify(output, null, 2));
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
displayDryRunSummary(selectedKits.map((k) => ({ name: k.name, version: k.manifest.version })), selectedHarnesses.map((h) => ({ name: h.name, displayName: h.displayName })), { scope: effectiveScope, projectRoot });
|
|
446
|
+
}
|
|
447
|
+
return { success: true, exitCode: ExitCode.Success };
|
|
448
|
+
}
|
|
449
|
+
// Execute installation
|
|
450
|
+
const installResults = await executeInstallation(selectedKits, selectedHarnesses, discovery, source, {
|
|
451
|
+
isInteractive,
|
|
452
|
+
verbose: options.verbose ?? false,
|
|
453
|
+
json: options.json ?? false,
|
|
454
|
+
scope: effectiveScope,
|
|
455
|
+
projectRoot,
|
|
456
|
+
env: options.env ?? {},
|
|
457
|
+
mcpInstance: options.mcpInstance ?? {},
|
|
458
|
+
allowCompatibleOnly,
|
|
459
|
+
compatiblePairs: allowCompatibleOnly ? compatiblePairs : [],
|
|
460
|
+
});
|
|
461
|
+
if (!installResults.success) {
|
|
462
|
+
const errorMsg = installResults.error || "Installation failed";
|
|
463
|
+
const exitCode = installResults.exitCode ?? ExitCode.InstallationFailed;
|
|
464
|
+
void debugLog({
|
|
465
|
+
level: "error",
|
|
466
|
+
event: "install.failed",
|
|
467
|
+
message: errorMsg,
|
|
468
|
+
});
|
|
469
|
+
if (isInteractive) {
|
|
470
|
+
outroError(errorMsg);
|
|
471
|
+
}
|
|
472
|
+
else if (options.json) {
|
|
473
|
+
console.log(JSON.stringify({ success: false, error: errorMsg }));
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
console.error(`Error: ${errorMsg}`);
|
|
477
|
+
}
|
|
478
|
+
return { success: false, exitCode, error: errorMsg };
|
|
479
|
+
}
|
|
480
|
+
// Success output
|
|
481
|
+
void debugLog({
|
|
482
|
+
level: "info",
|
|
483
|
+
event: "install.success",
|
|
484
|
+
data: {
|
|
485
|
+
scope: effectiveScope,
|
|
486
|
+
harnesses: selectedHarnesses.map((h) => h.name),
|
|
487
|
+
kits: selectedKits.map((k) => k.name),
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
if (options.json) {
|
|
491
|
+
console.log(JSON.stringify(installResults.output, null, 2));
|
|
492
|
+
}
|
|
493
|
+
else if (isInteractive) {
|
|
494
|
+
outroSuccess("Installation complete!");
|
|
495
|
+
}
|
|
496
|
+
const result = { success: true, exitCode: ExitCode.Success };
|
|
497
|
+
if (installResults.output) {
|
|
498
|
+
result.output = installResults.output;
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
504
|
+
if (isInteractive) {
|
|
505
|
+
logError(message);
|
|
506
|
+
outroError("Installation failed");
|
|
507
|
+
}
|
|
508
|
+
else if (options.json) {
|
|
509
|
+
console.log(JSON.stringify({ success: false, error: message }));
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
console.error(`Error: ${message}`);
|
|
513
|
+
}
|
|
514
|
+
return { success: false, exitCode: ExitCode.InstallationFailed, error: message };
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
// Clean up fetched source
|
|
518
|
+
if (fetchResult?.cleanup) {
|
|
519
|
+
try {
|
|
520
|
+
await fetchResult.cleanup();
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// Ignore cleanup errors
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Load MCP server config from a resolved primitive's source path.
|
|
530
|
+
*
|
|
531
|
+
* @param sourcePath - Path to the MCP primitive directory
|
|
532
|
+
* @returns MCP server config or null if not found/invalid
|
|
533
|
+
*/
|
|
534
|
+
async function loadMcpConfigFromPath(sourcePath) {
|
|
535
|
+
const fs = await import("node:fs/promises");
|
|
536
|
+
// Try config.json in the primitive directory
|
|
537
|
+
const configPath = path.join(sourcePath, "config.json");
|
|
538
|
+
try {
|
|
539
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
540
|
+
return JSON.parse(content);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// If config.json doesn't exist, the sourcePath might BE the config file
|
|
544
|
+
// (for inline primitives that point directly to a config.json)
|
|
545
|
+
if (sourcePath.endsWith(".json")) {
|
|
546
|
+
try {
|
|
547
|
+
const content = await fs.readFile(sourcePath, "utf-8");
|
|
548
|
+
return JSON.parse(content);
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Collect all environment variable definitions from MCP primitives.
|
|
559
|
+
*
|
|
560
|
+
* @param primitives - Resolved primitives (from multiple kits)
|
|
561
|
+
* @returns Merged record of all env var definitions
|
|
562
|
+
*/
|
|
563
|
+
async function collectAllMcpEnvDefs(primitives) {
|
|
564
|
+
const configs = [];
|
|
565
|
+
for (const primitive of primitives) {
|
|
566
|
+
if (primitive.type !== "mcp")
|
|
567
|
+
continue;
|
|
568
|
+
const config = await loadMcpConfigFromPath(primitive.sourcePath);
|
|
569
|
+
if (config) {
|
|
570
|
+
configs.push(config);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return mergeEnvDefs(configs);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Resolve environment variables for all MCP primitives.
|
|
577
|
+
*
|
|
578
|
+
* This function iterates through all MCP primitives and resolves their
|
|
579
|
+
* environment variable definitions to values using the resolution priority:
|
|
580
|
+
* 1. Sensitive vars: Always passthrough
|
|
581
|
+
* 2. CLI --env flags: For nonsensitive vars only
|
|
582
|
+
* 3. process.env: Current environment value
|
|
583
|
+
* 4. Default value: If specified
|
|
584
|
+
* 5. Passthrough: If no value resolved
|
|
585
|
+
*
|
|
586
|
+
* @param primitives - Resolved primitives (from multiple kits)
|
|
587
|
+
* @param cliEnvFlags - CLI --env flag values
|
|
588
|
+
* @returns Array of env var resolution results for each MCP server
|
|
589
|
+
*/
|
|
590
|
+
async function resolveMcpEnvVars(primitives, cliEnvFlags, instanceOverrides) {
|
|
591
|
+
const resolutions = [];
|
|
592
|
+
const seenInstances = new Set();
|
|
593
|
+
for (const primitive of primitives) {
|
|
594
|
+
if (primitive.type !== "mcp")
|
|
595
|
+
continue;
|
|
596
|
+
const config = await loadMcpConfigFromPath(primitive.sourcePath);
|
|
597
|
+
if (!config)
|
|
598
|
+
continue;
|
|
599
|
+
const instanceName = resolveInstanceName(config.name, primitive.instanceName, instanceOverrides, primitive.kitName);
|
|
600
|
+
if (!instanceName) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (seenInstances.has(instanceName)) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
seenInstances.add(instanceName);
|
|
607
|
+
const resolution = resolveEnvVarsFromConfig(instanceName, config, cliEnvFlags);
|
|
608
|
+
resolutions.push(resolution);
|
|
609
|
+
}
|
|
610
|
+
return resolutions;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Build TUI env var requirements from resolution results.
|
|
614
|
+
*
|
|
615
|
+
* Converts the resolution results into the format expected by the TUI
|
|
616
|
+
* prompting system, including sensitivity classification.
|
|
617
|
+
*
|
|
618
|
+
* @param resolutions - Env var resolution results from resolveMcpEnvVars
|
|
619
|
+
* @returns Array of env var requirements for TUI prompting
|
|
620
|
+
*/
|
|
621
|
+
function buildEnvVarRequirements(resolutions) {
|
|
622
|
+
const requirements = [];
|
|
623
|
+
const seen = new Set();
|
|
624
|
+
for (const resolution of resolutions) {
|
|
625
|
+
for (const resolved of resolution.resolved) {
|
|
626
|
+
// Deduplicate by name (same var may be used by multiple MCP servers)
|
|
627
|
+
if (seen.has(resolved.name))
|
|
628
|
+
continue;
|
|
629
|
+
seen.add(resolved.name);
|
|
630
|
+
requirements.push({
|
|
631
|
+
name: resolved.name,
|
|
632
|
+
description: resolved.description,
|
|
633
|
+
required: resolved.required,
|
|
634
|
+
sensitive: resolved.sensitive,
|
|
635
|
+
defaultValue: resolved.defaultValue,
|
|
636
|
+
currentValue: process.env[resolved.name],
|
|
637
|
+
source: resolution.mcpServer,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return requirements;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Apply TUI prompt results back to resolution results.
|
|
645
|
+
*
|
|
646
|
+
* Updates the resolution results with values provided by the user through
|
|
647
|
+
* the TUI prompting flow. Only updates nonsensitive vars.
|
|
648
|
+
*
|
|
649
|
+
* @param resolutions - Original resolution results to update
|
|
650
|
+
* @param promptResults - Results from TUI prompting
|
|
651
|
+
*/
|
|
652
|
+
function applyPromptResultsToResolutions(resolutions, promptResults) {
|
|
653
|
+
for (const result of promptResults.variables) {
|
|
654
|
+
// Update all resolutions that have this var
|
|
655
|
+
for (const resolution of resolutions) {
|
|
656
|
+
const resolved = resolution.resolved.find((r) => r.name === result.name);
|
|
657
|
+
if (!resolved)
|
|
658
|
+
continue;
|
|
659
|
+
switch (result.choice) {
|
|
660
|
+
case "use-existing": {
|
|
661
|
+
if ("value" in resolved) {
|
|
662
|
+
delete resolved.value;
|
|
663
|
+
}
|
|
664
|
+
resolved.usePassthrough = true;
|
|
665
|
+
resolved.source = "environment";
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case "use-default": {
|
|
669
|
+
if (result.value !== undefined) {
|
|
670
|
+
resolved.value = result.value;
|
|
671
|
+
}
|
|
672
|
+
else if ("value" in resolved) {
|
|
673
|
+
delete resolved.value;
|
|
674
|
+
}
|
|
675
|
+
resolved.usePassthrough = false;
|
|
676
|
+
resolved.source = "default";
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
case "enter-new": {
|
|
680
|
+
if (result.value !== undefined) {
|
|
681
|
+
resolved.value = result.value;
|
|
682
|
+
}
|
|
683
|
+
else if ("value" in resolved) {
|
|
684
|
+
delete resolved.value;
|
|
685
|
+
}
|
|
686
|
+
resolved.usePassthrough = false;
|
|
687
|
+
resolved.source = "user-input";
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case "skip":
|
|
691
|
+
default: {
|
|
692
|
+
if ("value" in resolved) {
|
|
693
|
+
delete resolved.value;
|
|
694
|
+
}
|
|
695
|
+
resolved.usePassthrough = true;
|
|
696
|
+
resolved.source = "passthrough";
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// Remove from missingRequired if now satisfied or passthrough selected
|
|
701
|
+
const idx = resolution.missingRequired.indexOf(result.name);
|
|
702
|
+
if (idx !== -1) {
|
|
703
|
+
resolution.missingRequired.splice(idx, 1);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function resolveEnvForConfig(config, cliEnvFlags, promptResults, instanceNameOverride) {
|
|
709
|
+
const resolution = resolveEnvVarsFromConfig(instanceNameOverride ?? config.name, config, cliEnvFlags);
|
|
710
|
+
if (promptResults) {
|
|
711
|
+
applyPromptResultsToResolutions([resolution], promptResults);
|
|
712
|
+
}
|
|
713
|
+
return resolution;
|
|
714
|
+
}
|
|
715
|
+
function resolveInstanceName(configName, primitiveOverride, cliOverrides, kitName) {
|
|
716
|
+
const baseName = primitiveOverride ?? configName;
|
|
717
|
+
if (cliOverrides && kitName && cliOverrides[kitName]?.[baseName]) {
|
|
718
|
+
return cliOverrides[kitName][baseName];
|
|
719
|
+
}
|
|
720
|
+
return baseName;
|
|
721
|
+
}
|
|
722
|
+
function resolveHookInstanceName(programName, primitiveOverride, kitName) {
|
|
723
|
+
if (primitiveOverride) {
|
|
724
|
+
return primitiveOverride;
|
|
725
|
+
}
|
|
726
|
+
if (kitName) {
|
|
727
|
+
return `${kitName}.${programName}`;
|
|
728
|
+
}
|
|
729
|
+
return programName;
|
|
730
|
+
}
|
|
731
|
+
function buildPrimitiveKey(kitName, sourcePath) {
|
|
732
|
+
return `${kitName}::${sourcePath}`;
|
|
733
|
+
}
|
|
734
|
+
async function buildMcpPrimitiveInfoBySourcePath(resolvedByKit, cliEnvFlags, promptResults, instanceOverrides) {
|
|
735
|
+
const infoBySourcePath = new Map();
|
|
736
|
+
for (const [kitName, primitives] of resolvedByKit.entries()) {
|
|
737
|
+
for (const primitive of primitives) {
|
|
738
|
+
if (primitive.type !== "mcp")
|
|
739
|
+
continue;
|
|
740
|
+
const key = buildPrimitiveKey(kitName, primitive.sourcePath);
|
|
741
|
+
if (infoBySourcePath.has(key))
|
|
742
|
+
continue;
|
|
743
|
+
const config = await loadMcpConfigFromPath(primitive.sourcePath);
|
|
744
|
+
if (!config?.name)
|
|
745
|
+
continue;
|
|
746
|
+
const instanceName = resolveInstanceName(config.name, primitive.instanceName, instanceOverrides, kitName);
|
|
747
|
+
const resolution = resolveEnvForConfig(config, cliEnvFlags, promptResults, instanceName);
|
|
748
|
+
const configHash = hashMcpConfig(config, resolution.resolved, resolution.resolvedHeaders);
|
|
749
|
+
infoBySourcePath.set(key, {
|
|
750
|
+
instanceName,
|
|
751
|
+
configHash,
|
|
752
|
+
config,
|
|
753
|
+
resolvedEnv: resolution.resolved,
|
|
754
|
+
resolvedHeaders: resolution.resolvedHeaders,
|
|
755
|
+
primitiveName: primitive.name,
|
|
756
|
+
version: primitive.resolvedVersion ?? config.version,
|
|
757
|
+
configName: config.name,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return infoBySourcePath;
|
|
762
|
+
}
|
|
763
|
+
async function buildHookPrimitiveInfoBySourcePath(resolvedByKit) {
|
|
764
|
+
const infoBySourcePath = new Map();
|
|
765
|
+
for (const [kitName, primitives] of resolvedByKit.entries()) {
|
|
766
|
+
for (const primitive of primitives) {
|
|
767
|
+
if (primitive.type !== "hooks")
|
|
768
|
+
continue;
|
|
769
|
+
if (!primitive.hookProgram || !primitive.hookBinding)
|
|
770
|
+
continue;
|
|
771
|
+
const key = buildPrimitiveKey(kitName, primitive.sourcePath);
|
|
772
|
+
if (infoBySourcePath.has(key))
|
|
773
|
+
continue;
|
|
774
|
+
const instanceName = resolveHookInstanceName(primitive.hookProgram.name ?? primitive.name, primitive.instanceName, kitName);
|
|
775
|
+
const configHash = hashHookConfig(primitive.hookProgram, primitive.hookBinding);
|
|
776
|
+
infoBySourcePath.set(key, {
|
|
777
|
+
instanceName,
|
|
778
|
+
configHash,
|
|
779
|
+
program: primitive.hookProgram,
|
|
780
|
+
binding: primitive.hookBinding,
|
|
781
|
+
primitiveName: primitive.name,
|
|
782
|
+
version: primitive.resolvedVersion ?? primitive.hookProgram.version,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return infoBySourcePath;
|
|
787
|
+
}
|
|
788
|
+
async function buildMcpUsageByInstance(resolvedByKit, instanceOverrides) {
|
|
789
|
+
const usage = new Map();
|
|
790
|
+
for (const [kitName, primitives] of resolvedByKit.entries()) {
|
|
791
|
+
for (const primitive of primitives) {
|
|
792
|
+
if (primitive.type !== "mcp")
|
|
793
|
+
continue;
|
|
794
|
+
const config = await loadMcpConfigFromPath(primitive.sourcePath);
|
|
795
|
+
if (!config?.name)
|
|
796
|
+
continue;
|
|
797
|
+
const instanceName = resolveInstanceName(config.name, primitive.instanceName, instanceOverrides, kitName);
|
|
798
|
+
const existing = usage.get(instanceName);
|
|
799
|
+
if (existing) {
|
|
800
|
+
existing.add(kitName);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
usage.set(instanceName, new Set([kitName]));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return usage;
|
|
808
|
+
}
|
|
809
|
+
function getMcpConfigPath(adapter, scope, projectRoot) {
|
|
810
|
+
const configLocations = adapter.getConfigLocations(scope);
|
|
811
|
+
const mcpPath = configLocations.mcp || configLocations.primary;
|
|
812
|
+
if (scope === "project" && projectRoot) {
|
|
813
|
+
return path.join(projectRoot, mcpPath.replace(/^\.\//, ""));
|
|
814
|
+
}
|
|
815
|
+
return expandPath(mcpPath);
|
|
816
|
+
}
|
|
817
|
+
function getHookConfigPath(adapter, scope, projectRoot) {
|
|
818
|
+
const paths = adapter.getInstallationPaths(scope);
|
|
819
|
+
const hookPath = paths.hooks ?? adapter.getConfigLocations(scope).primary;
|
|
820
|
+
if (scope === "project" && projectRoot) {
|
|
821
|
+
return path.join(projectRoot, hookPath.replace(/^\.\//, ""));
|
|
822
|
+
}
|
|
823
|
+
return expandPath(hookPath);
|
|
824
|
+
}
|
|
825
|
+
async function promptMcpConflictResolution(options) {
|
|
826
|
+
const choice = await clack.select({
|
|
827
|
+
message: `MCP instance "${options.instanceName}" already exists in ${options.harnessDisplayName} with different configuration. How would you like to proceed?`,
|
|
828
|
+
options: [
|
|
829
|
+
{ value: "reuse", label: "Reuse existing instance" },
|
|
830
|
+
{ value: "fork", label: "Create a new instance name" },
|
|
831
|
+
{ value: "cancel", label: "Cancel installation" },
|
|
832
|
+
],
|
|
833
|
+
});
|
|
834
|
+
if (clack.isCancel(choice)) {
|
|
835
|
+
return "cancel";
|
|
836
|
+
}
|
|
837
|
+
return choice;
|
|
838
|
+
}
|
|
839
|
+
async function promptForMcpInstanceName(instanceName, usedNames) {
|
|
840
|
+
while (true) {
|
|
841
|
+
const value = await clack.text({
|
|
842
|
+
message: `Enter a new name for "${instanceName}":`,
|
|
843
|
+
placeholder: `${instanceName}-alt`,
|
|
844
|
+
});
|
|
845
|
+
if (clack.isCancel(value)) {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
const trimmed = String(value).trim();
|
|
849
|
+
if (!trimmed) {
|
|
850
|
+
clack.log.warn("Instance name cannot be empty.");
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
if (usedNames.has(trimmed)) {
|
|
854
|
+
clack.log.warn(`Instance name "${trimmed}" is already in use.`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
return trimmed;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Execute the actual installation of kits to harnesses.
|
|
862
|
+
*/
|
|
863
|
+
async function executeInstallation(kits, harnesses, discovery, source, options) {
|
|
864
|
+
const { isInteractive, verbose, scope, projectRoot, env, mcpInstance, allowCompatibleOnly = false, compatiblePairs = [], } = options;
|
|
865
|
+
// Load or create primitives registry loader
|
|
866
|
+
let registryLoader = null;
|
|
867
|
+
if (discovery.primitivesRegistry) {
|
|
868
|
+
registryLoader = new PrimitivesRegistryLoader(discovery.localPath);
|
|
869
|
+
await registryLoader.load();
|
|
870
|
+
}
|
|
871
|
+
// Pre-pass: resolve all primitives to collect MCP env var definitions for validation
|
|
872
|
+
const allResolvedPrimitives = [];
|
|
873
|
+
const resolvedByKit = new Map();
|
|
874
|
+
const resolutionFailures = [];
|
|
875
|
+
for (const kit of kits) {
|
|
876
|
+
if (registryLoader) {
|
|
877
|
+
const kitBasePath = path.join(discovery.localPath, kit.path);
|
|
878
|
+
const resolution = await resolvePrimitiveReferences(kit.manifest, registryLoader, kitBasePath);
|
|
879
|
+
if (resolution.errors.length > 0) {
|
|
880
|
+
resolutionFailures.push({
|
|
881
|
+
kit: kit.name,
|
|
882
|
+
errors: resolution.errors.map((e) => e.message),
|
|
883
|
+
});
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
resolvedByKit.set(kit.name, resolution.primitives);
|
|
887
|
+
allResolvedPrimitives.push(...resolution.primitives);
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
const inlinePrimitives = resolveInlinePrimitivesOnly(kit, discovery.localPath);
|
|
891
|
+
resolvedByKit.set(kit.name, inlinePrimitives);
|
|
892
|
+
allResolvedPrimitives.push(...inlinePrimitives);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (resolutionFailures.length > 0) {
|
|
896
|
+
const details = resolutionFailures
|
|
897
|
+
.map((f) => `${f.kit}: ${f.errors.join("; ")}`)
|
|
898
|
+
.join(" | ");
|
|
899
|
+
return {
|
|
900
|
+
success: false,
|
|
901
|
+
exitCode: ExitCode.ResolutionFailed,
|
|
902
|
+
error: `Resolution failed: ${details}`,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
// Collect all MCP env var definitions for validation
|
|
906
|
+
const allEnvDefs = await collectAllMcpEnvDefs(allResolvedPrimitives);
|
|
907
|
+
// Validate --env flags against sensitive vars (exit code 13)
|
|
908
|
+
if (env && Object.keys(env).length > 0) {
|
|
909
|
+
const invalidFlags = validateCliEnvFlags(env, allEnvDefs);
|
|
910
|
+
if (invalidFlags.length > 0) {
|
|
911
|
+
const message = `Cannot provide sensitive variables via --env flag: ${invalidFlags.join(", ")}. Sensitive variables must be set in your shell environment.`;
|
|
912
|
+
return {
|
|
913
|
+
success: false,
|
|
914
|
+
exitCode: ExitCode.SensitiveEnvVarProvided,
|
|
915
|
+
error: message,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
// In non-interactive mode, validate required nonsensitive vars are provided (exit code 12)
|
|
920
|
+
if (!isInteractive) {
|
|
921
|
+
// Check for missing required nonsensitive vars
|
|
922
|
+
// A var is considered "provided" if it's in --env flags, process.env, or has a default
|
|
923
|
+
const missingRequired = [];
|
|
924
|
+
for (const [name, def] of Object.entries(allEnvDefs)) {
|
|
925
|
+
// Skip sensitive vars (they're always passthrough, handled differently)
|
|
926
|
+
if (def.sensitive)
|
|
927
|
+
continue;
|
|
928
|
+
// Skip if not required
|
|
929
|
+
if (!def.required)
|
|
930
|
+
continue;
|
|
931
|
+
// Check if value is available from any source
|
|
932
|
+
const hasCliValue = env && name in env;
|
|
933
|
+
const hasEnvValue = !!process.env[name];
|
|
934
|
+
const hasDefault = def.default !== undefined;
|
|
935
|
+
if (!hasCliValue && !hasEnvValue && !hasDefault) {
|
|
936
|
+
missingRequired.push(name);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (missingRequired.length > 0) {
|
|
940
|
+
const message = `Required environment variables not provided: ${missingRequired.join(", ")}. Use --env KEY=VALUE to provide values, or run interactively.`;
|
|
941
|
+
return {
|
|
942
|
+
success: false,
|
|
943
|
+
exitCode: ExitCode.EnvVarRequired,
|
|
944
|
+
error: message,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
let effectiveMcpInstance = mcpInstance ?? {};
|
|
949
|
+
if (isInteractive) {
|
|
950
|
+
const usage = await buildMcpUsageByInstance(resolvedByKit, effectiveMcpInstance);
|
|
951
|
+
const sharedPrompt = await promptForSharedMcpInstances(usage, effectiveMcpInstance);
|
|
952
|
+
if (sharedPrompt.cancelled) {
|
|
953
|
+
return {
|
|
954
|
+
success: false,
|
|
955
|
+
exitCode: ExitCode.UserCancelled,
|
|
956
|
+
error: "Environment variable configuration cancelled",
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (Object.keys(sharedPrompt.overrides).length > 0) {
|
|
960
|
+
for (const [kitName, overrides] of Object.entries(sharedPrompt.overrides)) {
|
|
961
|
+
if (!effectiveMcpInstance[kitName]) {
|
|
962
|
+
effectiveMcpInstance[kitName] = {};
|
|
963
|
+
}
|
|
964
|
+
for (const [instanceName, override] of Object.entries(overrides)) {
|
|
965
|
+
if (!effectiveMcpInstance[kitName][instanceName]) {
|
|
966
|
+
effectiveMcpInstance[kitName][instanceName] = override;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Resolve environment variables for all MCP primitives
|
|
973
|
+
const envResolutions = await resolveMcpEnvVars(allResolvedPrimitives, env, effectiveMcpInstance);
|
|
974
|
+
let envPromptResults;
|
|
975
|
+
// Interactive mode: prompt for nonsensitive vars
|
|
976
|
+
if (isInteractive && envResolutions.length > 0) {
|
|
977
|
+
const requirements = buildEnvVarRequirements(envResolutions);
|
|
978
|
+
if (requirements.length > 0) {
|
|
979
|
+
logInfo("Configuring environment variables for MCP servers...");
|
|
980
|
+
const sourceToKits = await buildMcpUsageByInstance(resolvedByKit, effectiveMcpInstance);
|
|
981
|
+
const promptResults = await promptForEnvVars(requirements, {
|
|
982
|
+
sourceToKits,
|
|
983
|
+
skipSatisfied: false,
|
|
984
|
+
skipOptional: false,
|
|
985
|
+
});
|
|
986
|
+
if (promptResults.cancelled) {
|
|
987
|
+
return {
|
|
988
|
+
success: false,
|
|
989
|
+
exitCode: ExitCode.UserCancelled,
|
|
990
|
+
error: "Environment variable configuration cancelled",
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
envPromptResults = promptResults;
|
|
994
|
+
applyPromptResultsToResolutions(envResolutions, promptResults);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
// Read or create manifest for the specified scope
|
|
998
|
+
let manifest = await readManifest(scope, scope === "project" ? projectRoot : undefined);
|
|
999
|
+
if (!manifest) {
|
|
1000
|
+
manifest = createEmptyManifest(scope, scope === "project" ? projectRoot : undefined);
|
|
1001
|
+
}
|
|
1002
|
+
manifest.source = source;
|
|
1003
|
+
const harnessResults = [];
|
|
1004
|
+
const skippedResults = [];
|
|
1005
|
+
const compatibleKitsByHarness = new Map();
|
|
1006
|
+
for (const pair of compatiblePairs) {
|
|
1007
|
+
const existing = compatibleKitsByHarness.get(pair.harness);
|
|
1008
|
+
if (existing) {
|
|
1009
|
+
existing.add(pair.kit);
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
compatibleKitsByHarness.set(pair.harness, new Set([pair.kit]));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const mcpInfoByPrimitiveKey = await buildMcpPrimitiveInfoBySourcePath(resolvedByKit, env, envPromptResults, effectiveMcpInstance);
|
|
1016
|
+
const mcpAssignmentsByHarness = new Map();
|
|
1017
|
+
const mcpForkedInstancesByHarness = new Map();
|
|
1018
|
+
const mcpInstancesToInstallByHarness = new Map();
|
|
1019
|
+
const hookInfoByPrimitiveKey = await buildHookPrimitiveInfoBySourcePath(resolvedByKit);
|
|
1020
|
+
const hookAssignmentsByHarness = new Map();
|
|
1021
|
+
const hookInstancesToInstallByHarness = new Map();
|
|
1022
|
+
for (const harness of harnesses) {
|
|
1023
|
+
const kitsToCheck = allowCompatibleOnly
|
|
1024
|
+
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.name))
|
|
1025
|
+
: kits;
|
|
1026
|
+
if (kitsToCheck.length === 0 || mcpInfoByPrimitiveKey.size === 0) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const desiredByName = new Map();
|
|
1030
|
+
for (const kit of kitsToCheck) {
|
|
1031
|
+
const primitives = resolvedByKit.get(kit.name) ?? [];
|
|
1032
|
+
for (const primitive of primitives) {
|
|
1033
|
+
if (primitive.type !== "mcp")
|
|
1034
|
+
continue;
|
|
1035
|
+
const key = buildPrimitiveKey(kit.name, primitive.sourcePath);
|
|
1036
|
+
const info = mcpInfoByPrimitiveKey.get(key);
|
|
1037
|
+
if (!info)
|
|
1038
|
+
continue;
|
|
1039
|
+
const byHash = desiredByName.get(info.instanceName) ?? new Map();
|
|
1040
|
+
const entry = byHash.get(info.configHash) ?? {
|
|
1041
|
+
primitives: [],
|
|
1042
|
+
kits: new Set(),
|
|
1043
|
+
info,
|
|
1044
|
+
};
|
|
1045
|
+
entry.primitives.push({ primitive, kitName: kit.name });
|
|
1046
|
+
entry.kits.add(kit.name);
|
|
1047
|
+
byHash.set(info.configHash, entry);
|
|
1048
|
+
desiredByName.set(info.instanceName, byHash);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
for (const [instanceName, byHash] of desiredByName) {
|
|
1052
|
+
if (byHash.size > 1) {
|
|
1053
|
+
const message = `Conflicting MCP server configs detected for instance "${instanceName}". ` +
|
|
1054
|
+
"Resolve the conflict by renaming one of the MCP server instances.";
|
|
1055
|
+
return {
|
|
1056
|
+
success: false,
|
|
1057
|
+
exitCode: ExitCode.InstallationFailed,
|
|
1058
|
+
error: message,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const existingInstances = manifest.harnesses[harness.name]?.mcpInstances ?? {};
|
|
1063
|
+
const usedInstanceNames = new Set(Object.keys(existingInstances));
|
|
1064
|
+
const assignments = new Map();
|
|
1065
|
+
const forkedInstances = new Map();
|
|
1066
|
+
const instancesToInstall = new Set();
|
|
1067
|
+
for (const [instanceName, byHash] of desiredByName) {
|
|
1068
|
+
const candidate = Array.from(byHash.values())[0];
|
|
1069
|
+
let finalName = instanceName;
|
|
1070
|
+
let finalHash = candidate.info.configHash;
|
|
1071
|
+
let action = "install";
|
|
1072
|
+
const existing = existingInstances[instanceName];
|
|
1073
|
+
if (existing) {
|
|
1074
|
+
if (existing.configHash === candidate.info.configHash) {
|
|
1075
|
+
action = "reuse";
|
|
1076
|
+
finalHash = existing.configHash;
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
if (!isInteractive) {
|
|
1080
|
+
return {
|
|
1081
|
+
success: false,
|
|
1082
|
+
exitCode: ExitCode.InstallationFailed,
|
|
1083
|
+
error: `MCP instance "${instanceName}" already exists in ${harness.displayName} with a different configuration.`,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
const decision = await promptMcpConflictResolution({
|
|
1087
|
+
instanceName,
|
|
1088
|
+
harnessDisplayName: harness.displayName,
|
|
1089
|
+
});
|
|
1090
|
+
if (decision === "cancel") {
|
|
1091
|
+
return {
|
|
1092
|
+
success: false,
|
|
1093
|
+
exitCode: ExitCode.UserCancelled,
|
|
1094
|
+
error: "Installation cancelled",
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
if (decision === "reuse") {
|
|
1098
|
+
action = "reuse";
|
|
1099
|
+
finalHash = existing.configHash;
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
const newName = await promptForMcpInstanceName(instanceName, usedInstanceNames);
|
|
1103
|
+
if (!newName) {
|
|
1104
|
+
return {
|
|
1105
|
+
success: false,
|
|
1106
|
+
exitCode: ExitCode.UserCancelled,
|
|
1107
|
+
error: "Installation cancelled",
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
finalName = newName;
|
|
1111
|
+
action = "install";
|
|
1112
|
+
finalHash = candidate.info.configHash;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (action === "install") {
|
|
1117
|
+
instancesToInstall.add(finalName);
|
|
1118
|
+
usedInstanceNames.add(finalName);
|
|
1119
|
+
}
|
|
1120
|
+
for (const { primitive, kitName } of candidate.primitives) {
|
|
1121
|
+
const key = buildPrimitiveKey(kitName, primitive.sourcePath);
|
|
1122
|
+
assignments.set(key, {
|
|
1123
|
+
instanceName: finalName,
|
|
1124
|
+
configHash: finalHash,
|
|
1125
|
+
action,
|
|
1126
|
+
});
|
|
1127
|
+
if (finalName !== candidate.info.instanceName) {
|
|
1128
|
+
forkedInstances.set(finalName, candidate.info.instanceName);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (assignments.size > 0) {
|
|
1133
|
+
mcpAssignmentsByHarness.set(harness.name, assignments);
|
|
1134
|
+
}
|
|
1135
|
+
if (forkedInstances.size > 0) {
|
|
1136
|
+
mcpForkedInstancesByHarness.set(harness.name, forkedInstances);
|
|
1137
|
+
}
|
|
1138
|
+
if (instancesToInstall.size > 0) {
|
|
1139
|
+
mcpInstancesToInstallByHarness.set(harness.name, instancesToInstall);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (envResolutions.length > 0 && mcpForkedInstancesByHarness.size > 0) {
|
|
1143
|
+
const addedOverrides = new Set();
|
|
1144
|
+
for (const forkedInstances of mcpForkedInstancesByHarness.values()) {
|
|
1145
|
+
for (const [overrideName, baseName] of forkedInstances.entries()) {
|
|
1146
|
+
if (addedOverrides.has(overrideName))
|
|
1147
|
+
continue;
|
|
1148
|
+
const base = envResolutions.find((r) => r.mcpServer === baseName);
|
|
1149
|
+
if (!base)
|
|
1150
|
+
continue;
|
|
1151
|
+
envResolutions.push({
|
|
1152
|
+
mcpServer: overrideName,
|
|
1153
|
+
resolved: [...base.resolved],
|
|
1154
|
+
resolvedHeaders: [...base.resolvedHeaders],
|
|
1155
|
+
missingRequired: [...base.missingRequired],
|
|
1156
|
+
warnings: [...base.warnings],
|
|
1157
|
+
});
|
|
1158
|
+
addedOverrides.add(overrideName);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
for (const harness of harnesses) {
|
|
1163
|
+
const kitsToCheck = allowCompatibleOnly
|
|
1164
|
+
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.name))
|
|
1165
|
+
: kits;
|
|
1166
|
+
if (kitsToCheck.length === 0 || hookInfoByPrimitiveKey.size === 0) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const desiredByName = new Map();
|
|
1170
|
+
for (const kit of kitsToCheck) {
|
|
1171
|
+
const primitives = resolvedByKit.get(kit.name) ?? [];
|
|
1172
|
+
for (const primitive of primitives) {
|
|
1173
|
+
if (primitive.type !== "hooks")
|
|
1174
|
+
continue;
|
|
1175
|
+
const key = buildPrimitiveKey(kit.name, primitive.sourcePath);
|
|
1176
|
+
const info = hookInfoByPrimitiveKey.get(key);
|
|
1177
|
+
if (!info)
|
|
1178
|
+
continue;
|
|
1179
|
+
const byHash = desiredByName.get(info.instanceName) ?? new Map();
|
|
1180
|
+
const entry = byHash.get(info.configHash) ?? {
|
|
1181
|
+
primitives: [],
|
|
1182
|
+
kits: new Set(),
|
|
1183
|
+
info,
|
|
1184
|
+
};
|
|
1185
|
+
entry.primitives.push({ primitive, kitName: kit.name });
|
|
1186
|
+
entry.kits.add(kit.name);
|
|
1187
|
+
byHash.set(info.configHash, entry);
|
|
1188
|
+
desiredByName.set(info.instanceName, byHash);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
for (const [instanceName, byHash] of desiredByName) {
|
|
1192
|
+
if (byHash.size > 1) {
|
|
1193
|
+
const message = `Conflicting hook configs detected for instance "${instanceName}". ` +
|
|
1194
|
+
"Resolve the conflict by renaming one of the hook instances.";
|
|
1195
|
+
return {
|
|
1196
|
+
success: false,
|
|
1197
|
+
exitCode: ExitCode.InstallationFailed,
|
|
1198
|
+
error: message,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const existingInstances = manifest.harnesses[harness.name]?.hookInstances ?? {};
|
|
1203
|
+
const assignments = new Map();
|
|
1204
|
+
const instancesToInstall = new Set();
|
|
1205
|
+
for (const [instanceName, byHash] of desiredByName) {
|
|
1206
|
+
const candidate = Array.from(byHash.values())[0];
|
|
1207
|
+
let action = "install";
|
|
1208
|
+
let finalHash = candidate.info.configHash;
|
|
1209
|
+
const existing = existingInstances[instanceName];
|
|
1210
|
+
if (existing) {
|
|
1211
|
+
if (existing.configHash !== candidate.info.configHash) {
|
|
1212
|
+
return {
|
|
1213
|
+
success: false,
|
|
1214
|
+
exitCode: ExitCode.InstallationFailed,
|
|
1215
|
+
error: `Hook instance "${instanceName}" already exists in ${harness.displayName} with a different configuration.`,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
action = "reuse";
|
|
1219
|
+
finalHash = existing.configHash;
|
|
1220
|
+
}
|
|
1221
|
+
if (action === "install") {
|
|
1222
|
+
instancesToInstall.add(instanceName);
|
|
1223
|
+
}
|
|
1224
|
+
for (const { primitive, kitName } of candidate.primitives) {
|
|
1225
|
+
const key = buildPrimitiveKey(kitName, primitive.sourcePath);
|
|
1226
|
+
assignments.set(key, {
|
|
1227
|
+
instanceName,
|
|
1228
|
+
configHash: finalHash,
|
|
1229
|
+
action,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (assignments.size > 0) {
|
|
1234
|
+
hookAssignmentsByHarness.set(harness.name, assignments);
|
|
1235
|
+
}
|
|
1236
|
+
if (instancesToInstall.size > 0) {
|
|
1237
|
+
hookInstancesToInstallByHarness.set(harness.name, instancesToInstall);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
// Install to each harness
|
|
1241
|
+
for (const harness of harnesses) {
|
|
1242
|
+
const kitsToInstall = allowCompatibleOnly
|
|
1243
|
+
? kits.filter((kit) => compatibleKitsByHarness.get(harness.name)?.has(kit.name))
|
|
1244
|
+
: kits;
|
|
1245
|
+
if (kitsToInstall.length === 0) {
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const mcpAssignments = mcpAssignmentsByHarness.get(harness.name);
|
|
1249
|
+
const mcpInstancesToInstall = mcpInstancesToInstallByHarness.get(harness.name);
|
|
1250
|
+
const hookAssignments = hookAssignmentsByHarness.get(harness.name);
|
|
1251
|
+
const hookInstancesToInstall = hookInstancesToInstallByHarness.get(harness.name);
|
|
1252
|
+
if (isInteractive) {
|
|
1253
|
+
logInfo(`Installing to ${harness.displayName}...`);
|
|
1254
|
+
}
|
|
1255
|
+
else if (verbose) {
|
|
1256
|
+
console.error(`Installing to ${harness.displayName}...`);
|
|
1257
|
+
}
|
|
1258
|
+
const kitResults = [];
|
|
1259
|
+
for (const kit of kitsToInstall) {
|
|
1260
|
+
// Check compatibility with scope (should already be validated)
|
|
1261
|
+
const compatibility = harness.adapter.checkCompatibility(kit.manifest, scope);
|
|
1262
|
+
if (!compatibility.compatible) {
|
|
1263
|
+
if (allowCompatibleOnly) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
return {
|
|
1267
|
+
success: false,
|
|
1268
|
+
exitCode: ExitCode.KitIncompatible,
|
|
1269
|
+
error: compatibility.message || "Incompatible kit-harness combination",
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
// Use resolved primitives from pre-pass
|
|
1273
|
+
const resolvedPrimitives = resolvedByKit.get(kit.name);
|
|
1274
|
+
if (!resolvedPrimitives) {
|
|
1275
|
+
return {
|
|
1276
|
+
success: false,
|
|
1277
|
+
exitCode: ExitCode.ResolutionFailed,
|
|
1278
|
+
error: `Resolution failed: no primitives resolved for ${kit.name}`,
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
const mcpConfigPath = getMcpConfigPath(harness.adapter, scope, projectRoot);
|
|
1282
|
+
const hookConfigPath = getHookConfigPath(harness.adapter, scope, projectRoot);
|
|
1283
|
+
const primitivesForAdapter = [];
|
|
1284
|
+
const skippedMcpPrimitives = [];
|
|
1285
|
+
const skippedHookPrimitives = [];
|
|
1286
|
+
const installedInstanceNames = new Set();
|
|
1287
|
+
const installedHookInstanceNames = new Set();
|
|
1288
|
+
for (const primitive of resolvedPrimitives) {
|
|
1289
|
+
if (primitive.type === "mcp" && mcpAssignments) {
|
|
1290
|
+
const assignment = mcpAssignments.get(buildPrimitiveKey(kit.name, primitive.sourcePath));
|
|
1291
|
+
if (!assignment) {
|
|
1292
|
+
primitivesForAdapter.push(primitive);
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
const canInstall = assignment.action === "install";
|
|
1296
|
+
const shouldInstall = canInstall &&
|
|
1297
|
+
(!mcpInstancesToInstall || mcpInstancesToInstall.has(assignment.instanceName));
|
|
1298
|
+
if (!shouldInstall) {
|
|
1299
|
+
skippedMcpPrimitives.push(primitive);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
primitivesForAdapter.push({ ...primitive, instanceName: assignment.instanceName });
|
|
1303
|
+
installedInstanceNames.add(assignment.instanceName);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (primitive.type === "hooks" && hookAssignments) {
|
|
1307
|
+
const assignment = hookAssignments.get(buildPrimitiveKey(kit.name, primitive.sourcePath));
|
|
1308
|
+
if (!assignment) {
|
|
1309
|
+
primitivesForAdapter.push(primitive);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const canInstall = assignment.action === "install";
|
|
1313
|
+
const shouldInstall = canInstall &&
|
|
1314
|
+
(!hookInstancesToInstall || hookInstancesToInstall.has(assignment.instanceName));
|
|
1315
|
+
if (!shouldInstall) {
|
|
1316
|
+
skippedHookPrimitives.push(primitive);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
primitivesForAdapter.push({ ...primitive, instanceName: assignment.instanceName });
|
|
1320
|
+
installedHookInstanceNames.add(assignment.instanceName);
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
primitivesForAdapter.push(primitive);
|
|
1324
|
+
}
|
|
1325
|
+
// Install the kit
|
|
1326
|
+
const installOptions = {
|
|
1327
|
+
backup: true,
|
|
1328
|
+
createDirs: true,
|
|
1329
|
+
scope,
|
|
1330
|
+
envResolutions, // Pass resolved env vars to adapter (Phase 6)
|
|
1331
|
+
onProgress: (progress) => {
|
|
1332
|
+
if (verbose) {
|
|
1333
|
+
const primitiveInfo = progress.primitive ? ` ${progress.primitive}` : "";
|
|
1334
|
+
console.error(` [${progress.phase}] ${progress.operation}${primitiveInfo}`);
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
};
|
|
1338
|
+
if (scope === "project") {
|
|
1339
|
+
installOptions.projectRoot = projectRoot;
|
|
1340
|
+
}
|
|
1341
|
+
const installResult = await harness.adapter.install(kit.manifest, primitivesForAdapter, installOptions);
|
|
1342
|
+
if (!installResult.success) {
|
|
1343
|
+
skippedResults.push({
|
|
1344
|
+
kit: kit.name,
|
|
1345
|
+
harness: harness.name,
|
|
1346
|
+
reason: installResult.error || "Installation failed",
|
|
1347
|
+
});
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
for (const instanceName of installedInstanceNames) {
|
|
1351
|
+
mcpInstancesToInstall?.delete(instanceName);
|
|
1352
|
+
}
|
|
1353
|
+
for (const instanceName of installedHookInstanceNames) {
|
|
1354
|
+
hookInstancesToInstall?.delete(instanceName);
|
|
1355
|
+
}
|
|
1356
|
+
if (skippedMcpPrimitives.length > 0) {
|
|
1357
|
+
for (const primitive of skippedMcpPrimitives) {
|
|
1358
|
+
const namespacedName = harness.adapter.getNamespacedName(kit.name, primitive.name);
|
|
1359
|
+
const status = {
|
|
1360
|
+
type: primitive.type,
|
|
1361
|
+
name: primitive.name,
|
|
1362
|
+
namespacedName,
|
|
1363
|
+
status: "unchanged",
|
|
1364
|
+
destination: mcpConfigPath,
|
|
1365
|
+
};
|
|
1366
|
+
if (primitive.ref) {
|
|
1367
|
+
status.ref = primitive.ref;
|
|
1368
|
+
}
|
|
1369
|
+
if (primitive.resolvedVersion) {
|
|
1370
|
+
status.resolvedVersion = primitive.resolvedVersion;
|
|
1371
|
+
}
|
|
1372
|
+
installResult.installedPrimitives.push(status);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (skippedHookPrimitives.length > 0) {
|
|
1376
|
+
for (const primitive of skippedHookPrimitives) {
|
|
1377
|
+
const namespacedName = harness.adapter.getNamespacedName(kit.name, primitive.name);
|
|
1378
|
+
const status = {
|
|
1379
|
+
type: primitive.type,
|
|
1380
|
+
name: primitive.name,
|
|
1381
|
+
namespacedName,
|
|
1382
|
+
status: "unchanged",
|
|
1383
|
+
destination: hookConfigPath,
|
|
1384
|
+
};
|
|
1385
|
+
if (primitive.ref) {
|
|
1386
|
+
status.ref = primitive.ref;
|
|
1387
|
+
}
|
|
1388
|
+
if (primitive.resolvedVersion) {
|
|
1389
|
+
status.resolvedVersion = primitive.resolvedVersion;
|
|
1390
|
+
}
|
|
1391
|
+
installResult.installedPrimitives.push(status);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
// Record in manifest
|
|
1395
|
+
if (!manifest.harnesses[harness.name]) {
|
|
1396
|
+
manifest.harnesses[harness.name] = { kits: {} };
|
|
1397
|
+
}
|
|
1398
|
+
const statusByKey = new Map();
|
|
1399
|
+
for (const status of installResult.installedPrimitives) {
|
|
1400
|
+
statusByKey.set(`${status.type}:${status.name}`, status);
|
|
1401
|
+
}
|
|
1402
|
+
const installedPrimitives = resolvedPrimitives.map((primitive) => {
|
|
1403
|
+
const key = `${primitive.type}:${primitive.name}`;
|
|
1404
|
+
const status = statusByKey.get(key);
|
|
1405
|
+
const namespacedName = status?.namespacedName
|
|
1406
|
+
?? harness.adapter.getNamespacedName(kit.name, primitive.name);
|
|
1407
|
+
const installedPath = status?.destination
|
|
1408
|
+
?? (primitive.type === "mcp" ? mcpConfigPath : "");
|
|
1409
|
+
const installed = {
|
|
1410
|
+
name: primitive.name,
|
|
1411
|
+
type: primitive.type,
|
|
1412
|
+
namespacedName,
|
|
1413
|
+
isInline: !primitive.ref,
|
|
1414
|
+
installedPath,
|
|
1415
|
+
};
|
|
1416
|
+
if (primitive.resolvedVersion) {
|
|
1417
|
+
installed.version = primitive.resolvedVersion;
|
|
1418
|
+
}
|
|
1419
|
+
if (primitive.ref) {
|
|
1420
|
+
// Extract version spec from ref (e.g., "tf-plan@^1.0.0" -> "^1.0.0")
|
|
1421
|
+
const atIndex = primitive.ref.indexOf("@");
|
|
1422
|
+
if (atIndex !== -1) {
|
|
1423
|
+
installed.versionSpec = primitive.ref.substring(atIndex + 1);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (primitive.type === "mcp") {
|
|
1427
|
+
const assignment = mcpAssignments?.get(buildPrimitiveKey(kit.name, primitive.sourcePath));
|
|
1428
|
+
if (assignment) {
|
|
1429
|
+
installed.instanceName = assignment.instanceName;
|
|
1430
|
+
installed.configHash = assignment.configHash;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (primitive.type === "hooks") {
|
|
1434
|
+
const assignment = hookAssignments?.get(buildPrimitiveKey(kit.name, primitive.sourcePath));
|
|
1435
|
+
if (assignment) {
|
|
1436
|
+
installed.instanceName = assignment.instanceName;
|
|
1437
|
+
installed.configHash = assignment.configHash;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return installed;
|
|
1441
|
+
});
|
|
1442
|
+
const installedKit = {
|
|
1443
|
+
version: kit.manifest.version,
|
|
1444
|
+
installedAt: new Date().toISOString(),
|
|
1445
|
+
source,
|
|
1446
|
+
primitives: installedPrimitives,
|
|
1447
|
+
};
|
|
1448
|
+
manifest.harnesses[harness.name].kits[kit.name] = installedKit;
|
|
1449
|
+
if (mcpAssignments && mcpInfoByPrimitiveKey.size > 0) {
|
|
1450
|
+
const harnessEntry = manifest.harnesses[harness.name];
|
|
1451
|
+
if (!harnessEntry.mcpInstances) {
|
|
1452
|
+
harnessEntry.mcpInstances = {};
|
|
1453
|
+
}
|
|
1454
|
+
for (const primitive of resolvedPrimitives) {
|
|
1455
|
+
if (primitive.type !== "mcp")
|
|
1456
|
+
continue;
|
|
1457
|
+
const assignment = mcpAssignments.get(buildPrimitiveKey(kit.name, primitive.sourcePath));
|
|
1458
|
+
if (!assignment)
|
|
1459
|
+
continue;
|
|
1460
|
+
const instanceName = assignment.instanceName;
|
|
1461
|
+
const existingInstance = harnessEntry.mcpInstances[instanceName];
|
|
1462
|
+
if (existingInstance) {
|
|
1463
|
+
if (!existingInstance.usedBy.includes(kit.name)) {
|
|
1464
|
+
existingInstance.usedBy.push(kit.name);
|
|
1465
|
+
}
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
const info = mcpInfoByPrimitiveKey.get(buildPrimitiveKey(kit.name, primitive.sourcePath));
|
|
1469
|
+
if (!info)
|
|
1470
|
+
continue;
|
|
1471
|
+
harnessEntry.mcpInstances[instanceName] = {
|
|
1472
|
+
version: info.version,
|
|
1473
|
+
configHash: assignment.configHash,
|
|
1474
|
+
usedBy: [kit.name],
|
|
1475
|
+
sourcePrimitive: info.primitiveName,
|
|
1476
|
+
installedAt: new Date().toISOString(),
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (hookAssignments && hookInfoByPrimitiveKey.size > 0) {
|
|
1481
|
+
const harnessEntry = manifest.harnesses[harness.name];
|
|
1482
|
+
if (!harnessEntry.hookInstances) {
|
|
1483
|
+
harnessEntry.hookInstances = {};
|
|
1484
|
+
}
|
|
1485
|
+
for (const primitive of resolvedPrimitives) {
|
|
1486
|
+
if (primitive.type !== "hooks")
|
|
1487
|
+
continue;
|
|
1488
|
+
const assignment = hookAssignments.get(buildPrimitiveKey(kit.name, primitive.sourcePath));
|
|
1489
|
+
if (!assignment)
|
|
1490
|
+
continue;
|
|
1491
|
+
const instanceName = assignment.instanceName;
|
|
1492
|
+
const existingInstance = harnessEntry.hookInstances[instanceName];
|
|
1493
|
+
if (existingInstance) {
|
|
1494
|
+
if (!existingInstance.usedBy.includes(kit.name)) {
|
|
1495
|
+
existingInstance.usedBy.push(kit.name);
|
|
1496
|
+
}
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const info = hookInfoByPrimitiveKey.get(buildPrimitiveKey(kit.name, primitive.sourcePath));
|
|
1500
|
+
if (!info)
|
|
1501
|
+
continue;
|
|
1502
|
+
harnessEntry.hookInstances[instanceName] = {
|
|
1503
|
+
version: info.version,
|
|
1504
|
+
configHash: assignment.configHash,
|
|
1505
|
+
usedBy: [kit.name],
|
|
1506
|
+
sourcePrimitive: info.primitiveName,
|
|
1507
|
+
installedAt: new Date().toISOString(),
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
// Build result for output
|
|
1512
|
+
kitResults.push({
|
|
1513
|
+
name: kit.name,
|
|
1514
|
+
version: kit.manifest.version,
|
|
1515
|
+
installed: installResult.installedPrimitives.map((p) => {
|
|
1516
|
+
const primitiveOutput = {
|
|
1517
|
+
type: p.type,
|
|
1518
|
+
name: p.name,
|
|
1519
|
+
namespacedName: p.namespacedName,
|
|
1520
|
+
status: p.status,
|
|
1521
|
+
};
|
|
1522
|
+
if (p.resolvedVersion) {
|
|
1523
|
+
primitiveOutput.version = p.resolvedVersion;
|
|
1524
|
+
}
|
|
1525
|
+
return primitiveOutput;
|
|
1526
|
+
}),
|
|
1527
|
+
});
|
|
1528
|
+
if (isInteractive) {
|
|
1529
|
+
logInfo(`${formatKit(kit.name, kit.manifest.version)} ${pc.green("✓")}`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (kitResults.length > 0) {
|
|
1533
|
+
harnessResults.push({
|
|
1534
|
+
name: harness.name,
|
|
1535
|
+
configPath: harness.configPath || "",
|
|
1536
|
+
kits: kitResults,
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
// Write manifest
|
|
1541
|
+
try {
|
|
1542
|
+
await writeManifest(manifest, scope, scope === "project" ? projectRoot : undefined);
|
|
1543
|
+
}
|
|
1544
|
+
catch (error) {
|
|
1545
|
+
return {
|
|
1546
|
+
success: false,
|
|
1547
|
+
error: `Failed to write manifest: ${error instanceof Error ? error.message : String(error)}`,
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
// Build output
|
|
1551
|
+
const output = {
|
|
1552
|
+
success: true,
|
|
1553
|
+
source,
|
|
1554
|
+
scope,
|
|
1555
|
+
harnesses: harnessResults,
|
|
1556
|
+
skipped: skippedResults,
|
|
1557
|
+
};
|
|
1558
|
+
if (scope === "project") {
|
|
1559
|
+
output.projectRoot = projectRoot;
|
|
1560
|
+
}
|
|
1561
|
+
// Add env var status to output (Phase 6)
|
|
1562
|
+
if (envResolutions.length > 0) {
|
|
1563
|
+
const configured = [];
|
|
1564
|
+
const passthrough = [];
|
|
1565
|
+
const warnings = [];
|
|
1566
|
+
for (const resolution of envResolutions) {
|
|
1567
|
+
for (const resolved of resolution.resolved) {
|
|
1568
|
+
if (resolved.usePassthrough) {
|
|
1569
|
+
passthrough.push({
|
|
1570
|
+
name: resolved.name,
|
|
1571
|
+
sensitive: resolved.sensitive,
|
|
1572
|
+
mcpServer: resolution.mcpServer,
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
else if (resolved.value !== undefined) {
|
|
1576
|
+
configured.push({
|
|
1577
|
+
name: resolved.name,
|
|
1578
|
+
source: resolved.source,
|
|
1579
|
+
mcpServer: resolution.mcpServer,
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
for (const warning of resolution.warnings) {
|
|
1584
|
+
warnings.push({
|
|
1585
|
+
name: warning.name,
|
|
1586
|
+
message: warning.message,
|
|
1587
|
+
type: warning.type,
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
// Only include if there's content
|
|
1592
|
+
if (configured.length > 0 || passthrough.length > 0 || warnings.length > 0) {
|
|
1593
|
+
output.envVars = { configured, passthrough, warnings };
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return { success: true, output };
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Resolve inline primitives only (when no primitives registry exists).
|
|
1600
|
+
*/
|
|
1601
|
+
function resolveInlinePrimitivesOnly(kit, localPath) {
|
|
1602
|
+
const resolved = [];
|
|
1603
|
+
const kitBasePath = path.join(localPath, kit.path);
|
|
1604
|
+
for (const [type, primitives] of Object.entries(kit.manifest.primitives)) {
|
|
1605
|
+
if (!primitives)
|
|
1606
|
+
continue;
|
|
1607
|
+
for (const primitive of primitives) {
|
|
1608
|
+
// Only handle inline primitives
|
|
1609
|
+
if ("name" in primitive && "entrypoint" in primitive) {
|
|
1610
|
+
resolved.push({
|
|
1611
|
+
kitName: kit.name,
|
|
1612
|
+
name: primitive.name,
|
|
1613
|
+
type: type,
|
|
1614
|
+
sourcePath: path.resolve(kitBasePath, primitive.entrypoint),
|
|
1615
|
+
isInline: true,
|
|
1616
|
+
metadata: primitive.description ? { description: primitive.description } : {},
|
|
1617
|
+
instanceName: primitive.instanceName,
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return resolved;
|
|
1623
|
+
}
|
|
1624
|
+
//# sourceMappingURL=install.js.map
|