get-shit-done-cc 1.41.0 → 1.41.1
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/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.md +2 -0
- package/bin/install.js +445 -19
- package/get-shit-done/bin/gsd-tools.cjs +47 -7
- package/get-shit-done/bin/lib/config-schema.cjs +4 -0
- package/get-shit-done/bin/lib/core.cjs +8 -95
- package/get-shit-done/bin/lib/model-catalog.cjs +92 -0
- package/get-shit-done/bin/lib/model-profiles.cjs +12 -180
- package/get-shit-done/bin/lib/phase.cjs +130 -21
- package/get-shit-done/bin/lib/state.cjs +122 -26
- package/get-shit-done/workflows/code-review.md +10 -4
- package/get-shit-done/workflows/execute-phase.md +39 -0
- package/get-shit-done/workflows/plant-seed.md +144 -87
- package/get-shit-done/workflows/settings-advanced.md +1 -0
- package/package.json +2 -1
- package/scripts/build-hooks.js +18 -23
- package/sdk/dist/model-catalog.d.ts +31 -0
- package/sdk/dist/model-catalog.d.ts.map +1 -0
- package/sdk/dist/model-catalog.js +31 -0
- package/sdk/dist/model-catalog.js.map +1 -0
- package/sdk/dist/plan-parser.d.ts +4 -0
- package/sdk/dist/plan-parser.d.ts.map +1 -1
- package/sdk/dist/plan-parser.js +7 -3
- package/sdk/dist/plan-parser.js.map +1 -1
- package/sdk/dist/query/config-query.d.ts +1 -12
- package/sdk/dist/query/config-query.d.ts.map +1 -1
- package/sdk/dist/query/config-query.js +7 -41
- package/sdk/dist/query/config-query.js.map +1 -1
- package/sdk/dist/query/config-schema.d.ts.map +1 -1
- package/sdk/dist/query/config-schema.js +6 -0
- package/sdk/dist/query/config-schema.js.map +1 -1
- package/sdk/dist/query/frontmatter.d.ts +4 -1
- package/sdk/dist/query/frontmatter.d.ts.map +1 -1
- package/sdk/dist/query/frontmatter.js +5 -7
- package/sdk/dist/query/frontmatter.js.map +1 -1
- package/sdk/dist/query/helpers.d.ts +2 -5
- package/sdk/dist/query/helpers.d.ts.map +1 -1
- package/sdk/dist/query/helpers.js +6 -7
- package/sdk/dist/query/helpers.js.map +1 -1
- package/sdk/dist/query/phase-lifecycle.d.ts +4 -1
- package/sdk/dist/query/phase-lifecycle.d.ts.map +1 -1
- package/sdk/dist/query/phase-lifecycle.js +115 -54
- package/sdk/dist/query/phase-lifecycle.js.map +1 -1
- package/sdk/dist/query/phase.d.ts.map +1 -1
- package/sdk/dist/query/phase.js +125 -28
- package/sdk/dist/query/phase.js.map +1 -1
- package/sdk/dist/query/query-dispatch.d.ts.map +1 -1
- package/sdk/dist/query/query-dispatch.js +9 -0
- package/sdk/dist/query/query-dispatch.js.map +1 -1
- package/sdk/dist/query/state.d.ts.map +1 -1
- package/sdk/dist/query/state.js +36 -14
- package/sdk/dist/query/state.js.map +1 -1
- package/sdk/dist/session-runner.d.ts.map +1 -1
- package/sdk/dist/session-runner.js +9 -6
- package/sdk/dist/session-runner.js.map +1 -1
- package/sdk/dist/types.d.ts +6 -0
- package/sdk/dist/types.d.ts.map +1 -1
- package/sdk/dist/types.js.map +1 -1
- package/sdk/package-lock.json +2 -2
- package/sdk/package.json +2 -1
- package/sdk/shared/model-catalog.json +122 -0
- package/sdk/src/model-catalog.ts +70 -0
- package/sdk/src/plan-parser.test.ts +51 -0
- package/sdk/src/plan-parser.ts +7 -3
- package/sdk/src/query/config-query.test.ts +37 -12
- package/sdk/src/query/config-query.ts +8 -45
- package/sdk/src/query/config-schema.ts +6 -0
- package/sdk/src/query/frontmatter.test.ts +48 -3
- package/sdk/src/query/frontmatter.ts +5 -7
- package/sdk/src/query/helpers.test.ts +3 -1
- package/sdk/src/query/helpers.ts +6 -10
- package/sdk/src/query/phase-lifecycle.test.ts +200 -0
- package/sdk/src/query/phase-lifecycle.ts +123 -54
- package/sdk/src/query/phase.test.ts +201 -0
- package/sdk/src/query/phase.ts +154 -29
- package/sdk/src/query/query-dispatch.test.ts +210 -1
- package/sdk/src/query/query-dispatch.ts +15 -0
- package/sdk/src/query/state.test.ts +207 -0
- package/sdk/src/query/state.ts +35 -14
- package/sdk/src/session-runner.ts +8 -6
- package/sdk/src/types.ts +6 -0
- package/sdk-bundle/gsd-sdk.tgz +0 -0
package/agents/gsd-code-fixer.md
CHANGED
|
@@ -153,7 +153,7 @@ Each finding starts with:
|
|
|
153
153
|
### {ID}: {Title}
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
Where ID matches: `CR-\d+` (Critical), `WR-\d+` (Warning), or `IN-\d+` (Info)
|
|
156
|
+
Where ID matches: `CR-\d+` or `BL-\d+` (Critical-tier-equivalent), `WR-\d+` (Warning), or `IN-\d+` (Info)
|
|
157
157
|
|
|
158
158
|
**Required Fields:**
|
|
159
159
|
|
|
@@ -387,7 +387,7 @@ Read `./CLAUDE.md` and check for `.claude/skills/` or `.agents/skills/` (as desc
|
|
|
387
387
|
|
|
388
388
|
For each finding, extract:
|
|
389
389
|
- `id`: Finding identifier (e.g., CR-01, WR-03, IN-12)
|
|
390
|
-
- `severity`: Critical (CR-*), Warning (WR-*), Info (IN-*)
|
|
390
|
+
- `severity`: Critical (CR-* or BL-*), Warning (WR-*), Info (IN-*)
|
|
391
391
|
- `title`: Issue title from `### ` heading
|
|
392
392
|
- `file`: Primary file path from **File:** line
|
|
393
393
|
- `files`: ALL file paths referenced in finding (including in Fix section) — for multi-file fixes
|
|
@@ -396,11 +396,11 @@ For each finding, extract:
|
|
|
396
396
|
- `fix`: Full fix content from **Fix:** section (may be multi-line, may contain code fences)
|
|
397
397
|
|
|
398
398
|
**2. Filter by fix_scope:**
|
|
399
|
-
- If `fix_scope == "critical_warning"`: include only CR
|
|
400
|
-
- If `fix_scope == "all"`: include CR-*, WR-*, and IN-* findings
|
|
399
|
+
- If `fix_scope == "critical_warning"`: include only CR-*, BL-*, and WR-* findings
|
|
400
|
+
- If `fix_scope == "all"`: include CR-*, BL-*, WR-*, and IN-* findings
|
|
401
401
|
|
|
402
402
|
**3. Sort findings by severity:**
|
|
403
|
-
- Critical first, then Warning, then Info
|
|
403
|
+
- Critical (CR-* and BL-*) first, then Warning, then Info
|
|
404
404
|
- Within same severity, maintain document order
|
|
405
405
|
|
|
406
406
|
**4. Count findings in scope:**
|
|
@@ -269,6 +269,8 @@ status: clean | issues_found
|
|
|
269
269
|
---
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
+
**Label equivalence:** The canonical frontmatter key is `critical:`. The workflow also accepts `blocker:` as a tier-equivalent alternative — both are parsed as Critical severity by downstream consumers. Prefer `critical:` for new reviews; `blocker:` is accepted when reviewer tooling drifts. Similarly, finding IDs beginning with `BL-` are treated as Critical-tier-equivalent to `CR-` IDs by the fixer and pipeline; prefer `CR-` as the canonical prefix.
|
|
273
|
+
|
|
272
274
|
The `files_reviewed_list` field is REQUIRED — it preserves the exact file scope for downstream consumers (e.g., --auto re-review in code-review-fix workflow). List every file that was reviewed, one per line in YAML list format.
|
|
273
275
|
|
|
274
276
|
**3. Body structure:**
|
package/bin/install.js
CHANGED
|
@@ -3689,23 +3689,44 @@ function parseTomlValue(text, i) {
|
|
|
3689
3689
|
}
|
|
3690
3690
|
}
|
|
3691
3691
|
|
|
3692
|
-
// Number
|
|
3693
|
-
//
|
|
3694
|
-
//
|
|
3695
|
-
|
|
3692
|
+
// Number — integer or TOML 1.0 float. (#2760 CR4 finding 3 required explicit
|
|
3693
|
+
// rejection of floats; #3245 inverts that: Codex CLI's serde schema requires
|
|
3694
|
+
// f64 for tool_timeout_sec / startup_timeout_sec, so integers are what Codex
|
|
3695
|
+
// rejects. Accept TOML floats and store as JS Number.)
|
|
3696
|
+
//
|
|
3697
|
+
// Still rejected: date/time literals (`-`, `:`, `T`, `Z` after integer prefix)
|
|
3698
|
+
// and hex/oct/bin literals (`0x`, `0o`, `0b` — `x`, `o`, `b` fall through to
|
|
3699
|
+
// the unsupported-value throw below because the integer-part pattern won't match `x`).
|
|
3700
|
+
// TOML 1.0 §2: underscores in numeric literals are only allowed BETWEEN
|
|
3701
|
+
// digits (each underscore must have a digit on both sides). The pre-check
|
|
3702
|
+
// regex uses (?:_?\d)* rather than [\d_]* so `1__0`, `1_.0`, and `1._0`
|
|
3703
|
+
// are rejected before normalization silently hides them.
|
|
3704
|
+
//
|
|
3705
|
+
// TOML 1.0 §2 (integer part): the integer part of a number must follow
|
|
3706
|
+
// decimal-integer rules — no leading zeros except the value 0 itself.
|
|
3707
|
+
// `01`, `00`, `01.5`, `00e2`, `+01`, `-01` are therefore all invalid.
|
|
3708
|
+
// The pre-check and float regexes use (0|[1-9](?:_?\d)*) for the integer
|
|
3709
|
+
// part so that `01` and `00` are rejected (k021 sibling rule).
|
|
3710
|
+
const numMatch = text.slice(i).match(/^[+-]?(0|[1-9](?:_?\d)*)/);
|
|
3696
3711
|
if (numMatch) {
|
|
3697
|
-
const
|
|
3698
|
-
// Reject
|
|
3699
|
-
|
|
3700
|
-
if (after !== undefined && /[.eE:\-TZ]/.test(after)) {
|
|
3712
|
+
const afterInt = text[i + numMatch[0].length];
|
|
3713
|
+
// Reject date/time separators that cannot be part of a float.
|
|
3714
|
+
if (afterInt !== undefined && /[:\-TZ]/.test(afterInt)) {
|
|
3701
3715
|
throw new Error(
|
|
3702
|
-
`unsupported TOML value at offset ${i}:
|
|
3716
|
+
`unsupported TOML value at offset ${i}: dates and times are not supported (got ${text.slice(i, i + 20)})`
|
|
3703
3717
|
);
|
|
3704
3718
|
}
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3719
|
+
// Accept float: optional decimal part, optional exponent part.
|
|
3720
|
+
// Each segment uses (?:_?\d)* so underscores are only between digits.
|
|
3721
|
+
// Integer part uses (0|[1-9](?:_?\d)*) to reject leading zeros per TOML 1.0.
|
|
3722
|
+
const floatMatch = text.slice(i).match(
|
|
3723
|
+
/^[+-]?(0|[1-9](?:_?\d)*)(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/
|
|
3724
|
+
);
|
|
3725
|
+
const raw = floatMatch ? floatMatch[0] : numMatch[0];
|
|
3726
|
+
const normalized = raw.replace(/_/g, '');
|
|
3727
|
+
const n = Number(normalized);
|
|
3728
|
+
if (!Number.isFinite(n)) throw new Error(`invalid number: ${raw}`);
|
|
3729
|
+
return { value: n, end: i + raw.length };
|
|
3709
3730
|
}
|
|
3710
3731
|
|
|
3711
3732
|
throw new Error(`unsupported value at offset ${i}: ${text.slice(i, i + 20)}`);
|
|
@@ -4172,14 +4193,24 @@ function rewriteTomlKeyLines(content, matches, key) {
|
|
|
4172
4193
|
* write leaves the temp file (which we clean up) but never truncates the
|
|
4173
4194
|
* original target. Used for any mutation of Codex config.toml so we cannot
|
|
4174
4195
|
* leave the user with a half-written file (#2760 fix 4).
|
|
4196
|
+
*
|
|
4197
|
+
* Every temp path written is recorded in __atomicWrittenTmps so that
|
|
4198
|
+
* _cleanTmpFiles() can scope cleanup to files this installer process actually
|
|
4199
|
+
* created, avoiding accidental deletion of unrelated tools' temp files.
|
|
4175
4200
|
*/
|
|
4176
4201
|
let __atomicWriteCounter = 0;
|
|
4202
|
+
// Set<string> — absolute paths of .tmp-<pid>-<n> files this process created.
|
|
4203
|
+
const __atomicWrittenTmps = new Set();
|
|
4177
4204
|
function atomicWriteFileSync(target, data, options) {
|
|
4178
4205
|
__atomicWriteCounter += 1;
|
|
4179
4206
|
const tmp = `${target}.tmp-${process.pid}-${__atomicWriteCounter}`;
|
|
4207
|
+
__atomicWrittenTmps.add(tmp);
|
|
4180
4208
|
try {
|
|
4181
4209
|
fs.writeFileSync(tmp, data, options);
|
|
4182
4210
|
fs.renameSync(tmp, target);
|
|
4211
|
+
// Successful rename: the tmp path no longer exists, but leave it in the
|
|
4212
|
+
// Set so _cleanTmpFiles can recognise it as installer-owned if it somehow
|
|
4213
|
+
// lingers (e.g. a rename succeeded but left a stale entry on some FS).
|
|
4183
4214
|
} catch (e) {
|
|
4184
4215
|
// Best-effort cleanup of the partial temp file; never mask the real error.
|
|
4185
4216
|
try { fs.rmSync(tmp, { force: true }); } catch (_) { /* ignore */ }
|
|
@@ -7436,6 +7467,176 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
7436
7467
|
// Clean up orphaned files from previous versions
|
|
7437
7468
|
cleanupOrphanedFiles(targetDir);
|
|
7438
7469
|
|
|
7470
|
+
// #3245 — Codex idempotent rollback. Capture pre-install state of ALL
|
|
7471
|
+
// directories and files GSD will mutate so that any post-install validation
|
|
7472
|
+
// failure (config.toml schema check, write failure, etc.) can revert the
|
|
7473
|
+
// entire install atomically — not just config.toml.
|
|
7474
|
+
//
|
|
7475
|
+
// Captured BEFORE the first Codex-specific write (skills/) so the snapshots
|
|
7476
|
+
// reflect the true pre-GSD state. Non-Codex runtimes skip this block.
|
|
7477
|
+
//
|
|
7478
|
+
// Snapshot contents:
|
|
7479
|
+
// codexPreInstallSkillNames — Set of gsd-* skill dir names that existed
|
|
7480
|
+
// codexPreInstallSkillContents — Map<skillName, Map<relPath, Buffer>> of
|
|
7481
|
+
// the full file tree of each pre-existing gsd-* skill dir, so that
|
|
7482
|
+
// overwritten dirs can be fully restored on rollback (not just removed).
|
|
7483
|
+
// codexPreInstallAgentFiles — Set of gsd-*.{md,toml} filenames in agents/
|
|
7484
|
+
// codexPreInstallAgentContents — Map<filename, Buffer> of pre-existing agent
|
|
7485
|
+
// file bytes, enabling full content restore (not just deletion) on rollback.
|
|
7486
|
+
// codexPreInstallVersionBytes — Buffer (or null) of get-shit-done/VERSION
|
|
7487
|
+
//
|
|
7488
|
+
// These are referenced by restoreCodexSnapshot(), defined below inside the
|
|
7489
|
+
// config block. Defining the variables here (outer scope) makes them
|
|
7490
|
+
// accessible by closure.
|
|
7491
|
+
const codexPreInstallSkillNames = new Set();
|
|
7492
|
+
// Map<skillDirName, Map<relPath, Buffer>> — full content snapshot of each
|
|
7493
|
+
// pre-existing gsd-* skill directory. Best-effort: read errors are silently
|
|
7494
|
+
// skipped so a partial snapshot is still better than none.
|
|
7495
|
+
const codexPreInstallSkillContents = new Map();
|
|
7496
|
+
const codexPreInstallAgentFiles = new Set();
|
|
7497
|
+
// Map<filename, Buffer> — content snapshot of each pre-existing gsd-* agent file.
|
|
7498
|
+
const codexPreInstallAgentContents = new Map();
|
|
7499
|
+
let codexPreInstallVersionBytes = null;
|
|
7500
|
+
if (isCodex && !isMinimalMode(installMode)) {
|
|
7501
|
+
const _preSkillsDir = path.join(targetDir, 'skills');
|
|
7502
|
+
if (fs.existsSync(_preSkillsDir)) {
|
|
7503
|
+
for (const entry of fs.readdirSync(_preSkillsDir, { withFileTypes: true })) {
|
|
7504
|
+
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
7505
|
+
codexPreInstallSkillNames.add(entry.name);
|
|
7506
|
+
// Recursively snapshot all files in this skill dir.
|
|
7507
|
+
const skillDir = path.join(_preSkillsDir, entry.name);
|
|
7508
|
+
const fileMap = new Map();
|
|
7509
|
+
const _snapshotDir = (dir, relBase) => {
|
|
7510
|
+
let children;
|
|
7511
|
+
try { children = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
7512
|
+
for (const child of children) {
|
|
7513
|
+
const relPath = relBase ? `${relBase}/${child.name}` : child.name;
|
|
7514
|
+
const fullPath = path.join(dir, child.name);
|
|
7515
|
+
if (child.isDirectory()) {
|
|
7516
|
+
_snapshotDir(fullPath, relPath);
|
|
7517
|
+
} else {
|
|
7518
|
+
try { fileMap.set(relPath, fs.readFileSync(fullPath)); } catch (_) { /* best-effort */ }
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
};
|
|
7522
|
+
_snapshotDir(skillDir, '');
|
|
7523
|
+
codexPreInstallSkillContents.set(entry.name, fileMap);
|
|
7524
|
+
}
|
|
7525
|
+
}
|
|
7526
|
+
}
|
|
7527
|
+
const _preAgentsDir = path.join(targetDir, 'agents');
|
|
7528
|
+
if (fs.existsSync(_preAgentsDir)) {
|
|
7529
|
+
for (const file of fs.readdirSync(_preAgentsDir)) {
|
|
7530
|
+
if (file.startsWith('gsd-') && (file.endsWith('.md') || file.endsWith('.toml'))) {
|
|
7531
|
+
codexPreInstallAgentFiles.add(file);
|
|
7532
|
+
try {
|
|
7533
|
+
codexPreInstallAgentContents.set(file, fs.readFileSync(path.join(_preAgentsDir, file)));
|
|
7534
|
+
} catch (_) { /* best-effort */ }
|
|
7535
|
+
}
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
const _preVersionPath = path.join(targetDir, 'get-shit-done', 'VERSION');
|
|
7539
|
+
if (fs.existsSync(_preVersionPath)) {
|
|
7540
|
+
try { codexPreInstallVersionBytes = fs.readFileSync(_preVersionPath); } catch (_) { /* best-effort */ }
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
|
|
7544
|
+
// #3245 CR finding 2 — Rollback coverage extends to ALL post-snapshot operations,
|
|
7545
|
+
// not just the Codex config/hook error paths. Any throw between snapshot capture and
|
|
7546
|
+
// the Codex config block (skills copy, agents copy, VERSION write, manifest write, etc.)
|
|
7547
|
+
// must also trigger rollback so the caller is never left in a partially-installed state.
|
|
7548
|
+
//
|
|
7549
|
+
// _codexPreConfigRollback covers the four surfaces that can be mutated before
|
|
7550
|
+
// config.toml is touched: skills/, agents/, get-shit-done/VERSION, and orphaned
|
|
7551
|
+
// atomic-write temp files. It is safe to call before any writes have happened.
|
|
7552
|
+
// The full restoreCodexSnapshot() (defined inside the config block) additionally
|
|
7553
|
+
// handles config.toml, which is not yet touched at this point in the pipeline.
|
|
7554
|
+
const _codexPreConfigRollback = !isCodex || isMinimalMode(installMode) ? null : () => {
|
|
7555
|
+
// skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
|
|
7556
|
+
const _earlySkillsDir = path.join(targetDir, 'skills');
|
|
7557
|
+
for (const skillName of codexPreInstallSkillNames) {
|
|
7558
|
+
const skillDirPath = path.join(_earlySkillsDir, skillName);
|
|
7559
|
+
const fileMap = codexPreInstallSkillContents.get(skillName);
|
|
7560
|
+
try {
|
|
7561
|
+
fs.rmSync(skillDirPath, { recursive: true, force: true });
|
|
7562
|
+
fs.mkdirSync(skillDirPath, { recursive: true });
|
|
7563
|
+
if (fileMap) {
|
|
7564
|
+
for (const [relPath, buf] of fileMap) {
|
|
7565
|
+
const destFile = path.join(skillDirPath, relPath);
|
|
7566
|
+
try {
|
|
7567
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
7568
|
+
fs.writeFileSync(destFile, buf);
|
|
7569
|
+
} catch (_) { /* best-effort */ }
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
} catch (_) { /* best-effort */ }
|
|
7573
|
+
}
|
|
7574
|
+
// skills/gsd-* — pass 2: remove any newly-created dirs not in the snapshot.
|
|
7575
|
+
if (fs.existsSync(_earlySkillsDir)) {
|
|
7576
|
+
try {
|
|
7577
|
+
for (const entry of fs.readdirSync(_earlySkillsDir, { withFileTypes: true })) {
|
|
7578
|
+
if (entry.isDirectory() && entry.name.startsWith('gsd-') && !codexPreInstallSkillNames.has(entry.name)) {
|
|
7579
|
+
try { fs.rmSync(path.join(_earlySkillsDir, entry.name), { recursive: true, force: true }); }
|
|
7580
|
+
catch (_) { /* best-effort */ }
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7583
|
+
} catch (_) { /* best-effort */ }
|
|
7584
|
+
}
|
|
7585
|
+
// agents/gsd-* — pass 1: restore snapshot entries.
|
|
7586
|
+
const _earlyAgentsDir = path.join(targetDir, 'agents');
|
|
7587
|
+
for (const file of codexPreInstallAgentFiles) {
|
|
7588
|
+
const buf = codexPreInstallAgentContents.get(file);
|
|
7589
|
+
if (buf !== undefined) {
|
|
7590
|
+
try {
|
|
7591
|
+
fs.mkdirSync(_earlyAgentsDir, { recursive: true });
|
|
7592
|
+
fs.writeFileSync(path.join(_earlyAgentsDir, file), buf);
|
|
7593
|
+
} catch (_) { /* best-effort */ }
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
// agents/gsd-* — pass 2: remove any newly-created files not in the snapshot.
|
|
7597
|
+
if (fs.existsSync(_earlyAgentsDir)) {
|
|
7598
|
+
try {
|
|
7599
|
+
for (const file of fs.readdirSync(_earlyAgentsDir)) {
|
|
7600
|
+
if (file.startsWith('gsd-') && (file.endsWith('.md') || file.endsWith('.toml')) && !codexPreInstallAgentFiles.has(file)) {
|
|
7601
|
+
try { fs.unlinkSync(path.join(_earlyAgentsDir, file)); } catch (_) { /* best-effort */ }
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7604
|
+
} catch (_) { /* best-effort */ }
|
|
7605
|
+
}
|
|
7606
|
+
// get-shit-done/VERSION
|
|
7607
|
+
const _earlyVersionPath = path.join(targetDir, 'get-shit-done', 'VERSION');
|
|
7608
|
+
if (codexPreInstallVersionBytes !== null) {
|
|
7609
|
+
try { fs.writeFileSync(_earlyVersionPath, codexPreInstallVersionBytes); } catch (_) { /* best-effort */ }
|
|
7610
|
+
} else if (fs.existsSync(_earlyVersionPath)) {
|
|
7611
|
+
try { fs.unlinkSync(_earlyVersionPath); } catch (_) { /* best-effort */ }
|
|
7612
|
+
}
|
|
7613
|
+
// Orphaned atomic-write temp files.
|
|
7614
|
+
const _earlyTmpPattern = /\.tmp-\d+-\d+$/;
|
|
7615
|
+
function _earlyCleanTmpFiles(dir) {
|
|
7616
|
+
if (!fs.existsSync(dir)) return;
|
|
7617
|
+
let entries;
|
|
7618
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
7619
|
+
for (const entry of entries) {
|
|
7620
|
+
const full = path.join(dir, entry.name);
|
|
7621
|
+
if (entry.isDirectory()) {
|
|
7622
|
+
_earlyCleanTmpFiles(full);
|
|
7623
|
+
} else if (_earlyTmpPattern.test(entry.name) && __atomicWrittenTmps.has(full)) {
|
|
7624
|
+
try { fs.unlinkSync(full); } catch (_) { /* best-effort */ }
|
|
7625
|
+
}
|
|
7626
|
+
}
|
|
7627
|
+
}
|
|
7628
|
+
_earlyCleanTmpFiles(targetDir);
|
|
7629
|
+
};
|
|
7630
|
+
|
|
7631
|
+
// #3245 CR finding 2 — wrap the pre-config install operations in a try/catch so
|
|
7632
|
+
// that ANY throw between snapshot capture and the Codex config block triggers rollback.
|
|
7633
|
+
// Non-Codex paths are unaffected (_codexPreConfigRollback is null for them).
|
|
7634
|
+
//
|
|
7635
|
+
// agentsSrc is declared here (let, not const) because installCodexConfig() inside the
|
|
7636
|
+
// Codex config block below also references it, and that block is outside the try scope.
|
|
7637
|
+
let agentsSrc = path.join(src, 'agents');
|
|
7638
|
+
try {
|
|
7639
|
+
|
|
7439
7640
|
// OpenCode/Kilo use command/ (flat), Codex uses skills/, Claude/Gemini use commands/gsd/
|
|
7440
7641
|
if (isOpencode || isKilo) {
|
|
7441
7642
|
// OpenCode/Kilo: flat structure in command/ directory
|
|
@@ -7754,7 +7955,9 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
7754
7955
|
// Skipped under --minimal: gsd-* subagent descriptions are eagerly loaded
|
|
7755
7956
|
// into the runtime's Agent tool schema, costing ~6k tokens per turn even
|
|
7756
7957
|
// when no GSD workflow is active. See gsd-build/get-shit-done#2762.
|
|
7757
|
-
|
|
7958
|
+
// Note: agentsSrc is declared as let before the enclosing try block so it
|
|
7959
|
+
// is accessible by installCodexConfig() in the Codex config section below.
|
|
7960
|
+
agentsSrc = path.join(src, 'agents');
|
|
7758
7961
|
const agentsDest = path.join(targetDir, 'agents');
|
|
7759
7962
|
|
|
7760
7963
|
// Always remove stale gsd-* agents first so re-installing with
|
|
@@ -8028,6 +8231,16 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
8028
8231
|
}
|
|
8029
8232
|
}
|
|
8030
8233
|
|
|
8234
|
+
} catch (_earlyInstallErr) {
|
|
8235
|
+
// #3245 CR finding 2 — any throw in the pre-config install operations (skills copy,
|
|
8236
|
+
// agents copy, VERSION write, manifest write, etc.) triggers the Codex pre-config
|
|
8237
|
+
// rollback so the caller is never left in a partially-installed state.
|
|
8238
|
+
if (_codexPreConfigRollback) {
|
|
8239
|
+
_codexPreConfigRollback();
|
|
8240
|
+
}
|
|
8241
|
+
throw _earlyInstallErr;
|
|
8242
|
+
}
|
|
8243
|
+
|
|
8031
8244
|
if (isCodex && !isMinimalMode(installMode)) {
|
|
8032
8245
|
// Capture pre-install snapshot of config.toml before ANY GSD mutation
|
|
8033
8246
|
// (#2760 fix 3). On post-write schema-validation failure OR any throw
|
|
@@ -8041,13 +8254,129 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
8041
8254
|
? fs.readFileSync(codexConfigPathPreInstall)
|
|
8042
8255
|
: null;
|
|
8043
8256
|
|
|
8257
|
+
// #3245 — unified idempotent rollback. Reverts ALL Codex-specific mutations:
|
|
8258
|
+
// config.toml — restore pre-install bytes (or remove if was absent)
|
|
8259
|
+
// skills/gsd-* — restore pre-existing dirs from content snapshot; remove
|
|
8260
|
+
// newly-created dirs (i.e. those not in the pre-install Set)
|
|
8261
|
+
// agents/gsd-* — restore pre-existing files from content snapshot; remove
|
|
8262
|
+
// newly-created files
|
|
8263
|
+
// get-shit-done/VERSION — restore or remove
|
|
8264
|
+
// *.tmp-* — best-effort cleanup of installer-owned atomic-write temps
|
|
8265
|
+
//
|
|
8266
|
+
// Safe to call multiple times (idempotent): each remove/write is guarded by
|
|
8267
|
+
// existence checks. Safe to call before any snapshots are captured (variables
|
|
8268
|
+
// default to empty Set / null). Does NOT touch non-gsd-* user content.
|
|
8044
8269
|
const restoreCodexSnapshot = () => {
|
|
8270
|
+
// 1. config.toml
|
|
8045
8271
|
if (codexConfigPreInstallSnapshot !== null) {
|
|
8046
8272
|
try { fs.writeFileSync(codexConfigPathPreInstall, codexConfigPreInstallSnapshot); }
|
|
8047
8273
|
catch (_) { /* best-effort restore — surface the original error */ }
|
|
8048
8274
|
} else if (fs.existsSync(codexConfigPathPreInstall)) {
|
|
8049
8275
|
try { fs.rmSync(codexConfigPathPreInstall); } catch (_) { /* best-effort */ }
|
|
8050
8276
|
}
|
|
8277
|
+
|
|
8278
|
+
// 2. skills/gsd-*
|
|
8279
|
+
// • Dirs that pre-existed: wipe current contents, restore snapshotted files.
|
|
8280
|
+
// The restore iterates the SNAPSHOT manifest (codexPreInstallSkillNames) rather
|
|
8281
|
+
// than just the current filesystem so that dirs deleted during the install
|
|
8282
|
+
// (copyCommandsAsCodexSkills removes pre-existing gsd-* dirs before re-writing)
|
|
8283
|
+
// are restored even when they are absent from disk at rollback time (#3245 CR).
|
|
8284
|
+
// • Dirs that did not pre-exist: remove entirely.
|
|
8285
|
+
const _rollbackSkillsDir = path.join(targetDir, 'skills');
|
|
8286
|
+
// Pass 1 — restore snapshot entries (may be absent from disk if deleted mid-install).
|
|
8287
|
+
for (const skillName of codexPreInstallSkillNames) {
|
|
8288
|
+
const skillDirPath = path.join(_rollbackSkillsDir, skillName);
|
|
8289
|
+
const fileMap = codexPreInstallSkillContents.get(skillName);
|
|
8290
|
+
try {
|
|
8291
|
+
fs.rmSync(skillDirPath, { recursive: true, force: true });
|
|
8292
|
+
fs.mkdirSync(skillDirPath, { recursive: true });
|
|
8293
|
+
if (fileMap) {
|
|
8294
|
+
for (const [relPath, buf] of fileMap) {
|
|
8295
|
+
const destFile = path.join(skillDirPath, relPath);
|
|
8296
|
+
try {
|
|
8297
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
8298
|
+
fs.writeFileSync(destFile, buf);
|
|
8299
|
+
} catch (_) { /* best-effort file restore */ }
|
|
8300
|
+
}
|
|
8301
|
+
}
|
|
8302
|
+
} catch (_) { /* best-effort dir restore */ }
|
|
8303
|
+
}
|
|
8304
|
+
// Pass 2 — remove any newly-created gsd-* dirs (not in the pre-install snapshot).
|
|
8305
|
+
if (fs.existsSync(_rollbackSkillsDir)) {
|
|
8306
|
+
try {
|
|
8307
|
+
for (const entry of fs.readdirSync(_rollbackSkillsDir, { withFileTypes: true })) {
|
|
8308
|
+
if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
|
|
8309
|
+
if (!codexPreInstallSkillNames.has(entry.name)) {
|
|
8310
|
+
// New dir written this session: remove entirely.
|
|
8311
|
+
try { fs.rmSync(path.join(_rollbackSkillsDir, entry.name), { recursive: true, force: true }); }
|
|
8312
|
+
catch (_) { /* best-effort */ }
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
} catch (_) { /* best-effort */ }
|
|
8316
|
+
}
|
|
8317
|
+
|
|
8318
|
+
// 3. agents/gsd-*.{md,toml}
|
|
8319
|
+
// • Files that pre-existed: restore bytes from content snapshot.
|
|
8320
|
+
// Iterates the SNAPSHOT manifest (codexPreInstallAgentFiles) so that files
|
|
8321
|
+
// deleted by the pre-copy stale-removal pass (lines 7862-7870) are restored
|
|
8322
|
+
// even when absent from disk at rollback time (#3245 CR).
|
|
8323
|
+
// • Files that did not pre-exist: remove.
|
|
8324
|
+
const _rollbackAgentsDir = path.join(targetDir, 'agents');
|
|
8325
|
+
// Pass 1 — restore snapshot entries (may be absent from disk if deleted mid-install).
|
|
8326
|
+
for (const file of codexPreInstallAgentFiles) {
|
|
8327
|
+
const buf = codexPreInstallAgentContents.get(file);
|
|
8328
|
+
if (buf !== undefined) {
|
|
8329
|
+
try {
|
|
8330
|
+
fs.mkdirSync(_rollbackAgentsDir, { recursive: true });
|
|
8331
|
+
fs.writeFileSync(path.join(_rollbackAgentsDir, file), buf);
|
|
8332
|
+
} catch (_) { /* best-effort */ }
|
|
8333
|
+
}
|
|
8334
|
+
}
|
|
8335
|
+
// Pass 2 — remove any newly-created gsd-* agent files (not in the pre-install snapshot).
|
|
8336
|
+
if (fs.existsSync(_rollbackAgentsDir)) {
|
|
8337
|
+
try {
|
|
8338
|
+
for (const file of fs.readdirSync(_rollbackAgentsDir)) {
|
|
8339
|
+
if (!file.startsWith('gsd-') || (!file.endsWith('.md') && !file.endsWith('.toml'))) continue;
|
|
8340
|
+
if (!codexPreInstallAgentFiles.has(file)) {
|
|
8341
|
+
// New file written this session: remove.
|
|
8342
|
+
try { fs.unlinkSync(path.join(_rollbackAgentsDir, file)); } catch (_) { /* best-effort */ }
|
|
8343
|
+
}
|
|
8344
|
+
}
|
|
8345
|
+
} catch (_) { /* best-effort */ }
|
|
8346
|
+
}
|
|
8347
|
+
|
|
8348
|
+
// 4. get-shit-done/VERSION
|
|
8349
|
+
const _rollbackVersionPath = path.join(targetDir, 'get-shit-done', 'VERSION');
|
|
8350
|
+
if (codexPreInstallVersionBytes !== null) {
|
|
8351
|
+
try { fs.writeFileSync(_rollbackVersionPath, codexPreInstallVersionBytes); }
|
|
8352
|
+
catch (_) { /* best-effort */ }
|
|
8353
|
+
} else if (fs.existsSync(_rollbackVersionPath)) {
|
|
8354
|
+
try { fs.unlinkSync(_rollbackVersionPath); } catch (_) { /* best-effort */ }
|
|
8355
|
+
}
|
|
8356
|
+
|
|
8357
|
+
// 5. Orphaned atomic-write temp files (<file>.tmp-<pid>-<n>) in targetDir.
|
|
8358
|
+
// These can accumulate if an atomic write fails mid-rename. Best-effort scan.
|
|
8359
|
+
//
|
|
8360
|
+
// Only delete temp files whose absolute path is in __atomicWrittenTmps —
|
|
8361
|
+
// the Set populated by atomicWriteFileSync for every temp this installer
|
|
8362
|
+
// process actually created. This scopes cleanup to installer-owned writes
|
|
8363
|
+
// and avoids clobbering unrelated tools' temp files that happen to match
|
|
8364
|
+
// the same *.tmp-<pid>-<n> suffix pattern.
|
|
8365
|
+
const _tmpPattern = /\.tmp-\d+-\d+$/;
|
|
8366
|
+
function _cleanTmpFiles(dir) {
|
|
8367
|
+
if (!fs.existsSync(dir)) return;
|
|
8368
|
+
let entries;
|
|
8369
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
8370
|
+
for (const entry of entries) {
|
|
8371
|
+
const full = path.join(dir, entry.name);
|
|
8372
|
+
if (entry.isDirectory()) {
|
|
8373
|
+
_cleanTmpFiles(full);
|
|
8374
|
+
} else if (_tmpPattern.test(entry.name) && __atomicWrittenTmps.has(full)) {
|
|
8375
|
+
try { fs.unlinkSync(full); } catch (_) { /* best-effort */ }
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
_cleanTmpFiles(targetDir);
|
|
8051
8380
|
};
|
|
8052
8381
|
|
|
8053
8382
|
let agentCount;
|
|
@@ -9437,8 +9766,14 @@ function installSdkIfNeeded(opts) {
|
|
|
9437
9766
|
// strictly weaker invariant than the one workflows depend on
|
|
9438
9767
|
// (`command -v gsd-sdk` resolving), and led to a false ✓ in npx-cache
|
|
9439
9768
|
// installs (issue #2775).
|
|
9769
|
+
//
|
|
9770
|
+
// #3231: strip transient npx-injected PATH segments before checking. The
|
|
9771
|
+
// installer subprocess PATH includes `~/.npm/_npx/<hash>/node_modules/.bin`
|
|
9772
|
+
// which is ephemeral — it is NOT reachable from the user's interactive
|
|
9773
|
+
// shell. A gsd-sdk found there must NOT count as "on PATH".
|
|
9440
9774
|
const shimSrc = path.resolve(__dirname, 'gsd-sdk.js');
|
|
9441
|
-
|
|
9775
|
+
const persistentPath = filterNpxFromPath(process.env.PATH || '');
|
|
9776
|
+
let onPath = isGsdSdkOnPath(persistentPath);
|
|
9442
9777
|
|
|
9443
9778
|
// Track WHERE we wrote the shim so the diagnostic can be specific even
|
|
9444
9779
|
// when isGsdSdkOnPath() returns false because the write target isn't on
|
|
@@ -9455,7 +9790,7 @@ function installSdkIfNeeded(opts) {
|
|
|
9455
9790
|
const linked = trySelfLinkGsdSdk(shimSrc);
|
|
9456
9791
|
if (linked) {
|
|
9457
9792
|
shimDir = path.dirname(linked);
|
|
9458
|
-
onPath = isGsdSdkOnPath();
|
|
9793
|
+
onPath = isGsdSdkOnPath(persistentPath);
|
|
9459
9794
|
if (onPath) {
|
|
9460
9795
|
console.log(` ${dim}↪ linked gsd-sdk → ${linked}${reset}`);
|
|
9461
9796
|
}
|
|
@@ -9470,13 +9805,23 @@ function installSdkIfNeeded(opts) {
|
|
|
9470
9805
|
// require the shim to be reachable there too before claiming ✓.
|
|
9471
9806
|
// POSIX-only probe; on Windows getUserShellPath() returns null and
|
|
9472
9807
|
// we trust the existing check (Windows-specific fix is separate).
|
|
9808
|
+
//
|
|
9809
|
+
// #3231: when getUserShellPath() returns null (e.g. $SHELL unset on
|
|
9810
|
+
// Linux, rc-file timeout), we cannot confirm persistent reachability.
|
|
9811
|
+
// In that case, do NOT preserve a true onPath — require the initial
|
|
9812
|
+
// check (on persistentPath) to have found the shim in a persistent
|
|
9813
|
+
// location. Since we already filtered npx dirs above, onPath=true here
|
|
9814
|
+
// means a non-transient dir has the shim, which is sufficient.
|
|
9473
9815
|
const userShellPath = getUserShellPath();
|
|
9474
9816
|
if (onPath && userShellPath !== null) {
|
|
9475
|
-
const
|
|
9817
|
+
const persistentUserShellPath = filterNpxFromPath(userShellPath);
|
|
9818
|
+
const userSees = isGsdSdkOnPath(persistentUserShellPath);
|
|
9476
9819
|
if (!userSees) {
|
|
9477
9820
|
onPath = false;
|
|
9478
9821
|
}
|
|
9479
9822
|
}
|
|
9823
|
+
// If userShellPath is null (POSIX probe failed), onPath reflects
|
|
9824
|
+
// the persistent-PATH check — that is the best available invariant.
|
|
9480
9825
|
|
|
9481
9826
|
if (onPath) {
|
|
9482
9827
|
console.log(` ${green}✓${reset} GSD SDK ready (sdk/dist/cli.js)`);
|
|
@@ -9517,6 +9862,74 @@ function installSdkIfNeeded(opts) {
|
|
|
9517
9862
|
}
|
|
9518
9863
|
}
|
|
9519
9864
|
|
|
9865
|
+
/**
|
|
9866
|
+
* #3231 helper: detect whether a `gsd-sdk` binary is the legacy deprecated
|
|
9867
|
+
* shim pointing at `gsd-tools.cjs`.
|
|
9868
|
+
*
|
|
9869
|
+
* Reads the first 512 bytes of the file and looks for the `@deprecated`
|
|
9870
|
+
* marker alongside a `gsd-tools.cjs` reference — the fingerprint that
|
|
9871
|
+
* distinguishes the old binary from the modern SDK. Treats any I/O error
|
|
9872
|
+
* (missing file, EACCES) as "not legacy" so callers do not need to guard.
|
|
9873
|
+
*
|
|
9874
|
+
* This is intentionally a plain-text sniff of the file header, not a
|
|
9875
|
+
* semantic parse — the marker is a stable, human-authored string that we
|
|
9876
|
+
* own. Returns false conservatively (prefer false positives to false
|
|
9877
|
+
* negatives: a non-legacy binary reported as legacy triggers a harmless
|
|
9878
|
+
* replacement; a legacy binary reported as non-legacy would keep the broken
|
|
9879
|
+
* shim in place).
|
|
9880
|
+
*/
|
|
9881
|
+
function isLegacyGsdSdkShim(filePath) {
|
|
9882
|
+
const fs = require('fs');
|
|
9883
|
+
try {
|
|
9884
|
+
const fd = fs.openSync(filePath, 'r');
|
|
9885
|
+
let header;
|
|
9886
|
+
try {
|
|
9887
|
+
const buf = Buffer.alloc(512);
|
|
9888
|
+
const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
|
|
9889
|
+
header = buf.slice(0, bytesRead).toString('utf8');
|
|
9890
|
+
} finally {
|
|
9891
|
+
try { fs.closeSync(fd); } catch {}
|
|
9892
|
+
}
|
|
9893
|
+
// The legacy binary contains "@deprecated" AND "gsd-tools.cjs" within
|
|
9894
|
+
// its first 512 bytes.
|
|
9895
|
+
return header.includes('@deprecated') && header.includes('gsd-tools.cjs');
|
|
9896
|
+
} catch {
|
|
9897
|
+
return false;
|
|
9898
|
+
}
|
|
9899
|
+
}
|
|
9900
|
+
|
|
9901
|
+
/**
|
|
9902
|
+
* #3231 helper: strip transient npx-injected PATH segments.
|
|
9903
|
+
*
|
|
9904
|
+
* npm/npx injects `~/.npm/_npx/<hash>/node_modules/.bin` (and equivalents)
|
|
9905
|
+
* into the installer subprocess PATH. Those directories are ephemeral — they
|
|
9906
|
+
* exist only for the duration of the `npx` run — and MUST NOT be treated as
|
|
9907
|
+
* evidence that `gsd-sdk` is durably reachable.
|
|
9908
|
+
*
|
|
9909
|
+
* Strips any segment whose absolute form contains `/_npx/` or `\\_npx\\`
|
|
9910
|
+
* as a proper path-component boundary. A user-named directory that merely
|
|
9911
|
+
* contains the substring "npx" (e.g. `/home/user/my-npx-scripts/bin`) is
|
|
9912
|
+
* preserved: we require the boundary characters (`/` or `\`) on both sides.
|
|
9913
|
+
*
|
|
9914
|
+
* Returns the filtered PATH string (may be empty if all segments were npx).
|
|
9915
|
+
*/
|
|
9916
|
+
function filterNpxFromPath(pathString) {
|
|
9917
|
+
const path = require('path');
|
|
9918
|
+
const input = typeof pathString === 'string' ? pathString : (process.env.PATH || '');
|
|
9919
|
+
return input
|
|
9920
|
+
.split(path.delimiter)
|
|
9921
|
+
.filter((seg) => {
|
|
9922
|
+
if (!seg) return false;
|
|
9923
|
+
// Normalize to forward-slash form for the pattern check so both
|
|
9924
|
+
// POSIX and Windows paths match a single expression. The sep-anchored
|
|
9925
|
+
// pattern avoids matching "my-npx-scripts" etc.
|
|
9926
|
+
const norm = seg.replace(/\\/g, '/');
|
|
9927
|
+
// Must have /_npx/ as a real path component, not just a substring.
|
|
9928
|
+
return !norm.includes('/_npx/');
|
|
9929
|
+
})
|
|
9930
|
+
.join(path.delimiter);
|
|
9931
|
+
}
|
|
9932
|
+
|
|
9520
9933
|
/**
|
|
9521
9934
|
* #2775 helper: check whether a callable `gsd-sdk` exists on a PATH.
|
|
9522
9935
|
*
|
|
@@ -9531,6 +9944,10 @@ function installSdkIfNeeded(opts) {
|
|
|
9531
9944
|
* shims). Callers can pass the user-shell PATH from getUserShellPath() to
|
|
9532
9945
|
* verify the shim is reachable from the runtime shell, not just the
|
|
9533
9946
|
* install context. Zero-arg form preserves existing behavior.
|
|
9947
|
+
*
|
|
9948
|
+
* #3231: a candidate that passes the file/exec check is further tested via
|
|
9949
|
+
* isLegacyGsdSdkShim — a symlink pointing at the deprecated gsd-tools.cjs
|
|
9950
|
+
* binary must NOT be treated as "on PATH" even if it is executable.
|
|
9534
9951
|
*/
|
|
9535
9952
|
function isGsdSdkOnPath(pathString) {
|
|
9536
9953
|
const path = require('path');
|
|
@@ -9547,8 +9964,15 @@ function isGsdSdkOnPath(pathString) {
|
|
|
9547
9964
|
try {
|
|
9548
9965
|
const st = fs.statSync(candidate);
|
|
9549
9966
|
if (st.isFile()) {
|
|
9550
|
-
if (process.platform === 'win32')
|
|
9551
|
-
|
|
9967
|
+
if (process.platform === 'win32') {
|
|
9968
|
+
if (!isLegacyGsdSdkShim(candidate)) return true;
|
|
9969
|
+
} else if ((st.mode & 0o111) !== 0) {
|
|
9970
|
+
// #3231: resolve symlink before sniffing, so we detect legacy
|
|
9971
|
+
// through any level of indirection.
|
|
9972
|
+
let target = candidate;
|
|
9973
|
+
try { target = fs.realpathSync(candidate); } catch {}
|
|
9974
|
+
if (!isLegacyGsdSdkShim(target)) return true;
|
|
9975
|
+
}
|
|
9552
9976
|
}
|
|
9553
9977
|
} catch {
|
|
9554
9978
|
// missing / EACCES on dir — keep scanning.
|
|
@@ -10040,6 +10464,8 @@ if (process.env.GSD_TEST_MODE) {
|
|
|
10040
10464
|
trySelfLinkGsdSdkWindows,
|
|
10041
10465
|
buildWindowsShimTriple,
|
|
10042
10466
|
formatSdkPathDiagnostic,
|
|
10467
|
+
filterNpxFromPath,
|
|
10468
|
+
isLegacyGsdSdkShim,
|
|
10043
10469
|
isGsdSdkOnPath,
|
|
10044
10470
|
getUserShellPath,
|
|
10045
10471
|
homePathCoveredByRc,
|