@boyingliu01/xp-gate 0.25.0 → 0.27.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/adapters/python.sh +53 -3
- package/bin/xp-gate.js +161 -28
- package/hooks/lib/python-incremental-typecheck.sh +104 -0
- package/hooks/lib/typecheck.sh +10 -0
- package/hooks/pre-commit +84 -32
- package/lib/__tests__/check-alignment-cli.test.mjs +77 -0
- package/lib/__tests__/doctor-469-hook-env.test.js +12 -7
- package/lib/__tests__/doctor-module-resolution-parity.test.js +15 -6
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +16 -4
- package/lib/__tests__/test-alignment.test.ts +37 -0
- package/lib/__tests__/windows-bash-probe.test.js +73 -0
- package/lib/check-alignment-runner.cjs +74 -0
- package/lib/detect-deps.js +82 -19
- package/lib/gate-runner.js +12 -5
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/sprint-status.js +5 -2
- package/lib/test-alignment.ts +136 -12
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/README.md +1 -1
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +2 -2
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/skills.test.ts +20 -7
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/scripts/prepack.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/rules/__tests__/clean-code/code-duplication.test.ts +9 -8
- package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +3 -4
- package/principles/rules/__tests__/clean-code/god-class.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/large-file.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/long-function.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +2 -3
- package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/too-many-params.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/unused-imports.test.ts +4 -4
- package/principles/rules/__tests__/solid/dip.test.ts +2 -4
- package/principles/rules/__tests__/solid/isp.test.ts +2 -4
- package/principles/rules/__tests__/solid/lsp.test.ts +2 -4
- package/principles/rules/__tests__/solid/ocp.test.ts +2 -4
- package/principles/rules/__tests__/solid/srp.test.ts +2 -4
- package/principles/rules/clean-code/code-duplication.ts +12 -14
- package/principles/rules/clean-code/deep-nesting.ts +18 -20
- package/principles/rules/clean-code/god-class.ts +32 -34
- package/principles/rules/clean-code/large-file.ts +14 -16
- package/principles/rules/clean-code/long-function.ts +26 -28
- package/principles/rules/clean-code/magic-numbers.ts +39 -38
- package/principles/rules/clean-code/many-exports.ts +21 -23
- package/principles/rules/clean-code/missing-error-handling.ts +13 -15
- package/principles/rules/clean-code/too-many-params.ts +15 -17
- package/principles/rules/clean-code/unused-imports.ts +13 -15
- package/principles/rules/solid/dip.ts +16 -18
- package/principles/rules/solid/isp.ts +17 -19
- package/principles/rules/solid/lsp.ts +4 -6
- package/principles/rules/solid/ocp.ts +17 -19
- package/principles/rules/solid/srp.ts +20 -22
- package/scripts/__tests__/sync-package-content.test.js +0 -1
- package/scripts/sync-package-content.js +0 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/qoder/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/skills/clipboard-vision/SKILL.md +0 -48
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
|
@@ -17,11 +17,16 @@ const describeWin = isWindows ? describe : describe.skip;
|
|
|
17
17
|
// Walkthrough FC-02: a bare `return` inside it() silently reports PASS on a
|
|
18
18
|
// runner without bash — a false-green in the very suite that guards against
|
|
19
19
|
// false greens. Bash-gated tests must SKIP loudly instead.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// On Windows a bare PATH 'bash' may resolve to the WSL System32 stub, whose
|
|
21
|
+
// argv handling drops `-c` positional args — probing through it answers for
|
|
22
|
+
// the wrong environment (the exact #469 failure class). Resolve the same
|
|
23
|
+
// authoritative bash the hooks get: checkBash().
|
|
24
|
+
const { checkBash } = require('../detect-deps.js');
|
|
25
|
+
const BASH_PROBE = (() => {
|
|
26
|
+
const b = checkBash();
|
|
27
|
+
return b.ok ? b.path : null;
|
|
23
28
|
})();
|
|
24
|
-
const itWithBash =
|
|
29
|
+
const itWithBash = BASH_PROBE ? it : it.skip;
|
|
25
30
|
|
|
26
31
|
describe('doctor hook-env probing (#469)', () => {
|
|
27
32
|
function mod() {
|
|
@@ -30,7 +35,7 @@ describe('doctor hook-env probing (#469)', () => {
|
|
|
30
35
|
|
|
31
36
|
itWithBash('AC-469-01: a tool the hook env sees is reported with its hook-env path', async () => {
|
|
32
37
|
const { probeToolInHookEnv } = mod();
|
|
33
|
-
const bashPath = isWindows ?
|
|
38
|
+
const bashPath = isWindows ? BASH_PROBE : '/bin/sh';
|
|
34
39
|
const p = await probeToolInHookEnv(isWindows ? 'node' : 'sh', bashPath);
|
|
35
40
|
expect(typeof p).toBe('string');
|
|
36
41
|
expect(p.length).toBeGreaterThan(0);
|
|
@@ -39,7 +44,7 @@ describe('doctor hook-env probing (#469)', () => {
|
|
|
39
44
|
itWithBash('AC-469-01: a tool absent from the hook env returns null — no fall-through', async () => {
|
|
40
45
|
const { detectCliToolAsync } = mod();
|
|
41
46
|
const result = await detectCliToolAsync('definitely-not-a-real-tool-xyz-469', isWindows, {
|
|
42
|
-
bashPath:
|
|
47
|
+
bashPath: BASH_PROBE,
|
|
43
48
|
});
|
|
44
49
|
// With bash present there is NO fall-through: `where` finding nothing is
|
|
45
50
|
// irrelevant here, the hook env verdict is final.
|
|
@@ -56,7 +61,7 @@ describe('doctor hook-env probing (#469)', () => {
|
|
|
56
61
|
|
|
57
62
|
itWithBash('AC-469-01: an installed tool is visible through the hook-env tier', async () => {
|
|
58
63
|
const { detectCliToolAsync } = mod();
|
|
59
|
-
const result = await detectCliToolAsync('node', true, { bashPath:
|
|
64
|
+
const result = await detectCliToolAsync('node', true, { bashPath: BASH_PROBE });
|
|
60
65
|
expect(result.available).toBe(true);
|
|
61
66
|
// The hook-env path is POSIX form on Windows — cmd.exe cannot run it.
|
|
62
67
|
if (isWindows) expect(result.path.startsWith('/')).toBe(true);
|
|
@@ -51,13 +51,22 @@ function matchBlock(source, pattern, label) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function bashBin() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
if (process.env.XP_GATE_BASH_BIN) {
|
|
55
|
+
const probe = spawnSync(process.env.XP_GATE_BASH_BIN, ['-c', 'echo ok'], { encoding: 'utf8' });
|
|
56
|
+
if (probe.status !== 0 || probe.error) {
|
|
57
|
+
throw new Error(`bash runner unavailable (${process.env.XP_GATE_BASH_BIN}): ${probe.error ? probe.error.message : `exit ${probe.status}`}`);
|
|
58
|
+
}
|
|
59
|
+
return process.env.XP_GATE_BASH_BIN;
|
|
60
|
+
}
|
|
61
|
+
// Resolve the authoritative bash the hooks actually get. On Windows a bare
|
|
62
|
+
// PATH 'bash' may be the WSL System32 stub, whose `-c` scripts receive an
|
|
63
|
+
// empty $1 and resolve nothing — a vacuous resolver run, not a passing one.
|
|
64
|
+
const { checkBash } = require('../detect-deps.js');
|
|
65
|
+
const b = checkBash();
|
|
66
|
+
if (!b.ok) {
|
|
67
|
+
throw new Error(`bash runner unavailable: ${b.message || 'checkBash failed'}`);
|
|
59
68
|
}
|
|
60
|
-
return
|
|
69
|
+
return b.path;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/**
|
|
@@ -32,12 +32,20 @@ describe('doctor-tui skill freshness (#439)', () => {
|
|
|
32
32
|
// HOME has no platform marker, so detectPlatform() resolves to opencode.
|
|
33
33
|
function installSkill(name, content) {
|
|
34
34
|
const dir = path.join(tmpHome, '.config', 'opencode', 'skills', name);
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), content);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new Error(`failed to install test skill ${name}: ${err && err.message}`);
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
function readBundled(name) {
|
|
40
|
-
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(path.join(bundledSkillsDir, name, 'SKILL.md'), 'utf8');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(`failed to read bundled skill ${name}: ${err && err.message}`);
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
/** Same bytes, opposite line endings — the only difference a fresh install can have. */
|
|
@@ -134,7 +142,11 @@ describe('doctor-tui skill freshness (#439)', () => {
|
|
|
134
142
|
.readdirSync(bundledSkillsDir, { withFileTypes: true })
|
|
135
143
|
.filter((d) => d.isDirectory())
|
|
136
144
|
.map((d) => d.name);
|
|
137
|
-
|
|
145
|
+
// Sanity floor only: the substantive assertion below is that every bundled
|
|
146
|
+
// skill declares a version. #522 removed clipboard-vision, dropping the
|
|
147
|
+
// bundle from 13 to 12 skills, so the floor tracks the current bundle size
|
|
148
|
+
// instead of a stale magic number (Delphi Round 1, finding A-Critical).
|
|
149
|
+
expect(skills.length).toBeGreaterThanOrEqual(12);
|
|
138
150
|
const missing = skills.filter((name) => {
|
|
139
151
|
const md = path.join(bundledSkillsDir, name, 'SKILL.md');
|
|
140
152
|
return !/^version:\s*\d+\.\d+\.\d+/m.test(fs.readFileSync(md, 'utf8'));
|
|
@@ -325,6 +325,43 @@ test('multiple assertions', () => {
|
|
|
325
325
|
expect(result.tests[0].assertions).toBeGreaterThanOrEqual(4);
|
|
326
326
|
expect(result.totalAssertions).toBeGreaterThanOrEqual(4);
|
|
327
327
|
});
|
|
328
|
+
|
|
329
|
+
it('AC-FIXTURE-ASSERT: should count assertions per-test, not per-file (#392)', async () => {
|
|
330
|
+
// Regression for #392-#1: assertion counting must be scoped to the test
|
|
331
|
+
// body, otherwise totalAssertions is inflated by fileCount x perFileCount.
|
|
332
|
+
const testTs = `
|
|
333
|
+
/**
|
|
334
|
+
* @test REQ-MULTI-001 First test
|
|
335
|
+
* @intent Assertion counting is per-test
|
|
336
|
+
* @covers AC-FIXTURE-MULTI-01
|
|
337
|
+
*/
|
|
338
|
+
test('first test', () => {
|
|
339
|
+
expect(a()).toBe(1);
|
|
340
|
+
expect(b()).toBe(2);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @test REQ-MULTI-002 Second test
|
|
345
|
+
* @intent Assertion counting is per-test
|
|
346
|
+
* @covers AC-FIXTURE-MULTI-02
|
|
347
|
+
*/
|
|
348
|
+
test('second test', () => {
|
|
349
|
+
expect(c()).toBe(3);
|
|
350
|
+
});
|
|
351
|
+
`;
|
|
352
|
+
const testFile = writeTempFile('multi-assert.test.ts', testTs);
|
|
353
|
+
const result = parseTestFiles([testFile]);
|
|
354
|
+
|
|
355
|
+
expect(result.tests).toHaveLength(2);
|
|
356
|
+
// countAssertions counts matching *patterns*, so `expect(x).toBe(y)` yields
|
|
357
|
+
// 2 (one `expect(` + one `toBe(`). First test body has 2 such assertions
|
|
358
|
+
// => 4; second has 1 => 2. The key regression: each test counts ONLY its own
|
|
359
|
+
// body (total 6), never the whole file (3 expect + 3 toBe = 6 per test,
|
|
360
|
+
// which would inflate totalAssertions to 12 under the old per-file bug).
|
|
361
|
+
expect(result.tests[0].assertions).toBe(4);
|
|
362
|
+
expect(result.tests[1].assertions).toBe(2);
|
|
363
|
+
expect(result.totalAssertions).toBe(6);
|
|
364
|
+
});
|
|
328
365
|
});
|
|
329
366
|
|
|
330
367
|
// ============================================================================
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-windows-bash probe behavior
|
|
3
|
+
* @intent resolveWindowsBashCandidates must accept only a bash that round-trips
|
|
4
|
+
* `-c` positional argv AND reports MINGW/MSYS/CYGWIN via uname — the WSL
|
|
5
|
+
* System32 stub (Linux uname, dropped $1) is rejected even when PATH
|
|
6
|
+
* leads with it. Behavior is exercised through an injected fake spawner,
|
|
7
|
+
* so no real interpreter configuration is assumed on the host (#469
|
|
8
|
+
* follow-up, Delphi walkthrough R1 MN-06).
|
|
9
|
+
* @covers AC-WINBASH-01
|
|
10
|
+
*/
|
|
11
|
+
const { describe, it, expect } = await import('vitest');
|
|
12
|
+
const { resolveWindowsBashCandidates } = require('../detect-deps.js');
|
|
13
|
+
|
|
14
|
+
// The candidate loop reads `where bash` from the host; tests inject both the
|
|
15
|
+
// discovery runner and the probe spawner, so results are deterministic on any
|
|
16
|
+
// machine regardless of what interpreter configuration it happens to have.
|
|
17
|
+
function fakeWhere(whereOutput) {
|
|
18
|
+
return (cmd) => {
|
|
19
|
+
if (/^where bash/.test(cmd)) {
|
|
20
|
+
if (whereOutput instanceof Error) throw whereOutput;
|
|
21
|
+
return whereOutput || '';
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`unexpected execSync: ${cmd}`);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fakeSpawn(behaviors) {
|
|
28
|
+
const probed = [];
|
|
29
|
+
const fn = (candidate) => {
|
|
30
|
+
probed.push(candidate);
|
|
31
|
+
const b = behaviors[candidate];
|
|
32
|
+
if (b instanceof Error) throw b;
|
|
33
|
+
// Unknown candidates model a missing file: no stdout at all.
|
|
34
|
+
return { stdout: b !== undefined ? b : '' };
|
|
35
|
+
};
|
|
36
|
+
fn.probed = probed;
|
|
37
|
+
return fn;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('resolveWindowsBashCandidates (win32 authoritative bash)', () => {
|
|
41
|
+
it('AC-WINBASH-01: rejects a WSL stub whose $1 is empty and uname is Linux', () => {
|
|
42
|
+
const spawn = fakeSpawn({ 'C:\\Windows\\System32\\bash.exe': '[ ]\nLinux\n' });
|
|
43
|
+
// No MSYS candidate exists in this world (hardcoded fallbacks report no
|
|
44
|
+
// stdout), so the resolver must find nothing rather than settle for the stub.
|
|
45
|
+
expect(resolveWindowsBashCandidates(spawn, fakeWhere('C:\\Windows\\System32\\bash.exe\r\n'))).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('AC-WINBASH-01: accepts the first candidate with argv round-trip + MINGW uname', () => {
|
|
49
|
+
const spawn = fakeSpawn({
|
|
50
|
+
'C:\\a\\bash.exe': '[]\nLinux\n', // drops $1 → reject
|
|
51
|
+
'C:\\b\\bash.exe': '[XPGATE_ARGV_PROBE]\nMINGW64_NT-10.0\n', // accept
|
|
52
|
+
});
|
|
53
|
+
const result = resolveWindowsBashCandidates(spawn, fakeWhere('C:\\a\\bash.exe\r\nC:\\b\\bash.exe\r\n'));
|
|
54
|
+
expect(result).toBe('C:\\b\\bash.exe');
|
|
55
|
+
expect(spawn.probed.slice(0, 2)).toEqual(['C:\\a\\bash.exe', 'C:\\b\\bash.exe']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('AC-WINBASH-01: a Linux uname with intact argv still fails the platform test', () => {
|
|
59
|
+
// A real WSL bash answers [XPGATE_ARGV_PROBE] correctly under some
|
|
60
|
+
// invocations but its filesystem view cannot run these hooks — uname wins.
|
|
61
|
+
const spawn = fakeSpawn({ 'C:\\wsl\\bash.exe': '[XPGATE_ARGV_PROBE]\nLinux\n' });
|
|
62
|
+
expect(resolveWindowsBashCandidates(spawn, fakeWhere('C:\\wsl\\bash.exe\r\n'))).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('AC-WINBASH-01: a throwing candidate is skipped, not fatal', () => {
|
|
66
|
+
const spawn = fakeSpawn({
|
|
67
|
+
'C:\\gone\\bash.exe': new Error('ENOENT'),
|
|
68
|
+
'C:\\msys64\\usr\\bin\\bash.exe': '[XPGATE_ARGV_PROBE]\nMSYS_NT-10.0\n',
|
|
69
|
+
});
|
|
70
|
+
const result = resolveWindowsBashCandidates(spawn, fakeWhere(new Error('where unavailable')));
|
|
71
|
+
expect(result).toBe('C:\\msys64\\usr\\bin\\bash.exe');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-alignment-runner.cjs — Thin argv shim for test-alignment.ts
|
|
4
|
+
*
|
|
5
|
+
* Bridges the `xp-gate check-alignment` CLI to the TypeScript engine without
|
|
6
|
+
* shell-command interpolation of user-supplied paths (#392-#6).
|
|
7
|
+
*
|
|
8
|
+
* It is designed to be launched EITHER way:
|
|
9
|
+
* 1. In-repo (fast path): `node` loads this file after xp-gate.js already
|
|
10
|
+
* called `require('tsx/cjs')`, so requiring `./test-alignment.ts` works.
|
|
11
|
+
* 2. Published global install (zero-install path): `npx -y tsx` runs this
|
|
12
|
+
* file, and npx self-provisions tsx on demand (never a package dep), so
|
|
13
|
+
* `./test-alignment.ts` loads even though the package ships zero deps.
|
|
14
|
+
*
|
|
15
|
+
* All user-supplied paths are passed as discrete argv elements, never embedded
|
|
16
|
+
* into a shell command string, preserving the #392-#6 injection fix.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
// --- argv parsing ----------------------------------------------------------
|
|
23
|
+
function argValue(argv, flag) {
|
|
24
|
+
const idx = argv.indexOf(flag);
|
|
25
|
+
return idx >= 0 && idx + 1 < argv.length ? argv[idx + 1] : undefined;
|
|
26
|
+
}
|
|
27
|
+
function argFlag(argv, flag) {
|
|
28
|
+
return argv.includes(flag);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const specPath = argValue(process.argv, '--spec');
|
|
32
|
+
const testDirsArg = argValue(process.argv, '--tests');
|
|
33
|
+
const outputPath = argValue(process.argv, '--output');
|
|
34
|
+
const jsonMode = argFlag(process.argv, '--json');
|
|
35
|
+
|
|
36
|
+
if (!specPath) {
|
|
37
|
+
console.error('check-alignment: --spec <path> is required');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const testDirs = testDirsArg ? [testDirsArg] : undefined;
|
|
42
|
+
|
|
43
|
+
let headCommit = 'unknown';
|
|
44
|
+
try {
|
|
45
|
+
headCommit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() || 'unknown';
|
|
46
|
+
} catch (err) {
|
|
47
|
+
headCommit = 'unknown';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { runFullAlignment } = require('./test-alignment.ts');
|
|
51
|
+
|
|
52
|
+
let report;
|
|
53
|
+
try {
|
|
54
|
+
report = runFullAlignment({
|
|
55
|
+
specPath,
|
|
56
|
+
testDirs,
|
|
57
|
+
headCommit,
|
|
58
|
+
outputPath: outputPath || undefined,
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('check-alignment failed:', err instanceof Error ? err.message : String(err));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (jsonMode) {
|
|
66
|
+
console.log(JSON.stringify(report, null, 2));
|
|
67
|
+
} else {
|
|
68
|
+
console.log('Status:', report.alignment_status);
|
|
69
|
+
console.log('Score:', report.score);
|
|
70
|
+
console.log('Misaligned:', report.misaligned_tests.length);
|
|
71
|
+
}
|
|
72
|
+
// Set exitCode instead of process.exit() so buffered stdout (notably the
|
|
73
|
+
// --json document) flushes fully before the process terminates (#392 review).
|
|
74
|
+
process.exitCode = report.alignment_status === 'PASS' ? 0 : 1;
|
package/lib/detect-deps.js
CHANGED
|
@@ -270,12 +270,88 @@ function getSkillsDirs(platform) {
|
|
|
270
270
|
return profile.skillsDirs;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Resolve a bash that can actually run the hooks.
|
|
275
|
+
*
|
|
276
|
+
* On Windows, `bash` on PATH is ambiguous: WSL installs C:\Windows\System32\
|
|
277
|
+
* bash.exe (a Linux interpreter whose filesystem view does not contain the
|
|
278
|
+
* Windows temp dirs the tooling passes to it), and its argv handling silently
|
|
279
|
+
* drops the positional arguments a `-c` script expects ($1 empty → scripts
|
|
280
|
+
* source nothing and "succeed" vacuously). Git for Windows ships the MSYS bash
|
|
281
|
+
* the hooks are written for. Probe candidates with an argv round-trip plus an
|
|
282
|
+
* uname check — only an MSYS/MINGW/CYGWIN bash counts on win32; a plain
|
|
283
|
+
* "bash --version" success proves nothing.
|
|
284
|
+
* @returns {string|null}
|
|
285
|
+
*/
|
|
286
|
+
function probeWindowsBash() {
|
|
287
|
+
const { spawnSync } = require('child_process');
|
|
288
|
+
// `where` runs through the same module so tests can fake discovery + probe.
|
|
289
|
+
const { execSync: localExecSync } = require('child_process');
|
|
290
|
+
return resolveWindowsBashCandidates(spawnSync, localExecSync);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Candidate list + probe loop split out for behavior tests: a fake spawner can
|
|
294
|
+
// simulate the WSL stub (echoes Linux / drops $1) vs an MSYS bash without any
|
|
295
|
+
// real interpreter on PATH (Delphi walkthrough R1, MN-06).
|
|
296
|
+
function resolveWindowsBashCandidates(spawnProbe, whereExecSync) {
|
|
297
|
+
const _execSync = whereExecSync || execSync;
|
|
298
|
+
const candidates = [];
|
|
299
|
+
try {
|
|
300
|
+
const found = _execSync('where bash', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
301
|
+
for (const line of found.split(/\r?\n/)) if (line.trim()) candidates.push(line.trim());
|
|
302
|
+
} catch { /* where unavailable */ }
|
|
303
|
+
candidates.push(
|
|
304
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
305
|
+
'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
|
|
306
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
307
|
+
'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe',
|
|
308
|
+
'C:\\msys64\\usr\\bin\\bash.exe',
|
|
309
|
+
'C:\\cygwin64\\bin\\bash.exe'
|
|
310
|
+
);
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
try {
|
|
313
|
+
const r = spawnProbe(candidate, ['-c', 'echo "[$1]"; uname -s', 'sh', 'XPGATE_ARGV_PROBE'], {
|
|
314
|
+
encoding: 'utf8',
|
|
315
|
+
timeout: 5000,
|
|
316
|
+
windowsHide: true,
|
|
317
|
+
});
|
|
318
|
+
const out = String((r && r.stdout) || '');
|
|
319
|
+
if (out.includes('[XPGATE_ARGV_PROBE]') && /(MINGW|MSYS|CYGWIN)/i.test(out)) {
|
|
320
|
+
return candidate;
|
|
321
|
+
}
|
|
322
|
+
} catch { /* next candidate */ }
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
273
327
|
/**
|
|
274
328
|
* Check if bash is available on the system.
|
|
275
329
|
* XP-Gate hooks are bash scripts — Windows users need Git Bash installed.
|
|
276
330
|
* @returns {{ok: boolean, path?: string, message?: string}}
|
|
277
331
|
*/
|
|
278
332
|
function checkBash() {
|
|
333
|
+
// On Windows resolve explicitly: PATH may lead with the WSL System32 stub,
|
|
334
|
+
// which cannot execute these hooks (wrong filesystem view, dropped $1).
|
|
335
|
+
if (process.platform === 'win32') {
|
|
336
|
+
const winBash = probeWindowsBash();
|
|
337
|
+
if (winBash) {
|
|
338
|
+
let version = 'unknown';
|
|
339
|
+
try {
|
|
340
|
+
// spawnSync with discrete argv — never interpolate a discovered path
|
|
341
|
+
// into a shell string (a PATH dir name may carry cmd metacharacters).
|
|
342
|
+
const result = spawnSync(winBash, ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
343
|
+
const versionMatch = String(result.stdout || '').match(/version\s+([^\s]+)/);
|
|
344
|
+
if (versionMatch) version = versionMatch[1];
|
|
345
|
+
} catch { /* keep unknown */ }
|
|
346
|
+
return { ok: true, path: winBash, version };
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
message: 'bash not found. Windows users must install [Git for Windows](https://git-scm.com/download/win).\n' +
|
|
351
|
+
' During installation, ensure "Git Bash Here" is checked — this adds bash.exe to PATH.\n' +
|
|
352
|
+
' After installation, restart your terminal and run `npm install` again.'
|
|
353
|
+
};
|
|
354
|
+
}
|
|
279
355
|
try {
|
|
280
356
|
// Try 'bash' first (works on Linux/macOS and when Git Bash is in PATH)
|
|
281
357
|
const result = execSync('bash --version', {
|
|
@@ -289,17 +365,11 @@ function checkBash() {
|
|
|
289
365
|
version: versionMatch ? versionMatch[1] : 'unknown'
|
|
290
366
|
};
|
|
291
367
|
} catch (e) {
|
|
292
|
-
//
|
|
368
|
+
// POSIX fallback: common absolute locations
|
|
293
369
|
const bashPaths = [
|
|
294
|
-
|
|
295
|
-
'
|
|
296
|
-
'
|
|
297
|
-
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
298
|
-
'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe',
|
|
299
|
-
// MSYS2
|
|
300
|
-
'C:\\msys64\\usr\\bin\\bash.exe',
|
|
301
|
-
// Cygwin
|
|
302
|
-
'C:\\cygwin64\\bin\\bash.exe',
|
|
370
|
+
'/bin/bash',
|
|
371
|
+
'/usr/bin/bash',
|
|
372
|
+
'/usr/local/bin/bash',
|
|
303
373
|
];
|
|
304
374
|
|
|
305
375
|
for (const bashPath of bashPaths) {
|
|
@@ -319,15 +389,7 @@ function checkBash() {
|
|
|
319
389
|
}
|
|
320
390
|
}
|
|
321
391
|
|
|
322
|
-
// Windows
|
|
323
|
-
if (process.platform === 'win32') {
|
|
324
|
-
return {
|
|
325
|
-
ok: false,
|
|
326
|
-
message: 'bash not found. Windows users must install [Git for Windows](https://git-scm.com/download/win).\n' +
|
|
327
|
-
' During installation, ensure "Git Bash Here" is checked — this adds bash.exe to PATH.\n' +
|
|
328
|
-
' After installation, restart your terminal and run `npm install` again.'
|
|
329
|
-
};
|
|
330
|
-
}
|
|
392
|
+
// Windows is handled above; this fallback only runs on POSIX now.
|
|
331
393
|
|
|
332
394
|
return {
|
|
333
395
|
ok: false,
|
|
@@ -505,6 +567,7 @@ function compareVersions(a, b) {
|
|
|
505
567
|
module.exports = {
|
|
506
568
|
checkDeps,
|
|
507
569
|
checkBash,
|
|
570
|
+
resolveWindowsBashCandidates,
|
|
508
571
|
autoInstallDeps,
|
|
509
572
|
detectPlatform,
|
|
510
573
|
getSkillsDirs,
|
package/lib/gate-runner.js
CHANGED
|
@@ -18,15 +18,22 @@ const path = require('node:path');
|
|
|
18
18
|
* behaves exactly as it does inside the commit hook.
|
|
19
19
|
*/
|
|
20
20
|
function runGateAdapter(scriptPath, cwd = process.cwd()) {
|
|
21
|
+
let bashCommand = 'bash';
|
|
21
22
|
if (process.platform === 'win32') {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// PATH may lead with the WSL System32 stub, which cannot run these hooks
|
|
24
|
+
// (Linux filesystem view, and `-c` scripts receive an empty $1 through it).
|
|
25
|
+
// checkBash probes for an MSYS/MINGW bash that round-trips argv correctly.
|
|
26
|
+
const { checkBash } = require('./detect-deps.js');
|
|
27
|
+
const bash = checkBash();
|
|
28
|
+
if (!bash.ok) {
|
|
25
29
|
console.log('⚠️ This gate requires bash to run shell scripts.');
|
|
26
|
-
|
|
30
|
+
// No WSL suggestion: these hooks need Git Bash specifically — the WSL
|
|
31
|
+
// bash is rejected by the probe above (Linux filesystem view, dropped $1).
|
|
32
|
+
console.log(' On Windows, install Git for Windows (https://git-scm.com/download/win).');
|
|
27
33
|
console.log(' Alternatively, install the required tool directly and run the gate again.');
|
|
28
34
|
return;
|
|
29
35
|
}
|
|
36
|
+
bashCommand = bash.path;
|
|
30
37
|
}
|
|
31
38
|
const scriptDir = path.dirname(scriptPath);
|
|
32
39
|
const commonScript = [
|
|
@@ -62,7 +69,7 @@ function runGateAdapter(scriptPath, cwd = process.cwd()) {
|
|
|
62
69
|
// execFileSync avoids an extra /bin/sh layer, so bash receives the script as a
|
|
63
70
|
// single argv arg (no shell-metacharacter injection through the path).
|
|
64
71
|
execFileSync(
|
|
65
|
-
|
|
72
|
+
bashCommand,
|
|
66
73
|
['--noprofile', '--norc', '-c', wrap, 'xp-gate', commonScript, nowMsScript, scriptPath, process.execPath],
|
|
67
74
|
{ stdio: 'inherit', cwd }
|
|
68
75
|
);
|
package/lib/sprint-status.js
CHANGED
|
@@ -345,8 +345,11 @@ async function handleSprintStatus(args = []) {
|
|
|
345
345
|
|
|
346
346
|
if (dirIdx >= 0 && dirIdx + 1 < args.length) {
|
|
347
347
|
searchDir = path.resolve(args[dirIdx + 1]);
|
|
348
|
-
// Path traversal protection: must be under cwd
|
|
349
|
-
|
|
348
|
+
// Path traversal protection: must be under cwd. Use path.relative instead
|
|
349
|
+
// of a prefix match so a sibling like `<cwd>2` cannot bypass the check
|
|
350
|
+
// (#392-#5).
|
|
351
|
+
const rel = path.relative(process.cwd(), searchDir);
|
|
352
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
350
353
|
console.error('Error: --dir path must be under current working directory');
|
|
351
354
|
return 1;
|
|
352
355
|
}
|