@opengsd/gsd-core 1.16.0 → 1.17.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-debug-session-manager.compact.md +6 -0
- package/agents/gsd-debug-session-manager.md +6 -0
- package/bin/gsd-mcp-server.js +3 -1
- package/bin/install.js +361 -94
- package/gsd-core/bin/gsd-tools.cjs +17 -2
- package/gsd-core/bin/lib/adr-parser.cjs +37 -5
- package/gsd-core/bin/lib/capability-registry.cjs +132 -80
- package/gsd-core/bin/lib/capability-validator.cjs +2 -1
- package/gsd-core/bin/lib/capability-writer.cjs +2 -1
- package/gsd-core/bin/lib/commands.cjs +28 -8
- package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
- 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 +32 -12
- package/gsd-core/bin/lib/install-engine.cjs +11 -0
- 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 +21 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +95 -4
- 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/smart-entry.cjs +92 -53
- package/gsd-core/bin/lib/state-transition.cjs +67 -9
- package/gsd-core/bin/lib/state.cjs +454 -66
- package/gsd-core/bin/lib/update-context.cjs +18 -1
- package/gsd-core/bin/lib/verification.cjs +107 -17
- package/gsd-core/workflows/code-review.md +16 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
- package/gsd-core/workflows/execute-phase.md +3 -1
- package/gsd-core/workflows/resume-project.md +14 -10
- package/hooks/dist/gsd-check-update.js +26 -20
- package/hooks/dist/gsd-context-monitor.js +14 -5
- package/hooks/dist/gsd-secret-read-guard.js +117 -1
- package/hooks/dist/gsd-statusline.js +321 -22
- package/hooks/gsd-check-update.js +26 -20
- package/hooks/gsd-context-monitor.js +14 -5
- package/hooks/gsd-secret-read-guard.js +117 -1
- package/hooks/gsd-statusline.js +321 -22
- package/package.json +1 -1
- 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 +4 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +10 -0
- package/scripts/mutation-matrix.cjs +3 -1
- package/scripts/release-tarball-smoke.cjs +14 -7
- package/vscode/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -190,7 +190,7 @@ function isCodexHooksFeatureKey(key) {
|
|
|
190
190
|
// GSD_CLAUDE_LEGACY_DENY_PERMISSIONS below.)
|
|
191
191
|
//
|
|
192
192
|
// Format: each string uses Claude Code's documented permission rule syntax \u2014
|
|
193
|
-
// "Tool(pattern)" e.g. "
|
|
193
|
+
// "Tool(pattern)" e.g. "Read(.planning/*)", "Edit(STATE.md)"
|
|
194
194
|
// "Tool" (bare tool name, no pattern)
|
|
195
195
|
//
|
|
196
196
|
// Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
|
|
@@ -199,7 +199,6 @@ function isCodexHooksFeatureKey(key) {
|
|
|
199
199
|
// instead of scattered literal 'claude' defaults/rosters (#2086).
|
|
200
200
|
const DEFAULT_RUNTIME = 'claude';
|
|
201
201
|
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
|
|
202
|
-
'Bash(npx gsd-core *)',
|
|
203
202
|
'Read(.planning/*)',
|
|
204
203
|
'Edit(.planning/*)',
|
|
205
204
|
'Read(STATE.md)',
|
|
@@ -232,9 +231,14 @@ const GSD_CLAUDE_LEGACY_DENY_PERMISSIONS = Object.freeze([
|
|
|
232
231
|
// mergeClaudePermissions and uninstall cleanup retire those stale entries on
|
|
233
232
|
// existing installs while the current GSD_CLAUDE_ALLOW_PERMISSIONS above
|
|
234
233
|
// carries the working `Edit(...)` forms.
|
|
234
|
+
// #5054 — `Bash(npx gsd-core *)` is retired the same way. The project ships
|
|
235
|
+
// as `@opengsd/gsd-core`; the unscoped `gsd-core` npm name belongs to someone
|
|
236
|
+
// else, so the rule pre-authorized whatever that name's owner publishes next.
|
|
237
|
+
// GSD never runs the bare form (update.md uses `--package=@opengsd/gsd-core`).
|
|
235
238
|
const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
|
|
236
239
|
'Write(.planning/*)',
|
|
237
240
|
'Write(STATE.md)',
|
|
241
|
+
'Bash(npx gsd-core *)',
|
|
238
242
|
]);
|
|
239
243
|
|
|
240
244
|
/**
|
|
@@ -249,6 +253,7 @@ const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
|
|
|
249
253
|
* `Write(...)` forms from before the fix) is removed from permissions.allow,
|
|
250
254
|
* so existing installs end up with the working `Edit(...)` forms instead of
|
|
251
255
|
* both the dead legacy entry and its replacement sitting side by side.
|
|
256
|
+
* The same filter retires the unscoped `Bash(npx gsd-core *)` rule (#5054).
|
|
252
257
|
*
|
|
253
258
|
* Migration (#4221): the retired GSD_CLAUDE_LEGACY_DENY_PERMISSIONS entries
|
|
254
259
|
* are removed from permissions.deny (byte-equal only). Nothing is added to
|
|
@@ -408,6 +413,26 @@ const GSD_WINDSURF_HOOK_SCRIPTS = hooksSurface.GSD_WINDSURF_HOOK_SCRIPTS;
|
|
|
408
413
|
// two guards require so their copies cannot drift.
|
|
409
414
|
const GSD_HOOK_LIB_FILES = ['git-cmd.js', 'gsd-graphify-rebuild.sh', 'cursor-workspace.js', 'injection-patterns.js'];
|
|
410
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Every file GSD stages under `hooks/lib/` (#5335). The install copies `hooks/dist/lib/` wholesale (and the
|
|
418
|
+
* allow-listed names from `hooks/lib/`), so a fixed list drifts the moment a helper is added: the new file is
|
|
419
|
+
* staged but never removed, which also keeps `hooks/lib/` non-empty and blocks its rmdir. The set is the
|
|
420
|
+
* allow-list plus the top-level files of the directories the installer copies from, so uninstall and the
|
|
421
|
+
* manifest cannot disagree with install. Only these names are ever removed; a user's own helper beside them
|
|
422
|
+
* is left alone.
|
|
423
|
+
*/
|
|
424
|
+
function gsdHookLibFiles() {
|
|
425
|
+
const names = new Set(GSD_HOOK_LIB_FILES);
|
|
426
|
+
for (const parts of [['hooks', 'dist', 'lib'], ['hooks', 'lib']]) {
|
|
427
|
+
try {
|
|
428
|
+
for (const entry of fs.readdirSync(path.join(__dirname, '..', ...parts), { withFileTypes: true })) {
|
|
429
|
+
if (entry.isFile()) names.add(entry.name);
|
|
430
|
+
}
|
|
431
|
+
} catch { /* the directory is absent from this package layout */ }
|
|
432
|
+
}
|
|
433
|
+
return [...names];
|
|
434
|
+
}
|
|
435
|
+
|
|
411
436
|
/**
|
|
412
437
|
* Directory name GSD stages its shared hook bundle under, inside a runtime's
|
|
413
438
|
* install root. Defaults to 'hooks' — the name every runtime used before #3023.
|
|
@@ -762,8 +787,11 @@ const {
|
|
|
762
787
|
discoverInstallerMigrations,
|
|
763
788
|
MANIFEST_SCHEMA_VERSION,
|
|
764
789
|
readInstallManifest,
|
|
790
|
+
recordHostGeneration,
|
|
765
791
|
runInstallerMigrations,
|
|
766
792
|
} = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
|
|
793
|
+
const { setJsoncValue, removeJsoncValue, hasJsoncFeatures, parseJsonc } = require(path.join(_gsdLibDir, 'jsonc-edit.cjs'));
|
|
794
|
+
const { isGsdMcpEntry, GSD_MCP_NO_PROMPTS_FLAG } = require(path.join(_gsdLibDir, 'opencode-mcp-entry.cjs'));
|
|
767
795
|
const {
|
|
768
796
|
assertInstallerMigrationsUnblocked,
|
|
769
797
|
resolveInstallerMigrationPromptsForNonTty,
|
|
@@ -1256,6 +1284,17 @@ const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
|
|
|
1256
1284
|
// writeKimiHooksToml / removeKimiHooksToml: kimi's native config.toml [[hooks]]
|
|
1257
1285
|
// surface (#2095 EoS/kimi Upgrade 1) — separate from settings.json entirely.
|
|
1258
1286
|
const writeKimiHooksToml = hooksSurface.writeKimiHooksToml;
|
|
1287
|
+
|
|
1288
|
+
// #4840: false when the runtime declares `context-warnings` unsupported in its
|
|
1289
|
+
// descriptor (`hostBehaviors.unsupportedFeatures`). gsd-context-monitor.js reads
|
|
1290
|
+
// the metrics bridge file only the statusline hook writes, and the statusline
|
|
1291
|
+
// is wired for Claude Code alone, so on any other host the hook fires on every
|
|
1292
|
+
// tool event, finds no file and exits silently — forever. Such a host gets no
|
|
1293
|
+
// monitor registration (stale ones are stripped), as Codex does since #2586.
|
|
1294
|
+
function runtimeSupportsContextWarnings(runtime) {
|
|
1295
|
+
const unsupported = hostBehaviorsFor(runtime).unsupportedFeatures;
|
|
1296
|
+
return !(Array.isArray(unsupported) && unsupported.includes('context-warnings'));
|
|
1297
|
+
}
|
|
1259
1298
|
const removeKimiHooksToml = hooksSurface.removeKimiHooksToml;
|
|
1260
1299
|
// processAttribution: pure Co-Authored-By content transform, relocated to the
|
|
1261
1300
|
// conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
|
|
@@ -6959,6 +6998,82 @@ function writeCopilotHookConfig(targetDir) {
|
|
|
6959
6998
|
* Reads agent .md files from source, extracts metadata, writes .toml configs.
|
|
6960
6999
|
*/
|
|
6961
7000
|
|
|
7001
|
+
// #5335: ~/.gsd/defaults.json is machine-wide, so uninstalling one runtime must not strip a default another
|
|
7002
|
+
// still needs, nor one the user chose. A sidecar records which keys an install SET (not ones it found), the
|
|
7003
|
+
// value it set, and which non-Claude runtimes have installed since; uninstall reverts a key only when its
|
|
7004
|
+
// last such runtime is gone and the value is still the one the installer wrote.
|
|
7005
|
+
const DEFAULTS_PROVENANCE_NAME = 'defaults-provenance.json';
|
|
7006
|
+
|
|
7007
|
+
function readDefaultsProvenance(gsdDir) {
|
|
7008
|
+
try {
|
|
7009
|
+
const p = JSON.parse(fs.readFileSync(path.join(gsdDir, DEFAULTS_PROVENANCE_NAME), 'utf8'));
|
|
7010
|
+
if (p && typeof p === 'object' && p.keys && typeof p.keys === 'object') return p;
|
|
7011
|
+
} catch { /* absent or unreadable */ }
|
|
7012
|
+
return { version: 1, keys: {} };
|
|
7013
|
+
}
|
|
7014
|
+
|
|
7015
|
+
function recordDefaultsProvenance(gsdDir, runtime, defaults, setNow) {
|
|
7016
|
+
const prov = readDefaultsProvenance(gsdDir);
|
|
7017
|
+
let changed = false;
|
|
7018
|
+
for (const key of ['resolve_model_ids', 'runtime']) {
|
|
7019
|
+
let entry = prov.keys[key];
|
|
7020
|
+
if (setNow[key]) {
|
|
7021
|
+
entry = { setValue: defaults[key], runtimes: [] };
|
|
7022
|
+
prov.keys[key] = entry;
|
|
7023
|
+
changed = true;
|
|
7024
|
+
}
|
|
7025
|
+
if (entry && Array.isArray(entry.runtimes) && !entry.runtimes.includes(runtime)) {
|
|
7026
|
+
entry.runtimes.push(runtime);
|
|
7027
|
+
changed = true;
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
if (changed) atomicWriteFileSync(path.join(gsdDir, DEFAULTS_PROVENANCE_NAME), JSON.stringify(prov, null, 2) + '\n', 'utf8');
|
|
7031
|
+
}
|
|
7032
|
+
|
|
7033
|
+
/** @returns {boolean} true when it removed or reverted something */
|
|
7034
|
+
function revertNonClaudeDefaults(runtime) {
|
|
7035
|
+
if (hostBehaviorsFor(runtime).nativeModelAliases || process.env.GSD_TEST_MODE) return false;
|
|
7036
|
+
const gsdDir = path.join(os.homedir(), '.gsd');
|
|
7037
|
+
const provPath = path.join(gsdDir, DEFAULTS_PROVENANCE_NAME);
|
|
7038
|
+
if (!fs.existsSync(provPath)) return false;
|
|
7039
|
+
let releaseLock = null;
|
|
7040
|
+
try {
|
|
7041
|
+
releaseLock = acquireInstallMigrationLock(gsdDir);
|
|
7042
|
+
const prov = readDefaultsProvenance(gsdDir);
|
|
7043
|
+
const defaultsPath = path.join(gsdDir, 'defaults.json');
|
|
7044
|
+
let defaults = null;
|
|
7045
|
+
try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* absent or corrupt */ }
|
|
7046
|
+
const reverted = [];
|
|
7047
|
+
for (const [key, entry] of Object.entries(prov.keys)) {
|
|
7048
|
+
if (!entry || !Array.isArray(entry.runtimes)) { delete prov.keys[key]; continue; }
|
|
7049
|
+
entry.runtimes = entry.runtimes.filter((r) => r !== runtime);
|
|
7050
|
+
if (entry.runtimes.length > 0) continue;
|
|
7051
|
+
if (defaults && typeof defaults === 'object' && defaults[key] === entry.setValue) {
|
|
7052
|
+
delete defaults[key];
|
|
7053
|
+
reverted.push(key);
|
|
7054
|
+
}
|
|
7055
|
+
delete prov.keys[key];
|
|
7056
|
+
}
|
|
7057
|
+
if (reverted.length > 0 && defaults) {
|
|
7058
|
+
if (Object.keys(defaults).length === 0) fs.rmSync(defaultsPath, { force: true });
|
|
7059
|
+
else atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
|
|
7060
|
+
console.log(` ${green}✓${reset} Reverted ${reverted.join(', ')} in ~/.gsd/defaults.json (set by the ${runtime} install)`);
|
|
7061
|
+
}
|
|
7062
|
+
if (Object.keys(prov.keys).length === 0) fs.rmSync(provPath, { force: true });
|
|
7063
|
+
else atomicWriteFileSync(provPath, JSON.stringify(prov, null, 2) + '\n', 'utf8');
|
|
7064
|
+
return reverted.length > 0;
|
|
7065
|
+
} catch (e) {
|
|
7066
|
+
console.log(` ${yellow}⚠${reset} Could not update ~/.gsd/defaults.json: ${e.message}`);
|
|
7067
|
+
return false;
|
|
7068
|
+
} finally {
|
|
7069
|
+
if (releaseLock) {
|
|
7070
|
+
try { releaseLock(); } catch { /* the stale-lock reaper clears it */ }
|
|
7071
|
+
}
|
|
7072
|
+
// ~/.gsd is GSD's own directory; drop it once the lock is released and nothing else lives in it.
|
|
7073
|
+
try { fs.rmdirSync(gsdDir); } catch { /* other GSD state lives here */ }
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
7076
|
+
|
|
6962
7077
|
/**
|
|
6963
7078
|
* #2834: Write ~/.gsd/defaults.json for non-Claude runtimes — sets
|
|
6964
7079
|
* resolve_model_ids="omit" (so resolveModelInternal() returns '' instead of
|
|
@@ -7005,6 +7120,10 @@ function writeNonClaudeDefaults(runtime) {
|
|
|
7005
7120
|
atomicWriteFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
|
|
7006
7121
|
for (const message of applied) console.log(` ${green}✓${reset} ${message}`);
|
|
7007
7122
|
}
|
|
7123
|
+
recordDefaultsProvenance(gsdDir, runtime, defaults, {
|
|
7124
|
+
resolve_model_ids: shouldDefaultToOmit,
|
|
7125
|
+
runtime: applied.some((m) => m.startsWith('Set runtime:')),
|
|
7126
|
+
});
|
|
7008
7127
|
} catch (e) {
|
|
7009
7128
|
console.log(` ${yellow}⚠${reset} Could not write ~/.gsd/defaults.json: ${e.message}`);
|
|
7010
7129
|
} finally {
|
|
@@ -7212,7 +7331,11 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7212
7331
|
const newLines = [];
|
|
7213
7332
|
let inAllowedTools = false;
|
|
7214
7333
|
let inSkippedArray = false;
|
|
7334
|
+
let inAgentTools = false;
|
|
7335
|
+
let inDisallowedTools = false;
|
|
7215
7336
|
const allowedTools = [];
|
|
7337
|
+
const agentTools = [];
|
|
7338
|
+
const agentDisallowed = [];
|
|
7216
7339
|
|
|
7217
7340
|
for (const line of lines) {
|
|
7218
7341
|
const trimmed = line.trim();
|
|
@@ -7228,11 +7351,31 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7228
7351
|
continue;
|
|
7229
7352
|
}
|
|
7230
7353
|
|
|
7354
|
+
// Agents: collect the `tools:` allowlist and `disallowedTools:` denylist (block-list continuation
|
|
7355
|
+
// lines) so they can be rendered as `permission:` entries below (mirrors
|
|
7356
|
+
// src/runtime-artifact-conversion.cts's convertClaudeToOpencodeFrontmatter, #5338).
|
|
7357
|
+
if (isAgent && (inAgentTools || inDisallowedTools)) {
|
|
7358
|
+
if (trimmed.startsWith('- ')) {
|
|
7359
|
+
const tool = runtimeArtifactConversion._decodeToolScalar(trimmed.substring(2));
|
|
7360
|
+
if (tool !== null) (inAgentTools ? agentTools : agentDisallowed).push(tool);
|
|
7361
|
+
continue;
|
|
7362
|
+
}
|
|
7363
|
+
if (trimmed && !trimmed.startsWith('-')) {
|
|
7364
|
+
inAgentTools = false;
|
|
7365
|
+
inDisallowedTools = false;
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
|
|
7231
7369
|
// Detect inline tools: field (comma-separated string)
|
|
7232
7370
|
if (trimmed.startsWith('tools:')) {
|
|
7233
7371
|
if (isAgent) {
|
|
7234
|
-
|
|
7235
|
-
|
|
7372
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
7373
|
+
// A comment-only value (`tools: # note`) is not inline content: read the block list instead.
|
|
7374
|
+
if (toolsValue && !toolsValue.startsWith('#')) {
|
|
7375
|
+
agentTools.push(...runtimeArtifactConversion._splitToolScalars(toolsValue).map(runtimeArtifactConversion._decodeToolScalar).filter((tool) => tool !== null));
|
|
7376
|
+
} else {
|
|
7377
|
+
inAgentTools = true;
|
|
7378
|
+
}
|
|
7236
7379
|
continue;
|
|
7237
7380
|
}
|
|
7238
7381
|
const toolsValue = trimmed.substring(6).trim();
|
|
@@ -7244,8 +7387,18 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7244
7387
|
continue;
|
|
7245
7388
|
}
|
|
7246
7389
|
|
|
7247
|
-
|
|
7248
|
-
|
|
7390
|
+
if (isAgent && trimmed.startsWith('disallowedTools:')) {
|
|
7391
|
+
const value = trimmed.substring(16).trim();
|
|
7392
|
+
if (value && !value.startsWith('#')) {
|
|
7393
|
+
agentDisallowed.push(...runtimeArtifactConversion._splitToolScalars(value).map(runtimeArtifactConversion._decodeToolScalar).filter((tool) => tool !== null));
|
|
7394
|
+
} else {
|
|
7395
|
+
inDisallowedTools = true;
|
|
7396
|
+
}
|
|
7397
|
+
continue;
|
|
7398
|
+
}
|
|
7399
|
+
|
|
7400
|
+
// For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:
|
|
7401
|
+
if (isAgent && /^(skills|color|memory|maxTurns|permissionMode):/.test(trimmed)) {
|
|
7249
7402
|
inSkippedArray = true;
|
|
7250
7403
|
continue;
|
|
7251
7404
|
}
|
|
@@ -7318,6 +7471,7 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
|
|
|
7318
7471
|
if (modelOverride) {
|
|
7319
7472
|
newLines.push(`model: ${modelOverride}`);
|
|
7320
7473
|
}
|
|
7474
|
+
newLines.push(...runtimeArtifactConversion._buildOpencodeAgentPermissionBlock(agentTools, agentDisallowed));
|
|
7321
7475
|
}
|
|
7322
7476
|
|
|
7323
7477
|
// For commands: add tools object if we had allowed-tools or tools
|
|
@@ -8239,7 +8393,7 @@ function reclaimKimiHooksRoot(kimiHooksRoot) {
|
|
|
8239
8393
|
const kimiHooksLibDir = path.join(kimiHooksDir, 'lib');
|
|
8240
8394
|
if (fs.existsSync(kimiHooksLibDir)) {
|
|
8241
8395
|
let removedKimiLibFiles = 0;
|
|
8242
|
-
for (const file of
|
|
8396
|
+
for (const file of gsdHookLibFiles()) {
|
|
8243
8397
|
try {
|
|
8244
8398
|
fs.unlinkSync(path.join(kimiHooksLibDir, file));
|
|
8245
8399
|
removedKimiLibFiles++;
|
|
@@ -8875,7 +9029,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8875
9029
|
const hooksLibDir = path.join(hooksDir, 'lib');
|
|
8876
9030
|
if (fs.existsSync(hooksLibDir)) {
|
|
8877
9031
|
let removedLibFiles = 0;
|
|
8878
|
-
for (const file of
|
|
9032
|
+
for (const file of gsdHookLibFiles()) {
|
|
8879
9033
|
const filePath = path.join(hooksLibDir, file);
|
|
8880
9034
|
try {
|
|
8881
9035
|
fs.unlinkSync(filePath);
|
|
@@ -8911,6 +9065,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8911
9065
|
console.log(` ${green}✓${reset} Removed GSD hooks/package.json (CommonJS marker)`);
|
|
8912
9066
|
}
|
|
8913
9067
|
} catch { /* best-effort */ }
|
|
9068
|
+
// Once GSD's hooks, helpers and marker are gone, an empty hooks dir is GSD's own leftover (#5335).
|
|
9069
|
+
// rmdir fails on a non-empty dir, so a user's own hook scripts keep it in place.
|
|
9070
|
+
try { fs.rmdirSync(hooksDir); } catch { /* not empty or absent */ }
|
|
8914
9071
|
}
|
|
8915
9072
|
|
|
8916
9073
|
// 4z. Remove the native plugin adapter (#1914, extended to Kilo by #2093).
|
|
@@ -8944,6 +9101,20 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
8944
9101
|
// unlink above failed, pulling the marker out from under a still-present
|
|
8945
9102
|
// CommonJS adapter would leave it unloadable. The exact content match
|
|
8946
9103
|
// still leaves any user-authored package.json in place.
|
|
9104
|
+
// The terminal-client plugin directory (OpenCode 2.x statusline): GSD's file, then its now-empty directory.
|
|
9105
|
+
const _cliU = hostBehaviorsFor(runtime).nativeCliPlugin;
|
|
9106
|
+
if (_cliU) {
|
|
9107
|
+
const cliFile = path.join(targetDir, _cliU.dir, _cliU.file);
|
|
9108
|
+
if (fs.existsSync(cliFile)) {
|
|
9109
|
+
try {
|
|
9110
|
+
fs.unlinkSync(cliFile);
|
|
9111
|
+
removedCount++;
|
|
9112
|
+
removedFromPluginsDir = true;
|
|
9113
|
+
console.log(` ${green}✓${reset} Removed terminal-client plugin (${runtime})`);
|
|
9114
|
+
} catch (_) { /* best-effort */ }
|
|
9115
|
+
}
|
|
9116
|
+
try { fs.rmdirSync(path.join(targetDir, _cliU.dir)); } catch (_) { /* not empty or absent */ }
|
|
9117
|
+
}
|
|
8947
9118
|
if (!fs.existsSync(pluginPath) && removeCommonJsMarker(pluginsDir)) {
|
|
8948
9119
|
removedCount++;
|
|
8949
9120
|
removedFromPluginsDir = true;
|
|
@@ -9171,40 +9342,56 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9171
9342
|
}
|
|
9172
9343
|
}
|
|
9173
9344
|
|
|
9174
|
-
// 6. For OpenCode, clean up permissions from opencode.json or opencode.jsonc
|
|
9345
|
+
// 6. For OpenCode, clean up permissions and the companion MCP entry from opencode.json or opencode.jsonc
|
|
9175
9346
|
if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
|
|
9176
9347
|
const configPath = resolveOpencodeConfigPath(targetDir);
|
|
9177
9348
|
if (fs.existsSync(configPath)) {
|
|
9178
9349
|
try {
|
|
9179
|
-
const
|
|
9180
|
-
|
|
9350
|
+
const originalText = fs.readFileSync(configPath, 'utf8');
|
|
9351
|
+
const config = parseJsonc(originalText);
|
|
9352
|
+
const edits = [];
|
|
9181
9353
|
|
|
9182
9354
|
// Remove GSD permission entries
|
|
9183
|
-
if (config.permission) {
|
|
9355
|
+
if (config.permission && typeof config.permission === 'object') {
|
|
9184
9356
|
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];
|
|
9357
|
+
const section = config.permission[permType];
|
|
9358
|
+
if (!section || typeof section !== 'object') continue;
|
|
9359
|
+
for (const key of Object.keys(section)) {
|
|
9360
|
+
if (key.includes('gsd-core')) {
|
|
9361
|
+
delete section[key];
|
|
9362
|
+
edits.push({ path: ['permission', permType, key], remove: true });
|
|
9196
9363
|
}
|
|
9197
9364
|
}
|
|
9365
|
+
// Clean up empty objects
|
|
9366
|
+
if (Object.keys(section).length === 0) delete config.permission[permType];
|
|
9198
9367
|
}
|
|
9199
|
-
if (Object.keys(config.permission).length === 0)
|
|
9200
|
-
|
|
9368
|
+
if (Object.keys(config.permission).length === 0) delete config.permission;
|
|
9369
|
+
}
|
|
9370
|
+
|
|
9371
|
+
// #5333: remove the companion MCP entry, but only while it is still the entry GSD wrote — a
|
|
9372
|
+
// user-edited or user-defined `gsd` server stays. Both OpenCode generations' shapes: the 1.x
|
|
9373
|
+
// `mcp.gsd` and the 2.x native `mcp.servers.gsd`.
|
|
9374
|
+
let mcpRemoved = false;
|
|
9375
|
+
if (config.mcp && typeof config.mcp === 'object') {
|
|
9376
|
+
if (isGsdMcpEntry(config.mcp.gsd)) {
|
|
9377
|
+
delete config.mcp.gsd;
|
|
9378
|
+
edits.push({ path: ['mcp', 'gsd'], remove: true });
|
|
9379
|
+
mcpRemoved = true;
|
|
9201
9380
|
}
|
|
9381
|
+
const servers = config.mcp.servers;
|
|
9382
|
+
if (servers && typeof servers === 'object' && isGsdMcpEntry(servers.gsd)) {
|
|
9383
|
+
delete servers.gsd;
|
|
9384
|
+
edits.push({ path: ['mcp', 'servers', 'gsd'], remove: true });
|
|
9385
|
+
if (Object.keys(servers).length === 0) delete config.mcp.servers;
|
|
9386
|
+
mcpRemoved = true;
|
|
9387
|
+
}
|
|
9388
|
+
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
|
9202
9389
|
}
|
|
9203
9390
|
|
|
9204
|
-
if (
|
|
9205
|
-
|
|
9391
|
+
if (edits.length > 0) {
|
|
9392
|
+
writeJsoncConfig(configPath, originalText, config, edits);
|
|
9206
9393
|
removedCount++;
|
|
9207
|
-
console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
|
|
9394
|
+
console.log(` ${green}✓${reset} Removed GSD permissions${mcpRemoved ? ' and MCP server' : ''} from ${path.basename(configPath)}`);
|
|
9208
9395
|
}
|
|
9209
9396
|
} catch (e) {
|
|
9210
9397
|
// Ignore JSON parse errors
|
|
@@ -9219,8 +9406,9 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9219
9406
|
const configPath = resolveKiloConfigPath(targetDir);
|
|
9220
9407
|
if (fs.existsSync(configPath)) {
|
|
9221
9408
|
try {
|
|
9222
|
-
const
|
|
9223
|
-
|
|
9409
|
+
const originalText = fs.readFileSync(configPath, 'utf8');
|
|
9410
|
+
const config = parseJsonc(originalText);
|
|
9411
|
+
const edits = [];
|
|
9224
9412
|
|
|
9225
9413
|
// Remove GSD permission entries
|
|
9226
9414
|
if (config.permission) {
|
|
@@ -9230,7 +9418,7 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9230
9418
|
for (const key of keys) {
|
|
9231
9419
|
if (key.includes('gsd-core')) {
|
|
9232
9420
|
delete config.permission[permType][key];
|
|
9233
|
-
|
|
9421
|
+
edits.push({ path: ['permission', permType, key], remove: true });
|
|
9234
9422
|
}
|
|
9235
9423
|
}
|
|
9236
9424
|
// Clean up empty objects
|
|
@@ -9244,8 +9432,8 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9244
9432
|
}
|
|
9245
9433
|
}
|
|
9246
9434
|
|
|
9247
|
-
if (
|
|
9248
|
-
|
|
9435
|
+
if (edits.length > 0) {
|
|
9436
|
+
writeJsoncConfig(configPath, originalText, config, edits);
|
|
9249
9437
|
removedCount++;
|
|
9250
9438
|
console.log(` ${green}✓${reset} Removed GSD permissions from ${path.basename(configPath)}`);
|
|
9251
9439
|
}
|
|
@@ -9287,6 +9475,67 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9287
9475
|
console.log(` ${green}✓${reset} Removed ${MANIFEST_NAME}`);
|
|
9288
9476
|
}
|
|
9289
9477
|
|
|
9478
|
+
// #5335: the installer's own bookkeeping goes with it — the applied-migrations state and the migration
|
|
9479
|
+
// journal. The journal's `-backups/` folders hold copies of files the USER had modified, so a folder that
|
|
9480
|
+
// still has content is kept; everything GSD generated for itself (run records, rollback snapshots, empty
|
|
9481
|
+
// folders) is removed.
|
|
9482
|
+
const installStatePath = path.join(targetDir, 'gsd-install-state.json');
|
|
9483
|
+
if (fs.existsSync(installStatePath)) {
|
|
9484
|
+
fs.rmSync(installStatePath, { force: true });
|
|
9485
|
+
removedCount++;
|
|
9486
|
+
console.log(` ${green}✓${reset} Removed gsd-install-state.json`);
|
|
9487
|
+
}
|
|
9488
|
+
const journalDir = path.join(targetDir, 'gsd-migration-journal');
|
|
9489
|
+
if (fs.existsSync(journalDir)) {
|
|
9490
|
+
let journalEntriesRemoved = 0;
|
|
9491
|
+
try {
|
|
9492
|
+
for (const entry of fs.readdirSync(journalDir, { withFileTypes: true })) {
|
|
9493
|
+
const full = path.join(journalDir, entry.name);
|
|
9494
|
+
if (entry.isSymbolicLink()) continue;
|
|
9495
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
9496
|
+
fs.rmSync(full, { force: true });
|
|
9497
|
+
journalEntriesRemoved++;
|
|
9498
|
+
} else if (entry.isDirectory() && entry.name.endsWith('-rollback')) {
|
|
9499
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
9500
|
+
journalEntriesRemoved++;
|
|
9501
|
+
} else if (entry.isDirectory() && entry.name.endsWith('-backups')) {
|
|
9502
|
+
try { fs.rmdirSync(full); journalEntriesRemoved++; } catch { /* holds the user's backed-up files: keep */ }
|
|
9503
|
+
}
|
|
9504
|
+
}
|
|
9505
|
+
fs.rmdirSync(journalDir);
|
|
9506
|
+
} catch { /* a non-empty journal dir (kept backups) stays */ }
|
|
9507
|
+
if (journalEntriesRemoved > 0) {
|
|
9508
|
+
removedCount++;
|
|
9509
|
+
console.log(` ${green}✓${reset} Removed gsd-migration-journal/ records`);
|
|
9510
|
+
}
|
|
9511
|
+
}
|
|
9512
|
+
|
|
9513
|
+
// #5335: the installer's user-artifact staging area is empty once the install it served is over. rmdir only
|
|
9514
|
+
// succeeds on an empty directory, so anything still staged there is left for its owner.
|
|
9515
|
+
try {
|
|
9516
|
+
fs.rmdirSync(path.join(targetDir, '.gsd-staging', 'user-artifacts'));
|
|
9517
|
+
fs.rmdirSync(path.join(targetDir, '.gsd-staging'));
|
|
9518
|
+
} catch { /* absent or not empty */ }
|
|
9519
|
+
|
|
9520
|
+
// #5335: OpenCode never reads settings.json, but the shared-settings install step creates it as `{}`.
|
|
9521
|
+
// An empty one carries nothing of the user's; a populated one is left alone.
|
|
9522
|
+
if (resolveInstallPlan(runtime).finishPermissionWriter === 'opencode') {
|
|
9523
|
+
const emptySettingsPath = path.join(targetDir, 'settings.json');
|
|
9524
|
+
try {
|
|
9525
|
+
const parsed = JSON.parse(fs.readFileSync(emptySettingsPath, 'utf8'));
|
|
9526
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Object.keys(parsed).length === 0) {
|
|
9527
|
+
fs.rmSync(emptySettingsPath, { force: true });
|
|
9528
|
+
removedCount++;
|
|
9529
|
+
console.log(` ${green}✓${reset} Removed empty settings.json`);
|
|
9530
|
+
}
|
|
9531
|
+
} catch { /* absent, unreadable or populated: leave it */ }
|
|
9532
|
+
}
|
|
9533
|
+
|
|
9534
|
+
// #5335: the machine-wide defaults the install wrote for this runtime, when nothing else needs them.
|
|
9535
|
+
if (revertNonClaudeDefaults(runtime)) {
|
|
9536
|
+
removedCount++;
|
|
9537
|
+
}
|
|
9538
|
+
|
|
9290
9539
|
if (removedCount === 0) {
|
|
9291
9540
|
console.log(` ${yellow}⚠${reset} No GSD files found to remove.`);
|
|
9292
9541
|
}
|
|
@@ -9298,64 +9547,30 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
|
|
9298
9547
|
}
|
|
9299
9548
|
|
|
9300
9549
|
/**
|
|
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
|
-
}
|
|
9550
|
+
* Write back an OpenCode/Kilo config the installer changed (#5334).
|
|
9551
|
+
*
|
|
9552
|
+
* A file that carries JSONC syntax (a `.jsonc` name, comments or trailing commas) is edited as text, so
|
|
9553
|
+
* only the keys GSD owns change and the user's annotations and layout survive. Anything the text editor
|
|
9554
|
+
* cannot handle, and plain JSON, keeps the previous `JSON.stringify` rewrite.
|
|
9555
|
+
*
|
|
9556
|
+
* @param {string} configPath
|
|
9557
|
+
* @param {string|null} originalText current file text, or null when the file is new
|
|
9558
|
+
* @param {object} config the mutated parsed config (used for the fallback)
|
|
9559
|
+
* @param {Array<{path: string[], value?: unknown, remove?: boolean}>} edits
|
|
9560
|
+
*/
|
|
9561
|
+
function writeJsoncConfig(configPath, originalText, config, edits) {
|
|
9562
|
+
if (typeof originalText === 'string' && (configPath.endsWith('.jsonc') || hasJsoncFeatures(originalText))) {
|
|
9563
|
+
let text = originalText;
|
|
9564
|
+
for (const edit of edits) {
|
|
9565
|
+
text = edit.remove ? removeJsoncValue(text, edit.path) : setJsoncValue(text, edit.path, edit.value);
|
|
9566
|
+
if (text === null) break;
|
|
9567
|
+
}
|
|
9568
|
+
if (text !== null) {
|
|
9569
|
+
fs.writeFileSync(configPath, text);
|
|
9570
|
+
return;
|
|
9352
9571
|
}
|
|
9353
9572
|
}
|
|
9354
|
-
|
|
9355
|
-
// Remove trailing commas before } or ]
|
|
9356
|
-
result = result.replace(/,(\s*[}\]])/g, '$1');
|
|
9357
|
-
|
|
9358
|
-
return JSON.parse(result);
|
|
9573
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
9359
9574
|
}
|
|
9360
9575
|
|
|
9361
9576
|
/**
|
|
@@ -9377,9 +9592,11 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9377
9592
|
|
|
9378
9593
|
// Read existing config or create empty object
|
|
9379
9594
|
let config = {};
|
|
9595
|
+
let originalText = null;
|
|
9380
9596
|
if (fs.existsSync(configPath)) {
|
|
9381
9597
|
try {
|
|
9382
9598
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
9599
|
+
originalText = content;
|
|
9383
9600
|
config = parseJsonc(content);
|
|
9384
9601
|
} catch (e) {
|
|
9385
9602
|
// Cannot parse - DO NOT overwrite user's config
|
|
@@ -9410,6 +9627,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9410
9627
|
: `${opencodeConfigDir.replace(/\\/g, '/')}/gsd-core/*`;
|
|
9411
9628
|
|
|
9412
9629
|
let modified = false;
|
|
9630
|
+
const edits = [];
|
|
9413
9631
|
|
|
9414
9632
|
// Configure read permission
|
|
9415
9633
|
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
|
@@ -9417,6 +9635,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9417
9635
|
}
|
|
9418
9636
|
if (config.permission.read[gsdPath] !== 'allow') {
|
|
9419
9637
|
config.permission.read[gsdPath] = 'allow';
|
|
9638
|
+
edits.push({ path: ['permission', 'read', gsdPath], value: 'allow' });
|
|
9420
9639
|
modified = true;
|
|
9421
9640
|
}
|
|
9422
9641
|
|
|
@@ -9426,6 +9645,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9426
9645
|
}
|
|
9427
9646
|
if (config.permission.external_directory[gsdPath] !== 'allow') {
|
|
9428
9647
|
config.permission.external_directory[gsdPath] = 'allow';
|
|
9648
|
+
edits.push({ path: ['permission', 'external_directory', gsdPath], value: 'allow' });
|
|
9429
9649
|
modified = true;
|
|
9430
9650
|
}
|
|
9431
9651
|
|
|
@@ -9442,9 +9662,10 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9442
9662
|
if (config.mcp.gsd === undefined) {
|
|
9443
9663
|
config.mcp.gsd = {
|
|
9444
9664
|
type: 'local',
|
|
9445
|
-
command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
|
|
9665
|
+
command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server', GSD_MCP_NO_PROMPTS_FLAG],
|
|
9446
9666
|
enabled: true,
|
|
9447
9667
|
};
|
|
9668
|
+
edits.push({ path: ['mcp', 'gsd'], value: config.mcp.gsd });
|
|
9448
9669
|
modified = true;
|
|
9449
9670
|
}
|
|
9450
9671
|
|
|
@@ -9453,7 +9674,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
9453
9674
|
}
|
|
9454
9675
|
|
|
9455
9676
|
// Write config back
|
|
9456
|
-
|
|
9677
|
+
writeJsoncConfig(configPath, originalText, config, edits);
|
|
9457
9678
|
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
|
9458
9679
|
}
|
|
9459
9680
|
|
|
@@ -9476,9 +9697,11 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9476
9697
|
|
|
9477
9698
|
// Read existing config or create empty object
|
|
9478
9699
|
let config = {};
|
|
9700
|
+
let originalText = null;
|
|
9479
9701
|
if (fs.existsSync(configPath)) {
|
|
9480
9702
|
try {
|
|
9481
9703
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
9704
|
+
originalText = content;
|
|
9482
9705
|
config = parseJsonc(content);
|
|
9483
9706
|
} catch (e) {
|
|
9484
9707
|
// Cannot parse - DO NOT overwrite user's config
|
|
@@ -9503,6 +9726,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9503
9726
|
: `${kiloConfigDir.replace(/\\/g, '/')}/gsd-core/*`;
|
|
9504
9727
|
|
|
9505
9728
|
let modified = false;
|
|
9729
|
+
const edits = [];
|
|
9506
9730
|
|
|
9507
9731
|
// Configure read permission
|
|
9508
9732
|
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
|
@@ -9510,6 +9734,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9510
9734
|
}
|
|
9511
9735
|
if (config.permission.read[gsdPath] !== 'allow') {
|
|
9512
9736
|
config.permission.read[gsdPath] = 'allow';
|
|
9737
|
+
edits.push({ path: ['permission', 'read', gsdPath], value: 'allow' });
|
|
9513
9738
|
modified = true;
|
|
9514
9739
|
}
|
|
9515
9740
|
|
|
@@ -9519,6 +9744,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9519
9744
|
}
|
|
9520
9745
|
if (config.permission.external_directory[gsdPath] !== 'allow') {
|
|
9521
9746
|
config.permission.external_directory[gsdPath] = 'allow';
|
|
9747
|
+
edits.push({ path: ['permission', 'external_directory', gsdPath], value: 'allow' });
|
|
9522
9748
|
modified = true;
|
|
9523
9749
|
}
|
|
9524
9750
|
|
|
@@ -9527,7 +9753,7 @@ function configureKiloPermissions(isGlobal = true, configDir = null) {
|
|
|
9527
9753
|
}
|
|
9528
9754
|
|
|
9529
9755
|
// Write config back
|
|
9530
|
-
|
|
9756
|
+
writeJsoncConfig(configPath, originalText, config, edits);
|
|
9531
9757
|
console.log(` ${green}✓${reset} Configured read permission for GSD docs`);
|
|
9532
9758
|
}
|
|
9533
9759
|
|
|
@@ -10012,8 +10238,9 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10012
10238
|
// to git-cmd.js (validate-commit classifier) and gsd-graphify-rebuild.sh.
|
|
10013
10239
|
const hooksLibDir = path.join(hooksDir, 'lib');
|
|
10014
10240
|
if (fs.existsSync(hooksLibDir)) {
|
|
10241
|
+
const hookLibNames = new Set(gsdHookLibFiles());
|
|
10015
10242
|
for (const file of fs.readdirSync(hooksLibDir)) {
|
|
10016
|
-
if (
|
|
10243
|
+
if (hookLibNames.has(file)) {
|
|
10017
10244
|
manifest.files[sharedHooksDirName + '/lib/' + file] = fileHash(path.join(hooksLibDir, file));
|
|
10018
10245
|
}
|
|
10019
10246
|
}
|
|
@@ -10063,6 +10290,14 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
10063
10290
|
manifest.files[`${_npM.dir}/${_npM.file}`] = fileHash(pluginInstallPath);
|
|
10064
10291
|
}
|
|
10065
10292
|
}
|
|
10293
|
+
// The OpenCode 2.x terminal-client plugin (statusline / update banner) is tracked beside it.
|
|
10294
|
+
const _cliM = hostBehaviorsFor(runtime).nativeCliPlugin;
|
|
10295
|
+
if (_cliM) {
|
|
10296
|
+
const cliInstallPath = path.join(configDir, _cliM.dir, _cliM.file);
|
|
10297
|
+
if (fs.existsSync(cliInstallPath)) {
|
|
10298
|
+
manifest.files[`${_cliM.dir}/${_cliM.file}`] = fileHash(cliInstallPath);
|
|
10299
|
+
}
|
|
10300
|
+
}
|
|
10066
10301
|
|
|
10067
10302
|
fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
|
|
10068
10303
|
return manifest;
|
|
@@ -13024,7 +13259,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13024
13259
|
}
|
|
13025
13260
|
const kimiHookOpts = { portableHooks: hasPortableHooks, runtime };
|
|
13026
13261
|
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
|
13027
|
-
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts });
|
|
13262
|
+
const kimiHooksResult = writeKimiHooksToml(kimiHooksTomlPath, kimiHooksRoot, { hookOpts: kimiHookOpts, contextWarnings: runtimeSupportsContextWarnings(runtime) });
|
|
13028
13263
|
configuredEntrypoints.push(...kimiHooksResult.configuredEntrypoints);
|
|
13029
13264
|
if (kimiHooksResult.changed) {
|
|
13030
13265
|
console.log(` ${green}✓${reset} Configured ${kimiHooksResult.entryCount} GSD hook(s) in ${kimiHooksTomlPath}`);
|
|
@@ -13306,6 +13541,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13306
13541
|
const updateCheckCommand = isGlobal
|
|
13307
13542
|
? buildHookCommand(targetDir, 'gsd-check-update.js', hookOpts)
|
|
13308
13543
|
: localCmd('gsd-check-update.js');
|
|
13544
|
+
// #4840: no metrics writer on this host -> no monitor to register (see runtimeSupportsContextWarnings).
|
|
13545
|
+
const contextWarnings = runtimeSupportsContextWarnings(runtime);
|
|
13546
|
+
// The command stays built: the extended lifecycle events (SubagentStop, PreCompact, ...) still route to it.
|
|
13309
13547
|
const contextMonitorCommand = isGlobal
|
|
13310
13548
|
? buildHookCommand(targetDir, 'gsd-context-monitor.js', hookOpts)
|
|
13311
13549
|
: localCmd('gsd-context-monitor.js');
|
|
@@ -13350,6 +13588,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13350
13588
|
hooksSurface: plan.hooksSurface,
|
|
13351
13589
|
updateCheckCommand,
|
|
13352
13590
|
contextMonitorCommand,
|
|
13591
|
+
contextWarnings,
|
|
13353
13592
|
promptGuardCommand,
|
|
13354
13593
|
readGuardCommand,
|
|
13355
13594
|
readInjectionScannerCommand,
|
|
@@ -13358,6 +13597,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
|
|
13358
13597
|
localCmd,
|
|
13359
13598
|
localShellCmd,
|
|
13360
13599
|
});
|
|
13600
|
+
if (!contextWarnings) {
|
|
13601
|
+
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`);
|
|
13602
|
+
}
|
|
13361
13603
|
|
|
13362
13604
|
// Compute the update-banner hook command alongside the others so
|
|
13363
13605
|
// installAllRuntimes can register it at finalize time when the user opts
|
|
@@ -13626,6 +13868,31 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
13626
13868
|
// Configure OpenCode permissions
|
|
13627
13869
|
if (plan.finishPermissionWriter === 'opencode' && !process.env.GSD_TEST_MODE) {
|
|
13628
13870
|
configureOpencodePermissions(isGlobal, configDir);
|
|
13871
|
+
// ADR-5331: say which OpenCode generation this install will run under. Informational only —
|
|
13872
|
+
// a failed probe never affects the install.
|
|
13873
|
+
try {
|
|
13874
|
+
const ocv = require('../gsd-core/bin/lib/opencode-version.cjs');
|
|
13875
|
+
const gen = ocv.detectOpencodeGeneration();
|
|
13876
|
+
if (gen.generation !== null && typeof configDir === 'string' && configDir) {
|
|
13877
|
+
recordHostGeneration(configDir, { generation: gen.generation, version: gen.version });
|
|
13878
|
+
}
|
|
13879
|
+
if (gen.generation === 2) {
|
|
13880
|
+
console.log(` ${green}✓${reset} OpenCode ${gen.version || '2.x'} detected — GSD loads its OpenCode 2.x plugin entrypoint`);
|
|
13881
|
+
} else if (gen.generation === 1) {
|
|
13882
|
+
console.log(` ${green}✓${reset} OpenCode ${gen.version || '1.x'} detected — GSD loads its OpenCode 1.x plugin entrypoint`);
|
|
13883
|
+
}
|
|
13884
|
+
// ADR-5331 D6 / E8: OpenCode 2.x migrates its data directory one way. Say so before it happens, and say
|
|
13885
|
+
// when 1.x is installed over a data directory 2.x already migrated (1.x cannot open it).
|
|
13886
|
+
const dataDir = ocv.resolveOpencodeDataDir();
|
|
13887
|
+
const notice = ocv.describeDataDirNotice(gen.generation, ocv.classifyOpencodeDatabase(path.join(dataDir, 'opencode.db')), dataDir);
|
|
13888
|
+
if (notice) {
|
|
13889
|
+
console.log(notice.level === 'warn' ? ` ${yellow}⚠${reset} ${notice.text}` : ` ${dim}${notice.text}${reset}`);
|
|
13890
|
+
}
|
|
13891
|
+
// Phase 3 (#5339): OpenCode 1.x is bug-fix-only until the freeze date. The `opencode` runtime only —
|
|
13892
|
+
// this block does not run for Kilo.
|
|
13893
|
+
const deprecation = ocv.describeDeprecationNotice(gen.generation);
|
|
13894
|
+
if (deprecation) console.log(` ${yellow}⚠${reset} ${deprecation}`);
|
|
13895
|
+
} catch { /* detection is advisory */ }
|
|
13629
13896
|
}
|
|
13630
13897
|
|
|
13631
13898
|
// Configure Kilo permissions
|