@su-record/vibe 3.1.0 → 3.2.1
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 +2 -2
- package/README.en.md +9 -6
- package/README.md +9 -4
- package/dist/__tests__/wiring-integrity.test.d.ts +2 -0
- package/dist/__tests__/wiring-integrity.test.d.ts.map +1 -0
- package/dist/__tests__/wiring-integrity.test.js +160 -0
- package/dist/__tests__/wiring-integrity.test.js.map +1 -0
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +2 -1
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +4 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/design/design-md-parser.test.js +2 -2
- package/dist/cli/design/design-md-parser.test.js.map +1 -1
- package/dist/cli/index.js +102 -128
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
- package/dist/cli/postinstall/claude-agents.js +0 -5
- package/dist/cli/postinstall/claude-agents.js.map +1 -1
- package/dist/cli/postinstall/constants.d.ts +6 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +42 -60
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/inline-skills.js +2 -2
- package/dist/cli/postinstall/inline-skills.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +2 -0
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/postinstall.d.ts +6 -1
- package/dist/cli/postinstall.d.ts.map +1 -1
- package/dist/cli/postinstall.js +6 -1
- package/dist/cli/postinstall.js.map +1 -1
- package/dist/cli/setup/GlobalInstaller.d.ts +1 -15
- package/dist/cli/setup/GlobalInstaller.d.ts.map +1 -1
- package/dist/cli/setup/GlobalInstaller.js +16 -115
- package/dist/cli/setup/GlobalInstaller.js.map +1 -1
- package/dist/cli/setup/GlobalInstaller.test.d.ts +2 -0
- package/dist/cli/setup/GlobalInstaller.test.d.ts.map +1 -0
- package/dist/cli/setup/GlobalInstaller.test.js +16 -0
- package/dist/cli/setup/GlobalInstaller.test.js.map +1 -0
- package/dist/cli/setup/ProjectSetup.js +2 -2
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/cli/setup/Provisioner.d.ts.map +1 -1
- package/dist/cli/setup/Provisioner.js +23 -7
- package/dist/cli/setup/Provisioner.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/utils/cli-detector.d.ts +3 -3
- package/dist/cli/utils/cli-detector.d.ts.map +1 -1
- package/dist/cli/utils/cli-detector.js +7 -27
- package/dist/cli/utils/cli-detector.js.map +1 -1
- package/dist/cli/utils/cli-detector.test.js +3 -2
- package/dist/cli/utils/cli-detector.test.js.map +1 -1
- package/dist/infra/lib/SkillRepository.js +1 -1
- package/dist/infra/lib/evolution/__tests__/integration.test.js +0 -42
- package/dist/infra/lib/evolution/__tests__/integration.test.js.map +1 -1
- package/dist/infra/lib/llm-availability.d.ts.map +1 -1
- package/dist/infra/lib/llm-availability.js +2 -10
- package/dist/infra/lib/llm-availability.js.map +1 -1
- package/dist/infra/lib/llm-availability.test.js +3 -2
- package/dist/infra/lib/llm-availability.test.js.map +1 -1
- package/dist/infra/lib/utils.d.ts +6 -0
- package/dist/infra/lib/utils.d.ts.map +1 -1
- package/dist/infra/lib/utils.js +33 -0
- package/dist/infra/lib/utils.js.map +1 -1
- package/dist/tools/convention/validateCodeQuality.d.ts +1 -0
- package/dist/tools/convention/validateCodeQuality.d.ts.map +1 -1
- package/dist/tools/convention/validateCodeQuality.js +3 -2
- package/dist/tools/convention/validateCodeQuality.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- 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/spec/executionPacket.d.ts +101 -0
- package/dist/tools/spec/executionPacket.d.ts.map +1 -0
- package/dist/tools/spec/executionPacket.js +394 -0
- package/dist/tools/spec/executionPacket.js.map +1 -0
- package/dist/tools/spec/executionPacket.test.d.ts +2 -0
- package/dist/tools/spec/executionPacket.test.d.ts.map +1 -0
- package/dist/tools/spec/executionPacket.test.js +360 -0
- package/dist/tools/spec/executionPacket.test.js.map +1 -0
- package/dist/tools/spec/index.d.ts +2 -0
- package/dist/tools/spec/index.d.ts.map +1 -1
- package/dist/tools/spec/index.js +2 -0
- package/dist/tools/spec/index.js.map +1 -1
- package/dist/tools/spec/specGenerator.d.ts +12 -0
- package/dist/tools/spec/specGenerator.d.ts.map +1 -1
- package/dist/tools/spec/specGenerator.js +96 -14
- package/dist/tools/spec/specGenerator.js.map +1 -1
- package/dist/tools/spec/specGenerator.test.d.ts +2 -0
- package/dist/tools/spec/specGenerator.test.d.ts.map +1 -0
- package/dist/tools/spec/specGenerator.test.js +130 -0
- package/dist/tools/spec/specGenerator.test.js.map +1 -0
- package/hooks/scripts/__tests__/.vibe/command-log.txt +3 -3
- package/hooks/scripts/__tests__/clone-behaviors.test.js +33 -1
- package/hooks/scripts/__tests__/code-check-memory-write.test.js +81 -0
- package/hooks/scripts/__tests__/llm-orchestrate-antigravity.test.js +5 -2
- package/hooks/scripts/__tests__/run-ledger-verify-required.test.js +7 -2
- package/hooks/scripts/__tests__/run-ledger.test.js +161 -10
- package/hooks/scripts/__tests__/step-counter.test.js +21 -0
- package/hooks/scripts/__tests__/utils-npm-root.test.js +5 -4
- package/hooks/scripts/auto-format.js +2 -11
- package/hooks/scripts/auto-test.js +2 -11
- package/hooks/scripts/clone-extract.js +146 -10
- package/hooks/scripts/code-check.js +27 -55
- package/hooks/scripts/lib/glob.js +32 -0
- package/hooks/scripts/lib/hook-context.js +19 -2
- package/hooks/scripts/lib/run-ledger.js +123 -21
- package/hooks/scripts/post-edit.js +2 -3
- package/hooks/scripts/prompt-dispatcher.js +1 -1
- package/hooks/scripts/scope-guard.js +3 -42
- package/hooks/scripts/sentinel-guard.js +5 -20
- package/hooks/scripts/session-start.js +2 -2
- package/hooks/scripts/step-counter.js +35 -57
- package/hooks/scripts/verify-ledger.js +23 -3
- package/package.json +2 -3
- package/skills/clone/SKILL.md +10 -216
- package/skills/clone/references/capture-rules.md +48 -0
- package/skills/clone/references/legal-and-error-recovery.md +40 -0
- package/skills/clone/references/refine-rules.md +61 -0
- package/skills/clone/references/scaffold-phases.md +59 -0
- package/skills/clone/references/setup-and-layout.md +30 -0
- package/skills/docs/SKILL.md +39 -4
- package/skills/docs/references/api-docs-changelog.md +34 -0
- package/skills/docs/references/codemaps-output.md +23 -0
- package/{agents/diagrammer.md → skills/docs/references/diagram-spec.md} +2 -15
- package/skills/exec-plan/SKILL.md +1 -1
- package/skills/exec-plan/templates/plan.md +1 -1
- package/skills/handoff/SKILL.md +3 -3
- package/skills/handoff/templates/handoff.md +1 -1
- package/skills/spec/SKILL.md +7 -0
- package/skills/vibe/SKILL.md +3 -2
- package/skills/vibe.analyze/SKILL.md +12 -249
- package/skills/vibe.analyze/references/output-templates.md +188 -0
- package/skills/vibe.analyze/references/quality-gate.md +93 -0
- package/skills/vibe.clone/SKILL.md +14 -1
- package/skills/vibe.continue/SKILL.md +32 -0
- package/skills/vibe.design/SKILL.md +23 -1
- package/skills/vibe.design/references/ui-preview.md +20 -0
- package/skills/vibe.docs/SKILL.md +4 -2
- package/skills/vibe.figma/SKILL.md +33 -260
- package/skills/vibe.figma/references/branch-phases.md +113 -0
- package/skills/vibe.figma/references/state-schema.md +42 -0
- package/skills/vibe.figma/references/step-algorithms.md +204 -0
- package/skills/vibe.image/SKILL.md +62 -0
- package/skills/vibe.image/references/image-generation-examples.md +39 -0
- package/skills/vibe.reason/SKILL.md +3 -183
- package/skills/vibe.reason/references/output-format-template.md +57 -0
- package/skills/vibe.reason/references/quality-rubrics.md +94 -0
- package/skills/vibe.reason/references/worked-example.md +45 -0
- package/skills/vibe.review/SKILL.md +13 -348
- package/skills/vibe.review/references/boundary-check.md +25 -0
- package/skills/vibe.review/references/output-template.md +74 -0
- package/skills/vibe.review/references/quality-gate.md +84 -0
- package/skills/vibe.review/references/race-mode.md +117 -0
- package/skills/vibe.review/references/worked-examples.md +94 -0
- package/skills/vibe.run/SKILL.md +29 -6
- package/skills/vibe.trace/SKILL.md +3 -3
- package/skills/vibe.verify/SKILL.md +10 -4
- package/vibe/rules/loop-contract.md +7 -3
- package/vibe/templates/claudemd-template.md +1 -1
- package/vibe/templates/spec-template.md +32 -5
- package/agents/acceptance-tester.md +0 -56
- package/agents/documenter.md +0 -43
- package/agents/figma/figma-engineer.md +0 -76
- package/dist/cli/postinstall/index.d.ts +0 -23
- package/dist/cli/postinstall/index.d.ts.map +0 -1
- package/dist/cli/postinstall/index.js +0 -23
- package/dist/cli/postinstall/index.js.map +0 -1
- package/dist/cli/setup/index.d.ts +0 -9
- package/dist/cli/setup/index.d.ts.map +0 -1
- package/dist/cli/setup/index.js +0 -12
- package/dist/cli/setup/index.js.map +0 -1
- 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/evolution-engine.js +0 -91
- package/hooks/scripts/hud-status.js +0 -321
- package/hooks/scripts/skill-injector.js +0 -83
- package/skills/vibe.utils/SKILL.md +0 -415
|
@@ -18,6 +18,11 @@ import { execFileSync, spawnSync } from 'child_process';
|
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
|
|
20
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const PASS_RESULTS = [{ command: 'npm test', exitCode: 0 }];
|
|
22
|
+
|
|
23
|
+
function verifyOptions(ledger, results = PASS_RESULTS) {
|
|
24
|
+
return { runId: ledger.runId, verificationResults: results };
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
// 테스트 격리용 임시 디렉토리
|
|
23
28
|
let tmpDir;
|
|
@@ -59,7 +64,7 @@ describe('run-ledger: round-trip', () => {
|
|
|
59
64
|
it('recordRunStart는 verifyPassed를 false로 리셋', async () => {
|
|
60
65
|
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
61
66
|
recordRunStart(tmpDir, 'f1');
|
|
62
|
-
recordVerify(tmpDir, true);
|
|
67
|
+
recordVerify(tmpDir, true, verifyOptions(readLedger(tmpDir)));
|
|
63
68
|
// 두 번째 run — 리셋
|
|
64
69
|
recordRunStart(tmpDir, 'f2');
|
|
65
70
|
const ledger = readLedger(tmpDir);
|
|
@@ -72,7 +77,7 @@ describe('run-ledger: round-trip', () => {
|
|
|
72
77
|
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
73
78
|
recordRunStart(tmpDir, 'feat');
|
|
74
79
|
const before = new Date().toISOString();
|
|
75
|
-
recordVerify(tmpDir, true);
|
|
80
|
+
recordVerify(tmpDir, true, verifyOptions(readLedger(tmpDir)));
|
|
76
81
|
const ledger = readLedger(tmpDir);
|
|
77
82
|
expect(ledger.verifyPassed).toBe(true);
|
|
78
83
|
expect(ledger.verifyAt).toBeDefined();
|
|
@@ -82,10 +87,133 @@ describe('run-ledger: round-trip', () => {
|
|
|
82
87
|
it('recordVerify(fail) → verifyPassed=false', async () => {
|
|
83
88
|
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
84
89
|
recordRunStart(tmpDir, 'feat');
|
|
85
|
-
recordVerify(tmpDir, false);
|
|
90
|
+
recordVerify(tmpDir, false, verifyOptions(readLedger(tmpDir), []));
|
|
86
91
|
const ledger = readLedger(tmpDir);
|
|
87
92
|
expect(ledger.verifyPassed).toBe(false);
|
|
88
93
|
});
|
|
94
|
+
|
|
95
|
+
it('recordVerify → 안정된 runId 경로에 evidence.json 원자 기록', async () => {
|
|
96
|
+
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
97
|
+
recordRunStart(tmpDir, 'feat');
|
|
98
|
+
const started = readLedger(tmpDir);
|
|
99
|
+
fs.mkdirSync(path.join(tmpDir, '.vibe', 'specs'), { recursive: true });
|
|
100
|
+
fs.writeFileSync(path.join(tmpDir, '.vibe', 'specs', 'feat.md'), '# SPEC');
|
|
101
|
+
expect(recordVerify(tmpDir, true, verifyOptions(started))).toBe(true);
|
|
102
|
+
|
|
103
|
+
const after = readLedger(tmpDir);
|
|
104
|
+
expect(after.runId).toBe(started.runId);
|
|
105
|
+
const evidencePath = path.join(tmpDir, '.vibe', 'runs', after.runId, 'evidence.json');
|
|
106
|
+
expect(fs.existsSync(evidencePath)).toBe(true);
|
|
107
|
+
const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
|
|
108
|
+
expect(evidence).toMatchObject({
|
|
109
|
+
schemaVersion: '1.0.0',
|
|
110
|
+
runId: after.runId,
|
|
111
|
+
specPath: '.vibe/specs/feat.md',
|
|
112
|
+
judges: {
|
|
113
|
+
deterministic: { authority: 'blocking', verifyPassed: true },
|
|
114
|
+
model: { authority: 'advisory-only', canComplete: false },
|
|
115
|
+
humanTaste: { authority: 'release-only', canComplete: false },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('레거시 ledger도 runId를 보강하고 누락된 검증 결과는 빈 배열로 기록', async () => {
|
|
121
|
+
const p = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
|
|
122
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
123
|
+
fs.writeFileSync(p, JSON.stringify({ runStarted: '2026-01-01T00:00:00.000Z' }));
|
|
124
|
+
const { recordVerify, readLedger } = await importLedger();
|
|
125
|
+
expect(recordVerify(tmpDir, false)).toBe(true);
|
|
126
|
+
const ledger = readLedger(tmpDir);
|
|
127
|
+
const evidencePath = path.join(tmpDir, '.vibe', 'runs', ledger.runId, 'evidence.json');
|
|
128
|
+
const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
|
|
129
|
+
expect(evidence.specPath).toBeNull();
|
|
130
|
+
expect(evidence.judges.deterministic.verificationResults).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('기존 verificationResults의 command와 exitCode를 evidence에 보존', async () => {
|
|
134
|
+
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
135
|
+
recordRunStart(tmpDir, 'feat');
|
|
136
|
+
const p = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
|
|
137
|
+
const ledger = readLedger(tmpDir);
|
|
138
|
+
fs.writeFileSync(p, JSON.stringify({
|
|
139
|
+
...ledger,
|
|
140
|
+
verificationResults: [{ command: 'npm test', exitCode: 0 }],
|
|
141
|
+
}));
|
|
142
|
+
recordVerify(tmpDir, true, verifyOptions(ledger, [
|
|
143
|
+
{ command: 'npm test', exitCode: 0 },
|
|
144
|
+
]));
|
|
145
|
+
const evidencePath = path.join(tmpDir, '.vibe', 'runs', ledger.runId, 'evidence.json');
|
|
146
|
+
const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
|
|
147
|
+
expect(evidence.judges.deterministic.verificationResults)
|
|
148
|
+
.toEqual([{ command: 'npm test', exitCode: 0 }]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('새 run은 이전 run의 검증 결과를 제거', async () => {
|
|
152
|
+
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
153
|
+
recordRunStart(tmpDir, 'first');
|
|
154
|
+
const first = readLedger(tmpDir);
|
|
155
|
+
recordVerify(tmpDir, true, verifyOptions(first, [{ command: 'old-test', exitCode: 0 }]));
|
|
156
|
+
|
|
157
|
+
recordRunStart(tmpDir, 'second');
|
|
158
|
+
const second = readLedger(tmpDir);
|
|
159
|
+
expect(second.verificationResults).toBeUndefined();
|
|
160
|
+
recordVerify(tmpDir, true, verifyOptions(second));
|
|
161
|
+
const evidencePath = path.join(tmpDir, '.vibe', 'runs', second.runId, 'evidence.json');
|
|
162
|
+
const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
|
|
163
|
+
expect(evidence.judges.deterministic.verificationResults).toEqual(PASS_RESULTS);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('조작된 runId는 프로젝트 밖에 evidence를 기록하지 않음', async () => {
|
|
167
|
+
const ledgerPath = path.join(tmpDir, '.vibe', 'metrics', 'run-ledger.json');
|
|
168
|
+
const escapedDir = path.join(path.dirname(tmpDir), `escaped-${path.basename(tmpDir)}`);
|
|
169
|
+
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
170
|
+
fs.writeFileSync(ledgerPath, JSON.stringify({
|
|
171
|
+
runId: `../../../${path.basename(escapedDir)}`,
|
|
172
|
+
runStarted: new Date().toISOString(),
|
|
173
|
+
verifyPassed: false,
|
|
174
|
+
}));
|
|
175
|
+
const { recordVerify } = await importLedger();
|
|
176
|
+
expect(recordVerify(tmpDir, true, {
|
|
177
|
+
runId: `../../../${path.basename(escapedDir)}`,
|
|
178
|
+
verificationResults: PASS_RESULTS,
|
|
179
|
+
})).toBe(false);
|
|
180
|
+
expect(fs.existsSync(path.join(escapedDir, 'evidence.json'))).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('evidence 기록 실패 시 ledger를 통과 상태로 바꾸지 않음', async () => {
|
|
184
|
+
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
185
|
+
recordRunStart(tmpDir, 'feat');
|
|
186
|
+
const ledger = readLedger(tmpDir);
|
|
187
|
+
const runPath = path.join(tmpDir, '.vibe', 'runs', ledger.runId);
|
|
188
|
+
fs.mkdirSync(path.dirname(runPath), { recursive: true });
|
|
189
|
+
fs.writeFileSync(runPath, 'occupied');
|
|
190
|
+
|
|
191
|
+
expect(recordVerify(tmpDir, true, verifyOptions(ledger))).toBe(false);
|
|
192
|
+
expect(readLedger(tmpDir).verifyPassed).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('다른 runId의 verify 결과를 현재 run에 적용하지 않음', async () => {
|
|
196
|
+
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
197
|
+
recordRunStart(tmpDir, 'feat');
|
|
198
|
+
expect(recordVerify(tmpDir, true, {
|
|
199
|
+
runId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
|
|
200
|
+
verificationResults: PASS_RESULTS,
|
|
201
|
+
})).toBe(false);
|
|
202
|
+
expect(readLedger(tmpDir).verifyPassed).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('분할 SPEC의 _index.md를 evidence 경로로 기록', async () => {
|
|
206
|
+
const { recordRunStart, recordVerify, readLedger } = await importLedger();
|
|
207
|
+
const specDir = path.join(tmpDir, '.vibe', 'specs', 'large-feature');
|
|
208
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
209
|
+
fs.writeFileSync(path.join(specDir, '_index.md'), '# Master SPEC');
|
|
210
|
+
recordRunStart(tmpDir, 'large-feature');
|
|
211
|
+
const ledger = readLedger(tmpDir);
|
|
212
|
+
recordVerify(tmpDir, true, verifyOptions(ledger));
|
|
213
|
+
const evidencePath = path.join(tmpDir, '.vibe', 'runs', ledger.runId, 'evidence.json');
|
|
214
|
+
const evidence = JSON.parse(fs.readFileSync(evidencePath, 'utf-8'));
|
|
215
|
+
expect(evidence.specPath).toBe('.vibe/specs/large-feature/_index.md');
|
|
216
|
+
});
|
|
89
217
|
});
|
|
90
218
|
|
|
91
219
|
// ──────────────────────────────────────────────────────────────────
|
|
@@ -101,7 +229,7 @@ describe('run-ledger: verifyAt > runStarted', () => {
|
|
|
101
229
|
const patched = { ...ledger, runStarted: new Date(Date.now() - 1000).toISOString() };
|
|
102
230
|
fs.writeFileSync(p, JSON.stringify(patched), 'utf-8');
|
|
103
231
|
|
|
104
|
-
recordVerify(tmpDir, true);
|
|
232
|
+
recordVerify(tmpDir, true, verifyOptions(readLedger(tmpDir)));
|
|
105
233
|
const after = readLedger(tmpDir);
|
|
106
234
|
expect(after.verifyAt > after.runStarted).toBe(true);
|
|
107
235
|
expect(after.verifyPassed).toBe(true);
|
|
@@ -170,6 +298,12 @@ describe('run-ledger: vibe.run 감지 헬퍼', () => {
|
|
|
170
298
|
expect(extractRunFeature('$vibe.run auth-login ultrawork')).toBe('auth-login');
|
|
171
299
|
});
|
|
172
300
|
|
|
301
|
+
it('extractRunFeature: 따옴표를 제거해 SPEC 경로용 기능명을 반환', async () => {
|
|
302
|
+
const { extractRunFeature } = await importLedger();
|
|
303
|
+
expect(extractRunFeature('/vibe.run "auth-login"')).toBe('auth-login');
|
|
304
|
+
expect(extractRunFeature("$vibe.run 'auth-login'")).toBe('auth-login');
|
|
305
|
+
});
|
|
306
|
+
|
|
173
307
|
it('extractRunFeature: 기능명 없으면 null', async () => {
|
|
174
308
|
const { extractRunFeature } = await importLedger();
|
|
175
309
|
expect(extractRunFeature('/vibe.run --phase 1')).toBeNull();
|
|
@@ -200,11 +334,17 @@ describe('run-ledger: stopWarned', () => {
|
|
|
200
334
|
describe('verify-ledger CLI', () => {
|
|
201
335
|
const CLI = path.resolve(__dirname, '..', 'verify-ledger.js');
|
|
202
336
|
|
|
337
|
+
function cliArgs(status, ledger, results = PASS_RESULTS) {
|
|
338
|
+
const resultsPath = path.join(tmpDir, 'verification-results.json');
|
|
339
|
+
fs.writeFileSync(resultsPath, JSON.stringify(results));
|
|
340
|
+
return [CLI, status, ledger.runId, resultsPath];
|
|
341
|
+
}
|
|
342
|
+
|
|
203
343
|
it('pass 인자 → verifyPassed=true, exit 0', async () => {
|
|
204
|
-
const { recordRunStart } = await importLedger();
|
|
344
|
+
const { recordRunStart, readLedger } = await importLedger();
|
|
205
345
|
recordRunStart(tmpDir, 'feat');
|
|
206
346
|
|
|
207
|
-
const result = spawnSync('node',
|
|
347
|
+
const result = spawnSync('node', cliArgs('pass', readLedger(tmpDir)), {
|
|
208
348
|
encoding: 'utf-8',
|
|
209
349
|
timeout: 5000,
|
|
210
350
|
env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
|
|
@@ -212,16 +352,15 @@ describe('verify-ledger CLI', () => {
|
|
|
212
352
|
expect(result.status).toBe(0);
|
|
213
353
|
expect(result.stdout).toContain('verifyPassed=true');
|
|
214
354
|
|
|
215
|
-
const { readLedger } = await importLedger();
|
|
216
355
|
const ledger = readLedger(tmpDir);
|
|
217
356
|
expect(ledger.verifyPassed).toBe(true);
|
|
218
357
|
});
|
|
219
358
|
|
|
220
359
|
it('fail 인자 → verifyPassed=false, exit 0', async () => {
|
|
221
|
-
const { recordRunStart } = await importLedger();
|
|
360
|
+
const { recordRunStart, readLedger } = await importLedger();
|
|
222
361
|
recordRunStart(tmpDir, 'feat');
|
|
223
362
|
|
|
224
|
-
const result = spawnSync('node',
|
|
363
|
+
const result = spawnSync('node', cliArgs('fail', readLedger(tmpDir), []), {
|
|
225
364
|
encoding: 'utf-8',
|
|
226
365
|
timeout: 5000,
|
|
227
366
|
env: { ...process.env, CLAUDE_PROJECT_DIR: tmpDir },
|
|
@@ -229,7 +368,6 @@ describe('verify-ledger CLI', () => {
|
|
|
229
368
|
expect(result.status).toBe(0);
|
|
230
369
|
expect(result.stdout).toContain('verifyPassed=false');
|
|
231
370
|
|
|
232
|
-
const { readLedger } = await importLedger();
|
|
233
371
|
const ledger = readLedger(tmpDir);
|
|
234
372
|
expect(ledger.verifyPassed).toBe(false);
|
|
235
373
|
});
|
|
@@ -242,6 +380,19 @@ describe('verify-ledger CLI', () => {
|
|
|
242
380
|
});
|
|
243
381
|
expect(result.status).toBe(0);
|
|
244
382
|
});
|
|
383
|
+
|
|
384
|
+
it('기록 실패는 exit 0을 유지하되 stderr 경고로 노출', () => {
|
|
385
|
+
const projectFile = path.join(tmpDir, 'not-a-directory');
|
|
386
|
+
fs.writeFileSync(projectFile, 'occupied');
|
|
387
|
+
const result = spawnSync('node', [CLI, 'pass'], {
|
|
388
|
+
encoding: 'utf-8',
|
|
389
|
+
timeout: 5000,
|
|
390
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectFile },
|
|
391
|
+
});
|
|
392
|
+
expect(result.status).toBe(0);
|
|
393
|
+
expect(result.stderr).toContain('evidence.json write failed');
|
|
394
|
+
expect(result.stdout).not.toContain('recorded:');
|
|
395
|
+
});
|
|
245
396
|
});
|
|
246
397
|
|
|
247
398
|
// ──────────────────────────────────────────────────────────────────
|
|
@@ -435,4 +435,25 @@ describe('step-counter PostToolUse hook', () => {
|
|
|
435
435
|
expect(files).toHaveLength(1);
|
|
436
436
|
});
|
|
437
437
|
});
|
|
438
|
+
|
|
439
|
+
// ───────── A2/D2: 64KB 초과 stdin 페이로드 (구 단일 버퍼 리더 절단 버그 회귀) ─────────
|
|
440
|
+
describe('stdin 64KB 초과 페이로드 — 절단 없이 처리', () => {
|
|
441
|
+
it('페이로드가 64KB를 넘어도 JSON 전체를 읽어 target_file을 정확히 기록', () => {
|
|
442
|
+
// 구현이 64KB 단일 버퍼로 되돌아가면 JSON이 중간에 잘려 파싱 실패 →
|
|
443
|
+
// stdinPayload=null → jsonl 라인 자체가 기록되지 않는다.
|
|
444
|
+
const padding = 'x'.repeat(80000); // 총 페이로드 > 64KB 보장
|
|
445
|
+
runCounter({
|
|
446
|
+
payload: {
|
|
447
|
+
tool_name: 'Edit',
|
|
448
|
+
tool_input: { file_path: 'src/foo.ts', padding },
|
|
449
|
+
tool_response: {},
|
|
450
|
+
},
|
|
451
|
+
projectDir,
|
|
452
|
+
});
|
|
453
|
+
const lines = readJsonl(runJsonl);
|
|
454
|
+
expect(lines).toHaveLength(1);
|
|
455
|
+
expect(lines[0].target_file).toBe('src/foo.ts');
|
|
456
|
+
expect(lines[0].tool).toBe('Edit');
|
|
457
|
+
});
|
|
458
|
+
});
|
|
438
459
|
});
|
|
@@ -15,10 +15,11 @@ import { spawnSync } from 'child_process';
|
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import os from 'os';
|
|
17
17
|
import path from 'path';
|
|
18
|
-
import { fileURLToPath } from 'url';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
19
19
|
|
|
20
20
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
|
|
21
|
+
// Windows 절대경로(C:\...)는 ESM import 지정자가 될 수 없으므로 file:// URL로 변환
|
|
22
|
+
const UTILS_URL = pathToFileURL(path.resolve(__dirname, '..', 'utils.js')).href;
|
|
22
23
|
|
|
23
24
|
function makeTempCacheFile() {
|
|
24
25
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vibe-npm-root-test-'));
|
|
@@ -30,8 +31,8 @@ function makeTempCacheFile() {
|
|
|
30
31
|
* VIBE_NPM_ROOT_CACHE_FILE 환경 변수로 캐시 파일 경로를 주입한다.
|
|
31
32
|
*/
|
|
32
33
|
function runGetNpmRoot(cacheFilePath) {
|
|
33
|
-
return spawnSync(
|
|
34
|
-
`import { getGlobalNpmPath } from '${
|
|
34
|
+
return spawnSync(process.execPath, ['--input-type=module', '--eval',
|
|
35
|
+
`import { getGlobalNpmPath } from '${UTILS_URL}';
|
|
35
36
|
process.stdout.write(getGlobalNpmPath() || '');`
|
|
36
37
|
], {
|
|
37
38
|
encoding: 'utf-8',
|
|
@@ -24,15 +24,6 @@ const PYTHON_EXT_RE = /\.py$/;
|
|
|
24
24
|
const GO_EXT_RE = /\.go$/;
|
|
25
25
|
const FORMAT_TIMEOUT_MS = 5000;
|
|
26
26
|
|
|
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
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
27
|
// PATH 직접 스캔 — `which` execSync는 매 파일 저장마다 자식 프로세스를 동기
|
|
37
28
|
// spawn하므로, fs.existsSync로 대체하고 프로세스 내 캐싱한다.
|
|
38
29
|
const _binCache = new Map();
|
|
@@ -109,13 +100,13 @@ async function formatFile(filePath) {
|
|
|
109
100
|
|
|
110
101
|
/**
|
|
111
102
|
* in-process 진입점 — 포맷 실행. finding 문자열 배열 반환.
|
|
112
|
-
* @param {{
|
|
103
|
+
* @param {{ filePath: string }} ctx
|
|
113
104
|
* @returns {Promise<{ exitCode: number, findings: string[] }>}
|
|
114
105
|
*/
|
|
115
106
|
export async function run(ctx) {
|
|
116
107
|
const findings = [];
|
|
117
108
|
try {
|
|
118
|
-
const filePath =
|
|
109
|
+
const filePath = ctx.filePath;
|
|
119
110
|
if (filePath) {
|
|
120
111
|
const finding = await formatFile(filePath);
|
|
121
112
|
if (finding) findings.push(finding);
|
|
@@ -33,15 +33,6 @@ const DEBOUNCE_COOLDOWN_MS = 120_000;
|
|
|
33
33
|
/** debounce 상태 파일 경로 */
|
|
34
34
|
const DEBOUNCE_STATE_FILE = path.join(PROJECT_DIR, '.vibe', 'metrics', 'auto-test-state.json');
|
|
35
35
|
|
|
36
|
-
function getFilePath(ctx) {
|
|
37
|
-
try {
|
|
38
|
-
const input = JSON.parse(ctx.toolInput || '{}');
|
|
39
|
-
return input.file_path || input.path || '';
|
|
40
|
-
} catch {
|
|
41
|
-
return '';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
36
|
function isTestFile(filePath) {
|
|
46
37
|
return TEST_SUFFIXES.some(s => filePath.includes(s));
|
|
47
38
|
}
|
|
@@ -163,13 +154,13 @@ function updateDebounceState(testFile, srcFile) {
|
|
|
163
154
|
|
|
164
155
|
/**
|
|
165
156
|
* in-process 진입점 — 테스트 실행. findings 반환.
|
|
166
|
-
* @param {{
|
|
157
|
+
* @param {{ filePath: string }} ctx
|
|
167
158
|
* @returns {Promise<{ exitCode: number, findings: string[] }>}
|
|
168
159
|
*/
|
|
169
160
|
export async function run(ctx) {
|
|
170
161
|
const findings = [];
|
|
171
162
|
try {
|
|
172
|
-
const filePath =
|
|
163
|
+
const filePath = ctx.filePath;
|
|
173
164
|
if (!filePath || !CODE_EXT_RE.test(filePath)) return { exitCode: 0, findings };
|
|
174
165
|
|
|
175
166
|
const mode = getTestMode();
|
|
@@ -78,6 +78,11 @@ const CSS_PROPS = [
|
|
|
78
78
|
'mask-image', 'mask-size', 'mask-position', 'clip-path',
|
|
79
79
|
];
|
|
80
80
|
|
|
81
|
+
const SUB_URL_LIMIT = 200;
|
|
82
|
+
const SUB_EXCLUDE_TEXT_RE = /^(?:검색|search|top|home|kor|eng|chn|vtn|language|언어|개인정보|privacy|문의|제안|sitemap|사이트맵|youtube|뉴스룸|newsroom|cookie|닫기|partner|파트너)$/i;
|
|
83
|
+
const SUB_EXCLUDE_PATH_RE = /\/(?:search|sitemap|privacy|contact|inquiry|cookie|login|auth|policy)(?:\/|$)/i;
|
|
84
|
+
const SUB_EXCLUDE_EXT_RE = /\.(?:pdf|zip|jpe?g|png|gif|webp|svg|mp4|mov|avi|docx?|xlsx?|pptx?)$/i;
|
|
85
|
+
|
|
81
86
|
// ─── CLI parse ──────────────────────────────────────────────────────
|
|
82
87
|
function parseArgs(argv) {
|
|
83
88
|
const [, , cmd, urlArg, ...rest] = argv;
|
|
@@ -104,6 +109,74 @@ function parseViewport(v) {
|
|
|
104
109
|
};
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
function localePrefixOf(rawUrl) {
|
|
113
|
+
const first = new URL(rawUrl).pathname.split('/').filter(Boolean)[0];
|
|
114
|
+
if (!first || !/^[a-z]{2,3}(?:-[a-z]{2})?$/i.test(first)) return null;
|
|
115
|
+
return `/${first}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeSubUrl(startUrl, href) {
|
|
119
|
+
if (!href || typeof href !== 'string') return null;
|
|
120
|
+
try {
|
|
121
|
+
const target = new URL(href, startUrl);
|
|
122
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:') return null;
|
|
123
|
+
target.hash = '';
|
|
124
|
+
target.search = '';
|
|
125
|
+
target.pathname = target.pathname.replace(/\/+$/, '') || '/';
|
|
126
|
+
return target;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isSameMenuScope(target, start, localePrefix) {
|
|
133
|
+
if (target.origin !== start.origin) return false;
|
|
134
|
+
if (!localePrefix) return true;
|
|
135
|
+
return target.pathname === localePrefix || target.pathname.startsWith(`${localePrefix}/`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isExcludedSubLink(target, text) {
|
|
139
|
+
const label = String(text || '').trim();
|
|
140
|
+
if (SUB_EXCLUDE_TEXT_RE.test(label)) return true;
|
|
141
|
+
if (SUB_EXCLUDE_PATH_RE.test(target.pathname)) return true;
|
|
142
|
+
return SUB_EXCLUDE_EXT_RE.test(target.pathname);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function collectSubUrls(startUrl, links, limit = SUB_URL_LIMIT) {
|
|
146
|
+
const start = normalizeSubUrl(startUrl, startUrl);
|
|
147
|
+
if (!start) return [];
|
|
148
|
+
const localePrefix = localePrefixOf(start.href);
|
|
149
|
+
const urls = [start.href];
|
|
150
|
+
const seen = new Set(urls);
|
|
151
|
+
|
|
152
|
+
for (const link of links) {
|
|
153
|
+
if (urls.length >= limit) break;
|
|
154
|
+
const target = normalizeSubUrl(start.href, link.href);
|
|
155
|
+
if (!target || seen.has(target.href)) continue;
|
|
156
|
+
if (!isSameMenuScope(target, start, localePrefix)) continue;
|
|
157
|
+
if (isExcludedSubLink(target, link.text)) continue;
|
|
158
|
+
seen.add(target.href);
|
|
159
|
+
urls.push(target.href);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return urls;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function findSitemapUrl(startUrl, links) {
|
|
166
|
+
const start = normalizeSubUrl(startUrl, startUrl);
|
|
167
|
+
if (!start) return null;
|
|
168
|
+
const localePrefix = localePrefixOf(start.href);
|
|
169
|
+
|
|
170
|
+
for (const link of links) {
|
|
171
|
+
const label = String(link.text || '').trim();
|
|
172
|
+
if (!/sitemap|사이트맵/i.test(`${label} ${link.href}`)) continue;
|
|
173
|
+
const target = normalizeSubUrl(start.href, link.href);
|
|
174
|
+
if (target && isSameMenuScope(target, start, localePrefix)) return target.href;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
107
180
|
// ─── Puppeteer dynamic loader (optional dep) ────────────────────────
|
|
108
181
|
async function loadPuppeteer() {
|
|
109
182
|
try {
|
|
@@ -905,6 +978,14 @@ const SEO_EXTRACT = `(function () {
|
|
|
905
978
|
return Array.from(new Set(icons.concat(og)));
|
|
906
979
|
})`;
|
|
907
980
|
|
|
981
|
+
const SUB_LINK_EXTRACT = `(function () {
|
|
982
|
+
const links = Array.from(document.querySelectorAll('a[href]')).map((a) => ({
|
|
983
|
+
href: a.getAttribute('href'),
|
|
984
|
+
text: (a.innerText || a.textContent || '').replace(/\\s+/g, ' ').trim(),
|
|
985
|
+
}));
|
|
986
|
+
return { links };
|
|
987
|
+
})`;
|
|
988
|
+
|
|
908
989
|
// HTML sanitization: strip scripts/analytics before saving
|
|
909
990
|
function sanitizeHtml(html) {
|
|
910
991
|
return html
|
|
@@ -914,6 +995,65 @@ function sanitizeHtml(html) {
|
|
|
914
995
|
.replace(/\son[a-z]+='[^']*'/gi, '');
|
|
915
996
|
}
|
|
916
997
|
|
|
998
|
+
async function newClonePage(browser) {
|
|
999
|
+
const page = await browser.newPage();
|
|
1000
|
+
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
|
|
1001
|
+
await page.setUserAgent(
|
|
1002
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
1003
|
+
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
1004
|
+
);
|
|
1005
|
+
await page.setCacheEnabled(false);
|
|
1006
|
+
return page;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function discoverSubUrls({ url, opts }) {
|
|
1010
|
+
if (!url) throw new Error('URL is required');
|
|
1011
|
+
if (!opts.out) throw new Error('--out=<file> is required');
|
|
1012
|
+
|
|
1013
|
+
if (!opts.ignoreRobots) {
|
|
1014
|
+
const blocked = await checkRobots(url);
|
|
1015
|
+
if (blocked) {
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`robots.txt disallows ${url}. Pass --ignore-robots only with the site owner's permission.`,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const puppeteer = await loadPuppeteer();
|
|
1023
|
+
const browser = await puppeteer.launch({
|
|
1024
|
+
headless: 'new',
|
|
1025
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
const page = await newClonePage(browser);
|
|
1030
|
+
await page.goto(url, { waitUntil: 'networkidle2', timeout: 90000 });
|
|
1031
|
+
await wait(800);
|
|
1032
|
+
|
|
1033
|
+
let source = 'menu';
|
|
1034
|
+
let sourceUrl = url;
|
|
1035
|
+
let { links } = await page.evaluate(`(${SUB_LINK_EXTRACT})()`);
|
|
1036
|
+
const sitemapUrl = findSitemapUrl(url, links);
|
|
1037
|
+
if (sitemapUrl) {
|
|
1038
|
+
await page.goto(sitemapUrl, { waitUntil: 'networkidle2', timeout: 90000 });
|
|
1039
|
+
await wait(800);
|
|
1040
|
+
source = 'sitemap';
|
|
1041
|
+
sourceUrl = sitemapUrl;
|
|
1042
|
+
({ links } = await page.evaluate(`(${SUB_LINK_EXTRACT})()`));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const urls = collectSubUrls(url, links);
|
|
1046
|
+
fs.mkdirSync(path.dirname(opts.out), { recursive: true });
|
|
1047
|
+
fs.writeFileSync(
|
|
1048
|
+
opts.out,
|
|
1049
|
+
JSON.stringify({ meta: { url, source, sourceUrl, count: urls.length }, urls }, null, 2),
|
|
1050
|
+
);
|
|
1051
|
+
process.stdout.write(`[clone-extract] suburls ${urls.length} → ${opts.out}\n`);
|
|
1052
|
+
} finally {
|
|
1053
|
+
await browser.close();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
917
1057
|
// ─── Main capture flow ──────────────────────────────────────────────
|
|
918
1058
|
async function capture({ url, opts }) {
|
|
919
1059
|
if (!url) throw new Error('URL is required');
|
|
@@ -954,14 +1094,8 @@ async function capture({ url, opts }) {
|
|
|
954
1094
|
});
|
|
955
1095
|
|
|
956
1096
|
try {
|
|
957
|
-
const page = await browser
|
|
1097
|
+
const page = await newClonePage(browser);
|
|
958
1098
|
await page.setViewport(viewport);
|
|
959
|
-
await page.setUserAgent(
|
|
960
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
961
|
-
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
962
|
-
);
|
|
963
|
-
// disable cache to get fresh assets
|
|
964
|
-
await page.setCacheEnabled(false);
|
|
965
1099
|
|
|
966
1100
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: 90000 });
|
|
967
1101
|
await scrollToBottom(page);
|
|
@@ -1140,16 +1274,18 @@ const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('
|
|
|
1140
1274
|
if (isMain) {
|
|
1141
1275
|
const { cmd, url, opts } = parseArgs(process.argv);
|
|
1142
1276
|
|
|
1143
|
-
if (cmd !== 'capture') {
|
|
1277
|
+
if (cmd !== 'capture' && cmd !== 'suburls') {
|
|
1144
1278
|
console.error('Usage: node clone-extract.js capture <URL> --out=<dir> --viewport=WxH[@DPR] --bp=mo|pc [--stealth] [--ignore-robots] [--no-interact] [--wait=ms]');
|
|
1279
|
+
console.error(' node clone-extract.js suburls <URL> --out=<file> [--ignore-robots]');
|
|
1145
1280
|
process.exit(1);
|
|
1146
1281
|
}
|
|
1147
1282
|
|
|
1148
|
-
|
|
1283
|
+
const run = cmd === 'suburls' ? discoverSubUrls : capture;
|
|
1284
|
+
run({ url, opts }).catch((err) => {
|
|
1149
1285
|
console.error(`[clone-extract] FAIL: ${err.message}`);
|
|
1150
1286
|
if (process.env.DEBUG) console.error(err.stack);
|
|
1151
1287
|
process.exit(1);
|
|
1152
1288
|
});
|
|
1153
1289
|
}
|
|
1154
1290
|
|
|
1155
|
-
export { diffStyles };
|
|
1291
|
+
export { collectSubUrls, diffStyles };
|