@opengsd/gsd-core 1.7.0 → 1.9.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +45 -1
- package/README.md +2 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +78 -4
- package/agents/gsd-debugger.md +87 -29
- package/agents/gsd-executor.md +49 -9
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +44 -59
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +1338 -135
- package/commands/gsd/ai-integration-phase.md +1 -1
- package/commands/gsd/mempalace-capture.md +9 -5
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +5 -3
- package/commands/gsd/plan-review-convergence.md +7 -2
- package/gsd-core/bin/gsd-tools.cjs +2690 -2472
- package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
- package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
- package/gsd-core/bin/lib/api-coverage.cjs +360 -53
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/broken-windows.cjs +716 -0
- package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/capability-writer.cjs +6 -1
- package/gsd-core/bin/lib/check-command-router.cjs +140 -27
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
- package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
- package/gsd-core/bin/lib/command-aliases.cjs +14 -0
- package/gsd-core/bin/lib/commands.cjs +326 -21
- package/gsd-core/bin/lib/config-loader.cjs +214 -30
- package/gsd-core/bin/lib/config.cjs +158 -22
- package/gsd-core/bin/lib/core-utils.cjs +6 -1
- package/gsd-core/bin/lib/decisions.cjs +32 -8
- package/gsd-core/bin/lib/docs.cjs +6 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/gap-checker.cjs +17 -2
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +155 -66
- package/gsd-core/bin/lib/install-engine.cjs +299 -23
- package/gsd-core/bin/lib/install-profiles.cjs +239 -1
- package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
- package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
- package/gsd-core/bin/lib/milestone.cjs +248 -14
- package/gsd-core/bin/lib/model-catalog.cjs +69 -4
- package/gsd-core/bin/lib/model-resolver.cjs +189 -7
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +304 -9
- package/gsd-core/bin/lib/phase.cjs +258 -17
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +70 -2
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
- package/gsd-core/bin/lib/roadmap.cjs +23 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +70 -5
- package/gsd-core/bin/lib/state-document.cjs +171 -24
- package/gsd-core/bin/lib/state-transition.cjs +50 -11
- package/gsd-core/bin/lib/state.cjs +206 -32
- package/gsd-core/bin/lib/surface.cjs +51 -9
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +428 -11
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +44 -8
- package/gsd-core/bin/lib/verification.cjs +163 -31
- package/gsd-core/bin/lib/verify.cjs +348 -42
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/api-coverage.md +37 -7
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/common-bug-patterns.md +13 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
- package/gsd-core/references/debugger-fix-acceptance.md +157 -0
- package/gsd-core/references/debugger-philosophy.md +1 -0
- package/gsd-core/references/debugger-prevention.md +98 -0
- package/gsd-core/references/debugger-rca-branching.md +98 -0
- package/gsd-core/references/debugger-repro-hardening.md +130 -0
- package/gsd-core/references/debugger-sbfl.md +110 -0
- package/gsd-core/references/debugger-semantic-recall.md +81 -0
- package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
- package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
- package/gsd-core/references/execute-phase-response-language.md +7 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planner-antipatterns.md +6 -0
- package/gsd-core/references/planner-mvp-mode.md +12 -13
- package/gsd-core/references/planner-preconditions.md +156 -0
- package/gsd-core/references/planner-reversibility.md +132 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -19
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/skeleton-template.md +1 -1
- package/gsd-core/references/thinking-models-planning.md +3 -1
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/DEBUG.md +5 -3
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +3 -1
- package/gsd-core/workflows/add-todo.md +32 -1
- package/gsd-core/workflows/ai-integration-phase.md +8 -6
- package/gsd-core/workflows/audit-fix.md +6 -2
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +5 -3
- package/gsd-core/workflows/cleanup.md +7 -1
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -24
- package/gsd-core/workflows/complete-milestone.md +3 -0
- package/gsd-core/workflows/debug.md +35 -7
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discovery-phase.md +7 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
- package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +7 -1
- package/gsd-core/workflows/docs-update.md +9 -0
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
- package/gsd-core/workflows/execute-phase.md +110 -149
- package/gsd-core/workflows/execute-plan.md +20 -8
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/graduation.md +3 -0
- package/gsd-core/workflows/health.md +7 -1
- package/gsd-core/workflows/help/modes/full.md +9 -5
- package/gsd-core/workflows/import.md +11 -2
- package/gsd-core/workflows/inbox.md +7 -0
- package/gsd-core/workflows/ingest-docs.md +19 -10
- package/gsd-core/workflows/manager.md +3 -1
- package/gsd-core/workflows/map-codebase.md +17 -10
- package/gsd-core/workflows/mvp-phase.md +3 -0
- package/gsd-core/workflows/new-milestone.md +79 -23
- package/gsd-core/workflows/new-project.md +28 -19
- package/gsd-core/workflows/new-workspace.md +3 -1
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/onboard.md +3 -0
- package/gsd-core/workflows/plan-phase.md +56 -51
- package/gsd-core/workflows/plan-review-convergence.md +61 -12
- package/gsd-core/workflows/plant-seed.md +3 -0
- package/gsd-core/workflows/profile-user.md +7 -1
- package/gsd-core/workflows/progress.md +31 -3
- package/gsd-core/workflows/quick.md +33 -10
- package/gsd-core/workflows/remove-workspace.md +3 -0
- package/gsd-core/workflows/review.md +172 -585
- package/gsd-core/workflows/scan.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -2
- package/gsd-core/workflows/settings-integrations.md +3 -0
- package/gsd-core/workflows/settings.md +3 -0
- package/gsd-core/workflows/ship.md +88 -11
- package/gsd-core/workflows/sketch.md +3 -0
- package/gsd-core/workflows/smart-entry.md +4 -1
- package/gsd-core/workflows/spike.md +7 -1
- package/gsd-core/workflows/ui-phase.md +11 -2
- package/gsd-core/workflows/ui-review.md +11 -1
- package/gsd-core/workflows/undo.md +7 -0
- package/gsd-core/workflows/update.md +106 -5
- package/gsd-core/workflows/validate-phase.md +13 -2
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +15 -4
- package/hooks/dist/gsd-context-monitor.js +27 -9
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +97 -9
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-context-monitor.js +27 -9
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +97 -9
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +10 -8
- package/pi/gsd.cjs +34 -3
- package/scripts/changeset/lint.cjs +1 -0
- package/scripts/changeset/parse.cjs +26 -0
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +244 -0
- package/scripts/ci-rebase-check.cjs +48 -4
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +528 -0
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-test-timings.cjs +201 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-portable-timeout.cjs +140 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/release-tarball-smoke.cjs +18 -11
- package/scripts/run-tests.cjs +420 -58
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +9 -5
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +5 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -27,7 +27,9 @@ const runtimeArtifactLayout = require("./runtime-artifact-layout.cjs");
|
|
|
27
27
|
const runtimeArtifactInstallPlan = require("./runtime-artifact-install-plan.cjs");
|
|
28
28
|
const runtimeNamePolicy = require("./runtime-name-policy.cjs");
|
|
29
29
|
const installProfiles = require("./install-profiles.cjs");
|
|
30
|
+
const installerMigrations = require("./installer-migrations.cjs");
|
|
30
31
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
32
|
+
const external_descriptor_trust_cjs_1 = require("./external-descriptor-trust.cjs");
|
|
31
33
|
const { processAttribution } = runtimeArtifactConversion;
|
|
32
34
|
// resolveRuntimeArtifactLayout: accessed via module ref (not destructured) so
|
|
33
35
|
// test stubs that monkeypatch the module's exports are seen at call time.
|
|
@@ -113,6 +115,7 @@ function convertClaudeCommandToKiloSkill(content, skillName) {
|
|
|
113
115
|
const SKILLS_CONVERTER_REGISTRY = {
|
|
114
116
|
convertClaudeCommandToOpencodeSkill,
|
|
115
117
|
convertClaudeCommandToKiloSkill,
|
|
118
|
+
convertClaudeCommandToKimiCodeSkill: runtimeArtifactConversion.convertClaudeCommandToKimiCodeSkill,
|
|
116
119
|
};
|
|
117
120
|
// ---------------------------------------------------------------------------
|
|
118
121
|
// User-artifact preservation helpers
|
|
@@ -156,19 +159,93 @@ function restoreUserArtifacts(destDir, saved) {
|
|
|
156
159
|
// ---------------------------------------------------------------------------
|
|
157
160
|
// Symlink-escape guard
|
|
158
161
|
// ---------------------------------------------------------------------------
|
|
162
|
+
/**
|
|
163
|
+
* Opt-in for intentional symlinked-dest layouts (#2393). When the env var is
|
|
164
|
+
* set to "1" or "true", `hasExistingSymlinkBetween` follows symlinks instead of
|
|
165
|
+
* refusing them, EXCEPT for two load-bearing cases that always refuse regardless
|
|
166
|
+
* of opt-in (preserving ADR-1239 Phase B's threat model):
|
|
167
|
+
*
|
|
168
|
+
* (a) The `fullPath` itself, before any symlink resolution, escapes `root`
|
|
169
|
+
* via `..`-traversal — protects against untrusted `destSubpath` strings
|
|
170
|
+
* like `../../etc`. This is the line `resolvedFullPath !== resolvedRoot
|
|
171
|
+
* && !resolvedFullPath.startsWith(resolvedRoot + path.sep)` below.
|
|
172
|
+
* (b) A symlink's resolved real path equals the install root itself — this
|
|
173
|
+
* would let `_removeGsdEntries` (the prune pass) wipe the install root,
|
|
174
|
+
* which is the config-root-wipe threat from #1704 threat model item (b).
|
|
175
|
+
*
|
|
176
|
+
* What opt-in RELAXES specifically: the "pre-existing symlink that points
|
|
177
|
+
* outside configHome" refusal — threat (c) in #1704. The user has asserted
|
|
178
|
+
* they own and trust the symlink target. The default (no env var) keeps all
|
|
179
|
+
* three refusals, exactly the pre-#2393 behavior.
|
|
180
|
+
*
|
|
181
|
+
* Cross-platform note: on Windows, `fs.lstatSync().isSymbolicLink()` returns
|
|
182
|
+
* true for both symbolic links and NTFS junctions (Node ≥ 16), so Mamiki's
|
|
183
|
+
* Junction case (#2393 comment) is handled by the same code path as POSIX
|
|
184
|
+
* symlinks.
|
|
185
|
+
*
|
|
186
|
+
* @returns true when the caller MUST refuse; false when writes may proceed.
|
|
187
|
+
*/
|
|
188
|
+
function isSymlinkedDestOptIn() {
|
|
189
|
+
const v = process.env.GSD_ALLOW_SYMLINKED_DEST;
|
|
190
|
+
return v === '1' || v === 'true';
|
|
191
|
+
}
|
|
159
192
|
/**
|
|
160
193
|
* Returns true if any path component between `root` and `fullPath` is a
|
|
161
|
-
* symbolic link
|
|
194
|
+
* symbolic link that would redirect writes outside the install root in a way
|
|
195
|
+
* the caller must refuse.
|
|
196
|
+
*
|
|
197
|
+
* When `options.allowOptInFollow` is true (caller checked `isSymlinkedDestOptIn`),
|
|
198
|
+
* symlinks are followed instead of refused, except for the two always-refuse
|
|
199
|
+
* cases documented on `isSymlinkedDestOptIn` — (a) path-traversal in `fullPath`
|
|
200
|
+
* itself, (b) a resolved symlink target that equals the install root (would let
|
|
201
|
+
* the prune pass wipe it).
|
|
162
202
|
*/
|
|
163
|
-
function hasExistingSymlinkBetween(root, fullPath) {
|
|
203
|
+
function hasExistingSymlinkBetween(root, fullPath, options = {}) {
|
|
164
204
|
const resolvedRoot = node_path_1.default.resolve(root);
|
|
165
205
|
const resolvedFullPath = node_path_1.default.resolve(fullPath);
|
|
206
|
+
// (a) Path-traversal refusal — ALWAYS enforced, even with opt-in. An untrusted
|
|
207
|
+
// destSubpath string that escapes the install root via '..' is rejected
|
|
208
|
+
// regardless of user opt-in state (ADR-1239 Phase B threat (a)).
|
|
166
209
|
if (resolvedFullPath !== resolvedRoot && !resolvedFullPath.startsWith(resolvedRoot + node_path_1.default.sep)) {
|
|
167
210
|
return true;
|
|
168
211
|
}
|
|
212
|
+
// #2393 (security-review finding): realpathSync fully resolves all symlink
|
|
213
|
+
// components, path.resolve only normalizes lexically. On macOS, /var is a
|
|
214
|
+
// symlink to /private/var — so resolvedRoot='/var/foo/.claude' but its real
|
|
215
|
+
// path is '/private/var/foo/.claude'. A symlink whose real target equals the
|
|
216
|
+
// install root (the threat-(b) wipe case) would compare unequal without this
|
|
217
|
+
// normalization, defeating the guard exactly in the reporter's case (Azd325,
|
|
218
|
+
// nix-darwin: ~/.claude is itself a symlink). Compute realRoot once; fall
|
|
219
|
+
// back to the lexical form on any realpath failure (broken/missing/exotic FS)
|
|
220
|
+
// — threat (a) above still confines regardless.
|
|
221
|
+
let realRoot;
|
|
222
|
+
try {
|
|
223
|
+
realRoot = node_fs_1.default.existsSync(resolvedRoot) ? node_fs_1.default.realpathSync(resolvedRoot) : resolvedRoot;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
realRoot = resolvedRoot;
|
|
227
|
+
}
|
|
228
|
+
const allowFollow = options.allowOptInFollow === true;
|
|
229
|
+
// #2393: when root itself is a symlink (e.g. nix-darwin manages ~/.claude as a
|
|
230
|
+
// symlink to a dotfiles repo — Azd325's #2393 report), the pre-#2393 guard
|
|
231
|
+
// refused unconditionally via an early return before the component loop. The
|
|
232
|
+
// wipe threat (b) does NOT apply to the root itself being a symlink: destDir is
|
|
233
|
+
// a CHILD of root, and resolving root gives root's target — there is no
|
|
234
|
+
// circular back-reference to root from a path that descends from a resolved
|
|
235
|
+
// root. So under opt-in, just follow the root symlink and continue the walk.
|
|
236
|
+
// Default behavior (no opt-in) preserves the pre-#2393 refuse.
|
|
169
237
|
let cursor = resolvedRoot;
|
|
170
238
|
if (node_fs_1.default.existsSync(cursor) && node_fs_1.default.lstatSync(cursor).isSymbolicLink()) {
|
|
171
|
-
|
|
239
|
+
if (!allowFollow)
|
|
240
|
+
return true;
|
|
241
|
+
try {
|
|
242
|
+
cursor = node_fs_1.default.realpathSync(cursor);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// realpathSync failed (broken symlink, permission denied, exotic FS) — refuse,
|
|
246
|
+
// matching fail-closed posture.
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
172
249
|
}
|
|
173
250
|
const relative = node_path_1.default.relative(resolvedRoot, resolvedFullPath);
|
|
174
251
|
for (const segment of relative.split(node_path_1.default.sep)) {
|
|
@@ -177,8 +254,37 @@ function hasExistingSymlinkBetween(root, fullPath) {
|
|
|
177
254
|
cursor = node_path_1.default.join(cursor, segment);
|
|
178
255
|
if (!node_fs_1.default.existsSync(cursor))
|
|
179
256
|
return false;
|
|
180
|
-
if (node_fs_1.default.lstatSync(cursor).isSymbolicLink())
|
|
181
|
-
|
|
257
|
+
if (node_fs_1.default.lstatSync(cursor).isSymbolicLink()) {
|
|
258
|
+
if (!allowFollow)
|
|
259
|
+
return true;
|
|
260
|
+
// Opt-in active: follow the symlink. Refuse if the resolved target is the
|
|
261
|
+
// install root itself (threat (b) — would let _removeGsdEntries wipe the
|
|
262
|
+
// root). Other targets are acceptable per the user's explicit opt-in. A
|
|
263
|
+
// broken symlink (realpathSync throws) is still refused.
|
|
264
|
+
//
|
|
265
|
+
// Threat (b) check uses BOTH lexical and real forms of root to defend
|
|
266
|
+
// against macOS /var ↔ /private/var-style normalization gaps: realpathSync
|
|
267
|
+
// fully resolves, path.resolve only normalizes lexically, so a root path
|
|
268
|
+
// containing a symlink component would compare unequal to a realtarget
|
|
269
|
+
// that matches by real path. Compare both.
|
|
270
|
+
//
|
|
271
|
+
// Transitivity note: once followed, the walk continues from the resolved
|
|
272
|
+
// real path WITHOUT re-checking that further segments stay inside any
|
|
273
|
+
// confining boundary. The user's opt-in asserts trust in the target dir
|
|
274
|
+
// AND any further symlinks reachable through it — transitive and unbounded
|
|
275
|
+
// by design (one opt-in trusts the whole reachable tree). This is the
|
|
276
|
+
// documented opt-in semantics; do not add a "follow one symlink only"
|
|
277
|
+
// expectation here without revisiting the threat model.
|
|
278
|
+
try {
|
|
279
|
+
const realTarget = node_fs_1.default.realpathSync(cursor);
|
|
280
|
+
if (realTarget === realRoot || realTarget === resolvedRoot)
|
|
281
|
+
return true; // (b)
|
|
282
|
+
cursor = realTarget;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
182
288
|
}
|
|
183
289
|
return false;
|
|
184
290
|
}
|
|
@@ -226,8 +332,9 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
|
|
|
226
332
|
return false;
|
|
227
333
|
// Symlink-escape guard: reject if any path component between targetDir and
|
|
228
334
|
// skillDir is a symlink that would redirect writes outside the config root.
|
|
229
|
-
|
|
230
|
-
|
|
335
|
+
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
336
|
+
if (hasExistingSymlinkBetween(node_path_1.default.resolve(targetDir), skillDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
337
|
+
throw new Error(`migrateLegacyDevPreferencesToSkill: skillDir "${skillDir}" contains a symlink the install root "${targetDir}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
|
|
231
338
|
}
|
|
232
339
|
try {
|
|
233
340
|
node_fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
@@ -275,8 +382,9 @@ function _copyStaged(stagedDir, destDir, kind, configDir, runtime) {
|
|
|
275
382
|
const resolvedDest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, destDir);
|
|
276
383
|
// Symlink-escape guard: reject if any path component between the install root and
|
|
277
384
|
// destDir is a symlink that would redirect writes outside the install root.
|
|
278
|
-
|
|
279
|
-
|
|
385
|
+
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
386
|
+
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), resolvedDest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
387
|
+
throw new Error(`_copyStaged: destDir "${destDir}" contains a symlink the install root "${installRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
|
|
280
388
|
}
|
|
281
389
|
// Use the validated absolute path for the actual writes below.
|
|
282
390
|
destDir = resolvedDest;
|
|
@@ -560,20 +668,29 @@ function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
|
|
|
560
668
|
* @param scope
|
|
561
669
|
* @param resolvedProfile from resolveProfile() / resolveEffectiveProfile()
|
|
562
670
|
* @param resolveAttribution injection: (runtime) => attribution string | undefined
|
|
671
|
+
* @param capabilityRegistry #2322: optional composed capability registry
|
|
672
|
+
* (capabilityClusters view) — threaded into resolveRuntimeArtifactLayout so
|
|
673
|
+
* the skills kind can materialize installed third-party capability skills
|
|
674
|
+
* bound to their declaring capId. Absent -> no third-party skills staged
|
|
675
|
+
* (fail closed), matching the layout resolver's own optional-registry contract.
|
|
563
676
|
*/
|
|
564
|
-
function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined) {
|
|
677
|
+
function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, capabilityRegistry) {
|
|
565
678
|
// Combined-family runtimes (OpenCode/Kilo, ADR-1239 / #2087): route through
|
|
566
679
|
// the dedicated combined commands+skills+plugin orchestrator instead of the
|
|
567
680
|
// generic layout-driven loop below, mirroring the bespoke install path that
|
|
568
681
|
// previously lived inline in bin/install.js.
|
|
569
682
|
const behaviors = _hostBehaviors(runtime);
|
|
570
683
|
if (behaviors.combinedFamilyInstall) {
|
|
571
|
-
|
|
684
|
+
// #2329: combined-family runtimes (OpenCode/Kilo) bypass
|
|
685
|
+
// _runLegacyInstallMigrations below entirely (early return), so their
|
|
686
|
+
// legacy-directory cleanup needs its own pre-materialization hook here.
|
|
687
|
+
_migrateLegacyOpencodeCommandDir(runtime, configDir, behaviors);
|
|
688
|
+
installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution, behaviors, capabilityRegistry);
|
|
572
689
|
return;
|
|
573
690
|
}
|
|
574
691
|
// Legacy cleanup before layout-driven writes
|
|
575
692
|
_runLegacyInstallMigrations(runtime, configDir, scope);
|
|
576
|
-
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
693
|
+
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, configDir, scope, capabilityRegistry);
|
|
577
694
|
const planResult = runtimeArtifactInstallPlan.createRuntimeArtifactInstallPlan({
|
|
578
695
|
// `Layout` is structurally identical across the layout/install-plan .cjs
|
|
579
696
|
// modules but nominally distinct to tsc (untyped .cjs boundary) — bridge it.
|
|
@@ -603,8 +720,11 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
|
|
|
603
720
|
// resolved alternate root instead, matching assertDestWithinConfigHome's
|
|
604
721
|
// own root selection in createRuntimeArtifactInstallPlan.
|
|
605
722
|
const installRoot = (kind && typeof kind.home === 'string' && kind.home !== '') ? kind.home : configDir;
|
|
606
|
-
|
|
607
|
-
|
|
723
|
+
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
724
|
+
// Threat model from #1704 / ADR-1239 Phase B preserved: path-traversal and
|
|
725
|
+
// resolved-target-equals-root still refuse regardless of opt-in.
|
|
726
|
+
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
727
|
+
throw new Error(`installRuntimeArtifacts: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust — refusing to create. If this is an intentional user-owned symlink layout (e.g. externalized skills/hooks dir, multi-account configHome, or a dotfiles-managed configHome), re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
|
|
608
728
|
}
|
|
609
729
|
node_fs_1.default.mkdirSync(dest, { recursive: true });
|
|
610
730
|
if (kind.kind === 'skills' && node_fs_1.default.existsSync(dest)) {
|
|
@@ -698,9 +818,20 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
|
|
|
698
818
|
* @param rawCommandsDir - staged RAW Claude command dir (caller's _stageSkills output)
|
|
699
819
|
* @param pathPrefix - computed config-path prefix for body rewrites
|
|
700
820
|
* @param resolveAttribution - injection: (runtime) => attribution string | undefined
|
|
821
|
+
* @param resolvedProfile - #2362: from resolveProfile()/resolveEffectiveProfile(); only
|
|
822
|
+
* `.skills` is consulted (either the `'*'` full-profile sentinel or a concrete Set
|
|
823
|
+
* of stems), and only to gate which THIRD-PARTY capability stems are candidates for
|
|
824
|
+
* staging below. Absent -> no third-party skills staged (fail closed).
|
|
825
|
+
* @param capabilityRegistry - #2362: optional composed capability registry
|
|
826
|
+
* (capabilityClusters view). When present, installed third-party capability
|
|
827
|
+
* skills bound to their declaring capId are unioned into the staged output —
|
|
828
|
+
* the actual #2322 seam (install-profiles.cts stageSkillsForRuntimeAsSkills)
|
|
829
|
+
* this bespoke OpenCode/Kilo writer never called. Absent -> no third-party
|
|
830
|
+
* skills staged (fail closed), matching the seam's own optional-registry
|
|
831
|
+
* contract.
|
|
701
832
|
* @returns number of gsd-* skill directories written
|
|
702
833
|
*/
|
|
703
|
-
function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPrefix, resolveAttribution = () => undefined) {
|
|
834
|
+
function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPrefix, resolveAttribution = () => undefined, resolvedProfile, capabilityRegistry) {
|
|
704
835
|
const layout = runtimeArtifactLayout.resolveRuntimeArtifactLayout(runtime, targetDir);
|
|
705
836
|
const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills');
|
|
706
837
|
if (!skillsKindEntry)
|
|
@@ -721,8 +852,9 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
721
852
|
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(targetDir, skillsKindEntry.destSubpath);
|
|
722
853
|
// Symlink-escape guard: reject if any path component between targetDir and
|
|
723
854
|
// dest is a symlink that would redirect writes outside the config root.
|
|
724
|
-
|
|
725
|
-
|
|
855
|
+
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
856
|
+
if (hasExistingSymlinkBetween(node_path_1.default.resolve(targetDir), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
857
|
+
throw new Error(`installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${targetDir}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`);
|
|
726
858
|
}
|
|
727
859
|
node_fs_1.default.mkdirSync(dest, { recursive: true });
|
|
728
860
|
// Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune.
|
|
@@ -742,10 +874,12 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
742
874
|
}
|
|
743
875
|
_removeGsdEntries(dest, skillsKindEntry);
|
|
744
876
|
let count = 0;
|
|
877
|
+
const firstPartyStems = new Set();
|
|
745
878
|
for (const entry of node_fs_1.default.readdirSync(rawDir, { withFileTypes: true })) {
|
|
746
879
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
747
880
|
continue;
|
|
748
881
|
const stem = entry.name.slice(0, -3);
|
|
882
|
+
firstPartyStems.add(stem);
|
|
749
883
|
const skillName = `${skillsKindEntry.prefix}${stem}`;
|
|
750
884
|
let content = node_fs_1.default.readFileSync(node_path_1.default.join(rawDir, entry.name), 'utf8');
|
|
751
885
|
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
@@ -756,6 +890,51 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
756
890
|
node_fs_1.default.writeFileSync(node_path_1.default.join(skillDir, 'SKILL.md'), content);
|
|
757
891
|
count++;
|
|
758
892
|
}
|
|
893
|
+
// #2362: materialize installed THIRD-PARTY capability skills, bound to their
|
|
894
|
+
// DECLARING capability via the registry's capabilityClusters view — mirrors
|
|
895
|
+
// install-profiles.cts stageSkillsForRuntimeAsSkills's third-party fill-in
|
|
896
|
+
// (the actual #2322 seam), reusing its exported security-reviewed helpers
|
|
897
|
+
// rather than hand-rolling a second scan (DEFECT.GENERATIVE-FIX guard).
|
|
898
|
+
// First-party always wins on stem collision. The full/'*' sentinel resolves
|
|
899
|
+
// through capabilityClusterStems (BLOCKER-2 parity: `resolveProfile`
|
|
900
|
+
// short-circuits `full` to `'*'` before consulting a registry, so a bare
|
|
901
|
+
// `resolvedProfile.skills !== '*'` gate would silently skip this pass for
|
|
902
|
+
// the default full install). No registry in scope -> stage NOTHING
|
|
903
|
+
// third-party (fail closed — never fall back to scanning).
|
|
904
|
+
//
|
|
905
|
+
// Unlike the seam (which stages third-party bodies as-is and relies on a
|
|
906
|
+
// later applySurface rewrite pass), this install path has no such later
|
|
907
|
+
// pass — so third-party bodies get the SAME inline path-prefix/attribution
|
|
908
|
+
// rewrite as first-party ones for on-disk parity. They do NOT go through
|
|
909
|
+
// `converter`: an installed capability skill is already a complete
|
|
910
|
+
// SKILL.md, not a Claude-command body awaiting frontmatter conversion.
|
|
911
|
+
if (capabilityRegistry) {
|
|
912
|
+
const candidateStems = resolvedProfile && resolvedProfile.skills === '*'
|
|
913
|
+
? installProfiles.capabilityClusterStems(capabilityRegistry)
|
|
914
|
+
: (resolvedProfile && resolvedProfile.skills) || [];
|
|
915
|
+
for (const stem of candidateStems) {
|
|
916
|
+
if (firstPartyStems.has(stem))
|
|
917
|
+
continue; // first-party always wins
|
|
918
|
+
const found = installProfiles.readInstalledCapabilitySkill(stem, capabilityRegistry);
|
|
919
|
+
if (found === null)
|
|
920
|
+
continue; // absent/malformed/unowned -> skip gracefully
|
|
921
|
+
const skillName = `${skillsKindEntry.prefix}${stem}`;
|
|
922
|
+
if (!(0, external_descriptor_trust_cjs_1.isPathConfined)(skillName, dest))
|
|
923
|
+
continue; // defense-in-depth
|
|
924
|
+
let content = found.content;
|
|
925
|
+
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
926
|
+
content = processAttribution(content, resolveAttribution(runtime));
|
|
927
|
+
const skillDir = node_path_1.default.join(dest, skillName);
|
|
928
|
+
node_fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
929
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(skillDir, 'SKILL.md'), content);
|
|
930
|
+
// #2322 HIGH-3 parity: persist the capability-owned marker so a later
|
|
931
|
+
// prune pass can identify this directory even once the owning
|
|
932
|
+
// capability is uninstalled/unsurfaced and no longer appears in any
|
|
933
|
+
// registry view.
|
|
934
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(skillDir, installProfiles.CAPABILITY_SKILL_MARKER), found.capId + '\n', 'utf8');
|
|
935
|
+
count++;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
759
938
|
// Restore user-owned dirs after the prune+copy.
|
|
760
939
|
for (const [dirName, snap] of toPreserve) {
|
|
761
940
|
_restoreDir(node_path_1.default.join(dest, dirName), snap);
|
|
@@ -847,13 +1026,97 @@ function _installNativePluginIfDeclared(runtime, configDir, behaviors, src) {
|
|
|
847
1026
|
if (np && np.source) {
|
|
848
1027
|
const pluginSrc = node_path_1.default.join(src, np.source);
|
|
849
1028
|
if (node_fs_1.default.existsSync(pluginSrc)) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
1029
|
+
// Confine the FULL dest path (dir + file), not just the dir. Previously
|
|
1030
|
+
// only `np.dir` was validated and `np.file` was joined on unchecked, so a
|
|
1031
|
+
// descriptor whose `file` carried `..`, an absolute path, or a NUL byte
|
|
1032
|
+
// would have written outside configHome. Not reachable today — descriptors
|
|
1033
|
+
// are first-party and compiled into the capability registry at build time —
|
|
1034
|
+
// but `np.file` is exactly the field #2470 changes, and the guard costs
|
|
1035
|
+
// nothing. For a well-formed descriptor this resolves identically to the
|
|
1036
|
+
// previous mkdir(dir) + join(dir, file).
|
|
1037
|
+
const destPath = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, node_path_1.default.join(np.dir, np.file));
|
|
1038
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(destPath), { recursive: true });
|
|
1039
|
+
node_fs_1.default.copyFileSync(pluginSrc, destPath);
|
|
853
1040
|
}
|
|
854
1041
|
}
|
|
855
1042
|
}
|
|
856
1043
|
// ---------------------------------------------------------------------------
|
|
1044
|
+
// _migrateLegacyOpencodeCommandDir
|
|
1045
|
+
// ---------------------------------------------------------------------------
|
|
1046
|
+
/**
|
|
1047
|
+
* #2329: migrate a pre-fix OpenCode install's legacy singular `command/`
|
|
1048
|
+
* command directory into the current descriptor-driven destination (plural
|
|
1049
|
+
* `commands/` for OpenCode — the dir OpenCode actually discovers slash
|
|
1050
|
+
* commands from; unaffected for Kilo, whose descriptor still declares
|
|
1051
|
+
* `command`, so `currentName === LEGACY_NAME` short-circuits below).
|
|
1052
|
+
*
|
|
1053
|
+
* Runs BEFORE materialization writes the fresh command set to the new
|
|
1054
|
+
* location (mirroring `_runLegacyInstallMigrations`'s ordering for the
|
|
1055
|
+
* generic branch, which combined-family runtimes otherwise skip entirely).
|
|
1056
|
+
*
|
|
1057
|
+
* Ownership safety mirrors installer-migrations 003
|
|
1058
|
+
* (rename-get-shit-done-to-gsd-core): only files present, and unchanged or
|
|
1059
|
+
* locally modified, in the PRIOR install manifest under the legacy
|
|
1060
|
+
* `command/<file>` key are removed here — the materialization call
|
|
1061
|
+
* immediately following writes the current command set fresh into the new
|
|
1062
|
+
* location, so removing the stale copies is safe. Anything not proven
|
|
1063
|
+
* manifest-managed (unrelated user content someone dropped into `command/`)
|
|
1064
|
+
* is left untouched, never deleted. The emptied legacy directory is removed
|
|
1065
|
+
* only once nothing else is left inside it.
|
|
1066
|
+
*
|
|
1067
|
+
* Implemented as inline pre-materialization cleanup rather than a
|
|
1068
|
+
* `src/installer-migrations/*.cts` record: the formal migrations framework
|
|
1069
|
+
* only ever DELETES individual files (never directories, and never a
|
|
1070
|
+
* relocate/move primitive — see docs/installer-migrations.md's Action
|
|
1071
|
+
* Types), so the empty-directory removal below would need this same
|
|
1072
|
+
* hand-written glue regardless. It also intentionally is NOT reachable via
|
|
1073
|
+
* combinedFamilyInstall's early return above `_runLegacyInstallMigrations`,
|
|
1074
|
+
* matching the existing precedent that OpenCode/Kilo's bespoke install path
|
|
1075
|
+
* owns its own legacy cleanup rather than routing through the generic
|
|
1076
|
+
* layout-driven migrations hook.
|
|
1077
|
+
*/
|
|
1078
|
+
function _migrateLegacyOpencodeCommandDir(runtime, configDir, behaviors) {
|
|
1079
|
+
const LEGACY_NAME = 'command';
|
|
1080
|
+
const currentName = behaviors.flatCommandDir || LEGACY_NAME;
|
|
1081
|
+
if (currentName === LEGACY_NAME)
|
|
1082
|
+
return; // e.g. Kilo — legacy IS the current location; nothing to migrate
|
|
1083
|
+
const legacyDir = node_path_1.default.join(configDir, LEGACY_NAME);
|
|
1084
|
+
if (!node_fs_1.default.existsSync(legacyDir))
|
|
1085
|
+
return;
|
|
1086
|
+
// Never follow a symlinked legacy dir out of configDir.
|
|
1087
|
+
if (node_fs_1.default.lstatSync(legacyDir).isSymbolicLink())
|
|
1088
|
+
return;
|
|
1089
|
+
const manifest = installerMigrations.readInstallManifest(configDir);
|
|
1090
|
+
let entries;
|
|
1091
|
+
try {
|
|
1092
|
+
entries = node_fs_1.default.readdirSync(legacyDir, { withFileTypes: true });
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
for (const entry of entries) {
|
|
1098
|
+
// command/ is a flat directory of gsd-*.md files; skip anything that
|
|
1099
|
+
// isn't a plain file (nested dirs, symlinks) rather than guess intent.
|
|
1100
|
+
if (!entry.isFile())
|
|
1101
|
+
continue;
|
|
1102
|
+
const relPath = `${LEGACY_NAME}/${entry.name}`;
|
|
1103
|
+
const { classification } = installerMigrations.classifyArtifact(configDir, relPath, manifest);
|
|
1104
|
+
if (classification === 'managed-pristine' || classification === 'managed-modified') {
|
|
1105
|
+
try {
|
|
1106
|
+
node_fs_1.default.unlinkSync(node_path_1.default.join(legacyDir, entry.name));
|
|
1107
|
+
}
|
|
1108
|
+
catch { /* best-effort */ }
|
|
1109
|
+
}
|
|
1110
|
+
// 'unknown' (not manifest-tracked) is left untouched — GSD cannot prove
|
|
1111
|
+
// ownership, so it must never be deleted as collateral damage.
|
|
1112
|
+
}
|
|
1113
|
+
try {
|
|
1114
|
+
if (node_fs_1.default.readdirSync(legacyDir).length === 0)
|
|
1115
|
+
node_fs_1.default.rmdirSync(legacyDir);
|
|
1116
|
+
}
|
|
1117
|
+
catch { /* best-effort — a non-empty or otherwise-busy dir is left in place */ }
|
|
1118
|
+
}
|
|
1119
|
+
// ---------------------------------------------------------------------------
|
|
857
1120
|
// installOpencodeFamilyArtifacts
|
|
858
1121
|
// ---------------------------------------------------------------------------
|
|
859
1122
|
/**
|
|
@@ -869,8 +1132,13 @@ function _installNativePluginIfDeclared(runtime, configDir, behaviors, src) {
|
|
|
869
1132
|
* @param resolvedProfile - from resolveProfile() / resolveEffectiveProfile()
|
|
870
1133
|
* @param resolveAttribution - injection: (runtime) => attribution string | undefined
|
|
871
1134
|
* @param behaviors - the runtime's hostBehaviors descriptor (already resolved by the caller)
|
|
1135
|
+
* @param capabilityRegistry - #2362: optional composed capability registry
|
|
1136
|
+
* (capabilityClusters view), threaded straight through to
|
|
1137
|
+
* installOpencodeFamilySkills so an installed third-party capability skill
|
|
1138
|
+
* materializes for this combined-family (OpenCode/Kilo) install path too.
|
|
1139
|
+
* Absent -> no third-party skills staged (fail closed).
|
|
872
1140
|
*/
|
|
873
|
-
function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, behaviors = {}) {
|
|
1141
|
+
function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfile, resolveAttribution = () => undefined, behaviors = {}, capabilityRegistry) {
|
|
874
1142
|
const isGlobal = scope === 'global';
|
|
875
1143
|
// findInstallSourceRoot resolves DIRECTLY to the commands/gsd source dir
|
|
876
1144
|
// (via the .gsd-source marker or a walk-up from __dirname) — every other
|
|
@@ -887,9 +1155,16 @@ function installOpencodeFamilyArtifacts(runtime, configDir, scope, resolvedProfi
|
|
|
887
1155
|
resolvedTarget: (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir)),
|
|
888
1156
|
homeDir: (0, shell_command_projection_cjs_1.posixNormalize)(node_os_1.default.homedir()),
|
|
889
1157
|
});
|
|
890
|
-
|
|
1158
|
+
// #2329: destDir is derived from the SAME hostBehaviors.flatCommandDir
|
|
1159
|
+
// descriptor value read by writeManifest's manifest-key prefix and by
|
|
1160
|
+
// resolveRuntimeArtifactLayout's commands-kind destSubpath — a hardcoded
|
|
1161
|
+
// literal here would silently diverge from the descriptor the moment either
|
|
1162
|
+
// is edited (Generative Fix Divergence guard). OpenCode uses 'commands'
|
|
1163
|
+
// (plural, the dir OpenCode actually discovers slash commands from); Kilo
|
|
1164
|
+
// keeps its own descriptor value ('command', singular) unchanged.
|
|
1165
|
+
const commandDir = runtimeArtifactInstallPlan.assertDestWithinConfigHome(configDir, behaviors.flatCommandDir || 'command');
|
|
891
1166
|
installOpencodeFamilyCommands(runtime, commandDir, rawCommandsDir, pathPrefix, resolveAttribution);
|
|
892
|
-
installOpencodeFamilySkills(runtime, configDir, rawCommandsDir, pathPrefix, resolveAttribution);
|
|
1167
|
+
installOpencodeFamilySkills(runtime, configDir, rawCommandsDir, pathPrefix, resolveAttribution, resolvedProfile, capabilityRegistry);
|
|
893
1168
|
_installNativePluginIfDeclared(runtime, configDir, behaviors, src);
|
|
894
1169
|
}
|
|
895
1170
|
// ---------------------------------------------------------------------------
|
|
@@ -953,6 +1228,7 @@ module.exports = {
|
|
|
953
1228
|
_hostBehaviors,
|
|
954
1229
|
_copyStaged,
|
|
955
1230
|
hasExistingSymlinkBetween,
|
|
1231
|
+
isSymlinkedDestOptIn,
|
|
956
1232
|
preserveUserArtifacts,
|
|
957
1233
|
restoreUserArtifacts,
|
|
958
1234
|
migrateLegacyDevPreferencesToSkill,
|