@boyingliu01/xp-gate 0.8.2 → 0.8.5
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 +111 -129
- package/hooks/pre-commit +5 -4
- package/lib/__tests__/baseline.test.js +123 -0
- package/lib/__tests__/migrate.test.js +20 -0
- package/lib/__tests__/uninstall.test.js +36 -0
- package/lib/__tests__/update-skill.test.js +11 -0
- package/lib/baseline.js +392 -0
- package/lib/doctor.js +127 -141
- package/lib/gate-audit.ts +25 -8
- package/lib/init.js +13 -2
- package/lib/migrate.js +74 -73
- package/lib/uninstall.js +63 -31
- package/lib/update-skill.js +32 -34
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +31 -0
- package/plugins/opencode/README.md +1 -1
- package/plugins/opencode/package.json +29 -5
- package/plugins/opencode/scripts/prepack.cjs +85 -0
- package/plugins/opencode/skills/sprint-flow/SKILL.md +31 -0
- package/skills/sprint-flow/SKILL.md +31 -0
package/bin/xp-gate.js
CHANGED
|
@@ -7,57 +7,96 @@ const { uninstall } = require('../lib/uninstall.js');
|
|
|
7
7
|
const { doctor } = require('../lib/doctor.js');
|
|
8
8
|
const { checkDeps } = require('../lib/detect-deps.js');
|
|
9
9
|
const { migrate } = require('../lib/migrate.js');
|
|
10
|
+
const { handleBaseline } = require('../lib/baseline.js');
|
|
11
|
+
|
|
12
|
+
function handleUIReview() {
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
const uiReviewPath = path.join(__dirname, '..', 'lib', 'ui-review.ts');
|
|
15
|
+
try {
|
|
16
|
+
execSync(`npx -y tsx "${uiReviewPath}"`, { stdio: 'inherit' });
|
|
17
|
+
process.exit(0);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
const COMMANDS = {
|
|
12
24
|
'init': {
|
|
13
25
|
description: 'Initialize xp-gate (use --global for all projects)',
|
|
14
|
-
|
|
26
|
+
run: subargs => init(subargs).then(code => process.exit(code)),
|
|
15
27
|
usage: 'xp-gate init [--global]'
|
|
16
28
|
},
|
|
17
29
|
'setup-global': {
|
|
18
30
|
description: 'Set up xp-gate globally for all git projects',
|
|
19
|
-
|
|
31
|
+
run: () => init(['--global']).then(code => process.exit(code)),
|
|
20
32
|
usage: 'xp-gate setup-global'
|
|
21
33
|
},
|
|
22
34
|
'install-skill': {
|
|
23
35
|
description: 'Install a xp-gate skill from GitHub',
|
|
24
|
-
|
|
36
|
+
run: subargs => {
|
|
37
|
+
const name = subargs[0];
|
|
38
|
+
if (!name) {
|
|
39
|
+
console.error('Error: Skill name required');
|
|
40
|
+
console.error('Usage: xp-gate install-skill <name>[@<version>]');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const options = parseOptions(subargs.slice(1));
|
|
44
|
+
installSkill(name, options).then(code => process.exit(code));
|
|
45
|
+
},
|
|
25
46
|
usage: 'xp-gate install-skill <name>[@<version>] [--offline] [--verbose] [--force]'
|
|
26
47
|
},
|
|
27
48
|
'update-skill': {
|
|
28
49
|
description: 'Update installed skill(s)',
|
|
29
|
-
|
|
50
|
+
run: subargs => {
|
|
51
|
+
const name = subargs[0];
|
|
52
|
+
const options = parseOptions(subargs.slice(1));
|
|
53
|
+
updateSkill(name, options).then(code => process.exit(code));
|
|
54
|
+
},
|
|
30
55
|
usage: 'xp-gate update-skill [<name>] [--all] [--check]'
|
|
31
56
|
},
|
|
32
57
|
'uninstall-skill': {
|
|
33
58
|
description: 'Uninstall a xp-gate skill',
|
|
34
|
-
|
|
59
|
+
run: subargs => {
|
|
60
|
+
const name = subargs[0];
|
|
61
|
+
if (!name) {
|
|
62
|
+
console.error('Error: Skill name required');
|
|
63
|
+
console.error('Usage: xp-gate uninstall-skill <name>');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const options = parseOptions(subargs.slice(1));
|
|
67
|
+
uninstallSkill(name, options).then(code => process.exit(code));
|
|
68
|
+
},
|
|
35
69
|
usage: 'xp-gate uninstall-skill <name> [--force]'
|
|
36
70
|
},
|
|
37
71
|
'uninstall': {
|
|
38
72
|
description: 'Uninstall xp-gate (reverse of init)',
|
|
39
|
-
|
|
73
|
+
run: subargs => uninstall(subargs).then(code => process.exit(code)),
|
|
40
74
|
usage: 'xp-gate uninstall [--dry-run] [--force] [--local|--global]'
|
|
41
75
|
},
|
|
42
76
|
'migrate': {
|
|
43
77
|
description: 'Migrate from v0.4.x (GitHub Packages) to v0.5.x (public npm)',
|
|
44
|
-
|
|
78
|
+
run: subargs => migrate(subargs).then(code => process.exit(code)),
|
|
45
79
|
usage: 'xp-gate migrate [--dry-run]'
|
|
46
80
|
},
|
|
47
81
|
'doctor': {
|
|
48
82
|
description: 'Diagnose xp-gate installation health',
|
|
49
|
-
|
|
83
|
+
run: subargs => doctor(subargs).then(code => process.exit(code)),
|
|
50
84
|
usage: 'xp-gate doctor [--fix]'
|
|
51
85
|
},
|
|
52
86
|
'ui-review': {
|
|
53
87
|
description: 'Run UI review for non-sprint developers (generates .ui-gate-result.json)',
|
|
54
|
-
|
|
88
|
+
run: () => handleUIReview(),
|
|
55
89
|
usage: 'xp-gate ui-review'
|
|
56
90
|
},
|
|
57
91
|
'audit': {
|
|
58
92
|
description: 'Gate audit logging (record, --tail, --stats)',
|
|
59
|
-
|
|
93
|
+
run: subargs => handleAudit(subargs),
|
|
60
94
|
usage: 'xp-gate audit [--tail [N]|--stats|record --gate-id X --gate-name Y ...]'
|
|
95
|
+
},
|
|
96
|
+
'baseline': {
|
|
97
|
+
description: 'Manage lint baselines (create, show, reset, diff)',
|
|
98
|
+
run: subargs => handleBaseline(subargs).then(code => process.exit(code)),
|
|
99
|
+
usage: 'xp-gate baseline <create|show|reset|diff>'
|
|
61
100
|
}
|
|
62
101
|
};
|
|
63
102
|
|
|
@@ -92,151 +131,94 @@ function main() {
|
|
|
92
131
|
|
|
93
132
|
const command = args[0];
|
|
94
133
|
const subargs = args.slice(1);
|
|
134
|
+
const cmd = COMMANDS[command];
|
|
95
135
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
136
|
+
if (!cmd) {
|
|
137
|
+
console.error(`Unknown command: ${command}`);
|
|
138
|
+
printHelp();
|
|
139
|
+
process.exit(1);
|
|
99
140
|
return;
|
|
100
141
|
}
|
|
101
142
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
143
|
+
cmd.run(subargs);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleAuditTail(args, auditPath) {
|
|
147
|
+
const tailIdx = args.indexOf('--tail');
|
|
148
|
+
const count = parseInt(args[tailIdx + 1] || '20', 10);
|
|
149
|
+
try {
|
|
150
|
+
const { readTailEntries } = require(auditPath);
|
|
151
|
+
const entries = readTailEntries(count);
|
|
152
|
+
if (entries.length === 0) {
|
|
153
|
+
console.log('No audit entries found.');
|
|
108
154
|
return;
|
|
109
155
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
156
|
+
printAuditTable(entries);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('Error reading audit entries:', err.message);
|
|
159
|
+
process.exit(1);
|
|
113
160
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (command === 'uninstall-skill') {
|
|
123
|
-
const name = subargs[0];
|
|
124
|
-
if (!name) {
|
|
125
|
-
console.error('Error: Skill name required');
|
|
126
|
-
console.error('Usage: xp-gate uninstall-skill <name>');
|
|
127
|
-
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleAuditStats(auditPath) {
|
|
164
|
+
try {
|
|
165
|
+
const { computeStats } = require(auditPath);
|
|
166
|
+
const stats = computeStats();
|
|
167
|
+
if (stats.length === 0) {
|
|
168
|
+
console.log('No audit data found.');
|
|
128
169
|
return;
|
|
129
170
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (command === 'uninstall') {
|
|
136
|
-
uninstall(subargs).then(code => process.exit(code));
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (command === 'migrate') {
|
|
141
|
-
migrate(subargs).then(code => process.exit(code));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (command === 'doctor') {
|
|
146
|
-
doctor(subargs).then(code => process.exit(code));
|
|
147
|
-
return;
|
|
171
|
+
printStatsTable(stats);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('Error computing stats:', err.message);
|
|
174
|
+
process.exit(1);
|
|
148
175
|
}
|
|
176
|
+
}
|
|
149
177
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
178
|
+
function handleAuditRecord(args, auditPath) {
|
|
179
|
+
try {
|
|
180
|
+
const { appendAuditEntry } = require(auditPath);
|
|
181
|
+
const rest = args.slice(1);
|
|
182
|
+
const opts = {};
|
|
183
|
+
for (let i = 0; i < rest.length; i++) {
|
|
184
|
+
if (rest[i].startsWith('--') && i + 1 < rest.length) {
|
|
185
|
+
opts[rest[i].slice(2)] = rest[i + 1];
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
159
188
|
}
|
|
189
|
+
const entry = {
|
|
190
|
+
timestamp: new Date().toISOString(),
|
|
191
|
+
gate_id: opts['gate-id'] || 'unknown',
|
|
192
|
+
gate_name: opts['gate-name'] || 'unknown',
|
|
193
|
+
passed: opts['passed'] === 'true',
|
|
194
|
+
issues_found: parseInt(opts['issues-found'] || '0', 10),
|
|
195
|
+
duration_ms: parseInt(opts['duration-ms'] || '0', 10),
|
|
196
|
+
trigger: opts['trigger'] || 'manual',
|
|
197
|
+
repo_path: process.cwd(),
|
|
198
|
+
commit_hash: opts['commit-hash'] || 'HEAD',
|
|
199
|
+
};
|
|
200
|
+
appendAuditEntry(entry);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error('Error recording audit entry:', err.message);
|
|
203
|
+
process.exit(1);
|
|
160
204
|
}
|
|
161
|
-
|
|
162
|
-
if (command === 'audit') {
|
|
163
|
-
handleAudit(subargs);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
console.error(`Unknown command: ${command}`);
|
|
168
|
-
printHelp();
|
|
169
|
-
process.exit(1);
|
|
170
205
|
}
|
|
171
206
|
|
|
172
207
|
function handleAudit(args) {
|
|
173
|
-
const path = require('path');
|
|
174
208
|
const auditPath = path.join(__dirname, '..', 'lib', 'gate-audit.ts');
|
|
175
209
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (tailIdx !== -1) {
|
|
179
|
-
const count = parseInt(args[tailIdx + 1] || '20', 10);
|
|
180
|
-
try {
|
|
181
|
-
const { readTailEntries } = require(auditPath);
|
|
182
|
-
const entries = readTailEntries(count);
|
|
183
|
-
if (entries.length === 0) {
|
|
184
|
-
console.log('No audit entries found.');
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
printAuditTable(entries);
|
|
188
|
-
} catch (err) {
|
|
189
|
-
console.error('Error reading audit entries:', err.message);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
210
|
+
if (args.includes('--tail')) {
|
|
211
|
+
handleAuditTail(args, auditPath);
|
|
192
212
|
return;
|
|
193
213
|
}
|
|
194
214
|
|
|
195
|
-
// --stats
|
|
196
215
|
if (args.includes('--stats')) {
|
|
197
|
-
|
|
198
|
-
const { computeStats } = require(auditPath);
|
|
199
|
-
const stats = computeStats();
|
|
200
|
-
if (stats.length === 0) {
|
|
201
|
-
console.log('No audit data found.');
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
printStatsTable(stats);
|
|
205
|
-
} catch (err) {
|
|
206
|
-
console.error('Error computing stats:', err.message);
|
|
207
|
-
process.exit(1);
|
|
208
|
-
}
|
|
216
|
+
handleAuditStats(auditPath);
|
|
209
217
|
return;
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
// record --gate-id X --gate-name Y --passed true/false ...
|
|
213
220
|
if (args[0] === 'record') {
|
|
214
|
-
|
|
215
|
-
const { appendAuditEntry } = require(auditPath);
|
|
216
|
-
const rest = args.slice(1);
|
|
217
|
-
const opts = {};
|
|
218
|
-
for (let i = 0; i < rest.length; i++) {
|
|
219
|
-
if (rest[i].startsWith('--') && i + 1 < rest.length) {
|
|
220
|
-
opts[rest[i].slice(2)] = rest[i + 1];
|
|
221
|
-
i++;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const entry = {
|
|
225
|
-
timestamp: new Date().toISOString(),
|
|
226
|
-
gate_id: opts['gate-id'] || 'unknown',
|
|
227
|
-
gate_name: opts['gate-name'] || 'unknown',
|
|
228
|
-
passed: opts['passed'] === 'true',
|
|
229
|
-
issues_found: parseInt(opts['issues-found'] || '0', 10),
|
|
230
|
-
duration_ms: parseInt(opts['duration-ms'] || '0', 10),
|
|
231
|
-
trigger: opts['trigger'] || 'manual',
|
|
232
|
-
repo_path: process.cwd(),
|
|
233
|
-
commit_hash: opts['commit-hash'] || 'HEAD',
|
|
234
|
-
};
|
|
235
|
-
appendAuditEntry(entry);
|
|
236
|
-
} catch (err) {
|
|
237
|
-
console.error('Error recording audit entry:', err.message);
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
221
|
+
handleAuditRecord(args, auditPath);
|
|
240
222
|
return;
|
|
241
223
|
}
|
|
242
224
|
|
package/hooks/pre-commit
CHANGED
|
@@ -32,14 +32,15 @@ source "$ADAPTER_DIR/adapter-common.sh" 2>/dev/null || {
|
|
|
32
32
|
# Trap: ensure quality report generated on ANY exit (pass or fail).
|
|
33
33
|
# Using a guard prevents recursion when 'exit' is called from inside the trap.
|
|
34
34
|
_QUALITY_REPORT_DONE=0
|
|
35
|
-
_quality_report_on_exit() {
|
|
35
|
+
_quality_report_on_exit() {
|
|
36
|
+
local _saved_exit=$?
|
|
36
37
|
if [ "$_QUALITY_REPORT_DONE" = "1" ]; then
|
|
37
|
-
exit
|
|
38
|
+
exit $_saved_exit
|
|
38
39
|
return
|
|
39
40
|
fi
|
|
40
41
|
_QUALITY_REPORT_DONE=1
|
|
41
|
-
generate_quality_report
|
|
42
|
-
exit
|
|
42
|
+
command -v generate_quality_report >/dev/null 2>&1 && generate_quality_report 2>/dev/null || true
|
|
43
|
+
exit $_saved_exit
|
|
43
44
|
}
|
|
44
45
|
trap '_quality_report_on_exit' EXIT
|
|
45
46
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-152 Baseline CLI handler
|
|
3
|
+
* @intent Verify baseline create/show/reset/diff commands
|
|
4
|
+
* @covers AC-152
|
|
5
|
+
*/
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const { handleBaseline, showBaseline } = require('../baseline.js');
|
|
11
|
+
|
|
12
|
+
describe('baseline.js - CLI handler', () => {
|
|
13
|
+
let tmpDir;
|
|
14
|
+
const originalCwd = process.cwd;
|
|
15
|
+
|
|
16
|
+
afterAll(() => {
|
|
17
|
+
process.cwd = originalCwd;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'baseline-cli-test-'));
|
|
22
|
+
process.cwd = () => tmpDir;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('handleBaseline - help', () => {
|
|
26
|
+
it('prints usage when no subcommand given', async () => {
|
|
27
|
+
const logs = [];
|
|
28
|
+
const origLog = console.log;
|
|
29
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
30
|
+
|
|
31
|
+
const code = await handleBaseline([]);
|
|
32
|
+
expect(code).toBe(0);
|
|
33
|
+
expect(logs.some(l => l.includes('Usage'))).toBe(true);
|
|
34
|
+
|
|
35
|
+
console.log = origLog;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('prints usage for --help', async () => {
|
|
39
|
+
const logs = [];
|
|
40
|
+
const origLog = console.log;
|
|
41
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
42
|
+
|
|
43
|
+
const code = await handleBaseline(['--help']);
|
|
44
|
+
expect(code).toBe(0);
|
|
45
|
+
expect(logs.some(l => l.includes('Usage'))).toBe(true);
|
|
46
|
+
|
|
47
|
+
console.log = origLog;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('handleBaseline - unknown subcommand', () => {
|
|
52
|
+
it('returns error for unknown subcommand', async () => {
|
|
53
|
+
const logs = [];
|
|
54
|
+
const errs = [];
|
|
55
|
+
const origLog = console.log;
|
|
56
|
+
const origErr = console.error;
|
|
57
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
58
|
+
console.error = (...args) => errs.push(args.join(' '));
|
|
59
|
+
|
|
60
|
+
const code = await handleBaseline(['unknown-cmd']);
|
|
61
|
+
expect(code).toBe(1);
|
|
62
|
+
expect(errs.some(l => l.includes('Unknown baseline subcommand'))).toBe(true);
|
|
63
|
+
|
|
64
|
+
console.log = origLog;
|
|
65
|
+
console.error = origErr;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('showBaseline - no baseline file', () => {
|
|
70
|
+
it('shows message when no baseline exists', () => {
|
|
71
|
+
const logs = [];
|
|
72
|
+
const origLog = console.log;
|
|
73
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
74
|
+
|
|
75
|
+
const code = showBaseline();
|
|
76
|
+
expect(code).toBe(1);
|
|
77
|
+
expect(logs.some(l => l.includes('No baseline'))).toBe(true);
|
|
78
|
+
|
|
79
|
+
console.log = origLog;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('showBaseline - with baseline file', () => {
|
|
84
|
+
it('shows summary when baseline exists', () => {
|
|
85
|
+
const baselinePath = path.join(tmpDir, '.xp-gate', 'lint-baseline.json');
|
|
86
|
+
fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
|
|
87
|
+
fs.writeFileSync(baselinePath, JSON.stringify({
|
|
88
|
+
'src/app.ts': {
|
|
89
|
+
eslint: { warnings: 3, errors: 1 },
|
|
90
|
+
totalWarnings: 3,
|
|
91
|
+
lastAnalyzed: '2026-06-01T12:00:00.000Z',
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
const logs = [];
|
|
96
|
+
const origLog = console.log;
|
|
97
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
98
|
+
|
|
99
|
+
const code = showBaseline();
|
|
100
|
+
expect(code).toBe(0);
|
|
101
|
+
expect(logs.some(l => l.includes('3'))).toBe(true);
|
|
102
|
+
expect(logs.some(l => l.includes('ESLint'))).toBe(true);
|
|
103
|
+
|
|
104
|
+
console.log = origLog;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('shows empty message for baseline with no entries', () => {
|
|
108
|
+
const baselinePath = path.join(tmpDir, '.xp-gate', 'lint-baseline.json');
|
|
109
|
+
fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
|
|
110
|
+
fs.writeFileSync(baselinePath, '{}');
|
|
111
|
+
|
|
112
|
+
const logs = [];
|
|
113
|
+
const origLog = console.log;
|
|
114
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
115
|
+
|
|
116
|
+
const code = showBaseline();
|
|
117
|
+
expect(code).toBe(0);
|
|
118
|
+
expect(logs.some(l => l.includes('empty'))).toBe(true);
|
|
119
|
+
|
|
120
|
+
console.log = origLog;
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -193,6 +193,26 @@ describe('migrate', () => {
|
|
|
193
193
|
|
|
194
194
|
// === Print summary ===
|
|
195
195
|
|
|
196
|
+
// === Phase isolation: removing npmrc should not affect cache check ===
|
|
197
|
+
|
|
198
|
+
it('phases are isolated: npmrc cleanup and cache check operate independently', async () => {
|
|
199
|
+
// Only npmrc has github packages lines, no cache dir
|
|
200
|
+
fs.writeFileSync(npmrcPath(), [
|
|
201
|
+
'//npm.pkg.github.com/:_authToken=ghp_abc123',
|
|
202
|
+
'registry=https://registry.npmjs.org/'
|
|
203
|
+
].join('\n') + '\n');
|
|
204
|
+
|
|
205
|
+
const { migrate } = require('../migrate');
|
|
206
|
+
const result = await migrate([]);
|
|
207
|
+
|
|
208
|
+
expect(result).toBe(0);
|
|
209
|
+
// npmrc should be cleaned
|
|
210
|
+
const content = fs.readFileSync(npmrcPath(), 'utf8');
|
|
211
|
+
expect(content).not.toContain('npm.pkg.github.com');
|
|
212
|
+
// Should report no old cache (dir doesn't exist)
|
|
213
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('No old cache'));
|
|
214
|
+
});
|
|
215
|
+
|
|
196
216
|
it('prints full summary of actions taken', async () => {
|
|
197
217
|
fs.writeFileSync(npmrcPath(), [
|
|
198
218
|
'//npm.pkg.github.com/:_authToken=ghp_abc123',
|
|
@@ -577,6 +577,42 @@ describe('uninstall', () => {
|
|
|
577
577
|
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('adapter-common.sh'));
|
|
578
578
|
});
|
|
579
579
|
|
|
580
|
+
// === Execution loop: mixed item types plan ===
|
|
581
|
+
|
|
582
|
+
it('executes mixed-type plan (file + dir + gitconfig items) correctly', async () => {
|
|
583
|
+
setupGlobalInstall();
|
|
584
|
+
const expectedHooksPath = globalHooksDir();
|
|
585
|
+
mockExecGlobalHooksPath(expectedHooksPath);
|
|
586
|
+
const { uninstall } = require('../uninstall');
|
|
587
|
+
|
|
588
|
+
const result = await uninstall([]);
|
|
589
|
+
|
|
590
|
+
expect(result).toBe(0);
|
|
591
|
+
|
|
592
|
+
// Gitconfig type unset core.hooksPath
|
|
593
|
+
expect(execSpy).toHaveBeenCalledWith(
|
|
594
|
+
expect.stringContaining('git config --global --unset core.hooksPath'),
|
|
595
|
+
expect.any(Object)
|
|
596
|
+
);
|
|
597
|
+
// Dir items removed: global hooks, adapters, template
|
|
598
|
+
expect(fs.existsSync(globalHooksDir())).toBe(false);
|
|
599
|
+
expect(fs.existsSync(globalAdaptersDir())).toBe(false);
|
|
600
|
+
expect(fs.existsSync(templateDir())).toBe(false);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('skips non-existent file items in execution loop gracefully', async () => {
|
|
604
|
+
setupLocalInstall();
|
|
605
|
+
// Delete one file before uninstall
|
|
606
|
+
fs.unlinkSync(path.join(projectHooksDir(), 'pre-commit'));
|
|
607
|
+
mockExecSuccess();
|
|
608
|
+
const { uninstall } = require('../uninstall');
|
|
609
|
+
|
|
610
|
+
const result = await uninstall([]);
|
|
611
|
+
expect(result).toBe(0);
|
|
612
|
+
// Should not crash — remaining items still processed
|
|
613
|
+
expect(fs.existsSync(path.join(projectHooksDir(), 'pre-push'))).toBe(false);
|
|
614
|
+
});
|
|
615
|
+
|
|
580
616
|
it('saves rollback snapshot before destructive operations', async () => {
|
|
581
617
|
setupLocalInstall();
|
|
582
618
|
mockExecSuccess();
|
|
@@ -263,6 +263,17 @@ describe('update-skill', () => {
|
|
|
263
263
|
expect(errorSpy).toHaveBeenCalledWith('Error: foo is not installed');
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
// === Mode handler isolation: check/all/single operate independently ===
|
|
267
|
+
|
|
268
|
+
it('check and all modes handle empty config without crashing (edge cases)', async () => {
|
|
269
|
+
// No config file at all — getConfig returns {}
|
|
270
|
+
const { updateSkill } = require('../update-skill');
|
|
271
|
+
expect(await updateSkill(null, { check: true })).toBe(0);
|
|
272
|
+
expect(await updateSkill(null, { all: true })).toBe(0);
|
|
273
|
+
expect(await updateSkill('anything')).toBe(1);
|
|
274
|
+
expect(errorSpy).toHaveBeenCalledWith('Error: anything is not installed');
|
|
275
|
+
});
|
|
276
|
+
|
|
266
277
|
it('getConfig parses valid config (single-name path succeeds)', async () => {
|
|
267
278
|
writeConfig({ foo: { version: '1.0.0' } });
|
|
268
279
|
const { updateSkill } = require('../update-skill');
|