@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 +57 -0
- package/dist/commands/check-pattern.d.ts +7 -0
- package/dist/commands/check-pattern.js +143 -0
- package/dist/commands/check.js +0 -22
- package/dist/commands/review.d.ts +14 -0
- package/dist/commands/review.js +241 -0
- package/dist/commands/scan.js +1 -2
- package/dist/commands/security-audit.d.ts +5 -0
- package/dist/commands/security-audit.js +63 -0
- package/package.json +2 -2
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,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
|
+
}
|
package/dist/commands/check.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/scan.js
CHANGED
|
@@ -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,
|
|
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,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.
|
|
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.
|
|
47
|
+
"@rigour-labs/core": "5.2.2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/fs-extra": "^11.0.4",
|