@opengsd/gsd-core 1.16.0 → 1.18.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 -2
- package/.opencode/plugins/gsd-core.js +251 -1
- package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
- package/GEMINI.md +1 -1
- package/agents/gsd-advisor-researcher.compact.md +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.compact.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.compact.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.compact.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.compact.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.compact.md +6 -0
- package/agents/gsd-debug-session-manager.md +6 -0
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-verifier.compact.md +4 -4
- package/agents/gsd-doc-verifier.md +4 -3
- package/agents/gsd-doc-writer.compact.md +23 -5
- package/agents/gsd-doc-writer.md +19 -6
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +7 -3
- package/agents/gsd-integration-checker.compact.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.compact.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +2 -1
- package/agents/gsd-planner.md +5 -4
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +5 -5
- package/agents/gsd-research-synthesizer.md +5 -5
- package/agents/gsd-roadmapper.compact.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/agents/gsd-security-auditor.compact.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.compact.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.compact.md +13 -1
- package/agents/gsd-ui-researcher.md +28 -1
- package/agents/gsd-verifier.md +1 -1
- package/bin/gsd-mcp-server.js +3 -1
- package/bin/install.js +398 -95
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +34 -12
- package/gsd-core/bin/lib/adr-parser.cjs +37 -5
- package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
- package/gsd-core/bin/lib/capability-lock.cjs +29 -7
- package/gsd-core/bin/lib/capability-registry.cjs +138 -80
- package/gsd-core/bin/lib/capability-source.cjs +3 -1
- package/gsd-core/bin/lib/capability-state.cjs +6 -3
- package/gsd-core/bin/lib/capability-validator.cjs +8 -5
- package/gsd-core/bin/lib/capability-writer.cjs +2 -1
- package/gsd-core/bin/lib/commands.cjs +33 -9
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
- package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
- package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
- package/gsd-core/bin/lib/graphify.cjs +3 -2
- package/gsd-core/bin/lib/init.cjs +43 -16
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- package/gsd-core/bin/lib/install-engine.cjs +11 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
- package/gsd-core/bin/lib/install-profiles.cjs +5 -3
- package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
- package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
- package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
- package/gsd-core/bin/lib/mcp-server.cjs +5 -1
- package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
- package/gsd-core/bin/lib/opencode-version.cjs +486 -0
- package/gsd-core/bin/lib/phase.cjs +141 -11
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
- package/gsd-core/bin/lib/report-parser.cjs +57 -1
- package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
- package/gsd-core/bin/lib/roadmap.cjs +56 -5
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
- package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +100 -53
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +98 -19
- package/gsd-core/bin/lib/state.cjs +544 -126
- package/gsd-core/bin/lib/task-command-router.cjs +2 -3
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
- package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
- package/gsd-core/bin/lib/update-context.cjs +18 -1
- package/gsd-core/bin/lib/verification.cjs +107 -17
- package/gsd-core/bin/lib/verify.cjs +36 -1
- package/gsd-core/references/agent-contracts.md +1 -1
- package/gsd-core/references/agent-skills-bootstrap.md +26 -7
- package/gsd-core/references/git-integration.md +3 -3
- package/gsd-core/references/planner-reversibility.md +18 -4
- package/gsd-core/references/planner-verify-command-grounding.md +18 -0
- package/gsd-core/references/tdd.md +4 -3
- package/gsd-core/references/verifier-phase-gates.md +8 -1
- package/gsd-core/workflows/add-backlog.md +10 -2
- package/gsd-core/workflows/add-phase.md +5 -1
- package/gsd-core/workflows/add-tests.md +5 -1
- package/gsd-core/workflows/add-todo.md +5 -1
- package/gsd-core/workflows/ai-integration-phase.md +5 -1
- package/gsd-core/workflows/audit-milestone.md +21 -9
- package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
- package/gsd-core/workflows/autonomous.md +52 -64
- package/gsd-core/workflows/check-todos.md +5 -1
- package/gsd-core/workflows/code-review-fix.md +5 -1
- package/gsd-core/workflows/code-review.md +21 -2
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
- package/gsd-core/workflows/complete-milestone.md +25 -5
- package/gsd-core/workflows/debug.md +5 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
- package/gsd-core/workflows/discuss-phase.md +5 -1
- package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
- package/gsd-core/workflows/docs-update.md +27 -5
- package/gsd-core/workflows/edit-phase.md +5 -1
- package/gsd-core/workflows/eval-review.md +11 -3
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
- package/gsd-core/workflows/execute-phase.md +40 -7
- package/gsd-core/workflows/execute-plan.md +8 -4
- package/gsd-core/workflows/extract-learnings.md +6 -2
- package/gsd-core/workflows/forensics.md +5 -1
- package/gsd-core/workflows/import.md +5 -1
- package/gsd-core/workflows/insert-phase.md +5 -1
- package/gsd-core/workflows/manager.md +10 -2
- package/gsd-core/workflows/map-codebase.md +5 -1
- package/gsd-core/workflows/milestone-summary.md +10 -2
- package/gsd-core/workflows/new-milestone.md +13 -5
- package/gsd-core/workflows/new-project.md +9 -5
- package/gsd-core/workflows/next.md +8 -4
- package/gsd-core/workflows/pause-work.md +3 -3
- package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
- package/gsd-core/workflows/plan-phase.md +41 -73
- package/gsd-core/workflows/plan-review-convergence.md +17 -5
- package/gsd-core/workflows/plant-seed.md +8 -4
- package/gsd-core/workflows/progress.md +6 -2
- package/gsd-core/workflows/quick-batch.md +5 -1
- package/gsd-core/workflows/quick.md +22 -7
- package/gsd-core/workflows/remove-phase.md +5 -1
- package/gsd-core/workflows/resume-project.md +19 -11
- package/gsd-core/workflows/review.md +6 -2
- package/gsd-core/workflows/scan.md +5 -1
- package/gsd-core/workflows/secure-phase.md +6 -2
- package/gsd-core/workflows/settings.md +3 -3
- package/gsd-core/workflows/ship.md +13 -10
- package/gsd-core/workflows/thread.md +10 -2
- package/gsd-core/workflows/transition.md +5 -1
- package/gsd-core/workflows/ui-phase.md +5 -1
- package/gsd-core/workflows/ui-review.md +6 -2
- package/gsd-core/workflows/ultraplan-phase.md +5 -1
- package/gsd-core/workflows/update.md +5 -1
- package/gsd-core/workflows/validate-phase.md +6 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
- package/gsd-core/workflows/verify-work.md +11 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
- package/hooks/dist/gsd-check-update.js +26 -20
- package/hooks/dist/gsd-context-monitor.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +189 -15
- package/hooks/dist/gsd-statusline.js +321 -22
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-check-update.js +26 -20
- package/hooks/gsd-context-monitor.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +189 -15
- package/hooks/gsd-statusline.js +321 -22
- package/hooks/gsd-validate-commit.sh +27 -5
- package/package.json +1 -1
- package/scripts/audit-workflow-script-paths.cjs +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
- package/scripts/diagnose-validate-commit-phases.cjs +348 -0
- package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-capability-registry.cjs +7 -1
- package/scripts/gen-loop-host-contract.cjs +4 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/mutation-matrix.cjs +3 -1
- package/scripts/release-tarball-smoke.cjs +15 -7
- package/scripts/xtrace-phase-timing.cjs +205 -0
- package/skills/gsd-code-review/SKILL.md +8 -0
- package/skills/gsd-pause-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
getGlobalSkillsBase,
|
|
36
36
|
resolveKimiHooksTomlDir,
|
|
37
37
|
isRegisteredRuntimeId,
|
|
38
|
+
resolveGsdHome,
|
|
38
39
|
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
|
39
40
|
// #2870: the Install Scope Module — turns a bare 'global' | 'local' scope id
|
|
40
41
|
// plus a runtime into one resolved value (configHome, settingsFile,
|
|
@@ -190,7 +191,7 @@ function isCodexHooksFeatureKey(key) {
|
|
|
190
191
|
// GSD_CLAUDE_LEGACY_DENY_PERMISSIONS below.)
|
|
191
192
|
//
|
|
192
193
|
// Format: each string uses Claude Code's documented permission rule syntax \u2014
|
|
193
|
-
// "Tool(pattern)" e.g. "
|
|
194
|
+
// "Tool(pattern)" e.g. "Read(.planning/*)", "Edit(STATE.md)"
|
|
194
195
|
// "Tool" (bare tool name, no pattern)
|
|
195
196
|
//
|
|
196
197
|
// Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
|
|
@@ -199,7 +200,6 @@ function isCodexHooksFeatureKey(key) {
|
|
|
199
200
|
// instead of scattered literal 'claude' defaults/rosters (#2086).
|
|
200
201
|
const DEFAULT_RUNTIME = 'claude';
|
|
201
202
|
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
|
|
202
|
-
'Bash(npx gsd-core *)',
|
|
203
203
|
'Read(.planning/*)',
|
|
204
204
|
'Edit(.planning/*)',
|
|
205
205
|
'Read(STATE.md)',
|
|
@@ -232,9 +232,14 @@ const GSD_CLAUDE_LEGACY_DENY_PERMISSIONS = Object.freeze([
|
|
|
232
232
|
// mergeClaudePermissions and uninstall cleanup retire those stale entries on
|
|
233
233
|
// existing installs while the current GSD_CLAUDE_ALLOW_PERMISSIONS above
|
|
234
234
|
// carries the working `Edit(...)` forms.
|
|
235
|
+
// #5054 — `Bash(npx gsd-core *)` is retired the same way. The project ships
|
|
236
|
+
// as `@opengsd/gsd-core`; the unscoped `gsd-core` npm name belongs to someone
|
|
237
|
+
// else, so the rule pre-authorized whatever that name's owner publishes next.
|
|
238
|
+
// GSD never runs the bare form (update.md uses `--package=@opengsd/gsd-core`).
|
|
235
239
|
const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
|
|
236
240
|
'Write(.planning/*)',
|
|
237
241
|
'Write(STATE.md)',
|
|
242
|
+
'Bash(npx gsd-core *)',
|
|
238
243
|
]);
|
|
239
244
|
|
|
240
245
|
/**
|
|
@@ -249,6 +254,7 @@ const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
|
|
|
249
254
|
* `Write(...)` forms from before the fix) is removed from permissions.allow,
|
|
250
255
|
* so existing installs end up with the working `Edit(...)` forms instead of
|
|
251
256
|
* both the dead legacy entry and its replacement sitting side by side.
|
|
257
|
+
* The same filter retires the unscoped `Bash(npx gsd-core *)` rule (#5054).
|
|
252
258
|
*
|
|
253
259
|
* Migration (#4221): the retired GSD_CLAUDE_LEGACY_DENY_PERMISSIONS entries
|
|
254
260
|
* are removed from permissions.deny (byte-equal only). Nothing is added to
|
|
@@ -408,6 +414,26 @@ const GSD_WINDSURF_HOOK_SCRIPTS = hooksSurface.GSD_WINDSURF_HOOK_SCRIPTS;
|
|
|
408
414
|
// two guards require so their copies cannot drift.
|
|
409
415
|
const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js', 'injection-patterns.js'];
|
|
410
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Every file GSD stages under `hooks/lib/` (#5335). The install copies `hooks/dist/lib/` wholesale (and the
|
|
419
|
+
* allow-listed names from `hooks/lib/`), so a fixed list drifts the moment a helper is added: the new file is
|
|
420
|
+
* staged but never removed, which also keeps `hooks/lib/` non-empty and blocks its rmdir. The set is the
|
|
421
|
+
* allow-list plus the top-level files of the directories the installer copies from, so uninstall and the
|
|
422
|
+
* manifest cannot disagree with install. Only these names are ever removed; a user's own helper beside them
|
|
423
|
+
* is left alone.
|
|
424
|
+
*/
|
|
425
|
+
function gsdHookLibFiles() {
|
|
426
|
+
const names = new Set(GSD_HOOK_LIB_FILES);
|
|
427
|
+
for (const parts of [['hooks', 'dist', 'lib'], ['hooks', 'lib']]) {
|
|
428
|
+
try {
|
|
429
|
+
for (const entry of fs.readdirSync(path.join(__dirname, '..', ...parts), { withFileTypes: true })) {
|
|
430
|
+
if (entry.isFile()) names.add(entry.name);
|
|
431
|
+
}
|
|
432
|
+
} catch { /* the directory is absent from this package layout */ }
|
|
433
|
+
}
|
|
434
|
+
return [...names];
|
|
435
|
+
}
|
|
436
|
+
|
|
411
437
|
/**
|
|
412
438
|
* Directory name GSD stages its shared hook bundle under, inside a runtime's
|
|
413
439
|
* install root. Defaults to 'hooks' — the name every runtime used before #3023.
|
|
@@ -762,8 +788,11 @@ const {
|
|
|
762
788
|
discoverInstallerMigrations,
|
|
763
789
|
MANIFEST_SCHEMA_VERSION,
|
|
764
790
|
readInstallManifest,
|
|
791
|
+
recordHostGeneration,
|
|
765
792
|
runInstallerMigrations,
|
|
766
793
|
} = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
|
|
794
|
+
const { setJsoncValue, removeJsoncValue, hasJsoncFeatures, parseJsonc } = require(path.join(_gsdLibDir, 'jsonc-edit.cjs'));
|
|
795
|
+
const { isGsdMcpEntry, GSD_MCP_NO_PROMPTS_FLAG } = require(path.join(_gsdLibDir, 'opencode-mcp-entry.cjs'));
|
|
767
796
|
const {
|
|
768
797
|
assertInstallerMigrationsUnblocked,
|
|
769
798
|
resolveInstallerMigrationPromptsForNonTty,
|
|
@@ -1256,6 +1285,17 @@ const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
|
|
|
1256
1285
|
// writeKimiHooksToml / removeKimiHooksToml: kimi's native config.toml [[hooks]]
|
|
1257
1286
|
// surface (#2095 EoS/kimi Upgrade 1) — separate from settings.json entirely.
|
|
1258
1287
|
const writeKimiHooksToml = hooksSurface.writeKimiHooksToml;
|
|
1288
|
+
|
|
1289
|
+
// #4840: false when the runtime declares `context-warnings` unsupported in its
|
|
1290
|
+
// descriptor (`hostBehaviors.unsupportedFeatures`). gsd-context-monitor.js reads
|
|
1291
|
+
// the metrics bridge file only the statusline hook writes, and the statusline
|
|
1292
|
+
// is wired for Claude Code alone, so on any other host the hook fires on every
|
|
1293
|
+
// tool event, finds no file and exits silently — forever. Such a host gets no
|
|
1294
|
+
// monitor registration (stale ones are stripped), as Codex does since #2586.
|
|
1295
|
+
function runtimeSupportsContextWarnings(runtime) {
|
|
1296
|
+
const unsupported = hostBehaviorsFor(runtime).unsupportedFeatures;
|
|
1297
|
+
return !(Array.isArray(unsupported) && unsupported.includes('context-warnings'));
|
|
1298
|
+
}
|
|
1259
1299
|
const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
|
|
1260
1300
|
// processAttribution: pure Co-Authored-By content transform, relocated to the
|
|
1261
1301
|
// conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
|
|
@@ -6959,6 +6999,82 @@ function writeCopilotHookConfig(targetDir) {
|
|
|
6959
6999
|
* Reads agent .md files from source, extracts metadata, writes .toml configs.
|
|
6960
7000
|
*/
|
|
6961
7001
|
|
|
7002
|
+
// #5335: ~/.gsd/defaults.json is machine-wide, so uninstalling one runtime must not strip a default another
|
|
7003
|
+
// still needs, nor one the user chose. A sidecar records which keys an install SET (not ones it found), the
|
|
7004
|
+
// value it set, and which non-Claude runtimes have installed since; uninstall reverts a key only when its
|
|
7005
|
+
// last such runtime is gone and the value is still the one the installer wrote.
|
|
7006
|
+
const DEFAULTS_PROVENANCE_NAME = 'defaults-provenance.json';
|
|
7007
|
+
|
|
7008
|
+
function readDefaultsProvenance(gsdDir) {
|
|
7009
|
+
try {
|
|
7010
|
+
const p = JSON.parse(fs.readFileSync(path.join(gsdDir, DEFAULTS_PROVENANCE_NAME), 'utf8'));
|
|
7011
|
+
if (p && typeof p === 'object' && p.keys && typeof p.keys === 'object') return p;
|
|
7012
|
+
} catch { /* absent or unreadable */ }
|
|
7013
|
+
return { version: 1, keys: {} };
|
|
7014
|
+
}
|
|
7015
|
+
|
|
7016
|
+
function recordDefaultsProvenance(gsdDir, runtime, defaults, setNow) {
|
|
7017
|
+
const prov = readDefaultsProvenance(gsdDir);
|
|
7018
|
+
let changed = false;
|
|
7019
|
+
for (const key of ['resolve_model_ids', 'runtime']) {
|
|
7020
|
+
let entry = prov.keys[key];
|
|
7021
|
+
if (setNow[key]) {
|
|
7022
|
+
entry = { setValue: defaults[key], runtimes: [] };
|
|
7023
|
+
prov.keys[key] = entry;
|
|
7024
|
+
changed = true;
|
|
7025
|
+
}
|
|
7026
|
+
if (entry && Array.isArray(entry.runtimes) && !entry.runtimes.includes(runtime)) {
|
|
7027
|
+
entry.runtimes.push(runtime);
|
|
7028
|
+
changed = true;
|
|
7029
|
+
}
|
|
7030
|
+
}
|
|
7031
|
+
if (changed) atomicWriteFileSync(path.join(gsdDir, DEFAULTS_PROVENANCE_NAME), JSON.stringify(prov, null, 2) + '\n', 'utf8');
|
|
7032
|
+
}
|
|
7033
|
+
|
|
7034
|
+
/** @returns {boolean} true when it removed or reverted something */
|
|
7035
|
+
function revertNonClaudeDefaults(runtime) {
|
|
7036
|
+
if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return false;
|
|
7037
|
+
const gsdDir = path.join(resolveGsdHome(), '.gsd'); // #5017: GSD_HOME-aware, like the loader
|
|
7038
|
+
const provPath = path.join(gsdDir, DEFAULTS_PROVENANCE_NAME);
|
|
7039
|
+
if (!fs.existsSync(provPath)) return false;
|
|
7040
|
+
let releaseLock = null;
|
|
7041
|
+
try {
|
|
7042
|
+
releaseLock = acquireInstallMigrationLock(gsdDir);
|
|
7043
|
+
const prov = readDefaultsProvenance(gsdDir);
|
|
7044
|
+
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
7045
|
+
let defaults = null;
|
|
7046
|
+
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* absent or corrupt */ }
|
|
7047
|
+
const reverted = [];
|
|
7048
|
+
for (const [key, entry] of Object.entries(prov.keys)) {
|
|
7049
|
+
if (!entry || !Array.isArray(entry.runtimes)) { delete prov.keys[key]; continue; }
|
|
7050
|
+
entry.runtimes = entry.runtimes.filter((r) => r !== runtime);
|
|
7051
|
+
if (entry.runtimes.length > 0) continue;
|
|
7052
|
+
if (defaults && typeof defaults === 'object' && defaults[key] === entry.setValue) {
|
|
7053
|
+
delete defaults[key];
|
|
7054
|
+
reverted.push(key);
|
|
7055
|
+
}
|
|
7056
|
+
delete prov.keys[key];
|
|
7057
|
+
}
|
|
7058
|
+
if (reverted.length > 0 && defaults) {
|
|
7059
|
+
if (Object.keys(defaults).length === 0) fs.rmSync(defaultsPath, { force: true });
|
|
7060
|
+
else atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
|
|
7061
|
+
console.log(` ${green}✓${reset} Reverted ${reverted.join(', ')} in ~/.gsd/defaults.json (set by the ${runtime} install)`);
|
|
7062
|
+
}
|
|
7063
|
+
if (Object.keys(prov.keys).length === 0) fs.rmSync(provPath, { force: true });
|
|
7064
|
+
else atomicWriteFileSync(provPath, JSON.stringify(prov, null, 2) + '\n', 'utf8');
|
|
7065
|
+
return reverted.length > 0;
|
|
7066
|
+
} catch (e) {
|
|
7067
|
+
console.log(` ${yellow}⚠${reset} Could not update ~/.gsd/defaults.json: ${e.message}`);
|
|
7068
|
+
return false;
|
|
7069
|
+
} finally {
|
|
7070
|
+
if (releaseLock) {
|
|
7071
|
+
try { releaseLock(); } catch { /* the stale-lock reaper clears it */ }
|
|
7072
|
+
}
|
|
7073
|
+
// ~/.gsd is GSD's own directory; drop it once the lock is released and nothing else lives in it.
|
|
7074
|
+
try { fs.rmdirSync(gsdDir); } catch { /* other GSD state lives here */ }
|
|
7075
|
+
}
|
|
7076
|
+
}
|
|
7077
|
+
|
|
6962
7078
|
/**
|
|
6963
7079
|
* #2834: Write ~/.gsd/defaults.json for non-Claude runtimes — sets
|
|
6964
7080
|
* resolve_model_ids="omit" (so resolveModelInternal() returns '' instead of
|
|
@@ -6971,7 +7087,7 @@ function writeCopilotHookConfig(targetDir) {
|
|
|
6971
7087
|
*/
|
|
6972
7088
|
function writeNonClaudeDefaults(runtime) {
|
|
6973
7089
|
if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return;
|
|
6974
|
-
const gsdDir = path.join(
|
|
7090
|
+
const gsdDir = path.join(resolveGsdHome(), '.gsd'); // #5017: GSD_HOME-aware, like the loader
|
|
6975
7091
|
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
6976
7092
|
let releaseLock = null;
|
|
6977
7093
|
try {
|
|
@@ -7005,6 +7121,10 @@ function writeNonClaudeDefaults(runtime) {
|
|
|
7005
7121
|
atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
|
|
7006
7122
|
for (const message of applied) console.log(` ${green}✓${reset} ${message}`);
|
|
7007
7123
|
}
|
|
7124
|
+
recordDefaultsProvenance(gsdDir, runtime, defaults, {
|
|
7125
|
+
resolve_model_ids: shouldDefaultToOmit,
|
|
7126
|
+
runtime: applied.some((m) => m.startsWith('Set runtime:')),
|
|
7127
|
+
});
|
|
7008
7128
|
} catch (e) {
|
|
7009
7129
|
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
7010
7130
|
} finally {
|
|
@@ -7212,7 +7332,11 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7212
7332
|
const newLines = [];
|
|
7213
7333
|
let inAllowedTools = false;
|
|
7214
7334
|
let inSkippedArray = false;
|
|
7335
|
+
let inAgentTools = false;
|
|
7336
|
+
let inDisallowedTools = false;
|
|
7215
7337
|
const allowedTools = [];
|
|
7338
|
+
const agentTools = [];
|
|
7339
|
+
const agentDisallowed = [];
|
|
7216
7340
|
|
|
7217
7341
|
for (const line of lines) {
|
|
7218
7342
|
const trimmed = line.trim();
|
|
@@ -7228,11 +7352,31 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7228
7352
|
continue;
|
|
7229
7353
|
}
|
|
7230
7354
|
|
|
7355
|
+
// Agents: collect the `tools:` allowlist and `disallowedTools:` denylist (block-list continuation
|
|
7356
|
+
// lines) so they can be rendered as `permission:` entries below (mirrors
|
|
7357
|
+
// src/runtime-artifact-conversion.cts's convertClaudeToOpencodeFrontmatter, #5338).
|
|
7358
|
+
if (isAgent && (inAgentTools || inDisallowedTools)) {
|
|
7359
|
+
if (trimmed.startsWith('- ')) {
|
|
7360
|
+
const tool = runtimeArtifactConversion._decodeToolScalar(trimmed.substring(2));
|
|
7361
|
+
if (tool !== null) (inAgentTools ? agentTools : agentDisallowed).push(tool);
|
|
7362
|
+
continue;
|
|
7363
|
+
}
|
|
7364
|
+
if (trimmed && !trimmed.startsWith('-')) {
|
|
7365
|
+
inAgentTools = false;
|
|
7366
|
+
inDisallowedTools = false;
|
|
7367
|
+
}
|
|
7368
|
+
}
|
|
7369
|
+
|
|
7231
7370
|
// Detect inline tools: field (comma-separated string)
|
|
7232
7371
|
if (trimmed.startsWith('tools:')) {
|
|
7233
7372
|
if (isAgent) {
|
|
7234
|
-
|
|
7235
|
-
|
|
7373
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
7374
|
+
// A comment-only value (`tools: # note`) is not inline content: read the block list instead.
|
|
7375
|
+
if (toolsValue && !toolsValue.startsWith('#')) {
|
|
7376
|
+
agentTools.push(...runtimeArtifactConversion._splitToolScalars(toolsValue).map(runtimeArtifactConversion._decodeToolScalar).filter((tool) => tool !== null));
|
|
7377
|
+
} else {
|
|
7378
|
+
inAgentTools = true;
|
|
7379
|
+
}
|
|
7236
7380
|
continue;
|
|
7237
7381
|
}
|
|
7238
7382
|
const toolsValue = trimmed.substring(6).trim();
|
|
@@ -7244,8 +7388,18 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7244
7388
|
continue;
|
|
7245
7389
|
}
|
|
7246
7390
|
|
|
7247
|
-
|
|
7248
|
-
|
|
7391
|
+
if (isAgent && trimmed.startsWith('disallowedTools:')) {
|
|
7392
|
+
const value = trimmed.substring(16).trim();
|
|
7393
|
+
if (value && !value.startsWith('#')) {
|
|
7394
|
+
agentDisallowed.push(...runtimeArtifactConversion._splitToolScalars(value).map(runtimeArtifactConversion._decodeToolScalar).filter((tool) => tool !== null));
|
|
7395
|
+
} else {
|
|
7396
|
+
inDisallowedTools = true;
|
|
7397
|
+
}
|
|
7398
|
+
continue;
|
|
7399
|
+
}
|
|
7400
|
+
|
|
7401
|
+
// For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:
|
|
7402
|
+
if (isAgent && /^(skills|color|memory|maxTurns|permissionMode):/.test(trimmed)) {
|
|
7249
7403
|
inSkippedArray = true;
|
|
7250
7404
|
continue;
|
|
7251
7405
|
}
|
|
@@ -7318,6 +7472,7 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7318
7472
|
if (modelOverride) {
|
|
7319
7473
|
newLines.push(`model: ${modelOverride}`);
|
|
7320
7474
|
}
|
|
7475
|
+
newLines.push(...runtimeArtifactConversion._buildOpencodeAgentPermissionBlock(agentTools, agentDisallowed));
|
|
7321
7476
|
}
|
|
7322
7477
|
|
|
7323
7478
|
// For commands: add tools object if we had allowed-tools or tools
|
|
@@ -8239,7 +8394,7 @@ function reclaimKimiHooksRoot(kimiHooksRoot) {
|
|
|
8239
8394
|
const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
|
|
8240
8395
|
if (fs.existsSync(kimiHooksLibDir)) {
|
|
8241
8396
|
let removedKimiLibFiles = 0;
|
|
8242
|
-
for (const file of
|
|
8397
|
+
for (const file of gsdHookLibFiles()) {
|
|
8243
8398
|
try {
|
|
8244
8399
|
fs.unlinkSync(path.join(kimiHooksLibDir, file));
|
|
8245
8400
|
removedKimiLibFiles++;
|
|
@@ -8875,7 +9030,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8875
9030
|
const hooksLibDir = path.join(hooksDir, 'lib');
|
|
8876
9031
|
if (fs.existsSync(hooksLibDir)) {
|
|
8877
9032
|
let removedLibFiles = 0;
|
|
8878
|
-
for (const file of
|
|
9033
|
+
for (const file of gsdHookLibFiles()) {
|
|
8879
9034
|
const filePath = path.join(hooksLibDir, file);
|
|
8880
9035
|
try {
|
|
8881
9036
|
fs.unlinkSync(filePath);
|
|
@@ -8911,6 +9066,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8911
9066
|
console.log(` ${green}✓${reset} Removed GSD hooks/package.json (CommonJS marker)`);
|
|
8912
9067
|
}
|
|
8913
9068
|
} catch { /* best-effort */ }
|
|
9069
|
+
// Once GSD's hooks, helpers and marker are gone, an empty hooks dir is GSD's own leftover (#5335).
|
|
9070
|
+
// rmdir fails on a non-empty dir, so a user's own hook scripts keep it in place.
|
|
9071
|
+
try { fs.rmdirSync(hooksDir); } catch { /* not empty or absent */ }
|
|
8914
9072
|
}
|
|
8915
9073
|
|
|
8916
9074
|
// 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093).
|
|
@@ -8944,6 +9102,20 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8944
9102
|
// unlink above failed, pulling the marker out from under a still-present
|
|
8945
9103
|
// CommonJS adapter would leave it unloadable. The exact content match
|
|
8946
9104
|
// still leaves any user-authored package.json in place.
|
|
9105
|
+
// The terminal-client plugin directory (OpenCode 2.x statusline): GSD's file, then its now-empty directory.
|
|
9106
|
+
const _cliU = hostBehaviorsFor(runtime).nativeCliPlugin;
|
|
9107
|
+
if (_cliU) {
|
|
9108
|
+
const cliFile = path.join(targetDir, _cliU.dir, _cliU.file);
|
|
9109
|
+
if (fs.existsSync(cliFile)) {
|
|
9110
|
+
try {
|
|
9111
|
+
fs.unlinkSync(cliFile);
|
|
9112
|
+
removedCount++;
|
|
9113
|
+
removedFromPluginsDir = true;
|
|
9114
|
+
console.log(` ${green}✓${reset} Removed terminal-client plugin (${runtime})`);
|
|
9115
|
+
} catch (_) { /* best-effort */ }
|
|
9116
|
+
}
|
|
9117
|
+
try { fs.rmdirSync(path.join(targetDir, _cliU.dir)); } catch (_) { /* not empty or absent */ }
|
|
9118
|
+
}
|
|
8947
9119
|
if (!fs.existsSync(pluginPath) && removeCommonJsMarker(pluginsDir)) {
|
|
8948
9120
|
removedCount++;
|
|
8949
9121
|
removedFromPluginsDir = true;
|
|
@@ -9171,40 +9343,56 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9171
9343
|
}
|
|
9172
9344
|
}
|
|
9173
9345
|
|
|
9174
|
-
// 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
|
|
9346
|
+
// 6. For OpenCode, clean up permissions and the companion MCP entry from opencode.json or opencode.jsonc
|
|
9175
9347
|
if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
|
|
9176
9348
|
const configPath = resolveOpencodeConfigPath(targetDir);
|
|
9177
9349
|
if (fs.existsSync(configPath)) {
|
|
9178
9350
|
try {
|
|
9179
|
-
const
|
|
9180
|
-
|
|
9351
|
+
const originalText = fs.readFileSync(configPath, 'utf8');
|
|
9352
|
+
const config = parseJsonc(originalText);
|
|
9353
|
+
const edits = [];
|
|
9181
9354
|
|
|
9182
9355
|
// Remove GSD permission entries
|
|
9183
|
-
if (config.permission) {
|
|
9356
|
+
if (config.permission && typeof config.permission === 'object') {
|
|
9184
9357
|
for (const permType of ['read', 'external_directory']) {
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
}
|
|
9192
|
-
}
|
|
9193
|
-
// Clean up empty objects
|
|
9194
|
-
if (Object.keys(config.permission[permType]).length === 0) {
|
|
9195
|
-
delete config.permission[permType];
|
|
9358
|
+
const section = config.permission[permType];
|
|
9359
|
+
if (!section || typeof section !== 'object') continue;
|
|
9360
|
+
for (const key of Object.keys(section)) {
|
|
9361
|
+
if (key.includes('gsd-core')) {
|
|
9362
|
+
delete section[key];
|
|
9363
|
+
edits.push({ path: ['permission', permType, key], remove: true });
|
|
9196
9364
|
}
|
|
9197
9365
|
}
|
|
9366
|
+
// Clean up empty objects
|
|
9367
|
+
if (Object.keys(section).length === 0) delete config.permission[permType];
|
|
9198
9368
|
}
|
|
9199
|
-
if (Object.keys(config.permission).length === 0)
|
|
9200
|
-
|
|
9369
|
+
if (Object.keys(config.permission).length === 0) delete config.permission;
|
|
9370
|
+
}
|
|
9371
|
+
|
|
9372
|
+
// #5333: remove the companion MCP entry, but only while it is still the entry GSD wrote — a
|
|
9373
|
+
// user-edited or user-defined `gsd` server stays. Both OpenCode generations' shapes: the 1.x
|
|
9374
|
+
// `mcp.gsd` and the 2.x native `mcp.servers.gsd`.
|
|
9375
|
+
let mcpRemoved = false;
|
|
9376
|
+
if (config.mcp && typeof config.mcp === 'object') {
|
|
9377
|
+
if (isGsdMcpEntry(config.mcp.gsd)) {
|
|
9378
|
+
delete config.mcp.gsd;
|
|
9379
|
+
edits.push({ path: ['mcp', 'gsd'], remove: true });
|
|
9380
|
+
mcpRemoved = true;
|
|
9201
9381
|
}
|
|
9382
|
+
const servers = config.mcp.servers;
|
|
9383
|
+
if (servers && typeof servers === 'object' && isGsdMcpEntry(servers.gsd)) {
|
|
9384
|
+
delete servers.gsd;
|
|
9385
|
+
edits.push({ path: ['mcp', 'servers', 'gsd'], remove: true });
|
|
9386
|
+
if (Object.keys(servers).length === 0) delete config.mcp.servers;
|
|
9387
|
+
mcpRemoved = true;
|
|
9388
|
+
}
|
|
9389
|
+
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
|
9202
9390
|
}
|
|
9203
9391
|
|
|
9204
|
-
if (
|
|
9205
|
-
|
|
9392
|
+
if (edits.length > 0) {
|
|
9393
|
+
writeJsoncConfig(configPath, originalText, config, edits);
|
|
9206
9394
|
removedCount++;
|
|
9207
|
-
console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
|
|
9395
|
+
console.log(` ${green}✓${reset} Removed GSD permissions${mcpRemoved ? ' and MCP server' : ''} from ${path.basename(configPath)}`);
|
|
9208
9396
|
}
|
|
9209
9397
|
} catch (e) {
|
|
9210
9398
|
// Ignore JSON parse errors
|
|
@@ -9219,8 +9407,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9219
9407
|
const configPath = resolveKiloConfigPath(targetDir);
|
|
9220
9408
|
if (fs.existsSync(configPath)) {
|
|
9221
9409
|
try {
|
|
9222
|
-
const
|
|
9223
|
-
|
|
9410
|
+
const originalText = fs.readFileSync(configPath, 'utf8');
|
|
9411
|
+
const config = parseJsonc(originalText);
|
|
9412
|
+
const edits = [];
|
|
9224
9413
|
|
|
9225
9414
|
// Remove GSD permission entries
|
|
9226
9415
|
if (config.permission) {
|
|
@@ -9230,7 +9419,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9230
9419
|
for (const key of keys) {
|
|
9231
9420
|
if (key.includes('gsd-core')) {
|
|
9232
9421
|
delete config.permission[permType][key];
|
|
9233
|
-
|
|
9422
|
+
edits.push({ path: ['permission', permType, key], remove: true });
|
|
9234
9423
|
}
|
|
9235
9424
|
}
|
|
9236
9425
|
// Clean up empty objects
|
|
@@ -9244,8 +9433,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9244
9433
|
}
|
|
9245
9434
|
}
|
|
9246
9435
|
|
|
9247
|
-
if (
|
|
9248
|
-
|
|
9436
|
+
if (edits.length > 0) {
|
|
9437
|
+
writeJsoncConfig(configPath, originalText, config, edits);
|
|
9249
9438
|
removedCount++;
|
|
9250
9439
|
console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
|
|
9251
9440
|
}
|
|
@@ -9287,6 +9476,67 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9287
9476
|
console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
|
|
9288
9477
|
}
|
|
9289
9478
|
|
|
9479
|
+
// #5335: the installer's own bookkeeping goes with it — the applied-migrations state and the migration
|
|
9480
|
+
// journal. The journal's `-backups/` folders hold copies of files the USER had modified, so a folder that
|
|
9481
|
+
// still has content is kept; everything GSD generated for itself (run records, rollback snapshots, empty
|
|
9482
|
+
// folders) is removed.
|
|
9483
|
+
const installStatePath = path.join(targetDir, 'gsd-install-state.json');
|
|
9484
|
+
if (fs.existsSync(installStatePath)) {
|
|
9485
|
+
fs.rmSync(installStatePath, { force: true });
|
|
9486
|
+
removedCount++;
|
|
9487
|
+
console.log(` ${green}✓${reset} Removed gsd-install-state.json`);
|
|
9488
|
+
}
|
|
9489
|
+
const journalDir = path.join(targetDir, 'gsd-migration-journal');
|
|
9490
|
+
if (fs.existsSync(journalDir)) {
|
|
9491
|
+
let journalEntriesRemoved = 0;
|
|
9492
|
+
try {
|
|
9493
|
+
for (const entry of fs.readdirSync(journalDir, { withFileTypes: true })) {
|
|
9494
|
+
const full = path.join(journalDir, entry.name);
|
|
9495
|
+
if (entry.isSymbolicLink()) continue;
|
|
9496
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
9497
|
+
fs.rmSync(full, { force: true });
|
|
9498
|
+
journalEntriesRemoved++;
|
|
9499
|
+
} else if (entry.isDirectory() && entry.name.endsWith('-rollback')) {
|
|
9500
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
9501
|
+
journalEntriesRemoved++;
|
|
9502
|
+
} else if (entry.isDirectory() && entry.name.endsWith('-backups')) {
|
|
9503
|
+
try { fs.rmdirSync(full); journalEntriesRemoved++; } catch { /* holds the user's backed-up files: keep */ }
|
|
9504
|
+
}
|
|
9505
|
+
}
|
|
9506
|
+
fs.rmdirSync(journalDir);
|
|
9507
|
+
} catch { /* a non-empty journal dir (kept backups) stays */ }
|
|
9508
|
+
if (journalEntriesRemoved > 0) {
|
|
9509
|
+
removedCount++;
|
|
9510
|
+
console.log(` ${green}✓${reset} Removed gsd-migration-journal/ records`);
|
|
9511
|
+
}
|
|
9512
|
+
}
|
|
9513
|
+
|
|
9514
|
+
// #5335: the installer's user-artifact staging area is empty once the install it served is over. rmdir only
|
|
9515
|
+
// succeeds on an empty directory, so anything still staged there is left for its owner.
|
|
9516
|
+
try {
|
|
9517
|
+
fs.rmdirSync(path.join(targetDir, '.gsd-staging', 'user-artifacts'));
|
|
9518
|
+
fs.rmdirSync(path.join(targetDir, '.gsd-staging'));
|
|
9519
|
+
} catch { /* absent or not empty */ }
|
|
9520
|
+
|
|
9521
|
+
// #5335: OpenCode never reads settings.json, but the shared-settings install step creates it as `{}`.
|
|
9522
|
+
// An empty one carries nothing of the user's; a populated one is left alone.
|
|
9523
|
+
if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
|
|
9524
|
+
const emptySettingsPath = path.join(targetDir, 'settings.json');
|
|
9525
|
+
try {
|
|
9526
|
+
const parsed = JSON.parse(fs.readFileSync(emptySettingsPath, 'utf8'));
|
|
9527
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Object.keys(parsed).length === 0) {
|
|
9528
|
+
fs.rmSync(emptySettingsPath, { force: true });
|
|
9529
|
+
removedCount++;
|
|
9530
|
+
console.log(` ${green}✓${reset} Removed empty settings.json`);
|
|
9531
|
+
}
|
|
9532
|
+
} catch { /* absent, unreadable or populated: leave it */ }
|
|
9533
|
+
}
|
|
9534
|
+
|
|
9535
|
+
// #5335: the machine-wide defaults the install wrote for this runtime, when nothing else needs them.
|
|
9536
|
+
if (revertNonClaudeDefaults(runtime)) {
|
|
9537
|
+
removedCount++;
|
|
9538
|
+
}
|
|
9539
|
+
|
|
9290
9540
|
if (removedCount === 0) {
|
|
9291
9541
|
console.log(` ${yellow}⚠${reset} No GSD files found to remove.`);
|
|
9292
9542
|
}
|
|
@@ -9298,64 +9548,30 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9298
9548
|
}
|
|
9299
9549
|
|
|
9300
9550
|
/**
|
|
9301
|
-
*
|
|
9302
|
-
*
|
|
9303
|
-
*
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
if (
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
if (char === '\\' && i + 1 < content.length) {
|
|
9323
|
-
result += next;
|
|
9324
|
-
i += 2;
|
|
9325
|
-
continue;
|
|
9326
|
-
}
|
|
9327
|
-
if (char === '"') {
|
|
9328
|
-
inString = false;
|
|
9329
|
-
}
|
|
9330
|
-
i++;
|
|
9331
|
-
} else {
|
|
9332
|
-
if (char === '"') {
|
|
9333
|
-
inString = true;
|
|
9334
|
-
result += char;
|
|
9335
|
-
i++;
|
|
9336
|
-
} else if (char === '/' && next === '/') {
|
|
9337
|
-
// Skip single-line comment until end of line
|
|
9338
|
-
while (i < content.length && content[i] !== '\n') {
|
|
9339
|
-
i++;
|
|
9340
|
-
}
|
|
9341
|
-
} else if (char === '/' && next === '*') {
|
|
9342
|
-
// Skip block comment
|
|
9343
|
-
i += 2;
|
|
9344
|
-
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) {
|
|
9345
|
-
i++;
|
|
9346
|
-
}
|
|
9347
|
-
i += 2; // Skip closing */
|
|
9348
|
-
} else {
|
|
9349
|
-
result += char;
|
|
9350
|
-
i++;
|
|
9351
|
-
}
|
|
9551
|
+
* Write back an OpenCode/Kilo config the installer changed (#5334).
|
|
9552
|
+
*
|
|
9553
|
+
* A file that carries JSONC syntax (a `.jsonc` name, comments or trailing commas) is edited as text, so
|
|
9554
|
+
* only the keys GSD owns change and the user's annotations and layout survive. Anything the text editor
|
|
9555
|
+
* cannot handle, and plain JSON, keeps the previous `JSON.stringify` rewrite.
|
|
9556
|
+
*
|
|
9557
|
+
* @param {string} configPath
|
|
9558
|
+
* @param {string|null} originalText current file text, or null when the file is new
|
|
9559
|
+
* @param {object} config the mutated parsed config (used for the fallback)
|
|
9560
|
+
* @param {Array<{path: string[], value?: unknown, remove?: boolean}>} edits
|
|
9561
|
+
*/
|
|
9562
|
+
function writeJsoncConfig(configPath, originalText, config, edits) {
|
|
9563
|
+
if (typeof originalText === 'string' && (configPath.endsWith('.jsonc') || hasJsoncFeatures(originalText))) {
|
|
9564
|
+
let text = originalText;
|
|
9565
|
+
for (const edit of edits) {
|
|
9566
|
+
text = edit.remove ? removeJsoncValue(text, edit.path) : setJsoncValue(text, edit.path, edit.value);
|
|
9567
|
+
if (text === null) break;
|
|
9568
|
+
}
|
|
9569
|
+
if (text !== null) {
|
|
9570
|
+
fs.writeFileSync(configPath, text);
|
|
9571
|
+
return;
|
|
9352
9572
|
}
|
|
9353
9573
|
}
|
|
9354
|
-
|
|
9355
|
-
// Remove trailing commas before } or ]
|
|
9356
|
-
result = result.replace(/,(\s*[}\]])/g, '$1');
|
|
9357
|
-
|
|
9358
|
-
return JSON.parse(result);
|
|
9574
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
9359
9575
|
}
|
|
9360
9576
|
|
|
9361
9577
|
/**
|
|
@@ -9377,9 +9593,11 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9377
9593
|
|
|
9378
9594
|
// Read existing config or create empty object
|
|
9379
9595
|
let config = {};
|
|
9596
|
+
let originalText = null;
|
|
9380
9597
|
if (fs.existsSync(configPath)) {
|
|
9381
9598
|
try {
|
|
9382
9599
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
9600
|
+
originalText = content;
|
|
9383
9601
|
config = parseJsonc(content);
|
|
9384
9602
|
} catch (e) {
|
|
9385
9603
|
// Cannot parse - DO NOT overwrite user's config
|
|
@@ -9410,6 +9628,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9410
9628
|
: `${opencodeConfigDir.replace(/\\/g, '/')}/gsd-core/*`;
|
|
9411
9629
|
|
|
9412
9630
|
let modified = false;
|
|
9631
|
+
const edits = [];
|
|
9413
9632
|
|
|
9414
9633
|
// Configure read permission
|
|
9415
9634
|
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
|
@@ -9417,6 +9636,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9417
9636
|
}
|
|
9418
9637
|
if (config.permission.read[gsdPath] !== 'allow') {
|
|
9419
9638
|
config.permission.read[gsdPath] = 'allow';
|
|
9639
|
+
edits.push({ path: ['permission', 'read', gsdPath], value: 'allow' });
|
|
9420
9640
|
modified = true;
|
|
9421
9641
|
}
|
|
9422
9642
|
|
|
@@ -9426,6 +9646,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9426
9646
|
}
|
|
9427
9647
|
if (config.permission.external_directory[gsdPath] !== 'allow') {
|
|
9428
9648
|
config.permission.external_directory[gsdPath] = 'allow';
|
|
9649
|
+
edits.push({ path: ['permission', 'external_directory', gsdPath], value: 'allow' });
|
|
9429
9650
|
modified = true;
|
|
9430
9651
|
}
|
|
9431
9652
|
|
|
@@ -9442,9 +9663,10 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9442
9663
|
if (config.mcp.gsd === undefined) {
|
|
9443
9664
|
config.mcp.gsd = {
|
|
9444
9665
|
type: 'local',
|
|
9445
|
-
command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
|
|
9666
|
+
command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server', GSD_MCP_NO_PROMPTS_FLAG],
|
|
9446
9667
|
enabled: true,
|
|
9447
9668
|
};
|
|
9669
|
+
edits.push({ path: ['mcp', 'gsd'], value: config.mcp.gsd });
|
|
9448
9670
|
modified = true;
|
|
9449
9671
|
}
|
|
9450
9672
|
|
|
@@ -9453,7 +9675,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9453
9675
|
}
|
|
9454
9676
|
|
|
9455
9677
|
// Write config back
|
|
9456
|
-
|
|
9678
|
+
writeJsoncConfig(configPath, originalText, config, edits);
|
|
9457
9679
|
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
|
9458
9680
|
}
|
|
9459
9681
|
|
|
@@ -9476,9 +9698,11 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9476
9698
|
|
|
9477
9699
|
// Read existing config or create empty object
|
|
9478
9700
|
let config = {};
|
|
9701
|
+
let originalText = null;
|
|
9479
9702
|
if (fs.existsSync(configPath)) {
|
|
9480
9703
|
try {
|
|
9481
9704
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
9705
|
+
originalText = content;
|
|
9482
9706
|
config = parseJsonc(content);
|
|
9483
9707
|
} catch (e) {
|
|
9484
9708
|
// Cannot parse - DO NOT overwrite user's config
|
|
@@ -9503,6 +9727,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9503
9727
|
: `${kiloConfigDir.replace(/\\/g, '/')}/gsd-core/*`;
|
|
9504
9728
|
|
|
9505
9729
|
let modified = false;
|
|
9730
|
+
const edits = [];
|
|
9506
9731
|
|
|
9507
9732
|
// Configure read permission
|
|
9508
9733
|
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
|
@@ -9510,6 +9735,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9510
9735
|
}
|
|
9511
9736
|
if (config.permission.read[gsdPath] !== 'allow') {
|
|
9512
9737
|
config.permission.read[gsdPath] = 'allow';
|
|
9738
|
+
edits.push({ path: ['permission', 'read', gsdPath], value: 'allow' });
|
|
9513
9739
|
modified = true;
|
|
9514
9740
|
}
|
|
9515
9741
|
|
|
@@ -9519,6 +9745,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9519
9745
|
}
|
|
9520
9746
|
if (config.permission.external_directory[gsdPath] !== 'allow') {
|
|
9521
9747
|
config.permission.external_directory[gsdPath] = 'allow';
|
|
9748
|
+
edits.push({ path: ['permission', 'external_directory', gsdPath], value: 'allow' });
|
|
9522
9749
|
modified = true;
|
|
9523
9750
|
}
|
|
9524
9751
|
|
|
@@ -9527,7 +9754,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9527
9754
|
}
|
|
9528
9755
|
|
|
9529
9756
|
// Write config back
|
|
9530
|
-
|
|
9757
|
+
writeJsoncConfig(configPath, originalText, config, edits);
|
|
9531
9758
|
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
|
9532
9759
|
}
|
|
9533
9760
|
|
|
@@ -10012,8 +10239,9 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10012
10239
|
// to git-cmd.js (validate-commit classifier) and gsd-graphify-rebuild.sh.
|
|
10013
10240
|
const hooksLibDir = path.join(hooksDir, 'lib');
|
|
10014
10241
|
if (fs.existsSync(hooksLibDir)) {
|
|
10242
|
+
const hookLibNames = new Set(gsdHookLibFiles());
|
|
10015
10243
|
for (const file of fs.readdirSync(hooksLibDir)) {
|
|
10016
|
-
if (
|
|
10244
|
+
if (hookLibNames.has(file)) {
|
|
10017
10245
|
manifest.files[sharedHooksDirName + '/lib/' + file] = fileHash(path.join(hooksLibDir, file));
|
|
10018
10246
|
}
|
|
10019
10247
|
}
|
|
@@ -10063,6 +10291,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10063
10291
|
manifest.files[`${_npM.dir}/${_npM.file}`] = fileHash(pluginInstallPath);
|
|
10064
10292
|
}
|
|
10065
10293
|
}
|
|
10294
|
+
// The OpenCode 2.x terminal-client plugin (statusline / update banner) is tracked beside it.
|
|
10295
|
+
const _cliM = hostBehaviorsFor(runtime).nativeCliPlugin;
|
|
10296
|
+
if (_cliM) {
|
|
10297
|
+
const cliInstallPath = path.join(configDir, _cliM.dir, _cliM.file);
|
|
10298
|
+
if (fs.existsSync(cliInstallPath)) {
|
|
10299
|
+
manifest.files[`${_cliM.dir}/${_cliM.file}`] = fileHash(cliInstallPath);
|
|
10300
|
+
}
|
|
10301
|
+
}
|
|
10066
10302
|
|
|
10067
10303
|
fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
|
|
10068
10304
|
return manifest;
|
|
@@ -12330,6 +12566,41 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
12330
12566
|
}
|
|
12331
12567
|
}
|
|
12332
12568
|
}
|
|
12569
|
+
// #5123: the Copilot and Antigravity converters write their DEFAULT home
|
|
12570
|
+
// into `@` includes and Kimi's prompt converter leaves a prose "GSD core/"
|
|
12571
|
+
// path, none of which exist when the payload lives somewhere else. Point
|
|
12572
|
+
// them at the real install root (the same `$HOME/…` / absolute prefix the
|
|
12573
|
+
// other runtimes use). Global installs only: a local install resolves
|
|
12574
|
+
// against its project, which the converters already handle.
|
|
12575
|
+
const includeRetarget = hostBehaviorsFor(runtime);
|
|
12576
|
+
if (isGlobal && (includeRetarget.defaultHomeIncludeRoot || includeRetarget.retargetsGsdCoreProsePath === true)) {
|
|
12577
|
+
const includePrefix = runtimeArtifactConversion._computePathPrefix({
|
|
12578
|
+
isGlobal: true,
|
|
12579
|
+
isOpencode: false,
|
|
12580
|
+
resolvedTarget: targetDir,
|
|
12581
|
+
homeDir: os.homedir(),
|
|
12582
|
+
});
|
|
12583
|
+
for (const relPath of manifestFiles) {
|
|
12584
|
+
const fileName = path.basename(relPath);
|
|
12585
|
+
if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
|
|
12586
|
+
if (fileName === 'CHANGELOG.md') continue;
|
|
12587
|
+
const retargetPath = path.join(targetDir, relPath);
|
|
12588
|
+
let retargetContent;
|
|
12589
|
+
try {
|
|
12590
|
+
retargetContent = fs.readFileSync(retargetPath, 'utf8');
|
|
12591
|
+
} catch (_retargetReadErr) {
|
|
12592
|
+
continue; // inaccessible or missing — never fail the install over this pass
|
|
12593
|
+
}
|
|
12594
|
+
const retargeted = runtimeArtifactConversion.retargetInstalledIncludes(retargetContent, includeRetarget, includePrefix);
|
|
12595
|
+
if (retargeted !== retargetContent) {
|
|
12596
|
+
try {
|
|
12597
|
+
fs.writeFileSync(retargetPath, retargeted);
|
|
12598
|
+
} catch (_retargetWriteErr) {
|
|
12599
|
+
continue;
|
|
12600
|
+
}
|
|
12601
|
+
}
|
|
12602
|
+
}
|
|
12603
|
+
}
|
|
12333
12604
|
for (const relPath of manifestFiles) {
|
|
12334
12605
|
const fileName = path.basename(relPath);
|
|
12335
12606
|
if (!(fileName.endsWith('.md') || fileName.endsWith('.toml'))) continue;
|
|
@@ -13024,7 +13295,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13024
13295
|
}
|
|
13025
13296
|
const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
13026
13297
|
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
13027
|
-
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
|
|
13298
|
+
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts, contextWarnings: runtimeSupportsContextWarnings(runtime) });
|
|
13028
13299
|
configuredEntrypoints.push(...kimiHooksResult.configuredEntrypoints);
|
|
13029
13300
|
if (kimiHooksResult.changed) {
|
|
13030
13301
|
console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
|
|
@@ -13306,6 +13577,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13306
13577
|
const updateCheckCommand = isGlobal
|
|
13307
13578
|
? buildHookCommand(targetDir, 'gsd-check-update.js', hookOpts)
|
|
13308
13579
|
: localCmd('gsd-check-update.js');
|
|
13580
|
+
// #4840: no metrics writer on this host -> no monitor to register (see runtimeSupportsContextWarnings).
|
|
13581
|
+
const contextWarnings = runtimeSupportsContextWarnings(runtime);
|
|
13582
|
+
// The command stays built: the extended lifecycle events (SubagentStop, PreCompact, ...) still route to it.
|
|
13309
13583
|
const contextMonitorCommand = isGlobal
|
|
13310
13584
|
? buildHookCommand(targetDir, 'gsd-context-monitor.js', hookOpts)
|
|
13311
13585
|
: localCmd('gsd-context-monitor.js');
|
|
@@ -13350,6 +13624,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13350
13624
|
hooksSurface: plan.hooksSurface,
|
|
13351
13625
|
updateCheckCommand,
|
|
13352
13626
|
contextMonitorCommand,
|
|
13627
|
+
contextWarnings,
|
|
13353
13628
|
promptGuardCommand,
|
|
13354
13629
|
readGuardCommand,
|
|
13355
13630
|
readInjectionScannerCommand,
|
|
@@ -13358,6 +13633,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13358
13633
|
localCmd,
|
|
13359
13634
|
localShellCmd,
|
|
13360
13635
|
});
|
|
13636
|
+
if (!contextWarnings) {
|
|
13637
|
+
console.log(` ${dim}↳${reset} ${getRuntimeLabel(runtime)}: agent-facing context warnings are unsupported (no statusline writes the context metrics the monitor reads), so the context monitor hook is not registered`);
|
|
13638
|
+
}
|
|
13361
13639
|
|
|
13362
13640
|
// Compute the update-banner hook command alongside the others so
|
|
13363
13641
|
// installAllRuntimes can register it at finalize time when the user opts
|
|
@@ -13626,6 +13904,31 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
13626
13904
|
// Configure OpenCode permissions
|
|
13627
13905
|
if (plan.finishPermissionWriter === 'opencode' && !process.env.GSD_TEST_MODE) {
|
|
13628
13906
|
configureOpencodePermissions(isGlobal, configDir);
|
|
13907
|
+
// ADR-5331: say which OpenCode generation this install will run under. Informational only —
|
|
13908
|
+
// a failed probe never affects the install.
|
|
13909
|
+
try {
|
|
13910
|
+
const ocv = require('../gsd-core/bin/lib/opencode-version.cjs');
|
|
13911
|
+
const gen = ocv.detectOpencodeGeneration();
|
|
13912
|
+
if (gen.generation !== null && typeof configDir === 'string' && configDir) {
|
|
13913
|
+
recordHostGeneration(configDir, { generation: gen.generation, version: gen.version });
|
|
13914
|
+
}
|
|
13915
|
+
if (gen.generation === 2) {
|
|
13916
|
+
console.log(` ${green}✓${reset} OpenCode ${gen.version || '2.x'} detected — GSD loads its OpenCode 2.x plugin entrypoint`);
|
|
13917
|
+
} else if (gen.generation === 1) {
|
|
13918
|
+
console.log(` ${green}✓${reset} OpenCode ${gen.version || '1.x'} detected — GSD loads its OpenCode 1.x plugin entrypoint`);
|
|
13919
|
+
}
|
|
13920
|
+
// ADR-5331 D6 / E8: OpenCode 2.x migrates its data directory one way. Say so before it happens, and say
|
|
13921
|
+
// when 1.x is installed over a data directory 2.x already migrated (1.x cannot open it).
|
|
13922
|
+
const dataDir = ocv.resolveOpencodeDataDir();
|
|
13923
|
+
const notice = ocv.describeDataDirNotice(gen.generation, ocv.classifyOpencodeDatabase(path.join(dataDir, 'opencode.db')), dataDir);
|
|
13924
|
+
if (notice) {
|
|
13925
|
+
console.log(notice.level === 'warn' ? ` ${yellow}⚠${reset} ${notice.text}` : ` ${dim}${notice.text}${reset}`);
|
|
13926
|
+
}
|
|
13927
|
+
// Phase 3 (#5339): OpenCode 1.x is bug-fix-only until the freeze date. The `opencode` runtime only —
|
|
13928
|
+
// this block does not run for Kilo.
|
|
13929
|
+
const deprecation = ocv.describeDeprecationNotice(gen.generation);
|
|
13930
|
+
if (deprecation) console.log(` ${yellow}⚠${reset} ${deprecation}`);
|
|
13931
|
+
} catch { /* detection is advisory */ }
|
|
13629
13932
|
}
|
|
13630
13933
|
|
|
13631
13934
|
// Configure Kilo permissions
|