@xenos1996/usa 2.15.0 → 2.16.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/dist/cli.js CHANGED
@@ -1,100 +1,15 @@
1
- #!/usr/bin/env node
2
1
  import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
5
- import { runAudit } from './engine/audit.js';
6
- import { loadRulePacks, loadPackFile } from './engine/loader.js';
7
- import { loadDetectorFile } from './detect/index.js';
8
- import { Project } from './util/project.js';
9
- import { writeTextFile } from './util/files.js';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { VERSION, bool, parseArgs } from './cli-args.js';
4
+ import { HELP } from './cli-help.js';
10
5
  import { loadEnvFile } from './util/env.js';
11
- import { detect } from './detect/index.js';
12
- import { renderMarkdown, parseTrailer } from './report/markdown.js';
13
- import { parseDetachedSignature, verifyDetachedSignature, } from './report/signature.js';
14
- import { renderJson } from './report/json.js';
15
- import { renderSarif } from './report/sarif.js';
16
- import { renderNarrative } from './report/narrative.js';
17
- import { diffReports } from './engine/diff.js';
18
- import { bootstrapPacks, writeBootstrapPacks } from './bootstrap/index.js';
19
- import { EXAMPLE_CONFIG, loadConfig } from './config.js';
20
- import { evaluateGate, evaluateNewCodeGate, parseBaselineTrailer } from './engine/gate.js';
21
- import { learnFromReport, renderSuggestions } from './learn/index.js';
22
- import { Store } from './store/index.js';
23
- import { runEvolutionCycle } from './evolution/run.js';
24
- import { capabilityFromPack } from './evolution/capability.js';
25
- import { ruleAutomatability } from './engine/automatability.js';
26
- import { catalogueCoverage, catalogueOf, loadCatalogues } from './engine/catalogues.js';
27
- import { categoryCoverage, loadCategories } from './engine/categories.js';
28
- import { createStdinAsk, runFoundationInit, runFoundationShow } from './foundation/interview.js';
29
- import { checkModelAdvertised, runLiveSession } from './live/runner.js';
30
- import { complete, listModels, loadProviderConfig } from './agent/index.js';
31
- const HERE = path.dirname(fileURLToPath(import.meta.url));
32
- const DEFAULT_RULES_DIR = path.resolve(HERE, '..', 'rules');
33
- const VERSION = readVersion();
34
- const SEVERITY_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'FUTURE'];
35
- const DEPTHS = ['quick', 'standard', 'deep'];
36
- const MATURITIES = ['prototype', 'mvp', 'beta', 'production', 'legacy'];
37
- function parseArgs(argv) {
38
- const args = { _: [] };
39
- for (let i = 0; i < argv.length; i++) {
40
- const tok = argv[i];
41
- if (!tok.startsWith('--')) {
42
- args._.push(tok);
43
- continue;
44
- }
45
- const body = tok.slice(2);
46
- if (body.includes('=')) {
47
- const [k, ...rest] = body.split('=');
48
- args[k] = rest.join('=');
49
- continue;
50
- }
51
- const next = argv[i + 1];
52
- if (next === undefined || next.startsWith('--')) {
53
- args[body] = true;
54
- }
55
- else {
56
- args[body] = next;
57
- i++;
58
- }
59
- }
60
- return args;
61
- }
62
- function str(args, key, fallback) {
63
- const v = args[key];
64
- return typeof v === 'string' ? v : fallback;
65
- }
66
- function bool(args, key) {
67
- return args[key] === true || args[key] === 'true';
68
- }
69
- /**
70
- * CLI-003: every command that writes files honors --dry-run. Call after pure
71
- * argument validation with the resolved paths that WOULD be written: it
72
- * prints one line per path and returns false, and the caller exits 0 before
73
- * doing any work — no audit run, no model calls, no disk writes. With an
74
- * empty list it says so explicitly rather than silently succeeding.
75
- */
76
- function dryRunWrites(args, files) {
77
- if (!bool(args, 'dry-run'))
78
- return true;
79
- if (files.length === 0) {
80
- console.log('dry-run: nothing would be written');
81
- return false;
82
- }
83
- for (const f of files)
84
- console.log(`dry-run: would write ${path.resolve(f)}`);
85
- return false;
86
- }
87
- function list(args, key) {
88
- const v = args[key];
89
- if (Array.isArray(v))
90
- return v;
91
- if (typeof v === 'string')
92
- return v
93
- .split(',')
94
- .map((s) => s.trim())
95
- .filter(Boolean);
96
- return [];
97
- }
6
+ import { cmdAudit } from './cli-audit.js';
7
+ import { cmdDetect, cmdRules, cmdExplain, cmdStandards, cmdCategories } from './cli-query.js';
8
+ import { cmdFoundation, cmdLive, cmdModels } from './cli-session.js';
9
+ import { cmdDiff, cmdVerifyReport } from './cli-reports.js';
10
+ import { cmdBootstrap, cmdInit, cmdLearn } from './cli-scaffold.js';
11
+ import { cmdEvolve } from './cli-evolve.js';
12
+ /** Dispatch only: every command lives in its own cli-*.ts module. */
98
13
  /* -------------------------------------------------------------------- main -- */
99
14
  const COMMANDS = {
100
15
  audit: cmdAudit,
@@ -158,1009 +73,8 @@ export function main(argv) {
158
73
  return 2;
159
74
  }
160
75
  }
161
- const FORMATS = ['md', 'json', 'sarif', 'narrative'];
76
+ /* ------------------------------------------------------------------ audit -- */
162
77
  /** Resolves the report format from an explicit flag, else the --out suffix. */
