deepflow 0.1.90 → 0.1.92
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/install.js +13 -4
- package/bin/ratchet.js +392 -0
- package/bin/ratchet.test.js +1173 -0
- package/bin/wave-runner.js +259 -0
- package/bin/wave-runner.test.js +556 -0
- package/hooks/df-snapshot-guard.js +105 -0
- package/hooks/df-snapshot-guard.test.js +506 -0
- package/package.json +1 -1
- package/src/commands/df/execute.md +117 -23
- package/templates/config-template.yaml +24 -0
package/bin/install.js
CHANGED
|
@@ -241,6 +241,7 @@ async function configureHooks(claudeDir) {
|
|
|
241
241
|
const dashboardPushCmd = `node "${path.join(claudeDir, 'hooks', 'df-dashboard-push.js')}"`;
|
|
242
242
|
const executionHistoryCmd = `node "${path.join(claudeDir, 'hooks', 'df-execution-history.js')}"`;
|
|
243
243
|
const worktreeGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-worktree-guard.js')}"`;
|
|
244
|
+
const snapshotGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-snapshot-guard.js')}"`;
|
|
244
245
|
const invariantCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-invariant-check.js')}"`;
|
|
245
246
|
|
|
246
247
|
let settings = {};
|
|
@@ -347,10 +348,10 @@ async function configureHooks(claudeDir) {
|
|
|
347
348
|
settings.hooks.PostToolUse = [];
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
// Remove any existing deepflow tool usage / execution history / worktree guard / invariant check hooks from PostToolUse
|
|
351
|
+
// Remove any existing deepflow tool usage / execution history / worktree guard / snapshot guard / invariant check hooks from PostToolUse
|
|
351
352
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
|
|
352
353
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
353
|
-
return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-invariant-check');
|
|
354
|
+
return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check');
|
|
354
355
|
});
|
|
355
356
|
|
|
356
357
|
// Add tool usage hook
|
|
@@ -377,6 +378,14 @@ async function configureHooks(claudeDir) {
|
|
|
377
378
|
}]
|
|
378
379
|
});
|
|
379
380
|
|
|
381
|
+
// Add snapshot guard hook (blocks Write/Edit to ratchet-baseline files in auto-snapshot.txt)
|
|
382
|
+
settings.hooks.PostToolUse.push({
|
|
383
|
+
hooks: [{
|
|
384
|
+
type: 'command',
|
|
385
|
+
command: snapshotGuardCmd
|
|
386
|
+
}]
|
|
387
|
+
});
|
|
388
|
+
|
|
380
389
|
// Add invariant check hook (exits 1 on hard failures after git commit)
|
|
381
390
|
settings.hooks.PostToolUse.push({
|
|
382
391
|
hooks: [{
|
|
@@ -566,7 +575,7 @@ async function uninstall() {
|
|
|
566
575
|
];
|
|
567
576
|
|
|
568
577
|
if (level === 'global') {
|
|
569
|
-
toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js');
|
|
578
|
+
toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js', 'hooks/df-snapshot-guard.js');
|
|
570
579
|
}
|
|
571
580
|
|
|
572
581
|
for (const item of toRemove) {
|
|
@@ -613,7 +622,7 @@ async function uninstall() {
|
|
|
613
622
|
if (settings.hooks?.PostToolUse) {
|
|
614
623
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
|
|
615
624
|
const cmd = hook.hooks?.[0]?.command || '';
|
|
616
|
-
return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-invariant-check');
|
|
625
|
+
return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check');
|
|
617
626
|
});
|
|
618
627
|
if (settings.hooks.PostToolUse.length === 0) {
|
|
619
628
|
delete settings.hooks.PostToolUse;
|
package/bin/ratchet.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deepflow ratchet
|
|
4
|
+
* Mechanical health-check gate with auto-revert on failure.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node bin/ratchet.js [--task T{N}] [--worktree PATH] [--snapshot PATH]
|
|
7
|
+
*
|
|
8
|
+
* Outputs exactly one JSON line to stdout:
|
|
9
|
+
* {"result":"PASS"}
|
|
10
|
+
* {"result":"FAIL","stage":"build","log":"..."}
|
|
11
|
+
* {"result":"SALVAGEABLE","stage":"lint","log":"..."}
|
|
12
|
+
*
|
|
13
|
+
* Exit codes: 0=PASS, 1=FAIL, 2=SALVAGEABLE
|
|
14
|
+
* On FAIL: executes `git revert HEAD --no-edit` before exiting.
|
|
15
|
+
* On PASS + --task T{N}: updates PLAN.md [ ] → [x] and appends commit hash.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Git / path helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function gitCommonDir(cwd) {
|
|
29
|
+
try {
|
|
30
|
+
return execFileSync('git', ['rev-parse', '--git-common-dir'], {
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
cwd,
|
|
33
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
34
|
+
}).trim();
|
|
35
|
+
} catch (_) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mainRepoRoot(cwd) {
|
|
41
|
+
const commonDir = gitCommonDir(cwd);
|
|
42
|
+
if (!commonDir) return cwd;
|
|
43
|
+
// --git-common-dir returns absolute path for worktrees, relative for normal repos
|
|
44
|
+
const absCommonDir = path.isAbsolute(commonDir)
|
|
45
|
+
? commonDir
|
|
46
|
+
: path.resolve(cwd, commonDir);
|
|
47
|
+
// common dir is <repo>/.git for normal repos, <repo>/.git/worktrees/<name> for worktrees
|
|
48
|
+
// Walk up until we find a directory that is not inside .git
|
|
49
|
+
return path.resolve(absCommonDir, '..', '..').replace(/[/\\]\.git.*$/, '') ||
|
|
50
|
+
path.dirname(absCommonDir);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Config loading (simple regex, consistent with bin/install.js style)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function loadConfig(repoRoot) {
|
|
58
|
+
const configPath = path.join(repoRoot, '.deepflow', 'config.yaml');
|
|
59
|
+
const cfg = {};
|
|
60
|
+
if (!fs.existsSync(configPath)) return cfg;
|
|
61
|
+
|
|
62
|
+
const text = fs.readFileSync(configPath, 'utf8');
|
|
63
|
+
|
|
64
|
+
const keys = ['build_command', 'test_command', 'typecheck_command', 'lint_command'];
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
const m = text.match(new RegExp(`^${key}:\\s*["']?([^"'\\n]+?)["']?\\s*$`, 'm'));
|
|
67
|
+
if (m) cfg[key] = m[1].trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return cfg;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Snapshot: read auto-snapshot.txt and absolutize paths
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function loadSnapshotFiles(repoRoot) {
|
|
78
|
+
const snapshotPath = path.join(repoRoot, '.deepflow', 'auto-snapshot.txt');
|
|
79
|
+
if (!fs.existsSync(snapshotPath)) return [];
|
|
80
|
+
|
|
81
|
+
return fs.readFileSync(snapshotPath, 'utf8')
|
|
82
|
+
.split('\n')
|
|
83
|
+
.map(l => l.trim())
|
|
84
|
+
.filter(l => l.length > 0)
|
|
85
|
+
.map(rel => path.join(repoRoot, rel));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Project type detection
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
function detectProjectType(repoRoot) {
|
|
93
|
+
if (fs.existsSync(path.join(repoRoot, 'package.json'))) return 'node';
|
|
94
|
+
if (fs.existsSync(path.join(repoRoot, 'pyproject.toml'))) return 'python';
|
|
95
|
+
if (fs.existsSync(path.join(repoRoot, 'Cargo.toml'))) return 'rust';
|
|
96
|
+
if (fs.existsSync(path.join(repoRoot, 'go.mod'))) return 'go';
|
|
97
|
+
return 'unknown';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hasNpmScript(repoRoot, scriptName) {
|
|
101
|
+
try {
|
|
102
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
103
|
+
return !!(pkg.scripts && pkg.scripts[scriptName]);
|
|
104
|
+
} catch (_) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Command builders per project type
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function buildCommands(repoRoot, projectType, snapshotFiles, cfg) {
|
|
114
|
+
const cmds = {};
|
|
115
|
+
|
|
116
|
+
if (projectType === 'node') {
|
|
117
|
+
// build
|
|
118
|
+
if (cfg.build_command) {
|
|
119
|
+
cmds.build = cfg.build_command;
|
|
120
|
+
} else if (hasNpmScript(repoRoot, 'build')) {
|
|
121
|
+
cmds.build = 'npm run build';
|
|
122
|
+
}
|
|
123
|
+
// test — always use snapshot files, never test-discovery flags
|
|
124
|
+
if (cfg.test_command) {
|
|
125
|
+
cmds.test = cfg.test_command;
|
|
126
|
+
} else if (snapshotFiles.length > 0) {
|
|
127
|
+
cmds.test = ['node', '--test', ...snapshotFiles];
|
|
128
|
+
}
|
|
129
|
+
// typecheck
|
|
130
|
+
if (cfg.typecheck_command) {
|
|
131
|
+
cmds.typecheck = cfg.typecheck_command;
|
|
132
|
+
} else {
|
|
133
|
+
// only add if tsc is available
|
|
134
|
+
cmds.typecheck = 'npx tsc --noEmit';
|
|
135
|
+
}
|
|
136
|
+
// lint
|
|
137
|
+
if (cfg.lint_command) {
|
|
138
|
+
cmds.lint = cfg.lint_command;
|
|
139
|
+
} else if (hasNpmScript(repoRoot, 'lint')) {
|
|
140
|
+
cmds.lint = 'npm run lint';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
} else if (projectType === 'python') {
|
|
144
|
+
// build: skip
|
|
145
|
+
if (cfg.build_command) cmds.build = cfg.build_command;
|
|
146
|
+
// test
|
|
147
|
+
if (cfg.test_command) {
|
|
148
|
+
cmds.test = cfg.test_command;
|
|
149
|
+
} else if (snapshotFiles.length > 0) {
|
|
150
|
+
cmds.test = ['pytest', ...snapshotFiles];
|
|
151
|
+
} else {
|
|
152
|
+
cmds.test = 'pytest';
|
|
153
|
+
}
|
|
154
|
+
// typecheck
|
|
155
|
+
if (cfg.typecheck_command) {
|
|
156
|
+
cmds.typecheck = cfg.typecheck_command;
|
|
157
|
+
} else {
|
|
158
|
+
cmds.typecheck = 'mypy .';
|
|
159
|
+
}
|
|
160
|
+
// lint
|
|
161
|
+
if (cfg.lint_command) {
|
|
162
|
+
cmds.lint = cfg.lint_command;
|
|
163
|
+
} else {
|
|
164
|
+
cmds.lint = 'ruff check .';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
} else if (projectType === 'rust') {
|
|
168
|
+
cmds.build = cfg.build_command || 'cargo build';
|
|
169
|
+
cmds.test = cfg.test_command || 'cargo test';
|
|
170
|
+
// typecheck: skip (cargo build covers it)
|
|
171
|
+
if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
|
|
172
|
+
cmds.lint = cfg.lint_command || 'cargo clippy';
|
|
173
|
+
|
|
174
|
+
} else if (projectType === 'go') {
|
|
175
|
+
cmds.build = cfg.build_command || 'go build ./...';
|
|
176
|
+
// go test doesn't work well with individual file paths — use packages
|
|
177
|
+
cmds.test = cfg.test_command || 'go test ./...';
|
|
178
|
+
// typecheck: skip
|
|
179
|
+
if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
|
|
180
|
+
cmds.lint = cfg.lint_command || 'go vet ./...';
|
|
181
|
+
|
|
182
|
+
} else {
|
|
183
|
+
// unknown: only use config overrides
|
|
184
|
+
if (cfg.build_command) cmds.build = cfg.build_command;
|
|
185
|
+
if (cfg.test_command) cmds.test = cfg.test_command;
|
|
186
|
+
if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
|
|
187
|
+
if (cfg.lint_command) cmds.lint = cfg.lint_command;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return cmds;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Command runner
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
// Parse a string command into [executable, ...args] safely
|
|
198
|
+
function parseCommand(cmd) {
|
|
199
|
+
// Very simple tokenizer — handles quoted strings and plain tokens
|
|
200
|
+
const tokens = [];
|
|
201
|
+
let current = '';
|
|
202
|
+
let inQuote = null;
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
205
|
+
const ch = cmd[i];
|
|
206
|
+
if (inQuote) {
|
|
207
|
+
if (ch === inQuote) { inQuote = null; }
|
|
208
|
+
else { current += ch; }
|
|
209
|
+
} else if (ch === '"' || ch === "'") {
|
|
210
|
+
inQuote = ch;
|
|
211
|
+
} else if (ch === ' ') {
|
|
212
|
+
if (current) { tokens.push(current); current = ''; }
|
|
213
|
+
} else {
|
|
214
|
+
current += ch;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (current) tokens.push(current);
|
|
218
|
+
return tokens;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Run a command (string or array). Returns { ok, log }.
|
|
223
|
+
* Captures stdout+stderr combined for the log.
|
|
224
|
+
*/
|
|
225
|
+
function runCommand(cmd, cwd) {
|
|
226
|
+
const args = Array.isArray(cmd) ? cmd : parseCommand(cmd);
|
|
227
|
+
const [exe, ...rest] = args;
|
|
228
|
+
|
|
229
|
+
const result = spawnSync(exe, rest, {
|
|
230
|
+
cwd,
|
|
231
|
+
encoding: 'utf8',
|
|
232
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
233
|
+
shell: false,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const log = ((result.stdout || '') + (result.stderr || '')).trim();
|
|
237
|
+
|
|
238
|
+
if (result.error) {
|
|
239
|
+
// executable not found / not available — treat as skip
|
|
240
|
+
return { ok: null, log: result.error.message };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { ok: result.status === 0, log };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if an executable is available on PATH.
|
|
248
|
+
*/
|
|
249
|
+
function commandExists(exe) {
|
|
250
|
+
const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [exe], {
|
|
251
|
+
encoding: 'utf8',
|
|
252
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
253
|
+
});
|
|
254
|
+
return result.status === 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Auto-revert
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
function autoRevert(cwd) {
|
|
262
|
+
try {
|
|
263
|
+
spawnSync('git', ['revert', 'HEAD', '--no-edit'], {
|
|
264
|
+
cwd,
|
|
265
|
+
stdio: 'ignore',
|
|
266
|
+
});
|
|
267
|
+
} catch (_) {
|
|
268
|
+
// best-effort
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Health-check stages in order
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
const STAGE_ORDER = ['build', 'test', 'typecheck', 'lint'];
|
|
277
|
+
|
|
278
|
+
// Stages where failure is SALVAGEABLE (not FAIL)
|
|
279
|
+
const SALVAGEABLE_STAGES = new Set(['lint']);
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// CLI argument parser
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
function parseArgs(argv) {
|
|
286
|
+
const args = { task: null, worktree: null, snapshot: null };
|
|
287
|
+
for (let i = 0; i < argv.length; i++) {
|
|
288
|
+
if (argv[i] === '--task' && argv[i + 1]) {
|
|
289
|
+
args.task = argv[++i];
|
|
290
|
+
} else if (argv[i] === '--worktree' && argv[i + 1]) {
|
|
291
|
+
args.worktree = argv[++i];
|
|
292
|
+
} else if (argv[i] === '--snapshot' && argv[i + 1]) {
|
|
293
|
+
args.snapshot = argv[++i];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return args;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// PLAN.md updater
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
function updatePlanMd(repoRoot, taskId, cwd) {
|
|
304
|
+
const planPath = path.join(repoRoot, 'PLAN.md');
|
|
305
|
+
if (!fs.existsSync(planPath)) return;
|
|
306
|
+
|
|
307
|
+
let hash = '';
|
|
308
|
+
try {
|
|
309
|
+
hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
310
|
+
encoding: 'utf8',
|
|
311
|
+
cwd,
|
|
312
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
313
|
+
}).trim();
|
|
314
|
+
} catch (_) {
|
|
315
|
+
// best-effort
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const text = fs.readFileSync(planPath, 'utf8');
|
|
319
|
+
// Match lines like: - [ ] **T54** ...
|
|
320
|
+
const re = new RegExp(`(^.*- \\[ \\].*\\*\\*${taskId}\\*\\*.*)`, 'm');
|
|
321
|
+
const updated = text.replace(re, (line) => {
|
|
322
|
+
let result = line.replace('- [ ]', '- [x]');
|
|
323
|
+
if (hash) result += ` (${hash})`;
|
|
324
|
+
return result;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (updated !== text) {
|
|
328
|
+
fs.writeFileSync(planPath, updated, 'utf8');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// Main
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
function main() {
|
|
337
|
+
const cliArgs = parseArgs(process.argv.slice(2));
|
|
338
|
+
const cwd = cliArgs.worktree || process.cwd();
|
|
339
|
+
const repoRoot = mainRepoRoot(cwd);
|
|
340
|
+
|
|
341
|
+
const cfg = loadConfig(repoRoot);
|
|
342
|
+
const projectType = detectProjectType(repoRoot);
|
|
343
|
+
const snapshotFiles = loadSnapshotFiles(repoRoot);
|
|
344
|
+
const cmds = buildCommands(repoRoot, projectType, snapshotFiles, cfg);
|
|
345
|
+
// --snapshot flag overrides the snapshot-derived test command
|
|
346
|
+
if (cliArgs.snapshot && fs.existsSync(cliArgs.snapshot)) {
|
|
347
|
+
const snapFiles = fs.readFileSync(cliArgs.snapshot, 'utf8')
|
|
348
|
+
.split('\n').map(l => l.trim()).filter(l => l.length > 0)
|
|
349
|
+
.map(rel => path.isAbsolute(rel) ? rel : path.join(repoRoot, rel));
|
|
350
|
+
if (snapFiles.length > 0 && projectType === 'node' && !cfg.test_command) {
|
|
351
|
+
cmds.test = ['node', '--test', ...snapFiles];
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const stage of STAGE_ORDER) {
|
|
356
|
+
const cmd = cmds[stage];
|
|
357
|
+
if (!cmd) continue; // stage not applicable
|
|
358
|
+
|
|
359
|
+
// For string commands, check if the primary executable exists
|
|
360
|
+
if (typeof cmd === 'string') {
|
|
361
|
+
const exe = parseCommand(cmd)[0];
|
|
362
|
+
// npx is always available if npm is; skip existence check for npx
|
|
363
|
+
if (exe !== 'npx' && !commandExists(exe)) continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const { ok, log } = runCommand(cmd, repoRoot);
|
|
367
|
+
|
|
368
|
+
if (ok === null) {
|
|
369
|
+
// executable spawning error — skip stage
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!ok) {
|
|
374
|
+
if (SALVAGEABLE_STAGES.has(stage)) {
|
|
375
|
+
process.stdout.write(JSON.stringify({ result: 'SALVAGEABLE', stage, log }) + '\n');
|
|
376
|
+
process.exit(2);
|
|
377
|
+
} else {
|
|
378
|
+
autoRevert(cwd);
|
|
379
|
+
process.stdout.write(JSON.stringify({ result: 'FAIL', stage, log }) + '\n');
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
process.stdout.write(JSON.stringify({ result: 'PASS' }) + '\n');
|
|
386
|
+
if (cliArgs.task) {
|
|
387
|
+
updatePlanMd(repoRoot, cliArgs.task, cwd);
|
|
388
|
+
}
|
|
389
|
+
process.exit(0);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
main();
|