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.
- package/bin/install.js +21 -0
- package/bin/install.test.js +205 -0
- package/hooks/df-command-usage.js +17 -26
- package/hooks/df-dashboard-push.js +4 -4
- package/hooks/df-dashboard-push.test.js +256 -0
- package/hooks/df-execution-history.js +43 -53
- package/hooks/df-explore-protocol.js +30 -39
- package/hooks/df-invariant-check.js +88 -61
- package/hooks/df-invariant-check.test.js +315 -0
- package/hooks/df-quota-logger.js +11 -23
- package/hooks/df-quota-logger.test.js +324 -0
- package/hooks/df-snapshot-guard.js +32 -40
- package/hooks/df-statusline.js +3 -12
- package/hooks/df-stdin-migration.test.js +106 -0
- package/hooks/df-subagent-registry.js +42 -48
- package/hooks/df-tool-usage-spike.js +15 -26
- package/hooks/df-tool-usage.js +37 -47
- package/hooks/df-worktree-guard.js +28 -36
- package/hooks/lib/hook-stdin.js +47 -0
- package/hooks/lib/hook-stdin.test.js +200 -0
- package/hooks/lib/lint-no-bare-stdin.js +68 -0
- package/hooks/lib/lint-no-bare-stdin.test.js +82 -0
- package/package.json +1 -1
- package/src/commands/df/execute.md +25 -174
- package/src/eval/git-memory.js +8 -8
- package/src/eval/git-memory.test.js +128 -1
- package/src/eval/loop.js +3 -3
- package/src/eval/loop.test.js +158 -0
- package/templates/config-template.yaml +0 -6
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
});
|
package/hooks/df-tool-usage.js
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
88
|
+
const logDir = path.dirname(TOOL_USAGE_LOG);
|
|
89
|
+
if (!fs.existsSync(logDir)) {
|
|
90
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
102
91
|
}
|
|
103
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
73
|
-
|
|
63
|
+
// Only guard Write and Edit
|
|
64
|
+
if (toolName !== 'Write' && toolName !== 'Edit') {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
71
|
+
// Allowlisted paths always pass
|
|
72
|
+
if (isAllowlisted(filePath)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
if (branch !== 'main' && branch !== 'master') {
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
76
|
+
const branch = currentBranch(cwd);
|
|
86
77
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
// Only guard when on main/master
|
|
79
|
+
if (branch !== 'main' && branch !== 'master') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
});
|