163
- function resolveFormat(flag, out) {
164
- if (flag)
165
- return flag.toLowerCase();
166
- const ext = path.extname(out).toLowerCase();
167
- if (ext === '.json')
168
- return 'json';
169
- if (ext === '.sarif')
170
- return 'sarif';
171
- return 'md';
172
- }
173
- function parseCountFlag(v) {
174
- if (v === undefined)
175
- return undefined;
176
- const n = Number(v);
177
- return Number.isFinite(n) ? n : NaN;
178
- }
179
- function readAuditOptions(args) {
180
- const out = str(args, 'out', 'AUDIT.md') ?? 'AUDIT.md';
181
- return {
182
- target: args._[1] ?? '.',
183
- rulesDir: path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR),
184
- depth: str(args, 'depth', 'standard') ?? 'standard',
185
- profileArg: str(args, 'profile', 'auto') ?? 'auto',
186
- out,
187
- format: resolveFormat(str(args, 'format'), out),
188
- failOn: (str(args, 'fail-on', 'none') ?? 'none').toLowerCase(),
189
- baseline: str(args, 'baseline'),
190
- quiet: bool(args, 'quiet'),
191
- maxFiles: parseCountFlag(str(args, 'max-files')),
192
- maxBytes: parseCountFlag(str(args, 'max-bytes')),
193
- };
194
- }
195
- function validateAuditOptions(o) {
196
- if (!DEPTHS.includes(o.depth))
197
- return `--depth must be one of ${DEPTHS.join('|')}`;
198
- if (o.profileArg !== 'auto' && !MATURITIES.includes(o.profileArg)) {
199
- return `--profile must be auto or one of ${MATURITIES.join('|')}`;
200
- }
201
- if (!FORMATS.includes(o.format))
202
- return `--format must be one of ${FORMATS.join('|')}`;
203
- if (o.maxFiles !== undefined && !(o.maxFiles > 0))
204
- return '--max-files must be a positive number';
205
- if (o.maxBytes !== undefined && !(o.maxBytes > 0))
206
- return '--max-bytes must be a positive number';
207
- return null;
208
- }
209
- function cmdAudit(args) {
210
- const o = readAuditOptions(args);
211
- const invalid = validateAuditOptions(o);
212
- if (invalid) {
213
- console.error(invalid);
214
- return 2;
215
- }
216
- if (!fs.existsSync(o.target)) {
217
- console.error(`Target path does not exist: ${o.target}`);
218
- return 2;
219
- }
220
- // CLI-003: preview the write before the (expensive) audit runs.
221
- if (!dryRunWrites(args, [o.out]))
222
- return 0;
223
- let config;
224
- try {
225
- config = loadConfig(o.target, str(args, 'config'));
226
- }
227
- catch (err) {
228
- console.error(err.message);
229
- return 2;
230
- }
231
- if (config.facts)
232
- config.facts = [...config.facts, ...list(args, 'fact')];
233
- const { report, warnings, profile } = runAudit({
234
- target: o.target,
235
- rulesDir: o.rulesDir,
236
- depth: o.depth,
237
- profile: o.profileArg,
238
- config,
239
- allowCommands: bool(args, 'allow-commands'),
240
- usaVersion: VERSION,
241
- includePacks: list(args, 'include'),
242
- excludePacks: list(args, 'exclude'),
243
- maxFiles: o.maxFiles,
244
- maxBytes: o.maxBytes,
245
- });
246
- for (const w of warnings)
247
- console.error(`warning: ${w}`);
248
- const rendered = renderReport(report, profile, o.format);
249
- try {
250
- writeTextFile(o.out, rendered);
251
- }
252
- catch (err) {
253
- // CLI-004: an unwritable --out is a usage error (exit 2), not a crash.
254
- console.error(err.message);
255
- return 2;
256
- }
257
- if (!o.quiet) {
258
- console.log(summaryLine(report));
259
- console.log(` report → ${o.out}`);
260
- }
261
- // Without --baseline the absolute gate below is byte-identical to before:
262
- // same function, same arguments, same exit codes.
263
- if (!o.baseline)
264
- return evaluateGate(report, o.failOn, o.quiet);
265
- return runBaselineGate(report, o.baseline, o.failOn, o.quiet, args);
266
- }
267
- /**
268
- * New-code gate: fail only on findings this change introduced or regressed,
269
- * compared against a previous report's trailer. Every baseline failure mode
270
- * exits 2 loudly — a gate that cannot read its baseline must never pass.
271
- */
272
- function runBaselineGate(report, baselinePath, failOn, quiet, args) {
273
- let text;
274
- try {
275
- text = fs.readFileSync(baselinePath, 'utf8');
276
- }
277
- catch {
278
- console.error(`--baseline file not found or unreadable: ${baselinePath}`);
279
- return 2;
280
- }
281
- // Tolerate a raw YAML trailer file, mirroring `usa diff`.
282
- const trailerYaml = parseTrailer(text) ?? text;
283
- if (!trailerYaml.trim()) {
284
- console.error(`--baseline has no report trailer and no YAML content: ${baselinePath}`);
285
- return 2;
286
- }
287
- try {
288
- const baseline = parseBaselineTrailer(trailerYaml);
289
- // An explicit --fail-on (even none) wins; otherwise the new-code gate
290
- // defaults to HIGH so legacy MEDIUM/LOW debt never blocks adoption.
291
- const threshold = failOn === 'none' && args['baseline'] !== undefined && args['fail-on'] === undefined
292
- ? 'high'
293
- : failOn;
294
- return evaluateNewCodeGate(report, baseline, threshold, quiet);
295
- }
296
- catch (err) {
297
- console.error(`--baseline is malformed (${baselinePath}): ${err.message}`);
298
- return 2;
299
- }
300
- }
301
- function renderReport(report, profile, format) {
302
- switch (format) {
303
- case 'json':
304
- return renderJson(report);
305
- case 'sarif':
306
- return renderSarif(report);
307
- case 'narrative':
308
- return renderNarrative(report, profile);
309
- default:
310
- return renderMarkdown(report, profile);
311
- }
312
- }
313
- function summaryLine(report) {
314
- const s = report.score.severityCounts;
315
- const parts = SEVERITY_ORDER.filter((k) => s[k] > 0)
316
- .map((k) => `${k.toLowerCase()} ${s[k]}`)
317
- .join(' · ');
318
- return (`usa ${report.score.overall}/100 (${report.detection.maturity}) — ` +
319
- `${report.score.counts.PASS} passed, ` +
320
- (parts ? `open: ${parts}` : 'no open findings') +
321
- `, ${report.score.counts.UNKNOWN} to review` +
322
- ` (${report.score.automationCoverage}% verified automatically)`);
323
- }
324
- /* ----------------------------------------------------------------- detect -- */
325
- function cmdDetect(args) {
326
- const target = args._[1] ?? '.';
327
- const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
328
- const config = loadConfig(target, str(args, 'config'));
329
- const project = new Project(target, config.ignore ?? []);
330
- const git = project.gitInfo();
331
- const detection = detect(project, loadDetectorFile(rulesDir), git, [
332
- ...list(args, 'fact'),
333
- ...(config.facts ?? []),
334
- ]);
335
- const grouped = groupFlagsByNamespace(detection.facts.flags);
336
- console.log(`# Detection: ${path.resolve(target)}`);
337
- console.log();
338
- console.log(`maturity: ${detection.maturity}`);
339
- for (const [ns, values] of [...grouped.entries()].sort()) {
340
- console.log(`${ns}: ${[...new Set(values)].sort().join(', ')}`);
341
- }
342
- console.log();
343
- console.log('metrics:');
344
- for (const [k, v] of Object.entries(detection.facts.metrics))
345
- console.log(` ${k}: ${v}`);
346
- console.log();
347
- console.log('maturity signals:');
348
- for (const s of detection.maturitySignals)
349
- console.log(` ${s}`);
350
- return 0;
351
- }
352
- function groupFlagsByNamespace(flags) {
353
- const grouped = new Map();
354
- for (const f of flags) {
355
- const [ns, value] = f.split(':');
356
- const key = value === undefined ? 'other' : ns;
357
- const list2 = grouped.get(key) ?? [];
358
- list2.push(value === undefined ? f : value);
359
- grouped.set(key, list2);
360
- }
361
- return grouped;
362
- }
363
- /* ------------------------------------------------------------------ rules -- */
364
- function cmdRules(args) {
365
- if (args['format'] !== undefined) {
366
- console.error('warning: rules has no --format; ignoring');
367
- }
368
- const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
369
- const { packs } = loadRulePacks(rulesDir);
370
- const filter = str(args, 'section');
371
- let total = 0;
372
- for (const pack of packs) {
373
- const rules = filter ? pack.rules.filter((r) => r.section === filter) : pack.rules;
374
- if (filter && rules.length === 0)
375
- continue;
376
- console.log(`\n## ${pack.id} — ${pack.title} (${rules.length} rules)`);
377
- if (pack.description)
378
- console.log(` ${pack.description}`);
379
- for (const r of rules) {
380
- total++;
381
- console.log(` ${r.id.padEnd(10)} ${r.severity.padEnd(8)} ${r.ruleClass.padEnd(16)} ${r.title}`);
382
- }
383
- }
384
- console.log(`\n${total} rule(s) across ${packs.length} pack(s).`);
385
- return 0;
386
- }
387
- /* ---------------------------------------------------------------- explain -- */
388
- function cmdExplain(args) {
389
- const id = args._[1];
390
- if (!id) {
391
- console.error('Usage: usa explain <RULE-ID>');
392
- return 2;
393
- }
394
- const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
395
- const { packs } = loadRulePacks(rulesDir);
396
- const { catalogues } = loadCatalogues(rulesDir);
397
- for (const pack of packs) {
398
- const rule = pack.rules.find((r) => r.id.toLowerCase() === id.toLowerCase());
399
- if (!rule)
400
- continue;
401
- printRuleDetail(pack.id, rule, catalogues);
402
- return 0;
403
- }
404
- console.error(`Rule not found: ${id}`);
405
- return 1;
406
- }
407
- function cmdStandards(args) {
408
- const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
409
- const { packs } = loadRulePacks(rulesDir);
410
- const { catalogues } = loadCatalogues(rulesDir);
411
- const rows = catalogueCoverage(packs, catalogues);
412
- const fmt = (str(args, 'format') ?? 'md').toLowerCase();
413
- if (fmt === 'json') {
414
- console.log(JSON.stringify(rows, null, 2));
415
- return 0;
416
- }
417
- // Markdown table: catalogue → rules → how many the engine can fully decide.
418
- console.log('| Catalogue | Rules | Fully automated | Assisted | Manual |');
419
- console.log('| --------- | ----- | --------------- | -------- | ------ |');
420
- for (const row of rows) {
421
- console.log(`| ${row.name} (\`${row.catalogue}\`) | ${row.rules} | ${row.automatable.full} | ${row.automatable.assist} | ${row.automatable.manual} |`);
422
- }
423
- return 0;
424
- }
425
- /**
426
- * Category coverage is derived, never scored (ADR-0030 taxonomy rule): the
427
- * table answers "which future domains already have rules" without touching
428
- * scoring, gates, or confidence.
429
- */
430
- function cmdCategories(args) {
431
- const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
432
- const { packs } = loadRulePacks(rulesDir);
433
- const { categories, warnings } = loadCategories(rulesDir);
434
- for (const w of warnings)
435
- console.error(`warning: ${w}`);
436
- const rows = categoryCoverage(packs, categories);
437
- const fmt = (str(args, 'format') ?? 'md').toLowerCase();
438
- if (fmt === 'json') {
439
- console.log(JSON.stringify(rows, null, 2));
440
- return 0;
441
- }
442
- console.log('| Category | Rules | Sections |');
443
- console.log('| -------- | ----- | -------- |');
444
- for (const row of rows) {
445
- console.log(`| ${row.name} (\`${row.id}\`) | ${row.ruleCount} | ${row.sections.join(', ') || '—'} |`);
446
- }
447
- return 0;
448
- }
449
- /* ------------------------------------------------------------ foundation -- */
450
- function cmdFoundation(args) {
451
- const sub = args._[1];
452
- const dir = str(args, 'dir') ?? args._[2] ?? '.';
453
- if (sub === 'init') {
454
- return runFoundationInit({
455
- dir,
456
- nonInteractive: bool(args, 'non-interactive'),
457
- dryRun: bool(args, 'dry-run'),
458
- });
459
- }
460
- if (sub === 'show' || sub === undefined) {
461
- return runFoundationShow({ dir });
462
- }
463
- console.error('Usage: usa foundation <init|show> [path] [--dir <path>] [--non-interactive] [--dry-run]');
464
- return 2;
465
- }
466
- /* ------------------------------------------------------------------ live -- */
467
- /**
468
- * `usa live`: conversational audit session (foundation → audit → triage →
469
- * report). The default chat is the agent layer's `complete()`; without a
470
- * configured provider the session runs deterministically (loud skips, no
471
- * model calls). Async only because the LLM transport is async — every
472
- * deterministic step stays synchronous underneath.
473
- */
474
- async function cmdLive(args) {
475
- if (bool(args, 'help')) {
476
- console.log(LIVE_HELP_TEXT);
477
- return 0;
478
- }
479
- const dir = args._[1] ?? '.';
480
- const providerId = str(args, 'provider') ?? process.env['USA_PROVIDER'] ?? process.env['USA_LIVE_PROVIDER'];
481
- const modelOpt = str(args, 'model') ?? process.env['USA_MODEL'];
482
- const transcriptPath = str(args, 'transcript');
483
- const triageLimit = parseTriageLimit(str(args, 'triage-limit'));
484
- if (triageLimit === null) {
485
- console.error('--triage-limit must be a positive number');
486
- return 2;
487
- }
488
- // CLI-003: preview before any provider call or prompting.
489
- if (!dryRunWrites(args, transcriptPath ? [transcriptPath] : []))
490
- return 0;
491
- const chat = await resolveLiveChatOrAnnounce(providerId, modelOpt);
492
- return runLiveSession({
493
- dir,
494
- ask: createStdinAsk(),
495
- print: (line) => console.log(line),
496
- chat,
497
- transcriptPath,
498
- provider: providerId,
499
- model: modelOpt,
500
- triageLimit: triageLimit ?? undefined,
501
- });
502
- }
503
- /**
504
- * Resolve the session chat, announcing deterministic-only mode when no
505
- * provider is configured. The pre-flight warns once up front about
506
- * unadvertised model ids (the classic mid-session 404); the call itself
507
- * stays authoritative.
508
- */
509
- async function resolveLiveChatOrAnnounce(providerId, modelOpt) {
510
- if (providerId)
511
- return resolveLiveChat(providerId, modelOpt);
512
- console.log('live: no provider configured (set --provider or USA_PROVIDER) — deterministic-only mode.');
513
- return undefined;
514
- }
515
- /**
516
- * Resolve the session chat, or undefined for deterministic-only mode. The
517
- * pre-flight warns once up front about unadvertised model ids (the classic
518
- * mid-session 404); the call itself stays authoritative.
519
- */
520
- async function resolveLiveChat(providerId, modelOpt) {
521
- try {
522
- const cfg = loadProviderConfig(providerId, modelOpt === undefined ? undefined : { model: modelOpt });
523
- const provider = providerId;
524
- const model = modelOpt;
525
- const admonition = await checkModelAdvertised(providerId, cfg.model, listModels);
526
- if (admonition)
527
- console.error(admonition);
528
- return (req) => complete({ ...req, provider, ...(model === undefined ? {} : { model }) });
529
- }
530
- catch {
531
- // Deliberately a STATIC message: provider failures name key material
532
- // (env vars, endpoints) and even the provider id is env-derived, so a
533
- // transcript-adjacent CLI interpolates nothing here — neither the
534
- // error nor the id. The user just typed it; they know which one failed.
535
- console.error('live: LLM provider unavailable (check its API key) — continuing in deterministic-only mode.');
536
- return undefined;
537
- }
538
- }
539
- /** Positive integer, or null when the flag value is unusable. */
540
- function parseTriageLimit(raw) {
541
- if (raw === undefined)
542
- return undefined;
543
- const n = Number(raw);
544
- if (!Number.isInteger(n) || n < 1)
545
- return null;
546
- return n;
547
- }
548
- /**
549
- * `usa models`: what the provider actually serves, so a model id can be
550
- * checked before a session burns calls on a 404. Read-only and side-effect
551
- * free beyond one GET.
552
- */
553
- async function cmdModels(args) {
554
- if (bool(args, 'help')) {
555
- console.log(MODELS_HELP_TEXT);
556
- return 0;
557
- }
558
- const providerId = str(args, 'provider') ?? process.env['USA_PROVIDER'];
559
- if (!providerId) {
560
- console.error('Usage: usa models --provider <id> (or set USA_PROVIDER)');
561
- return 2;
562
- }
563
- let names;
564
- try {
565
- // Validates the key and resolves static defaults first — same fail-closed
566
- // contract as the session; the static message policy applies here too.
567
- loadProviderConfig(providerId);
568
- names = (await listModels(providerId)).map((m) => m.name);
569
- }
570
- catch {
571
- console.error('live: LLM provider unavailable (check its API key).');
572
- return 2;
573
- }
574
- for (const name of names)
575
- console.log(name);
576
- return 0;
577
- }
578
- const MODELS_HELP_TEXT = `
579
- usa models — list models a provider advertises
580
-
581
- usa models --provider ID
582
-
583
- Prints one model id per line (live listing, static default on fallback).
584
- Use it to check a model id exists before a session spends calls on a 404.
585
- `.trim();
586
- const LIVE_HELP_TEXT = `
587
- usa live — conversational audit session
588
-
589
- usa live [path] [--provider ID] [--model M] [--transcript FILE] [--triage-limit N] [--dry-run]
590
-
591
- Walks foundation → audit → triage → report with you. The deterministic
592
- engine verifies; the model only proposes. Without a provider the session
593
- runs deterministically (model turns are skipped loudly, never faked).
594
-
595
- --provider <id> LLM provider preset (default $USA_PROVIDER, else deterministic-only)
596
- --model <m> Model id (default preset default or $USA_MODEL)
597
- --transcript <file> Write the session transcript to this file
598
- --triage-limit <n> Max findings to walk in triage (default 15, ceiling 50)
599
- --dry-run Print the transcript path that would be written and exit
600
- `.trim();
601
- function printRuleDetail(packId, rule, catalogues) {
602
- console.log(`# ${rule.id} — ${rule.title}`);
603
- console.log();
604
- console.log(`pack : ${packId}`);
605
- console.log(`section : ${rule.section} ${rule.sectionTitle ?? ''}`);
606
- console.log(`severity : ${rule.severity} (when violated)`);
607
- console.log(`class : ${rule.ruleClass}`);
608
- console.log(`weight : ${rule.weight ?? 'default'}`);
609
- console.log(`depths : ${rule.depths?.join(', ') ?? 'all'}`);
610
- console.log(`automatable: ${ruleAutomatability(rule)}`);
611
- const catalogue = catalogueOf(rule, catalogues);
612
- if (catalogue)
613
- console.log(`catalogue : ${catalogue}`);
614
- console.log(`check : ${JSON.stringify(rule.check)}`);
615
- printRuleOptional(rule);
616
- }
617
- function printRuleOptional(rule) {
618
- if (rule.appliesWhen)
619
- console.log(`applies : ${JSON.stringify(rule.appliesWhen)}`);
620
- if (rule.why)
621
- console.log(`\nwhy:\n ${rule.why}`);
622
- if (rule.evidence)
623
- console.log(`\nevidence:\n ${rule.evidence}`);
624
- if (rule.remediation)
625
- console.log(`\nfix:\n ${rule.remediation}`);
626
- if (rule.references?.length)
627
- console.log(`\nreferences:\n ${rule.references.join('\n ')}`);
628
- }
629
- /* ------------------------------------------------------------------- diff -- */
630
- function cmdDiff(args) {
631
- const beforePath = args._[1];
632
- const afterPath = args._[2];
633
- if (!beforePath || !afterPath) {
634
- console.error('Usage: usa diff <before.md> <after.md> [--out DIFF.md] [--dry-run]');
635
- return 2;
636
- }
637
- // CLI-003: preview before reading or comparing anything.
638
- const diffOut = str(args, 'out');
639
- if (!dryRunWrites(args, diffOut ? [diffOut] : []))
640
- return 0;
641
- const readTrailer = (file) => {
642
- const text = fs.readFileSync(file, 'utf8');
643
- return parseTrailer(text) ?? text; // tolerate a raw YAML trailer file
644
- };
645
- try {
646
- const md = diffReports(readTrailer(beforePath), readTrailer(afterPath));
647
- if (diffOut)
648
- writeTextFile(diffOut, md);
649
- console.log(md);
650
- return 0;
651
- }
652
- catch (err) {
653
- console.error(err.message);
654
- return 2;
655
- }
656
- }
657
- /* -------------------------------------------------------- verify-report -- */
658
- /** Reads a text input, or reports it loudly and returns null (exit 2). */
659
- function readTextInput(file, label) {
660
- try {
661
- return fs.readFileSync(file, 'utf8');
662
- }
663
- catch {
664
- console.error(`${label} not found or unreadable: ${file}`);
665
- return null;
666
- }
667
- }
668
- function cmdVerifyReport(args) {
669
- const reportPath = args._[1];
670
- const bundlePath = str(args, 'bundle');
671
- if (!reportPath || !bundlePath) {
672
- console.error('Usage: usa verify-report <AUDIT.md> --bundle <sidecar.sig.json> [--key <pubkey>] [--cosign-binary <path>] [--quiet]');
673
- return 2;
674
- }
675
- const markdown = readTextInput(reportPath, 'Report file');
676
- if (markdown === null)
677
- return 2;
678
- const sidecarRaw = readTextInput(bundlePath, 'Signature sidecar');
679
- if (sidecarRaw === null)
680
- return 2;
681
- const trailerYaml = parseTrailer(markdown);
682
- if (!trailerYaml || !trailerYaml.trim()) {
683
- console.error(`Report has no machine-readable trailer: ${reportPath}`);
684
- return 2;
685
- }
686
- let signature;
687
- try {
688
- signature = parseDetachedSignature(sidecarRaw);
689
- }
690
- catch (err) {
691
- console.error(`Signature sidecar is malformed (${bundlePath}): ${err.message}`);
692
- return 2;
693
- }
694
- // Payload bytes are exactly canonicalReportBytes(report) for the report
695
- // that rendered this trailer: the trailer projection plus '\n'. The CLI
696
- // only ever sees rendered bytes, so it reconstructs them from the parsed
697
- // trailer rather than re-deriving a report object.
698
- const payload = Buffer.from(`${trailerYaml}\n`, 'utf8');
699
- return runSignatureVerify(args, reportPath, payload, signature);
700
- }
701
- /**
702
- * Runs the cosign verification and maps the outcome onto the gate
703
- * convention: 0 verified, 1 mismatch/failed, 2 environment refusal (an
704
- * absent cosign binary throws — a loud error, never a fake pass).
705
- */
706
- function runSignatureVerify(args, reportPath, payload, signature) {
707
- const quiet = bool(args, 'quiet');
708
- const cosignBinary = str(args, 'cosign-binary');
709
- let result;
710
- try {
711
- result = verifyDetachedSignature(payload, signature, {
712
- publicKeyPath: str(args, 'key') ?? '',
713
- ...(cosignBinary === undefined ? {} : { cosignBinary }),
714
- });
715
- }
716
- catch (err) {
717
- console.error(err.message);
718
- return 2;
719
- }
720
- if (result.ok) {
721
- if (!quiet)
722
- console.log(`Signature verified: ${reportPath}`);
723
- return 0;
724
- }
725
- // Gate convention: the mismatch detail goes to stderr, suppressed by
726
- // --quiet; the exit code alone carries the verdict then.
727
- if (!quiet)
728
- console.error(`Signature verification failed for ${reportPath}: ${result.detail}`);
729
- return 1;
730
- }
731
- /* -------------------------------------------------------------- bootstrap -- */
732
- function cmdBootstrap(args) {
733
- const target = args._[1] ?? '.';
734
- const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
735
- const config = loadConfig(target, str(args, 'config'));
736
- const project = new Project(target, config.ignore ?? []);
737
- const git = project.gitInfo();
738
- const out = str(args, 'out');
739
- // CLI-003: preview before detection runs.
740
- if (out && !dryRunWrites(args, [out]))
741
- return 0;
742
- const detection = detect(project, loadDetectorFile(rulesDir), git, [
743
- ...list(args, 'fact'),
744
- ...(config.facts ?? []),
745
- ]);
746
- const outcome = bootstrapPacks(detection.facts);
747
- if (outcome.packs.length === 0) {
748
- console.log('No uncovered stacks: every detected language already has a pack.');
749
- return 0;
750
- }
751
- if (!out) {
752
- printBootstrap(outcome);
753
- return 0;
754
- }
755
- return writeBootstrapResult(outcome, out);
756
- }
757
- /** Write packs to --out with a clean error (exit 2), never a stack trace. */
758
- function writeBootstrapResult(outcome, out) {
759
- try {
760
- writeBootstrapPacks(outcome, out);
761
- }
762
- catch (err) {
763
- console.error(err.message);
764
- return 2;
765
- }
766
- console.log(`wrote ${outcome.packs.length} pack(s) to ${out}`);
767
- for (const note of outcome.notes)
768
- console.log(`- ${note}`);
769
- return 0;
770
- }
771
- function printBootstrap(outcome) {
772
- for (const pack of outcome.packs) {
773
- console.log(`--- ${pack.filename} ---`);
774
- console.log(pack.yaml);
775
- }
776
- for (const note of outcome.notes)
777
- console.log(`- ${note}`);
778
- }
779
- /* ------------------------------------------------------------------- init -- */
780
- function cmdInit(args) {
781
- const target = args._[1] ?? '.';
782
- const configPath = path.join(target, '.usa.yaml');
783
- const workflowDir = path.join(target, '.github', 'workflows');
784
- const workflowPath = path.join(workflowDir, 'usa.yml');
785
- // CLI-003: preview the scaffold before creating anything.
786
- if (!dryRunWrites(args, [configPath, workflowPath].filter((p) => !fs.existsSync(p))))
787
- return 0;
788
- fs.mkdirSync(target, { recursive: true });
789
- if (fs.existsSync(configPath)) {
790
- console.error(`${configPath} already exists — leaving it alone.`);
791
- }
792
- else {
793
- fs.writeFileSync(configPath, EXAMPLE_CONFIG, 'utf8');
794
- console.log(`created ${configPath}`);
795
- }
796
- if (fs.existsSync(workflowPath)) {
797
- console.error(`${workflowPath} already exists — leaving it alone.`);
798
- }
799
- else {
800
- fs.mkdirSync(workflowDir, { recursive: true });
801
- fs.writeFileSync(workflowPath, WORKFLOW_TEMPLATE, 'utf8');
802
- console.log(`created ${workflowPath}`);
803
- }
804
- console.log('\nNext: run `npx usa audit .` or `usa audit . --depth standard`.');
805
- return 0;
806
- }
807
- /* ------------------------------------------------------------------ learn -- */
808
- function cmdLearn(args) {
809
- const reportPath = args._[1];
810
- if (!reportPath) {
811
- console.error('Usage: usa learn <report.md> [--out <file>] [--min-severity MEDIUM] [--dry-run]');
812
- return 2;
813
- }
814
- if (!fs.existsSync(reportPath)) {
815
- console.error(`Report file not found: ${reportPath}`);
816
- return 2;
817
- }
818
- const outFile = str(args, 'out', 'learn-suggestions.yaml') ?? 'learn-suggestions.yaml';
819
- const minSeverity = (str(args, 'min-severity', 'MEDIUM') ?? 'MEDIUM').toUpperCase();
820
- const allowed = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'FUTURE'];
821
- if (!allowed.includes(minSeverity)) {
822
- console.error(`--min-severity must be one of: ${allowed.join(', ')}`);
823
- return 2;
824
- }
825
- // CLI-003: preview before the report is mined.
826
- if (!dryRunWrites(args, [outFile]))
827
- return 0;
828
- const options = { reportPath, out: outFile, minSeverity };
829
- try {
830
- const suggestions = learnFromReport(options);
831
- if (suggestions.length === 0) {
832
- console.log(`No actionable suggestions found in ${reportPath}.`);
833
- return 0;
834
- }
835
- const yaml = renderSuggestions(suggestions);
836
- writeTextFile(outFile, yaml);
837
- console.log(`Wrote ${suggestions.length} suggestion(s) to ${outFile}`);
838
- console.log(' Review the rules, test them, then register in rules/index.yaml.');
839
- return 0;
840
- }
841
- catch (err) {
842
- console.error(err.message);
843
- return 2;
844
- }
845
- }
846
- /* ----------------------------------------------------------------- evolve -- */
847
- function cmdEvolve(args) {
848
- const target = args._[1] ?? '.';
849
- const rulesDir = path.resolve(str(args, 'rules-dir', DEFAULT_RULES_DIR) ?? DEFAULT_RULES_DIR);
850
- if (!fs.existsSync(target)) {
851
- console.error(`Target path does not exist: ${target}`);
852
- return 2;
853
- }
854
- const storeDir = str(args, 'store');
855
- // CLI-003: preview before the cycle runs (or the store dir is created).
856
- if (!dryRunWrites(args, storeDir ? [path.join(storeDir, 'objects')] : []))
857
- return 0;
858
- const store = storeDir ? new Store(storeDir) : undefined;
859
- const candidateCapability = loadEvolveCandidate(args);
860
- const cases = loadEvolveCases(args, candidateCapability);
861
- const learnReportPath = loadEvolveLearn(args);
862
- const proposeFromGaps = bool(args, 'propose') && !candidateCapability && !learnReportPath;
863
- const allGaps = bool(args, 'all-gaps');
864
- const result = runEvolutionCycle({
865
- target,
866
- rulesDir,
867
- engineVersion: VERSION,
868
- candidateCapability,
869
- benchmarkCases: cases,
870
- store,
871
- learnReportPath,
872
- learnMinSeverity: str(args, 'min-severity', 'MEDIUM'),
873
- proposeFromGaps,
874
- allGaps,
875
- onUnproposable: (gap, reason) => {
876
- console.error(`warning: ${gap.id} not proposable (${reason})`);
877
- },
878
- });
879
- printEvolutionResult(result);
880
- return 0;
881
- }
882
- /** Resolve `--learn <report.md>` to an existing path, or undefined. */
883
- function loadEvolveLearn(args) {
884
- const reportPath = str(args, 'learn');
885
- if (!reportPath)
886
- return undefined;
887
- if (!fs.existsSync(reportPath)) {
888
- console.error(`warning: learn report not found (${reportPath}) — ignoring --learn`);
889
- return undefined;
890
- }
891
- return reportPath;
892
- }
893
- function loadEvolveCandidate(args) {
894
- const candidateFile = str(args, 'candidate');
895
- if (!candidateFile)
896
- return undefined;
897
- const { pack, warnings } = loadPackFile(candidateFile);
898
- for (const w of warnings)
899
- console.error(`warning: ${w}`);
900
- if (!pack) {
901
- console.error(`Could not load candidate pack: ${candidateFile}`);
902
- return undefined;
903
- }
904
- return capabilityFromPack(pack, { createdBy: 'cli' });
905
- }
906
- function loadEvolveCases(args, candidateCapability) {
907
- const benchDir = str(args, 'bench-dir');
908
- const cases = benchDir ? loadBenchCases(benchDir) : [];
909
- if (candidateCapability && cases.length === 0) {
910
- console.error('warning: no benchmark cases supplied (--bench-dir). The release decision will be vacuous.');
911
- }
912
- return cases;
913
- }
914
- function loadBenchCases(dir) {
915
- if (!fs.existsSync(dir))
916
- return [];
917
- const cases = [];
918
- for (const entry of fs.readdirSync(dir)) {
919
- if (!entry.endsWith('.json'))
920
- continue;
921
- try {
922
- const raw = JSON.parse(fs.readFileSync(path.join(dir, entry), 'utf8'));
923
- if (raw && raw.id && Array.isArray(raw.expected) && raw.fixture)
924
- cases.push(raw);
925
- }
926
- catch {
927
- console.error(`warning: could not read benchmark case ${entry}`);
928
- }
929
- }
930
- return cases;
931
- }
932
- function printEvolutionResult(result) {
933
- console.log(`# Evolution: ${result.snapshot.root}`);
934
- console.log();
935
- console.log(`snapshot : ${result.snapshot.id}`);
936
- printCoverage('before', result.before.coverage);
937
- printGaps(result.before.gaps);
938
- if (!result.candidate) {
939
- console.log();
940
- if (result.proposed.length > 0) {
941
- console.log(`proposed : ${result.proposed.length} candidate(s) (none benchmarked)`);
942
- printProposed(result.proposed);
943
- }
944
- else {
945
- console.log('No candidate capability supplied (--candidate <pack.yaml>).');
946
- console.log('This was the deterministic BEFORE half of the loop only.');
947
- console.log('Pass --propose to auto-propose a candidate from the gaps.');
948
- }
949
- printQueue(result.queue);
950
- printSchedule(result.schedule);
951
- return;
952
- }
953
- const release = result.candidate.release;
954
- console.log();
955
- console.log(`candidate : ${result.candidate.capabilityId} (gaps: ${result.candidate.gapIds.join(', ')})`);
956
- if (result.proposed.length > 0)
957
- printProposed(result.proposed);
958
- if (result.learnedSuggestions !== undefined) {
959
- console.log(`learned : ${result.learnedSuggestions} suggestion(s) from report`);
960
- }
961
- if (release) {
962
- console.log(`release decision: ${release.decision}`);
963
- console.log(` precision ${release.precision.toFixed(3)} · recall ${release.recall.toFixed(3)} · regressions ${release.regressions}`);
964
- for (const r of release.reasons)
965
- console.log(` - ${r}`);
966
- }
967
- if (result.after) {
968
- console.log();
969
- printCoverage('after', result.after.coverage);
970
- const d = result.after.delta;
971
- console.log(`delta : coverage +${d.coverageDelta}pt · ${d.findingsAdded.length} finding(s) added · gap reduced: ${d.gapReduced}`);
972
- if (d.findingsAdded.length)
973
- console.log(` new findings: ${d.findingsAdded.join(', ')}`);
974
- }
975
- if (result.queue) {
976
- console.log(`queue : ${result.queue.open} open · ${result.queue.closed} closed · ${result.queue.total} total`);
977
- }
978
- printSchedule(result.schedule);
979
- }
980
- function printSchedule(schedule) {
981
- if (!schedule)
982
- return;
983
- for (const c of schedule.candidates) {
984
- const blocked = c.blockedReason ? ` · blocked: ${c.blockedReason}` : '';
985
- console.log(`schedule : ${c.capabilityId} → ${c.outcome} ` +
986
- `(precision ${c.release.precision.toFixed(3)} · recall ${c.release.recall.toFixed(3)} · ` +
987
- `regressions ${c.release.regressions}, closed: ${c.closedGapIds.length} gap(s))${blocked}`);
988
- }
989
- console.log(`schedule : ${schedule.attempted} attempted · ${schedule.released} released · ${schedule.rejected} rejected`);
990
- }
991
- function printQueue(queue) {
992
- if (!queue)
993
- return;
994
- console.log(`queue : ${queue.open} open · ${queue.closed} closed · ${queue.total} total`);
995
- }
996
- function printCoverage(label, cov) {
997
- console.log(`${label.padEnd(16)}: language coverage ${cov.languageCoverage}% · automation ${cov.automationCoverage}%`);
998
- if (cov.unsupportedLanguages.length) {
999
- console.log(` unsupported : ${cov.unsupportedLanguages.join(', ')}`);
1000
- }
1001
- }
1002
- function printGaps(gaps) {
1003
- if (gaps.length === 0) {
1004
- console.log('gaps : none');
1005
- return;
1006
- }
1007
- console.log(`gaps : ${gaps.length}`);
1008
- for (const g of gaps)
1009
- console.log(` - ${g.id} [${g.priority}] ${g.requiredCapability}`);
1010
- }
1011
- function printProposed(proposed) {
1012
- for (const c of proposed) {
1013
- const langs = c.capability.languages?.join(', ') ?? 'unknown';
1014
- const rules = c.capability.pack?.rules.length ?? 0;
1015
- console.log(` proposed : ${c.id} (${rules} rule(s), langs: ${langs}) → gap ${c.gapIds.join(', ')} [${c.status}]`);
1016
- }
1017
- console.log(' review required — proposals are unreviewed bootstrap packs, never registered.');
1018
- }
1019
- /* ------------------------------------------------------------------ utils -- */
1020
- function readVersion() {
1021
- const candidates = [
1022
- path.resolve(HERE, '..', 'package.json'),
1023
- path.resolve(HERE, '..', '..', 'package.json'),
1024
- ];
1025
- for (const c of candidates) {
1026
- try {
1027
- const pkg = JSON.parse(fs.readFileSync(c, 'utf8'));
1028
- if (pkg.version)
1029
- return pkg.version;
1030
- }
1031
- catch {
1032
- /* fall through */
1033
- }
1034
- }
1035
- return '0.0.0';
1036
- }
1037
- const HELP = `
1038
- usa — Universal Software Auditor
1039
-
1040
- usa audit [path] Audit a project and write a Markdown report
1041
- usa detect [path] Print the auto-detected facts and maturity
1042
- usa rules [--section S2] List all loaded rule packs and rules
1043
- usa explain <RULE-ID> Show everything about one rule
1044
- usa diff <before> <after> Compare two previously generated reports
1045
- usa verify-report <AUDIT.md> Verify a report's detached signature sidecar
1046
- usa init [path] Scaffold .usa.yaml + a GitHub Actions workflow
1047
- usa bootstrap [path] Propose rule packs for stacks USA cannot audit yet
1048
- usa learn <report.md> Generate suggested rules from audit findings
1049
- usa evolve [path] Run the audit → gap → candidate → release loop
1050
- usa standards Report catalogue coverage and automatability
1051
- usa categories Report future-domain category coverage
1052
- usa foundation init [path] Capture project intent into .usa/foundation.yaml
1053
- usa foundation show [path] Print the effective intent and asserted facts
1054
- usa live [path] Conversational audit session (deterministic without a provider)
1055
- usa models --provider ID List models the provider advertises
1056
-
1057
- evolve options
1058
- --dry-run Print what would be written and exit (no cycle, no store writes)
1059
- --store <dir> Persist audit runs/results (content-addressed store)
1060
- --candidate <file> A candidate capability pack (YAML) to benchmark and release
1061
- --propose Auto-propose a candidate from the gaps (bootstrap catalog)
1062
- --all-gaps Evaluate every proposable candidate (one per open gap)
1063
- --learn <report.md> Propose a candidate from a report's open findings (usa learn)
1064
- --min-severity <s> Min severity for --learn (default MEDIUM)
1065
- --bench-dir <dir> Directory of benchmark case *.json files
1066
- --rules-dir <dir> Rule pack directory (default bundled rules/)
1067
-
1068
- learn options
1069
- --dry-run Print the output path that would be written and exit
1070
- --out <file> Output YAML file (default learn-suggestions.yaml)
1071
- --min-severity <s> Minimum severity to consider (CRITICAL|HIGH|MEDIUM|LOW|FUTURE, default MEDIUM)
1072
-
1073
- standards options
1074
- --format <fmt> md | json (default md)
1075
- --rules-dir <dir> Rule pack directory (default bundled rules/)
1076
-
1077
- categories options
1078
- --format <fmt> md | json (default md)
1079
- --rules-dir <dir> Rule pack directory (default bundled rules/)
1080
-
1081
- foundation options
1082
- --dir <path> Project directory (default .; a positional path works too)
1083
- --non-interactive Write the defaults file without prompting (init only)
1084
- --dry-run Print the file that would be written and exit (init only)
1085
-
1086
- live options
1087
- --dry-run Print the transcript path that would be written and exit
1088
- --provider <id> LLM provider preset (default $USA_PROVIDER, else deterministic-only)
1089
- --model <m> Model id (default preset default or $USA_MODEL)
1090
- --transcript <file> Write the session transcript to this file
1091
- --triage-limit <n> Max findings to walk in triage (default 15, ceiling 50)
1092
-
1093
- models options
1094
- --provider <id> LLM provider preset (or $USA_PROVIDER)
1095
-
1096
- audit options
1097
- --dry-run Print the report path that would be written and exit
1098
- --out <file> Report path (default AUDIT.md)
1099
- --format <fmt> md | json | sarif | narrative (default: inferred from --out)
1100
- --depth <level> quick | standard | deep (default standard)
1101
- --profile <stage> auto | prototype | mvp | beta | production | legacy
1102
- --rules-dir <dir> Rule pack directory (default bundled rules/)
1103
- --config <file> Explicit .usa.yaml location
1104
- --include <packs> Force these packs on (comma separated)
1105
- --exclude <packs> Force these packs off
1106
- --fact <ns:value> Assert a fact detection missed, e.g. --fact has:database
1107
- --allow-commands Run \`command:\` checks (shells out; off by default)
1108
- --fail-on <sev> Exit 1 on findings >= sev: critical|high|medium|low|none
1109
- --baseline <file> New-code gate: only fail on findings new or regressed
1110
- vs this previous report (default threshold high;
1111
- explicit --fail-on overrides it)
1112
- --quiet Only errors
1113
- --max-files <n> Index at most n files (overrides config; default 60000)
1114
- --max-bytes <n> Skip files larger than n bytes (overrides config; default 2 MiB)
1115
-
1116
- bootstrap options
1117
- --dry-run Print the path that would be written and exit (with --out)
1118
- --out <file|dir> Write pack files instead of printing (default: print)
1119
-
1120
- init options
1121
- --dry-run Print the files that would be scaffolded and exit
1122
-
1123
- verify-report options
1124
- --bundle <file> Signature sidecar (required, e.g. AUDIT.md.sig.json)
1125
- --key <file> PEM public key the bundle is verified against
1126
- --cosign-binary <bin> Override the cosign binary (default cosign)
1127
- --quiet Only errors
1128
-
1129
- examples
1130
- usa audit . --depth deep
1131
- usa bootstrap ~/code/legacy-php-app --out /tmp/packs
1132
- usa audit ../api --profile production --fail-on high
1133
- usa audit . --baseline reports/2026-08.md --fail-on high
1134
- usa audit . --out reports/audit-$(date +%F).md
1135
- usa learn AUDIT.md --out swift-suggestions.yaml
1136
- usa verify-report AUDIT.md --bundle AUDIT.md.sig.json --key cosign.pub
1137
- `.trim();
1138
- const WORKFLOW_TEMPLATE = `# USA — Universal Software Auditor
1139
- # Runs on every PR and pushes a Markdown summary you can read in the Actions UI.
1140
- name: USA Audit
1141
-
1142
- on:
1143
- pull_request:
1144
- push:
1145
- branches: [master]
1146
- workflow_dispatch:
1147
- inputs:
1148
- depth:
1149
- description: Audit depth
1150
- required: false
1151
- default: standard
1152
- type: choice
1153
- options: [quick, standard, deep]
1154
-
1155
- permissions:
1156
- contents: read
1157
- `;
1158
- // ---- main entry point ----
1159
- // Run only when this file is the entry point. Comparing `import.meta.url` to
1160
- // `file://${process.argv[1]}` breaks under npm's bin symlinks: argv[1] is the
1161
- // `.bin/usa` symlink while import.meta.url is the real dist/cli.js path, so
1162
- // the guard never matches and the CLI exits silently. Resolve argv[1] to its
1163
- // real path first (and guard the fs call so a non-file argv never throws).
1164
78
  function isEntryPoint(argv1 = process.argv[1], thisUrl = import.meta.url) {
1165
79
  if (!argv1)
1166
80
  return false;