@boyingliu01/xp-gate 0.13.2 → 0.14.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/bin/xp-gate.js +8 -0
- package/gate-9.sh +13 -13
- package/hooks/gate-9.sh +13 -13
- package/lib/__tests__/bootstrap.test.js +78 -0
- package/lib/__tests__/detect-deps.test.js +88 -0
- package/lib/__tests__/doctor.test.js +28 -0
- package/lib/__tests__/install-cmd.test.js +41 -0
- package/lib/__tests__/uninstall.test.js +53 -0
- package/lib/detect-deps.js +2 -0
- package/lib/doctor.js +13 -0
- package/lib/gate-tools.js +87 -0
- package/lib/init.js +1 -36
- package/lib/install-cmd.js +57 -0
- package/lib/shared-utils.js +35 -1
- package/lib/uninstall.js +54 -9
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
package/bin/xp-gate.js
CHANGED
|
@@ -157,6 +157,14 @@ const COMMANDS = {
|
|
|
157
157
|
}
|
|
158
158
|
},
|
|
159
159
|
usage: 'xp-gate update-hooks [--global] [--force] [--dry-run] [--no-backup] [--scope hooks|adapters|all]'
|
|
160
|
+
},
|
|
161
|
+
'install': {
|
|
162
|
+
description: 'One-step install (init + bootstrap + doctor)',
|
|
163
|
+
run: subargs => {
|
|
164
|
+
const { install } = require('../lib/install-cmd');
|
|
165
|
+
install(subargs).then(code => process.exit(code));
|
|
166
|
+
},
|
|
167
|
+
usage: 'xp-gate install [--global]'
|
|
160
168
|
}
|
|
161
169
|
};
|
|
162
170
|
|
package/gate-9.sh
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# ============================================================================
|
|
2
|
-
# GATE
|
|
2
|
+
# GATE 9: Semgrep SAST Security Scan
|
|
3
3
|
# Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
|
|
4
4
|
# Tool: semgrep -- https://semgrep.dev
|
|
5
5
|
# ============================================================================
|
|
6
6
|
|
|
7
7
|
2>&1 echo ""
|
|
8
|
-
2>&1 echo "→ Gate
|
|
9
|
-
|
|
8
|
+
2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
|
|
9
|
+
GATE_9_START=$(gate_start_ms)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
GATE_9_STATUS=""
|
|
12
12
|
|
|
13
13
|
# Semgrep availability check
|
|
14
14
|
SEMGREP_CMD=""
|
|
@@ -22,15 +22,15 @@ if [ -z "$SEMGREP_CMD" ]; then
|
|
|
22
22
|
echo " ⚠️ WARN - semgrep not installed — SAST scanning unavailable"
|
|
23
23
|
echo " Install: brew install semgrep (macOS) | pip install semgrep (Linux) | pip install semgrep (Windows)"
|
|
24
24
|
echo " Pre-cache rules: semgrep --config=p/security-audit"
|
|
25
|
-
echo " Gate
|
|
26
|
-
|
|
25
|
+
echo " Gate 9: SAST Security (WARN, semgrep not installed)"
|
|
26
|
+
GATE_9_STATUS="WARN"
|
|
27
27
|
else
|
|
28
28
|
# Get staged files filtered to Semgrep-supported languages
|
|
29
29
|
SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
|
|
30
30
|
|
|
31
31
|
if [ -z "$SEMGREP_FILES" ]; then
|
|
32
32
|
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
|
33
|
-
|
|
33
|
+
GATE_9_STATUS="SKIP"
|
|
34
34
|
else
|
|
35
35
|
# Run semgrep with JSON output
|
|
36
36
|
# --config=p/security-audit: explicit security ruleset
|
|
@@ -41,7 +41,7 @@ else
|
|
|
41
41
|
|
|
42
42
|
if [ "$SEMGREP_EXIT" -eq 0 ]; then
|
|
43
43
|
echo " ✅ PASSED - No security vulnerabilities found."
|
|
44
|
-
|
|
44
|
+
GATE_9_STATUS="PASS"
|
|
45
45
|
elif [ "$SEMGREP_EXIT" -eq 1 ]; then
|
|
46
46
|
# Findings detected - parse JSON to categorize
|
|
47
47
|
CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | python3 -c "
|
|
@@ -99,7 +99,7 @@ except:
|
|
|
99
99
|
if [ "$CRITICAL_HIGH" -gt 0 ]; then
|
|
100
100
|
echo ""
|
|
101
101
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
102
|
-
echo " GATE
|
|
102
|
+
echo " GATE 9: Semgrep Security Gate"
|
|
103
103
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
104
104
|
echo " CRITICAL/HIGH: ${CRITICAL_HIGH} ❌ BLOCKED"
|
|
105
105
|
echo " MEDIUM/LOW: ${MEDIUM_LOW} ⚠️ warning"
|
|
@@ -108,7 +108,7 @@ except:
|
|
|
108
108
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
109
109
|
echo "$FINDING_DETAILS"
|
|
110
110
|
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
111
|
-
|
|
111
|
+
GATE_9_STATUS="FAIL"
|
|
112
112
|
exit 1
|
|
113
113
|
else
|
|
114
114
|
echo ""
|
|
@@ -117,14 +117,14 @@ except:
|
|
|
117
117
|
echo " ⚠️ ${MEDIUM_LOW} medium/low findings (warnings only)"
|
|
118
118
|
echo "$FINDING_DETAILS"
|
|
119
119
|
fi
|
|
120
|
-
|
|
120
|
+
GATE_9_STATUS="PASS"
|
|
121
121
|
fi
|
|
122
122
|
else
|
|
123
123
|
# semgrep runtime error (timeout, config error, etc.)
|
|
124
124
|
echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
|
|
125
125
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
126
|
-
|
|
126
|
+
GATE_9_STATUS="SKIP"
|
|
127
127
|
fi
|
|
128
128
|
fi
|
|
129
129
|
fi
|
|
130
|
-
record_gate_audit "gate-
|
|
130
|
+
record_gate_audit "gate-9" "sast-security" "$GATE_9_STATUS" "0" "$GATE_9_START"
|
package/hooks/gate-9.sh
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# ============================================================================
|
|
2
|
-
# GATE
|
|
2
|
+
# GATE 9: Semgrep SAST Security Scan
|
|
3
3
|
# Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
|
|
4
4
|
# Tool: semgrep -- https://semgrep.dev
|
|
5
5
|
# ============================================================================
|
|
6
6
|
|
|
7
7
|
2>&1 echo ""
|
|
8
|
-
2>&1 echo "→ Gate
|
|
9
|
-
|
|
8
|
+
2>&1 echo "→ Gate 9: Semgrep SAST Security Scan..."
|
|
9
|
+
GATE_9_START=$(gate_start_ms)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
GATE_9_STATUS=""
|
|
12
12
|
|
|
13
13
|
# Semgrep availability check
|
|
14
14
|
SEMGREP_CMD=""
|
|
@@ -22,15 +22,15 @@ if [ -z "$SEMGREP_CMD" ]; then
|
|
|
22
22
|
echo " ⚠️ WARN - semgrep not installed — SAST scanning unavailable"
|
|
23
23
|
echo " Install: brew install semgrep (macOS) | pip install semgrep (Linux) | pip install semgrep (Windows)"
|
|
24
24
|
echo " Pre-cache rules: semgrep --config=p/security-audit"
|
|
25
|
-
echo " Gate
|
|
26
|
-
|
|
25
|
+
echo " Gate 9: SAST Security (WARN, semgrep not installed)"
|
|
26
|
+
GATE_9_STATUS="WARN"
|
|
27
27
|
else
|
|
28
28
|
# Get staged files filtered to Semgrep-supported languages
|
|
29
29
|
SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
|
|
30
30
|
|
|
31
31
|
if [ -z "$SEMGREP_FILES" ]; then
|
|
32
32
|
echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
|
|
33
|
-
|
|
33
|
+
GATE_9_STATUS="SKIP"
|
|
34
34
|
else
|
|
35
35
|
# Run semgrep with JSON output
|
|
36
36
|
# --config=p/security-audit: explicit security ruleset
|
|
@@ -41,7 +41,7 @@ else
|
|
|
41
41
|
|
|
42
42
|
if [ "$SEMGREP_EXIT" -eq 0 ]; then
|
|
43
43
|
echo " ✅ PASSED - No security vulnerabilities found."
|
|
44
|
-
|
|
44
|
+
GATE_9_STATUS="PASS"
|
|
45
45
|
elif [ "$SEMGREP_EXIT" -eq 1 ]; then
|
|
46
46
|
# Findings detected - parse JSON to categorize
|
|
47
47
|
CRITICAL_HIGH=$(echo "$SEMGREP_OUTPUT" | python3 -c "
|
|
@@ -99,7 +99,7 @@ except:
|
|
|
99
99
|
if [ "$CRITICAL_HIGH" -gt 0 ]; then
|
|
100
100
|
echo ""
|
|
101
101
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
102
|
-
echo " GATE
|
|
102
|
+
echo " GATE 9: Semgrep Security Gate"
|
|
103
103
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
104
104
|
echo " CRITICAL/HIGH: ${CRITICAL_HIGH} ❌ BLOCKED"
|
|
105
105
|
echo " MEDIUM/LOW: ${MEDIUM_LOW} ⚠️ warning"
|
|
@@ -108,7 +108,7 @@ except:
|
|
|
108
108
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
109
109
|
echo "$FINDING_DETAILS"
|
|
110
110
|
echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
|
|
111
|
-
|
|
111
|
+
GATE_9_STATUS="FAIL"
|
|
112
112
|
exit 1
|
|
113
113
|
else
|
|
114
114
|
echo ""
|
|
@@ -117,14 +117,14 @@ except:
|
|
|
117
117
|
echo " ⚠️ ${MEDIUM_LOW} medium/low findings (warnings only)"
|
|
118
118
|
echo "$FINDING_DETAILS"
|
|
119
119
|
fi
|
|
120
|
-
|
|
120
|
+
GATE_9_STATUS="PASS"
|
|
121
121
|
fi
|
|
122
122
|
else
|
|
123
123
|
# semgrep runtime error (timeout, config error, etc.)
|
|
124
124
|
echo " ⚠️ semgrep exited with code ${SEMGREP_EXIT} — skipping gate"
|
|
125
125
|
echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
|
|
126
|
-
|
|
126
|
+
GATE_9_STATUS="SKIP"
|
|
127
127
|
fi
|
|
128
128
|
fi
|
|
129
129
|
fi
|
|
130
|
-
record_gate_audit "gate-
|
|
130
|
+
record_gate_audit "gate-9" "sast-security" "$GATE_9_STATUS" "0" "$GATE_9_START"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const { bootstrap } = require('../bootstrap.js');
|
|
2
|
+
|
|
3
|
+
describe('bootstrap', () => {
|
|
4
|
+
let consoleLogSpy;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns 0 when --dry-run given with no missing tools', () => {
|
|
15
|
+
vi.mock('../detect-deps.js', () => ({
|
|
16
|
+
GATE_CLI_TOOLS: [],
|
|
17
|
+
checkCliTool: () => ({ available: true }),
|
|
18
|
+
getToolInstallCmd: () => 'echo done',
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const { bootstrap } = require('../bootstrap.js');
|
|
22
|
+
const code = bootstrap(['--dry-run']);
|
|
23
|
+
expect(code).toBe(0);
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns 0 when all CLI tools are available', () => {
|
|
28
|
+
vi.mock('../detect-deps.js', () => ({
|
|
29
|
+
GATE_CLI_TOOLS: [{ tool: 'testtool', gates: ['Gate 1'], install: { linux: 'echo ok' } }],
|
|
30
|
+
checkCliTool: () => ({ available: true, version: '1.0' }),
|
|
31
|
+
getToolInstallCmd: () => 'echo ok',
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const { bootstrap } = require('../bootstrap.js');
|
|
35
|
+
const code = bootstrap([]);
|
|
36
|
+
expect(code).toBe(0);
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('accepts --lang ts parameter', () => {
|
|
41
|
+
vi.mock('../detect-deps.js', () => ({
|
|
42
|
+
GATE_CLI_TOOLS: [],
|
|
43
|
+
checkCliTool: () => ({ available: true }),
|
|
44
|
+
getToolInstallCmd: () => 'echo done',
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const { bootstrap } = require('../bootstrap.js');
|
|
48
|
+
const code = bootstrap(['--lang', 'ts']);
|
|
49
|
+
expect(code).toBe(0);
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('accepts --lang ts,py parameter', () => {
|
|
54
|
+
vi.mock('../detect-deps.js', () => ({
|
|
55
|
+
GATE_CLI_TOOLS: [],
|
|
56
|
+
checkCliTool: () => ({ available: true }),
|
|
57
|
+
getToolInstallCmd: () => 'echo done',
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const { bootstrap } = require('../bootstrap.js');
|
|
61
|
+
const code = bootstrap(['--lang', 'ts,py']);
|
|
62
|
+
expect(code).toBe(0);
|
|
63
|
+
vi.restoreAllMocks();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('recognizes --verbose flag without error', () => {
|
|
67
|
+
vi.mock('../detect-deps.js', () => ({
|
|
68
|
+
GATE_CLI_TOOLS: [],
|
|
69
|
+
checkCliTool: () => ({ available: true }),
|
|
70
|
+
getToolInstallCmd: () => 'echo done',
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
const { bootstrap } = require('../bootstrap.js');
|
|
74
|
+
const code = bootstrap(['--verbose']);
|
|
75
|
+
expect(code).toBe(0);
|
|
76
|
+
vi.restoreAllMocks();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -413,6 +413,94 @@ describe('detect-deps', () => {
|
|
|
413
413
|
|
|
414
414
|
// ── checkCliTool tests (Issue #299 — Windows cross-platform) ──
|
|
415
415
|
|
|
416
|
+
describe('GATE_TOOLS classification', () => {
|
|
417
|
+
it('exports GATE_TOOLS with required categories', () => {
|
|
418
|
+
const { GATE_TOOLS } = require('../detect-deps');
|
|
419
|
+
expect(GATE_TOOLS.PLATFORM).toEqual(expect.arrayContaining(['jscpd', 'lizard', 'semgrep', 'gitleaks', 'npx']));
|
|
420
|
+
expect(GATE_TOOLS.IAC).toEqual(expect.arrayContaining(['checkov', 'hadolint', 'kube-score', 'tflint']));
|
|
421
|
+
expect(GATE_TOOLS.LANG_MAP.ts).toBe('typescript');
|
|
422
|
+
expect(GATE_TOOLS.LANG_MAP.py).toBe('python');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('has LINT entries for all 12 supported languages', () => {
|
|
426
|
+
const { GATE_TOOLS } = require('../detect-deps');
|
|
427
|
+
expect(Object.keys(GATE_TOOLS.LINT).sort()).toEqual([
|
|
428
|
+
'cpp', 'dart', 'flutter', 'go', 'java', 'kotlin',
|
|
429
|
+
'objectivec', 'powershell', 'python', 'shell', 'swift', 'typescript',
|
|
430
|
+
]);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('has TEST entries for major languages', () => {
|
|
434
|
+
const { GATE_TOOLS } = require('../detect-deps');
|
|
435
|
+
expect(GATE_TOOLS.TEST.typescript).toContain('vitest');
|
|
436
|
+
expect(GATE_TOOLS.TEST.python).toContain('pytest');
|
|
437
|
+
expect(GATE_TOOLS.TEST.go).toContain('go');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('has MUTATION entries for languages with mutation support', () => {
|
|
441
|
+
const { GATE_TOOLS } = require('../detect-deps');
|
|
442
|
+
expect(GATE_TOOLS.MUTATION.typescript).toContain('stryker');
|
|
443
|
+
expect(GATE_TOOLS.MUTATION.python).toContain('mutmut');
|
|
444
|
+
expect(GATE_TOOLS.MUTATION.go).toContain('gomutants');
|
|
445
|
+
expect(GATE_TOOLS.MUTATION.java).toContain('pitest');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('has SPECIAL with jq, tsx, node descriptors', () => {
|
|
449
|
+
const { GATE_TOOLS } = require('../detect-deps');
|
|
450
|
+
expect(GATE_TOOLS.SPECIAL.jq.gate).toBe('MW');
|
|
451
|
+
expect(GATE_TOOLS.SPECIAL.tsx.gate).toContain('M');
|
|
452
|
+
expect(GATE_TOOLS.SPECIAL.node.gate).toContain('0');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('LANG_MAP values all exist as LINT keys', () => {
|
|
456
|
+
const { GATE_TOOLS } = require('../detect-deps');
|
|
457
|
+
const lintKeys = Object.keys(GATE_TOOLS.LINT);
|
|
458
|
+
for (const full of Object.values(GATE_TOOLS.LANG_MAP)) {
|
|
459
|
+
expect(lintKeys).toContain(full);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe('detectProjectLang', () => {
|
|
465
|
+
it('returns empty array for project with no known markers', () => {
|
|
466
|
+
const { detectProjectLang } = require('../detect-deps');
|
|
467
|
+
expect(detectProjectLang(tmpHome)).toEqual([]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('detects typescript from tsconfig.json', () => {
|
|
471
|
+
fs.writeFileSync(path.join(tmpHome, 'tsconfig.json'), '{}');
|
|
472
|
+
const { detectProjectLang } = require('../detect-deps');
|
|
473
|
+
expect(detectProjectLang(tmpHome)).toContain('typescript');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('detects python from pyproject.toml', () => {
|
|
477
|
+
fs.writeFileSync(path.join(tmpHome, 'pyproject.toml'), '[project]\n');
|
|
478
|
+
const { detectProjectLang } = require('../detect-deps');
|
|
479
|
+
expect(detectProjectLang(tmpHome)).toContain('python');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('detects go from go.mod', () => {
|
|
483
|
+
fs.writeFileSync(path.join(tmpHome, 'go.mod'), 'module example.com/m\n');
|
|
484
|
+
const { detectProjectLang } = require('../detect-deps');
|
|
485
|
+
expect(detectProjectLang(tmpHome)).toContain('go');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('detects java from pom.xml', () => {
|
|
489
|
+
fs.writeFileSync(path.join(tmpHome, 'pom.xml'), '<project></project>');
|
|
490
|
+
const { detectProjectLang } = require('../detect-deps');
|
|
491
|
+
expect(detectProjectLang(tmpHome)).toContain('java');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('detects multiple languages in monorepo', () => {
|
|
495
|
+
fs.writeFileSync(path.join(tmpHome, 'package.json'), JSON.stringify({ devDependencies: { typescript: '^5.0.0' } }));
|
|
496
|
+
fs.writeFileSync(path.join(tmpHome, 'go.mod'), 'module example.com/m\n');
|
|
497
|
+
const { detectProjectLang } = require('../detect-deps');
|
|
498
|
+
const langs = detectProjectLang(tmpHome);
|
|
499
|
+
expect(langs).toContain('typescript');
|
|
500
|
+
expect(langs).toContain('go');
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
416
504
|
describe('checkCliTool', () => {
|
|
417
505
|
const { execSync: realExecSync } = require('child_process');
|
|
418
506
|
|
|
@@ -931,4 +931,32 @@ describe('doctor', () => {
|
|
|
931
931
|
const currentContent = fs.readFileSync(path.join(projectAdaptersDir(), 'gate-3.sh'), 'utf8');
|
|
932
932
|
expect(currentContent).toBe(originalContent);
|
|
933
933
|
});
|
|
934
|
+
|
|
935
|
+
it('--format json returns valid JSON with tools field', async () => {
|
|
936
|
+
setupLocalInstall();
|
|
937
|
+
seedVersionCache();
|
|
938
|
+
mockExecSuccess();
|
|
939
|
+
const { doctor } = require('../doctor');
|
|
940
|
+
const { formatDoctorJson } = require('../doctor');
|
|
941
|
+
|
|
942
|
+
const checks = [
|
|
943
|
+
{ name: 'CLI tool: jscpd (Gate 2)', status: 'PASS', detail: 'v0.3.5' },
|
|
944
|
+
{ name: 'CLI tool: lizard (Gate 3)', status: 'WARN', detail: 'Not found' },
|
|
945
|
+
];
|
|
946
|
+
const json = formatDoctorJson(checks, 1);
|
|
947
|
+
expect(json.ok).toBe(false);
|
|
948
|
+
expect(json.issues).toBe(1);
|
|
949
|
+
expect(json.checks).toHaveLength(2);
|
|
950
|
+
expect(json.missing_tools).toBeDefined();
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it('--format json returns ok:true when no issues', async () => {
|
|
954
|
+
const { formatDoctorJson } = require('../doctor');
|
|
955
|
+
const checks = [
|
|
956
|
+
{ name: 'CLI tool: jscpd (Gate 2)', status: 'PASS', detail: 'v0.3.5' },
|
|
957
|
+
];
|
|
958
|
+
const json = formatDoctorJson(checks, 0);
|
|
959
|
+
expect(json.ok).toBe(true);
|
|
960
|
+
expect(json.issues).toBe(0);
|
|
961
|
+
});
|
|
934
962
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
describe('install-cmd', () => {
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let consoleLogSpy;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-install-'));
|
|
11
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('exports install function', () => {
|
|
21
|
+
const { install } = require('../install-cmd');
|
|
22
|
+
expect(typeof install).toBe('function');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('install accepts --global flag', async () => {
|
|
26
|
+
const { install } = require('../install-cmd');
|
|
27
|
+
const code = await install(['--global'], tmpDir);
|
|
28
|
+
expect(typeof code).toBe('number');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('install runs without --global (local mode) in git repo', async () => {
|
|
32
|
+
// Simulate git repo
|
|
33
|
+
const gitDir = path.join(tmpDir, '.git');
|
|
34
|
+
fs.mkdirSync(gitDir, { recursive: true });
|
|
35
|
+
fs.mkdirSync(path.join(gitDir, 'hooks'), { recursive: true });
|
|
36
|
+
|
|
37
|
+
const { install } = require('../install-cmd');
|
|
38
|
+
const code = await install([], tmpDir);
|
|
39
|
+
expect(typeof code).toBe('number');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -623,4 +623,57 @@ describe('uninstall', () => {
|
|
|
623
623
|
// After successful uninstall, backup should be cleaned up
|
|
624
624
|
expect(fs.existsSync(backupDir())).toBe(false);
|
|
625
625
|
});
|
|
626
|
+
|
|
627
|
+
it('--purge in global mode removes ~/.xp-gate/ and ~/.config/xp-gate/ config dir', async () => {
|
|
628
|
+
setupGlobalInstall();
|
|
629
|
+
const expectedHooksPath = globalHooksDir();
|
|
630
|
+
|
|
631
|
+
const xpGateDir = path.join(tmpHome, '.xp-gate');
|
|
632
|
+
fs.mkdirSync(path.join(xpGateDir, 'reports', 'pre-commit'), { recursive: true });
|
|
633
|
+
fs.writeFileSync(path.join(xpGateDir, 'reports', 'pre-commit', 'test.json'), '{}');
|
|
634
|
+
|
|
635
|
+
mockExecGlobalHooksPath(expectedHooksPath);
|
|
636
|
+
const { uninstall } = require('../uninstall');
|
|
637
|
+
|
|
638
|
+
const result = await uninstall(['--purge']);
|
|
639
|
+
|
|
640
|
+
expect(result).toBe(0);
|
|
641
|
+
expect(fs.existsSync(xpGateDir)).toBe(false);
|
|
642
|
+
expect(fs.existsSync(path.join(tmpHome, '.config', 'xp-gate'))).toBe(false);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('--purge in local mode removes project and global .xp-gate/ dirs', async () => {
|
|
646
|
+
setupLocalInstall();
|
|
647
|
+
|
|
648
|
+
const projectXpGate = path.join(tmpProject, '.xp-gate');
|
|
649
|
+
fs.mkdirSync(path.join(projectXpGate, 'quality-status'), { recursive: true });
|
|
650
|
+
fs.writeFileSync(path.join(projectXpGate, 'quality-status', 'main.json'), '{}');
|
|
651
|
+
|
|
652
|
+
const globalXpGate = path.join(tmpHome, '.xp-gate');
|
|
653
|
+
fs.mkdirSync(path.join(globalXpGate, 'reports', 'pre-commit'), { recursive: true });
|
|
654
|
+
fs.writeFileSync(path.join(globalXpGate, 'reports', 'pre-commit', 'test.json'), '{}');
|
|
655
|
+
|
|
656
|
+
mockExecSuccess();
|
|
657
|
+
const { uninstall } = require('../uninstall');
|
|
658
|
+
|
|
659
|
+
const result = await uninstall(['--purge']);
|
|
660
|
+
|
|
661
|
+
expect(result).toBe(0);
|
|
662
|
+
expect(fs.existsSync(projectXpGate)).toBe(false);
|
|
663
|
+
expect(fs.existsSync(globalXpGate)).toBe(false);
|
|
664
|
+
expect(fs.existsSync(path.join(tmpHome, '.config', 'xp-gate'))).toBe(false);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('--purge --dry-run prints purge plan without deleting', async () => {
|
|
668
|
+
setupGlobalInstall();
|
|
669
|
+
mockExecGlobalHooksPath(globalHooksDir());
|
|
670
|
+
const { uninstall } = require('../uninstall');
|
|
671
|
+
|
|
672
|
+
const result = await uninstall(['--purge', '--dry-run']);
|
|
673
|
+
|
|
674
|
+
expect(result).toBe(0);
|
|
675
|
+
expect(fs.existsSync(globalHooksDir())).toBe(true);
|
|
676
|
+
expect(fs.existsSync(globalAdaptersDir())).toBe(true);
|
|
677
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Purge'));
|
|
678
|
+
});
|
|
626
679
|
});
|
package/lib/detect-deps.js
CHANGED
package/lib/doctor.js
CHANGED
|
@@ -804,6 +804,18 @@ async function doctor(args) {
|
|
|
804
804
|
return issues > 0 ? 1 : 0;
|
|
805
805
|
}
|
|
806
806
|
|
|
807
|
+
function formatDoctorJson(checks, issues) {
|
|
808
|
+
const missing_tools = checks
|
|
809
|
+
.filter(c => c.status === 'WARN' || c.status === 'FAIL')
|
|
810
|
+
.map(c => ({ name: c.name, detail: c.detail }));
|
|
811
|
+
return {
|
|
812
|
+
ok: issues === 0,
|
|
813
|
+
issues,
|
|
814
|
+
checks: checks.map(c => ({ name: c.name, status: c.status, detail: c.detail })),
|
|
815
|
+
missing_tools,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
807
819
|
module.exports = {
|
|
808
820
|
doctor, isXpGateFile, SIGNATURES,
|
|
809
821
|
fixVersionMismatch,
|
|
@@ -815,4 +827,5 @@ module.exports = {
|
|
|
815
827
|
fixTuiRegistration,
|
|
816
828
|
ensureTuiRegistration,
|
|
817
829
|
readTuiJson,
|
|
830
|
+
formatDoctorJson,
|
|
818
831
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const GATE_TOOLS = {
|
|
5
|
+
PLATFORM: ['jscpd', 'lizard', 'semgrep', 'gitleaks', 'npx'],
|
|
6
|
+
IAC: ['checkov', 'hadolint', 'kube-score', 'tflint'],
|
|
7
|
+
|
|
8
|
+
LANG_MAP: {
|
|
9
|
+
ts: 'typescript', py: 'python', go: 'go', java: 'java',
|
|
10
|
+
kt: 'kotlin', cpp: 'cpp', swift: 'swift', dart: 'dart',
|
|
11
|
+
flutter: 'flutter', sh: 'shell', ps: 'powershell', objc: 'objectivec',
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
LINT: {
|
|
15
|
+
typescript: ['biome', 'eslint', 'tsc'],
|
|
16
|
+
python: ['ruff', 'flake8', 'mypy', 'black', 'isort'],
|
|
17
|
+
go: ['golangci-lint', 'go'],
|
|
18
|
+
java: ['checkstyle', 'pmd'],
|
|
19
|
+
kotlin: ['ktlint', 'detekt'],
|
|
20
|
+
cpp: ['clang-tidy', 'cppcheck', 'cmake'],
|
|
21
|
+
swift: ['swiftlint', 'swift'],
|
|
22
|
+
dart: ['dart'],
|
|
23
|
+
flutter: ['flutter'],
|
|
24
|
+
shell: ['shellcheck'],
|
|
25
|
+
powershell: ['pwsh'],
|
|
26
|
+
objectivec: ['oclint'],
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
TEST: {
|
|
30
|
+
typescript: ['vitest', 'jest'],
|
|
31
|
+
python: ['pytest'],
|
|
32
|
+
go: ['go'],
|
|
33
|
+
java: ['mvn', 'gradle'],
|
|
34
|
+
kotlin: ['mvn', 'gradle'],
|
|
35
|
+
swift: ['swift'],
|
|
36
|
+
cpp: ['ctest', 'gcovr'],
|
|
37
|
+
dart: ['dart'],
|
|
38
|
+
flutter: ['flutter'],
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
MUTATION: {
|
|
42
|
+
typescript: ['stryker'],
|
|
43
|
+
python: ['mutmut'],
|
|
44
|
+
go: ['gomutants'],
|
|
45
|
+
java: ['pitest'],
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
SPECIAL: {
|
|
49
|
+
jq: { gate: 'MW', desc: 'code-walkthrough JSON parser (mandatory)' },
|
|
50
|
+
tsx: { gate: 'M/M3/4/6', desc: 'TypeScript executor (via npx)' },
|
|
51
|
+
node: { gate: '0/4/6/M/M3', desc: 'All TypeScript gate runtime' },
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const LANG_MARKERS = [
|
|
56
|
+
{ file: 'tsconfig.json', lang: 'typescript' },
|
|
57
|
+
{ file: 'package.json', check: (p) => {
|
|
58
|
+
try {
|
|
59
|
+
const json = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
60
|
+
return !!(json.devDependencies && json.devDependencies.typescript)
|
|
61
|
+
|| !!(json.dependencies && json.dependencies.typescript);
|
|
62
|
+
} catch { return false; }
|
|
63
|
+
}, lang: 'typescript' },
|
|
64
|
+
{ file: 'go.mod', lang: 'go' },
|
|
65
|
+
{ file: 'pyproject.toml', lang: 'python' },
|
|
66
|
+
{ file: 'manage.py', lang: 'python' },
|
|
67
|
+
{ file: 'requirements.txt', lang: 'python' },
|
|
68
|
+
{ file: 'pom.xml', lang: 'java' },
|
|
69
|
+
{ file: 'build.gradle', lang: 'java' },
|
|
70
|
+
{ file: 'build.gradle.kts', lang: 'kotlin' },
|
|
71
|
+
{ file: 'pubspec.yaml', lang: 'flutter' },
|
|
72
|
+
{ file: 'CMakeLists.txt', lang: 'cpp' },
|
|
73
|
+
{ file: 'Package.swift', lang: 'swift' },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function detectProjectLang(projectRoot) {
|
|
77
|
+
const found = new Set();
|
|
78
|
+
const exists = (f) => fs.existsSync(path.join(projectRoot, f));
|
|
79
|
+
for (const marker of LANG_MARKERS) {
|
|
80
|
+
if (!exists(marker.file)) continue;
|
|
81
|
+
if (marker.check && !marker.check(path.join(projectRoot, marker.file))) continue;
|
|
82
|
+
found.add(marker.lang);
|
|
83
|
+
}
|
|
84
|
+
return [...found];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { GATE_TOOLS, detectProjectLang };
|
package/lib/init.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { checkDeps, checkBash, autoInstallDeps, detectPlatform, GATE_CLI_TOOLS, checkCliTool } = require('./detect-deps.js');
|
|
5
|
+
const { copyHooks, copyAdapters } = require('./shared-utils.js');
|
|
5
6
|
const {
|
|
6
7
|
HOME_DIR,
|
|
7
8
|
CONFIG_DIR,
|
|
@@ -11,42 +12,6 @@ const {
|
|
|
11
12
|
GLOBAL_ADAPTERS_DIR,
|
|
12
13
|
} = require('./shared-paths.js');
|
|
13
14
|
|
|
14
|
-
function copyHooks(srcDir, destDir) {
|
|
15
|
-
['pre-commit', 'pre-push'].forEach(hook => {
|
|
16
|
-
const src = path.join(srcDir, 'hooks', hook);
|
|
17
|
-
const dest = path.join(destDir, hook);
|
|
18
|
-
if (fs.existsSync(src)) {
|
|
19
|
-
fs.copyFileSync(src, dest);
|
|
20
|
-
fs.chmodSync(dest, 0o755);
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function copyAdapters(srcDir, destDir) {
|
|
26
|
-
const adapterSrc = path.join(srcDir, 'adapter-common.sh');
|
|
27
|
-
if (fs.existsSync(adapterSrc)) {
|
|
28
|
-
fs.copyFileSync(adapterSrc, path.join(destDir, 'adapter-common.sh'));
|
|
29
|
-
}
|
|
30
|
-
const adaptersDir = path.join(srcDir, 'adapters');
|
|
31
|
-
if (fs.existsSync(adaptersDir)) {
|
|
32
|
-
fs.readdirSync(adaptersDir).forEach(f => {
|
|
33
|
-
if (f.endsWith('.sh')) {
|
|
34
|
-
fs.copyFileSync(path.join(adaptersDir, f), path.join(destDir, f));
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
// Copy gate scripts (gate-*.sh) from githooks/ source-of-truth to destDir.
|
|
39
|
-
// These are sourced by pre-commit via GATE_DIR which resolves to the adapter dir.
|
|
40
|
-
const githooksDir = path.resolve(srcDir, '..', '..', '..', 'githooks');
|
|
41
|
-
if (fs.existsSync(githooksDir)) {
|
|
42
|
-
fs.readdirSync(githooksDir).forEach(f => {
|
|
43
|
-
if (f.startsWith('gate-') && f.endsWith('.sh')) {
|
|
44
|
-
fs.copyFileSync(path.join(githooksDir, f), path.join(destDir, f));
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
15
|
function copyRecursive(src, dest) {
|
|
51
16
|
const stat = fs.statSync(src);
|
|
52
17
|
if (stat.isDirectory()) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { copyHooks, copyAdapters } = require('./shared-utils');
|
|
5
|
+
|
|
6
|
+
async function install(args = [], cwd = process.cwd()) {
|
|
7
|
+
const isGlobal = args.includes('--global');
|
|
8
|
+
|
|
9
|
+
if (isGlobal) {
|
|
10
|
+
return setupGlobal(cwd);
|
|
11
|
+
}
|
|
12
|
+
return setupLocal(cwd);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function setupGlobal(cwd) {
|
|
16
|
+
const srcDir = path.dirname(__dirname);
|
|
17
|
+
|
|
18
|
+
const globalHooksDir = path.join(require('os').homedir(), '.config', 'xp-gate', 'hooks');
|
|
19
|
+
const globalAdaptersDir = path.join(require('os').homedir(), '.config', 'xp-gate', 'adapters');
|
|
20
|
+
|
|
21
|
+
fs.mkdirSync(globalHooksDir, { recursive: true });
|
|
22
|
+
fs.mkdirSync(globalAdaptersDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
copyHooks(srcDir, globalHooksDir);
|
|
25
|
+
copyAdapters(srcDir, globalAdaptersDir);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
execSync(`git config --global core.hooksPath "${globalHooksDir}"`, { stdio: 'pipe' });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.warn(` Warning: Could not set global core.hooksPath: ${err.message}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('Global installation complete.');
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setupLocal(projectRoot) {
|
|
38
|
+
const gitDir = path.join(projectRoot, '.git');
|
|
39
|
+
if (!fs.existsSync(gitDir)) {
|
|
40
|
+
console.error('Error: Not a git repository');
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const srcDir = path.dirname(__dirname);
|
|
45
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
46
|
+
const githooksDir = path.join(projectRoot, 'githooks');
|
|
47
|
+
|
|
48
|
+
copyHooks(srcDir, hooksDir);
|
|
49
|
+
|
|
50
|
+
fs.mkdirSync(path.join(githooksDir, 'adapters'), { recursive: true });
|
|
51
|
+
copyAdapters(srcDir, githooksDir);
|
|
52
|
+
|
|
53
|
+
console.log('Local installation complete.');
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { install };
|
package/lib/shared-utils.js
CHANGED
|
@@ -33,4 +33,38 @@ function readXpGateConfig() {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
function copyHooks(srcDir, destDir) {
|
|
37
|
+
['pre-commit', 'pre-push'].forEach(hook => {
|
|
38
|
+
const src = path.join(srcDir, 'hooks', hook);
|
|
39
|
+
const dest = path.join(destDir, hook);
|
|
40
|
+
if (fs.existsSync(src)) {
|
|
41
|
+
fs.copyFileSync(src, dest);
|
|
42
|
+
fs.chmodSync(dest, 0o755);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function copyAdapters(srcDir, destDir) {
|
|
48
|
+
const adaptersDir = path.join(srcDir, 'adapters');
|
|
49
|
+
const adapterCommon = path.join(srcDir, 'adapter-common.sh');
|
|
50
|
+
if (fs.existsSync(adapterCommon)) {
|
|
51
|
+
fs.copyFileSync(adapterCommon, path.join(destDir, 'adapter-common.sh'));
|
|
52
|
+
}
|
|
53
|
+
if (fs.existsSync(adaptersDir)) {
|
|
54
|
+
fs.readdirSync(adaptersDir).forEach(f => {
|
|
55
|
+
if (f.endsWith('.sh')) {
|
|
56
|
+
fs.copyFileSync(path.join(adaptersDir, f), path.join(destDir, f));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const githooksDir = path.resolve(srcDir, '..', '..', '..', 'githooks');
|
|
61
|
+
if (fs.existsSync(githooksDir)) {
|
|
62
|
+
fs.readdirSync(githooksDir).forEach(f => {
|
|
63
|
+
if (f.startsWith('gate-') && f.endsWith('.sh')) {
|
|
64
|
+
fs.copyFileSync(path.join(githooksDir, f), path.join(destDir, f));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { copyDirRecursive, readXpGateConfig, copyHooks, copyAdapters };
|
package/lib/uninstall.js
CHANGED
|
@@ -331,7 +331,8 @@ async function uninstall(args) {
|
|
|
331
331
|
dryRun: args.includes('--dry-run'),
|
|
332
332
|
force: args.includes('--force'),
|
|
333
333
|
forceLocal: args.includes('--local'),
|
|
334
|
-
forceGlobal: args.includes('--global')
|
|
334
|
+
forceGlobal: args.includes('--global'),
|
|
335
|
+
purge: args.includes('--purge'),
|
|
335
336
|
};
|
|
336
337
|
|
|
337
338
|
// §4.3 Step 1: Read config
|
|
@@ -362,7 +363,23 @@ async function uninstall(args) {
|
|
|
362
363
|
// §4.3 Step 3: Build uninstall plan
|
|
363
364
|
const plan = buildPlan(mode);
|
|
364
365
|
|
|
365
|
-
|
|
366
|
+
// §4.3 Step 3b: Add purge items if --purge
|
|
367
|
+
const purgeDirs = [];
|
|
368
|
+
if (options.purge) {
|
|
369
|
+
const configDir = CONFIG_DIR;
|
|
370
|
+
const xpGateDir = path.join(HOME_DIR, '.xp-gate');
|
|
371
|
+
purgeDirs.push({ label: '~/.xp-gate/', path: xpGateDir });
|
|
372
|
+
purgeDirs.push({ label: '~/.config/xp-gate/', path: configDir });
|
|
373
|
+
if (mode === 'local') {
|
|
374
|
+
const gitDir = getGitDir();
|
|
375
|
+
if (gitDir) {
|
|
376
|
+
const projectXpGate = path.join(path.dirname(gitDir), '.xp-gate');
|
|
377
|
+
purgeDirs.push({ label: '.xp-gate/ (project)', path: projectXpGate });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (plan.length === 0 && purgeDirs.length === 0) {
|
|
366
383
|
console.log('Nothing to uninstall');
|
|
367
384
|
// Still update config to uninstalled
|
|
368
385
|
saveConfig({ ...config, mode: 'uninstalled', uninstalled: new Date().toISOString() });
|
|
@@ -377,6 +394,12 @@ async function uninstall(args) {
|
|
|
377
394
|
for (const item of plan) {
|
|
378
395
|
console.log(` • ${item.label}`);
|
|
379
396
|
}
|
|
397
|
+
if (options.purge) {
|
|
398
|
+
console.log('\nPurge mode — additional cleanup:');
|
|
399
|
+
for (const d of purgeDirs) {
|
|
400
|
+
console.log(` • ${d.label}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
380
403
|
|
|
381
404
|
// §4.3 Step 5: Dry-run → exit
|
|
382
405
|
if (options.dryRun) {
|
|
@@ -406,14 +429,36 @@ async function uninstall(args) {
|
|
|
406
429
|
}
|
|
407
430
|
}
|
|
408
431
|
|
|
432
|
+
if (options.purge) {
|
|
433
|
+
for (const d of purgeDirs) {
|
|
434
|
+
if (options.dryRun) {
|
|
435
|
+
console.log(` • ${d.label}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (!fs.existsSync(d.path)) continue;
|
|
439
|
+
try {
|
|
440
|
+
fs.rmSync(d.path, { recursive: true, force: true });
|
|
441
|
+
console.log(` Removed ${d.label}`);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
console.warn(` Warning: Could not remove ${d.label}: ${e.message}`);
|
|
444
|
+
hadErrors = true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
409
449
|
// §4.8 State machine: uninstalling → uninstalled
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
450
|
+
try {
|
|
451
|
+
saveConfig({
|
|
452
|
+
...getConfig() || config,
|
|
453
|
+
mode: 'uninstalled',
|
|
454
|
+
uninstalled: new Date().toISOString(),
|
|
455
|
+
manifest: undefined,
|
|
456
|
+
});
|
|
457
|
+
} catch (e) {
|
|
458
|
+
// purge mode: config dir has been deleted, save is non-critical
|
|
459
|
+
if (!options.purge) throw e;
|
|
460
|
+
console.warn(` Warning: Could not save config: ${e.message}`);
|
|
461
|
+
}
|
|
417
462
|
|
|
418
463
|
// §4.12 Step 11: Clean up backup on success
|
|
419
464
|
if (!hadErrors) {
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-07-08
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 229cf7d
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.14.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|