@rigour-labs/cli 3.0.6 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { studioCommand } from './commands/studio.js';
12
12
  import { exportAuditCommand } from './commands/export-audit.js';
13
13
  import { demoCommand } from './commands/demo.js';
14
14
  import { hooksInitCommand } from './commands/hooks.js';
15
+ import { settingsShowCommand, settingsSetKeyCommand, settingsRemoveKeyCommand, settingsSetCommand, settingsGetCommand, settingsResetCommand, settingsPathCommand } from './commands/settings.js';
15
16
  import { checkForUpdates } from './utils/version.js';
16
17
  import chalk from 'chalk';
17
18
  const CLI_VERSION = '2.0.0';
@@ -59,13 +60,24 @@ program
59
60
  .option('--json', 'Output report in JSON format')
60
61
  .option('-i, --interactive', 'Run in interactive mode with rich output')
61
62
  .option('-c, --config <path>', 'Path to custom rigour.yml configuration')
63
+ .option('--deep', 'Enable deep LLM-powered analysis (local, 350MB one-time download)')
64
+ .option('--pro', 'Use larger model for deep analysis (900MB, higher quality)')
65
+ .option('-k, --api-key <key>', 'Use cloud API key instead of local model (BYOK)')
66
+ .option('--provider <name>', 'Cloud provider: claude, openai, gemini, groq, mistral, together, deepseek, ollama, or any OpenAI-compatible')
67
+ .option('--api-base-url <url>', 'Custom API base URL (for self-hosted or proxy endpoints)')
68
+ .option('--model-name <name>', 'Override cloud model name')
69
+ .option('--agents <count>', 'Number of parallel agents for deep scan (cloud-only, default: 1)', '1')
62
70
  .addHelpText('after', `
63
71
  Examples:
64
- $ rigour check # Run standard check
65
- $ rigour check ./src # Check only the src directory
66
- $ rigour check ./src/app.ts # Check only app.ts
67
- $ rigour check --interactive # Run with rich, interactive output
68
- $ rigour check --ci # Run in CI environment
72
+ $ rigour check # AST only. Instant. Free.
73
+ $ rigour check --deep # AST + local LLM (350MB one-time)
74
+ $ rigour check --deep --pro # AST + larger local LLM (900MB)
75
+ $ rigour check --deep -k sk-ant-xxx # AST + Claude API (BYOK)
76
+ $ rigour check --deep -k gsk_xxx --provider groq # Use Groq
77
+ $ rigour check --deep -k xxx --provider ollama # Use local Ollama
78
+ $ rigour check --deep -k xxx --provider custom --api-base-url http://my-server/v1 # Any endpoint
79
+ $ rigour check ./src --deep # Deep on specific directory
80
+ $ rigour check --ci # CI environment
69
81
  `)
