@nerviq/cli 0.0.1 → 0.9.0-beta.2
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/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/cli.js +749 -0
- package/content/case-study-template.md +91 -0
- package/content/claims-governance.md +37 -0
- package/content/claude-code/audit-repo/SKILL.md +20 -0
- package/content/claude-native-integration.md +60 -0
- package/content/devto-article.json +9 -0
- package/content/launch-posts.md +226 -0
- package/content/pilot-rollout-kit.md +30 -0
- package/content/release-checklist.md +31 -0
- package/package.json +53 -4
- package/src/activity.js +529 -0
- package/src/aider/activity.js +226 -0
- package/src/aider/config-parser.js +166 -0
- package/src/aider/context.js +158 -0
- package/src/aider/deep-review.js +316 -0
- package/src/aider/domain-packs.js +278 -0
- package/src/aider/freshness.js +168 -0
- package/src/aider/governance.js +253 -0
- package/src/aider/interactive.js +334 -0
- package/src/aider/mcp-packs.js +98 -0
- package/src/aider/patch.js +214 -0
- package/src/aider/plans.js +186 -0
- package/src/aider/premium.js +360 -0
- package/src/aider/setup.js +404 -0
- package/src/aider/techniques.js +1323 -0
- package/src/analyze.js +821 -0
- package/src/audit.js +1003 -0
- package/src/badge.js +13 -0
- package/src/benchmark.js +339 -0
- package/src/claudex-sync.json +7 -0
- package/src/codex/activity.js +324 -0
- package/src/codex/config-parser.js +183 -0
- package/src/codex/context.js +221 -0
- package/src/codex/deep-review.js +493 -0
- package/src/codex/domain-packs.js +372 -0
- package/src/codex/freshness.js +167 -0
- package/src/codex/governance.js +192 -0
- package/src/codex/interactive.js +618 -0
- package/src/codex/mcp-packs.js +660 -0
- package/src/codex/patch.js +209 -0
- package/src/codex/plans.js +251 -0
- package/src/codex/premium.js +614 -0
- package/src/codex/setup.js +603 -0
- package/src/codex/techniques.js +2649 -0
- package/src/context.js +272 -0
- package/src/copilot/activity.js +309 -0
- package/src/copilot/config-parser.js +226 -0
- package/src/copilot/context.js +197 -0
- package/src/copilot/deep-review.js +346 -0
- package/src/copilot/domain-packs.js +350 -0
- package/src/copilot/freshness.js +197 -0
- package/src/copilot/governance.js +222 -0
- package/src/copilot/interactive.js +406 -0
- package/src/copilot/mcp-packs.js +572 -0
- package/src/copilot/patch.js +238 -0
- package/src/copilot/plans.js +253 -0
- package/src/copilot/premium.js +450 -0
- package/src/copilot/setup.js +488 -0
- package/src/copilot/techniques.js +1822 -0
- package/src/cursor/activity.js +301 -0
- package/src/cursor/config-parser.js +265 -0
- package/src/cursor/context.js +236 -0
- package/src/cursor/deep-review.js +334 -0
- package/src/cursor/domain-packs.js +346 -0
- package/src/cursor/freshness.js +214 -0
- package/src/cursor/governance.js +229 -0
- package/src/cursor/interactive.js +391 -0
- package/src/cursor/mcp-packs.js +571 -0
- package/src/cursor/patch.js +243 -0
- package/src/cursor/plans.js +254 -0
- package/src/cursor/premium.js +468 -0
- package/src/cursor/setup.js +488 -0
- package/src/cursor/techniques.js +1786 -0
- package/src/deep-review.js +345 -0
- package/src/domain-packs.js +364 -0
- package/src/formatters/sarif.js +115 -0
- package/src/gemini/activity.js +402 -0
- package/src/gemini/config-parser.js +275 -0
- package/src/gemini/context.js +221 -0
- package/src/gemini/deep-review.js +559 -0
- package/src/gemini/domain-packs.js +371 -0
- package/src/gemini/freshness.js +204 -0
- package/src/gemini/governance.js +201 -0
- package/src/gemini/interactive.js +860 -0
- package/src/gemini/mcp-packs.js +658 -0
- package/src/gemini/patch.js +229 -0
- package/src/gemini/plans.js +269 -0
- package/src/gemini/premium.js +759 -0
- package/src/gemini/setup.js +692 -0
- package/src/gemini/techniques.js +2084 -0
- package/src/governance.js +523 -0
- package/src/harmony/advisor.js +383 -0
- package/src/harmony/audit.js +303 -0
- package/src/harmony/canon.js +444 -0
- package/src/harmony/cli.js +331 -0
- package/src/harmony/drift.js +401 -0
- package/src/harmony/governance.js +313 -0
- package/src/harmony/memory.js +238 -0
- package/src/harmony/sync.js +458 -0
- package/src/harmony/watch.js +336 -0
- package/src/index.js +256 -0
- package/src/insights.js +119 -0
- package/src/interactive.js +118 -0
- package/src/mcp-packs.js +597 -0
- package/src/opencode/activity.js +286 -0
- package/src/opencode/config-parser.js +109 -0
- package/src/opencode/context.js +247 -0
- package/src/opencode/deep-review.js +313 -0
- package/src/opencode/domain-packs.js +240 -0
- package/src/opencode/freshness.js +158 -0
- package/src/opencode/governance.js +159 -0
- package/src/opencode/interactive.js +392 -0
- package/src/opencode/mcp-packs.js +474 -0
- package/src/opencode/patch.js +184 -0
- package/src/opencode/plans.js +231 -0
- package/src/opencode/premium.js +413 -0
- package/src/opencode/setup.js +449 -0
- package/src/opencode/techniques.js +1713 -0
- package/src/plans.js +655 -0
- package/src/secret-patterns.js +30 -0
- package/src/setup.js +1274 -0
- package/src/synergy/adaptive.js +261 -0
- package/src/synergy/compensation.js +156 -0
- package/src/synergy/evidence.js +193 -0
- package/src/synergy/learning.js +184 -0
- package/src/synergy/patterns.js +227 -0
- package/src/synergy/ranking.js +83 -0
- package/src/synergy/report.js +163 -0
- package/src/synergy/routing.js +152 -0
- package/src/techniques.js +1354 -0
- package/src/watch.js +229 -0
- package/src/windsurf/activity.js +302 -0
- package/src/windsurf/config-parser.js +267 -0
- package/src/windsurf/context.js +249 -0
- package/src/windsurf/deep-review.js +337 -0
- package/src/windsurf/domain-packs.js +348 -0
- package/src/windsurf/freshness.js +215 -0
- package/src/windsurf/governance.js +231 -0
- package/src/windsurf/interactive.js +388 -0
- package/src/windsurf/mcp-packs.js +535 -0
- package/src/windsurf/patch.js +231 -0
- package/src/windsurf/plans.js +247 -0
- package/src/windsurf/premium.js +467 -0
- package/src/windsurf/setup.js +471 -0
- package/src/windsurf/techniques.js +1758 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { audit } = require('../src/audit');
|
|
4
|
+
const { setup } = require('../src/setup');
|
|
5
|
+
const { analyzeProject, printAnalysis, exportMarkdown } = require('../src/analyze');
|
|
6
|
+
const { buildProposalBundle, printProposalBundle, writePlanFile, applyProposalBundle, printApplyResult } = require('../src/plans');
|
|
7
|
+
const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, renderGovernanceMarkdown } = require('../src/governance');
|
|
8
|
+
const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
|
|
9
|
+
const { writeSnapshotArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
|
|
10
|
+
const { version } = require('../package.json');
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const COMMAND_ALIASES = {
|
|
14
|
+
review: 'deep-review',
|
|
15
|
+
wizard: 'interactive',
|
|
16
|
+
learn: 'insights',
|
|
17
|
+
discover: 'audit',
|
|
18
|
+
starter: 'setup',
|
|
19
|
+
suggest: 'suggest-only',
|
|
20
|
+
gov: 'governance',
|
|
21
|
+
outcome: 'feedback',
|
|
22
|
+
};
|
|
23
|
+
const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'help', 'version'];
|
|
24
|
+
|
|
25
|
+
function levenshtein(a, b) {
|
|
26
|
+
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
27
|
+
for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
|
|
28
|
+
for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
|
|
29
|
+
for (let i = 1; i <= a.length; i++) {
|
|
30
|
+
for (let j = 1; j <= b.length; j++) {
|
|
31
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
32
|
+
matrix[i][j] = Math.min(
|
|
33
|
+
matrix[i - 1][j] + 1,
|
|
34
|
+
matrix[i][j - 1] + 1,
|
|
35
|
+
matrix[i - 1][j - 1] + cost
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return matrix[a.length][b.length];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function suggestCommand(input) {
|
|
43
|
+
const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
|
|
44
|
+
let best = null;
|
|
45
|
+
let bestDistance = Infinity;
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
const distance = levenshtein(input, candidate);
|
|
48
|
+
if (distance < bestDistance) {
|
|
49
|
+
best = candidate;
|
|
50
|
+
bestDistance = distance;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return bestDistance <= 3 ? best : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseArgs(rawArgs) {
|
|
57
|
+
const flags = [];
|
|
58
|
+
let command = 'audit';
|
|
59
|
+
let threshold = null;
|
|
60
|
+
let out = null;
|
|
61
|
+
let planFile = null;
|
|
62
|
+
let only = [];
|
|
63
|
+
let profile = 'safe-write';
|
|
64
|
+
let mcpPacks = [];
|
|
65
|
+
let requireChecks = [];
|
|
66
|
+
let feedbackKey = null;
|
|
67
|
+
let feedbackStatus = null;
|
|
68
|
+
let feedbackEffect = null;
|
|
69
|
+
let feedbackNotes = null;
|
|
70
|
+
let feedbackSource = null;
|
|
71
|
+
let feedbackScoreDelta = null;
|
|
72
|
+
let platform = 'claude';
|
|
73
|
+
let format = null;
|
|
74
|
+
let commandSet = false;
|
|
75
|
+
let extraArgs = [];
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
78
|
+
const arg = rawArgs[i];
|
|
79
|
+
|
|
80
|
+
if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format') {
|
|
81
|
+
const value = rawArgs[i + 1];
|
|
82
|
+
if (!value || value.startsWith('--')) {
|
|
83
|
+
throw new Error(`${arg} requires a value`);
|
|
84
|
+
}
|
|
85
|
+
if (arg === '--threshold') threshold = value;
|
|
86
|
+
if (arg === '--out') out = value;
|
|
87
|
+
if (arg === '--plan') planFile = value;
|
|
88
|
+
if (arg === '--only') only = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
89
|
+
if (arg === '--profile') profile = value.trim();
|
|
90
|
+
if (arg === '--mcp-pack') mcpPacks = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
91
|
+
if (arg === '--require') requireChecks = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
92
|
+
if (arg === '--key') feedbackKey = value.trim();
|
|
93
|
+
if (arg === '--status') feedbackStatus = value.trim();
|
|
94
|
+
if (arg === '--effect') feedbackEffect = value.trim();
|
|
95
|
+
if (arg === '--notes') feedbackNotes = value;
|
|
96
|
+
if (arg === '--source') feedbackSource = value.trim();
|
|
97
|
+
if (arg === '--score-delta') feedbackScoreDelta = value.trim();
|
|
98
|
+
if (arg === '--platform') platform = value.trim().toLowerCase();
|
|
99
|
+
if (arg === '--format') format = value.trim().toLowerCase();
|
|
100
|
+
i++;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (arg.startsWith('--require=')) {
|
|
105
|
+
requireChecks = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (arg.startsWith('--threshold=')) {
|
|
110
|
+
threshold = arg.split('=')[1];
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (arg.startsWith('--out=')) {
|
|
115
|
+
out = arg.split('=').slice(1).join('=');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (arg.startsWith('--plan=')) {
|
|
120
|
+
planFile = arg.split('=').slice(1).join('=');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (arg.startsWith('--only=')) {
|
|
125
|
+
only = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (arg.startsWith('--profile=')) {
|
|
130
|
+
profile = arg.split('=').slice(1).join('=').trim();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (arg.startsWith('--mcp-pack=')) {
|
|
135
|
+
mcpPacks = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (arg.startsWith('--key=')) {
|
|
140
|
+
feedbackKey = arg.split('=').slice(1).join('=').trim();
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (arg.startsWith('--status=')) {
|
|
145
|
+
feedbackStatus = arg.split('=').slice(1).join('=').trim();
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (arg.startsWith('--effect=')) {
|
|
150
|
+
feedbackEffect = arg.split('=').slice(1).join('=').trim();
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (arg.startsWith('--notes=')) {
|
|
155
|
+
feedbackNotes = arg.split('=').slice(1).join('=');
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (arg.startsWith('--source=')) {
|
|
160
|
+
feedbackSource = arg.split('=').slice(1).join('=').trim();
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (arg.startsWith('--score-delta=')) {
|
|
165
|
+
feedbackScoreDelta = arg.split('=').slice(1).join('=').trim();
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (arg.startsWith('--platform=')) {
|
|
170
|
+
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (arg.startsWith('--format=')) {
|
|
175
|
+
format = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (arg.startsWith('--')) {
|
|
180
|
+
flags.push(arg);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!commandSet) {
|
|
185
|
+
command = arg;
|
|
186
|
+
commandSet = true;
|
|
187
|
+
} else {
|
|
188
|
+
extraArgs.push(arg);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
193
|
+
|
|
194
|
+
return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, extraArgs };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const HELP = `
|
|
198
|
+
nerviq v${version}
|
|
199
|
+
Score your repo's Claude Code setup. Fix gaps safely. Benchmark the impact.
|
|
200
|
+
|
|
201
|
+
Start here (read-only, nothing changes):
|
|
202
|
+
npx nerviq Audit your project (10 seconds)
|
|
203
|
+
npx nerviq --lite Quick scan: top 3 gaps + next command
|
|
204
|
+
npx nerviq --platform codex Audit your Codex repo setup
|
|
205
|
+
npx nerviq --platform codex augment Codex-aware advisory pass, no writes
|
|
206
|
+
npx nerviq --platform codex suggest-only Structured Codex report, no writes
|
|
207
|
+
npx nerviq augment Repo-aware analysis, no writes
|
|
208
|
+
npx nerviq suggest-only Structured report, no writes
|
|
209
|
+
|
|
210
|
+
Plan and apply (when you're ready to change things):
|
|
211
|
+
npx nerviq plan Export proposal bundles with previews
|
|
212
|
+
npx nerviq apply Apply proposals selectively with rollback
|
|
213
|
+
npx nerviq setup Generate starter-safe baseline
|
|
214
|
+
npx nerviq setup --auto Apply all generated files without prompts
|
|
215
|
+
|
|
216
|
+
Track progress over time:
|
|
217
|
+
npx nerviq history Show score history from saved snapshots
|
|
218
|
+
npx nerviq compare Compare latest vs previous snapshot
|
|
219
|
+
npx nerviq trend --out r.md Export trend report as markdown
|
|
220
|
+
|
|
221
|
+
Multi-repo:
|
|
222
|
+
npx nerviq scan dir1 dir2 Compare multiple repos side-by-side
|
|
223
|
+
|
|
224
|
+
Advanced:
|
|
225
|
+
npx nerviq governance Permission profiles, hooks, policy packs
|
|
226
|
+
npx nerviq benchmark Before/after in isolated temp copy
|
|
227
|
+
npx nerviq deep-review AI-powered config review (opt-in, uses API)
|
|
228
|
+
npx nerviq interactive Step-by-step guided wizard
|
|
229
|
+
npx nerviq watch Live monitoring on config changes with cross-platform watch fallback
|
|
230
|
+
npx nerviq badge Generate shields.io badge markdown
|
|
231
|
+
npx nerviq feedback Record recommendation outcomes or show local outcome summary
|
|
232
|
+
|
|
233
|
+
Options:
|
|
234
|
+
--threshold N Exit with code 1 if score is below N (useful for CI)
|
|
235
|
+
--require A,B Exit with code 1 if named checks fail (e.g. --require secretsProtection,permissionDeny)
|
|
236
|
+
--out FILE Write JSON or markdown output to a file
|
|
237
|
+
--plan FILE Load a previously exported plan file
|
|
238
|
+
--only A,B Limit plan/apply to selected proposal ids or technique keys
|
|
239
|
+
--profile NAME Choose permission profile (read-only, suggest-only, safe-write, power-user, internal-research)
|
|
240
|
+
--mcp-pack A,B Merge named MCP packs into generated settings (e.g. context7-docs,next-devtools)
|
|
241
|
+
--key NAME Recommendation key for feedback logging (e.g. permissionDeny)
|
|
242
|
+
--status VALUE Feedback status: accepted, rejected, deferred
|
|
243
|
+
--effect VALUE Feedback effect: positive, neutral, negative
|
|
244
|
+
--notes TEXT Short notes to store with a feedback event
|
|
245
|
+
--source NAME Source label for feedback event (default: manual-cli)
|
|
246
|
+
--score-delta N Optional observed score delta tied to the outcome
|
|
247
|
+
--platform NAME Choose platform surface (claude default, codex advisory/build preview)
|
|
248
|
+
--format NAME Output format for audit results (json, sarif)
|
|
249
|
+
--snapshot Save a normalized snapshot artifact under .claude/nerviq/snapshots/
|
|
250
|
+
--lite Show a short top-3 quick scan with one clear next command
|
|
251
|
+
--dry-run Preview apply without writing files
|
|
252
|
+
--verbose Show all recommendations (not just critical/high)
|
|
253
|
+
--json Output as JSON (for CI pipelines)
|
|
254
|
+
--auto Apply all generated setup files without prompting
|
|
255
|
+
--insights Enable anonymous usage insights (off by default)
|
|
256
|
+
--help Show this help
|
|
257
|
+
--version Show version
|
|
258
|
+
|
|
259
|
+
Examples:
|
|
260
|
+
npx nerviq
|
|
261
|
+
npx nerviq --lite
|
|
262
|
+
npx nerviq --platform codex
|
|
263
|
+
npx nerviq --platform codex augment
|
|
264
|
+
npx nerviq --platform codex suggest-only --json
|
|
265
|
+
npx nerviq --platform codex setup
|
|
266
|
+
npx nerviq --platform codex plan --out codex-plan.json
|
|
267
|
+
npx nerviq --platform codex --format sarif
|
|
268
|
+
npx nerviq --snapshot
|
|
269
|
+
npx nerviq augment
|
|
270
|
+
npx nerviq augment --snapshot
|
|
271
|
+
npx nerviq suggest-only --json
|
|
272
|
+
npx nerviq governance --snapshot
|
|
273
|
+
npx nerviq plan --out claudex-plan.json
|
|
274
|
+
npx nerviq plan --profile safe-write
|
|
275
|
+
npx nerviq setup --mcp-pack context7-docs
|
|
276
|
+
npx nerviq apply --plan claudex-plan.json --only hooks,commands
|
|
277
|
+
npx nerviq apply --mcp-pack context7-docs,next-devtools --only hooks
|
|
278
|
+
npx nerviq apply --profile power-user --only claude-md,hooks
|
|
279
|
+
npx nerviq governance --json
|
|
280
|
+
npx nerviq benchmark --out benchmark.md
|
|
281
|
+
npx nerviq feedback
|
|
282
|
+
npx nerviq feedback --key permissionDeny --status accepted --effect positive --score-delta 12
|
|
283
|
+
npx nerviq --json --threshold 60
|
|
284
|
+
npx nerviq setup --auto
|
|
285
|
+
npx nerviq interactive
|
|
286
|
+
|
|
287
|
+
Exit codes:
|
|
288
|
+
0 Success
|
|
289
|
+
1 Error, unknown command, or score below --threshold
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
async function main() {
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = parseArgs(args);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const { flags, command, normalizedCommand } = parsed;
|
|
302
|
+
|
|
303
|
+
if (flags.includes('--help') || command === 'help') {
|
|
304
|
+
console.log(HELP);
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (flags.includes('--version') || command === 'version') {
|
|
309
|
+
console.log(version);
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const options = {
|
|
314
|
+
verbose: flags.includes('--verbose'),
|
|
315
|
+
json: flags.includes('--json'),
|
|
316
|
+
auto: flags.includes('--auto'),
|
|
317
|
+
lite: flags.includes('--lite'),
|
|
318
|
+
snapshot: flags.includes('--snapshot'),
|
|
319
|
+
dryRun: flags.includes('--dry-run'),
|
|
320
|
+
threshold: parsed.threshold !== null ? Number(parsed.threshold) : null,
|
|
321
|
+
out: parsed.out,
|
|
322
|
+
planFile: parsed.planFile,
|
|
323
|
+
only: parsed.only,
|
|
324
|
+
profile: parsed.profile,
|
|
325
|
+
mcpPacks: parsed.mcpPacks,
|
|
326
|
+
require: parsed.requireChecks,
|
|
327
|
+
platform: parsed.platform || 'claude',
|
|
328
|
+
format: parsed.format || null,
|
|
329
|
+
dir: process.cwd()
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
if (!['claude', 'codex'].includes(options.platform)) {
|
|
333
|
+
console.error(`\n Error: Unsupported platform '${options.platform}'. Use 'claude' or 'codex'.\n`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (options.format !== null && !['json', 'sarif'].includes(options.format)) {
|
|
338
|
+
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json' or 'sarif'.\n`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (options.threshold !== null && (!Number.isFinite(options.threshold) || options.threshold < 0 || options.threshold > 100)) {
|
|
343
|
+
console.error('\n Error: --threshold must be a number between 0 and 100.\n');
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (options.require && options.require.length > 0 && normalizedCommand !== 'audit' && !['audit', 'discover'].includes(command)) {
|
|
348
|
+
console.error(`\n Warning: --require is only supported with the audit command. Ignoring for '${normalizedCommand}'.\n`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!KNOWN_COMMANDS.includes(normalizedCommand)) {
|
|
352
|
+
const suggestion = suggestCommand(command);
|
|
353
|
+
console.error(`\n Error: Unknown command '${command}'.`);
|
|
354
|
+
if (suggestion) {
|
|
355
|
+
console.error(` Did you mean '${suggestion}'?`);
|
|
356
|
+
}
|
|
357
|
+
console.error(' Run nerviq --help for usage.\n');
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!require('fs').existsSync(options.dir)) {
|
|
362
|
+
console.error(`\n Error: Directory not found: ${options.dir}`);
|
|
363
|
+
console.error(' Run nerviq from inside your project directory.\n');
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (['setup', 'apply', 'benchmark'].includes(normalizedCommand)) {
|
|
368
|
+
try {
|
|
369
|
+
ensureWritableProfile(options.profile, normalizedCommand, options.dryRun);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const FULL_COMMAND_SET = new Set([
|
|
378
|
+
'audit', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
379
|
+
'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
|
|
380
|
+
'history', 'compare', 'trend', 'feedback', 'help', 'version',
|
|
381
|
+
// Harmony + Synergy (cross-platform)
|
|
382
|
+
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
383
|
+
'harmony-watch', 'harmony-governance', 'synergy-report',
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
if (options.platform === 'codex') {
|
|
387
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
388
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform codex.`);
|
|
389
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (options.platform === 'gemini') {
|
|
395
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
396
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform gemini.`);
|
|
397
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (options.platform === 'copilot') {
|
|
403
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
404
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform copilot.`);
|
|
405
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (options.platform === 'cursor') {
|
|
411
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
412
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform cursor.`);
|
|
413
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (const plat of ['windsurf', 'aider', 'opencode']) {
|
|
419
|
+
if (options.platform === plat) {
|
|
420
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
421
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform ${plat}.`);
|
|
422
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (normalizedCommand === 'scan') {
|
|
429
|
+
const scanDirs = parsed.extraArgs;
|
|
430
|
+
if (scanDirs.length === 0) {
|
|
431
|
+
console.error('\n Error: scan requires at least one directory argument.');
|
|
432
|
+
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
const fs = require('fs');
|
|
436
|
+
const pathMod = require('path');
|
|
437
|
+
const rows = [];
|
|
438
|
+
for (const rawDir of scanDirs) {
|
|
439
|
+
const dir = pathMod.resolve(rawDir);
|
|
440
|
+
if (!fs.existsSync(dir)) {
|
|
441
|
+
rows.push({ name: pathMod.basename(rawDir), dir: rawDir, score: null, passed: '-', failed: '-', suggested: '-', error: 'directory not found' });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const result = await audit({ dir, silent: true, platform: options.platform });
|
|
446
|
+
rows.push({
|
|
447
|
+
name: pathMod.basename(dir),
|
|
448
|
+
dir: rawDir,
|
|
449
|
+
score: result.score,
|
|
450
|
+
passed: result.passed,
|
|
451
|
+
failed: result.failed,
|
|
452
|
+
suggested: result.suggestedNextCommand || '-',
|
|
453
|
+
error: null,
|
|
454
|
+
});
|
|
455
|
+
} catch (err) {
|
|
456
|
+
rows.push({ name: pathMod.basename(dir), dir: rawDir, score: null, passed: '-', failed: '-', suggested: '-', error: err.message });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (options.json) {
|
|
461
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
462
|
+
} else {
|
|
463
|
+
// Find weakest
|
|
464
|
+
const validRows = rows.filter(r => r.score !== null);
|
|
465
|
+
const minScore = validRows.length > 0 ? Math.min(...validRows.map(r => r.score)) : null;
|
|
466
|
+
const weakest = validRows.length > 1 && validRows.filter(r => r.score > minScore).length > 0
|
|
467
|
+
? validRows.find(r => r.score === minScore)
|
|
468
|
+
: null;
|
|
469
|
+
|
|
470
|
+
console.log('');
|
|
471
|
+
console.log('\x1b[1m nerviq multi-repo scan\x1b[0m');
|
|
472
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
473
|
+
console.log('');
|
|
474
|
+
|
|
475
|
+
// Table header
|
|
476
|
+
const nameW = Math.max(8, ...rows.map(r => r.name.length)) + 2;
|
|
477
|
+
const header = ` ${'Project'.padEnd(nameW)} ${'Score'.padStart(5)} ${'Pass'.padStart(4)} ${'Fail'.padStart(4)} Suggested Command`;
|
|
478
|
+
console.log('\x1b[1m' + header + '\x1b[0m');
|
|
479
|
+
console.log(' ' + '─'.repeat(header.trim().length));
|
|
480
|
+
|
|
481
|
+
for (const row of rows) {
|
|
482
|
+
if (row.error) {
|
|
483
|
+
console.log(` ${row.name.padEnd(nameW)} \x1b[31m${('ERR').padStart(5)}\x1b[0m ${String(row.passed).padStart(4)} ${String(row.failed).padStart(4)} ${row.error}`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const isWeak = weakest && row.name === weakest.name && row.dir === weakest.dir;
|
|
487
|
+
const scoreColor = row.score >= 70 ? '\x1b[32m' : row.score >= 40 ? '\x1b[33m' : '\x1b[31m';
|
|
488
|
+
const prefix = isWeak ? '\x1b[31m⚠ ' : ' ';
|
|
489
|
+
const suffix = isWeak ? ' ← weakest\x1b[0m' : '';
|
|
490
|
+
console.log(`${prefix}${row.name.padEnd(nameW)} ${scoreColor}${String(row.score).padStart(5)}\x1b[0m ${String(row.passed).padStart(4)} ${String(row.failed).padStart(4)} ${row.suggested}${suffix}`);
|
|
491
|
+
}
|
|
492
|
+
console.log('');
|
|
493
|
+
}
|
|
494
|
+
process.exit(0);
|
|
495
|
+
} else if (normalizedCommand === 'history') {
|
|
496
|
+
const { formatHistory } = require('../src/activity');
|
|
497
|
+
console.log('');
|
|
498
|
+
console.log(formatHistory(options.dir));
|
|
499
|
+
console.log('');
|
|
500
|
+
process.exit(0);
|
|
501
|
+
} else if (normalizedCommand === 'compare') {
|
|
502
|
+
const { compareLatest } = require('../src/activity');
|
|
503
|
+
const result = compareLatest(options.dir);
|
|
504
|
+
if (!result) {
|
|
505
|
+
console.log('\n Need at least 2 snapshots to compare. Run `npx nerviq --snapshot` twice.\n');
|
|
506
|
+
process.exit(0);
|
|
507
|
+
}
|
|
508
|
+
if (options.json) {
|
|
509
|
+
console.log(JSON.stringify(result, null, 2));
|
|
510
|
+
} else {
|
|
511
|
+
const sign = result.delta.score >= 0 ? '+' : '';
|
|
512
|
+
console.log('');
|
|
513
|
+
console.log(` Previous: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})`);
|
|
514
|
+
console.log(` Current: ${result.current.score}/100 (${result.current.date?.split('T')[0]})`);
|
|
515
|
+
console.log(` Delta: ${sign}${result.delta.score} points`);
|
|
516
|
+
console.log(` Trend: ${result.trend}`);
|
|
517
|
+
if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
|
|
518
|
+
if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
|
|
519
|
+
console.log('');
|
|
520
|
+
}
|
|
521
|
+
process.exit(0);
|
|
522
|
+
} else if (normalizedCommand === 'trend') {
|
|
523
|
+
const { exportTrendReport } = require('../src/activity');
|
|
524
|
+
const report = exportTrendReport(options.dir);
|
|
525
|
+
if (!report) {
|
|
526
|
+
console.log('\n No snapshots found. Run `npx nerviq --snapshot` to start tracking.\n');
|
|
527
|
+
process.exit(0);
|
|
528
|
+
}
|
|
529
|
+
if (options.out) {
|
|
530
|
+
require('fs').writeFileSync(options.out, report, 'utf8');
|
|
531
|
+
console.log(`\n Trend report exported to ${options.out}\n`);
|
|
532
|
+
} else {
|
|
533
|
+
console.log(report);
|
|
534
|
+
}
|
|
535
|
+
process.exit(0);
|
|
536
|
+
} else if (normalizedCommand === 'badge') {
|
|
537
|
+
const { getBadgeMarkdown } = require('../src/badge');
|
|
538
|
+
const result = await audit({ ...options, silent: true });
|
|
539
|
+
console.log(getBadgeMarkdown(result.score));
|
|
540
|
+
console.log('');
|
|
541
|
+
console.log('Add this to your README.md');
|
|
542
|
+
process.exit(0);
|
|
543
|
+
} else if (normalizedCommand === 'insights') {
|
|
544
|
+
const https = require('https');
|
|
545
|
+
const url = 'https://claudex-insights.claudex.workers.dev/v1/stats';
|
|
546
|
+
const req = https.get(url, (res) => {
|
|
547
|
+
let data = '';
|
|
548
|
+
res.on('data', chunk => data += chunk);
|
|
549
|
+
res.on('end', () => {
|
|
550
|
+
try {
|
|
551
|
+
const stats = JSON.parse(data);
|
|
552
|
+
console.log('');
|
|
553
|
+
console.log('\x1b[1m CLAUDEX Community Insights\x1b[0m');
|
|
554
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
555
|
+
console.log(` Total audits run: \x1b[1m${stats.totalRuns}\x1b[0m`);
|
|
556
|
+
console.log(` Average score: \x1b[1m${stats.averageScore}/100\x1b[0m`);
|
|
557
|
+
console.log('');
|
|
558
|
+
if (stats.topFailedChecks && stats.topFailedChecks.length > 0) {
|
|
559
|
+
console.log('\x1b[33m Most common gaps:\x1b[0m');
|
|
560
|
+
for (const f of stats.topFailedChecks.slice(0, 5)) {
|
|
561
|
+
console.log(` ${f.pct}% miss: \x1b[1m${f.check}\x1b[0m`);
|
|
562
|
+
}
|
|
563
|
+
console.log('');
|
|
564
|
+
}
|
|
565
|
+
if (stats.topStacks && stats.topStacks.length > 0) {
|
|
566
|
+
console.log('\x1b[36m Popular stacks:\x1b[0m');
|
|
567
|
+
console.log(` ${stats.topStacks.map(s => s.stack).join(', ')}`);
|
|
568
|
+
}
|
|
569
|
+
console.log('');
|
|
570
|
+
} catch (e) {
|
|
571
|
+
console.log(' No community data available yet. Be the first to run: npx nerviq');
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}).on('error', () => {
|
|
575
|
+
console.log(' Could not reach insights server. Run locally: npx nerviq');
|
|
576
|
+
});
|
|
577
|
+
req.setTimeout(10000, () => {
|
|
578
|
+
req.destroy();
|
|
579
|
+
console.log(' Insights request timed out. Run locally: npx nerviq');
|
|
580
|
+
});
|
|
581
|
+
return; // keep process alive for http
|
|
582
|
+
} else if (normalizedCommand === 'feedback') {
|
|
583
|
+
if (parsed.feedbackKey) {
|
|
584
|
+
if (!parsed.feedbackStatus) {
|
|
585
|
+
console.error('\n Error: feedback logging requires --status when --key is provided.\n');
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
const artifact = recordRecommendationOutcome(options.dir, {
|
|
589
|
+
key: parsed.feedbackKey,
|
|
590
|
+
status: parsed.feedbackStatus,
|
|
591
|
+
effect: parsed.feedbackEffect || 'neutral',
|
|
592
|
+
notes: parsed.feedbackNotes || '',
|
|
593
|
+
source: parsed.feedbackSource || 'manual-cli',
|
|
594
|
+
scoreDelta: parsed.feedbackScoreDelta !== null ? Number(parsed.feedbackScoreDelta) : null,
|
|
595
|
+
});
|
|
596
|
+
const summary = getRecommendationOutcomeSummary(options.dir);
|
|
597
|
+
if (options.json) {
|
|
598
|
+
console.log(JSON.stringify({ artifact, summary }, null, 2));
|
|
599
|
+
} else {
|
|
600
|
+
console.log('');
|
|
601
|
+
console.log(` Feedback recorded for ${parsed.feedbackKey}`);
|
|
602
|
+
console.log(` Artifact: ${artifact.relativePath}`);
|
|
603
|
+
console.log('');
|
|
604
|
+
console.log(formatRecommendationOutcomeSummary(options.dir));
|
|
605
|
+
console.log('');
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
if (options.json) {
|
|
609
|
+
console.log(JSON.stringify(getRecommendationOutcomeSummary(options.dir), null, 2));
|
|
610
|
+
} else {
|
|
611
|
+
console.log('');
|
|
612
|
+
console.log(formatRecommendationOutcomeSummary(options.dir));
|
|
613
|
+
console.log('');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
process.exit(0);
|
|
617
|
+
} else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
|
|
618
|
+
const report = await analyzeProject({ ...options, mode: normalizedCommand });
|
|
619
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
620
|
+
sourceCommand: normalizedCommand,
|
|
621
|
+
}) : null;
|
|
622
|
+
if (options.out && !options.json) {
|
|
623
|
+
const fs = require('fs');
|
|
624
|
+
const md = exportMarkdown(report);
|
|
625
|
+
fs.writeFileSync(options.out, md, 'utf8');
|
|
626
|
+
console.log(`\n Report exported to ${options.out}\n`);
|
|
627
|
+
}
|
|
628
|
+
printAnalysis(report, options);
|
|
629
|
+
if (snapshot && !options.json) {
|
|
630
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
631
|
+
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
632
|
+
console.log('');
|
|
633
|
+
}
|
|
634
|
+
} else if (normalizedCommand === 'plan') {
|
|
635
|
+
const bundle = await buildProposalBundle(options);
|
|
636
|
+
let artifact = null;
|
|
637
|
+
if (options.out) {
|
|
638
|
+
artifact = writePlanFile(bundle, options.out);
|
|
639
|
+
}
|
|
640
|
+
printProposalBundle(bundle, options);
|
|
641
|
+
if (options.out && !options.json) {
|
|
642
|
+
console.log(` Plan written to ${options.out}`);
|
|
643
|
+
if (artifact) {
|
|
644
|
+
console.log(` Activity log: ${artifact.relativePath}`);
|
|
645
|
+
}
|
|
646
|
+
console.log('');
|
|
647
|
+
}
|
|
648
|
+
} else if (normalizedCommand === 'apply') {
|
|
649
|
+
const result = await applyProposalBundle(options);
|
|
650
|
+
printApplyResult(result, options);
|
|
651
|
+
} else if (normalizedCommand === 'governance') {
|
|
652
|
+
const fs = require('fs');
|
|
653
|
+
const path = require('path');
|
|
654
|
+
const summary = getGovernanceSummary(options.platform);
|
|
655
|
+
if (options.out) {
|
|
656
|
+
fs.mkdirSync(path.dirname(options.out), { recursive: true });
|
|
657
|
+
const content = path.extname(options.out).toLowerCase() === '.md'
|
|
658
|
+
? renderGovernanceMarkdown(summary)
|
|
659
|
+
: JSON.stringify(summary, null, 2);
|
|
660
|
+
fs.writeFileSync(options.out, content, 'utf8');
|
|
661
|
+
}
|
|
662
|
+
printGovernanceSummary(summary, options);
|
|
663
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
664
|
+
sourceCommand: normalizedCommand,
|
|
665
|
+
}) : null;
|
|
666
|
+
if (options.out && !options.json) {
|
|
667
|
+
console.log(` Governance report written to ${options.out}`);
|
|
668
|
+
console.log('');
|
|
669
|
+
}
|
|
670
|
+
if (snapshot && !options.json) {
|
|
671
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
672
|
+
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
673
|
+
console.log('');
|
|
674
|
+
}
|
|
675
|
+
} else if (normalizedCommand === 'benchmark') {
|
|
676
|
+
const report = await runBenchmark(options);
|
|
677
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
678
|
+
sourceCommand: normalizedCommand,
|
|
679
|
+
}) : null;
|
|
680
|
+
if (options.out) {
|
|
681
|
+
writeBenchmarkReport(report, options.out);
|
|
682
|
+
}
|
|
683
|
+
printBenchmark(report, options);
|
|
684
|
+
if (options.out && !options.json) {
|
|
685
|
+
console.log(` Benchmark report written to ${options.out}`);
|
|
686
|
+
console.log('');
|
|
687
|
+
}
|
|
688
|
+
if (snapshot && !options.json) {
|
|
689
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
690
|
+
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
691
|
+
console.log('');
|
|
692
|
+
}
|
|
693
|
+
} else if (normalizedCommand === 'deep-review') {
|
|
694
|
+
const { deepReview } = require('../src/deep-review');
|
|
695
|
+
await deepReview(options);
|
|
696
|
+
} else if (normalizedCommand === 'interactive') {
|
|
697
|
+
const { interactive } = require('../src/interactive');
|
|
698
|
+
await interactive(options);
|
|
699
|
+
} else if (normalizedCommand === 'watch') {
|
|
700
|
+
const { watch } = require('../src/watch');
|
|
701
|
+
await watch(options);
|
|
702
|
+
} else if (normalizedCommand === 'setup') {
|
|
703
|
+
await setup(options);
|
|
704
|
+
if (options.snapshot) {
|
|
705
|
+
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
706
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
707
|
+
sourceCommand: 'setup',
|
|
708
|
+
});
|
|
709
|
+
if (!options.json) {
|
|
710
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
const result = await audit(options);
|
|
715
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
716
|
+
sourceCommand: normalizedCommand,
|
|
717
|
+
}) : null;
|
|
718
|
+
if (snapshot && !options.json) {
|
|
719
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
720
|
+
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
721
|
+
console.log('');
|
|
722
|
+
}
|
|
723
|
+
if (options.threshold !== null && result.score < options.threshold) {
|
|
724
|
+
if (!options.json) {
|
|
725
|
+
console.error(` Threshold failed: score ${result.score}/100 is below required ${options.threshold}/100.\n`);
|
|
726
|
+
}
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
if (options.require && options.require.length > 0) {
|
|
730
|
+
const failedRequired = options.require.filter(key => {
|
|
731
|
+
const check = result.results.find(r => r.key === key);
|
|
732
|
+
return !check || check.passed !== true;
|
|
733
|
+
});
|
|
734
|
+
if (failedRequired.length > 0) {
|
|
735
|
+
if (!options.json) {
|
|
736
|
+
console.error(`\n Required checks failed: ${failedRequired.join(', ')}`);
|
|
737
|
+
console.error(' These must pass for CI to succeed.\n');
|
|
738
|
+
}
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
} catch (err) {
|
|
744
|
+
console.error(`\n Error: ${err.message}`);
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
main();
|