@cjhyy/code-shell-core 0.5.0-rc.2 → 0.6.0-rc.2
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 +13 -10
- package/dist/agent/agent-definition-registry.d.ts +3 -0
- package/dist/agent/agent-definition-registry.js +3 -1
- package/dist/agent/agent-definition.d.ts +15 -0
- package/dist/agent/agent-definition.js +27 -3
- package/dist/arena/context/context-tools.js +47 -4
- package/dist/arena/ledger.js +9 -1
- package/dist/arena/strategies/utils.d.ts +1 -4
- package/dist/arena/strategies/utils.js +7 -84
- package/dist/automation/cron-expr.d.ts +10 -0
- package/dist/automation/cron-expr.js +23 -5
- package/dist/automation/runner.d.ts +21 -4
- package/dist/automation/runner.js +48 -12
- package/dist/automation/scheduler.d.ts +38 -0
- package/dist/automation/scheduler.js +89 -12
- package/dist/automation/store.js +5 -2
- package/dist/capability-control/disabled-lists.d.ts +25 -0
- package/dist/capability-control/disabled-lists.js +57 -0
- package/dist/capability-control/overlay.d.ts +15 -0
- package/dist/capability-control/overlay.js +27 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
- package/dist/cc-orchestrator/agent-adapter.js +133 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
- package/dist/cc-orchestrator/cc-capability.js +53 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
- package/dist/cc-orchestrator/codex-session-history.js +187 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +214 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
- package/dist/cc-orchestrator/external-agent-driver.js +69 -0
- package/dist/cc-orchestrator/index.d.ts +8 -0
- package/dist/cc-orchestrator/index.js +8 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
- package/dist/cc-orchestrator/relevance-judge.js +29 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
- package/dist/cc-orchestrator/session-discovery.js +125 -0
- package/dist/cc-orchestrator/session-history.d.ts +19 -0
- package/dist/cc-orchestrator/session-history.js +67 -0
- package/dist/cli/agent-server-stdio.d.ts +15 -1
- package/dist/cli/agent-server-stdio.js +115 -17
- package/dist/cli/agent-server-tcp.js +40 -26
- package/dist/context/compaction.d.ts +56 -0
- package/dist/context/compaction.js +186 -0
- package/dist/context/manager.js +25 -4
- package/dist/cost-tracker.js +5 -61
- package/dist/credentials/cipher.d.ts +49 -0
- package/dist/credentials/cipher.js +45 -0
- package/dist/credentials/cookie-jar.d.ts +24 -0
- package/dist/credentials/cookie-jar.js +40 -0
- package/dist/credentials/index.d.ts +7 -0
- package/dist/credentials/index.js +5 -0
- package/dist/credentials/inject-credential-tool.d.ts +20 -0
- package/dist/credentials/inject-credential-tool.js +130 -0
- package/dist/credentials/store.d.ts +72 -0
- package/dist/credentials/store.js +184 -0
- package/dist/credentials/types.d.ts +56 -0
- package/dist/credentials/use-credential-tool.d.ts +29 -0
- package/dist/credentials/use-credential-tool.js +205 -0
- package/dist/credentials/use-gate.d.ts +56 -0
- package/dist/credentials/use-gate.js +52 -0
- package/dist/data/model-metadata.d.ts +77 -0
- package/dist/data/model-metadata.js +56 -0
- package/dist/data/model-metadata.json +216 -0
- package/dist/data/openrouter-models.d.ts +18 -7
- package/dist/data/openrouter-models.js +35 -8
- package/dist/engine/aux-key.d.ts +10 -0
- package/dist/engine/aux-key.js +11 -0
- package/dist/engine/dynamic-tool-defs.d.ts +19 -0
- package/dist/engine/dynamic-tool-defs.js +36 -0
- package/dist/engine/engine.d.ts +267 -138
- package/dist/engine/engine.js +957 -191
- package/dist/engine/friendly-error.d.ts +18 -0
- package/dist/engine/friendly-error.js +63 -0
- package/dist/engine/goal.d.ts +145 -0
- package/dist/engine/goal.js +144 -0
- package/dist/engine/image-policy.d.ts +13 -0
- package/dist/engine/image-policy.js +24 -0
- package/dist/engine/model-connections-pool.d.ts +17 -0
- package/dist/engine/model-connections-pool.js +66 -0
- package/dist/engine/model-facade.d.ts +10 -0
- package/dist/engine/model-facade.js +25 -0
- package/dist/engine/patch-orphaned-tools.js +3 -0
- package/dist/engine/resolve-llm-config.d.ts +16 -0
- package/dist/engine/resolve-llm-config.js +44 -0
- package/dist/engine/runtime.js +2 -1
- package/dist/engine/sandbox-cache-key.d.ts +10 -0
- package/dist/engine/sandbox-cache-key.js +9 -0
- package/dist/engine/sandbox-config.d.ts +31 -0
- package/dist/engine/sandbox-config.js +38 -0
- package/dist/engine/session-usage.d.ts +19 -0
- package/dist/engine/session-usage.js +25 -0
- package/dist/engine/steer-queue.d.ts +32 -0
- package/dist/engine/steer-queue.js +26 -0
- package/dist/engine/streaming-tool-queue.d.ts +12 -0
- package/dist/engine/streaming-tool-queue.js +50 -10
- package/dist/engine/turn-loop.d.ts +67 -3
- package/dist/engine/turn-loop.js +292 -41
- package/dist/engine/types.d.ts +175 -0
- package/dist/engine/types.js +13 -0
- package/dist/external-agents/config.d.ts +2 -0
- package/dist/external-agents/config.js +15 -0
- package/dist/external-agents/types.d.ts +31 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/git/utils.js +11 -6
- package/dist/git/worktree.d.ts +48 -0
- package/dist/git/worktree.js +86 -10
- package/dist/hooks/goal-stop-hook.d.ts +28 -0
- package/dist/hooks/goal-stop-hook.js +186 -9
- package/dist/hooks/registry.d.ts +8 -0
- package/dist/hooks/registry.js +19 -0
- package/dist/hooks/shell-runner.js +10 -21
- package/dist/index.d.ts +49 -6
- package/dist/index.js +60 -4
- package/dist/llm/capabilities/param-specs.d.ts +14 -0
- package/dist/llm/capabilities/param-specs.js +62 -0
- package/dist/llm/capabilities/rules.js +4 -0
- package/dist/llm/capabilities/types.d.ts +10 -0
- package/dist/llm/client-base.d.ts +28 -1
- package/dist/llm/client-base.js +119 -13
- package/dist/llm/model-cache.js +4 -2
- package/dist/llm/model-pool.d.ts +14 -0
- package/dist/llm/model-pool.js +13 -0
- package/dist/llm/provider-auth.d.ts +41 -0
- package/dist/llm/provider-auth.js +76 -0
- package/dist/llm/provider-catalog.d.ts +4 -0
- package/dist/llm/providers/anthropic.js +64 -14
- package/dist/llm/providers/openai.d.ts +36 -0
- package/dist/llm/providers/openai.js +257 -51
- package/dist/llm/reasoning-setting.d.ts +3 -3
- package/dist/llm/reasoning-setting.js +9 -1
- package/dist/llm/stream-watchdog.js +5 -1
- package/dist/llm/token-counter.js +9 -2
- package/dist/llm/types.d.ts +4 -0
- package/dist/logging/sanitize-messages.js +22 -0
- package/dist/lsp/manager.d.ts +1 -1
- package/dist/lsp/manager.js +40 -10
- package/dist/model-catalog/builtin.d.ts +12 -0
- package/dist/model-catalog/builtin.js +407 -0
- package/dist/model-catalog/gen-connections.d.ts +20 -0
- package/dist/model-catalog/gen-connections.js +28 -0
- package/dist/model-catalog/index.d.ts +41 -0
- package/dist/model-catalog/index.js +90 -0
- package/dist/model-catalog/params.d.ts +20 -0
- package/dist/model-catalog/params.js +45 -0
- package/dist/model-catalog/resolve.d.ts +48 -0
- package/dist/model-catalog/resolve.js +33 -0
- package/dist/model-catalog/save-entry.d.ts +32 -0
- package/dist/model-catalog/save-entry.js +104 -0
- package/dist/model-catalog/types.d.ts +561 -0
- package/dist/model-catalog/types.js +93 -0
- package/dist/model-catalog/upsert.d.ts +9 -0
- package/dist/model-catalog/upsert.js +8 -0
- package/dist/onboarding.d.ts +12 -82
- package/dist/onboarding.js +61 -326
- package/dist/plugins/gitOps.d.ts +19 -0
- package/dist/plugins/gitOps.js +73 -4
- package/dist/plugins/installer/checkUpdate.d.ts +16 -0
- package/dist/plugins/installer/checkUpdate.js +32 -0
- package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
- package/dist/plugins/installer/codex/convertCommands.js +46 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +5 -2
- package/dist/plugins/installer/codex/convertMcp.js +45 -5
- package/dist/plugins/installer/install.js +25 -1
- package/dist/plugins/installer/installFromArchive.d.ts +43 -0
- package/dist/plugins/installer/installFromArchive.js +134 -0
- package/dist/plugins/installer/installFromSource.js +8 -2
- package/dist/plugins/installer/loadPluginAgents.js +6 -2
- package/dist/plugins/installer/loadPluginMcp.d.ts +9 -2
- package/dist/plugins/installer/loadPluginMcp.js +35 -2
- package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
- package/dist/plugins/installer/pruneDisabled.js +73 -0
- package/dist/plugins/installer/types.d.ts +5 -2
- package/dist/plugins/installer/types.js +1 -0
- package/dist/plugins/installer/uninstall.js +4 -0
- package/dist/plugins/installer/unzip.d.ts +14 -0
- package/dist/plugins/installer/unzip.js +82 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +53 -21
- package/dist/plugins/loadPluginHooks.d.ts +47 -1
- package/dist/plugins/loadPluginHooks.js +73 -1
- package/dist/plugins/marketplaceManager.d.ts +7 -0
- package/dist/plugins/marketplaceManager.js +20 -0
- package/dist/plugins/pluginCommandHook.js +4 -18
- package/dist/plugins/pluginContent.d.ts +30 -0
- package/dist/plugins/pluginContent.js +83 -0
- package/dist/plugins/pluginInstaller.d.ts +13 -0
- package/dist/plugins/pluginInstaller.js +42 -8
- package/dist/plugins/schemas.js +1 -0
- package/dist/plugins/types.d.ts +6 -0
- package/dist/preset/index.d.ts +7 -1
- package/dist/preset/index.js +96 -6
- package/dist/product/types.d.ts +1 -1
- package/dist/prompt/composer.d.ts +27 -0
- package/dist/prompt/composer.js +67 -21
- package/dist/prompt/instruction-scanner.js +5 -3
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/base.md +1 -0
- package/dist/prompt/sections/browser.md +10 -0
- package/dist/prompt/sections/coding.md +4 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -2
- package/dist/protocol/chat-session-manager.js +36 -0
- package/dist/protocol/chat-session.d.ts +65 -0
- package/dist/protocol/chat-session.js +94 -2
- package/dist/protocol/client.d.ts +27 -0
- package/dist/protocol/client.js +38 -0
- package/dist/protocol/server.d.ts +112 -0
- package/dist/protocol/server.js +586 -16
- package/dist/protocol/transport.js +3 -2
- package/dist/protocol/types.d.ts +46 -0
- package/dist/protocol/types.js +16 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +155 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/run/FileRunStore.js +8 -3
- package/dist/run/RunApprovalBackend.js +25 -5
- package/dist/run/RunManager.d.ts +12 -0
- package/dist/run/RunManager.js +35 -0
- package/dist/run/factory.d.ts +1 -1
- package/dist/runtime/background-shell.d.ts +139 -0
- package/dist/runtime/background-shell.js +495 -0
- package/dist/runtime/output-clean.d.ts +24 -0
- package/dist/runtime/output-clean.js +41 -0
- package/dist/runtime/ring-file.d.ts +64 -0
- package/dist/runtime/ring-file.js +174 -0
- package/dist/runtime/safe-spawn.js +15 -34
- package/dist/runtime/spawn-common.d.ts +148 -0
- package/dist/runtime/spawn-common.js +303 -0
- package/dist/runtime/truncate-output.d.ts +22 -0
- package/dist/runtime/truncate-output.js +49 -0
- package/dist/runtime/utf8-cut.d.ts +11 -0
- package/dist/runtime/utf8-cut.js +23 -0
- package/dist/services/auto-dream.d.ts +4 -0
- package/dist/services/auto-dream.js +26 -26
- package/dist/services/diagnostics.d.ts +1 -2
- package/dist/services/diagnostics.js +12 -7
- package/dist/services/extract-memories.d.ts +14 -1
- package/dist/services/extract-memories.js +45 -6
- package/dist/services/memory-orchestrator.d.ts +21 -0
- package/dist/services/memory-orchestrator.js +125 -50
- package/dist/services/session-memory.js +24 -12
- package/dist/session/file-history.d.ts +124 -1
- package/dist/session/file-history.js +222 -6
- package/dist/session/memory.d.ts +116 -2
- package/dist/session/memory.js +250 -28
- package/dist/session/session-manager.d.ts +40 -0
- package/dist/session/session-manager.js +113 -3
- package/dist/session/simple-diff.d.ts +23 -0
- package/dist/session/simple-diff.js +84 -0
- package/dist/session/transcript.d.ts +25 -1
- package/dist/session/transcript.js +35 -2
- package/dist/session/undo-target.d.ts +67 -0
- package/dist/session/undo-target.js +144 -0
- package/dist/settings/disk-defaults.d.ts +9 -2
- package/dist/settings/disk-defaults.js +11 -2
- package/dist/settings/feature-flags.d.ts +64 -0
- package/dist/settings/feature-flags.js +61 -0
- package/dist/settings/manager.d.ts +68 -1
- package/dist/settings/manager.js +266 -29
- package/dist/settings/migrate-config.d.ts +45 -0
- package/dist/settings/migrate-config.js +125 -0
- package/dist/settings/schema-export.d.ts +25 -0
- package/dist/settings/schema-export.js +38 -0
- package/dist/settings/schema.d.ts +1186 -771
- package/dist/settings/schema.js +268 -97
- package/dist/skills/scanner.d.ts +9 -0
- package/dist/skills/scanner.js +30 -2
- package/dist/stt/resolve-transcribe.d.ts +31 -0
- package/dist/stt/resolve-transcribe.js +108 -0
- package/dist/stt/transcribe.d.ts +51 -0
- package/dist/stt/transcribe.js +65 -0
- package/dist/tool-system/browser-bridge.d.ts +226 -0
- package/dist/tool-system/browser-bridge.js +163 -0
- package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
- package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
- package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
- package/dist/tool-system/builtin/agent-notifications.js +9 -3
- package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
- package/dist/tool-system/builtin/agent-output-file.js +72 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +12 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -0
- package/dist/tool-system/builtin/agent.d.ts +21 -1
- package/dist/tool-system/builtin/agent.js +489 -42
- package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
- package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
- package/dist/tool-system/builtin/apply-patch/index.js +0 -15
- package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
- package/dist/tool-system/builtin/background-jobs.js +124 -0
- package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
- package/dist/tool-system/builtin/background-shell-tools.js +108 -0
- package/dist/tool-system/builtin/background-work.d.ts +67 -0
- package/dist/tool-system/builtin/background-work.js +86 -0
- package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
- package/dist/tool-system/builtin/bash-output-style.js +40 -0
- package/dist/tool-system/builtin/bash.d.ts +5 -2
- package/dist/tool-system/builtin/bash.js +98 -62
- package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
- package/dist/tool-system/builtin/browser-tools.js +312 -0
- package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
- package/dist/tool-system/builtin/cancel-goal.js +64 -0
- package/dist/tool-system/builtin/check-quota.d.ts +15 -0
- package/dist/tool-system/builtin/check-quota.js +34 -0
- package/dist/tool-system/builtin/config.js +7 -0
- package/dist/tool-system/builtin/cron.d.ts +7 -0
- package/dist/tool-system/builtin/cron.js +55 -3
- package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
- package/dist/tool-system/builtin/drive-claude-code.js +157 -0
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
- package/dist/tool-system/builtin/edit-model-catalog.js +94 -0
- package/dist/tool-system/builtin/edit.js +20 -16
- package/dist/tool-system/builtin/eol.d.ts +29 -0
- package/dist/tool-system/builtin/eol.js +37 -0
- package/dist/tool-system/builtin/file-cache.d.ts +6 -0
- package/dist/tool-system/builtin/file-cache.js +8 -0
- package/dist/tool-system/builtin/generate-image.d.ts +35 -0
- package/dist/tool-system/builtin/generate-image.js +278 -50
- package/dist/tool-system/builtin/generate-video.d.ts +55 -0
- package/dist/tool-system/builtin/generate-video.js +361 -0
- package/dist/tool-system/builtin/glob.js +0 -7
- package/dist/tool-system/builtin/grep.js +6 -8
- package/dist/tool-system/builtin/image-providers.d.ts +86 -0
- package/dist/tool-system/builtin/image-providers.js +190 -0
- package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
- package/dist/tool-system/builtin/image-uploader.js +74 -0
- package/dist/tool-system/builtin/index.d.ts +7 -1
- package/dist/tool-system/builtin/index.js +249 -25
- package/dist/tool-system/builtin/mcp-tools.js +23 -3
- package/dist/tool-system/builtin/memory.js +45 -7
- package/dist/tool-system/builtin/notebook-edit.js +0 -7
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/read.js +10 -10
- package/dist/tool-system/builtin/repl.js +4 -1
- package/dist/tool-system/builtin/skill.js +9 -0
- package/dist/tool-system/builtin/sleep.js +3 -2
- package/dist/tool-system/builtin/tool-search.js +25 -7
- package/dist/tool-system/builtin/video-providers.d.ts +154 -0
- package/dist/tool-system/builtin/video-providers.js +235 -0
- package/dist/tool-system/builtin/web-fetch.js +12 -2
- package/dist/tool-system/builtin/web-search.js +21 -7
- package/dist/tool-system/builtin/worktree.d.ts +2 -1
- package/dist/tool-system/builtin/worktree.js +31 -4
- package/dist/tool-system/builtin/write.js +0 -6
- package/dist/tool-system/context.d.ts +154 -6
- package/dist/tool-system/executor.d.ts +3 -1
- package/dist/tool-system/executor.js +172 -88
- package/dist/tool-system/investigation-guard.js +1 -1
- package/dist/tool-system/mcp-manager.d.ts +42 -4
- package/dist/tool-system/mcp-manager.js +186 -20
- package/dist/tool-system/path-policy.d.ts +5 -0
- package/dist/tool-system/path-policy.js +307 -8
- package/dist/tool-system/permission.d.ts +36 -1
- package/dist/tool-system/permission.js +281 -69
- package/dist/tool-system/plan-mode-allowlist.d.ts +13 -2
- package/dist/tool-system/plan-mode-allowlist.js +24 -2
- package/dist/tool-system/registry.d.ts +1 -0
- package/dist/tool-system/registry.js +16 -2
- package/dist/tool-system/sandbox/index.d.ts +8 -0
- package/dist/tool-system/sandbox/index.js +7 -2
- package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
- package/dist/tool-system/validate-tool-metadata.js +63 -0
- package/dist/types.d.ts +217 -6
- package/dist/updater.js +20 -9
- package/dist/utils/envUtils.d.ts +0 -9
- package/dist/utils/envUtils.js +3 -28
- package/dist/utils/exec.d.ts +40 -0
- package/dist/utils/exec.js +144 -0
- package/dist/utils/json.d.ts +12 -0
- package/dist/utils/json.js +92 -0
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/toolDisplay.js +0 -1
- package/package.json +13 -7
- package/dist/agent/coordinator.d.ts +0 -49
- package/dist/agent/coordinator.js +0 -77
- package/dist/settings/manager.test.js +0 -73
- package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
- package/dist/tool-system/builtin/remote-trigger.js +0 -54
- package/dist/tool-system/builtin/send-message.d.ts +0 -6
- package/dist/tool-system/builtin/send-message.js +0 -47
- /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
package/dist/plugins/gitOps.js
CHANGED
|
@@ -15,6 +15,17 @@
|
|
|
15
15
|
* classification. See docs/architecture/17 for the trust framing.
|
|
16
16
|
*/
|
|
17
17
|
import { safeSpawn } from "../runtime/safe-spawn.js";
|
|
18
|
+
import { resolveGit, isGitAvailable } from "../utils/exec.js";
|
|
19
|
+
/**
|
|
20
|
+
* On a fresh macOS without the Command Line Tools, `/usr/bin/git` is an `xcrun`
|
|
21
|
+
* stub that spawns fine (so `isGitAvailable()`'s findExecutable check is fooled)
|
|
22
|
+
* but exits non-zero with an xcrun error on stderr. Classify that as a
|
|
23
|
+
* missing-git condition so runGit returns GIT_NOT_FOUND and the host shows the
|
|
24
|
+
* friendly "install Git" guidance instead of leaking the raw xcrun error.
|
|
25
|
+
*/
|
|
26
|
+
export function isMissingDeveloperToolsStderr(stderr) {
|
|
27
|
+
return /xcrun: error|not a developer tool|no developer tools/i.test(stderr);
|
|
28
|
+
}
|
|
18
29
|
/**
|
|
19
30
|
* Force git to never prompt interactively. gitOps runs in headless contexts
|
|
20
31
|
* (the Electron main process, the agent worker) that have no TTY, so a
|
|
@@ -37,7 +48,18 @@ export function nonInteractiveGitEnv(base) {
|
|
|
37
48
|
};
|
|
38
49
|
}
|
|
39
50
|
async function runGit(args, cwd, timeoutMs = 60_000) {
|
|
40
|
-
|
|
51
|
+
// Up-front check: if git isn't installed/reachable at all, return a clear,
|
|
52
|
+
// actionable error instead of a raw "spawn git ENOENT". The desktop host
|
|
53
|
+
// turns GIT_NOT_FOUND into a friendly "install Git" prompt.
|
|
54
|
+
if (!isGitAvailable()) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: "GIT_NOT_FOUND: git was not found. Install Git (https://git-scm.com/downloads) " +
|
|
58
|
+
"and restart, or set the `git.path` setting to your git binary.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Resolve git: user `git.path` override, else PATH×PATHEXT (.cmd/.exe on Win).
|
|
62
|
+
const r = await safeSpawn(resolveGit(), args, {
|
|
41
63
|
cwd: cwd ?? process.cwd(),
|
|
42
64
|
env: nonInteractiveGitEnv(process.env),
|
|
43
65
|
timeoutMs,
|
|
@@ -51,6 +73,16 @@ async function runGit(args, cwd, timeoutMs = 60_000) {
|
|
|
51
73
|
if (r.exitCode === 0) {
|
|
52
74
|
return { ok: true, stdout: r.stdout.trim() };
|
|
53
75
|
}
|
|
76
|
+
// mac CLT-missing stub: git spawned but exited non-zero with an xcrun error —
|
|
77
|
+
// treat as not-installed so the host shows install guidance (not a raw error).
|
|
78
|
+
if (isMissingDeveloperToolsStderr(r.stderr)) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: "GIT_NOT_FOUND: git was not found (the macOS Command Line Tools are not installed). " +
|
|
82
|
+
"Run `xcode-select --install`, or install Git from https://git-scm.com/downloads, " +
|
|
83
|
+
"then restart — or set the `git.path` setting to your git binary.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
54
86
|
return {
|
|
55
87
|
ok: false,
|
|
56
88
|
error: `git ${args.join(" ")} exited ${r.exitCode}: ${r.stderr.trim() || "unknown error"}`,
|
|
@@ -67,6 +99,11 @@ async function runGit(args, cwd, timeoutMs = 60_000) {
|
|
|
67
99
|
* (.claude-plugin and .agents/plugins). On any failure of the sparse setup we
|
|
68
100
|
* fall back to checking out the full tree, so correctness never depends on
|
|
69
101
|
* sparse support being present.
|
|
102
|
+
*
|
|
103
|
+
* Pass `full: true` to skip sparse entirely and check out the whole tree. This
|
|
104
|
+
* is required when the caller reads arbitrary plugin content from the clone
|
|
105
|
+
* (installing a plugin or a plugin subdir), as opposed to a marketplace where
|
|
106
|
+
* only the manifest dirs are needed up front.
|
|
70
107
|
*/
|
|
71
108
|
export async function gitClone(url, destDir, options) {
|
|
72
109
|
const sparsePaths = options?.sparsePaths ?? [".claude-plugin", ".agents/plugins"];
|
|
@@ -77,9 +114,20 @@ export async function gitClone(url, destDir, options) {
|
|
|
77
114
|
const cloned = await runGit(args);
|
|
78
115
|
if (!cloned.ok)
|
|
79
116
|
return cloned;
|
|
117
|
+
// Full-tree clone: skip sparse setup and check out everything. Used by plugin
|
|
118
|
+
// content clones, which read files anywhere in the repo.
|
|
119
|
+
if (options?.full) {
|
|
120
|
+
const checkout = await runGit(["checkout"], destDir);
|
|
121
|
+
if (!checkout.ok)
|
|
122
|
+
return checkout;
|
|
123
|
+
return cloned;
|
|
124
|
+
}
|
|
80
125
|
// Restrict the working tree to the manifest dirs, then check out. Use
|
|
81
126
|
// non-cone mode so nested paths like ".agents/plugins" match exactly.
|
|
82
|
-
|
|
127
|
+
// `--` before the manifest-derived paths: a malicious manifest path shaped
|
|
128
|
+
// like `--stdin`/`--cone` would otherwise be parsed as a sparse-checkout flag
|
|
129
|
+
// (arg-injection). No RCE flag here as in ls-remote, but match the discipline.
|
|
130
|
+
const init = await runGit(["sparse-checkout", "set", "--no-cone", "--", ...sparsePaths], destDir);
|
|
83
131
|
if (!init.ok) {
|
|
84
132
|
// Sparse unsupported/failed — fall back to a normal full checkout so the
|
|
85
133
|
// manifest is still present.
|
|
@@ -96,15 +144,36 @@ export async function gitClone(url, destDir, options) {
|
|
|
96
144
|
* harmlessly and the caller proceeds (the files are already present).
|
|
97
145
|
*/
|
|
98
146
|
export async function gitSparseCheckoutAdd(repoDir, relPath) {
|
|
99
|
-
return runGit(["sparse-checkout", "add", relPath], repoDir);
|
|
147
|
+
return runGit(["sparse-checkout", "add", "--", relPath], repoDir);
|
|
100
148
|
}
|
|
101
149
|
export async function gitRevParseHead(repoDir) {
|
|
102
150
|
return runGit(["rev-parse", "HEAD"], repoDir);
|
|
103
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a ref (default HEAD) on a remote git URL WITHOUT cloning — one
|
|
154
|
+
* cheap network round-trip. Returns the 40-char SHA on success.
|
|
155
|
+
*/
|
|
156
|
+
export async function gitLsRemote(url, ref) {
|
|
157
|
+
// `git ls-remote <url> <ref>` prints "<sha>\t<refname>"; take the first sha.
|
|
158
|
+
// SECURITY: the `--` end-of-options separator is REQUIRED before a
|
|
159
|
+
// user-controlled `url`. Without it a marketplace/plugin URL shaped like
|
|
160
|
+
// `--upload-pack=<cmd>` is parsed as a git flag and git EXECUTES <cmd> (RCE).
|
|
161
|
+
// gitClone already does this (`-- url destDir`); ls-remote must match.
|
|
162
|
+
const r = await runGit(["ls-remote", "--", url, ref ?? "HEAD"]);
|
|
163
|
+
if (!r.ok)
|
|
164
|
+
return r;
|
|
165
|
+
const sha = r.stdout.split(/\s+/)[0] ?? "";
|
|
166
|
+
if (!/^[0-9a-f]{7,40}$/i.test(sha)) {
|
|
167
|
+
return { ok: false, error: `ls-remote: no sha for ${ref ?? "HEAD"} in: ${r.stdout.slice(0, 120)}` };
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, stdout: sha };
|
|
170
|
+
}
|
|
104
171
|
export async function gitFetchAndReset(repoDir, ref) {
|
|
105
172
|
const fetchArgs = ["fetch", "--depth", "1", "origin"];
|
|
173
|
+
// `--` before a (possibly user/manifest-derived) ref so a flag-shaped ref
|
|
174
|
+
// can't be parsed as a git fetch option. Consistent with clone/ls-remote.
|
|
106
175
|
if (ref)
|
|
107
|
-
fetchArgs.push(ref);
|
|
176
|
+
fetchArgs.push("--", ref);
|
|
108
177
|
const fetch = await runGit(fetchArgs, repoDir);
|
|
109
178
|
if (!fetch.ok)
|
|
110
179
|
return fetch;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface UpdateCheck {
|
|
2
|
+
name: string;
|
|
3
|
+
updateAvailable: boolean;
|
|
4
|
+
currentCommit?: string;
|
|
5
|
+
latestCommit?: string;
|
|
6
|
+
reason?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Check whether a remote (git-sourced) plugin has a newer commit upstream,
|
|
10
|
+
* WITHOUT cloning. Compares the recorded install commit (meta.commit) against
|
|
11
|
+
* `git ls-remote`. Local-source plugins, older installs without a recorded
|
|
12
|
+
* commit, and ls-remote failures all resolve to `updateAvailable: false` with
|
|
13
|
+
* an explanatory `reason` rather than throwing — the only hard error is an
|
|
14
|
+
* unknown plugin name.
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkPluginUpdate(name: string): Promise<UpdateCheck>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { gitLsRemote } from "../gitOps.js";
|
|
3
|
+
import { pluginMetaPath } from "./paths.js";
|
|
4
|
+
import { parseSource } from "./parseSource.js";
|
|
5
|
+
import { CSMeta, PluginInstallError } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Check whether a remote (git-sourced) plugin has a newer commit upstream,
|
|
8
|
+
* WITHOUT cloning. Compares the recorded install commit (meta.commit) against
|
|
9
|
+
* `git ls-remote`. Local-source plugins, older installs without a recorded
|
|
10
|
+
* commit, and ls-remote failures all resolve to `updateAvailable: false` with
|
|
11
|
+
* an explanatory `reason` rather than throwing — the only hard error is an
|
|
12
|
+
* unknown plugin name.
|
|
13
|
+
*/
|
|
14
|
+
export async function checkPluginUpdate(name) {
|
|
15
|
+
const metaPath = pluginMetaPath(name);
|
|
16
|
+
if (!existsSync(metaPath))
|
|
17
|
+
throw new PluginInstallError(`no plugin named '${name}'`);
|
|
18
|
+
const meta = CSMeta.parse(JSON.parse(readFileSync(metaPath, "utf-8")));
|
|
19
|
+
const parsed = parseSource(meta.source);
|
|
20
|
+
if (parsed.kind !== "remote") {
|
|
21
|
+
return { name, updateAvailable: false, reason: "not a remote source" };
|
|
22
|
+
}
|
|
23
|
+
if (!meta.commit) {
|
|
24
|
+
return { name, updateAvailable: false, reason: "no recorded commit to compare" };
|
|
25
|
+
}
|
|
26
|
+
const latest = await gitLsRemote(parsed.url, parsed.ref);
|
|
27
|
+
if (!latest.ok) {
|
|
28
|
+
return { name, updateAvailable: false, currentCommit: meta.commit, reason: latest.error };
|
|
29
|
+
}
|
|
30
|
+
const updateAvailable = latest.stdout.toLowerCase() !== meta.commit.toLowerCase();
|
|
31
|
+
return { name, updateAvailable, currentCommit: meta.commit, latestCommit: latest.stdout };
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map a Codex plugin's slash commands into CC's `commands/` layout.
|
|
3
|
+
*
|
|
4
|
+
* Codex `prompts/*.md` and CC `commands/*.md` are isomorphic: each is a
|
|
5
|
+
* markdown file (optional `description`/`argument-hint` frontmatter) whose
|
|
6
|
+
* basename becomes the command name, and codeshell's `pluginCommandsLoader`
|
|
7
|
+
* already scans `commands/*.md`. So the conversion is a flat copy of every
|
|
8
|
+
* `.md` from `prompts/` (and any explicit `commands/`) into `dest/commands/`.
|
|
9
|
+
*
|
|
10
|
+
* Placeholder syntax differs (Codex `$1`/`$FILE` vs CC `$ARGUMENTS`) but the
|
|
11
|
+
* body is copied verbatim — v1 inert, the same stance taken for `codex_`
|
|
12
|
+
* agent fields (see convertAgents.ts). Non-`.md` files are ignored, matching
|
|
13
|
+
* Codex's own behaviour. On a filename collision an explicit `commands/`
|
|
14
|
+
* entry wins over `prompts/` (the more specific source).
|
|
15
|
+
*
|
|
16
|
+
* Async fs throughout: this runs in the Electron main process during install;
|
|
17
|
+
* sync copies block the event loop and freeze the UI (see convertSkills.ts).
|
|
18
|
+
*/
|
|
19
|
+
export declare function copyCodexCommands(sourceDir: string, destDir: string): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir, mkdir, copyFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Map a Codex plugin's slash commands into CC's `commands/` layout.
|
|
6
|
+
*
|
|
7
|
+
* Codex `prompts/*.md` and CC `commands/*.md` are isomorphic: each is a
|
|
8
|
+
* markdown file (optional `description`/`argument-hint` frontmatter) whose
|
|
9
|
+
* basename becomes the command name, and codeshell's `pluginCommandsLoader`
|
|
10
|
+
* already scans `commands/*.md`. So the conversion is a flat copy of every
|
|
11
|
+
* `.md` from `prompts/` (and any explicit `commands/`) into `dest/commands/`.
|
|
12
|
+
*
|
|
13
|
+
* Placeholder syntax differs (Codex `$1`/`$FILE` vs CC `$ARGUMENTS`) but the
|
|
14
|
+
* body is copied verbatim — v1 inert, the same stance taken for `codex_`
|
|
15
|
+
* agent fields (see convertAgents.ts). Non-`.md` files are ignored, matching
|
|
16
|
+
* Codex's own behaviour. On a filename collision an explicit `commands/`
|
|
17
|
+
* entry wins over `prompts/` (the more specific source).
|
|
18
|
+
*
|
|
19
|
+
* Async fs throughout: this runs in the Electron main process during install;
|
|
20
|
+
* sync copies block the event loop and freeze the UI (see convertSkills.ts).
|
|
21
|
+
*/
|
|
22
|
+
export async function copyCodexCommands(sourceDir, destDir) {
|
|
23
|
+
// prompts/ first, then commands/ — later writes overwrite earlier, so an
|
|
24
|
+
// explicit commands/ entry wins the collision.
|
|
25
|
+
const written = await copyMdFiles(join(sourceDir, "prompts"), join(destDir, "commands"));
|
|
26
|
+
await copyMdFiles(join(sourceDir, "commands"), join(destDir, "commands"), written);
|
|
27
|
+
}
|
|
28
|
+
/** Flat-copy *.md from srcDir into destDir; returns the set of basenames written. */
|
|
29
|
+
async function copyMdFiles(srcDir, destDir, written = new Set()) {
|
|
30
|
+
if (!existsSync(srcDir))
|
|
31
|
+
return written;
|
|
32
|
+
let ensuredDest = false;
|
|
33
|
+
for (const dirent of await readdir(srcDir, { withFileTypes: true })) {
|
|
34
|
+
if (!dirent.isFile() && !dirent.isSymbolicLink())
|
|
35
|
+
continue;
|
|
36
|
+
if (!dirent.name.endsWith(".md"))
|
|
37
|
+
continue;
|
|
38
|
+
if (!ensuredDest) {
|
|
39
|
+
await mkdir(destDir, { recursive: true });
|
|
40
|
+
ensuredDest = true;
|
|
41
|
+
}
|
|
42
|
+
await copyFile(join(srcDir, dirent.name), join(destDir, dirent.name));
|
|
43
|
+
written.add(dirent.name);
|
|
44
|
+
}
|
|
45
|
+
return written;
|
|
46
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
export declare function normalizeCodexMcpFields(cfg: Record<string, unknown>): Record<string, unknown>;
|
|
1
2
|
/**
|
|
2
3
|
* Resolve a Codex manifest's mcpServers declaration into a plain server map.
|
|
3
|
-
* -
|
|
4
|
+
* Each server's env-secret fields are normalized from Codex's snake_case to
|
|
5
|
+
* our runtime camelCase (see normalizeCodexMcpFields).
|
|
6
|
+
* - object → normalized inline map
|
|
4
7
|
* - string → read that file relative to sourceDir; accept either a bare map
|
|
5
|
-
* or a `{ mcpServers: {...} }` wrapper.
|
|
8
|
+
* or a `{ mcpServers: {...} }` wrapper, then normalize.
|
|
6
9
|
* - undefined → {}
|
|
7
10
|
*/
|
|
8
11
|
export declare function resolveCodexMcpServers(sourceDir: string, decl: string | Record<string, unknown> | undefined): Record<string, unknown>;
|
|
@@ -1,18 +1,58 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { PluginInstallError } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Codex `.mcp.json` writes env-secret fields in snake_case
|
|
6
|
+
* (`bearer_token_env_var`, `env_http_headers`, `env_vars`), but our runtime
|
|
7
|
+
* MCP manager reads camelCase (`bearerTokenEnvVar`, `envHeaders`, `envVars` —
|
|
8
|
+
* see buildStdioEnv / buildHttpHeaders in tool-system/mcp-manager.ts). Nothing
|
|
9
|
+
* between install and connect runs these through a zod schema (install spreads
|
|
10
|
+
* the object verbatim; loadPluginMcp casts raw JSON), so without this re-key an
|
|
11
|
+
* imported Codex plugin's secret references would silently never apply and the
|
|
12
|
+
* server would connect without its auth header / env. Re-key one server config
|
|
13
|
+
* object. An already-present camelCase key wins over its snake_case twin so we
|
|
14
|
+
* never clobber the canonical form. Non-object input passes through unchanged.
|
|
15
|
+
*/
|
|
16
|
+
const CODEX_FIELD_MAP = {
|
|
17
|
+
bearer_token_env_var: "bearerTokenEnvVar",
|
|
18
|
+
env_http_headers: "envHeaders",
|
|
19
|
+
env_vars: "envVars",
|
|
20
|
+
};
|
|
21
|
+
export function normalizeCodexMcpFields(cfg) {
|
|
22
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg))
|
|
23
|
+
return cfg;
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const [key, value] of Object.entries(cfg)) {
|
|
26
|
+
const mapped = CODEX_FIELD_MAP[key];
|
|
27
|
+
// Skip the snake_case form if its camelCase twin is already present.
|
|
28
|
+
if (mapped && mapped in cfg)
|
|
29
|
+
continue;
|
|
30
|
+
out[mapped ?? key] = value;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** Apply normalizeCodexMcpFields to every server in a name → config map. */
|
|
35
|
+
function normalizeServerMap(servers) {
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
38
|
+
out[name] = normalizeCodexMcpFields(cfg);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
4
42
|
/**
|
|
5
43
|
* Resolve a Codex manifest's mcpServers declaration into a plain server map.
|
|
6
|
-
* -
|
|
44
|
+
* Each server's env-secret fields are normalized from Codex's snake_case to
|
|
45
|
+
* our runtime camelCase (see normalizeCodexMcpFields).
|
|
46
|
+
* - object → normalized inline map
|
|
7
47
|
* - string → read that file relative to sourceDir; accept either a bare map
|
|
8
|
-
* or a `{ mcpServers: {...} }` wrapper.
|
|
48
|
+
* or a `{ mcpServers: {...} }` wrapper, then normalize.
|
|
9
49
|
* - undefined → {}
|
|
10
50
|
*/
|
|
11
51
|
export function resolveCodexMcpServers(sourceDir, decl) {
|
|
12
52
|
if (decl === undefined)
|
|
13
53
|
return {};
|
|
14
54
|
if (typeof decl === "object")
|
|
15
|
-
return decl;
|
|
55
|
+
return normalizeServerMap(decl);
|
|
16
56
|
// `decl` is an untrusted, unvalidated manifest field from a (possibly
|
|
17
57
|
// remote-cloned) plugin. Resolve it and assert it stays inside sourceDir so
|
|
18
58
|
// a value like "../../../../etc/secret.json" can't escape the plugin dir and
|
|
@@ -39,7 +79,7 @@ export function resolveCodexMcpServers(sourceDir, decl) {
|
|
|
39
79
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
40
80
|
return {};
|
|
41
81
|
if ("mcpServers" in parsed) {
|
|
42
|
-
return parsed.mcpServers ?? {};
|
|
82
|
+
return normalizeServerMap(parsed.mcpServers ?? {});
|
|
43
83
|
}
|
|
44
|
-
return parsed;
|
|
84
|
+
return normalizeServerMap(parsed);
|
|
45
85
|
}
|
|
@@ -7,6 +7,7 @@ import { CodexPluginManifest, PluginInstallError } from "./types.js";
|
|
|
7
7
|
import { convertCodexAgentToml } from "./codex/convertAgents.js";
|
|
8
8
|
import { resolveCodexMcpServers } from "./codex/convertMcp.js";
|
|
9
9
|
import { copyCodexSkills } from "./codex/convertSkills.js";
|
|
10
|
+
import { copyCodexCommands } from "./codex/convertCommands.js";
|
|
10
11
|
import { appendInstallEntry, pluginInstallKey } from "../installedPlugins.js";
|
|
11
12
|
/**
|
|
12
13
|
* Install a local plugin directory into ~/.code-shell/plugins/<name>/.
|
|
@@ -35,7 +36,12 @@ export async function installPluginFromPath(sourceDir, name, installedAt) {
|
|
|
35
36
|
// here; awaiting it yields the event loop so the Electron main process
|
|
36
37
|
// keeps answering IPC (no UI freeze during install).
|
|
37
38
|
await cp(sourceDir, tmpDir, { recursive: true });
|
|
38
|
-
|
|
39
|
+
// Read the declared version from .claude-plugin/plugin.json so the
|
|
40
|
+
// install record (and the UI's "installed version" chip) shows e.g.
|
|
41
|
+
// "v0.1.0" instead of falling back to the literal "local". Best-effort:
|
|
42
|
+
// CC plugins commonly omit version, and the file may be absent/malformed.
|
|
43
|
+
const version = await readCcPluginVersion(sourceDir);
|
|
44
|
+
meta = { name, format: "cc", version, source: sourceDir, installedAt };
|
|
39
45
|
}
|
|
40
46
|
else {
|
|
41
47
|
const manifest = CodexPluginManifest.parse(JSON.parse(await readFile(join(sourceDir, ".codex-plugin", "plugin.json"), "utf-8")));
|
|
@@ -43,6 +49,8 @@ export async function installPluginFromPath(sourceDir, name, installedAt) {
|
|
|
43
49
|
await copyCodexSkills(sourceDir, tmpDir);
|
|
44
50
|
// agents (TOML → MD)
|
|
45
51
|
await convertAgentsInto(sourceDir, tmpDir, name);
|
|
52
|
+
// prompts/ (+ commands/) → commands/ (Codex slash commands == CC commands)
|
|
53
|
+
await copyCodexCommands(sourceDir, tmpDir);
|
|
46
54
|
// mcp → mcp-servers.json keyed <plugin>:<server>
|
|
47
55
|
const servers = resolveCodexMcpServers(sourceDir, manifest.mcpServers);
|
|
48
56
|
const keyed = {};
|
|
@@ -74,6 +82,22 @@ export async function installPluginFromPath(sourceDir, name, installedAt) {
|
|
|
74
82
|
throw err;
|
|
75
83
|
}
|
|
76
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Best-effort read of a CC plugin's declared version from
|
|
87
|
+
* <sourceDir>/.claude-plugin/plugin.json. Returns undefined when the file is
|
|
88
|
+
* absent, unparseable, or has no string `version` — the caller then records the
|
|
89
|
+
* install without a version (UI shows the source tag instead of a number).
|
|
90
|
+
*/
|
|
91
|
+
async function readCcPluginVersion(sourceDir) {
|
|
92
|
+
try {
|
|
93
|
+
const raw = await readFile(join(sourceDir, ".claude-plugin", "plugin.json"), "utf-8");
|
|
94
|
+
const v = JSON.parse(raw).version;
|
|
95
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
77
101
|
/** Walk <sourceDir>/agents/**.toml → <destDir>/agents/**.md (structure preserved). */
|
|
78
102
|
async function convertAgentsInto(sourceDir, destDir, pluginName) {
|
|
79
103
|
const agentsSrc = join(sourceDir, "agents");
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Options for the unified local-install entry. */
|
|
2
|
+
export interface InstallLocalOptions {
|
|
3
|
+
/**
|
|
4
|
+
* When the target name is already installed, replace it instead of throwing
|
|
5
|
+
* "already installed". The replace is atomic (rename-backup + rollback via
|
|
6
|
+
* {@link reinstallAtomic}), so a failed overwrite keeps the old version and
|
|
7
|
+
* its registration intact. Unlike a manual uninstall+install, this preserves
|
|
8
|
+
* the user's disable flags in settings (an upgrade is not a removal).
|
|
9
|
+
*/
|
|
10
|
+
overwrite?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Install a local plugin from a .zip archive.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors installFromSource (the git path): extract to a private temp dir,
|
|
16
|
+
* locate the plugin root inside it, hand off to the local installer (which
|
|
17
|
+
* does CC/Codex detect + convert + register), then always clean up the temp
|
|
18
|
+
* dir. On any failure nothing is left behind.
|
|
19
|
+
*
|
|
20
|
+
* `name` is optional — when omitted it is derived from the plugin manifest
|
|
21
|
+
* (`name` field) or, failing that, the archive's top-level directory. The
|
|
22
|
+
* caller stamps `installedAt` to keep this pure of the unavailable Date.now().
|
|
23
|
+
*/
|
|
24
|
+
export declare function installPluginFromArchive(zipPath: string, installedAt: string, name?: string, options?: InstallLocalOptions): Promise<{
|
|
25
|
+
dir: string;
|
|
26
|
+
name: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Unified local-install entry for the desktop/UI: takes either a directory or
|
|
30
|
+
* a .zip and installs it as a global plugin, deriving the name when not given.
|
|
31
|
+
* The host (which has Date.now) stamps `installedAt`.
|
|
32
|
+
*
|
|
33
|
+
* For a directory we still run findPluginRoot/deriveName so a user can point at
|
|
34
|
+
* a repo checkout whose plugin sits one level down, and so the name comes from
|
|
35
|
+
* the manifest rather than forcing the caller to guess it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function installLocalPlugin(input: {
|
|
38
|
+
kind: "dir" | "zip";
|
|
39
|
+
path: string;
|
|
40
|
+
}, installedAt: string, name?: string, options?: InstallLocalOptions): Promise<{
|
|
41
|
+
dir: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { extractZip } from "./unzip.js";
|
|
6
|
+
import { installPluginFromPath } from "./install.js";
|
|
7
|
+
import { reinstallAtomic } from "./update.js";
|
|
8
|
+
import { pluginInstallDir } from "./paths.js";
|
|
9
|
+
import { PluginInstallError } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Install `root` as plugin `name`, atomically replacing an already-installed
|
|
12
|
+
* plugin of the same name when `overwrite` is set. When no same-name plugin
|
|
13
|
+
* exists this is a plain install (overwrite is a no-op). `root` must stay valid
|
|
14
|
+
* for the duration of the call (zip callers keep their extracted temp dir alive
|
|
15
|
+
* until this resolves).
|
|
16
|
+
*/
|
|
17
|
+
async function installRootMaybeOverwrite(root, resolvedName, installedAt, overwrite) {
|
|
18
|
+
const finalDir = pluginInstallDir(resolvedName);
|
|
19
|
+
if (overwrite && existsSync(finalDir)) {
|
|
20
|
+
await reinstallAtomic(resolvedName, root, () => installPluginFromPath(root, resolvedName, installedAt));
|
|
21
|
+
return finalDir;
|
|
22
|
+
}
|
|
23
|
+
return installPluginFromPath(root, resolvedName, installedAt);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Install a local plugin from a .zip archive.
|
|
27
|
+
*
|
|
28
|
+
* Mirrors installFromSource (the git path): extract to a private temp dir,
|
|
29
|
+
* locate the plugin root inside it, hand off to the local installer (which
|
|
30
|
+
* does CC/Codex detect + convert + register), then always clean up the temp
|
|
31
|
+
* dir. On any failure nothing is left behind.
|
|
32
|
+
*
|
|
33
|
+
* `name` is optional — when omitted it is derived from the plugin manifest
|
|
34
|
+
* (`name` field) or, failing that, the archive's top-level directory. The
|
|
35
|
+
* caller stamps `installedAt` to keep this pure of the unavailable Date.now().
|
|
36
|
+
*/
|
|
37
|
+
export async function installPluginFromArchive(zipPath, installedAt, name, options) {
|
|
38
|
+
if (!existsSync(zipPath) || !statSync(zipPath).isFile()) {
|
|
39
|
+
throw new PluginInstallError(`archive is not a file: ${zipPath}`);
|
|
40
|
+
}
|
|
41
|
+
const tmp = await mkdtemp(join(tmpdir(), "cs-tmp-zip-"));
|
|
42
|
+
try {
|
|
43
|
+
await extractZip(zipPath, tmp);
|
|
44
|
+
const root = await findPluginRoot(tmp);
|
|
45
|
+
const resolvedName = normalizePluginName(name ?? (await deriveName(root)));
|
|
46
|
+
// Overwrite (when set) runs inside this try so the extracted `root` is still
|
|
47
|
+
// present while reinstallAtomic installs from it; the finally removes tmp.
|
|
48
|
+
const dir = await installRootMaybeOverwrite(root, resolvedName, installedAt, options?.overwrite ?? false);
|
|
49
|
+
return { dir, name: resolvedName };
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await rm(tmp, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Unified local-install entry for the desktop/UI: takes either a directory or
|
|
57
|
+
* a .zip and installs it as a global plugin, deriving the name when not given.
|
|
58
|
+
* The host (which has Date.now) stamps `installedAt`.
|
|
59
|
+
*
|
|
60
|
+
* For a directory we still run findPluginRoot/deriveName so a user can point at
|
|
61
|
+
* a repo checkout whose plugin sits one level down, and so the name comes from
|
|
62
|
+
* the manifest rather than forcing the caller to guess it.
|
|
63
|
+
*/
|
|
64
|
+
export async function installLocalPlugin(input, installedAt, name, options) {
|
|
65
|
+
if (input.kind === "zip") {
|
|
66
|
+
return installPluginFromArchive(input.path, installedAt, name, options);
|
|
67
|
+
}
|
|
68
|
+
if (!existsSync(input.path) || !statSync(input.path).isDirectory()) {
|
|
69
|
+
throw new PluginInstallError(`source is not a directory: ${input.path}`);
|
|
70
|
+
}
|
|
71
|
+
const root = await findPluginRoot(input.path);
|
|
72
|
+
const resolvedName = normalizePluginName(name ?? (await deriveName(root)));
|
|
73
|
+
const dir = await installRootMaybeOverwrite(root, resolvedName, installedAt, options?.overwrite ?? false);
|
|
74
|
+
return { dir, name: resolvedName };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A plugin root is a dir that holds a recognizable manifest or component dir.
|
|
78
|
+
* Zip archives commonly wrap everything in a single top-level folder, so if
|
|
79
|
+
* `dir` itself isn't a plugin root but contains exactly one subdirectory, dive
|
|
80
|
+
* one level. Only one level — deeper nesting is treated as malformed.
|
|
81
|
+
*/
|
|
82
|
+
async function findPluginRoot(dir) {
|
|
83
|
+
if (looksLikePluginRoot(dir))
|
|
84
|
+
return dir;
|
|
85
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
86
|
+
const subDirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
87
|
+
if (subDirs.length === 1) {
|
|
88
|
+
const nested = join(dir, subDirs[0].name);
|
|
89
|
+
if (looksLikePluginRoot(nested))
|
|
90
|
+
return nested;
|
|
91
|
+
}
|
|
92
|
+
throw new PluginInstallError("no plugin found in archive (expected .claude-plugin/plugin.json, .codex-plugin/plugin.json, or a skills/commands/agents/hooks directory)");
|
|
93
|
+
}
|
|
94
|
+
function looksLikePluginRoot(dir) {
|
|
95
|
+
return (existsSync(join(dir, ".claude-plugin", "plugin.json")) ||
|
|
96
|
+
existsSync(join(dir, ".codex-plugin", "plugin.json")) ||
|
|
97
|
+
["skills", "commands", "agents", "hooks"].some((d) => existsSync(join(dir, d))));
|
|
98
|
+
}
|
|
99
|
+
/** Read the `name` field from a CC or Codex manifest; fall back to the dir name. */
|
|
100
|
+
async function deriveName(root) {
|
|
101
|
+
for (const rel of [
|
|
102
|
+
[".claude-plugin", "plugin.json"],
|
|
103
|
+
[".codex-plugin", "plugin.json"],
|
|
104
|
+
]) {
|
|
105
|
+
const manifestPath = join(root, ...rel);
|
|
106
|
+
if (!existsSync(manifestPath))
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
110
|
+
if (typeof parsed.name === "string" && parsed.name.trim())
|
|
111
|
+
return parsed.name.trim();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Malformed manifest → fall through to dir-name derivation.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return root.split(/[\\/]/).filter(Boolean).pop() ?? "plugin";
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Coerce an arbitrary derived name into a safe single path segment:
|
|
121
|
+
* lowercase, only [a-z0-9._-], collapse the rest to '-'. assertSafePluginName
|
|
122
|
+
* (called by installPluginFromPath) rejects separators outright; this makes a
|
|
123
|
+
* manifest name like "My Plugin" usable instead of erroring.
|
|
124
|
+
*/
|
|
125
|
+
function normalizePluginName(raw) {
|
|
126
|
+
const cleaned = raw
|
|
127
|
+
.trim()
|
|
128
|
+
.toLowerCase()
|
|
129
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
130
|
+
.replace(/^[-.]+|[-.]+$/g, "");
|
|
131
|
+
if (!cleaned)
|
|
132
|
+
throw new PluginInstallError(`cannot derive a valid plugin name from: ${JSON.stringify(raw)}`);
|
|
133
|
+
return cleaned;
|
|
134
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdtempSync, readFileSync, writeFileSync, rmSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { gitClone } from "../gitOps.js";
|
|
4
|
+
import { gitClone, gitRevParseHead } from "../gitOps.js";
|
|
5
5
|
import { installPluginFromPath } from "./install.js";
|
|
6
6
|
import { pluginMetaPath } from "./paths.js";
|
|
7
7
|
import { PluginInstallError } from "./types.js";
|
|
@@ -22,7 +22,7 @@ export async function installPluginFromSource(parsed, name, installedAt) {
|
|
|
22
22
|
}
|
|
23
23
|
const tmp = mkdtempSync(join(tmpdir(), "cs-tmp-clone-"));
|
|
24
24
|
try {
|
|
25
|
-
const clone = await gitClone(parsed.url, tmp, { ref: parsed.ref });
|
|
25
|
+
const clone = await gitClone(parsed.url, tmp, { ref: parsed.ref, full: true });
|
|
26
26
|
if (!clone.ok) {
|
|
27
27
|
throw new PluginInstallError(`clone failed: ${clone.error}`);
|
|
28
28
|
}
|
|
@@ -36,6 +36,12 @@ export async function installPluginFromSource(parsed, name, installedAt) {
|
|
|
36
36
|
const metaPath = pluginMetaPath(name);
|
|
37
37
|
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
38
38
|
meta.source = parsed.raw;
|
|
39
|
+
// Record the cloned repo's HEAD sha so `checkPluginUpdate` can compare it
|
|
40
|
+
// against `git ls-remote` later. A failed rev-parse only means "can't check
|
|
41
|
+
// updates", never an install failure — so omit commit and carry on.
|
|
42
|
+
const head = await gitRevParseHead(tmp);
|
|
43
|
+
if (head.ok)
|
|
44
|
+
meta.commit = head.stdout;
|
|
39
45
|
writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
40
46
|
return dir;
|
|
41
47
|
}
|
|
@@ -11,12 +11,16 @@ export function pluginAgentDirs(disabledPlugins = []) {
|
|
|
11
11
|
const out = [];
|
|
12
12
|
const data = readInstalledPlugins();
|
|
13
13
|
for (const [key, entries] of Object.entries(data.plugins)) {
|
|
14
|
-
|
|
14
|
+
const pluginName = pluginNameFromKey(key);
|
|
15
|
+
if (disabled.has(pluginName))
|
|
15
16
|
continue;
|
|
16
17
|
for (const entry of entries) {
|
|
17
18
|
const dir = join(entry.installPath, "agents");
|
|
19
|
+
// Carry pluginName so a plugin agent's bare skill allowlist
|
|
20
|
+
// (`director-skill`) can be namespaced to `<pluginName>:director-skill`
|
|
21
|
+
// at spawn time — see resolveAgentTypeOverrides.
|
|
18
22
|
if (existsSync(dir))
|
|
19
|
-
out.push({ dir, source: "plugin" });
|
|
23
|
+
out.push({ dir, source: "plugin", pluginName });
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
return out;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import type { MCPServerConfig } from "../../types.js";
|
|
1
|
+
import type { MCPServerConfig, MCPServerOverride } from "../../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Merge each registered plugin's MCP servers into a copy of `base`.
|
|
4
4
|
* Plugin keys are `<plugin>:<server>`. A key already present in `base`
|
|
5
5
|
* (user-configured) is NOT overwritten. Disabled plugins are skipped.
|
|
6
|
+
*
|
|
7
|
+
* `overrides` lets the user *supplement* a plugin server's env/credential
|
|
8
|
+
* fields without editing the plugin's manifest: for a plugin-sourced server,
|
|
9
|
+
* the override's {@link OVERRIDE_FIELDS} are layered on top (override wins),
|
|
10
|
+
* while command/args/url/transport stay from the plugin. Overrides do NOT
|
|
11
|
+
* apply to user-added (base) servers — those are edited via `mcpServers`
|
|
12
|
+
* directly — and an override for an unknown/disabled server has no effect.
|
|
6
13
|
*/
|
|
7
|
-
export declare function mergePluginMcpServers(base: Record<string, MCPServerConfig>, disabledPlugins?: string[]): Record<string, MCPServerConfig>;
|
|
14
|
+
export declare function mergePluginMcpServers(base: Record<string, MCPServerConfig>, disabledPlugins?: string[], overrides?: Record<string, MCPServerOverride>): Record<string, MCPServerConfig>;
|