@rigour-labs/cli 5.1.2 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -16,6 +16,9 @@ import { settingsShowCommand, settingsSetKeyCommand, settingsRemoveKeyCommand, s
16
16
  import { doctorCommand } from './commands/doctor.js';
17
17
  import { brainCommand } from './commands/brain.js';
18
18
  import { deepStatsCommand } from './commands/deep-stats.js';
19
+ import { reviewCommand } from './commands/review.js';
20
+ import { checkPatternCommand } from './commands/check-pattern.js';
21
+ import { securityAuditCommand } from './commands/security-audit.js';
19
22
  import { checkForUpdates } from './utils/version.js';
20
23
  import { getCliVersion } from './utils/cli-version.js';
21
24
  import chalk from 'chalk';
@@ -202,6 +205,60 @@ program
202
205
  .action(async () => {
203
206
  await doctorCommand();
204
207
  });
208
+ program
209
+ .command('review')
210
+ .description('Review a diff against quality gates (filter to changed lines)')
211
+ .option('--json', 'Output report in JSON format')
212
+ .option('--ci', 'CI mode (minimal output)')
213
+ .option('-c, --config <path>', 'Path to custom rigour.yml configuration')
214
+ .option('--diff <path>', 'Path to diff file (reads stdin if omitted)')
215
+ .option('--files <paths>', 'Comma-separated list of changed files (auto-detected from diff if omitted)')
216
+ .option('--deep', 'Enable deep LLM-powered analysis')
217
+ .option('--pro', 'Use full deep model for analysis')
218
+ .option('-k, --api-key <key>', 'Cloud API key for deep analysis')
219
+ .option('--provider <name>', 'Cloud provider for deep analysis')
220
+ .option('--api-base-url <url>', 'Custom API base URL')
221
+ .option('--model-name <name>', 'Override cloud model name')
222
+ .addHelpText('after', `
223
+ Examples:
224
+ $ git diff | rigour review --json # Review staged changes (JSON)
225
+ $ git diff main..HEAD | rigour review # Review branch changes
226
+ $ rigour review --diff changes.patch --deep # Review diff file with deep analysis
227
+ $ git diff | rigour review --ci # CI-friendly review
228
+ `)
229
+ .action(async (options) => {
230
+ await reviewCommand(process.cwd(), options);
231
+ });
232
+ program
233
+ .command('check-pattern')
234
+ .description('Check if a pattern already exists, is stale, or has security issues')
235
+ .requiredOption('-n, --name <name>', 'Name of the function, class, or component to create')
236
+ .option('-t, --type <type>', 'Pattern type (function, component, hook, class)')
237
+ .option('-i, --intent <intent>', 'What the code is for (e.g., "format dates", "import lodash")')
238
+ .option('--json', 'Output report in JSON format')
239
+ .addHelpText('after', `
240
+ Examples:
241
+ $ rigour check-pattern --name useDebounce --type hook # Check before creating hook
242
+ $ rigour check-pattern --name formatDate --type function --json # JSON output
243
+ $ rigour check-pattern --name lodash --intent "import lodash" # Checks CVEs too
244
+ `)
245
+ .action(async (options) => {
246
+ await checkPatternCommand(process.cwd(), options);
247
+ });
248
+ program
249
+ .command('security-audit')
250
+ .description('Run a CVE security audit on project dependencies')
251
+ .option('--json', 'Output report in JSON format')
252
+ .option('--ci', 'CI mode (minimal output)')
253
+ .addHelpText('after', `
254
+ Examples:
255
+ $ rigour security-audit # Human-readable security report
256
+ $ rigour security-audit --json # Machine-readable JSON
257
+ $ rigour security-audit --ci # CI pipeline integration
258
+ `)
259
+ .action(async (options) => {
260
+ await securityAuditCommand(process.cwd(), options);
261
+ });
205
262
  const hooksCmd = program
206
263
  .command('hooks')
207
264
  .description('Manage AI coding tool hook integrations');
@@ -0,0 +1,7 @@
1
+ export interface CheckPatternOptions {
2
+ name: string;
3
+ type?: string;
4
+ intent?: string;
5
+ json?: boolean;
6
+ }
7
+ export declare function checkPatternCommand(cwd: string, options: CheckPatternOptions): Promise<void>;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Pattern Check CLI Command
3
+ *
4
+ * Wraps the same core logic as the MCP `rigour_check_pattern` tool.
5
+ * Three-layer check before creating new code:
6
+ * 1. Reinvention detection (pattern index fuzzy match)
7
+ * 2. Staleness / anti-pattern detection
8
+ * 3. Security / CVE check (when intent includes imports)
9
+ *
10
+ * Usage:
11
+ * rigour check-pattern --name useDebounce --type hook --intent "debounce user input"
12
+ * rigour check-pattern --name formatDate --type function --json
13
+ */
14
+ import chalk from 'chalk';
15
+ import { PatternMatcher, loadPatternIndex, getDefaultIndexPath, StalenessDetector, SecurityDetector, } from '@rigour-labs/core/pattern-index';
16
+ const EXIT_PASS = 0;
17
+ const EXIT_FAIL = 1;
18
+ const EXIT_INTERNAL_ERROR = 3;
19
+ export async function checkPatternCommand(cwd, options) {
20
+ const { name: patternName, type, intent } = options;
21
+ if (!patternName) {
22
+ if (options.json) {
23
+ console.log(JSON.stringify({ error: 'INPUT_ERROR', message: '--name is required' }));
24
+ }
25
+ else {
26
+ console.error(chalk.red('Error: --name is required.'));
27
+ }
28
+ process.exit(EXIT_FAIL);
29
+ }
30
+ try {
31
+ const findings = [];
32
+ // 1. Check for Reinvention
33
+ const indexPath = getDefaultIndexPath(cwd);
34
+ const index = await loadPatternIndex(indexPath);
35
+ if (index) {
36
+ const matcher = new PatternMatcher(index);
37
+ const matchResult = await matcher.match({ name: patternName, type, intent });
38
+ if (matchResult.status === 'FOUND_SIMILAR') {
39
+ findings.push({
40
+ level: 'error',
41
+ category: 'reinvention',
42
+ message: `Similar pattern already exists: "${matchResult.matches[0].pattern.name}" in ${matchResult.matches[0].pattern.file}`,
43
+ suggestion: matchResult.suggestion,
44
+ });
45
+ }
46
+ }
47
+ else {
48
+ findings.push({
49
+ level: 'info',
50
+ category: 'index_missing',
51
+ message: 'Pattern index not found. Run `rigour index` to enable reinvention detection.',
52
+ });
53
+ }
54
+ // 2. Check for Staleness / Anti-patterns
55
+ const detector = new StalenessDetector(cwd);
56
+ const staleness = await detector.checkStaleness(`${type || 'function'} ${patternName} {}`);
57
+ if (staleness.status !== 'FRESH') {
58
+ for (const issue of staleness.issues) {
59
+ findings.push({
60
+ level: 'warning',
61
+ category: 'staleness',
62
+ message: issue.reason,
63
+ suggestion: issue.replacement,
64
+ });
65
+ }
66
+ }
67
+ // 3. Security / CVE check (when intent suggests imports)
68
+ if (intent && intent.includes('import')) {
69
+ const security = new SecurityDetector(cwd);
70
+ const audit = await security.runAudit();
71
+ const relatedVulns = audit.vulnerabilities.filter((v) => patternName.toLowerCase().includes(v.packageName.toLowerCase()) ||
72
+ intent.toLowerCase().includes(v.packageName.toLowerCase()));
73
+ if (relatedVulns.length > 0) {
74
+ for (const v of relatedVulns) {
75
+ findings.push({
76
+ level: 'error',
77
+ category: 'security',
78
+ message: `[${v.severity.toUpperCase()}] ${v.packageName}: ${v.title}`,
79
+ suggestion: v.url,
80
+ });
81
+ }
82
+ }
83
+ }
84
+ // Determine overall status
85
+ const hasErrors = findings.some(f => f.level === 'error');
86
+ const hasWarnings = findings.some(f => f.level === 'warning');
87
+ const status = hasErrors ? 'FAIL' : 'PASS';
88
+ // Determine recommendation
89
+ let recommendation = 'Proceed with implementation.';
90
+ if (findings.some(f => f.category === 'reinvention')) {
91
+ recommendation = 'STOP and REUSE the existing pattern. Do not create a duplicate.';
92
+ }
93
+ else if (findings.some(f => f.category === 'security')) {
94
+ recommendation = 'STOP and update dependencies or find an alternative.';
95
+ }
96
+ else if (hasWarnings) {
97
+ recommendation = 'Proceed with caution, addressing the warnings above.';
98
+ }
99
+ // JSON output
100
+ if (options.json) {
101
+ const jsonOutput = JSON.stringify({
102
+ status,
103
+ pattern: patternName,
104
+ type: type || 'function',
105
+ intent: intent || '',
106
+ findings,
107
+ recommendation,
108
+ }, null, 2);
109
+ process.stdout.write(jsonOutput + '\n', () => {
110
+ process.exit(status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
111
+ });
112
+ return;
113
+ }
114
+ // Human-readable output
115
+ console.log(chalk.bold(`\nPattern Check: ${patternName}\n`));
116
+ if (findings.length === 0) {
117
+ console.log(chalk.green(`✅ Pattern "${patternName}" is fresh, secure, and unique to the codebase.\n`));
118
+ }
119
+ else {
120
+ for (const f of findings) {
121
+ const icon = f.level === 'error' ? chalk.red('🚨') :
122
+ f.level === 'warning' ? chalk.yellow('⚠️') :
123
+ chalk.dim('ℹ️');
124
+ console.log(`${icon} [${f.category.toUpperCase()}] ${f.message}`);
125
+ if (f.suggestion) {
126
+ console.log(chalk.cyan(` → ${f.suggestion}`));
127
+ }
128
+ }
129
+ console.log('');
130
+ }
131
+ console.log(chalk.bold(`Recommendation: ${recommendation}\n`));
132
+ process.exit(status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
133
+ }
134
+ catch (error) {
135
+ if (options.json) {
136
+ console.log(JSON.stringify({ error: 'INTERNAL_ERROR', message: error.message }));
137
+ }
138
+ else {
139
+ console.error(chalk.red(`Internal error: ${error.message}`));
140
+ }
141
+ process.exit(EXIT_INTERNAL_ERROR);
142
+ }
143
+ }
@@ -227,7 +227,21 @@ export function printClosing(cinematic) {
227
227
  console.log(chalk.white(' $ npx @rigour-labs/cli check'));
228
228
  console.log(chalk.white(' $ npx @rigour-labs/cli hooks init'));
229
229
  console.log('');
230
- console.log(chalk.dim('GitHub: https://github.com/rigour-labs/rigour'));
230
+ // ── Rigovo Ecosystem ──
231
+ console.log(divider);
232
+ console.log(chalk.bold('Rigovo Ecosystem — AI-Native Engineering Platform\n'));
233
+ console.log(chalk.bold(' Rigour'));
234
+ console.log(chalk.dim(' Quality gates for AI-generated code (this tool)'));
235
+ console.log(chalk.dim(' 27+ deterministic gates + local LLM deep analysis'));
236
+ console.log(chalk.white(' https://github.com/rigour-labs/rigour\n'));
237
+ console.log(chalk.bold(' Rigovo HR'));
238
+ console.log(chalk.dim(' AI-powered technical hiring — Maya AI interviewer'));
239
+ console.log(chalk.dim(' 15-signal verification, job success prediction, evidence reports'));
240
+ console.log(chalk.white(' https://rigovo.com\n'));
241
+ console.log(chalk.bold(' Rigovo Virtual Team'));
242
+ console.log(chalk.dim(' Multi-agent software delivery with deterministic quality gates'));
243
+ console.log(chalk.dim(' Planning → Coding → Review → QA → Security → DevOps pipeline'));
244
+ console.log(chalk.white(' https://github.com/rigovo/rigovo-virtual-team\n'));
231
245
  console.log(chalk.dim('Docs: https://docs.rigour.run'));
232
246
  console.log(chalk.dim('Paper: https://zenodo.org/records/18673564\n'));
233
247
  console.log(chalk.dim.italic('If this saved you from a bad commit, star the repo ⭐'));
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
- import { GateRunner, ConfigSchema } from '@rigour-labs/core';
5
+ import { GateRunner, ConfigSchema, IncrementalCache, FileScanner } from '@rigour-labs/core';
6
6
  import { recordScore } from '@rigour-labs/core';
7
7
  import { pause, typewrite } from './demo-helpers.js';
8
8
  import { simulateCodeWrite, simulateHookCatch, renderScoreBar, renderTrendChart, displayGateResults, } from './demo-display.js';
@@ -192,6 +192,27 @@ export async function handleRequest(req: express.Request, res: express.Response)
192
192
  console.log(chalk.green.bold(' All issues resolved!'));
193
193
  }
194
194
  console.log('');
195
+ // ── Incremental Cache Demo ──
196
+ // Save cache after the rescan, then run a third scan to show cache hit
197
+ try {
198
+ const allFiles = await FileScanner.findFiles({ cwd: demoDir, ignore: config.ignore });
199
+ const cache = new IncrementalCache(demoDir);
200
+ await cache.save(allFiles, configContent, report2);
201
+ await pause(300, options);
202
+ console.log(chalk.bold.blue('Running scan again (no changes)...\n'));
203
+ await pause(200, options);
204
+ const cacheStart = Date.now();
205
+ const cacheResult = await cache.check(allFiles, configContent);
206
+ const cacheMs = Date.now() - cacheStart;
207
+ if (cacheResult.hit) {
208
+ console.log(chalk.green.bold(` ⚡ Incremental cache hit — ${cacheMs}ms (no files changed)`));
209
+ console.log(chalk.dim(` Skipped all 27+ gates. Same result returned instantly.`));
210
+ console.log(chalk.dim(` Use --no-cache to force a full re-scan.\n`));
211
+ }
212
+ }
213
+ catch {
214
+ // Cache demo is best-effort
215
+ }
195
216
  }
196
217
  catch (error) {
197
218
  const msg = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,14 @@
1
+ export interface ReviewOptions {
2
+ json?: boolean;
3
+ ci?: boolean;
4
+ config?: string;
5
+ diff?: string;
6
+ files?: string;
7
+ deep?: boolean;
8
+ pro?: boolean;
9
+ apiKey?: string;
10
+ provider?: string;
11
+ apiBaseUrl?: string;
12
+ modelName?: string;
13
+ }
14
+ export declare function reviewCommand(cwd: string, options?: ReviewOptions): Promise<void>;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Code Review CLI Command
3
+ *
4
+ * Wraps the same core logic as the MCP `rigour_review` tool.
5
+ * Parses a git diff (from stdin or --diff flag), runs quality gates
6
+ * on changed files, and filters failures to modified lines only.
7
+ *
8
+ * Usage:
9
+ * git diff | rigour review --json
10
+ * rigour review --diff path/to/diff.patch --json
11
+ * rigour review --json --files src/a.ts,src/b.ts (stdin diff)
12
+ */
13
+ import fs from 'fs-extra';
14
+ import path from 'path';
15
+ import chalk from 'chalk';
16
+ import yaml from 'yaml';
17
+ import { GateRunner, ConfigSchema, resolveDeepOptions } from '@rigour-labs/core';
18
+ const EXIT_PASS = 0;
19
+ const EXIT_FAIL = 1;
20
+ const EXIT_CONFIG_ERROR = 2;
21
+ const EXIT_INTERNAL_ERROR = 3;
22
+ /**
23
+ * Parse unified diff into a mapping of file → modified line numbers.
24
+ * Same logic as MCP's parseDiff utility.
25
+ */
26
+ function parseDiff(diff) {
27
+ const lines = diff.split('\n');
28
+ const mapping = {};
29
+ let currentFile = '';
30
+ let currentLine = 0;
31
+ for (const line of lines) {
32
+ if (line.startsWith('+++ b/')) {
33
+ currentFile = line.slice(6);
34
+ mapping[currentFile] = new Set();
35
+ }
36
+ else if (line.startsWith('@@')) {
37
+ const match = line.match(/\+(\d+)/);
38
+ if (match) {
39
+ currentLine = parseInt(match[1], 10);
40
+ }
41
+ }
42
+ else if (line.startsWith('+') && !line.startsWith('+++')) {
43
+ if (currentFile) {
44
+ mapping[currentFile].add(currentLine);
45
+ }
46
+ currentLine++;
47
+ }
48
+ else if (!line.startsWith('-')) {
49
+ currentLine++;
50
+ }
51
+ }
52
+ return mapping;
53
+ }
54
+ async function readStdin() {
55
+ if (process.stdin.isTTY)
56
+ return '';
57
+ const chunks = [];
58
+ for await (const chunk of process.stdin) {
59
+ chunks.push(chunk);
60
+ }
61
+ return Buffer.concat(chunks).toString('utf-8');
62
+ }
63
+ export async function reviewCommand(cwd, options = {}) {
64
+ // Load diff from --diff file, or stdin
65
+ let diffContent = '';
66
+ if (options.diff) {
67
+ const diffPath = path.resolve(cwd, options.diff);
68
+ if (!(await fs.pathExists(diffPath))) {
69
+ if (options.json) {
70
+ console.log(JSON.stringify({ error: 'INPUT_ERROR', message: `Diff file not found: ${diffPath}` }));
71
+ }
72
+ else {
73
+ console.error(chalk.red(`Error: Diff file not found: ${diffPath}`));
74
+ }
75
+ process.exit(EXIT_CONFIG_ERROR);
76
+ }
77
+ diffContent = await fs.readFile(diffPath, 'utf-8');
78
+ }
79
+ else {
80
+ diffContent = await readStdin();
81
+ }
82
+ if (!diffContent.trim()) {
83
+ if (options.json) {
84
+ console.log(JSON.stringify({ status: 'PASS', score: 100, failures: [], message: 'No diff provided' }));
85
+ }
86
+ else {
87
+ console.log(chalk.green('No diff provided — nothing to review.'));
88
+ }
89
+ process.exit(EXIT_PASS);
90
+ }
91
+ // Load config
92
+ const configPath = options.config ? path.resolve(cwd, options.config) : path.join(cwd, 'rigour.yml');
93
+ if (!(await fs.pathExists(configPath))) {
94
+ if (options.json) {
95
+ console.log(JSON.stringify({ error: 'CONFIG_ERROR', message: `Config file not found: ${configPath}` }));
96
+ }
97
+ else {
98
+ console.error(chalk.red(`Error: Config file not found at ${configPath}. Run \`rigour init\` first.`));
99
+ }
100
+ process.exit(EXIT_CONFIG_ERROR);
101
+ }
102
+ try {
103
+ const configContent = await fs.readFile(configPath, 'utf-8');
104
+ const config = ConfigSchema.parse(yaml.parse(configContent));
105
+ // Parse diff into file→line mapping
106
+ const diffMapping = parseDiff(diffContent);
107
+ const explicitFiles = options.files ? options.files.split(',').map(f => f.trim()) : undefined;
108
+ const targetFiles = explicitFiles || Object.keys(diffMapping);
109
+ if (targetFiles.length === 0) {
110
+ if (options.json) {
111
+ console.log(JSON.stringify({ status: 'PASS', score: 100, failures: [] }));
112
+ }
113
+ else {
114
+ console.log(chalk.green('No changed files detected in diff.'));
115
+ }
116
+ process.exit(EXIT_PASS);
117
+ }
118
+ const isDeep = !!options.deep || !!options.pro || !!options.apiKey;
119
+ const isSilent = !!options.ci || !!options.json;
120
+ if (!isSilent) {
121
+ console.log(chalk.blue(`Reviewing ${targetFiles.length} file(s)...`));
122
+ if (isDeep)
123
+ console.log(chalk.blue.bold('Deep analysis enabled.\n'));
124
+ }
125
+ const runner = new GateRunner(config);
126
+ // Build deep options if enabled
127
+ let deepOpts;
128
+ if (isDeep) {
129
+ const resolved = resolveDeepOptions({
130
+ apiKey: options.apiKey,
131
+ provider: options.provider,
132
+ apiBaseUrl: options.apiBaseUrl,
133
+ modelName: options.modelName,
134
+ });
135
+ const hasApiKey = !!resolved.apiKey;
136
+ deepOpts = {
137
+ enabled: true,
138
+ pro: !!options.pro,
139
+ apiKey: resolved.apiKey,
140
+ provider: hasApiKey ? (resolved.provider || 'claude') : 'local',
141
+ apiBaseUrl: resolved.apiBaseUrl,
142
+ modelName: resolved.modelName,
143
+ };
144
+ }
145
+ const report = await runner.run(cwd, targetFiles, deepOpts);
146
+ // Filter failures to only those on changed lines (or global gate failures)
147
+ const filteredFailures = report.failures.filter(failure => {
148
+ if (!failure.files || failure.files.length === 0)
149
+ return true;
150
+ return failure.files.some(file => {
151
+ const fileModifiedLines = diffMapping[file];
152
+ if (!fileModifiedLines)
153
+ return false;
154
+ if (failure.line !== undefined)
155
+ return fileModifiedLines.has(failure.line);
156
+ return true;
157
+ });
158
+ });
159
+ const status = filteredFailures.length > 0 ? 'FAIL' : 'PASS';
160
+ // JSON output
161
+ if (options.json) {
162
+ const jsonOutput = JSON.stringify({
163
+ status,
164
+ score: report.stats.score,
165
+ ai_health_score: report.stats.ai_health_score,
166
+ structural_score: report.stats.structural_score,
167
+ total_failures: report.failures.length,
168
+ filtered_failures: filteredFailures.length,
169
+ failures: filteredFailures.map(f => ({
170
+ id: f.id,
171
+ gate: f.title,
172
+ severity: f.severity || 'medium',
173
+ provenance: f.provenance || 'traditional',
174
+ message: f.details,
175
+ file: f.files?.[0] || '',
176
+ line: f.line || 1,
177
+ suggestion: f.hint,
178
+ })),
179
+ }, null, 2);
180
+ process.stdout.write(jsonOutput + '\n', () => {
181
+ process.exit(status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
182
+ });
183
+ return;
184
+ }
185
+ // CI output
186
+ if (options.ci) {
187
+ const scoreStr = report.stats.score !== undefined ? ` (${report.stats.score}/100)` : '';
188
+ if (status === 'PASS') {
189
+ console.log(`PASS${scoreStr}`);
190
+ }
191
+ else {
192
+ console.log(`FAIL: ${filteredFailures.length} violation(s) on changed lines${scoreStr}`);
193
+ for (const f of filteredFailures) {
194
+ const sev = (f.severity || 'medium').toUpperCase();
195
+ console.log(` - [${sev}] ${f.files?.[0] || ''}:${f.line || '?'} ${f.title}`);
196
+ }
197
+ }
198
+ process.exit(status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
199
+ }
200
+ // Human-readable output
201
+ if (status === 'PASS') {
202
+ console.log(chalk.green.bold('\n✔ PASS — No quality issues on changed lines.\n'));
203
+ }
204
+ else {
205
+ console.log(chalk.red.bold(`\n✘ FAIL — ${filteredFailures.length} issue(s) on changed lines.\n`));
206
+ for (const f of filteredFailures) {
207
+ const sev = (f.severity || 'medium').toUpperCase();
208
+ console.log(` ${chalk.red(`[${sev}]`)} ${f.files?.[0] || '?'}:${f.line || '?'}`);
209
+ console.log(` ${f.title}`);
210
+ if (f.hint)
211
+ console.log(chalk.cyan(` → ${f.hint}`));
212
+ console.log('');
213
+ }
214
+ if (report.failures.length > filteredFailures.length) {
215
+ console.log(chalk.dim(` (${report.failures.length - filteredFailures.length} additional issue(s) on unchanged lines were excluded)\n`));
216
+ }
217
+ }
218
+ process.exit(status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
219
+ }
220
+ catch (error) {
221
+ if (error.name === 'ZodError') {
222
+ if (options.json) {
223
+ console.log(JSON.stringify({ error: 'CONFIG_ERROR', details: error.issues }));
224
+ }
225
+ else {
226
+ console.error(chalk.red('Invalid rigour.yml configuration:'));
227
+ error.issues.forEach((issue) => {
228
+ console.error(chalk.red(` • ${issue.path.join('.')}: ${issue.message}`));
229
+ });
230
+ }
231
+ process.exit(EXIT_CONFIG_ERROR);
232
+ }
233
+ if (options.json) {
234
+ console.log(JSON.stringify({ error: 'INTERNAL_ERROR', message: error.message }));
235
+ }
236
+ else {
237
+ console.error(chalk.red(`Internal error: ${error.message}`));
238
+ }
239
+ process.exit(EXIT_INTERNAL_ERROR);
240
+ }
241
+ }
@@ -0,0 +1,5 @@
1
+ export interface SecurityAuditOptions {
2
+ json?: boolean;
3
+ ci?: boolean;
4
+ }
5
+ export declare function securityAuditCommand(cwd: string, options?: SecurityAuditOptions): Promise<void>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Security Audit CLI Command
3
+ *
4
+ * Wraps the same core logic as the MCP `rigour_security_audit` tool.
5
+ * Runs CVE scanning against project dependencies.
6
+ *
7
+ * Usage:
8
+ * rigour security-audit
9
+ * rigour security-audit --json
10
+ */
11
+ import chalk from 'chalk';
12
+ import { SecurityDetector } from '@rigour-labs/core/pattern-index';
13
+ const EXIT_PASS = 0;
14
+ const EXIT_FAIL = 1;
15
+ const EXIT_INTERNAL_ERROR = 3;
16
+ export async function securityAuditCommand(cwd, options = {}) {
17
+ try {
18
+ const security = new SecurityDetector(cwd);
19
+ if (options.json) {
20
+ // For JSON mode, get the raw audit data
21
+ const audit = await security.runAudit();
22
+ const status = audit.vulnerabilities.length > 0 ? 'FAIL' : 'PASS';
23
+ const jsonOutput = JSON.stringify({
24
+ status,
25
+ total_vulnerabilities: audit.vulnerabilities.length,
26
+ vulnerabilities: audit.vulnerabilities.map((v) => ({
27
+ package: v.packageName,
28
+ severity: v.severity,
29
+ title: v.title,
30
+ url: v.url,
31
+ fixAvailable: v.fixAvailable,
32
+ })),
33
+ }, null, 2);
34
+ process.stdout.write(jsonOutput + '\n', () => {
35
+ process.exit(status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
36
+ });
37
+ return;
38
+ }
39
+ // Human-readable: delegate to SecurityDetector's formatted summary
40
+ const summary = await security.getSecuritySummary();
41
+ if (options.ci) {
42
+ // CI mode: compact
43
+ console.log(summary.includes('No known vulnerabilities') ? 'PASS' : 'FAIL');
44
+ console.log(summary);
45
+ const hasVulns = !summary.includes('No known vulnerabilities');
46
+ process.exit(hasVulns ? EXIT_FAIL : EXIT_PASS);
47
+ }
48
+ console.log(chalk.bold('\nSecurity Audit\n'));
49
+ console.log(summary);
50
+ console.log('');
51
+ const hasVulns = !summary.includes('No known vulnerabilities');
52
+ process.exit(hasVulns ? EXIT_FAIL : EXIT_PASS);
53
+ }
54
+ catch (error) {
55
+ if (options.json) {
56
+ console.log(JSON.stringify({ error: 'INTERNAL_ERROR', message: error.message }));
57
+ }
58
+ else {
59
+ console.error(chalk.red(`Internal error: ${error.message}`));
60
+ }
61
+ process.exit(EXIT_INTERNAL_ERROR);
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "5.1.2",
3
+ "version": "5.2.1",
4
4
  "description": "CLI quality gates for AI-generated code. Forces AI agents (Claude, Cursor, Copilot) to meet strict engineering standards with PASS/FAIL enforcement.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -44,7 +44,7 @@
44
44
  "inquirer": "9.2.16",
45
45
  "ora": "^8.0.1",
46
46
  "yaml": "^2.8.2",
47
- "@rigour-labs/core": "5.1.2"
47
+ "@rigour-labs/core": "5.2.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/fs-extra": "^11.0.4",