@boyingliu01/xp-gate 0.13.2 → 0.14.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/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/__tests__/update-hooks.test.js +4 -2
- 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/sprint-flow/SKILL.md +14 -9
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +33 -2
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +63 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +58 -3
- package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +60 -1
- 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/sprint-flow/SKILL.md +14 -9
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +33 -2
- package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +63 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +58 -3
- package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +60 -1
- 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 +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/sprint-flow/SKILL.md +14 -9
- package/skills/sprint-flow/references/phase-2-design.md +33 -2
- package/skills/sprint-flow/references/phase-3-build.md +63 -0
- package/skills/sprint-flow/references/phase-5-ship.md +58 -3
- package/skills/sprint-flow/references/phase-6-close.md +60 -1
- 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
|
});
|
|
@@ -347,10 +347,12 @@ describe('updateHooks', () => {
|
|
|
347
347
|
|
|
348
348
|
describe('getProjectHooksDir', () => {
|
|
349
349
|
it('returns .git/hooks path under cwd', () => {
|
|
350
|
+
const gitDir = path.join(tmpProject, '.git');
|
|
351
|
+
fs.mkdirSync(gitDir, { recursive: true });
|
|
352
|
+
vi.spyOn(process, 'cwd').mockReturnValue(tmpProject);
|
|
350
353
|
const mod = getModule();
|
|
351
|
-
const cwd = process.cwd();
|
|
352
354
|
const result = mod.getProjectHooksDir();
|
|
353
|
-
expect(result).toBe(path.join(
|
|
355
|
+
expect(result).toBe(path.join(tmpProject, '.git', 'hooks'));
|
|
354
356
|
});
|
|
355
357
|
|
|
356
358
|
it('throws when .git/ does not exist', () => {
|
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()) {
|