@vibefoundry/vibe-shield 1.1.0 → 1.1.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.
@@ -1,541 +1,545 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- /**
4
- * vibe-shield :: command line
5
- * ---------------------------
6
- * vibe-shield scan [paths] scan a project (default: `.`)
7
- * vibe-shield audit-skills [paths] audit agent skills / MCP config
8
- * vibe-shield init install the Agent Skill + CI gate
9
- * vibe-shield rules list every rule
10
- * vibe-shield explain <RULE_ID> full documentation for one rule
11
- * vibe-shield version | help
12
- *
13
- * The module exports `run(argv, io)` and only executes when invoked directly,
14
- * which is what lets the test suite drive the real CLI without a build step.
15
- */
16
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
- if (k2 === undefined) k2 = k;
18
- var desc = Object.getOwnPropertyDescriptor(m, k);
19
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
- desc = { enumerable: true, get: function() { return m[k]; } };
21
- }
22
- Object.defineProperty(o, k2, desc);
23
- }) : (function(o, m, k, k2) {
24
- if (k2 === undefined) k2 = k;
25
- o[k2] = m[k];
26
- }));
27
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
- Object.defineProperty(o, "default", { enumerable: true, value: v });
29
- }) : function(o, v) {
30
- o["default"] = v;
31
- });
32
- var __importStar = (this && this.__importStar) || (function () {
33
- var ownKeys = function(o) {
34
- ownKeys = Object.getOwnPropertyNames || function (o) {
35
- var ar = [];
36
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
- return ar;
38
- };
39
- return ownKeys(o);
40
- };
41
- return function (mod) {
42
- if (mod && mod.__esModule) return mod;
43
- var result = {};
44
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
- __setModuleDefault(result, mod);
46
- return result;
47
- };
48
- })();
49
- Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.HELP_TEXT = exports.EXIT_USAGE = exports.EXIT_FINDINGS = exports.EXIT_OK = exports.defaultIo = void 0;
51
- exports.parseArgs = parseArgs;
52
- exports.run = run;
53
- exports.commandScan = commandScan;
54
- exports.commandAuditSkills = commandAuditSkills;
55
- exports.commandInit = commandInit;
56
- exports.commandRules = commandRules;
57
- exports.commandExplain = commandExplain;
58
- exports.commandMcp = commandMcp;
59
- const node_fs_1 = require("node:fs");
60
- const path = __importStar(require("node:path"));
61
- const skill_auditor_1 = require("../src/mcp-guard/skill-auditor");
62
- const server_1 = require("../src/mcp/server");
63
- const scaffold_1 = require("../src/init/scaffold");
64
- const console_reporter_1 = require("../src/reporter/console-reporter");
65
- const formatters_1 = require("../src/reporter/formatters");
66
- const patch_prompt_writer_1 = require("../src/reporter/patch-prompt-writer");
67
- const engine_1 = require("../src/scanner/engine");
68
- const rules_1 = require("../src/scanner/rules");
69
- const types_1 = require("../src/scanner/types");
70
- const defaultIo = () => ({
71
- stdout: process.stdout,
72
- stderr: process.stderr,
73
- cwd: process.cwd(),
74
- env: process.env,
75
- });
76
- exports.defaultIo = defaultIo;
77
- /** Exit codes: 0 clean, 1 findings at/above `--fail-on`, 2 usage error. */
78
- exports.EXIT_OK = 0;
79
- exports.EXIT_FINDINGS = 1;
80
- exports.EXIT_USAGE = 2;
81
- const HELP = `vibe-shield — security scanner for AI-generated code
82
-
83
- USAGE
84
- vibe-shield <command> [options]
85
-
86
- COMMANDS
87
- scan [paths...] Scan the project (default: current directory).
88
- audit-skills [paths...] Audit agent skills, Cursor rules, AGENTS.md and MCP configs.
89
- init Install the Agent Skill, AGENTS.md section and CI workflow.
90
- rules List every detection rule.
91
- explain <RULE_ID> Show full documentation for a rule.
92
- mcp Serve the Model Context Protocol tools over stdio.
93
- version Print the version.
94
- help Print this help.
95
-
96
- SCAN OPTIONS
97
- --format <fmt> console | compact | summary | json | sarif | github (console)
98
- --out <file> Write the report to a file instead of stdout.
99
- --fix-prompt Write ${patch_prompt_writer_1.PATCH_PROMPT_FILENAME} with exact fix instructions.
100
- --fail-on <severity> Exit 1 when a finding at/above this severity exists. (critical)
101
- --severity <severity> Only report findings at/above this severity.
102
- --rules <ids> Comma-separated rule ids/slugs (supports VSB-DB-*).
103
- --ignore <glob> Extra gitignore-style pattern (repeatable).
104
- --include-tests Include test, spec and fixture files.
105
- --include-docs Include Markdown and docs/.
106
- --cache Reuse .vibe-shield-cache.json between runs.
107
- --max-findings <n> Limit the console list (0 = all). (50)
108
- --quiet Print only the verdict line and the exit code.
109
- --no-color Disable ANSI colour.
110
- --no-shield-ignore Ignore .vibe-shieldignore (e.g. to scan excluded fixtures).
111
- --json Shorthand for --format json.
112
-
113
- INIT OPTIONS
114
- --target <dir> Repository to install into. (cwd)
115
- --agent <kind> claude | cursor | generic | all. (all)
116
- --no-workflow Skip the GitHub Actions workflow.
117
- --force Overwrite existing files.
118
- --yes Accepted for non-interactive CI use (init never prompts).
119
-
120
- EXAMPLES
121
- npx github:avivgerz-hue/vibe-shield scan
122
- npx github:avivgerz-hue/vibe-shield scan --fail-on high --format github
123
- npx github:avivgerz-hue/vibe-shield scan --fix-prompt
124
- npx github:avivgerz-hue/vibe-shield init
125
- npx github:avivgerz-hue/vibe-shield explain VSB-DB-001
126
- npx github:avivgerz-hue/vibe-shield audit-skills .claude
127
- `;
128
- exports.HELP_TEXT = HELP;
129
- const BOOLEAN_FLAGS = new Set([
130
- 'fix-prompt',
131
- 'include-tests',
132
- 'include-docs',
133
- 'cache',
134
- 'quiet',
135
- 'no-shield-ignore',
136
- 'no-color',
137
- 'json',
138
- 'no-workflow',
139
- 'force',
140
- 'yes',
141
- 'list-rules',
142
- 'help',
143
- 'verbose',
144
- 'strict-gitignore',
145
- 'no-gitignore',
146
- ]);
147
- const VALUE_FLAGS = new Set([
148
- 'format',
149
- 'out',
150
- 'fail-on',
151
- 'severity',
152
- 'rules',
153
- 'ignore',
154
- 'max-findings',
155
- 'target',
156
- 'agent',
157
- 'category',
158
- ]);
159
- const SHORT_FLAGS = {
160
- '-h': 'help',
161
- '-v': 'version',
162
- '-f': 'format',
163
- '-o': 'out',
164
- '-q': 'quiet',
165
- };
166
- /** Parse `argv` without any dependency. Supports `--flag=value` and repeats. */
167
- function parseArgs(argv) {
168
- const positionals = [];
169
- const flags = new Map();
170
- const unknown = [];
171
- let command = '';
172
- for (let i = 0; i < argv.length; i += 1) {
173
- const raw = argv[i] ?? '';
174
- if (raw.startsWith('-')) {
175
- const eq = raw.indexOf('=');
176
- const name = (eq === -1 ? raw : raw.slice(0, eq)).replace(/^--?/, '');
177
- const normalised = SHORT_FLAGS[raw] ?? name;
178
- const inlineValue = eq === -1 ? undefined : raw.slice(eq + 1);
179
- if (BOOLEAN_FLAGS.has(normalised)) {
180
- flags.set(normalised, inlineValue === undefined ? true : inlineValue !== 'false');
181
- continue;
182
- }
183
- if (VALUE_FLAGS.has(normalised)) {
184
- const value = inlineValue ?? argv[i + 1];
185
- if (inlineValue === undefined && value !== undefined)
186
- i += 1;
187
- if (value === undefined) {
188
- unknown.push(raw);
189
- continue;
190
- }
191
- // Repeatable flag: keep every value.
192
- const existing = flags.get(normalised);
193
- if (normalised === 'ignore' && typeof existing === 'string') {
194
- flags.set(normalised, `${existing}\n${value}`);
195
- }
196
- else {
197
- flags.set(normalised, value);
198
- }
199
- continue;
200
- }
201
- unknown.push(raw);
202
- continue;
203
- }
204
- if (!command && !raw.includes(path.sep) && /^[a-z][a-z-]*$/i.test(raw) && !raw.endsWith('.ts')) {
205
- command = raw;
206
- continue;
207
- }
208
- positionals.push(raw);
209
- }
210
- return { command, positionals, flags, unknown };
211
- }
212
- const flagString = (parsed, name) => {
213
- const value = parsed.flags.get(name);
214
- return typeof value === 'string' ? value : undefined;
215
- };
216
- const flagBool = (parsed, name) => parsed.flags.get(name) === true;
217
- const flagNumber = (parsed, name, fallback) => {
218
- const raw = flagString(parsed, name);
219
- if (raw === undefined)
220
- return fallback;
221
- const value = Number.parseInt(raw, 10);
222
- return Number.isFinite(value) ? value : fallback;
223
- };
224
- /* -------------------------------------------------------------------------- */
225
- /* Output helpers */
226
- /* -------------------------------------------------------------------------- */
227
- const write = (io, stream, text) => {
228
- io[stream].write(text);
229
- };
230
- async function emitReport(result, io, options, parsed) {
231
- const noColor = flagBool(parsed, 'no-color') || io.env.NO_COLOR !== undefined;
232
- const color = (0, console_reporter_1.createColorizer)(io.stdout);
233
- if (noColor)
234
- color.enabled = false;
235
- let patchPath = null;
236
- if (options.fixPrompt) {
237
- patchPath = await (0, patch_prompt_writer_1.writePatchPrompt)(result, {
238
- outDir: io.cwd,
239
- minSeverity: ((0, types_1.isSeverity)(flagString(parsed, 'severity')) ? flagString(parsed, 'severity') : 'info'),
240
- });
241
- }
242
- if (options.quiet) {
243
- // One line: verdict + counts. Machine-friendly and CI-log friendly.
244
- const counts = result.score.counts;
245
- const files = `${result.stats.filesScanned} ${result.stats.filesScanned === 1 ? 'file' : 'files'}`;
246
- const verdict = result.stats.filesScanned === 0 ? '0 files scanned — not a pass' : `${result.score.score}/100 ${result.score.grade}`;
247
- write(io, 'stdout', `${verdict} — ${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low (${files})\n`);
248
- }
249
- else if (options.format === 'console') {
250
- (0, console_reporter_1.reportConsole)(result, io.stdout, {
251
- color,
252
- maxFindings: options.maxFindings,
253
- width: Math.min(io.stdout.columns ?? 100, 140),
254
- });
255
- }
256
- else {
257
- write(io, 'stdout', (0, formatters_1.formatResult)(result, options.format));
258
- }
259
- if (patchPath) {
260
- write(io, 'stderr', `\n📝 Wrote ${path.relative(io.cwd, patchPath) || patchPath} — hand it to your agent, or run \`npx github:avivgerz-hue/vibe-shield scan\` after fixing.\n`);
261
- }
262
- if (options.out) {
263
- const target = path.resolve(io.cwd, options.out);
264
- const { promises: fs } = await Promise.resolve().then(() => __importStar(require('node:fs')));
265
- await fs.writeFile(target, (0, formatters_1.formatResult)(result, options.format === 'console' ? 'json' : options.format), 'utf8');
266
- write(io, 'stderr', `📄 Report written to ${options.out}\n`);
267
- }
268
- }
269
- /* -------------------------------------------------------------------------- */
270
- /* Commands */
271
- /* -------------------------------------------------------------------------- */
272
- async function commandScan(parsed, io) {
273
- const format = (flagString(parsed, 'format') ?? (flagBool(parsed, 'json') ? 'json' : 'console')).toLowerCase();
274
- if (!(0, formatters_1.isOutputFormat)(format)) {
275
- write(io, 'stderr', `vibe-shield: unknown --format "${format}". Choose one of: ${formatters_1.OUTPUT_FORMATS.join(', ')}\n`);
276
- return exports.EXIT_USAGE;
277
- }
278
- const failOn = (flagString(parsed, 'fail-on') ?? 'critical').toLowerCase();
279
- if (!(0, types_1.isSeverity)(failOn)) {
280
- write(io, 'stderr', `vibe-shield: --fail-on expects one of: critical, high, medium, low, info\n`);
281
- return exports.EXIT_USAGE;
282
- }
283
- const severity = flagString(parsed, 'severity');
284
- if (severity && !(0, types_1.isSeverity)(severity)) {
285
- write(io, 'stderr', `vibe-shield: --severity expects one of: critical, high, medium, low, info\n`);
286
- return exports.EXIT_USAGE;
287
- }
288
- const ruleSelectors = (flagString(parsed, 'rules') ?? '').split(',').map((entry) => entry.trim()).filter(Boolean);
289
- const rules = ruleSelectors.length ? (0, rules_1.selectRules)(ruleSelectors) : undefined;
290
- if (ruleSelectors.length && (!rules || rules.length === 0)) {
291
- write(io, 'stderr', `vibe-shield: no rules matched --rules "${ruleSelectors.join(',')}"\n`);
292
- return exports.EXIT_USAGE;
293
- }
294
- // A typo in the path must not look like a clean pass.
295
- const missing = parsed.positionals.filter((target) => !(0, node_fs_1.existsSync)(path.resolve(io.cwd, target)));
296
- if (missing.length) {
297
- write(io, 'stderr', `vibe-shield: no such file or directory: ${missing.join(', ')}\n`);
298
- return exports.EXIT_USAGE;
299
- }
300
- const result = await (0, engine_1.scanProject)({
301
- rootDir: io.cwd,
302
- paths: parsed.positionals.length ? parsed.positionals : undefined,
303
- rules,
304
- ignorePatterns: (flagString(parsed, 'ignore') ?? '').split('\n').filter(Boolean),
305
- // `undefined` means "use the engine default" (which is: include everything
306
- // the user named explicitly, exclude tests/docs on an implicit whole-tree walk).
307
- includeTests: flagBool(parsed, 'include-tests') ? true : undefined,
308
- includeDocs: flagBool(parsed, 'include-docs') ? true : undefined,
309
- cache: flagBool(parsed, 'cache'),
310
- useGitignore: !flagBool(parsed, 'no-gitignore'),
311
- useShieldIgnore: flagBool(parsed, 'no-shield-ignore') ? false : undefined,
312
- });
313
- const filtered = severity && (0, types_1.isSeverity)(severity)
314
- ? {
315
- ...result,
316
- findings: result.findings.filter((finding) => (0, engine_1.hasFindingsAtOrAbove)({ ...result, findings: [finding] }, severity)),
317
- }
318
- : result;
319
- await emitReport(filtered, io, {
320
- fixPrompt: flagBool(parsed, 'fix-prompt'),
321
- format: format,
322
- out: flagString(parsed, 'out'),
323
- quiet: flagBool(parsed, 'quiet'),
324
- maxFindings: flagNumber(parsed, 'max-findings', 50),
325
- }, parsed);
326
- // An explicit target that matched nothing is a configuration error, not a
327
- // pass: a CI gate pointed at a path excluded by an ignore file would
328
- // otherwise report success forever without scanning a single file.
329
- if (parsed.positionals.length > 0 && filtered.stats.filesScanned === 0) {
330
- write(io, 'stderr', `vibe-shield: no scannable files matched ${parsed.positionals.join(', ')} (check ignore files, or pass --no-shield-ignore)\n`);
331
- return exports.EXIT_USAGE;
332
- }
333
- return (0, engine_1.hasFindingsAtOrAbove)(filtered, failOn) ? exports.EXIT_FINDINGS : exports.EXIT_OK;
334
- }
335
- async function commandAuditSkills(parsed, io) {
336
- const format = (flagString(parsed, 'format') ?? (flagBool(parsed, 'json') ? 'json' : 'console')).toLowerCase();
337
- if (!(0, formatters_1.isOutputFormat)(format)) {
338
- write(io, 'stderr', `vibe-shield: unknown --format "${format}".\n`);
339
- return exports.EXIT_USAGE;
340
- }
341
- const failOn = (flagString(parsed, 'fail-on') ?? 'high').toLowerCase();
342
- if (!(0, types_1.isSeverity)(failOn)) {
343
- write(io, 'stderr', `vibe-shield: --fail-on expects one of: critical, high, medium, low, info\n`);
344
- return exports.EXIT_USAGE;
345
- }
346
- const result = await (0, skill_auditor_1.auditSkills)(io.cwd, {
347
- paths: parsed.positionals.length ? parsed.positionals : undefined,
348
- version: await (0, engine_1.packageVersion)(),
349
- });
350
- await emitReport(result, io, {
351
- fixPrompt: flagBool(parsed, 'fix-prompt'),
352
- format: format,
353
- out: flagString(parsed, 'out'),
354
- quiet: flagBool(parsed, 'quiet'),
355
- maxFindings: flagNumber(parsed, 'max-findings', 50),
356
- }, parsed);
357
- return (0, engine_1.hasFindingsAtOrAbove)(result, failOn) ? exports.EXIT_FINDINGS : exports.EXIT_OK;
358
- }
359
- async function commandInit(parsed, io) {
360
- const target = path.resolve(io.cwd, flagString(parsed, 'target') ?? '.');
361
- const agent = (flagString(parsed, 'agent') ?? 'all').toLowerCase();
362
- if (!['claude', 'cursor', 'generic', 'all'].includes(agent)) {
363
- write(io, 'stderr', 'vibe-shield: --agent expects claude, cursor, generic or all\n');
364
- return exports.EXIT_USAGE;
365
- }
366
- const result = await (0, scaffold_1.installScaffold)({
367
- rootDir: target,
368
- agents: agent,
369
- workflow: !flagBool(parsed, 'no-workflow'),
370
- force: flagBool(parsed, 'force'),
371
- dryRun: false,
372
- env: io.env,
373
- });
374
- const color = (0, console_reporter_1.createColorizer)(io.stdout);
375
- if (flagBool(parsed, 'no-color'))
376
- color.enabled = false;
377
- write(io, 'stdout', `\n${color.bold(color.cyan('🛡️ vibe-shield init'))} ${color.dim(target)}\n\n`);
378
- for (const file of result.written)
379
- write(io, 'stdout', ` ${color.green('created')} ${file}\n`);
380
- for (const file of result.appended)
381
- write(io, 'stdout', ` ${color.green('updated')} ${file}\n`);
382
- for (const file of result.blocked)
383
- write(io, 'stdout', ` ${color.yellow('blocked')} ${file} ${color.dim('(left untouched — fix the file by hand)')}\n`);
384
- for (const file of result.skipped)
385
- write(io, 'stdout', ` ${color.gray('skipped')} ${file} ${color.dim('(exists — use --force)')}\n`);
386
- for (const warning of result.warnings)
387
- write(io, 'stdout', ` ${color.yellow('note')} ${warning}\n`);
388
- write(io, 'stdout', `\n${color.bold('Next steps')}\n`);
389
- write(io, 'stdout', ` 1. ${color.dim('npx github:avivgerz-hue/vibe-shield scan')} ${color.dim('# baseline your repo')}\n`);
390
- write(io, 'stdout', ` 2. ${color.dim('npx github:avivgerz-hue/vibe-shield scan --fix-prompt')} ${color.dim('# generate the fix list')}\n`);
391
- write(io, 'stdout', ` 3. Commit the new files — your agent now audits before it says "done".\n\n`);
392
- return exports.EXIT_OK;
393
- }
394
- function commandRules(parsed, io) {
395
- const category = flagString(parsed, 'category');
396
- const rules = category
397
- ? types_1.RULE_CATEGORIES.includes(category)
398
- ? (0, rules_1.rulesByCategory)(category)
399
- : []
400
- : [...rules_1.ALL_RULES];
401
- if (category && !rules.length) {
402
- write(io, 'stderr', `vibe-shield: unknown --category "${category}". Choose one of: ${types_1.RULE_CATEGORIES.join(', ')}\n`);
403
- return exports.EXIT_USAGE;
404
- }
405
- if (flagBool(parsed, 'json') || flagString(parsed, 'format') === 'json') {
406
- write(io, 'stdout', `${JSON.stringify(rules.map((rule) => ({
407
- id: rule.id,
408
- slug: rule.slug,
409
- title: rule.title,
410
- category: rule.category,
411
- severity: rule.severity,
412
- confidence: rule.confidence,
413
- cwe: rule.cwe.id,
414
- description: rule.description,
415
- remediation: rule.remediation,
416
- references: rule.references,
417
- })), null, 2)}\n`);
418
- return exports.EXIT_OK;
419
- }
420
- const color = (0, console_reporter_1.createColorizer)(io.stdout);
421
- if (flagBool(parsed, 'no-color'))
422
- color.enabled = false;
423
- write(io, 'stdout', `\n${color.bold(`${rules.length} detection rules`)}\n`);
424
- let currentCategory = '';
425
- for (const rule of rules) {
426
- if (rule.category !== currentCategory) {
427
- currentCategory = rule.category;
428
- write(io, 'stdout', `\n${color.bold(color.cyan(types_1.CATEGORY_TITLES[rule.category]))}\n`);
429
- }
430
- write(io, 'stdout', ` ${color.dim(rule.id.padEnd(14))} ${rule.severity.padEnd(9)} ${rule.title}\n`);
431
- }
432
- write(io, 'stdout', `\n${color.dim('Run `npx github:avivgerz-hue/vibe-shield explain <RULE_ID>` for the full documentation.')}\n\n`);
433
- return exports.EXIT_OK;
434
- }
435
- function commandExplain(parsed, io) {
436
- const id = parsed.positionals[0];
437
- if (!id) {
438
- write(io, 'stderr', 'vibe-shield: explain needs a rule id, e.g. `vibe-shield explain VSB-DB-001`\n');
439
- return exports.EXIT_USAGE;
440
- }
441
- const rule = (0, rules_1.getRule)(id);
442
- if (!rule) {
443
- write(io, 'stderr', `vibe-shield: unknown rule "${id}". Run \`vibe-shield rules\` to list them.\n`);
444
- return exports.EXIT_USAGE;
445
- }
446
- const color = (0, console_reporter_1.createColorizer)(io.stdout);
447
- if (flagBool(parsed, 'no-color'))
448
- color.enabled = false;
449
- const out = [];
450
- out.push('');
451
- out.push(`${color.bold(rule.id)} ${color.bold(rule.title)}`);
452
- out.push(color.dim(`${rule.category} · ${rule.severity} severity · ${rule.confidence} confidence · ${rule.cwe.id}`));
453
- out.push('');
454
- out.push(rule.description);
455
- out.push('');
456
- out.push(color.bold('Vulnerable'));
457
- out.push(color.red(rule.insecureExample));
458
- out.push('');
459
- out.push(color.bold('Fixed'));
460
- out.push(color.green(rule.secureExample));
461
- out.push('');
462
- out.push(color.bold('Remediation'));
463
- rule.remediation.forEach((step, index) => out.push(` ${index + 1}. ${step}`));
464
- out.push('');
465
- out.push(color.bold('References'));
466
- for (const reference of rule.references)
467
- out.push(` ${color.cyan(reference)}`);
468
- out.push('');
469
- out.push(color.dim(`CWE: ${rule.cwe.name} — ${rule.cwe.url}`));
470
- out.push('');
471
- write(io, 'stdout', `${out.join('\n')}\n`);
472
- return exports.EXIT_OK;
473
- }
474
- /* -------------------------------------------------------------------------- */
475
- /* mcp */
476
- /* -------------------------------------------------------------------------- */
477
- /**
478
- * Serve MCP over stdio.
479
- *
480
- * Nothing but JSON-RPC may reach stdout here — a banner or progress line would
481
- * corrupt the client session — so the one diagnostic permitted (an explicit
482
- * note that the server is ready) goes to stderr.
483
- */
484
- async function commandMcp(parsed, io) {
485
- if (parsed.positionals.length) {
486
- write(io, 'stderr', `vibe-shield: mcp takes no positional arguments (got ${parsed.positionals.join(', ')})\n`);
487
- return exports.EXIT_USAGE;
488
- }
489
- write(io, 'stderr', 'vibe-shield: MCP server listening on stdio\n');
490
- await (0, server_1.serveMcp)({ stdin: process.stdin, stdout: io.stdout, stderr: io.stderr }, { cwd: io.cwd, version: await (0, engine_1.packageVersion)() });
491
- return exports.EXIT_OK;
492
- }
493
- /* -------------------------------------------------------------------------- */
494
- /* Entry point */
495
- /* -------------------------------------------------------------------------- */
496
- /** Run the CLI. Returns the process exit code. */
497
- async function run(argv, io = (0, exports.defaultIo)()) {
498
- const parsed = parseArgs(argv);
499
- const command = parsed.command || (flagBool(parsed, 'version') ? 'version' : flagBool(parsed, 'help') ? 'help' : '');
500
- if (parsed.unknown.length) {
501
- write(io, 'stderr', `vibe-shield: unknown option${parsed.unknown.length === 1 ? '' : 's'} ${parsed.unknown.join(', ')}\n`);
502
- write(io, 'stderr', 'Run `vibe-shield help` for usage.\n');
503
- return exports.EXIT_USAGE;
504
- }
505
- switch (command) {
506
- case 'scan':
507
- return commandScan(parsed, io);
508
- case 'audit-skills':
509
- case 'audit':
510
- return commandAuditSkills(parsed, io);
511
- case 'init':
512
- return commandInit(parsed, io);
513
- case 'rules':
514
- return commandRules(parsed, io);
515
- case 'explain':
516
- return commandExplain(parsed, io);
517
- case 'mcp':
518
- return commandMcp(parsed, io);
519
- case 'version':
520
- write(io, 'stdout', `${await (0, engine_1.packageVersion)()}\n`);
521
- return exports.EXIT_OK;
522
- case 'help':
523
- case '':
524
- write(io, 'stdout', HELP);
525
- return command ? exports.EXIT_OK : exports.EXIT_USAGE;
526
- default:
527
- write(io, 'stderr', `vibe-shield: unknown command "${command}"\nRun \`vibe-shield help\` for usage.\n`);
528
- return exports.EXIT_USAGE;
529
- }
530
- }
531
- /* Self-execute only when invoked as a program (never under test). */
532
- if (require.main === module) {
533
- void run(process.argv.slice(2))
534
- .then((code) => {
535
- process.exitCode = code;
536
- })
537
- .catch((error) => {
538
- process.stderr.write(`vibe-shield: ${error instanceof Error ? error.message : String(error)}\n`);
539
- process.exitCode = exports.EXIT_USAGE;
540
- });
541
- }
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * vibe-shield :: command line
5
+ * ---------------------------
6
+ * vibe-shield scan [paths] scan a project (default: `.`)
7
+ * vibe-shield audit-skills [paths] audit agent skills / MCP config
8
+ * vibe-shield init install the Agent Skill + CI gate
9
+ * vibe-shield rules list every rule
10
+ * vibe-shield explain <RULE_ID> full documentation for one rule
11
+ * vibe-shield version | help
12
+ *
13
+ * The module exports `run(argv, io)` and only executes when invoked directly,
14
+ * which is what lets the test suite drive the real CLI without a build step.
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.HELP_TEXT = exports.EXIT_USAGE = exports.EXIT_FINDINGS = exports.EXIT_OK = exports.defaultIo = void 0;
51
+ exports.parseArgs = parseArgs;
52
+ exports.run = run;
53
+ exports.commandScan = commandScan;
54
+ exports.commandAuditSkills = commandAuditSkills;
55
+ exports.commandInit = commandInit;
56
+ exports.commandRules = commandRules;
57
+ exports.commandExplain = commandExplain;
58
+ exports.commandMcp = commandMcp;
59
+ const node_fs_1 = require("node:fs");
60
+ const path = __importStar(require("node:path"));
61
+ const skill_auditor_1 = require("../src/mcp-guard/skill-auditor");
62
+ const server_1 = require("../src/mcp/server");
63
+ const scaffold_1 = require("../src/init/scaffold");
64
+ const console_reporter_1 = require("../src/reporter/console-reporter");
65
+ const formatters_1 = require("../src/reporter/formatters");
66
+ const patch_prompt_writer_1 = require("../src/reporter/patch-prompt-writer");
67
+ const engine_1 = require("../src/scanner/engine");
68
+ const rules_1 = require("../src/scanner/rules");
69
+ const types_1 = require("../src/scanner/types");
70
+ const defaultIo = () => ({
71
+ stdout: process.stdout,
72
+ stderr: process.stderr,
73
+ cwd: process.cwd(),
74
+ env: process.env,
75
+ });
76
+ exports.defaultIo = defaultIo;
77
+ /** Exit codes: 0 clean, 1 findings at/above `--fail-on`, 2 usage error. */
78
+ exports.EXIT_OK = 0;
79
+ exports.EXIT_FINDINGS = 1;
80
+ exports.EXIT_USAGE = 2;
81
+ const HELP = `vibe-shield — security scanner for AI-generated code
82
+
83
+ USAGE
84
+ vibe-shield <command> [options]
85
+
86
+ COMMANDS
87
+ scan [paths...] Scan the project (default: current directory).
88
+ audit-skills [paths...] Audit agent skills, Cursor rules, AGENTS.md and MCP configs.
89
+ init Install the Agent Skill, AGENTS.md section and CI workflow.
90
+ rules List every detection rule.
91
+ explain <RULE_ID> Show full documentation for a rule.
92
+ mcp Serve the Model Context Protocol tools over stdio.
93
+ version Print the version.
94
+ help Print this help.
95
+
96
+ SCAN OPTIONS
97
+ --format <fmt> console | compact | summary | json | sarif | github (console)
98
+ --out <file> Write the report to a file instead of stdout.
99
+ --fix-prompt Write ${patch_prompt_writer_1.PATCH_PROMPT_FILENAME} with exact fix instructions.
100
+ --fail-on <severity> Exit 1 when a finding at/above this severity exists. (critical)
101
+ --severity <severity> Only report findings at/above this severity.
102
+ --rules <ids> Comma-separated rule ids/slugs (supports VSB-DB-*).
103
+ --ignore <glob> Extra gitignore-style pattern (repeatable).
104
+ --include-tests Include test, spec and fixture files.
105
+ --include-docs Include Markdown and docs/.
106
+ --cache Reuse .vibe-shield-cache.json between runs.
107
+ --max-findings <n> Limit the console list (0 = all). (50)
108
+ --quiet Print only the verdict line and the exit code.
109
+ --no-color Disable ANSI colour.
110
+ --no-shield-ignore Ignore .vibe-shieldignore (e.g. to scan excluded fixtures).
111
+ --json Shorthand for --format json.
112
+
113
+ INIT OPTIONS
114
+ --target <dir> Repository to install into. (cwd)
115
+ --agent <kind> claude | cursor | generic | all. (all)
116
+ --no-workflow Skip the GitHub Actions workflow.
117
+ --force Overwrite existing files.
118
+ --yes Accepted for non-interactive CI use (init never prompts).
119
+
120
+ EXAMPLES
121
+ npx -y @vibefoundry/vibe-shield scan
122
+ npx -y @vibefoundry/vibe-shield scan --fail-on high --format github
123
+ npx -y @vibefoundry/vibe-shield scan --fix-prompt
124
+ npx -y @vibefoundry/vibe-shield init
125
+ npx -y @vibefoundry/vibe-shield explain VSB-DB-001
126
+ npx -y @vibefoundry/vibe-shield audit-skills .claude
127
+ `;
128
+ exports.HELP_TEXT = HELP;
129
+ const BOOLEAN_FLAGS = new Set([
130
+ // `--mcp` is an alias for the `mcp` subcommand, so `npx -y
131
+ // @vibefoundry/vibe-shield --mcp` works the way most MCP clients expect.
132
+ 'mcp',
133
+ 'fix-prompt',
134
+ 'include-tests',
135
+ 'include-docs',
136
+ 'cache',
137
+ 'quiet',
138
+ 'no-shield-ignore',
139
+ 'no-color',
140
+ 'json',
141
+ 'no-workflow',
142
+ 'force',
143
+ 'yes',
144
+ 'list-rules',
145
+ 'help',
146
+ 'verbose',
147
+ 'strict-gitignore',
148
+ 'no-gitignore',
149
+ ]);
150
+ const VALUE_FLAGS = new Set([
151
+ 'format',
152
+ 'out',
153
+ 'fail-on',
154
+ 'severity',
155
+ 'rules',
156
+ 'ignore',
157
+ 'max-findings',
158
+ 'target',
159
+ 'agent',
160
+ 'category',
161
+ ]);
162
+ const SHORT_FLAGS = {
163
+ '-h': 'help',
164
+ '-v': 'version',
165
+ '-f': 'format',
166
+ '-o': 'out',
167
+ '-q': 'quiet',
168
+ };
169
+ /** Parse `argv` without any dependency. Supports `--flag=value` and repeats. */
170
+ function parseArgs(argv) {
171
+ const positionals = [];
172
+ const flags = new Map();
173
+ const unknown = [];
174
+ let command = '';
175
+ for (let i = 0; i < argv.length; i += 1) {
176
+ const raw = argv[i] ?? '';
177
+ if (raw.startsWith('-')) {
178
+ const eq = raw.indexOf('=');
179
+ const name = (eq === -1 ? raw : raw.slice(0, eq)).replace(/^--?/, '');
180
+ const normalised = SHORT_FLAGS[raw] ?? name;
181
+ const inlineValue = eq === -1 ? undefined : raw.slice(eq + 1);
182
+ if (BOOLEAN_FLAGS.has(normalised)) {
183
+ flags.set(normalised, inlineValue === undefined ? true : inlineValue !== 'false');
184
+ continue;
185
+ }
186
+ if (VALUE_FLAGS.has(normalised)) {
187
+ const value = inlineValue ?? argv[i + 1];
188
+ if (inlineValue === undefined && value !== undefined)
189
+ i += 1;
190
+ if (value === undefined) {
191
+ unknown.push(raw);
192
+ continue;
193
+ }
194
+ // Repeatable flag: keep every value.
195
+ const existing = flags.get(normalised);
196
+ if (normalised === 'ignore' && typeof existing === 'string') {
197
+ flags.set(normalised, `${existing}\n${value}`);
198
+ }
199
+ else {
200
+ flags.set(normalised, value);
201
+ }
202
+ continue;
203
+ }
204
+ unknown.push(raw);
205
+ continue;
206
+ }
207
+ if (!command && !raw.includes(path.sep) && /^[a-z][a-z-]*$/i.test(raw) && !raw.endsWith('.ts')) {
208
+ command = raw;
209
+ continue;
210
+ }
211
+ positionals.push(raw);
212
+ }
213
+ return { command, positionals, flags, unknown };
214
+ }
215
+ const flagString = (parsed, name) => {
216
+ const value = parsed.flags.get(name);
217
+ return typeof value === 'string' ? value : undefined;
218
+ };
219
+ const flagBool = (parsed, name) => parsed.flags.get(name) === true;
220
+ const flagNumber = (parsed, name, fallback) => {
221
+ const raw = flagString(parsed, name);
222
+ if (raw === undefined)
223
+ return fallback;
224
+ const value = Number.parseInt(raw, 10);
225
+ return Number.isFinite(value) ? value : fallback;
226
+ };
227
+ /* -------------------------------------------------------------------------- */
228
+ /* Output helpers */
229
+ /* -------------------------------------------------------------------------- */
230
+ const write = (io, stream, text) => {
231
+ io[stream].write(text);
232
+ };
233
+ async function emitReport(result, io, options, parsed) {
234
+ const noColor = flagBool(parsed, 'no-color') || io.env.NO_COLOR !== undefined;
235
+ const color = (0, console_reporter_1.createColorizer)(io.stdout);
236
+ if (noColor)
237
+ color.enabled = false;
238
+ let patchPath = null;
239
+ if (options.fixPrompt) {
240
+ patchPath = await (0, patch_prompt_writer_1.writePatchPrompt)(result, {
241
+ outDir: io.cwd,
242
+ minSeverity: ((0, types_1.isSeverity)(flagString(parsed, 'severity')) ? flagString(parsed, 'severity') : 'info'),
243
+ });
244
+ }
245
+ if (options.quiet) {
246
+ // One line: verdict + counts. Machine-friendly and CI-log friendly.
247
+ const counts = result.score.counts;
248
+ const files = `${result.stats.filesScanned} ${result.stats.filesScanned === 1 ? 'file' : 'files'}`;
249
+ const verdict = result.stats.filesScanned === 0 ? '0 files scanned — not a pass' : `${result.score.score}/100 ${result.score.grade}`;
250
+ write(io, 'stdout', `${verdict} — ${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low (${files})\n`);
251
+ }
252
+ else if (options.format === 'console') {
253
+ (0, console_reporter_1.reportConsole)(result, io.stdout, {
254
+ color,
255
+ maxFindings: options.maxFindings,
256
+ width: Math.min(io.stdout.columns ?? 100, 140),
257
+ });
258
+ }
259
+ else {
260
+ write(io, 'stdout', (0, formatters_1.formatResult)(result, options.format));
261
+ }
262
+ if (patchPath) {
263
+ write(io, 'stderr', `\n📝 Wrote ${path.relative(io.cwd, patchPath) || patchPath} — hand it to your agent, or run \`npx -y @vibefoundry/vibe-shield scan\` after fixing.\n`);
264
+ }
265
+ if (options.out) {
266
+ const target = path.resolve(io.cwd, options.out);
267
+ const { promises: fs } = await Promise.resolve().then(() => __importStar(require('node:fs')));
268
+ await fs.writeFile(target, (0, formatters_1.formatResult)(result, options.format === 'console' ? 'json' : options.format), 'utf8');
269
+ write(io, 'stderr', `📄 Report written to ${options.out}\n`);
270
+ }
271
+ }
272
+ /* -------------------------------------------------------------------------- */
273
+ /* Commands */
274
+ /* -------------------------------------------------------------------------- */
275
+ async function commandScan(parsed, io) {
276
+ const format = (flagString(parsed, 'format') ?? (flagBool(parsed, 'json') ? 'json' : 'console')).toLowerCase();
277
+ if (!(0, formatters_1.isOutputFormat)(format)) {
278
+ write(io, 'stderr', `vibe-shield: unknown --format "${format}". Choose one of: ${formatters_1.OUTPUT_FORMATS.join(', ')}\n`);
279
+ return exports.EXIT_USAGE;
280
+ }
281
+ const failOn = (flagString(parsed, 'fail-on') ?? 'critical').toLowerCase();
282
+ if (!(0, types_1.isSeverity)(failOn)) {
283
+ write(io, 'stderr', `vibe-shield: --fail-on expects one of: critical, high, medium, low, info\n`);
284
+ return exports.EXIT_USAGE;
285
+ }
286
+ const severity = flagString(parsed, 'severity');
287
+ if (severity && !(0, types_1.isSeverity)(severity)) {
288
+ write(io, 'stderr', `vibe-shield: --severity expects one of: critical, high, medium, low, info\n`);
289
+ return exports.EXIT_USAGE;
290
+ }
291
+ const ruleSelectors = (flagString(parsed, 'rules') ?? '').split(',').map((entry) => entry.trim()).filter(Boolean);
292
+ const rules = ruleSelectors.length ? (0, rules_1.selectRules)(ruleSelectors) : undefined;
293
+ if (ruleSelectors.length && (!rules || rules.length === 0)) {
294
+ write(io, 'stderr', `vibe-shield: no rules matched --rules "${ruleSelectors.join(',')}"\n`);
295
+ return exports.EXIT_USAGE;
296
+ }
297
+ // A typo in the path must not look like a clean pass.
298
+ const missing = parsed.positionals.filter((target) => !(0, node_fs_1.existsSync)(path.resolve(io.cwd, target)));
299
+ if (missing.length) {
300
+ write(io, 'stderr', `vibe-shield: no such file or directory: ${missing.join(', ')}\n`);
301
+ return exports.EXIT_USAGE;
302
+ }
303
+ const result = await (0, engine_1.scanProject)({
304
+ rootDir: io.cwd,
305
+ paths: parsed.positionals.length ? parsed.positionals : undefined,
306
+ rules,
307
+ ignorePatterns: (flagString(parsed, 'ignore') ?? '').split('\n').filter(Boolean),
308
+ // `undefined` means "use the engine default" (which is: include everything
309
+ // the user named explicitly, exclude tests/docs on an implicit whole-tree walk).
310
+ includeTests: flagBool(parsed, 'include-tests') ? true : undefined,
311
+ includeDocs: flagBool(parsed, 'include-docs') ? true : undefined,
312
+ cache: flagBool(parsed, 'cache'),
313
+ useGitignore: !flagBool(parsed, 'no-gitignore'),
314
+ useShieldIgnore: flagBool(parsed, 'no-shield-ignore') ? false : undefined,
315
+ });
316
+ const filtered = severity && (0, types_1.isSeverity)(severity)
317
+ ? {
318
+ ...result,
319
+ findings: result.findings.filter((finding) => (0, engine_1.hasFindingsAtOrAbove)({ ...result, findings: [finding] }, severity)),
320
+ }
321
+ : result;
322
+ await emitReport(filtered, io, {
323
+ fixPrompt: flagBool(parsed, 'fix-prompt'),
324
+ format: format,
325
+ out: flagString(parsed, 'out'),
326
+ quiet: flagBool(parsed, 'quiet'),
327
+ maxFindings: flagNumber(parsed, 'max-findings', 50),
328
+ }, parsed);
329
+ // An explicit target that matched nothing is a configuration error, not a
330
+ // pass: a CI gate pointed at a path excluded by an ignore file would
331
+ // otherwise report success forever without scanning a single file.
332
+ if (parsed.positionals.length > 0 && filtered.stats.filesScanned === 0) {
333
+ write(io, 'stderr', `vibe-shield: no scannable files matched ${parsed.positionals.join(', ')} (check ignore files, or pass --no-shield-ignore)\n`);
334
+ return exports.EXIT_USAGE;
335
+ }
336
+ return (0, engine_1.hasFindingsAtOrAbove)(filtered, failOn) ? exports.EXIT_FINDINGS : exports.EXIT_OK;
337
+ }
338
+ async function commandAuditSkills(parsed, io) {
339
+ const format = (flagString(parsed, 'format') ?? (flagBool(parsed, 'json') ? 'json' : 'console')).toLowerCase();
340
+ if (!(0, formatters_1.isOutputFormat)(format)) {
341
+ write(io, 'stderr', `vibe-shield: unknown --format "${format}".\n`);
342
+ return exports.EXIT_USAGE;
343
+ }
344
+ const failOn = (flagString(parsed, 'fail-on') ?? 'high').toLowerCase();
345
+ if (!(0, types_1.isSeverity)(failOn)) {
346
+ write(io, 'stderr', `vibe-shield: --fail-on expects one of: critical, high, medium, low, info\n`);
347
+ return exports.EXIT_USAGE;
348
+ }
349
+ const result = await (0, skill_auditor_1.auditSkills)(io.cwd, {
350
+ paths: parsed.positionals.length ? parsed.positionals : undefined,
351
+ version: await (0, engine_1.packageVersion)(),
352
+ });
353
+ await emitReport(result, io, {
354
+ fixPrompt: flagBool(parsed, 'fix-prompt'),
355
+ format: format,
356
+ out: flagString(parsed, 'out'),
357
+ quiet: flagBool(parsed, 'quiet'),
358
+ maxFindings: flagNumber(parsed, 'max-findings', 50),
359
+ }, parsed);
360
+ return (0, engine_1.hasFindingsAtOrAbove)(result, failOn) ? exports.EXIT_FINDINGS : exports.EXIT_OK;
361
+ }
362
+ async function commandInit(parsed, io) {
363
+ const target = path.resolve(io.cwd, flagString(parsed, 'target') ?? '.');
364
+ const agent = (flagString(parsed, 'agent') ?? 'all').toLowerCase();
365
+ if (!['claude', 'cursor', 'generic', 'all'].includes(agent)) {
366
+ write(io, 'stderr', 'vibe-shield: --agent expects claude, cursor, generic or all\n');
367
+ return exports.EXIT_USAGE;
368
+ }
369
+ const result = await (0, scaffold_1.installScaffold)({
370
+ rootDir: target,
371
+ agents: agent,
372
+ workflow: !flagBool(parsed, 'no-workflow'),
373
+ force: flagBool(parsed, 'force'),
374
+ dryRun: false,
375
+ env: io.env,
376
+ });
377
+ const color = (0, console_reporter_1.createColorizer)(io.stdout);
378
+ if (flagBool(parsed, 'no-color'))
379
+ color.enabled = false;
380
+ write(io, 'stdout', `\n${color.bold(color.cyan('🛡️ vibe-shield init'))} ${color.dim(target)}\n\n`);
381
+ for (const file of result.written)
382
+ write(io, 'stdout', ` ${color.green('created')} ${file}\n`);
383
+ for (const file of result.appended)
384
+ write(io, 'stdout', ` ${color.green('updated')} ${file}\n`);
385
+ for (const file of result.blocked)
386
+ write(io, 'stdout', ` ${color.yellow('blocked')} ${file} ${color.dim('(left untouched — fix the file by hand)')}\n`);
387
+ for (const file of result.skipped)
388
+ write(io, 'stdout', ` ${color.gray('skipped')} ${file} ${color.dim('(exists — use --force)')}\n`);
389
+ for (const warning of result.warnings)
390
+ write(io, 'stdout', ` ${color.yellow('note')} ${warning}\n`);
391
+ write(io, 'stdout', `\n${color.bold('Next steps')}\n`);
392
+ write(io, 'stdout', ` 1. ${color.dim('npx -y @vibefoundry/vibe-shield scan')} ${color.dim('# baseline your repo')}\n`);
393
+ write(io, 'stdout', ` 2. ${color.dim('npx -y @vibefoundry/vibe-shield scan --fix-prompt')} ${color.dim('# generate the fix list')}\n`);
394
+ write(io, 'stdout', ` 3. Commit the new files — your agent now audits before it says "done".\n\n`);
395
+ return exports.EXIT_OK;
396
+ }
397
+ function commandRules(parsed, io) {
398
+ const category = flagString(parsed, 'category');
399
+ const rules = category
400
+ ? types_1.RULE_CATEGORIES.includes(category)
401
+ ? (0, rules_1.rulesByCategory)(category)
402
+ : []
403
+ : [...rules_1.ALL_RULES];
404
+ if (category && !rules.length) {
405
+ write(io, 'stderr', `vibe-shield: unknown --category "${category}". Choose one of: ${types_1.RULE_CATEGORIES.join(', ')}\n`);
406
+ return exports.EXIT_USAGE;
407
+ }
408
+ if (flagBool(parsed, 'json') || flagString(parsed, 'format') === 'json') {
409
+ write(io, 'stdout', `${JSON.stringify(rules.map((rule) => ({
410
+ id: rule.id,
411
+ slug: rule.slug,
412
+ title: rule.title,
413
+ category: rule.category,
414
+ severity: rule.severity,
415
+ confidence: rule.confidence,
416
+ cwe: rule.cwe.id,
417
+ description: rule.description,
418
+ remediation: rule.remediation,
419
+ references: rule.references,
420
+ })), null, 2)}\n`);
421
+ return exports.EXIT_OK;
422
+ }
423
+ const color = (0, console_reporter_1.createColorizer)(io.stdout);
424
+ if (flagBool(parsed, 'no-color'))
425
+ color.enabled = false;
426
+ write(io, 'stdout', `\n${color.bold(`${rules.length} detection rules`)}\n`);
427
+ let currentCategory = '';
428
+ for (const rule of rules) {
429
+ if (rule.category !== currentCategory) {
430
+ currentCategory = rule.category;
431
+ write(io, 'stdout', `\n${color.bold(color.cyan(types_1.CATEGORY_TITLES[rule.category]))}\n`);
432
+ }
433
+ write(io, 'stdout', ` ${color.dim(rule.id.padEnd(14))} ${rule.severity.padEnd(9)} ${rule.title}\n`);
434
+ }
435
+ write(io, 'stdout', `\n${color.dim('Run `npx -y @vibefoundry/vibe-shield explain <RULE_ID>` for the full documentation.')}\n\n`);
436
+ return exports.EXIT_OK;
437
+ }
438
+ function commandExplain(parsed, io) {
439
+ const id = parsed.positionals[0];
440
+ if (!id) {
441
+ write(io, 'stderr', 'vibe-shield: explain needs a rule id, e.g. `vibe-shield explain VSB-DB-001`\n');
442
+ return exports.EXIT_USAGE;
443
+ }
444
+ const rule = (0, rules_1.getRule)(id);
445
+ if (!rule) {
446
+ write(io, 'stderr', `vibe-shield: unknown rule "${id}". Run \`vibe-shield rules\` to list them.\n`);
447
+ return exports.EXIT_USAGE;
448
+ }
449
+ const color = (0, console_reporter_1.createColorizer)(io.stdout);
450
+ if (flagBool(parsed, 'no-color'))
451
+ color.enabled = false;
452
+ const out = [];
453
+ out.push('');
454
+ out.push(`${color.bold(rule.id)} ${color.bold(rule.title)}`);
455
+ out.push(color.dim(`${rule.category} · ${rule.severity} severity · ${rule.confidence} confidence · ${rule.cwe.id}`));
456
+ out.push('');
457
+ out.push(rule.description);
458
+ out.push('');
459
+ out.push(color.bold('Vulnerable'));
460
+ out.push(color.red(rule.insecureExample));
461
+ out.push('');
462
+ out.push(color.bold('Fixed'));
463
+ out.push(color.green(rule.secureExample));
464
+ out.push('');
465
+ out.push(color.bold('Remediation'));
466
+ rule.remediation.forEach((step, index) => out.push(` ${index + 1}. ${step}`));
467
+ out.push('');
468
+ out.push(color.bold('References'));
469
+ for (const reference of rule.references)
470
+ out.push(` ${color.cyan(reference)}`);
471
+ out.push('');
472
+ out.push(color.dim(`CWE: ${rule.cwe.name} — ${rule.cwe.url}`));
473
+ out.push('');
474
+ write(io, 'stdout', `${out.join('\n')}\n`);
475
+ return exports.EXIT_OK;
476
+ }
477
+ /* -------------------------------------------------------------------------- */
478
+ /* mcp */
479
+ /* -------------------------------------------------------------------------- */
480
+ /**
481
+ * Serve MCP over stdio.
482
+ *
483
+ * Nothing but JSON-RPC may reach stdout here — a banner or progress line would
484
+ * corrupt the client session — so the one diagnostic permitted (an explicit
485
+ * note that the server is ready) goes to stderr.
486
+ */
487
+ async function commandMcp(parsed, io) {
488
+ if (parsed.positionals.length) {
489
+ write(io, 'stderr', `vibe-shield: mcp takes no positional arguments (got ${parsed.positionals.join(', ')})\n`);
490
+ return exports.EXIT_USAGE;
491
+ }
492
+ write(io, 'stderr', 'vibe-shield: MCP server listening on stdio\n');
493
+ await (0, server_1.serveMcp)({ stdin: process.stdin, stdout: io.stdout, stderr: io.stderr }, { cwd: io.cwd, version: await (0, engine_1.packageVersion)() });
494
+ return exports.EXIT_OK;
495
+ }
496
+ /* -------------------------------------------------------------------------- */
497
+ /* Entry point */
498
+ /* -------------------------------------------------------------------------- */
499
+ /** Run the CLI. Returns the process exit code. */
500
+ async function run(argv, io = (0, exports.defaultIo)()) {
501
+ const parsed = parseArgs(argv);
502
+ const command = parsed.command ||
503
+ (flagBool(parsed, 'mcp') ? 'mcp' : flagBool(parsed, 'version') ? 'version' : flagBool(parsed, 'help') ? 'help' : '');
504
+ if (parsed.unknown.length) {
505
+ write(io, 'stderr', `vibe-shield: unknown option${parsed.unknown.length === 1 ? '' : 's'} ${parsed.unknown.join(', ')}\n`);
506
+ write(io, 'stderr', 'Run `vibe-shield help` for usage.\n');
507
+ return exports.EXIT_USAGE;
508
+ }
509
+ switch (command) {
510
+ case 'scan':
511
+ return commandScan(parsed, io);
512
+ case 'audit-skills':
513
+ case 'audit':
514
+ return commandAuditSkills(parsed, io);
515
+ case 'init':
516
+ return commandInit(parsed, io);
517
+ case 'rules':
518
+ return commandRules(parsed, io);
519
+ case 'explain':
520
+ return commandExplain(parsed, io);
521
+ case 'mcp':
522
+ return commandMcp(parsed, io);
523
+ case 'version':
524
+ write(io, 'stdout', `${await (0, engine_1.packageVersion)()}\n`);
525
+ return exports.EXIT_OK;
526
+ case 'help':
527
+ case '':
528
+ write(io, 'stdout', HELP);
529
+ return command ? exports.EXIT_OK : exports.EXIT_USAGE;
530
+ default:
531
+ write(io, 'stderr', `vibe-shield: unknown command "${command}"\nRun \`vibe-shield help\` for usage.\n`);
532
+ return exports.EXIT_USAGE;
533
+ }
534
+ }
535
+ /* Self-execute only when invoked as a program (never under test). */
536
+ if (require.main === module) {
537
+ void run(process.argv.slice(2))
538
+ .then((code) => {
539
+ process.exitCode = code;
540
+ })
541
+ .catch((error) => {
542
+ process.stderr.write(`vibe-shield: ${error instanceof Error ? error.message : String(error)}\n`);
543
+ process.exitCode = exports.EXIT_USAGE;
544
+ });
545
+ }