@opengsd/gsd-core 1.3.1 → 1.4.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +23 -0
- package/GEMINI.md +53 -0
- package/agents/gsd-advisor-researcher.md +1 -20
- package/agents/gsd-ai-researcher.md +2 -21
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-domain-researcher.md +2 -21
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.md +1 -1
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-framework-selector.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-pattern-mapper.md +1 -1
- package/agents/gsd-phase-researcher.md +92 -166
- package/agents/gsd-planner.md +9 -36
- package/agents/gsd-project-researcher.md +62 -141
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.md +3 -22
- package/agents/gsd-user-profiler.md +1 -1
- package/agents/gsd-verifier.md +8 -2
- package/bin/install.js +1977 -339
- package/commands/gsd/autonomous.md +2 -0
- package/commands/gsd/execute-phase.md +2 -0
- package/commands/gsd/graphify.md +11 -6
- package/commands/gsd/import.md +6 -2
- package/commands/gsd/plan-phase.md +4 -2
- package/commands/gsd/progress.md +1 -0
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/update.md +3 -2
- package/gemini-extension.json +6 -0
- package/gsd-core/bin/check-latest-version.cjs +61 -6
- package/gsd-core/bin/gsd-tools.cjs +238 -32
- package/gsd-core/bin/lib/check-command-router.cjs +1 -0
- package/gsd-core/bin/lib/cli-exit.cjs +42 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
- package/gsd-core/bin/lib/commands.cjs +5 -4
- package/gsd-core/bin/lib/config.cjs +28 -4
- package/gsd-core/bin/lib/core.cjs +72 -28
- package/gsd-core/bin/lib/graphify.cjs +2 -2
- package/gsd-core/bin/lib/init-command-router.cjs +2 -2
- package/gsd-core/bin/lib/init.cjs +19 -3
- package/gsd-core/bin/lib/install-profiles.cjs +58 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
- package/gsd-core/bin/lib/intel.cjs +3 -20
- package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
- package/gsd-core/bin/lib/phase.cjs +3 -3
- package/gsd-core/bin/lib/research-provider.cjs +137 -0
- package/gsd-core/bin/lib/research-store.cjs +167 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
- package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
- package/gsd-core/bin/lib/security.cjs +73 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +9 -0
- package/gsd-core/bin/lib/surface.cjs +54 -11
- package/gsd-core/bin/lib/validate.cjs +2 -2
- package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
- package/gsd-core/bin/lib/verification.cjs +193 -0
- package/gsd-core/bin/lib/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
- package/gsd-core/references/planner-load-graph-context.md +36 -0
- package/gsd-core/references/planning-config.md +3 -1
- package/gsd-core/references/research-documentation-lookup.md +29 -0
- package/gsd-core/references/research-philosophy.md +29 -0
- package/gsd-core/references/research-verification-protocol.md +27 -0
- package/gsd-core/workflows/execute-phase.md +19 -8
- package/gsd-core/workflows/help/modes/full.md +4 -3
- package/gsd-core/workflows/ingest-docs.md +3 -2
- package/gsd-core/workflows/plan-phase.md +14 -10
- package/gsd-core/workflows/plan-review-convergence.md +3 -3
- package/gsd-core/workflows/review.md +24 -7
- package/gsd-core/workflows/ship.md +5 -8
- package/gsd-core/workflows/spec-phase.md +2 -1
- package/gsd-core/workflows/update.md +34 -6
- package/hooks/dist/gsd-config-reload.js +133 -0
- package/hooks/dist/gsd-context-monitor.js +1 -1
- package/hooks/dist/gsd-cursor-post-tool.js +75 -0
- package/hooks/dist/gsd-cursor-session-start.js +52 -0
- package/hooks/dist/gsd-workflow-guard.js +1 -0
- package/hooks/dist/gsd-worktree-path-guard.js +1 -1
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-config-reload.js +133 -0
- package/hooks/gsd-context-monitor.js +1 -1
- package/hooks/gsd-cursor-post-tool.js +75 -0
- package/hooks/gsd-cursor-session-start.js +52 -0
- package/hooks/gsd-workflow-guard.js +1 -0
- package/hooks/gsd-worktree-path-guard.js +1 -1
- package/hooks/hooks.json +69 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +8 -1
- package/scripts/affected-tests-lib.cjs +3 -2
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/cli.cjs +226 -28
- package/scripts/changeset/lint.cjs +5 -4
- package/scripts/changeset/new.cjs +4 -4
- package/scripts/check-alias-drift.cjs +77 -71
- package/scripts/check-env.cjs +185 -179
- package/scripts/check-npm-integrity.cjs +115 -109
- package/scripts/ci-guard-runner.cjs +11 -5
- package/scripts/ci-prepare-test-scope.cjs +27 -22
- package/scripts/ci-rebase-check.cjs +46 -45
- package/scripts/ci-test-scope.cjs +126 -22
- package/scripts/diff-touches-shipped-paths.cjs +52 -44
- package/scripts/gen-inventory-manifest.cjs +38 -32
- package/scripts/gen-research-agents.cjs +276 -0
- package/scripts/issue-dedupe.cjs +278 -0
- package/scripts/lib/cli-exit.cjs +56 -0
- package/scripts/lint-command-contract.cjs +28 -22
- package/scripts/lint-descriptions.cjs +32 -28
- package/scripts/lint-docs-required.cjs +4 -4
- package/scripts/lint-legacy-dir-name.cjs +56 -52
- package/scripts/lint-pr-check-project-dir.cjs +3 -1
- package/scripts/lint-shell-command-projection-drift.cjs +27 -22
- package/scripts/lint-skill-deps.cjs +31 -26
- package/scripts/lint-test-file-count.allowlist.json +2 -0
- package/scripts/lint-test-file-count.cjs +5 -4
- package/scripts/mutation-matrix.cjs +6 -3
- package/scripts/prompt-injection-scan.sh +1 -1
- package/scripts/release-notes/discord-release-summary.cjs +373 -0
- package/scripts/release-notes/format-github-release-notes.cjs +8 -3
- package/scripts/release-tarball-smoke.cjs +6 -4
- package/scripts/research-profiles.cjs +149 -0
- package/scripts/run-affected-tests.cjs +2 -1
- package/scripts/run-cross-platform-tests.cjs +11 -7
- package/scripts/run-tests.cjs +8 -7
- package/scripts/strip-prose-atrefs.cjs +1 -1
- package/scripts/sync-manifest-versions.cjs +119 -0
- package/scripts/sync-runtime-launcher.cjs +0 -3
- package/scripts/verify-npm-publish.cjs +14 -26
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Research Store Module
|
|
4
|
+
*
|
|
5
|
+
* Provides deterministic cache key generation, TTL policy, path resolution,
|
|
6
|
+
* and JSON-backed put/get operations for research entries.
|
|
7
|
+
*
|
|
8
|
+
* ADR-457 build-at-publish: authored as TypeScript .cts → emits .cjs via tsc.
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
17
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const DAY_MS = 86_400_000;
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// researchKey
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
function normalize(x) {
|
|
26
|
+
if (x === null || x === undefined)
|
|
27
|
+
return '';
|
|
28
|
+
if (typeof x === 'object')
|
|
29
|
+
return JSON.stringify(x).trim().toLowerCase();
|
|
30
|
+
// After excluding null, undefined, and object, x can only be a primitive —
|
|
31
|
+
// cast through number | string | boolean to avoid no-base-to-string on unknown.
|
|
32
|
+
return `${x}`.trim().toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function researchKey(input) {
|
|
35
|
+
const parts = {
|
|
36
|
+
ecosystem: normalize(input.ecosystem),
|
|
37
|
+
library: normalize(input.library),
|
|
38
|
+
version: normalize(input.version),
|
|
39
|
+
query: normalize(input.query),
|
|
40
|
+
kind: normalize(input.kind),
|
|
41
|
+
};
|
|
42
|
+
const serialized = JSON.stringify(parts);
|
|
43
|
+
return node_crypto_1.default.createHash('sha256').update(serialized).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// ttlForSource
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function ttlForSource(source, confidence) {
|
|
49
|
+
if (source === 'curated' && confidence === 'HIGH')
|
|
50
|
+
return 30 * DAY_MS;
|
|
51
|
+
if (source === 'curated' && confidence === 'MEDIUM')
|
|
52
|
+
return 7 * DAY_MS;
|
|
53
|
+
return DAY_MS;
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// tierForSource / resolveStorePath
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
const CURATED_SOURCES = new Set(['curated']);
|
|
59
|
+
function tierForSource(source) {
|
|
60
|
+
return CURATED_SOURCES.has(source) ? 'user' : 'project';
|
|
61
|
+
}
|
|
62
|
+
function resolveStorePath(cwd, source, { homeDir = node_os_1.default.homedir() } = {}) {
|
|
63
|
+
if (tierForSource(source) === 'user') {
|
|
64
|
+
return node_path_1.default.join(homeDir, '.gsd', 'research-cache');
|
|
65
|
+
}
|
|
66
|
+
return node_path_1.default.join(cwd, '.planning', 'research', '.cache');
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// isValidResearchKey
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* Returns true iff key is a valid 64-character lowercase hexadecimal SHA-256
|
|
73
|
+
* string (the exact shape produced by researchKey). Any other shape —
|
|
74
|
+
* including path-traversal sequences — is rejected.
|
|
75
|
+
*/
|
|
76
|
+
function isValidResearchKey(key) {
|
|
77
|
+
return typeof key === 'string' && /^[0-9a-f]{64}$/.test(key);
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// putResearch
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function putResearch(cwd, key, payload, { clock = Date, homeDir = node_os_1.default.homedir() } = {}) {
|
|
83
|
+
// Defense-in-depth: reject any key that is not a 64-char sha256 hex string.
|
|
84
|
+
if (!isValidResearchKey(key)) {
|
|
85
|
+
throw new Error('invalid research key');
|
|
86
|
+
}
|
|
87
|
+
const { content, source, provider, confidence, kind, version } = payload;
|
|
88
|
+
let ttl = ttlForSource(source, confidence);
|
|
89
|
+
// Cap TTL when version is blank/missing — a versionless curated entry must not
|
|
90
|
+
// get the long 30-day window since we can't know if it's still current.
|
|
91
|
+
if (!version) {
|
|
92
|
+
ttl = Math.min(ttl, DAY_MS);
|
|
93
|
+
}
|
|
94
|
+
const fetched_at = new Date(clock.now()).toISOString();
|
|
95
|
+
const entry = { content, source, provider, confidence, fetched_at, ttl, kind };
|
|
96
|
+
const dir = resolveStorePath(cwd, source, { homeDir });
|
|
97
|
+
// Belt-and-suspenders: ensure the resolved file path stays inside the store dir.
|
|
98
|
+
const resolvedDir = node_path_1.default.resolve(dir);
|
|
99
|
+
const filePath = node_path_1.default.join(dir, `${key}.json`);
|
|
100
|
+
const resolvedFile = node_path_1.default.resolve(filePath);
|
|
101
|
+
if (!resolvedFile.startsWith(resolvedDir + node_path_1.default.sep)) {
|
|
102
|
+
throw new Error('invalid research key');
|
|
103
|
+
}
|
|
104
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
105
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(filePath, JSON.stringify(entry));
|
|
106
|
+
return entry;
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// getResearch
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
function getResearch(cwd, key, { clock = Date, homeDir = node_os_1.default.homedir() } = {}) {
|
|
112
|
+
// Defense-in-depth: reject any key that is not a 64-char sha256 hex string.
|
|
113
|
+
if (!isValidResearchKey(key)) {
|
|
114
|
+
return { hit: false, stale: false, entry: null };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
// Search both physical tiers: user (curated) and project (web/etc.)
|
|
118
|
+
const userDir = node_path_1.default.join(homeDir, '.gsd', 'research-cache');
|
|
119
|
+
const projectDir = node_path_1.default.join(cwd, '.planning', 'research', '.cache');
|
|
120
|
+
const tierDirs = [userDir, projectDir];
|
|
121
|
+
const candidates = [];
|
|
122
|
+
for (const dir of tierDirs) {
|
|
123
|
+
const resolvedDir = node_path_1.default.resolve(dir);
|
|
124
|
+
const filePath = node_path_1.default.join(dir, `${key}.json`);
|
|
125
|
+
// Belt-and-suspenders: ensure path stays inside tier dir
|
|
126
|
+
if (!node_path_1.default.resolve(filePath).startsWith(resolvedDir + node_path_1.default.sep))
|
|
127
|
+
continue;
|
|
128
|
+
if (!node_fs_1.default.existsSync(filePath))
|
|
129
|
+
continue;
|
|
130
|
+
let entry;
|
|
131
|
+
try {
|
|
132
|
+
entry = JSON.parse(node_fs_1.default.readFileSync(filePath, 'utf8'));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Corrupt file in this tier — skip it
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Finding 3: validate entry metadata shape before accepting as a candidate.
|
|
139
|
+
// An entry with missing/invalid fetched_at or ttl must be treated as a miss.
|
|
140
|
+
const parsedFetchedAt = Date.parse(entry.fetched_at);
|
|
141
|
+
if (!Number.isFinite(parsedFetchedAt))
|
|
142
|
+
continue;
|
|
143
|
+
if (typeof entry.ttl !== 'number' ||
|
|
144
|
+
!Number.isFinite(entry.ttl) ||
|
|
145
|
+
entry.ttl <= 0)
|
|
146
|
+
continue;
|
|
147
|
+
const age = clock.now() - parsedFetchedAt;
|
|
148
|
+
const stale = age > entry.ttl;
|
|
149
|
+
candidates.push({ entry, stale, age });
|
|
150
|
+
}
|
|
151
|
+
if (candidates.length === 0) {
|
|
152
|
+
return { hit: false, stale: false, entry: null };
|
|
153
|
+
}
|
|
154
|
+
// Prefer: non-stale over stale; among same-staleness, lowest age (most recent)
|
|
155
|
+
candidates.sort((a, b) => {
|
|
156
|
+
if (a.stale !== b.stale)
|
|
157
|
+
return a.stale ? 1 : -1; // non-stale first
|
|
158
|
+
return a.age - b.age; // lower age (more recent) first
|
|
159
|
+
});
|
|
160
|
+
const best = candidates[0];
|
|
161
|
+
return { hit: true, stale: best.stale, entry: best.entry };
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return { hit: false, stale: false, entry: null };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
module.exports = { isValidResearchKey, researchKey, ttlForSource, tierForSource, resolveStorePath, putResearch, getResearch };
|
|
@@ -25,8 +25,6 @@ const MIGRATED_PHASE_HEADING_RE = /^#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+\d+-\d{2}
|
|
|
25
25
|
// Matches milestone section headings: ## v1.0, ## Roadmap v2.0, ## ✅ v1.0, ## [GSD] v1.0, etc.
|
|
26
26
|
// The optional bracket-token prefix (e.g., [GSD]) must be tested before the emoji group.
|
|
27
27
|
const MILESTONE_HEADING_RE = /^##\s+(?:\[[^\]]+\]\s+|Roadmap\s+|[✅🚧]\s*)?v(\d+)\.(\d+)(?:\s|:)/iu;
|
|
28
|
-
// Matches checklist phase references: - [ ] **Phase N:** or - [x] **Phase N:** (also decimal)
|
|
29
|
-
const CHECKLIST_PHASE_RE = /^(\s*-\s*\[[ x]\]\s*\*{0,2})Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
|
30
28
|
// ─── Pure computation helpers ─────────────────────────────────────────────────
|
|
31
29
|
/**
|
|
32
30
|
* Parse the ROADMAP.md content and build a list of phase entries with their
|
|
@@ -126,19 +124,6 @@ function buildNewDirName(oldDirName, newId, projectCode) {
|
|
|
126
124
|
const newBase = slug ? `${paddedMilestone}-${subStr}-${slug}` : `${paddedMilestone}-${subStr}`;
|
|
127
125
|
return projectCode ? `${projectCode}-${newBase}` : newBase;
|
|
128
126
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Read project_code from config.json if present.
|
|
131
|
-
*/
|
|
132
|
-
function readProjectCode(configPath) {
|
|
133
|
-
try {
|
|
134
|
-
const raw = node_fs_1.default.readFileSync(configPath, 'utf8');
|
|
135
|
-
const parsed = JSON.parse(raw);
|
|
136
|
-
return typeof parsed['project_code'] === 'string' ? parsed['project_code'] : null;
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
127
|
// ─── computeMigrationPlan ─────────────────────────────────────────────────────
|
|
143
128
|
/**
|
|
144
129
|
* Compute a migration plan without touching the filesystem.
|
|
@@ -390,7 +375,7 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
390
375
|
// ── Real run: verify clean working tree ───────────────────────────────────
|
|
391
376
|
let gitStatus;
|
|
392
377
|
try {
|
|
393
|
-
gitStatus = (0, node_child_process_1.execSync)('git status --porcelain', { cwd, encoding: 'utf8' });
|
|
378
|
+
gitStatus = (0, node_child_process_1.execSync)('git status --porcelain', { cwd, encoding: 'utf8', windowsHide: true });
|
|
394
379
|
}
|
|
395
380
|
catch (err) {
|
|
396
381
|
throw new Error(`git status failed: ${err.message}`);
|
|
@@ -401,7 +386,7 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
401
386
|
// Capture HEAD sha for rollback
|
|
402
387
|
let headSha;
|
|
403
388
|
try {
|
|
404
|
-
headSha = (0, node_child_process_1.execSync)('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
|
|
389
|
+
headSha = (0, node_child_process_1.execSync)('git rev-parse HEAD', { cwd, encoding: 'utf8', windowsHide: true }).trim();
|
|
405
390
|
}
|
|
406
391
|
catch (err) {
|
|
407
392
|
throw new Error(`git rev-parse HEAD failed: ${err.message}`);
|
|
@@ -475,8 +460,8 @@ function applyMigration(cwd, plan, options = {}) {
|
|
|
475
460
|
catch (err) {
|
|
476
461
|
// Rollback via git reset --hard + git clean
|
|
477
462
|
try {
|
|
478
|
-
(0, node_child_process_1.execSync)(`git reset --hard ${headSha}`, { cwd, stdio: 'pipe' });
|
|
479
|
-
(0, node_child_process_1.execSync)('git clean -fd .planning/phases/', { cwd, stdio: 'pipe' });
|
|
463
|
+
(0, node_child_process_1.execSync)(`git reset --hard ${headSha}`, { cwd, stdio: 'pipe', windowsHide: true });
|
|
464
|
+
(0, node_child_process_1.execSync)('git clean -fd .planning/phases/', { cwd, stdio: 'pipe', windowsHide: true });
|
|
480
465
|
}
|
|
481
466
|
catch {
|
|
482
467
|
// Swallow rollback errors — surface original error
|
|
@@ -18,7 +18,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
18
18
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
20
|
const installProfiles = require("./install-profiles.cjs");
|
|
21
|
-
const { stageSkillsForProfile, stageAgentsForProfile, stageSkillsForRuntimeAsSkills, } = installProfiles;
|
|
21
|
+
const { stageSkillsForProfile, stageAgentsForProfile, stageSkillsForRuntimeAsSkills, stageCommandsForRuntimeFlat, } = installProfiles;
|
|
22
22
|
// In .cts (CommonJS output) files, `require` is available as a global.
|
|
23
23
|
const _require = require;
|
|
24
24
|
/**
|
|
@@ -175,6 +175,34 @@ function skillsKind(destSubpath, prefix, converterName, runtime, configDir) {
|
|
|
175
175
|
},
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Build a converted-commands kind descriptor for runtimes that use a flat
|
|
180
|
+
* commands directory with per-file conversion (e.g. Cursor 1.6 slash commands).
|
|
181
|
+
*
|
|
182
|
+
* Unlike `commandsKind` (which passes raw source files through), this kind
|
|
183
|
+
* applies `converterName` from bin/install.js exports to each file during
|
|
184
|
+
* staging, writing flat `${prefix}${stem}.md` files to the staged directory.
|
|
185
|
+
*
|
|
186
|
+
* The staged files are then written by `_copyStaged` (commands branch) which
|
|
187
|
+
* handles prefix logic via the existing layout machinery.
|
|
188
|
+
*
|
|
189
|
+
* @param destSubpath destination subpath within configDir (e.g. 'commands')
|
|
190
|
+
* @param prefix filename prefix, e.g. 'gsd-'
|
|
191
|
+
* @param converterName name of converter function in bin/install.js exports
|
|
192
|
+
* @param configDir runtime config dir (for .gsd-source marker resolution)
|
|
193
|
+
*/
|
|
194
|
+
function convertedCommandsKind(destSubpath, prefix, converterName, configDir) {
|
|
195
|
+
return {
|
|
196
|
+
kind: 'commands',
|
|
197
|
+
destSubpath,
|
|
198
|
+
prefix,
|
|
199
|
+
stage: (resolved) => {
|
|
200
|
+
const installExports = getInstallExports();
|
|
201
|
+
const converter = installExports[converterName];
|
|
202
|
+
return stageCommandsForRuntimeFlat(findInstallSourceRoot(configDir), resolved, converter, prefix);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
178
206
|
// ---------------------------------------------------------------------------
|
|
179
207
|
// Public API
|
|
180
208
|
// ---------------------------------------------------------------------------
|
|
@@ -205,7 +233,14 @@ function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
|
|
|
205
233
|
}
|
|
206
234
|
break;
|
|
207
235
|
case 'cursor':
|
|
208
|
-
|
|
236
|
+
// Cursor 1.6+ supports two artifact surfaces:
|
|
237
|
+
// 1. skills/gsd-<name>/SKILL.md — rich skills with frontmatter + adapter header
|
|
238
|
+
// 2. commands/gsd-<name>.md — plain markdown slash commands (no frontmatter)
|
|
239
|
+
// accessed via '/' in the Agent input (#785)
|
|
240
|
+
kinds = [
|
|
241
|
+
skillsKind('skills', 'gsd-', 'convertClaudeCommandToCursorSkill', 'cursor', configDir),
|
|
242
|
+
convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCursorCommand', configDir),
|
|
243
|
+
];
|
|
209
244
|
break;
|
|
210
245
|
case 'gemini':
|
|
211
246
|
kinds = [commandsKind('commands/gsd', 'gsd-', configDir)];
|
|
@@ -223,7 +258,10 @@ function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
|
|
|
223
258
|
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToWindsurfSkill', 'windsurf', configDir)];
|
|
224
259
|
break;
|
|
225
260
|
case 'augment':
|
|
226
|
-
kinds = [
|
|
261
|
+
kinds = [
|
|
262
|
+
commandsKind('commands', 'gsd-', configDir),
|
|
263
|
+
skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir),
|
|
264
|
+
];
|
|
227
265
|
break;
|
|
228
266
|
case 'trae':
|
|
229
267
|
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToTraeSkill', 'trae', configDir)];
|
|
@@ -235,20 +273,40 @@ function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
|
|
|
235
273
|
kinds = [skillsKind('skills/gsd', '', 'convertClaudeCommandToClaudeSkill', 'hermes', configDir)];
|
|
236
274
|
break;
|
|
237
275
|
case 'codebuddy':
|
|
238
|
-
|
|
276
|
+
// CodeBuddy (Tencent) reads two user-level surfaces (codebuddy.ai/docs/cli):
|
|
277
|
+
// 1. commands/gsd-<name>.md — slash commands shown in the '/' menu (#789)
|
|
278
|
+
// 2. skills/gsd-<name>/SKILL.md — model-invocable skills, emitted with
|
|
279
|
+
// user-invocable:false so they stay OUT of '/' (the commands surface is
|
|
280
|
+
// the sole '/' entry point) — avoids a duplicated /gsd-* per workflow.
|
|
281
|
+
// Subagents (~/.codebuddy/agents/) are already emitted by the generic agents
|
|
282
|
+
// block in bin/install.js; MCP is excluded (gsd ships no MCP server).
|
|
283
|
+
kinds = [
|
|
284
|
+
convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCodebuddyCommand', configDir),
|
|
285
|
+
skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodebuddySkill', 'codebuddy', configDir),
|
|
286
|
+
];
|
|
239
287
|
break;
|
|
240
288
|
case 'cline':
|
|
241
|
-
kinds = [];
|
|
289
|
+
kinds = scope === 'global' ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClineSkill', 'cline', configDir)] : [];
|
|
242
290
|
break;
|
|
243
291
|
case 'opencode':
|
|
244
|
-
|
|
292
|
+
// OpenCode reads flat slash commands from command/ and on-demand skills
|
|
293
|
+
// from skills/<name>/SKILL.md (https://opencode.ai/docs/skills). Emit both.
|
|
294
|
+
kinds = [
|
|
295
|
+
commandsKind('command', 'gsd-', configDir),
|
|
296
|
+
skillsKind('skills', 'gsd-', 'convertClaudeCommandToOpencodeSkill', 'opencode', configDir),
|
|
297
|
+
];
|
|
245
298
|
break;
|
|
246
299
|
case 'kilo':
|
|
247
|
-
|
|
300
|
+
// Kilo derives from OpenCode and shares the skills/<name>/SKILL.md layout
|
|
301
|
+
// (https://kilo.ai/docs/customize/skills). Emit flat commands + skills.
|
|
302
|
+
kinds = [
|
|
303
|
+
commandsKind('command', 'gsd-', configDir),
|
|
304
|
+
skillsKind('skills', 'gsd-', 'convertClaudeCommandToKiloSkill', 'kilo', configDir),
|
|
305
|
+
];
|
|
248
306
|
break;
|
|
249
307
|
default:
|
|
250
308
|
throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
|
|
251
309
|
}
|
|
252
|
-
return { runtime, configDir, kinds };
|
|
310
|
+
return { runtime, configDir, scope, kinds };
|
|
253
311
|
}
|
|
254
|
-
module.exports = { resolveRuntimeArtifactLayout, findInstallSourceRoot };
|
|
312
|
+
module.exports = { resolveRuntimeArtifactLayout, findInstallSourceRoot, getInstallExports };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Registry
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
const REGISTRY = Object.freeze({
|
|
6
|
+
claude: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
|
|
7
|
+
gemini: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
|
|
8
|
+
antigravity: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
|
|
9
|
+
augment: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
|
|
10
|
+
qwen: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
|
|
11
|
+
hermes: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
|
|
12
|
+
codebuddy: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
|
|
13
|
+
opencode: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: 'opencode' }),
|
|
14
|
+
kilo: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: false, finishPermissionWriter: 'kilo' }),
|
|
15
|
+
codex: Object.freeze({ installSurface: 'codex-toml', writesSharedSettings: false, finishPermissionWriter: null }),
|
|
16
|
+
copilot: Object.freeze({ installSurface: 'copilot-instructions', writesSharedSettings: false, finishPermissionWriter: null }),
|
|
17
|
+
cline: Object.freeze({ installSurface: 'cline-rules', writesSharedSettings: false, finishPermissionWriter: null }),
|
|
18
|
+
cursor: Object.freeze({ installSurface: 'cursor-hooks-json', writesSharedSettings: false, finishPermissionWriter: null }),
|
|
19
|
+
windsurf: Object.freeze({ installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null }),
|
|
20
|
+
trae: Object.freeze({ installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null }),
|
|
21
|
+
});
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Exports
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** The complete set of 15 supported runtimes for config-adapter dispatch. */
|
|
26
|
+
const ALLOWED_CONFIG_RUNTIMES = new Set(Object.keys(REGISTRY));
|
|
27
|
+
/** All valid installSurface values. */
|
|
28
|
+
const INSTALL_SURFACES = Object.freeze([
|
|
29
|
+
'settings-json',
|
|
30
|
+
'codex-toml',
|
|
31
|
+
'copilot-instructions',
|
|
32
|
+
'cline-rules',
|
|
33
|
+
'cursor-hooks-json',
|
|
34
|
+
'profile-marker-only',
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the config adapter intent for a given runtime.
|
|
38
|
+
*
|
|
39
|
+
* Returns a fresh object each call so callers cannot poison the registry by
|
|
40
|
+
* mutating the returned value.
|
|
41
|
+
*
|
|
42
|
+
* @throws {TypeError} if runtime is not a known supported runtime.
|
|
43
|
+
*/
|
|
44
|
+
function resolveRuntimeConfigIntent(runtime) {
|
|
45
|
+
if (!Object.hasOwn(REGISTRY, runtime)) {
|
|
46
|
+
throw new TypeError(`Unknown runtime for config adapter: ${runtime}`);
|
|
47
|
+
}
|
|
48
|
+
const entry = REGISTRY[runtime];
|
|
49
|
+
return {
|
|
50
|
+
runtime,
|
|
51
|
+
installSurface: entry.installSurface,
|
|
52
|
+
writesSharedSettings: entry.writesSharedSettings,
|
|
53
|
+
finishPermissionWriter: entry.finishPermissionWriter,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
module.exports = { resolveRuntimeConfigIntent, ALLOWED_CONFIG_RUNTIMES, INSTALL_SURFACES };
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* Runtime-specific notes:
|
|
13
13
|
* hermes — GSD skills nest under skills/gsd/<skillName>/ (not the flat
|
|
14
14
|
* skills/<skillName>/ layout used by all other runtimes).
|
|
15
|
-
* cline —
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* cline — Skills-capable since v3.48.0 (#782). SKILL.md files live at
|
|
16
|
+
* ~/.cline/skills/<skillName>/SKILL.md (same flat layout as cursor/codex).
|
|
17
|
+
* .clinerules is also emitted (rules-based compatibility layer).
|
|
18
18
|
*/
|
|
19
19
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
20
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -62,8 +62,15 @@ function resolveAntigravityGlobalDir(opts = {}) {
|
|
|
62
62
|
/**
|
|
63
63
|
* Return the global config base directory for the given runtime.
|
|
64
64
|
* Respects the same env-var overrides as bin/install.js getGlobalDir().
|
|
65
|
+
*
|
|
66
|
+
* @param runtime - The runtime identifier (e.g. 'claude', 'opencode').
|
|
67
|
+
* @param explicitDir - If provided and non-empty, returned immediately after
|
|
68
|
+
* tilde-expansion, overriding all env-var and default logic. This matches
|
|
69
|
+
* the behaviour of bin/install.js getGlobalDir(runtime, explicitDir).
|
|
65
70
|
*/
|
|
66
|
-
function getGlobalConfigDir(runtime) {
|
|
71
|
+
function getGlobalConfigDir(runtime, explicitDir) {
|
|
72
|
+
if (explicitDir)
|
|
73
|
+
return expandTilde(explicitDir);
|
|
67
74
|
const home = node_os_1.default.homedir();
|
|
68
75
|
const env = process.env;
|
|
69
76
|
switch (runtime) {
|
|
@@ -84,7 +91,11 @@ function getGlobalConfigDir(runtime) {
|
|
|
84
91
|
return env['GROK_AGENTS_HOME'] ? expandTilde(env['GROK_AGENTS_HOME']) : node_path_1.default.join(home, '.agents');
|
|
85
92
|
// ── Copilot (VS Code) ────────────────────────────────────────────────────
|
|
86
93
|
case 'copilot':
|
|
87
|
-
|
|
94
|
+
if (env['COPILOT_CONFIG_DIR'])
|
|
95
|
+
return expandTilde(env['COPILOT_CONFIG_DIR']);
|
|
96
|
+
if (env['COPILOT_HOME'])
|
|
97
|
+
return expandTilde(env['COPILOT_HOME']);
|
|
98
|
+
return node_path_1.default.join(home, '.copilot');
|
|
88
99
|
// ── Antigravity ──────────────────────────────────────────────────────────
|
|
89
100
|
case 'antigravity':
|
|
90
101
|
return resolveAntigravityGlobalDir({ env, home });
|
|
@@ -115,6 +126,8 @@ function getGlobalConfigDir(runtime) {
|
|
|
115
126
|
case 'opencode': {
|
|
116
127
|
if (env['OPENCODE_CONFIG_DIR'])
|
|
117
128
|
return expandTilde(env['OPENCODE_CONFIG_DIR']);
|
|
129
|
+
if (env['OPENCODE_CONFIG'])
|
|
130
|
+
return node_path_1.default.dirname(expandTilde(env['OPENCODE_CONFIG']));
|
|
118
131
|
if (env['XDG_CONFIG_HOME'])
|
|
119
132
|
return node_path_1.default.join(expandTilde(env['XDG_CONFIG_HOME']), 'opencode');
|
|
120
133
|
return node_path_1.default.join(home, '.config', 'opencode');
|
|
@@ -123,6 +136,8 @@ function getGlobalConfigDir(runtime) {
|
|
|
123
136
|
case 'kilo': {
|
|
124
137
|
if (env['KILO_CONFIG_DIR'])
|
|
125
138
|
return expandTilde(env['KILO_CONFIG_DIR']);
|
|
139
|
+
if (env['KILO_CONFIG'])
|
|
140
|
+
return node_path_1.default.dirname(expandTilde(env['KILO_CONFIG']));
|
|
126
141
|
if (env['XDG_CONFIG_HOME'])
|
|
127
142
|
return node_path_1.default.join(expandTilde(env['XDG_CONFIG_HOME']), 'kilo');
|
|
128
143
|
return node_path_1.default.join(home, '.config', 'kilo');
|
|
@@ -136,19 +151,25 @@ function getGlobalConfigDir(runtime) {
|
|
|
136
151
|
* Return the global skills base directory for the given runtime.
|
|
137
152
|
* Most runtimes: <configDir>/skills
|
|
138
153
|
* Hermes: <configDir>/skills/gsd (nested category layout — #2841)
|
|
139
|
-
* Cline:
|
|
154
|
+
* Cline ≥ v3.48.0: <configDir>/skills (SKILL.md-based global skills — #782)
|
|
140
155
|
*/
|
|
141
156
|
function getGlobalSkillsBase(runtime) {
|
|
142
|
-
if (runtime === '
|
|
143
|
-
|
|
144
|
-
const configDir = getGlobalConfigDir(runtime);
|
|
145
|
-
if (runtime === 'hermes')
|
|
157
|
+
if (runtime === 'hermes') {
|
|
158
|
+
const configDir = getGlobalConfigDir(runtime);
|
|
146
159
|
return node_path_1.default.join(configDir, 'skills', 'gsd');
|
|
160
|
+
}
|
|
161
|
+
// Kilo Code discovers global skills from ~/.kilo/skills/ (HOME-relative),
|
|
162
|
+
// independent of the XDG-based config dir (~/.config/kilo) used for commands.
|
|
163
|
+
// See: https://kilo.ai/docs/customize/skills
|
|
164
|
+
// "Global skills are located in the `.kilo` directory within your Home
|
|
165
|
+
// directory: ~/.kilo/skills/"
|
|
166
|
+
if (runtime === 'kilo')
|
|
167
|
+
return node_path_1.default.join(node_os_1.default.homedir(), '.kilo', 'skills');
|
|
168
|
+
const configDir = getGlobalConfigDir(runtime);
|
|
147
169
|
return node_path_1.default.join(configDir, 'skills');
|
|
148
170
|
}
|
|
149
171
|
/**
|
|
150
172
|
* Return the full path to a specific skill's directory for the given runtime.
|
|
151
|
-
* Returns null for runtimes that don't use a skills directory (cline).
|
|
152
173
|
*/
|
|
153
174
|
function getGlobalSkillDir(runtime, skillName) {
|
|
154
175
|
const base = getGlobalSkillsBase(runtime);
|
|
@@ -24,6 +24,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.MARKDOWN_LINK_PATTERNS = exports.INJECTION_PATTERNS = void 0;
|
|
26
26
|
exports.validatePath = validatePath;
|
|
27
|
+
exports.loadTrustedGlobalRoots = loadTrustedGlobalRoots;
|
|
27
28
|
exports.requireSafePath = requireSafePath;
|
|
28
29
|
exports.scanForInjection = scanForInjection;
|
|
29
30
|
exports.sanitizeForPrompt = sanitizeForPrompt;
|
|
@@ -35,6 +36,7 @@ exports.validateFieldName = validateFieldName;
|
|
|
35
36
|
exports.validatePromptStructure = validatePromptStructure;
|
|
36
37
|
exports.scanEntropyAnomalies = scanEntropyAnomalies;
|
|
37
38
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
38
40
|
const node_path_1 = __importDefault(require("node:path"));
|
|
39
41
|
// ─── Path Traversal Prevention ──────────────────────────────────────────────
|
|
40
42
|
/**
|
|
@@ -92,6 +94,77 @@ function validatePath(filePath, baseDir, opts = {}) {
|
|
|
92
94
|
}
|
|
93
95
|
return { safe: true, resolved: resolvedPath };
|
|
94
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Load the opt-in trusted global roots allowlist from config.
|
|
99
|
+
*
|
|
100
|
+
* Reads `config.agent_skills_security.trusted_global_roots` (an array of
|
|
101
|
+
* path strings). Each entry is canonicalized via realpathSync: non-strings
|
|
102
|
+
* are dropped, leading `~/` is expanded to `os.homedir()`, entries that are
|
|
103
|
+
* not absolute after expansion are dropped (project-relative paths are
|
|
104
|
+
* rejected as a security boundary), and entries that do not exist on disk are
|
|
105
|
+
* dropped (a non-existent root is not trustworthy). The canonical realpath is
|
|
106
|
+
* used for all subsequent checks and as the stored value — this closes the
|
|
107
|
+
* case-insensitive bypass on macOS APFS (`/users/alice` vs `/Users/alice`)
|
|
108
|
+
* and ensures trust doesn't drift across re-invocations if a root is
|
|
109
|
+
* re-created at a different target. Results are de-duplicated by canonical path.
|
|
110
|
+
*/
|
|
111
|
+
function loadTrustedGlobalRoots(config) {
|
|
112
|
+
const roots = config?.['agent_skills_security'];
|
|
113
|
+
const raw = roots?.['trusted_global_roots'];
|
|
114
|
+
if (!Array.isArray(raw))
|
|
115
|
+
return [];
|
|
116
|
+
// Compute canonical homedir once for case-insensitive-safe comparison.
|
|
117
|
+
let realHome;
|
|
118
|
+
try {
|
|
119
|
+
realHome = node_fs_1.default.realpathSync(node_os_1.default.homedir());
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
realHome = node_os_1.default.homedir();
|
|
123
|
+
}
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const result = [];
|
|
126
|
+
for (const entry of raw) {
|
|
127
|
+
if (typeof entry !== 'string')
|
|
128
|
+
continue;
|
|
129
|
+
let expanded;
|
|
130
|
+
if (entry === '~') {
|
|
131
|
+
expanded = node_os_1.default.homedir();
|
|
132
|
+
}
|
|
133
|
+
else if (entry.startsWith('~/')) {
|
|
134
|
+
expanded = node_path_1.default.join(node_os_1.default.homedir(), entry.slice(2));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
expanded = entry;
|
|
138
|
+
}
|
|
139
|
+
if (!node_path_1.default.isAbsolute(expanded))
|
|
140
|
+
continue; // reject project-relative
|
|
141
|
+
// Canonicalize: resolve symlinks and normalise case. If the path doesn't
|
|
142
|
+
// exist or can't be read, skip it — a non-existent root is not trustworthy.
|
|
143
|
+
let real;
|
|
144
|
+
try {
|
|
145
|
+
real = node_fs_1.default.realpathSync(expanded);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue; // non-existent or unreadable — skip
|
|
149
|
+
}
|
|
150
|
+
// Reject dangerously broad roots: filesystem root (e.g. '/' or 'C:\' or UNC '\\server\share').
|
|
151
|
+
// Normalize both sides by stripping trailing path separators before comparing so that
|
|
152
|
+
// Windows UNC shares (where path.parse().root includes a trailing separator) are caught.
|
|
153
|
+
const stripTrailingSep = (p) => p.replace(/[\\/]+$/, '');
|
|
154
|
+
if (stripTrailingSep(node_path_1.default.parse(real).root) === stripTrailingSep(real))
|
|
155
|
+
continue;
|
|
156
|
+
// Reject homedir itself (canonical compare closes case-insensitive bypass).
|
|
157
|
+
// Apply stripTrailingSep for robustness on platforms where realpathSync may
|
|
158
|
+
// or may not include a trailing separator on the homedir path.
|
|
159
|
+
if (stripTrailingSep(real) === stripTrailingSep(realHome))
|
|
160
|
+
continue;
|
|
161
|
+
if (seen.has(real))
|
|
162
|
+
continue;
|
|
163
|
+
seen.add(real);
|
|
164
|
+
result.push(real);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
95
168
|
/**
|
|
96
169
|
* Validate a file path and throw on traversal attempt.
|
|
97
170
|
* Convenience wrapper around validatePath for use in CLI commands.
|
|
@@ -153,6 +153,7 @@ function projectManagedHookCommand({ absoluteRunner, scriptPath, runtime = 'gene
|
|
|
153
153
|
const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
|
|
154
154
|
'settings-json': new Set([
|
|
155
155
|
'gsd-check-update.js',
|
|
156
|
+
'gsd-config-reload.js',
|
|
156
157
|
'gsd-statusline.js',
|
|
157
158
|
'gsd-context-monitor.js',
|
|
158
159
|
'gsd-prompt-guard.js',
|
|
@@ -168,6 +169,7 @@ const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
|
|
|
168
169
|
const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
|
|
169
170
|
'settings-json': new Set([
|
|
170
171
|
'gsd-check-update.js',
|
|
172
|
+
'gsd-config-reload.js',
|
|
171
173
|
'gsd-statusline.js',
|
|
172
174
|
'gsd-context-monitor.js',
|
|
173
175
|
'gsd-prompt-guard.js',
|
|
@@ -188,6 +190,10 @@ const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
|
|
|
188
190
|
// reconcileCodexHooksJsonSessionStart can replace stale node-runner commands
|
|
189
191
|
// with the .cmd shim on reinstall (and vice-versa on cross-platform moves).
|
|
190
192
|
'gsd-check-update.cmd',
|
|
193
|
+
// #772: context-monitor is now registered for Codex SubagentStart/Stop/PostToolUse.
|
|
194
|
+
'gsd-context-monitor.js',
|
|
195
|
+
// #772: Windows .cmd shim for gsd-context-monitor — same #3426 pattern.
|
|
196
|
+
'gsd-context-monitor.cmd',
|
|
191
197
|
]),
|
|
192
198
|
};
|
|
193
199
|
const LEGACY_MANAGED_HOOK_ALIASES_BY_SURFACE = {
|
|
@@ -381,6 +387,7 @@ function execGit(args, opts = {}) {
|
|
|
381
387
|
encoding: 'utf-8',
|
|
382
388
|
stdio: 'pipe',
|
|
383
389
|
timeout: opts.timeout ?? 10_000,
|
|
390
|
+
windowsHide: true,
|
|
384
391
|
});
|
|
385
392
|
return _spawnResult(result, 'git');
|
|
386
393
|
}
|
|
@@ -391,6 +398,7 @@ function execNpm(args, opts = {}) {
|
|
|
391
398
|
encoding: 'utf-8',
|
|
392
399
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
393
400
|
timeout: opts.timeout ?? 15_000,
|
|
401
|
+
windowsHide: true,
|
|
394
402
|
});
|
|
395
403
|
return _spawnResult(result, 'npm');
|
|
396
404
|
}
|
|
@@ -401,6 +409,7 @@ function execTool(program, args, opts = {}) {
|
|
|
401
409
|
encoding: 'utf-8',
|
|
402
410
|
stdio: 'pipe',
|
|
403
411
|
timeout: opts.timeout ?? 30_000,
|
|
412
|
+
windowsHide: true,
|
|
404
413
|
});
|
|
405
414
|
return _spawnResult(result, program);
|
|
406
415
|
}
|