70
82
  .action(async (files, options) => {
71
83
  await checkCommand(process.cwd(), files, options);
@@ -184,6 +196,44 @@ Examples:
184
196
  .action(async (options) => {
185
197
  await hooksInitCommand(process.cwd(), options);
186
198
  });
199
+ // Settings management (like Claude Code's settings.json)
200
+ const settingsCmd = program
201
+ .command('settings')
202
+ .description('Manage global settings (~/.rigour/settings.json) — API keys, providers, defaults');
203
+ settingsCmd
204
+ .command('show', { isDefault: true })
205
+ .description('Show current settings')
206
+ .action(async () => { await settingsShowCommand(); });
207
+ settingsCmd
208
+ .command('set-key')
209
+ .description('Add or update an API key for a provider')
210
+ .argument('<provider>', 'Provider name: anthropic, openai, groq, deepseek, mistral, together, gemini, ollama')
211
+ .argument('<key>', 'API key')
212
+ .action(async (provider, key) => { await settingsSetKeyCommand(provider, key); });
213
+ settingsCmd
214
+ .command('remove-key')
215
+ .description('Remove an API key for a provider')
216
+ .argument('<provider>', 'Provider name')
217
+ .action(async (provider) => { await settingsRemoveKeyCommand(provider); });
218
+ settingsCmd
219
+ .command('set')
220
+ .description('Set a configuration value (dot-notation)')
221
+ .argument('<key>', 'Setting key (e.g., deep.defaultProvider, cli.verboseOutput)')
222
+ .argument('<value>', 'Setting value')
223
+ .action(async (key, value) => { await settingsSetCommand(key, value); });
224
+ settingsCmd
225
+ .command('get')
226
+ .description('Get a configuration value')
227
+ .argument('<key>', 'Setting key')
228
+ .action(async (key) => { await settingsGetCommand(key); });
229
+ settingsCmd
230
+ .command('reset')
231
+ .description('Reset all settings to defaults')
232
+ .action(async () => { await settingsResetCommand(); });
233
+ settingsCmd
234
+ .command('path')
235
+ .description('Show settings file path')
236
+ .action(async () => { await settingsPathCommand(); });
187
237
  // Check for updates before parsing (non-blocking)
188
238
  (async () => {
189
239
  try {
@@ -3,5 +3,12 @@ export interface CheckOptions {
3
3
  json?: boolean;
4
4
  interactive?: boolean;
5
5
  config?: string;
6
+ deep?: boolean;
7
+ pro?: boolean;
8
+ apiKey?: string;
9
+ provider?: string;
10
+ apiBaseUrl?: string;
11
+ modelName?: string;
12
+ agents?: string;
6
13
  }
7
14
  export declare function checkCommand(cwd: string, files?: string[], options?: CheckOptions): Promise<void>;
@@ -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, recordScore, getScoreTrend } from '@rigour-labs/core';
5
+ import { GateRunner, ConfigSchema, recordScore, getScoreTrend, resolveDeepOptions } from '@rigour-labs/core';
6
6
  import inquirer from 'inquirer';
7
7
  import { randomUUID } from 'crypto';
8
8
  // Exit codes per spec
@@ -42,8 +42,15 @@ export async function checkCommand(cwd, files = [], options = {}) {
42
42
  const configContent = await fs.readFile(configPath, 'utf-8');
43
43
  const rawConfig = yaml.parse(configContent);
44
44
  const config = ConfigSchema.parse(rawConfig);
45
- if (!options.ci && !options.json) {
46
- console.log(chalk.blue('Running Rigour checks...\n'));
45
+ const isDeep = !!options.deep || !!options.pro || !!options.apiKey;
46
+ const isSilent = !!options.ci || !!options.json;
47
+ if (!isSilent) {
48
+ if (isDeep) {
49
+ console.log(chalk.blue.bold('Running Rigour checks + deep analysis...\n'));
50
+ }
51
+ else {
52
+ console.log(chalk.blue('Running Rigour checks...\n'));
53
+ }
47
54
  }
48
55
  const runner = new GateRunner(config);
49
56
  const requestId = randomUUID();
@@ -51,14 +58,63 @@ export async function checkCommand(cwd, files = [], options = {}) {
51
58
  type: "tool_call",
52
59
  requestId,
53
60
  tool: "rigour_check",
54
- arguments: { files }
61
+ arguments: { files, deep: isDeep }
55
62
  });
56
- const report = await runner.run(cwd, files.length > 0 ? files : undefined);
63
+ // Build deep options if enabled
64
+ // Merges CLI flags with ~/.rigour/settings.json (CLI flags win)
65
+ let deepOpts;
66
+ if (isDeep) {
67
+ const resolved = resolveDeepOptions({
68
+ apiKey: options.apiKey,
69
+ provider: options.provider,
70
+ apiBaseUrl: options.apiBaseUrl,
71
+ modelName: options.modelName,
72
+ });
73
+ // If settings.json provided an API key but user didn't pass --deep explicitly,
74
+ // treat it as cloud mode
75
+ const hasApiKey = !!resolved.apiKey;
76
+ const agentCount = Math.max(1, parseInt(options.agents || '1', 10) || 1);
77
+ deepOpts = {
78
+ enabled: true,
79
+ pro: !!options.pro,
80
+ apiKey: resolved.apiKey,
81
+ provider: hasApiKey ? (resolved.provider || 'claude') : 'local',
82
+ apiBaseUrl: resolved.apiBaseUrl,
83
+ modelName: resolved.modelName,
84
+ agents: agentCount > 1 ? agentCount : undefined,
85
+ onProgress: isSilent ? undefined : (msg) => {
86
+ process.stderr.write(msg + '\n');
87
+ },
88
+ };
89
+ }
90
+ const report = await runner.run(cwd, files.length > 0 ? files : undefined, deepOpts);
57
91
  // Write machine report
58
92
  const reportPath = path.join(cwd, config.output.report_path);
59
93
  await fs.writeJson(reportPath, report, { spaces: 2 });
60
94
  // Record score for trend tracking
61
95
  recordScore(cwd, report);
96
+ // Persist to SQLite if deep analysis was used
97
+ if (isDeep) {
98
+ try {
99
+ const { openDatabase, insertScan, insertFindings } = await import('@rigour-labs/core');
100
+ const db = openDatabase();
101
+ if (db) {
102
+ const repoName = path.basename(cwd);
103
+ const scanId = insertScan(db, repoName, report, {
104
+ deepTier: options.pro ? 'pro' : (options.apiKey ? 'cloud' : 'deep'),
105
+ deepModel: report.stats.deep?.model,
106
+ });
107
+ insertFindings(db, scanId, report.failures);
108
+ db.close();
109
+ }
110
+ }
111
+ catch (dbError) {
112
+ // SQLite persistence is best-effort — log but don't fail
113
+ if (process.env.RIGOUR_DEBUG) {
114
+ console.error(`[rigour] SQLite persistence failed: ${dbError.message}`);
115
+ }
116
+ }
117
+ }
62
118
  await logStudioEvent(cwd, {
63
119
  type: "tool_response",
64
120
  requestId,
@@ -103,62 +159,14 @@ export async function checkCommand(cwd, files = [], options = {}) {
103
159
  await interactiveMode(report, config);
104
160
  process.exit(EXIT_FAIL);
105
161
  }
106
- // Normal human-readable output
107
- if (report.status === 'PASS') {
108
- console.log(chalk.green.bold('āœ” PASS - All quality gates satisfied.'));
162
+ // ─── HUMAN-READABLE OUTPUT (with deep analysis dopamine engineering) ───
163
+ if (isDeep) {
164
+ // Deep analysis output format (from product bible)
165
+ renderDeepOutput(report, config, options);
109
166
  }
110
167
  else {
111
- console.log(chalk.red.bold('✘ FAIL - Quality gate violations found.\n'));
112
- // Score summary line
113
- const stats = report.stats;
114
- const scoreParts = [];
115
- if (stats.score !== undefined)
116
- scoreParts.push(`Score: ${stats.score}/100`);
117
- if (stats.ai_health_score !== undefined)
118
- scoreParts.push(`AI Health: ${stats.ai_health_score}/100`);
119
- if (stats.structural_score !== undefined)
120
- scoreParts.push(`Structural: ${stats.structural_score}/100`);
121
- if (scoreParts.length > 0) {
122
- console.log(chalk.bold(scoreParts.join(' | ')) + '\n');
123
- }
124
- // Severity breakdown
125
- if (stats.severity_breakdown) {
126
- const parts = Object.entries(stats.severity_breakdown)
127
- .filter(([, count]) => count > 0)
128
- .map(([sev, count]) => {
129
- const color = sev === 'critical' ? chalk.red.bold : sev === 'high' ? chalk.red : sev === 'medium' ? chalk.yellow : chalk.dim;
130
- return color(`${sev}: ${count}`);
131
- });
132
- if (parts.length > 0) {
133
- console.log('Severity: ' + parts.join(', ') + '\n');
134
- }
135
- }
136
- // Group failures by provenance
137
- const severityIcon = (s) => {
138
- switch (s) {
139
- case 'critical': return chalk.red.bold('CRIT');
140
- case 'high': return chalk.red('HIGH');
141
- case 'medium': return chalk.yellow('MED ');
142
- case 'low': return chalk.dim('LOW ');
143
- case 'info': return chalk.dim('INFO');
144
- default: return chalk.yellow('MED ');
145
- }
146
- };
147
- for (const failure of report.failures) {
148
- const sev = severityIcon(failure.severity);
149
- const prov = failure.provenance ? chalk.dim(`[${failure.provenance}]`) : '';
150
- console.log(`${sev} ${prov} ${chalk.red(`[${failure.id}]`)} ${failure.title}`);
151
- console.log(chalk.dim(` Details: ${failure.details}`));
152
- if (failure.files && failure.files.length > 0) {
153
- console.log(chalk.dim(' Files:'));
154
- failure.files.forEach((f) => console.log(chalk.dim(` - ${f}`)));
155
- }
156
- if (failure.hint) {
157
- console.log(chalk.cyan(` Hint: ${failure.hint}`));
158
- }
159
- console.log('');
160
- }
161
- console.log(chalk.yellow(`See ${config.output.report_path} for full details.`));
168
+ // Standard AST-only output
169
+ renderStandardOutput(report, config);
162
170
  }
163
171
  // Score trend display
164
172
  const trend = getScoreTrend(cwd);
@@ -171,8 +179,8 @@ export async function checkCommand(cwd, files = [], options = {}) {
171
179
  console.log(trendColor(`\nScore Trend: ${scoresStr} (${trend.direction} ${arrow})`));
172
180
  }
173
181
  // Stats footer
174
- const footerParts = [`Finished in ${report.stats.duration_ms}ms`];
175
- if (report.status === 'PASS' && report.stats.score !== undefined) {
182
+ const footerParts = [`Finished in ${(report.stats.duration_ms / 1000).toFixed(1)}s`];
183
+ if (report.stats.score !== undefined) {
176
184
  footerParts.push(`Score: ${report.stats.score}/100`);
177
185
  }
178
186
  console.log(chalk.dim('\n' + footerParts.join(' | ')));
@@ -200,6 +208,217 @@ export async function checkCommand(cwd, files = [], options = {}) {
200
208
  process.exit(EXIT_INTERNAL_ERROR);
201
209
  }
202
210
  }
211
+ /**
212
+ * Render deep analysis output — enhanced with detailed findings grouped by provenance.
213
+ *
214
+ * Shows:
215
+ * - Score box at top (AI Health, Code Quality, Overall)
216
+ * - Detailed findings table grouped by provenance:
217
+ * - Deep analysis findings (most detailed): severity, category, file:line, description, suggestion
218
+ * - AI drift findings: severity, title, files
219
+ * - Security findings: severity, title, hint
220
+ * - Traditional findings: severity, title
221
+ * - Privacy badge and model info
222
+ * - Summary count at end
223
+ */
224
+ function renderDeepOutput(report, config, options) {
225
+ const stats = report.stats;
226
+ const isLocal = !options.apiKey;
227
+ console.log('');
228
+ if (report.status === 'PASS') {
229
+ console.log(chalk.green.bold(' ✨ All quality gates passed.\n'));
230
+ }
231
+ // Score breakdown — the screenshottable moment
232
+ const aiHealth = stats.ai_health_score ?? 100;
233
+ const codeQuality = stats.code_quality_score ?? stats.structural_score ?? 100;
234
+ const overall = stats.score ?? 100;
235
+ const scoreColor = (score) => score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
236
+ console.log(` ${chalk.bold('AI Health:')} ${scoreColor(aiHealth).bold(aiHealth + '/100')}`);
237
+ console.log(` ${chalk.bold('Code Quality:')} ${scoreColor(codeQuality).bold(codeQuality + '/100')}`);
238
+ console.log(` ${chalk.bold('Overall:')} ${scoreColor(overall).bold(overall + '/100')}`);
239
+ console.log('');
240
+ // Privacy badge — this IS the marketing
241
+ if (isLocal) {
242
+ console.log(chalk.green(' šŸ”’ 100% local. Your code never left this machine.'));
243
+ }
244
+ else {
245
+ console.log(chalk.yellow(` ā˜ļø Code was sent to ${options.provider || 'cloud'} API.`));
246
+ }
247
+ // Deep stats
248
+ if (stats.deep) {
249
+ const tier = stats.deep.tier === 'cloud' ? options.provider || 'cloud' : stats.deep.tier;
250
+ const model = stats.deep.model || 'unknown';
251
+ const inferenceSec = stats.deep.total_ms ? (stats.deep.total_ms / 1000).toFixed(1) + 's' : '';
252
+ console.log(chalk.dim(` Model: ${model} (${tier}) ${inferenceSec}`));
253
+ }
254
+ console.log('');
255
+ // Categorize findings by provenance
256
+ const deepFailures = report.failures.filter((f) => f.provenance === 'deep-analysis');
257
+ const aiDriftFailures = report.failures.filter((f) => f.provenance === 'ai-drift');
258
+ const securityFailures = report.failures.filter((f) => f.provenance === 'security');
259
+ const traditionalFailures = report.failures.filter((f) => f.provenance !== 'deep-analysis' && f.provenance !== 'ai-drift' && f.provenance !== 'security');
260
+ // DEEP ANALYSIS FINDINGS — most detailed
261
+ if (deepFailures.length > 0) {
262
+ console.log(chalk.bold(` ── Deep Analysis Findings (${deepFailures.length} verified) ──\n`));
263
+ for (const failure of deepFailures) {
264
+ const sev = severityIcon(failure.severity);
265
+ const cat = failure.category || failure.id;
266
+ const catLabel = formatCategory(cat);
267
+ // Extract file and line from files array if available
268
+ let fileLocation = '';
269
+ if (failure.files && failure.files.length > 0) {
270
+ fileLocation = failure.files[0];
271
+ }
272
+ // Description: up to 120 chars
273
+ const description = failure.details ? failure.details.substring(0, 120) : failure.title.substring(0, 120);
274
+ const descDisplay = description.length > 120 ? description.substring(0, 117) + '...' : description;
275
+ // Suggestion from hint
276
+ const suggestion = failure.hint || failure.suggestion || '';
277
+ console.log(` ${sev} [${catLabel}] ${fileLocation}`);
278
+ console.log(` ${descDisplay}`);
279
+ if (suggestion) {
280
+ console.log(` → ${suggestion}`);
281
+ }
282
+ // Show confidence and verified status if available
283
+ if (failure.confidence !== undefined || failure.verified !== undefined) {
284
+ const confStr = failure.confidence !== undefined ? ` (${(failure.confidence * 100).toFixed(0)}% conf)` : '';
285
+ const verStr = failure.verified !== undefined ? ` [${failure.verified ? 'verified' : 'unverified'}]` : '';
286
+ console.log(chalk.dim(` ${confStr}${verStr}`));
287
+ }
288
+ console.log('');
289
+ }
290
+ }
291
+ // AI DRIFT FINDINGS
292
+ if (aiDriftFailures.length > 0) {
293
+ console.log(chalk.bold(` ── AI Drift Findings (${aiDriftFailures.length}) ──\n`));
294
+ for (const failure of aiDriftFailures) {
295
+ const sev = severityIcon(failure.severity);
296
+ console.log(` ${sev} ${failure.title}`);
297
+ if (failure.files && failure.files.length > 0) {
298
+ console.log(` Files: ${failure.files.slice(0, 3).join(', ')}${failure.files.length > 3 ? ` +${failure.files.length - 3}` : ''}`);
299
+ }
300
+ console.log('');
301
+ }
302
+ }
303
+ // SECURITY FINDINGS
304
+ if (securityFailures.length > 0) {
305
+ console.log(chalk.bold(` ── Security Findings (${securityFailures.length}) ──\n`));
306
+ for (const failure of securityFailures) {
307
+ const sev = severityIcon(failure.severity);
308
+ console.log(` ${sev} ${failure.title}`);
309
+ if (failure.hint) {
310
+ console.log(chalk.cyan(` Hint: ${failure.hint}`));
311
+ }
312
+ console.log('');
313
+ }
314
+ }
315
+ // TRADITIONAL FINDINGS
316
+ if (traditionalFailures.length > 0) {
317
+ console.log(chalk.bold(` ── Traditional Quality Findings (${traditionalFailures.length}) ──\n`));
318
+ for (const failure of traditionalFailures) {
319
+ const sev = severityIcon(failure.severity);
320
+ const prov = failure.provenance ? chalk.dim(`[${failure.provenance}]`) : '';
321
+ console.log(` ${sev} ${prov} ${failure.title}`);
322
+ console.log('');
323
+ }
324
+ }
325
+ // Summary count at the end
326
+ if (deepFailures.length > 0 || aiDriftFailures.length > 0 || securityFailures.length > 0 || traditionalFailures.length > 0) {
327
+ const summary = [
328
+ deepFailures.length > 0 ? `${deepFailures.length} deep` : null,
329
+ aiDriftFailures.length > 0 ? `${aiDriftFailures.length} ai-drift` : null,
330
+ securityFailures.length > 0 ? `${securityFailures.length} security` : null,
331
+ traditionalFailures.length > 0 ? `${traditionalFailures.length} traditional` : null
332
+ ].filter(Boolean).join(' | ');
333
+ console.log(chalk.dim(` ${summary}`));
334
+ console.log('');
335
+ }
336
+ console.log(chalk.yellow(` See ${config.output.report_path} for full details.`));
337
+ }
338
+ /**
339
+ * Render standard AST-only output (existing behavior).
340
+ */
341
+ function renderStandardOutput(report, config) {
342
+ if (report.status === 'PASS') {
343
+ console.log(chalk.green.bold('āœ” PASS - All quality gates satisfied.'));
344
+ }
345
+ else {
346
+ console.log(chalk.red.bold('✘ FAIL - Quality gate violations found.\n'));
347
+ // Score summary line
348
+ const stats = report.stats;
349
+ const scoreParts = [];
350
+ if (stats.score !== undefined)
351
+ scoreParts.push(`Score: ${stats.score}/100`);
352
+ if (stats.ai_health_score !== undefined)
353
+ scoreParts.push(`AI Health: ${stats.ai_health_score}/100`);
354
+ if (stats.structural_score !== undefined)
355
+ scoreParts.push(`Structural: ${stats.structural_score}/100`);
356
+ if (scoreParts.length > 0) {
357
+ console.log(chalk.bold(scoreParts.join(' | ')) + '\n');
358
+ }
359
+ // Severity breakdown
360
+ if (stats.severity_breakdown) {
361
+ const parts = Object.entries(stats.severity_breakdown)
362
+ .filter(([, count]) => count > 0)
363
+ .map(([sev, count]) => {
364
+ const color = sev === 'critical' ? chalk.red.bold : sev === 'high' ? chalk.red : sev === 'medium' ? chalk.yellow : chalk.dim;
365
+ return color(`${sev}: ${count}`);
366
+ });
367
+ if (parts.length > 0) {
368
+ console.log('Severity: ' + parts.join(', ') + '\n');
369
+ }
370
+ }
371
+ for (const failure of report.failures) {
372
+ const sev = severityIcon(failure.severity);
373
+ const prov = failure.provenance ? chalk.dim(`[${failure.provenance}]`) : '';
374
+ console.log(`${sev} ${prov} ${chalk.red(`[${failure.id}]`)} ${failure.title}`);
375
+ console.log(chalk.dim(` Details: ${failure.details}`));
376
+ if (failure.files && failure.files.length > 0) {
377
+ console.log(chalk.dim(' Files:'));
378
+ failure.files.forEach((f) => console.log(chalk.dim(` - ${f}`)));
379
+ }
380
+ if (failure.hint) {
381
+ console.log(chalk.cyan(` Hint: ${failure.hint}`));
382
+ }
383
+ console.log('');
384
+ }
385
+ console.log(chalk.yellow(`See ${config.output.report_path} for full details.`));
386
+ }
387
+ }
388
+ function severityIcon(s) {
389
+ switch (s) {
390
+ case 'critical': return chalk.red.bold('CRIT');
391
+ case 'high': return chalk.red('HIGH');
392
+ case 'medium': return chalk.yellow('MED ');
393
+ case 'low': return chalk.dim('LOW ');
394
+ case 'info': return chalk.dim('INFO');
395
+ default: return chalk.yellow('MED ');
396
+ }
397
+ }
398
+ function formatCategory(cat) {
399
+ const labels = {
400
+ srp_violation: 'SOLID: Single Responsibility',
401
+ ocp_violation: 'SOLID: Open/Closed',
402
+ lsp_violation: 'SOLID: Liskov Substitution',
403
+ isp_violation: 'SOLID: Interface Segregation',
404
+ dip_violation: 'SOLID: Dependency Inversion',
405
+ dry_violation: 'DRY',
406
+ god_class: 'Pattern: God class',
407
+ god_function: 'Pattern: God function',
408
+ feature_envy: 'Pattern: Feature envy',
409
+ shotgun_surgery: 'Pattern: Shotgun surgery',
410
+ long_params: 'Params',
411
+ data_clump: 'Data clump',
412
+ inappropriate_intimacy: 'Coupling',
413
+ error_inconsistency: 'Error handling',
414
+ empty_catch: 'Empty catch',
415
+ test_quality: 'Test quality',
416
+ code_smell: 'Code smell',
417
+ architecture: 'Architecture',
418
+ language_idiom: 'Idiom',
419
+ };
420
+ return labels[cat] || cat;
421
+ }
203
422
  async function interactiveMode(report, config) {
204
423
  console.clear();
205
424
  console.log(chalk.bold.blue('══ Rigour Interactive Review ══\n'));
@@ -237,6 +456,13 @@ async function interactiveMode(report, config) {
237
456
  if (failure.hint) {
238
457
  console.log(`\n${chalk.bold.cyan('Hint:')} ${failure.hint}`);
239
458
  }
459
+ // Show deep analysis metadata if present
460
+ if (failure.confidence !== undefined) {
461
+ console.log(`\n${chalk.bold('Confidence:')} ${(failure.confidence * 100).toFixed(0)}%`);
462
+ }
463
+ if (failure.verified !== undefined) {
464
+ console.log(`${chalk.bold('Verified:')} ${failure.verified ? chalk.green('Yes') : chalk.red('No')}`);
465
+ }
240
466
  console.log(chalk.dim('\n' + '─'.repeat(40)));
241
467
  await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to return to list...' }]);
242
468
  console.clear();
@@ -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 { DiscoveryService } from '@rigour-labs/core';
5
+ import { DiscoveryService, loadSettings, isModelCached, getModelsDir } from '@rigour-labs/core';
6
6
  import { CODE_QUALITY_RULES, DEBUGGING_RULES, COLLABORATION_RULES, AGNOSTIC_AI_INSTRUCTIONS } from './constants.js';
7
7
  import { hooksInitCommand } from './hooks.js';
8
8
  import { randomUUID } from 'crypto';
@@ -370,6 +370,88 @@ ${ruleContent}`;
370
370
  status: "success",
371
371
  content: [{ type: "text", text: `Rigour Governance Initialized` }]
372
372
  });
373
+ // 5. Auto-prerequisites check
374
+ await checkPrerequisites();
375
+ }
376
+ /**
377
+ * Auto-prerequisites check — runs after init to guide deep analysis setup.
378
+ * Checks settings.json for API keys, local model availability, and prints
379
+ * actionable next steps.
380
+ */
381
+ async function checkPrerequisites() {
382
+ console.log(chalk.bold.cyan('\nšŸ”§ Deep Analysis Prerequisites:'));
383
+ const settings = loadSettings();
384
+ const providers = settings.providers || {};
385
+ const configuredKeys = Object.entries(providers).filter(([_, key]) => !!key);
386
+ // Check 1: API keys in settings.json
387
+ const hasApiKey = configuredKeys.length > 0;
388
+ if (hasApiKey) {
389
+ const providerNames = configuredKeys.map(([name]) => name).join(', ');
390
+ console.log(chalk.green(` āœ” API keys configured: ${providerNames}`));
391
+ }
392
+ else {
393
+ console.log(chalk.yellow(' ā—‹ No API keys configured'));
394
+ }
395
+ // Check 2: Local model availability
396
+ const hasDeepModel = isModelCached('deep');
397
+ const hasProModel = isModelCached('pro');
398
+ if (hasDeepModel || hasProModel) {
399
+ const models = [];
400
+ if (hasDeepModel)
401
+ models.push('deep (350MB)');
402
+ if (hasProModel)
403
+ models.push('pro (900MB)');
404
+ console.log(chalk.green(` āœ” Local models cached: ${models.join(', ')}`));
405
+ }
406
+ else {
407
+ console.log(chalk.yellow(' ā—‹ No local models cached'));
408
+ }
409
+ // Check 3: Sidecar binary
410
+ let hasSidecar = false;
411
+ try {
412
+ const { execSync } = await import('child_process');
413
+ const result = execSync('which llama-cli 2>/dev/null || which rigour-brain 2>/dev/null', { encoding: 'utf-8', timeout: 3000 }).trim();
414
+ hasSidecar = result.length > 0;
415
+ }
416
+ catch {
417
+ // Also check ~/.rigour/bin/
418
+ const binDir = path.join(getModelsDir(), '..', 'bin');
419
+ hasSidecar = fs.existsSync(path.join(binDir, 'rigour-brain')) || fs.existsSync(path.join(binDir, 'llama-cli'));
420
+ }
421
+ if (hasSidecar) {
422
+ console.log(chalk.green(' āœ” Inference binary available'));
423
+ }
424
+ else if (!hasApiKey) {
425
+ console.log(chalk.yellow(' ā—‹ No local inference binary found'));
426
+ }
427
+ // Summary: what can the user do?
428
+ const isReady = hasApiKey || (hasSidecar && (hasDeepModel || hasProModel));
429
+ if (isReady) {
430
+ console.log(chalk.green('\n āœ“ Deep analysis is ready!'));
431
+ if (hasApiKey) {
432
+ const defaultProvider = settings.deep?.defaultProvider || configuredKeys[0]?.[0] || 'unknown';
433
+ console.log(chalk.dim(` Run: rigour check --deep --provider ${defaultProvider}`));
434
+ }
435
+ if (hasSidecar && hasDeepModel) {
436
+ console.log(chalk.dim(' Run: rigour check --deep (100% local, free)'));
437
+ }
438
+ }
439
+ else {
440
+ console.log(chalk.bold.yellow('\n ⚔ Set up deep analysis (optional):'));
441
+ console.log('');
442
+ console.log(chalk.bold(' Option A: Cloud API (Recommended — instant, high quality)'));
443
+ console.log(chalk.dim(' rigour settings set-key anthropic sk-ant-xxx # Claude'));
444
+ console.log(chalk.dim(' rigour settings set-key openai sk-xxx # OpenAI'));
445
+ console.log(chalk.dim(' rigour settings set-key groq gsk_xxx # Groq (fast + free tier)'));
446
+ console.log(chalk.dim(' Then: rigour check --deep'));
447
+ console.log('');
448
+ console.log(chalk.bold(' Option B: 100% Local (Free, private, 350MB download)'));
449
+ console.log(chalk.dim(' rigour check --deep # Auto-downloads model'));
450
+ console.log(chalk.dim(' rigour check --deep --pro # Larger model (900MB)'));
451
+ console.log('');
452
+ console.log(chalk.dim(' Without deep analysis, Rigour still runs 15+ AST-based quality gates.'));
453
+ }
454
+ console.log('');
373
455
  }
374
456
  // Maps detected IDE to hook tool name
375
457
  const IDE_TO_HOOK_TOOL = {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `rigour settings` — manage ~/.rigour/settings.json
3
+ *
4
+ * Like Claude Code's settings.json or Gemini CLI's config.
5
+ * Stores API keys, default provider, multi-agent config, CLI preferences.
6
+ */
7
+ export declare function settingsShowCommand(): Promise<void>;
8
+ export declare function settingsSetKeyCommand(provider: string, apiKey: string): Promise<void>;
9
+ export declare function settingsRemoveKeyCommand(provider: string): Promise<void>;
10
+ export declare function settingsSetCommand(key: string, value: string): Promise<void>;
11
+ export declare function settingsGetCommand(key: string): Promise<void>;
12
+ export declare function settingsResetCommand(): Promise<void>;
13
+ export declare function settingsPathCommand(): Promise<void>;
@@ -0,0 +1,143 @@
1
+ import chalk from 'chalk';
2
+ import { loadSettings, saveSettings, getSettingsPath, updateProviderKey, removeProviderKey } from '@rigour-labs/core';
3
+ /**
4
+ * `rigour settings` — manage ~/.rigour/settings.json
5
+ *
6
+ * Like Claude Code's settings.json or Gemini CLI's config.
7
+ * Stores API keys, default provider, multi-agent config, CLI preferences.
8
+ */
9
+ export async function settingsShowCommand() {
10
+ const settingsPath = getSettingsPath();
11
+ const settings = loadSettings();
12
+ console.log(chalk.bold.cyan('\n Rigour Settings'));
13
+ console.log(chalk.dim(` ${settingsPath}\n`));
14
+ if (Object.keys(settings).length === 0) {
15
+ console.log(chalk.dim(' No settings configured yet.\n'));
16
+ console.log(chalk.dim(' Quick start:'));
17
+ console.log(chalk.dim(' rigour settings set-key anthropic sk-ant-xxx'));
18
+ console.log(chalk.dim(' rigour settings set-key openai sk-xxx'));
19
+ console.log(chalk.dim(' rigour settings set provider anthropic'));
20
+ console.log('');
21
+ return;
22
+ }
23
+ // Show providers
24
+ if (settings.providers && Object.keys(settings.providers).length > 0) {
25
+ console.log(chalk.bold(' Providers:'));
26
+ for (const [name, key] of Object.entries(settings.providers)) {
27
+ if (key) {
28
+ const masked = maskKey(key);
29
+ console.log(` ${chalk.green(name)}: ${chalk.dim(masked)}`);
30
+ }
31
+ }
32
+ console.log('');
33
+ }
34
+ // Show deep defaults
35
+ if (settings.deep) {
36
+ console.log(chalk.bold(' Deep Analysis Defaults:'));
37
+ if (settings.deep.defaultProvider)
38
+ console.log(` Provider: ${chalk.cyan(settings.deep.defaultProvider)}`);
39
+ if (settings.deep.defaultModel)
40
+ console.log(` Model: ${chalk.cyan(settings.deep.defaultModel)}`);
41
+ if (settings.deep.apiBaseUrl)
42
+ console.log(` API Base: ${chalk.cyan(settings.deep.apiBaseUrl)}`);
43
+ if (settings.deep.maxTokens)
44
+ console.log(` Max Tokens: ${settings.deep.maxTokens}`);
45
+ if (settings.deep.temperature !== undefined)
46
+ console.log(` Temperature: ${settings.deep.temperature}`);
47
+ console.log('');
48
+ }
49
+ // Show agent configs
50
+ if (settings.agents && Object.keys(settings.agents).length > 0) {
51
+ console.log(chalk.bold(' Agent Configurations:'));
52
+ for (const [name, config] of Object.entries(settings.agents)) {
53
+ const parts = [];
54
+ if (config.model)
55
+ parts.push(`model: ${config.model}`);
56
+ if (config.provider)
57
+ parts.push(`provider: ${config.provider}`);
58
+ if (config.fallback)
59
+ parts.push(`fallback: ${config.fallback}`);
60
+ console.log(` ${chalk.green(name)}: ${chalk.dim(parts.join(', '))}`);
61
+ }
62
+ console.log('');
63
+ }
64
+ // Show CLI prefs
65
+ if (settings.cli) {
66
+ console.log(chalk.bold(' CLI Preferences:'));
67
+ if (settings.cli.defaultPreset)
68
+ console.log(` Default Preset: ${settings.cli.defaultPreset}`);
69
+ if (settings.cli.colorOutput !== undefined)
70
+ console.log(` Color Output: ${settings.cli.colorOutput}`);
71
+ if (settings.cli.verboseOutput !== undefined)
72
+ console.log(` Verbose: ${settings.cli.verboseOutput}`);
73
+ console.log('');
74
+ }
75
+ }
76
+ export async function settingsSetKeyCommand(provider, apiKey) {
77
+ updateProviderKey(provider, apiKey);
78
+ const masked = maskKey(apiKey);
79
+ console.log(chalk.green(` āœ“ ${provider} API key saved: ${masked}`));
80
+ console.log(chalk.dim(` Stored in ${getSettingsPath()}`));
81
+ console.log('');
82
+ console.log(chalk.dim(` Usage: rigour check --deep --provider ${provider}`));
83
+ console.log(chalk.dim(` Or set as default: rigour settings set provider ${provider}`));
84
+ }
85
+ export async function settingsRemoveKeyCommand(provider) {
86
+ removeProviderKey(provider);
87
+ console.log(chalk.green(` āœ“ ${provider} API key removed`));
88
+ }
89
+ export async function settingsSetCommand(key, value) {
90
+ const settings = loadSettings();
91
+ // Parse dot-notation keys: "deep.defaultProvider" -> settings.deep.defaultProvider
92
+ const parts = key.split('.');
93
+ let target = settings;
94
+ for (let i = 0; i < parts.length - 1; i++) {
95
+ if (!target[parts[i]])
96
+ target[parts[i]] = {};
97
+ target = target[parts[i]];
98
+ }
99
+ const lastKey = parts[parts.length - 1];
100
+ // Auto-convert booleans and numbers
101
+ if (value === 'true')
102
+ target[lastKey] = true;
103
+ else if (value === 'false')
104
+ target[lastKey] = false;
105
+ else if (!isNaN(Number(value)) && value.trim() !== '')
106
+ target[lastKey] = Number(value);
107
+ else
108
+ target[lastKey] = value;
109
+ saveSettings(settings);
110
+ console.log(chalk.green(` āœ“ ${key} = ${value}`));
111
+ }
112
+ export async function settingsGetCommand(key) {
113
+ const settings = loadSettings();
114
+ const parts = key.split('.');
115
+ let value = settings;
116
+ for (const part of parts) {
117
+ if (value === undefined || value === null)
118
+ break;
119
+ value = value[part];
120
+ }
121
+ if (value === undefined) {
122
+ console.log(chalk.dim(` ${key} is not set`));
123
+ }
124
+ else if (typeof value === 'object') {
125
+ console.log(` ${key} = ${JSON.stringify(value, null, 2)}`);
126
+ }
127
+ else {
128
+ console.log(` ${key} = ${value}`);
129
+ }
130
+ }
131
+ export async function settingsResetCommand() {
132
+ saveSettings({});
133
+ console.log(chalk.green(' āœ“ Settings reset to defaults'));
134
+ console.log(chalk.dim(` ${getSettingsPath()}`));
135
+ }
136
+ export async function settingsPathCommand() {
137
+ console.log(getSettingsPath());
138
+ }
139
+ function maskKey(key) {
140
+ if (key.length <= 8)
141
+ return '***';
142
+ return key.substring(0, 6) + '...' + key.substring(key.length - 4);
143
+ }
@@ -1,22 +1,113 @@
1
1
  import chalk from 'chalk';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
4
+ import { loadSettings, getSettingsPath, isModelCached, getModelsDir } from '@rigour-labs/core';
2
5
  export async function setupCommand() {
3
- console.log(chalk.bold.cyan('\nšŸ› ļø Rigour Labs | Setup & Installation\n'));
4
- console.log(chalk.bold('1. Global Installation (Recommended)'));
5
- console.log(chalk.dim(' To use Rigour anywhere in your terminal:'));
6
- console.log(chalk.green(' $ npm install -g @rigour-labs/cli\n'));
7
- console.log(chalk.bold('2. Project-Local installation'));
8
- console.log(chalk.dim(' To keep Rigour versioned with your project:'));
9
- console.log(chalk.green(' $ npm install --save-dev @rigour-labs/cli\n'));
10
- console.log(chalk.bold('3. Standalone Binaries (Zero-Install)'));
11
- console.log(chalk.dim(' If you do not want to use Node.js:'));
12
- console.log(chalk.dim(' • macOS: ') + chalk.cyan('https://github.com/erashu212/rigour/releases/latest/download/rigour-macos'));
13
- console.log(chalk.dim(' • Linux: ') + chalk.cyan('https://github.com/erashu212/rigour/releases/latest/download/rigour-linux'));
14
- console.log(chalk.dim(' • Windows: ') + chalk.cyan('https://github.com/erashu212/rigour/releases/latest/download/rigour-windows.exe\n'));
15
- console.log(chalk.bold('4. MCP Integration (for AI Agents)'));
16
- console.log(chalk.dim(' To let Cursor or Claude use Rigour natively:'));
17
- console.log(chalk.dim(' Path to MCP: ') + chalk.cyan('packages/rigour-mcp/dist/index.js'));
18
- console.log(chalk.dim(' Add this to your Cursor/Claude settings.\n'));
19
- console.log(chalk.bold('Update Guidance:'));
20
- console.log(chalk.dim(' Keep Rigour sharp by updating regularly:'));
21
- console.log(chalk.green(' $ npm install -g @rigour-labs/cli@latest\n'));
6
+ console.log(chalk.bold.cyan('\nšŸ› ļø Rigour Labs | Setup & System Check\n'));
7
+ // ── Section 1: Installation Status ──
8
+ console.log(chalk.bold(' Installation'));
9
+ const cliVersion = getCliVersion();
10
+ if (cliVersion) {
11
+ console.log(chalk.green(` āœ” Rigour CLI ${cliVersion}`));
12
+ }
13
+ // Check if rigour.yml exists in cwd
14
+ const hasConfig = fs.existsSync(path.join(process.cwd(), 'rigour.yml'));
15
+ if (hasConfig) {
16
+ console.log(chalk.green(' āœ” rigour.yml found in current directory'));
17
+ }
18
+ else {
19
+ console.log(chalk.yellow(' ā—‹ No rigour.yml — run `rigour init` to set up'));
20
+ }
21
+ // ── Section 2: Settings & API Keys ──
22
+ console.log(chalk.bold('\n Settings'));
23
+ const settingsPath = getSettingsPath();
24
+ const settings = loadSettings();
25
+ const providers = settings.providers || {};
26
+ const configuredKeys = Object.entries(providers).filter(([_, key]) => !!key);
27
+ if (configuredKeys.length > 0) {
28
+ for (const [name, key] of configuredKeys) {
29
+ if (key) {
30
+ const masked = key.length > 8 ? key.substring(0, 6) + '...' + key.substring(key.length - 4) : '***';
31
+ console.log(chalk.green(` āœ” ${name}: ${chalk.dim(masked)}`));
32
+ }
33
+ }
34
+ }
35
+ else {
36
+ console.log(chalk.yellow(' ā—‹ No API keys configured'));
37
+ console.log(chalk.dim(` ${settingsPath}`));
38
+ }
39
+ if (settings.deep?.defaultProvider) {
40
+ console.log(chalk.green(` āœ” Default provider: ${settings.deep.defaultProvider}`));
41
+ }
42
+ // ── Section 3: Deep Analysis Readiness ──
43
+ console.log(chalk.bold('\n Deep Analysis'));
44
+ // Check local models
45
+ const hasDeep = isModelCached('deep');
46
+ const hasPro = isModelCached('pro');
47
+ if (hasDeep)
48
+ console.log(chalk.green(' āœ” Local model: deep (Qwen2.5-Coder-0.5B, 350MB)'));
49
+ if (hasPro)
50
+ console.log(chalk.green(' āœ” Local model: pro (Qwen2.5-Coder-1.5B, 900MB)'));
51
+ if (!hasDeep && !hasPro) {
52
+ console.log(chalk.yellow(' ā—‹ No local models cached'));
53
+ console.log(chalk.dim(` Models dir: ${getModelsDir()}`));
54
+ }
55
+ // Check sidecar binary
56
+ let hasSidecar = false;
57
+ try {
58
+ const { execSync } = await import('child_process');
59
+ execSync('which llama-cli 2>/dev/null || which rigour-brain 2>/dev/null', { encoding: 'utf-8', timeout: 3000 });
60
+ hasSidecar = true;
61
+ console.log(chalk.green(' āœ” Inference binary found'));
62
+ }
63
+ catch {
64
+ const binDir = path.join(getModelsDir(), '..', 'bin');
65
+ if (fs.existsSync(path.join(binDir, 'rigour-brain')) || fs.existsSync(path.join(binDir, 'llama-cli'))) {
66
+ hasSidecar = true;
67
+ console.log(chalk.green(' āœ” Inference binary found'));
68
+ }
69
+ else if (configuredKeys.length === 0) {
70
+ console.log(chalk.yellow(' ā—‹ No local inference binary'));
71
+ }
72
+ }
73
+ // Cloud readiness
74
+ const hasCloudKey = configuredKeys.length > 0;
75
+ const hasLocalReady = hasSidecar && (hasDeep || hasPro);
76
+ if (hasCloudKey || hasLocalReady) {
77
+ console.log(chalk.green.bold('\n āœ“ Deep analysis is ready'));
78
+ }
79
+ else {
80
+ console.log(chalk.yellow.bold('\n ⚠ Deep analysis not configured'));
81
+ }
82
+ // ── Section 4: Quick Setup Commands ──
83
+ if (!hasCloudKey && !hasLocalReady) {
84
+ console.log(chalk.bold('\n Quick Setup:'));
85
+ console.log(chalk.dim(' # Option A: Cloud (recommended)'));
86
+ console.log(` ${chalk.cyan('rigour settings set-key anthropic')} ${chalk.dim('sk-ant-xxx')}`);
87
+ console.log(` ${chalk.cyan('rigour settings set-key openai')} ${chalk.dim('sk-xxx')}`);
88
+ console.log(` ${chalk.cyan('rigour settings set-key groq')} ${chalk.dim('gsk_xxx')}`);
89
+ console.log('');
90
+ console.log(chalk.dim(' # Option B: 100% Local'));
91
+ console.log(` ${chalk.cyan('rigour check --deep')} ${chalk.dim('# auto-downloads 350MB model')}`);
92
+ }
93
+ // ── Section 5: Installation Methods ──
94
+ console.log(chalk.bold('\n Installation Methods:'));
95
+ console.log(chalk.dim(' Global: ') + chalk.cyan('npm install -g @rigour-labs/cli'));
96
+ console.log(chalk.dim(' Local: ') + chalk.cyan('npm install --save-dev @rigour-labs/cli'));
97
+ console.log(chalk.dim(' No-install: ') + chalk.cyan('npx @rigour-labs/cli check'));
98
+ console.log(chalk.dim(' MCP: ') + chalk.cyan('packages/rigour-mcp/dist/index.js'));
99
+ console.log('');
100
+ }
101
+ function getCliVersion() {
102
+ try {
103
+ const pkgPath = path.resolve(new URL(import.meta.url).pathname, '../../../package.json');
104
+ if (fs.existsSync(pkgPath)) {
105
+ const pkg = fs.readJsonSync(pkgPath);
106
+ return pkg.version || null;
107
+ }
108
+ }
109
+ catch {
110
+ // fallback
111
+ }
112
+ return '2.0.0';
22
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "3.0.6",
3
+ "version": "4.0.0",
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": "3.0.6"
47
+ "@rigour-labs/core": "4.0.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/fs-extra": "^11.0.4",