@rigour-labs/cli 5.2.0 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
+ }
@@ -167,28 +167,6 @@ export async function checkCommand(cwd, files = [], options = {}) {
167
167
  // Cache save is best-effort
168
168
  }
169
169
  }
170
- // Persist to SQLite if deep analysis was used
171
- if (isDeep) {
172
- try {
173
- const { openDatabase, insertScan, insertFindings } = await import('@rigour-labs/core');
174
- const db = await openDatabase();
175
- if (db) {
176
- const repoName = path.basename(cwd);
177
- const scanId = await insertScan(db, repoName, report, {
178
- deepTier: report.stats.deep?.tier || (options.pro ? 'deep' : (resolvedDeepMode?.isLocal ? 'lite' : 'cloud')),
179
- deepModel: report.stats.deep?.model,
180
- });
181
- await insertFindings(db, scanId, report.failures);
182
- await db.close();
183
- }
184
- }
185
- catch (dbError) {
186
- // SQLite persistence is best-effort — log but don't fail
187
- if (process.env.RIGOUR_DEBUG) {
188
- console.error(`[rigour] SQLite persistence failed: ${dbError.message}`);
189
- }
190
- }
191
- }
192
170
  await logStudioEvent(cwd, {
193
171
  type: "tool_response",
194
172
  requestId,
@@ -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
+ }
@@ -4,7 +4,7 @@ import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
5
  import { globby } from 'globby';
6
6
  import { GateRunner, ConfigSchema, DiscoveryService, FixPacketService, recordScore, getScoreTrend, } from '@rigour-labs/core';
7
- import { buildDeepOpts, persistDeepResults, renderDeepScanResults } from './scan-deep.js';
7
+ import { buildDeepOpts, renderDeepScanResults } from './scan-deep.js';
8
8
  // Exit codes per spec
9
9
  const EXIT_PASS = 0;
10
10
  const EXIT_FAIL = 1;
@@ -59,7 +59,6 @@ export async function scanCommand(cwd, files = [], options = {}) {
59
59
  const report = await runner.run(cwd, files.length > 0 ? files : undefined, deepOpts);
60
60
  await writeReportArtifacts(cwd, report, scanCtx.config);
61
61
  await writeLastScanJson(cwd, scanCtx, stackSignals, report, isDeep);
62
- persistDeepResults(cwd, report, isDeep, options);
63
62
  if (options.json) {
64
63
  outputJson(scanCtx, stackSignals, report);
65
64
  return;
@@ -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.2.0",
3
+ "version": "5.2.2",
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.2.0"
47
+ "@rigour-labs/core": "5.2.2"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/fs-extra": "^11.0.4",