@su-record/vibe 2.12.5 → 2.14.0
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/CLAUDE.md +25 -16
- package/README.en.md +16 -14
- package/README.md +13 -11
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +1 -0
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.d.ts +23 -0
- package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
- package/dist/cli/postinstall/fs-utils.js +71 -0
- package/dist/cli/postinstall/fs-utils.js.map +1 -1
- package/dist/cli/postinstall/fs-utils.test.js +69 -1
- package/dist/cli/postinstall/fs-utils.test.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +12 -2
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/setup/CodexHooks.test.js +27 -0
- package/dist/cli/setup/CodexHooks.test.js.map +1 -1
- package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
- package/dist/cli/setup/ProjectSetup.js +6 -5
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/infra/lib/DecisionTracer.d.ts +4 -0
- package/dist/infra/lib/DecisionTracer.d.ts.map +1 -1
- package/dist/infra/lib/DecisionTracer.js +4 -0
- package/dist/infra/lib/DecisionTracer.js.map +1 -1
- package/dist/infra/lib/LoopBreaker.d.ts +4 -0
- package/dist/infra/lib/LoopBreaker.d.ts.map +1 -1
- package/dist/infra/lib/LoopBreaker.js +4 -0
- package/dist/infra/lib/LoopBreaker.js.map +1 -1
- package/dist/infra/lib/ReviewRace.d.ts +4 -0
- package/dist/infra/lib/ReviewRace.d.ts.map +1 -1
- package/dist/infra/lib/ReviewRace.js +4 -0
- package/dist/infra/lib/ReviewRace.js.map +1 -1
- package/dist/infra/lib/SkillQualityGate.d.ts +4 -0
- package/dist/infra/lib/SkillQualityGate.d.ts.map +1 -1
- package/dist/infra/lib/SkillQualityGate.js +4 -0
- package/dist/infra/lib/SkillQualityGate.js.map +1 -1
- package/dist/infra/lib/UltraQA.d.ts +4 -0
- package/dist/infra/lib/UltraQA.d.ts.map +1 -1
- package/dist/infra/lib/UltraQA.js +4 -0
- package/dist/infra/lib/UltraQA.js.map +1 -1
- package/dist/infra/lib/VerificationLoop.d.ts +4 -0
- package/dist/infra/lib/VerificationLoop.d.ts.map +1 -1
- package/dist/infra/lib/VerificationLoop.js +4 -0
- package/dist/infra/lib/VerificationLoop.js.map +1 -1
- package/dist/infra/orchestrator/index.d.ts.map +1 -1
- package/dist/infra/orchestrator/index.js +1 -3
- package/dist/infra/orchestrator/index.js.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.d.ts.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.js +1 -4
- package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
- package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
- package/dist/tools/convention/validateCodeQuality.js +5 -4
- package/dist/tools/convention/validateCodeQuality.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/loop/index.d.ts +6 -0
- package/dist/tools/loop/index.d.ts.map +1 -0
- package/dist/tools/loop/index.js +5 -0
- package/dist/tools/loop/index.js.map +1 -0
- package/dist/tools/loop/validateLoopDefinition.d.ts +38 -0
- package/dist/tools/loop/validateLoopDefinition.d.ts.map +1 -0
- package/dist/tools/loop/validateLoopDefinition.js +224 -0
- package/dist/tools/loop/validateLoopDefinition.js.map +1 -0
- package/dist/tools/loop/validateLoopDefinition.test.d.ts +14 -0
- package/dist/tools/loop/validateLoopDefinition.test.d.ts.map +1 -0
- package/dist/tools/loop/validateLoopDefinition.test.js +229 -0
- package/dist/tools/loop/validateLoopDefinition.test.js.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.d.ts +2 -0
- package/dist/tools/spec/traceabilityMatrix.d.ts.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.js +50 -1
- package/dist/tools/spec/traceabilityMatrix.js.map +1 -1
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts +10 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.d.ts.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js +89 -0
- package/dist/tools/spec/traceabilityMatrix.path-resolution.test.js.map +1 -0
- package/dist/tools/spec/traceabilityMatrix.test.js +19 -0
- package/dist/tools/spec/traceabilityMatrix.test.js.map +1 -1
- package/hooks/hooks.json +1 -0
- package/hooks/scripts/__tests__/.vibe/command-log.txt +60 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-shm +0 -0
- package/hooks/scripts/__tests__/.vibe/memories/memories.db-wal +0 -0
- package/hooks/scripts/__tests__/auto-test-debounce.test.js +145 -0
- package/hooks/scripts/__tests__/code-check-detectors.test.js +155 -0
- package/hooks/scripts/__tests__/dispatcher-inprocess.test.js +99 -0
- package/hooks/scripts/__tests__/keyword-detector.test.js +26 -18
- package/hooks/scripts/__tests__/loop-ledger.test.js +321 -0
- package/hooks/scripts/__tests__/post-edit-dispatcher.test.js +139 -0
- package/hooks/scripts/__tests__/pre-tool-guard.test.js +115 -1
- package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +146 -0
- package/hooks/scripts/__tests__/run-ledger.test.js +330 -0
- package/hooks/scripts/__tests__/scope-from-spec.test.js +215 -0
- package/hooks/scripts/__tests__/sentinel-guard.test.js +79 -24
- package/hooks/scripts/__tests__/step-counter.test.js +95 -15
- package/hooks/scripts/__tests__/utils-npm-root.test.js +98 -0
- package/hooks/scripts/auto-commit.js +27 -1
- package/hooks/scripts/auto-format.js +85 -20
- package/hooks/scripts/auto-test.js +187 -37
- package/hooks/scripts/code-check.js +286 -90
- package/hooks/scripts/codex-hook-adapter.js +12 -1
- package/hooks/scripts/command-log.js +26 -16
- package/hooks/scripts/keyword-detector.js +22 -22
- package/hooks/scripts/lib/dispatcher.js +38 -0
- package/hooks/scripts/lib/hook-context.js +130 -0
- package/hooks/scripts/lib/loop-ledger.js +118 -0
- package/hooks/scripts/lib/pr-gate-runner.js +62 -0
- package/hooks/scripts/lib/run-ledger.js +169 -0
- package/hooks/scripts/lib/scope-from-spec.js +40 -7
- package/hooks/scripts/loop-ledger.js +56 -0
- package/hooks/scripts/post-edit-dispatcher.js +93 -20
- package/hooks/scripts/post-edit.js +40 -19
- package/hooks/scripts/pr-test-gate.js +8 -37
- package/hooks/scripts/pre-tool-dispatcher.js +18 -16
- package/hooks/scripts/pre-tool-guard.js +55 -52
- package/hooks/scripts/prompt-dispatcher.js +10 -0
- package/hooks/scripts/scope-guard.js +40 -39
- package/hooks/scripts/sentinel-guard.js +41 -41
- package/hooks/scripts/session-start.js +13 -1
- package/hooks/scripts/step-counter.js +100 -7
- package/hooks/scripts/stop-dispatcher.js +26 -0
- package/hooks/scripts/utils.js +63 -21
- package/hooks/scripts/verify-ledger.js +22 -0
- package/package.json +2 -2
- package/skills/spec/references/templates.md +11 -6
- package/skills/vibe/SKILL.md +40 -23
- package/skills/vibe.loop/SKILL.md +116 -0
- package/skills/vibe.run/SKILL.md +153 -1686
- package/skills/vibe.run/references/brand-assets.md +59 -0
- package/skills/vibe.run/references/parallel-agents.md +326 -0
- package/skills/vibe.run/references/race-review.md +272 -0
- package/skills/vibe.run/references/ralph-loop.md +173 -0
- package/skills/vibe.run/references/ultrawork-mode.md +151 -0
- package/skills/vibe.trace/SKILL.md +25 -38
- package/skills/vibe.verify/SKILL.md +15 -0
- package/vibe/rules/loop-contract.md +54 -0
- package/vibe/templates/loop-template.md +69 -0
- package/hooks/scripts/figma-guard.js +0 -219
|
@@ -24,7 +24,6 @@ function runGuard(args = []) {
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Run sentinel-guard.js with stdin JSON payload.
|
|
27
|
-
* 스크립트가 fs.readSync(0, ...)로 stdin을 읽으므로 execFileSync input 옵션이 동작.
|
|
28
27
|
*/
|
|
29
28
|
function runGuardWithStdin(payload) {
|
|
30
29
|
const json = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
@@ -41,24 +40,25 @@ function runGuardWithStdin(payload) {
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
// ══════════════════════════════════════════════════
|
|
44
|
-
// Sentinel path protection
|
|
43
|
+
// Sentinel path protection — evolution machinery
|
|
45
44
|
// ══════════════════════════════════════════════════
|
|
46
45
|
describe('sentinel-guard', () => {
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
// ─── 실제 보호 경로: src/infra/lib/evolution/ ───
|
|
47
|
+
describe('Write/Edit to evolution sentinel path via argv', () => {
|
|
48
|
+
it('should block Write to src/infra/lib/evolution/', () => {
|
|
49
49
|
const result = runGuard([
|
|
50
50
|
'Write',
|
|
51
|
-
JSON.stringify({ file_path: 'src/infra/lib/
|
|
51
|
+
JSON.stringify({ file_path: 'src/infra/lib/evolution/EvolutionOrchestrator.ts' }),
|
|
52
52
|
]);
|
|
53
53
|
expect(result.exitCode).toBe(2);
|
|
54
54
|
expect(result.stdout).toContain('block');
|
|
55
55
|
expect(result.stdout).toContain('Sentinel files are protected');
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it('should block Edit to sentinel path', () => {
|
|
58
|
+
it('should block Edit to evolution sentinel path', () => {
|
|
59
59
|
const result = runGuard([
|
|
60
60
|
'Edit',
|
|
61
|
-
JSON.stringify({ file_path: 'src/infra/lib/
|
|
61
|
+
JSON.stringify({ file_path: 'src/infra/lib/evolution/GuardAnalyzer.ts' }),
|
|
62
62
|
]);
|
|
63
63
|
expect(result.exitCode).toBe(2);
|
|
64
64
|
expect(result.stdout).toContain('block');
|
|
@@ -67,7 +67,7 @@ describe('sentinel-guard', () => {
|
|
|
67
67
|
it('should block Write with backslash path separators', () => {
|
|
68
68
|
const result = runGuard([
|
|
69
69
|
'Write',
|
|
70
|
-
JSON.stringify({ file_path: 'src\\infra\\lib\\
|
|
70
|
+
JSON.stringify({ file_path: 'src\\infra\\lib\\evolution\\file.ts' }),
|
|
71
71
|
]);
|
|
72
72
|
expect(result.exitCode).toBe(2);
|
|
73
73
|
expect(result.stdout).toContain('Sentinel files are protected');
|
|
@@ -76,32 +76,62 @@ describe('sentinel-guard', () => {
|
|
|
76
76
|
it('should block Write with ./ prefix', () => {
|
|
77
77
|
const result = runGuard([
|
|
78
78
|
'Write',
|
|
79
|
-
JSON.stringify({ file_path: './src/infra/lib/
|
|
79
|
+
JSON.stringify({ file_path: './src/infra/lib/evolution/index.ts' }),
|
|
80
80
|
]);
|
|
81
81
|
expect(result.exitCode).toBe(2);
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
// ─── 실제 보호 경로: hooks/scripts/lib/ ───
|
|
86
|
+
describe('Write/Edit to hooks/scripts/lib/ sentinel path', () => {
|
|
87
|
+
it('should block Write to hooks/scripts/lib/', () => {
|
|
88
|
+
const result = runGuard([
|
|
89
|
+
'Write',
|
|
90
|
+
JSON.stringify({ file_path: 'hooks/scripts/lib/dispatcher.js' }),
|
|
91
|
+
]);
|
|
92
|
+
expect(result.exitCode).toBe(2);
|
|
93
|
+
expect(result.stdout).toContain('Sentinel files are protected');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should block Edit to hook-context.js', () => {
|
|
97
|
+
const result = runGuard([
|
|
98
|
+
'Edit',
|
|
99
|
+
JSON.stringify({ file_path: 'hooks/scripts/lib/hook-context.js' }),
|
|
100
|
+
]);
|
|
101
|
+
expect(result.exitCode).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ─── stdin 경로 ───
|
|
85
106
|
describe('Write/Edit to sentinel paths via stdin', () => {
|
|
86
|
-
it('should block Edit via stdin payload', () => {
|
|
107
|
+
it('should block Edit evolution path via stdin payload', () => {
|
|
87
108
|
const result = runGuardWithStdin({
|
|
88
109
|
tool_name: 'Edit',
|
|
89
|
-
tool_input: { file_path: './src/infra/lib/
|
|
110
|
+
tool_input: { file_path: './src/infra/lib/evolution/InsightStore.ts' },
|
|
90
111
|
});
|
|
91
112
|
expect(result.exitCode).toBe(2);
|
|
92
113
|
expect(result.stdout).toContain('block');
|
|
93
114
|
});
|
|
94
115
|
|
|
95
|
-
it('should block Write via stdin payload', () => {
|
|
116
|
+
it('should block Write evolution path via stdin payload', () => {
|
|
96
117
|
const result = runGuardWithStdin({
|
|
97
118
|
tool_name: 'Write',
|
|
98
|
-
tool_input: { file_path: 'src/infra/lib/
|
|
119
|
+
tool_input: { file_path: 'src/infra/lib/evolution/CircuitBreaker.ts' },
|
|
99
120
|
});
|
|
100
121
|
expect(result.exitCode).toBe(2);
|
|
101
122
|
expect(result.stdout).toContain('Sentinel files are protected');
|
|
102
123
|
});
|
|
124
|
+
|
|
125
|
+
it('should block Write hooks/scripts/lib/ path via stdin', () => {
|
|
126
|
+
const result = runGuardWithStdin({
|
|
127
|
+
tool_name: 'Write',
|
|
128
|
+
tool_input: { file_path: 'hooks/scripts/lib/run-ledger.js' },
|
|
129
|
+
});
|
|
130
|
+
expect(result.exitCode).toBe(2);
|
|
131
|
+
});
|
|
103
132
|
});
|
|
104
133
|
|
|
134
|
+
// ─── 허용 경로 ───
|
|
105
135
|
describe('allowed operations', () => {
|
|
106
136
|
it('should allow Write to non-sentinel paths', () => {
|
|
107
137
|
const result = runGuard([
|
|
@@ -114,7 +144,7 @@ describe('sentinel-guard', () => {
|
|
|
114
144
|
it('should allow Read to sentinel paths (read is not blocked)', () => {
|
|
115
145
|
const result = runGuard([
|
|
116
146
|
'Read',
|
|
117
|
-
JSON.stringify({ file_path: 'src/infra/lib/
|
|
147
|
+
JSON.stringify({ file_path: 'src/infra/lib/evolution/GuardAnalyzer.ts' }),
|
|
118
148
|
]);
|
|
119
149
|
expect(result.exitCode).toBe(0);
|
|
120
150
|
});
|
|
@@ -126,23 +156,40 @@ describe('sentinel-guard', () => {
|
|
|
126
156
|
]);
|
|
127
157
|
expect(result.exitCode).toBe(0);
|
|
128
158
|
});
|
|
159
|
+
|
|
160
|
+
it('should allow Write to hooks/scripts/ top level (not lib/ subdir)', () => {
|
|
161
|
+
const result = runGuard([
|
|
162
|
+
'Write',
|
|
163
|
+
JSON.stringify({ file_path: 'hooks/scripts/step-counter.js' }),
|
|
164
|
+
]);
|
|
165
|
+
expect(result.exitCode).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should allow Write to src/infra/lib/ parent (not evolution subdir)', () => {
|
|
169
|
+
const result = runGuard([
|
|
170
|
+
'Write',
|
|
171
|
+
JSON.stringify({ file_path: 'src/infra/lib/constants.ts' }),
|
|
172
|
+
]);
|
|
173
|
+
expect(result.exitCode).toBe(0);
|
|
174
|
+
});
|
|
129
175
|
});
|
|
130
176
|
|
|
177
|
+
// ─── 위험한 bash + sentinel 경로 ───
|
|
131
178
|
describe('dangerous bash commands targeting sentinel paths', () => {
|
|
132
|
-
it('should block rm -rf targeting
|
|
179
|
+
it('should block rm -rf targeting evolution path', () => {
|
|
133
180
|
const result = runGuard([
|
|
134
181
|
'Bash',
|
|
135
|
-
JSON.stringify({ command: 'rm -rf src/infra/lib/
|
|
182
|
+
JSON.stringify({ command: 'rm -rf src/infra/lib/evolution/' }),
|
|
136
183
|
]);
|
|
137
184
|
expect(result.exitCode).toBe(2);
|
|
138
185
|
expect(result.stdout).toContain('block');
|
|
139
186
|
expect(result.stdout).toContain('Dangerous command targeting sentinel path');
|
|
140
187
|
});
|
|
141
188
|
|
|
142
|
-
it('should block
|
|
189
|
+
it('should block rm -rf targeting hooks/scripts/lib/', () => {
|
|
143
190
|
const result = runGuard([
|
|
144
191
|
'Bash',
|
|
145
|
-
JSON.stringify({ command: '
|
|
192
|
+
JSON.stringify({ command: 'rm -rf hooks/scripts/lib/' }),
|
|
146
193
|
]);
|
|
147
194
|
expect(result.exitCode).toBe(2);
|
|
148
195
|
});
|
|
@@ -164,33 +211,41 @@ describe('sentinel-guard', () => {
|
|
|
164
211
|
});
|
|
165
212
|
});
|
|
166
213
|
|
|
214
|
+
// ─── Bash 명령어 문자열이 sentinel 경로로 시작하는 경우 ───
|
|
167
215
|
describe('Bash command containing sentinel path in command string', () => {
|
|
168
|
-
it('should block when command string itself starts with sentinel path', () => {
|
|
216
|
+
it('should block when command string itself starts with evolution sentinel path', () => {
|
|
169
217
|
const result = runGuard([
|
|
170
218
|
'Bash',
|
|
171
|
-
JSON.stringify({ command: 'src/infra/lib/
|
|
219
|
+
JSON.stringify({ command: 'src/infra/lib/evolution/run.sh' }),
|
|
172
220
|
]);
|
|
173
221
|
expect(result.exitCode).toBe(2);
|
|
174
222
|
expect(result.stdout).toContain('Sentinel files are protected');
|
|
175
223
|
});
|
|
176
224
|
|
|
225
|
+
it('should block when command string starts with hooks/scripts/lib/', () => {
|
|
226
|
+
const result = runGuard([
|
|
227
|
+
'Bash',
|
|
228
|
+
JSON.stringify({ command: 'hooks/scripts/lib/dispatcher.js' }),
|
|
229
|
+
]);
|
|
230
|
+
expect(result.exitCode).toBe(2);
|
|
231
|
+
});
|
|
232
|
+
|
|
177
233
|
it('should not block non-dangerous commands referencing sentinel path mid-string', () => {
|
|
178
|
-
// isSentinelPath only checks startsWith, and the DANGEROUS_BASH_RE +
|
|
179
|
-
// includes check requires both a dangerous command and sentinel path
|
|
180
234
|
const result = runGuard([
|
|
181
235
|
'Bash',
|
|
182
|
-
JSON.stringify({ command: 'cat src/infra/lib/
|
|
236
|
+
JSON.stringify({ command: 'cat src/infra/lib/evolution/GuardAnalyzer.ts | wc -l' }),
|
|
183
237
|
]);
|
|
184
238
|
// 'cat' is not a dangerous command, command does not start with sentinel path
|
|
185
239
|
expect(result.exitCode).toBe(0);
|
|
186
240
|
});
|
|
187
241
|
});
|
|
188
242
|
|
|
243
|
+
// ─── stdin vs argv 우선순위 ───
|
|
189
244
|
describe('stdin vs argv priority', () => {
|
|
190
245
|
it('should prefer stdin payload over argv', () => {
|
|
191
246
|
const payload = JSON.stringify({
|
|
192
247
|
tool_name: 'Write',
|
|
193
|
-
tool_input: { file_path: 'src/infra/lib/
|
|
248
|
+
tool_input: { file_path: 'src/infra/lib/evolution/x.ts' },
|
|
194
249
|
});
|
|
195
250
|
try {
|
|
196
251
|
execFileSync('node', [SCRIPT, 'Read', '{}'], {
|
|
@@ -75,27 +75,31 @@ describe('step-counter PostToolUse hook', () => {
|
|
|
75
75
|
expect(data.startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
it('연속 호출 시 steps 누적', () => {
|
|
79
|
-
|
|
78
|
+
it('연속 호출 시 steps 누적 (10이벤트 재작성 주기 고려)', () => {
|
|
79
|
+
// 10이벤트마다 재작성 — 10회 호출 후에는 steps=10 이 보장됨
|
|
80
|
+
for (let i = 0; i < 10; i++) {
|
|
80
81
|
runCounter({
|
|
81
82
|
payload: { tool_name: 'Bash', tool_input: { command: 'echo hi' }, tool_response: {} },
|
|
82
83
|
projectDir,
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
86
|
const data = readJson(runJson);
|
|
86
|
-
expect(data.steps).toBe(
|
|
87
|
+
expect(data.steps).toBe(10);
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
it('손상된 JSON 이 있어도 새로 시작', () => {
|
|
90
|
+
it('손상된 JSON 이 있어도 새로 시작 (10회 후 재작성으로 확인)', () => {
|
|
90
91
|
fs.mkdirSync(path.join(projectDir, '.vibe', 'metrics'), { recursive: true });
|
|
91
92
|
fs.writeFileSync(runJson, '{ broken json');
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// 10회 호출해야 재작성 주기 도달
|
|
94
|
+
for (let i = 0; i < 10; i++) {
|
|
95
|
+
runCounter({
|
|
96
|
+
payload: { tool_name: 'Bash', tool_input: {}, tool_response: {} },
|
|
97
|
+
projectDir,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
97
100
|
const data = readJson(runJson);
|
|
98
|
-
expect(data.steps).toBe(
|
|
101
|
+
expect(data.steps).toBe(10);
|
|
102
|
+
expect(data.startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
99
103
|
});
|
|
100
104
|
});
|
|
101
105
|
|
|
@@ -149,7 +153,8 @@ describe('step-counter PostToolUse hook', () => {
|
|
|
149
153
|
expect(readJson(runJson).steps).toBe(1);
|
|
150
154
|
});
|
|
151
155
|
|
|
152
|
-
it('연속 호출 시 jsonl 누적', () => {
|
|
156
|
+
it('연속 호출 시 jsonl 누적 (Read는 조기 종료 — 기록 안 됨)', () => {
|
|
157
|
+
// Read 는 READ_ONLY_TOOLS 조기 종료 → jsonl 미기록
|
|
153
158
|
runCounter({
|
|
154
159
|
payload: { tool_name: 'Read', tool_input: { file_path: 'a.ts' }, tool_response: {} },
|
|
155
160
|
projectDir,
|
|
@@ -159,8 +164,8 @@ describe('step-counter PostToolUse hook', () => {
|
|
|
159
164
|
projectDir,
|
|
160
165
|
});
|
|
161
166
|
const lines = readJsonl(runJsonl);
|
|
162
|
-
expect(lines).toHaveLength(
|
|
163
|
-
expect(lines.
|
|
167
|
+
expect(lines).toHaveLength(1);
|
|
168
|
+
expect(lines[0].tool).toBe('Edit');
|
|
164
169
|
});
|
|
165
170
|
});
|
|
166
171
|
|
|
@@ -322,10 +327,10 @@ describe('step-counter PostToolUse hook', () => {
|
|
|
322
327
|
// 같은 카테고리 실패 2회
|
|
323
328
|
failBash('a', err, projectDir);
|
|
324
329
|
failBash('b', err, projectDir);
|
|
325
|
-
// 성공 툴콜로 윈도우 채움 (10줄 이상)
|
|
330
|
+
// 성공 툴콜로 윈도우 채움 (10줄 이상) — Bash 를 사용 (Read 는 조기 종료로 jsonl 미기록)
|
|
326
331
|
for (let i = 0; i < 10; i++) {
|
|
327
332
|
runCounter({
|
|
328
|
-
payload: { tool_name: '
|
|
333
|
+
payload: { tool_name: 'Bash', tool_input: { command: `echo ${i}` }, tool_response: {} },
|
|
329
334
|
projectDir,
|
|
330
335
|
});
|
|
331
336
|
}
|
|
@@ -355,4 +360,79 @@ describe('step-counter PostToolUse hook', () => {
|
|
|
355
360
|
expect(r.status).toBe(0);
|
|
356
361
|
});
|
|
357
362
|
});
|
|
363
|
+
|
|
364
|
+
// ───────── 읽기 전용 도구 조기 종료 (defense-in-depth) ─────────
|
|
365
|
+
describe('읽기 전용 도구 조기 종료', () => {
|
|
366
|
+
const READ_ONLY = ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'TodoWrite', 'ToolSearch', 'ListMcpResourcesTool'];
|
|
367
|
+
|
|
368
|
+
it.each(READ_ONLY)('%s 호출 시 파일 미생성 + exit 0', (toolName) => {
|
|
369
|
+
const r = runCounter({
|
|
370
|
+
payload: { tool_name: toolName, tool_input: { file_path: 'src/foo.ts' }, tool_response: {} },
|
|
371
|
+
projectDir,
|
|
372
|
+
});
|
|
373
|
+
expect(r.status).toBe(0);
|
|
374
|
+
// jsonl에 기록하지 않아야 함
|
|
375
|
+
const jsonlPath = path.join(projectDir, '.vibe', 'metrics', 'current-run.jsonl');
|
|
376
|
+
expect(fs.existsSync(jsonlPath)).toBe(false);
|
|
377
|
+
// current-run.json 도 생성하지 않아야 함
|
|
378
|
+
const jsonPath = path.join(projectDir, '.vibe', 'metrics', 'current-run.json');
|
|
379
|
+
expect(fs.existsSync(jsonPath)).toBe(false);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ───────── 쓰기 스로틀 ─────────
|
|
384
|
+
describe('쓰기 스로틀: jsonl 항상 append, json 조건부 재작성', () => {
|
|
385
|
+
it('첫 번째 이벤트는 current-run.json 생성 (파일 없음 → 스로틀 없음)', () => {
|
|
386
|
+
runCounter({
|
|
387
|
+
payload: { tool_name: 'Bash', tool_input: { command: 'x' }, tool_response: {} },
|
|
388
|
+
projectDir,
|
|
389
|
+
});
|
|
390
|
+
expect(fs.existsSync(runJson)).toBe(true);
|
|
391
|
+
expect(readJson(runJson).steps).toBe(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('10이벤트마다 강제 재작성: 10회 후 steps=jsonl라인수=10', () => {
|
|
395
|
+
for (let i = 0; i < 10; i++) {
|
|
396
|
+
runCounter({
|
|
397
|
+
payload: { tool_name: 'Edit', tool_input: { file_path: 'a.ts' }, tool_response: {} },
|
|
398
|
+
projectDir,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
// 10번째 이벤트에서 강제 재작성 (10%10===0) — steps = jsonl 라인 수 = 10
|
|
402
|
+
const data = readJson(runJson);
|
|
403
|
+
expect(data.steps).toBe(10);
|
|
404
|
+
// jsonl 도 10줄
|
|
405
|
+
const lines = readJsonl(runJsonl);
|
|
406
|
+
expect(lines).toHaveLength(10);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('jsonl 은 스로틀 무관 항상 즉시 append', () => {
|
|
410
|
+
for (let i = 0; i < 5; i++) {
|
|
411
|
+
runCounter({
|
|
412
|
+
payload: { tool_name: 'Bash', tool_input: { command: `cmd-${i}` }, tool_response: {} },
|
|
413
|
+
projectDir,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const lines = readJsonl(runJsonl);
|
|
417
|
+
expect(lines).toHaveLength(5);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('3-fail 감지는 jsonl 기반 — json 스로틀 후에도 동작', () => {
|
|
421
|
+
const err = "TypeError: Cannot read properties of undefined (reading 'x')";
|
|
422
|
+
// 3회 실패 (jsonl에 즉시 기록되므로 감지 가능)
|
|
423
|
+
for (let i = 0; i < 3; i++) {
|
|
424
|
+
runCounter({
|
|
425
|
+
payload: {
|
|
426
|
+
tool_name: 'Bash',
|
|
427
|
+
tool_input: { command: `run-${i}` },
|
|
428
|
+
tool_response: { is_error: true, error: err },
|
|
429
|
+
},
|
|
430
|
+
projectDir,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const apDir = path.join(projectDir, '.vibe', 'anti-patterns');
|
|
434
|
+
const files = fs.existsSync(apDir) ? fs.readdirSync(apDir).filter(f => f.endsWith('.md')) : [];
|
|
435
|
+
expect(files).toHaveLength(1);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
358
438
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils.js — getGlobalNpmPath() 파일 캐시 테스트
|
|
3
|
+
*
|
|
4
|
+
* 검증 범위:
|
|
5
|
+
* - L2 파일 캐시 히트 (TTL 내)
|
|
6
|
+
* - L2 파일 캐시 만료 (TTL 초과)
|
|
7
|
+
* - 캐시 파일 손상 시 fail-open (execSync 재실행)
|
|
8
|
+
* - 캐시 파일 없음 시 execSync 실행 후 파일 저장
|
|
9
|
+
*
|
|
10
|
+
* 격리 전략: 각 테스트는 별도 임시 디렉토리를 캐시 경로로 사용한다.
|
|
11
|
+
* NPM_ROOT_CACHE_FILE 환경 변수를 통해 경로를 주입한다.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { spawnSync } from 'child_process';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import os from 'os';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const UTILS_PATH = path.resolve(__dirname, '..', 'utils.js');
|
|
22
|
+
|
|
23
|
+
function makeTempCacheFile() {
|
|
24
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-npm-root-test-'));
|
|
25
|
+
return path.join(dir, 'npm-root.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* utils.js 의 getGlobalNpmPath() 를 별도 프로세스에서 실행.
|
|
30
|
+
* VIBE_NPM_ROOT_CACHE_FILE 환경 변수로 캐시 파일 경로를 주입한다.
|
|
31
|
+
*/
|
|
32
|
+
function runGetNpmRoot(cacheFilePath) {
|
|
33
|
+
return spawnSync('node', ['--input-type=module', '--eval',
|
|
34
|
+
`import { getGlobalNpmPath } from '${UTILS_PATH}';
|
|
35
|
+
process.stdout.write(getGlobalNpmPath() || '');`
|
|
36
|
+
], {
|
|
37
|
+
encoding: 'utf-8',
|
|
38
|
+
timeout: 10000,
|
|
39
|
+
env: {
|
|
40
|
+
...process.env,
|
|
41
|
+
VIBE_NPM_ROOT_CACHE_FILE: cacheFilePath,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('utils.js — getGlobalNpmPath() npm-root 파일 캐시', () => {
|
|
47
|
+
it('캐시 파일 없으면 execSync 실행 후 캐시 파일 생성', () => {
|
|
48
|
+
const cacheFile = makeTempCacheFile();
|
|
49
|
+
// 캐시 파일이 없는 상태에서 시작
|
|
50
|
+
expect(fs.existsSync(cacheFile)).toBe(false);
|
|
51
|
+
|
|
52
|
+
const result = runGetNpmRoot(cacheFile);
|
|
53
|
+
expect(result.status).toBe(0);
|
|
54
|
+
expect(result.stdout.trim()).toBeTruthy();
|
|
55
|
+
|
|
56
|
+
// 캐시 파일이 생성되어야 함
|
|
57
|
+
expect(fs.existsSync(cacheFile)).toBe(true);
|
|
58
|
+
const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
|
|
59
|
+
expect(cached.npmRoot).toBe(result.stdout.trim());
|
|
60
|
+
expect(typeof cached.savedAt).toBe('number');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('유효한 캐시 파일이 있으면 execSync 없이 캐시값 반환', () => {
|
|
64
|
+
const cacheFile = makeTempCacheFile();
|
|
65
|
+
const fakeRoot = '/fake/npm/root/for/test';
|
|
66
|
+
// 유효한 캐시 미리 작성
|
|
67
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ npmRoot: fakeRoot, savedAt: Date.now() }), { mode: 0o600 });
|
|
68
|
+
|
|
69
|
+
const result = runGetNpmRoot(cacheFile);
|
|
70
|
+
expect(result.status).toBe(0);
|
|
71
|
+
expect(result.stdout.trim()).toBe(fakeRoot);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('TTL 초과 캐시는 무효화 — execSync 재실행', () => {
|
|
75
|
+
const cacheFile = makeTempCacheFile();
|
|
76
|
+
const staleRoot = '/stale/path/should/not/be/used';
|
|
77
|
+
const expiredAt = Date.now() - (25 * 60 * 60 * 1000); // 25시간 전
|
|
78
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ npmRoot: staleRoot, savedAt: expiredAt }), { mode: 0o600 });
|
|
79
|
+
|
|
80
|
+
const result = runGetNpmRoot(cacheFile);
|
|
81
|
+
expect(result.status).toBe(0);
|
|
82
|
+
// stale 값이 아닌 실제 npm root 가 반환되어야 함
|
|
83
|
+
expect(result.stdout.trim()).not.toBe(staleRoot);
|
|
84
|
+
expect(result.stdout.trim()).toBeTruthy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('손상된 캐시 파일 — fail-open (execSync 실행)', () => {
|
|
88
|
+
const cacheFile = makeTempCacheFile();
|
|
89
|
+
fs.writeFileSync(cacheFile, '{ broken json :::');
|
|
90
|
+
|
|
91
|
+
const result = runGetNpmRoot(cacheFile);
|
|
92
|
+
expect(result.status).toBe(0);
|
|
93
|
+
expect(result.stdout.trim()).toBeTruthy(); // 실제 경로 반환
|
|
94
|
+
// 손상된 파일 때문에 프로세스가 crash 나지 않아야 함
|
|
95
|
+
const stderr = result.stderr || '';
|
|
96
|
+
expect(stderr).not.toMatch(/^Error:/m);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -13,12 +13,38 @@
|
|
|
13
13
|
* 으로 롤백 가능. 최근 5개만 유지.
|
|
14
14
|
*/
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
|
-
import { PROJECT_DIR, readProjectConfig } from './utils.js';
|
|
16
|
+
import { PROJECT_DIR, readProjectConfig, logHookDecision } from './utils.js';
|
|
17
|
+
import { readLedger } from './lib/run-ledger.js';
|
|
17
18
|
|
|
18
19
|
// Opt-in 가드 — 명시적으로 켜지 않았으면 아무것도 하지 않는다.
|
|
19
20
|
const __autoCommitCfg = readProjectConfig();
|
|
20
21
|
if (__autoCommitCfg?.hooks?.['auto-commit']?.enabled !== true) process.exit(0);
|
|
21
22
|
|
|
23
|
+
// verify 게이트 — vibe.run 세션이 시작됐으면 verifyPassed가 true이고
|
|
24
|
+
// verifyAt > runStarted 인 경우에만 커밋을 허용한다.
|
|
25
|
+
const __ledger = readLedger(PROJECT_DIR);
|
|
26
|
+
if (__ledger && __ledger.runStarted) {
|
|
27
|
+
const verifyOk = __ledger.verifyPassed === true
|
|
28
|
+
&& __ledger.verifyAt
|
|
29
|
+
&& __ledger.verifyAt > __ledger.runStarted;
|
|
30
|
+
if (!verifyOk) {
|
|
31
|
+
const reason = !__ledger.verifyPassed
|
|
32
|
+
? 'vibe.verify not passed — run /vibe.verify before committing'
|
|
33
|
+
: 'verifyAt is not after runStarted — re-run /vibe.verify';
|
|
34
|
+
logHookDecision('auto-commit', 'git-commit', 'block', reason);
|
|
35
|
+
process.stderr.write(`[auto-commit] SKIP: ${reason}\n`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// verifyRequired 게이트 — PostToolUse에서 P1 이슈가 발견되어 verify가 요구됨.
|
|
41
|
+
if (__ledger && __ledger.verifyRequired === true) {
|
|
42
|
+
const reason = `P1 issue requires verification: ${__ledger.verifyRequiredReason || 'see code-check findings'}`;
|
|
43
|
+
logHookDecision('auto-commit', 'git-commit', 'block', reason);
|
|
44
|
+
process.stderr.write(`[auto-commit] SKIP: ${reason}\n`);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
const PROTECTED_BRANCHES = ['main', 'master', 'develop', 'production'];
|
|
23
49
|
const MAX_FILES_IN_MSG = 5;
|
|
24
50
|
const MAX_CHECKPOINTS = 5;
|
|
@@ -4,19 +4,33 @@
|
|
|
4
4
|
* 프로젝트에 설치된 포매터를 감지하고 수정된 파일에 자동 실행.
|
|
5
5
|
* Prettier(JS/TS), Black(Python), gofmt(Go) 지원.
|
|
6
6
|
* 200ms 이내 완료 목표 — 단일 파일만 처리.
|
|
7
|
+
*
|
|
8
|
+
* 변경 감지: mtime 비교로 prettier가 실제 파일을 수정했는지 판단.
|
|
9
|
+
* 수정된 경우 finding을 반환 — 디스패처가 additionalContext에 포함시킨다.
|
|
7
10
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
11
|
+
import { execFile } from 'child_process';
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
import { existsSync, statSync } from 'fs';
|
|
10
14
|
import path from 'path';
|
|
11
15
|
import { PROJECT_DIR } from './utils.js';
|
|
16
|
+
import { buildCliCtx, isDirectRun } from './lib/hook-context.js';
|
|
17
|
+
|
|
18
|
+
// WHY async execFile (not execSync): in-process 디스패처에서 다른 step과
|
|
19
|
+
// Promise.all로 병렬 실행되므로, 동기 실행은 이벤트 루프를 막아 체인을 직렬화시킨다.
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
12
21
|
|
|
13
22
|
const CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|css|scss|json|md|html|vue|svelte)$/;
|
|
14
23
|
const PYTHON_EXT_RE = /\.py$/;
|
|
15
24
|
const GO_EXT_RE = /\.go$/;
|
|
25
|
+
const FORMAT_TIMEOUT_MS = 5000;
|
|
16
26
|
|
|
17
|
-
function getFilePath() {
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
function getFilePath(ctx) {
|
|
28
|
+
try {
|
|
29
|
+
const input = JSON.parse(ctx.toolInput || '{}');
|
|
30
|
+
return input.file_path || input.path || '';
|
|
31
|
+
} catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
// PATH 직접 스캔 — `which` execSync는 매 파일 저장마다 자식 프로세스를 동기
|
|
@@ -37,33 +51,84 @@ function hasPrettier() {
|
|
|
37
51
|
return existsSync(path.join(PROJECT_DIR, 'node_modules', '.bin', 'prettier'));
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
/**
|
|
55
|
+
* mtimeMs 읽기 — stat 실패 시 0 반환 (fail-open).
|
|
56
|
+
* @param {string} resolvedPath
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
function getMtime(resolvedPath) {
|
|
60
|
+
try {
|
|
61
|
+
return statSync(resolvedPath).mtimeMs;
|
|
62
|
+
} catch {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 파일 포맷 실행. 실제 변경이 발생했으면 finding 문자열을 반환.
|
|
69
|
+
* @param {string} filePath
|
|
70
|
+
* @returns {Promise<string|null>} finding or null
|
|
71
|
+
*/
|
|
72
|
+
async function formatFile(filePath) {
|
|
41
73
|
const resolved = path.resolve(filePath);
|
|
42
|
-
if (!existsSync(resolved)) return;
|
|
74
|
+
if (!existsSync(resolved)) return null;
|
|
43
75
|
|
|
44
76
|
try {
|
|
45
77
|
if (CODE_EXT_RE.test(filePath) && hasPrettier()) {
|
|
46
|
-
|
|
78
|
+
const mtimeBefore = getMtime(resolved);
|
|
79
|
+
await execFileAsync('npx', ['prettier', '--write', resolved], {
|
|
47
80
|
cwd: PROJECT_DIR,
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
timeout: FORMAT_TIMEOUT_MS,
|
|
82
|
+
// Windows에서 npx는 npx.cmd — shell 없이는 execFile이 찾지 못함
|
|
83
|
+
shell: process.platform === 'win32',
|
|
50
84
|
});
|
|
51
|
-
|
|
85
|
+
const mtimeAfter = getMtime(resolved);
|
|
86
|
+
if (mtimeAfter > mtimeBefore) {
|
|
87
|
+
return `auto-format reformatted ${path.basename(resolved)} — re-read before further edits to avoid stale old_string`;
|
|
88
|
+
}
|
|
52
89
|
} else if (PYTHON_EXT_RE.test(filePath) && hasBin('black')) {
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
const mtimeBefore = getMtime(resolved);
|
|
91
|
+
await execFileAsync('black', ['--quiet', resolved], { timeout: FORMAT_TIMEOUT_MS });
|
|
92
|
+
const mtimeAfter = getMtime(resolved);
|
|
93
|
+
if (mtimeAfter > mtimeBefore) {
|
|
94
|
+
return `auto-format reformatted ${path.basename(resolved)} — re-read before further edits to avoid stale old_string`;
|
|
95
|
+
}
|
|
55
96
|
} else if (GO_EXT_RE.test(filePath) && hasBin('gofmt')) {
|
|
56
|
-
|
|
57
|
-
|
|
97
|
+
const mtimeBefore = getMtime(resolved);
|
|
98
|
+
await execFileAsync('gofmt', ['-w', resolved], { timeout: FORMAT_TIMEOUT_MS });
|
|
99
|
+
const mtimeAfter = getMtime(resolved);
|
|
100
|
+
if (mtimeAfter > mtimeBefore) {
|
|
101
|
+
return `auto-format reformatted ${path.basename(resolved)} — re-read before further edits to avoid stale old_string`;
|
|
102
|
+
}
|
|
58
103
|
}
|
|
59
104
|
} catch {
|
|
60
105
|
// Format failure should never block — silently continue
|
|
61
106
|
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* in-process 진입점 — 포맷 실행. finding 문자열 배열 반환.
|
|
112
|
+
* @param {{ toolInput: string }} ctx
|
|
113
|
+
* @returns {Promise<{ exitCode: number, findings: string[] }>}
|
|
114
|
+
*/
|
|
115
|
+
export async function run(ctx) {
|
|
116
|
+
const findings = [];
|
|
117
|
+
try {
|
|
118
|
+
const filePath = getFilePath(ctx);
|
|
119
|
+
if (filePath) {
|
|
120
|
+
const finding = await formatFile(filePath);
|
|
121
|
+
if (finding) findings.push(finding);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Silent fail
|
|
125
|
+
}
|
|
126
|
+
return { exitCode: 0, findings };
|
|
62
127
|
}
|
|
63
128
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
129
|
+
// standalone CLI 모드
|
|
130
|
+
if (isDirectRun(import.meta.url)) {
|
|
131
|
+
const { exitCode, findings } = await run(buildCliCtx());
|
|
132
|
+
if (findings.length > 0) process.stdout.write(findings.join('\n') + '\n');
|
|
133
|
+
process.exit(exitCode);
|
|
69
134
|
}
|