deepflow 0.1.89 → 0.1.91
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/README.md +1 -7
- package/bin/install.js +17 -17
- package/bin/install.test.js +697 -0
- package/bin/ratchet.js +327 -0
- package/bin/ratchet.test.js +869 -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/auto-cycle.md +1 -143
- package/src/commands/df/auto.md +1 -1
- package/src/commands/df/execute.md +53 -26
- package/src/commands/df/verify.md +38 -8
- package/src/skills/auto-cycle/SKILL.md +148 -0
- package/templates/config-template.yaml +26 -3
- package/hooks/df-consolidation-check.js +0 -67
- package/src/commands/df/consolidate.md +0 -42
- package/src/commands/df/note.md +0 -73
- package/src/commands/df/report.md +0 -75
- package/src/commands/df/resume.md +0 -47
package/bin/ratchet.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
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
|
|
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
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Git / path helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
function gitCommonDir(cwd) {
|
|
28
|
+
try {
|
|
29
|
+
return execFileSync('git', ['rev-parse', '--git-common-dir'], {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
cwd,
|
|
32
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
33
|
+
}).trim();
|
|
34
|
+
} catch (_) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mainRepoRoot(cwd) {
|
|
40
|
+
const commonDir = gitCommonDir(cwd);
|
|
41
|
+
if (!commonDir) return cwd;
|
|
42
|
+
// --git-common-dir returns absolute path for worktrees, relative for normal repos
|
|
43
|
+
const absCommonDir = path.isAbsolute(commonDir)
|
|
44
|
+
? commonDir
|
|
45
|
+
: path.resolve(cwd, commonDir);
|
|
46
|
+
// common dir is <repo>/.git for normal repos, <repo>/.git/worktrees/<name> for worktrees
|
|
47
|
+
// Walk up until we find a directory that is not inside .git
|
|
48
|
+
return path.resolve(absCommonDir, '..', '..').replace(/[/\\]\.git.*$/, '') ||
|
|
49
|
+
path.dirname(absCommonDir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Config loading (simple regex, consistent with bin/install.js style)
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function loadConfig(repoRoot) {
|
|
57
|
+
const configPath = path.join(repoRoot, '.deepflow', 'config.yaml');
|
|
58
|
+
const cfg = {};
|
|
59
|
+
if (!fs.existsSync(configPath)) return cfg;
|
|
60
|
+
|
|
61
|
+
const text = fs.readFileSync(configPath, 'utf8');
|
|
62
|
+
|
|
63
|
+
const keys = ['build_command', 'test_command', 'typecheck_command', 'lint_command'];
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
const m = text.match(new RegExp(`^${key}:\\s*["']?([^"'\\n]+?)["']?\\s*$`, 'm'));
|
|
66
|
+
if (m) cfg[key] = m[1].trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return cfg;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Snapshot: read auto-snapshot.txt and absolutize paths
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
function loadSnapshotFiles(repoRoot) {
|
|
77
|
+
const snapshotPath = path.join(repoRoot, '.deepflow', 'auto-snapshot.txt');
|
|
78
|
+
if (!fs.existsSync(snapshotPath)) return [];
|
|
79
|
+
|
|
80
|
+
return fs.readFileSync(snapshotPath, 'utf8')
|
|
81
|
+
.split('\n')
|
|
82
|
+
.map(l => l.trim())
|
|
83
|
+
.filter(l => l.length > 0)
|
|
84
|
+
.map(rel => path.join(repoRoot, rel));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Project type detection
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
function detectProjectType(repoRoot) {
|
|
92
|
+
if (fs.existsSync(path.join(repoRoot, 'package.json'))) return 'node';
|
|
93
|
+
if (fs.existsSync(path.join(repoRoot, 'pyproject.toml'))) return 'python';
|
|
94
|
+
if (fs.existsSync(path.join(repoRoot, 'Cargo.toml'))) return 'rust';
|
|
95
|
+
if (fs.existsSync(path.join(repoRoot, 'go.mod'))) return 'go';
|
|
96
|
+
return 'unknown';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hasNpmScript(repoRoot, scriptName) {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
102
|
+
return !!(pkg.scripts && pkg.scripts[scriptName]);
|
|
103
|
+
} catch (_) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Command builders per project type
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
function buildCommands(repoRoot, projectType, snapshotFiles, cfg) {
|
|
113
|
+
const cmds = {};
|
|
114
|
+
|
|
115
|
+
if (projectType === 'node') {
|
|
116
|
+
// build
|
|
117
|
+
if (cfg.build_command) {
|
|
118
|
+
cmds.build = cfg.build_command;
|
|
119
|
+
} else if (hasNpmScript(repoRoot, 'build')) {
|
|
120
|
+
cmds.build = 'npm run build';
|
|
121
|
+
}
|
|
122
|
+
// test — always use snapshot files, never test-discovery flags
|
|
123
|
+
if (cfg.test_command) {
|
|
124
|
+
cmds.test = cfg.test_command;
|
|
125
|
+
} else if (snapshotFiles.length > 0) {
|
|
126
|
+
cmds.test = ['node', '--test', ...snapshotFiles];
|
|
127
|
+
}
|
|
128
|
+
// typecheck
|
|
129
|
+
if (cfg.typecheck_command) {
|
|
130
|
+
cmds.typecheck = cfg.typecheck_command;
|
|
131
|
+
} else {
|
|
132
|
+
// only add if tsc is available
|
|
133
|
+
cmds.typecheck = 'npx tsc --noEmit';
|
|
134
|
+
}
|
|
135
|
+
// lint
|
|
136
|
+
if (cfg.lint_command) {
|
|
137
|
+
cmds.lint = cfg.lint_command;
|
|
138
|
+
} else if (hasNpmScript(repoRoot, 'lint')) {
|
|
139
|
+
cmds.lint = 'npm run lint';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
} else if (projectType === 'python') {
|
|
143
|
+
// build: skip
|
|
144
|
+
if (cfg.build_command) cmds.build = cfg.build_command;
|
|
145
|
+
// test
|
|
146
|
+
if (cfg.test_command) {
|
|
147
|
+
cmds.test = cfg.test_command;
|
|
148
|
+
} else if (snapshotFiles.length > 0) {
|
|
149
|
+
cmds.test = ['pytest', ...snapshotFiles];
|
|
150
|
+
} else {
|
|
151
|
+
cmds.test = 'pytest';
|
|
152
|
+
}
|
|
153
|
+
// typecheck
|
|
154
|
+
if (cfg.typecheck_command) {
|
|
155
|
+
cmds.typecheck = cfg.typecheck_command;
|
|
156
|
+
} else {
|
|
157
|
+
cmds.typecheck = 'mypy .';
|
|
158
|
+
}
|
|
159
|
+
// lint
|
|
160
|
+
if (cfg.lint_command) {
|
|
161
|
+
cmds.lint = cfg.lint_command;
|
|
162
|
+
} else {
|
|
163
|
+
cmds.lint = 'ruff check .';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
} else if (projectType === 'rust') {
|
|
167
|
+
cmds.build = cfg.build_command || 'cargo build';
|
|
168
|
+
cmds.test = cfg.test_command || 'cargo test';
|
|
169
|
+
// typecheck: skip (cargo build covers it)
|
|
170
|
+
if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
|
|
171
|
+
cmds.lint = cfg.lint_command || 'cargo clippy';
|
|
172
|
+
|
|
173
|
+
} else if (projectType === 'go') {
|
|
174
|
+
cmds.build = cfg.build_command || 'go build ./...';
|
|
175
|
+
// go test doesn't work well with individual file paths — use packages
|
|
176
|
+
cmds.test = cfg.test_command || 'go test ./...';
|
|
177
|
+
// typecheck: skip
|
|
178
|
+
if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
|
|
179
|
+
cmds.lint = cfg.lint_command || 'go vet ./...';
|
|
180
|
+
|
|
181
|
+
} else {
|
|
182
|
+
// unknown: only use config overrides
|
|
183
|
+
if (cfg.build_command) cmds.build = cfg.build_command;
|
|
184
|
+
if (cfg.test_command) cmds.test = cfg.test_command;
|
|
185
|
+
if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
|
|
186
|
+
if (cfg.lint_command) cmds.lint = cfg.lint_command;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return cmds;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Command runner
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
// Parse a string command into [executable, ...args] safely
|
|
197
|
+
function parseCommand(cmd) {
|
|
198
|
+
// Very simple tokenizer — handles quoted strings and plain tokens
|
|
199
|
+
const tokens = [];
|
|
200
|
+
let current = '';
|
|
201
|
+
let inQuote = null;
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
204
|
+
const ch = cmd[i];
|
|
205
|
+
if (inQuote) {
|
|
206
|
+
if (ch === inQuote) { inQuote = null; }
|
|
207
|
+
else { current += ch; }
|
|
208
|
+
} else if (ch === '"' || ch === "'") {
|
|
209
|
+
inQuote = ch;
|
|
210
|
+
} else if (ch === ' ') {
|
|
211
|
+
if (current) { tokens.push(current); current = ''; }
|
|
212
|
+
} else {
|
|
213
|
+
current += ch;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (current) tokens.push(current);
|
|
217
|
+
return tokens;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Run a command (string or array). Returns { ok, log }.
|
|
222
|
+
* Captures stdout+stderr combined for the log.
|
|
223
|
+
*/
|
|
224
|
+
function runCommand(cmd, cwd) {
|
|
225
|
+
const args = Array.isArray(cmd) ? cmd : parseCommand(cmd);
|
|
226
|
+
const [exe, ...rest] = args;
|
|
227
|
+
|
|
228
|
+
const result = spawnSync(exe, rest, {
|
|
229
|
+
cwd,
|
|
230
|
+
encoding: 'utf8',
|
|
231
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
232
|
+
shell: false,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const log = ((result.stdout || '') + (result.stderr || '')).trim();
|
|
236
|
+
|
|
237
|
+
if (result.error) {
|
|
238
|
+
// executable not found / not available — treat as skip
|
|
239
|
+
return { ok: null, log: result.error.message };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { ok: result.status === 0, log };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if an executable is available on PATH.
|
|
247
|
+
*/
|
|
248
|
+
function commandExists(exe) {
|
|
249
|
+
const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [exe], {
|
|
250
|
+
encoding: 'utf8',
|
|
251
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
252
|
+
});
|
|
253
|
+
return result.status === 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Auto-revert
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
function autoRevert(cwd) {
|
|
261
|
+
try {
|
|
262
|
+
spawnSync('git', ['revert', 'HEAD', '--no-edit'], {
|
|
263
|
+
cwd,
|
|
264
|
+
stdio: 'ignore',
|
|
265
|
+
});
|
|
266
|
+
} catch (_) {
|
|
267
|
+
// best-effort
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Health-check stages in order
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
const STAGE_ORDER = ['build', 'test', 'typecheck', 'lint'];
|
|
276
|
+
|
|
277
|
+
// Stages where failure is SALVAGEABLE (not FAIL)
|
|
278
|
+
const SALVAGEABLE_STAGES = new Set(['lint']);
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Main
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
function main() {
|
|
285
|
+
const cwd = process.cwd();
|
|
286
|
+
const repoRoot = mainRepoRoot(cwd);
|
|
287
|
+
|
|
288
|
+
const cfg = loadConfig(repoRoot);
|
|
289
|
+
const projectType = detectProjectType(repoRoot);
|
|
290
|
+
const snapshotFiles = loadSnapshotFiles(repoRoot);
|
|
291
|
+
const cmds = buildCommands(repoRoot, projectType, snapshotFiles, cfg);
|
|
292
|
+
|
|
293
|
+
for (const stage of STAGE_ORDER) {
|
|
294
|
+
const cmd = cmds[stage];
|
|
295
|
+
if (!cmd) continue; // stage not applicable
|
|
296
|
+
|
|
297
|
+
// For string commands, check if the primary executable exists
|
|
298
|
+
if (typeof cmd === 'string') {
|
|
299
|
+
const exe = parseCommand(cmd)[0];
|
|
300
|
+
// npx is always available if npm is; skip existence check for npx
|
|
301
|
+
if (exe !== 'npx' && !commandExists(exe)) continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { ok, log } = runCommand(cmd, repoRoot);
|
|
305
|
+
|
|
306
|
+
if (ok === null) {
|
|
307
|
+
// executable spawning error — skip stage
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!ok) {
|
|
312
|
+
if (SALVAGEABLE_STAGES.has(stage)) {
|
|
313
|
+
process.stdout.write(JSON.stringify({ result: 'SALVAGEABLE', stage, log }) + '\n');
|
|
314
|
+
process.exit(2);
|
|
315
|
+
} else {
|
|
316
|
+
autoRevert(cwd);
|
|
317
|
+
process.stdout.write(JSON.stringify({ result: 'FAIL', stage, log }) + '\n');
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
process.stdout.write(JSON.stringify({ result: 'PASS' }) + '\n');
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
main();
|