deepflow 0.1.104 → 0.1.106

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.
@@ -11,31 +11,20 @@
11
11
  'use strict';
12
12
 
13
13
  const fs = require('fs');
14
+ const { readStdinIfMain } = require('./lib/hook-stdin');
14
15
 
15
- let raw = '';
16
- process.stdin.setEncoding('utf8');
17
- process.stdin.on('data', chunk => { raw += chunk; });
18
- process.stdin.on('end', () => {
19
- try {
20
- // Write raw payload for inspection
21
- fs.writeFileSync('/tmp/df-posttooluse-payload.json', raw);
22
-
23
- // Also append a minimal summary line for quick review
24
- const data = JSON.parse(raw);
25
- const summary = {
26
- hook_event_name: data.hook_event_name,
27
- tool_name: data.tool_name,
28
- tool_use_id: data.tool_use_id,
29
- session_id: data.session_id,
30
- cwd: data.cwd,
31
- permission_mode: data.permission_mode,
32
- tool_input_keys: data.tool_input ? Object.keys(data.tool_input) : [],
33
- tool_response_keys: data.tool_response ? Object.keys(data.tool_response) : [],
34
- transcript_path: data.transcript_path,
35
- };
36
- fs.appendFileSync('/tmp/df-posttooluse-summary.jsonl', JSON.stringify(summary) + '\n');
37
- } catch (_e) {
38
- // Fail silently — never break tool execution
39
- }
40
- process.exit(0);
16
+ readStdinIfMain(module, (data) => {
17
+ // Also append a minimal summary line for quick review
18
+ const summary = {
19
+ hook_event_name: data.hook_event_name,
20
+ tool_name: data.tool_name,
21
+ tool_use_id: data.tool_use_id,
22
+ session_id: data.session_id,
23
+ cwd: data.cwd,
24
+ permission_mode: data.permission_mode,
25
+ tool_input_keys: data.tool_input ? Object.keys(data.tool_input) : [],
26
+ tool_response_keys: data.tool_response ? Object.keys(data.tool_response) : [],
27
+ transcript_path: data.transcript_path,
28
+ };
29
+ fs.appendFileSync('/tmp/df-posttooluse-summary.jsonl', JSON.stringify(summary) + '\n');
41
30
  });
@@ -15,6 +15,7 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
+ const { readStdinIfMain } = require('./lib/hook-stdin');
18
19
 
19
20
  const TOOL_USAGE_LOG = path.join(os.homedir(), '.claude', 'tool-usage.jsonl');
20
21
 
@@ -49,56 +50,45 @@ function extractTaskId(cwd) {
49
50
  return taskMatch ? taskMatch[1].toUpperCase() : null;
50
51
  }
51
52
 
52
- // Read all stdin, then process
53
- let raw = '';
54
- process.stdin.setEncoding('utf8');
55
- process.stdin.on('data', chunk => { raw += chunk; });
56
- process.stdin.on('end', () => {
57
- try {
58
- const data = JSON.parse(raw);
59
-
60
- const toolName = data.tool_name || null;
61
- const toolResponse = data.tool_response;
62
- const cwd = data.cwd || '';
63
-
64
- let activeCommand = null;
65
- try {
66
- const markerPath = path.join(cwd || process.cwd(), '.deepflow', 'active-command.json');
67
- const markerRaw = fs.readFileSync(markerPath, 'utf8');
68
- activeCommand = JSON.parse(markerRaw).command || null;
69
- } catch (_e) { /* no marker or unreadable — null */ }
53
+ readStdinIfMain(module, (data) => {
54
+ const toolName = data.tool_name || null;
55
+ const toolResponse = data.tool_response;
56
+ const cwd = data.cwd || '';
70
57
 
71
- // Extract a compact tool_input summary per tool type
72
- const ti = data.tool_input || {};
73
- let inputSummary = null;
74
- if (toolName === 'Bash') inputSummary = ti.command || null;
75
- else if (toolName === 'LSP') inputSummary = `${ti.operation || '?'}:${(ti.filePath || '').split('/').pop()}:${ti.line || '?'}`;
76
- else if (toolName === 'Read') inputSummary = (ti.file_path || '').split('/').pop() + (ti.offset ? `:${ti.offset}-${ti.offset + (ti.limit || 0)}` : '');
77
- else if (toolName === 'Grep') inputSummary = ti.pattern || null;
78
- else if (toolName === 'Glob') inputSummary = ti.pattern || null;
79
- else if (toolName === 'Agent') inputSummary = `${ti.subagent_type || '?'}/${ti.model || '?'}`;
80
- else if (toolName === 'Edit' || toolName === 'Write') inputSummary = (ti.file_path || '').split('/').pop();
58
+ let activeCommand = null;
59
+ try {
60
+ const markerPath = path.join(cwd || process.cwd(), '.deepflow', 'active-command.json');
61
+ const markerRaw = fs.readFileSync(markerPath, 'utf8');
62
+ activeCommand = JSON.parse(markerRaw).command || null;
63
+ } catch (_e) { /* no marker or unreadable null */ }
81
64
 
82
- const record = {
83
- timestamp: new Date().toISOString(),
84
- session_id: data.session_id || null,
85
- tool_name: toolName,
86
- input: inputSummary,
87
- output_size_est_tokens: Math.ceil(JSON.stringify(toolResponse).length / 4),
88
- project: cwd ? path.basename(cwd) : null,
89
- phase: inferPhase(cwd),
90
- task_id: extractTaskId(cwd),
91
- active_command: activeCommand,
92
- };
65
+ // Extract a compact tool_input summary per tool type
66
+ const ti = data.tool_input || {};
67
+ let inputSummary = null;
68
+ if (toolName === 'Bash') inputSummary = ti.command || null;
69
+ else if (toolName === 'LSP') inputSummary = `${ti.operation || '?'}:${(ti.filePath || '').split('/').pop()}:${ti.line || '?'}`;
70
+ else if (toolName === 'Read') inputSummary = (ti.file_path || '').split('/').pop() + (ti.offset ? `:${ti.offset}-${ti.offset + (ti.limit || 0)}` : '');
71
+ else if (toolName === 'Grep') inputSummary = ti.pattern || null;
72
+ else if (toolName === 'Glob') inputSummary = ti.pattern || null;
73
+ else if (toolName === 'Agent') inputSummary = `${ti.subagent_type || '?'}/${ti.model || '?'}`;
74
+ else if (toolName === 'Edit' || toolName === 'Write') inputSummary = (ti.file_path || '').split('/').pop();
93
75
 
94
- const logDir = path.dirname(TOOL_USAGE_LOG);
95
- if (!fs.existsSync(logDir)) {
96
- fs.mkdirSync(logDir, { recursive: true });
97
- }
76
+ const record = {
77
+ timestamp: new Date().toISOString(),
78
+ session_id: data.session_id || null,
79
+ tool_name: toolName,
80
+ input: inputSummary,
81
+ output_size_est_tokens: Math.ceil(JSON.stringify(toolResponse).length / 4),
82
+ project: cwd ? path.basename(cwd) : null,
83
+ phase: inferPhase(cwd),
84
+ task_id: extractTaskId(cwd),
85
+ active_command: activeCommand,
86
+ };
98
87
 
99
- fs.appendFileSync(TOOL_USAGE_LOG, JSON.stringify(record) + '\n');
100
- } catch (_e) {
101
- // Fail silently never break tool execution
88
+ const logDir = path.dirname(TOOL_USAGE_LOG);
89
+ if (!fs.existsSync(logDir)) {
90
+ fs.mkdirSync(logDir, { recursive: true });
102
91
  }
103
- process.exit(0);
92
+
93
+ fs.appendFileSync(TOOL_USAGE_LOG, JSON.stringify(record) + '\n');
104
94
  });
@@ -19,6 +19,7 @@
19
19
  'use strict';
20
20
 
21
21
  const { execFileSync } = require('child_process');
22
+ const { readStdinIfMain } = require('./lib/hook-stdin');
22
23
 
23
24
  // Paths that are always allowed regardless of worktree state
24
25
  const ALLOWLIST = [
@@ -56,47 +57,38 @@ function dfWorktreeExists(cwd) {
56
57
  }
57
58
  }
58
59
 
59
- let raw = '';
60
- process.stdin.setEncoding('utf8');
61
- process.stdin.on('data', chunk => { raw += chunk; });
62
- process.stdin.on('end', () => {
63
- try {
64
- const data = JSON.parse(raw);
65
- const toolName = data.tool_name || '';
66
-
67
- // Only guard Write and Edit
68
- if (toolName !== 'Write' && toolName !== 'Edit') {
69
- process.exit(0);
70
- }
60
+ readStdinIfMain(module, (data) => {
61
+ const toolName = data.tool_name || '';
71
62
 
72
- const filePath = (data.tool_input && data.tool_input.file_path) || '';
73
- const cwd = data.cwd || process.cwd();
63
+ // Only guard Write and Edit
64
+ if (toolName !== 'Write' && toolName !== 'Edit') {
65
+ return;
66
+ }
74
67
 
75
- // Allowlisted paths always pass
76
- if (isAllowlisted(filePath)) {
77
- process.exit(0);
78
- }
68
+ const filePath = (data.tool_input && data.tool_input.file_path) || '';
69
+ const cwd = data.cwd || process.cwd();
79
70
 
80
- const branch = currentBranch(cwd);
71
+ // Allowlisted paths always pass
72
+ if (isAllowlisted(filePath)) {
73
+ return;
74
+ }
81
75
 
82
- // Only guard when on main/master
83
- if (branch !== 'main' && branch !== 'master') {
84
- process.exit(0);
85
- }
76
+ const branch = currentBranch(cwd);
86
77
 
87
- // Block only when a df/* worktree branch exists
88
- if (!dfWorktreeExists(cwd)) {
89
- process.exit(0);
90
- }
78
+ // Only guard when on main/master
79
+ if (branch !== 'main' && branch !== 'master') {
80
+ return;
81
+ }
91
82
 
92
- // All conditions met block the write
93
- console.error(
94
- `[df-worktree-guard] Blocked ${toolName} to "${filePath}" on main branch ` +
95
- `while df/* worktree exists. Make changes inside the worktree branch instead.`
96
- );
97
- process.exit(1);
98
- } catch (_e) {
99
- // Parse or unexpected error — fail open so we never break non-deepflow projects
100
- process.exit(0);
83
+ // Block only when a df/* worktree branch exists
84
+ if (!dfWorktreeExists(cwd)) {
85
+ return;
101
86
  }
87
+
88
+ // All conditions met — block the write
89
+ console.error(
90
+ `[df-worktree-guard] Blocked ${toolName} to "${filePath}" on main branch ` +
91
+ `while df/* worktree exists. Make changes inside the worktree branch instead.`
92
+ );
93
+ process.exit(1);
102
94
  });
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared stdin helper for deepflow hooks.
5
+ *
6
+ * Usage in a hook file:
7
+ * const { readStdinIfMain } = require('./lib/hook-stdin');
8
+ * readStdinIfMain(module, (data) => { ... });
9
+ *
10
+ * The helper checks if the CALLING hook is the main module (i.e. run directly,
11
+ * not required by a test). This prevents test files from hanging on stdin.
12
+ */
13
+
14
+ /**
15
+ * readStdinIfMain(callerModule, callback)
16
+ *
17
+ * @param {NodeModule} callerModule Pass `module` from the calling hook file.
18
+ * @param {function(Object): void} callback Called with the parsed JSON payload.
19
+ * If stdin is not valid JSON the process exits 0 without calling callback.
20
+ */
21
+ function readStdinIfMain(callerModule, callback) {
22
+ if (require.main !== callerModule) {
23
+ // Being required (e.g. by a test) — do not read stdin.
24
+ return;
25
+ }
26
+
27
+ let raw = '';
28
+ process.stdin.setEncoding('utf8');
29
+ process.stdin.on('data', chunk => { raw += chunk; });
30
+ process.stdin.on('end', () => {
31
+ let payload;
32
+ try {
33
+ payload = JSON.parse(raw);
34
+ } catch (_e) {
35
+ // Invalid JSON — exit 0 to avoid breaking Claude Code.
36
+ process.exit(0);
37
+ }
38
+ try {
39
+ callback(payload);
40
+ } catch (_e) {
41
+ // Never break Claude Code on hook errors.
42
+ }
43
+ process.exit(0);
44
+ });
45
+ }
46
+
47
+ module.exports = { readStdinIfMain };
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ const { test, describe, beforeEach, afterEach } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const { readStdinIfMain } = require('./hook-stdin');
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // 1. Guard: skips stdin when callerModule is NOT require.main
10
+ // ---------------------------------------------------------------------------
11
+
12
+ describe('readStdinIfMain', () => {
13
+ test('returns immediately when callerModule is not require.main', () => {
14
+ const fakeModule = { id: 'not-main' };
15
+ const callback = () => { throw new Error('should not be called'); };
16
+
17
+ // Should return undefined without hanging or calling callback
18
+ const result = readStdinIfMain(fakeModule, callback);
19
+ assert.equal(result, undefined);
20
+ });
21
+
22
+ test('does not set encoding on stdin when callerModule is not require.main', () => {
23
+ const fakeModule = { id: 'not-main' };
24
+ let encodingSet = false;
25
+ const originalSetEncoding = process.stdin.setEncoding;
26
+ process.stdin.setEncoding = () => { encodingSet = true; };
27
+
28
+ try {
29
+ readStdinIfMain(fakeModule, () => {});
30
+ assert.equal(encodingSet, false, 'should not touch stdin when not main');
31
+ } finally {
32
+ process.stdin.setEncoding = originalSetEncoding;
33
+ }
34
+ });
35
+
36
+ test('does not attach data listeners when callerModule is not require.main', () => {
37
+ const fakeModule = { id: 'not-main' };
38
+ const listenersBefore = process.stdin.listenerCount('data');
39
+
40
+ readStdinIfMain(fakeModule, () => {});
41
+
42
+ const listenersAfter = process.stdin.listenerCount('data');
43
+ assert.equal(listenersAfter, listenersBefore, 'should not add data listeners');
44
+ });
45
+
46
+ test('callback is never invoked when callerModule is not require.main', () => {
47
+ const fakeModule = { id: 'not-main' };
48
+ let callbackInvoked = false;
49
+
50
+ readStdinIfMain(fakeModule, () => { callbackInvoked = true; });
51
+ assert.equal(callbackInvoked, false);
52
+ });
53
+ });
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // 2. Module exports
57
+ // ---------------------------------------------------------------------------
58
+
59
+ describe('hook-stdin exports', () => {
60
+ test('exports readStdinIfMain as a function', () => {
61
+ assert.equal(typeof readStdinIfMain, 'function');
62
+ });
63
+
64
+ test('readStdinIfMain expects two parameters', () => {
65
+ assert.equal(readStdinIfMain.length, 2);
66
+ });
67
+
68
+ test('module exports only readStdinIfMain', () => {
69
+ const exports = require('./hook-stdin');
70
+ const keys = Object.keys(exports);
71
+ assert.deepEqual(keys, ['readStdinIfMain']);
72
+ });
73
+ });
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // 3. Integration via subprocess — tests the stdin-reading path
77
+ // Spawns hook-stdin-test-harness as a child process so require.main matches.
78
+ // ---------------------------------------------------------------------------
79
+
80
+ const { execFileSync, spawn } = require('node:child_process');
81
+ const path = require('node:path');
82
+ const fs = require('node:fs');
83
+
84
+ // We create a tiny harness script that uses readStdinIfMain with itself as main
85
+ const HARNESS_PATH = path.join(__dirname, '_test-harness-stdin.js');
86
+
87
+ describe('readStdinIfMain when callerModule IS require.main (subprocess)', () => {
88
+ beforeEach(() => {
89
+ // Write a small harness that exercises readStdinIfMain as main module
90
+ fs.writeFileSync(HARNESS_PATH, `
91
+ 'use strict';
92
+ const { readStdinIfMain } = require('./hook-stdin');
93
+ readStdinIfMain(module, (data) => {
94
+ // Write parsed payload to stdout so the test can verify
95
+ process.stdout.write(JSON.stringify(data));
96
+ });
97
+ `);
98
+ });
99
+
100
+ afterEach(() => {
101
+ try { fs.unlinkSync(HARNESS_PATH); } catch (_e) { /* ignore */ }
102
+ });
103
+
104
+ test('parses valid JSON from stdin and passes to callback', () => {
105
+ const input = JSON.stringify({ event: 'test', foo: 42 });
106
+ const result = execFileSync(process.execPath, [HARNESS_PATH], {
107
+ input,
108
+ encoding: 'utf8',
109
+ timeout: 5000,
110
+ });
111
+ const parsed = JSON.parse(result);
112
+ assert.deepEqual(parsed, { event: 'test', foo: 42 });
113
+ });
114
+
115
+ test('handles empty object from stdin', () => {
116
+ const input = JSON.stringify({});
117
+ const result = execFileSync(process.execPath, [HARNESS_PATH], {
118
+ input,
119
+ encoding: 'utf8',
120
+ timeout: 5000,
121
+ });
122
+ assert.deepEqual(JSON.parse(result), {});
123
+ });
124
+
125
+ test('handles array payload from stdin', () => {
126
+ const input = JSON.stringify([1, 2, 3]);
127
+ const result = execFileSync(process.execPath, [HARNESS_PATH], {
128
+ input,
129
+ encoding: 'utf8',
130
+ timeout: 5000,
131
+ });
132
+ assert.deepEqual(JSON.parse(result), [1, 2, 3]);
133
+ });
134
+
135
+ test('exits 0 on invalid JSON without calling callback', () => {
136
+ // Invalid JSON should cause process.exit(0) before callback
137
+ const result = execFileSync(process.execPath, [HARNESS_PATH], {
138
+ input: 'not valid json {{{',
139
+ encoding: 'utf8',
140
+ timeout: 5000,
141
+ });
142
+ // Callback writes to stdout; if not called, stdout should be empty
143
+ assert.equal(result, '');
144
+ });
145
+
146
+ test('exits 0 on empty stdin without calling callback', () => {
147
+ const result = execFileSync(process.execPath, [HARNESS_PATH], {
148
+ input: '',
149
+ encoding: 'utf8',
150
+ timeout: 5000,
151
+ });
152
+ assert.equal(result, '');
153
+ });
154
+
155
+ test('exits 0 even when callback throws', () => {
156
+ // Write a harness where the callback throws
157
+ const throwHarness = path.join(__dirname, '_test-harness-throw.js');
158
+ fs.writeFileSync(throwHarness, `
159
+ 'use strict';
160
+ const { readStdinIfMain } = require('./hook-stdin');
161
+ readStdinIfMain(module, (data) => {
162
+ throw new Error('callback error');
163
+ });
164
+ `);
165
+ try {
166
+ // Should not throw — the error is caught internally
167
+ execFileSync(process.execPath, [throwHarness], {
168
+ input: JSON.stringify({ ok: true }),
169
+ encoding: 'utf8',
170
+ timeout: 5000,
171
+ });
172
+ } finally {
173
+ try { fs.unlinkSync(throwHarness); } catch (_e) { /* ignore */ }
174
+ }
175
+ });
176
+
177
+ test('handles large JSON payload', () => {
178
+ const largeObj = { data: 'x'.repeat(10000), nested: { a: 1 } };
179
+ const input = JSON.stringify(largeObj);
180
+ const result = execFileSync(process.execPath, [HARNESS_PATH], {
181
+ input,
182
+ encoding: 'utf8',
183
+ timeout: 10000,
184
+ maxBuffer: 1024 * 1024,
185
+ });
186
+ const parsed = JSON.parse(result);
187
+ assert.equal(parsed.data.length, 10000);
188
+ assert.equal(parsed.nested.a, 1);
189
+ });
190
+
191
+ test('handles JSON with unicode characters', () => {
192
+ const input = JSON.stringify({ msg: '日本語テスト 🎉' });
193
+ const result = execFileSync(process.execPath, [HARNESS_PATH], {
194
+ input,
195
+ encoding: 'utf8',
196
+ timeout: 5000,
197
+ });
198
+ assert.deepEqual(JSON.parse(result), { msg: '日本語テスト 🎉' });
199
+ });
200
+ });
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-no-bare-stdin.js
4
+ *
5
+ * Guards against bare `process.stdin.on` calls being (re-)introduced in
6
+ * hooks/df-*.js files. Hooks that need stdin must use readStdinIfMain()
7
+ * from hooks/lib/hook-stdin.js instead.
8
+ *
9
+ * Excluded hooks (use alternative stdin strategies — documented below):
10
+ * df-check-update.js — spawns detached background process; never reads stdin
11
+ * df-dashboard-push.js — reads stdin synchronously via readFileSync('/dev/stdin')
12
+ * df-quota-logger.js — spawns detached background process; never reads stdin
13
+ * df-spec-lint.js — CLI tool (takes a filepath arg); never reads stdin
14
+ *
15
+ * Usage:
16
+ * node hooks/lib/lint-no-bare-stdin.js # exits 0 (clean) or 1 (violations)
17
+ *
18
+ * Integration:
19
+ * Add to your CI / pre-commit as:
20
+ * node hooks/lib/lint-no-bare-stdin.js
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ const HOOKS_DIR = path.resolve(__dirname, '..');
29
+ const PATTERN = /process\.stdin\.on\s*\(/;
30
+
31
+ function run() {
32
+ // Collect hooks/df-*.js (not inside hooks/lib/)
33
+ const entries = fs.readdirSync(HOOKS_DIR);
34
+ const hookFiles = entries
35
+ .filter((f) => f.startsWith('df-') && f.endsWith('.js') && !f.endsWith('.test.js'))
36
+ .map((f) => path.join(HOOKS_DIR, f));
37
+
38
+ const violations = [];
39
+
40
+ for (const filePath of hookFiles) {
41
+ const content = fs.readFileSync(filePath, 'utf8');
42
+ const lines = content.split('\n');
43
+ lines.forEach((line, idx) => {
44
+ if (PATTERN.test(line)) {
45
+ violations.push({
46
+ file: path.relative(path.resolve(__dirname, '..', '..'), filePath),
47
+ line: idx + 1,
48
+ text: line.trim(),
49
+ });
50
+ }
51
+ });
52
+ }
53
+
54
+ if (violations.length === 0) {
55
+ console.log('lint-no-bare-stdin: OK — no bare process.stdin.on calls found in hooks/df-*.js');
56
+ process.exit(0);
57
+ }
58
+
59
+ console.error('lint-no-bare-stdin: FAIL — bare process.stdin.on calls detected:');
60
+ for (const v of violations) {
61
+ console.error(` ${v.file}:${v.line} ${v.text}`);
62
+ }
63
+ console.error('');
64
+ console.error('Fix: use readStdinIfMain() from hooks/lib/hook-stdin.js instead of inline stdin listeners.');
65
+ process.exit(1);
66
+ }
67
+
68
+ run();
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const { test, describe, after } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { execFileSync } = require('node:child_process');
6
+ const path = require('node:path');
7
+ const fs = require('node:fs');
8
+
9
+ after(() => { process.stdin.destroy(); });
10
+
11
+ const LINT_SCRIPT = path.join(__dirname, 'lint-no-bare-stdin.js');
12
+ const HOOKS_DIR = path.resolve(__dirname, '..');
13
+
14
+ describe('lint-no-bare-stdin', () => {
15
+ test('is a valid Node.js script (can be parsed without syntax errors)', () => {
16
+ // --check parses without executing
17
+ execFileSync(process.execPath, ['--check', LINT_SCRIPT], {
18
+ encoding: 'utf8',
19
+ timeout: 5000,
20
+ });
21
+ assert.ok(true, 'script parsed without syntax errors');
22
+ });
23
+
24
+ test('exits 0 when run against current hooks directory (codebase is clean)', () => {
25
+ const result = execFileSync(process.execPath, [LINT_SCRIPT], {
26
+ encoding: 'utf8',
27
+ timeout: 5000,
28
+ });
29
+ assert.match(result, /OK/, 'should report OK when no violations found');
30
+ });
31
+
32
+ test('scans hooks/df-*.js files (not lib/ or test files)', () => {
33
+ // Verify the script's target pattern by checking that hook files exist
34
+ const entries = fs.readdirSync(HOOKS_DIR);
35
+ const hookFiles = entries.filter(
36
+ (f) => f.startsWith('df-') && f.endsWith('.js') && !f.endsWith('.test.js')
37
+ );
38
+ assert.ok(hookFiles.length > 0, 'should find at least one hooks/df-*.js file to lint');
39
+ });
40
+
41
+ test('detects bare process.stdin.on and exits 1', () => {
42
+ // Create a temporary hook file with a violation
43
+ const tempHook = path.join(HOOKS_DIR, 'df-_test-lint-violation.js');
44
+ fs.writeFileSync(tempHook, `'use strict';\nprocess.stdin.on('data', () => {});\n`);
45
+ try {
46
+ execFileSync(process.execPath, [LINT_SCRIPT], {
47
+ encoding: 'utf8',
48
+ timeout: 5000,
49
+ });
50
+ assert.fail('should have exited with code 1');
51
+ } catch (err) {
52
+ assert.equal(err.status, 1, 'should exit 1 when violations found');
53
+ assert.match(
54
+ err.stderr.toString(),
55
+ /FAIL/,
56
+ 'should report FAIL in stderr'
57
+ );
58
+ assert.match(
59
+ err.stderr.toString(),
60
+ /df-_test-lint-violation\.js/,
61
+ 'should name the violating file'
62
+ );
63
+ } finally {
64
+ try { fs.unlinkSync(tempHook); } catch (_e) { /* ignore */ }
65
+ }
66
+ });
67
+
68
+ test('ignores .test.js files when scanning', () => {
69
+ // Create a test file with a violation — should NOT be flagged
70
+ const tempTest = path.join(HOOKS_DIR, 'df-_test-lint-fake.test.js');
71
+ fs.writeFileSync(tempTest, `'use strict';\nprocess.stdin.on('data', () => {});\n`);
72
+ try {
73
+ const result = execFileSync(process.execPath, [LINT_SCRIPT], {
74
+ encoding: 'utf8',
75
+ timeout: 5000,
76
+ });
77
+ assert.match(result, /OK/, 'test files should be excluded from scanning');
78
+ } finally {
79
+ try { fs.unlinkSync(tempTest); } catch (_e) { /* ignore */ }
80
+ }
81
+ });
82
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepflow",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "description": "Doing reveals what thinking can't predict — spec-driven iterative development for Claude Code",
5
5
  "keywords": [
6
6
  "claude",