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