@rigour-labs/cli 2.0.0 → 2.2.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
@@ -43,17 +43,20 @@ Examples:
43
43
  program
44
44
  .command('check')
45
45
  .description('Run quality gate checks')
46
+ .argument('[files...]', 'Specific files or directories to check')
46
47
  .option('--ci', 'CI mode (minimal output, non-zero exit on fail)')
47
48
  .option('--json', 'Output report in JSON format')
48
49
  .option('-i, --interactive', 'Run in interactive mode with rich output')
49
50
  .addHelpText('after', `
50
51
  Examples:
51
52
  $ rigour check # Run standard check
53
+ $ rigour check ./src # Check only the src directory
54
+ $ rigour check ./src/app.ts # Check only app.ts
52
55
  $ rigour check --interactive # Run with rich, interactive output
53
56
  $ rigour check --ci # Run in CI environment
54
57
  `)
55
- .action(async (options) => {
56
- await (0, check_js_1.checkCommand)(process.cwd(), options);
58
+ .action(async (files, options) => {
59
+ await (0, check_js_1.checkCommand)(process.cwd(), files, options);
57
60
  });
58
61
  program
59
62
  .command('explain')
@@ -3,4 +3,4 @@ export interface CheckOptions {
3
3
  json?: boolean;
4
4
  interactive?: boolean;
5
5
  }
6
- export declare function checkCommand(cwd: string, options?: CheckOptions): Promise<void>;
6
+ export declare function checkCommand(cwd: string, files?: string[], options?: CheckOptions): Promise<void>;
@@ -9,12 +9,13 @@ const path_1 = __importDefault(require("path"));
9
9
  const chalk_1 = __importDefault(require("chalk"));
10
10
  const yaml_1 = __importDefault(require("yaml"));
11
11
  const core_1 = require("@rigour-labs/core");
12
+ const inquirer_1 = __importDefault(require("inquirer"));
12
13
  // Exit codes per spec
13
14
  const EXIT_PASS = 0;
14
15
  const EXIT_FAIL = 1;
15
16
  const EXIT_CONFIG_ERROR = 2;
16
17
  const EXIT_INTERNAL_ERROR = 3;
17
- async function checkCommand(cwd, options = {}) {
18
+ async function checkCommand(cwd, files = [], options = {}) {
18
19
  const configPath = path_1.default.join(cwd, 'rigour.yml');
19
20
  if (!(await fs_extra_1.default.pathExists(configPath))) {
20
21
  if (options.json) {
@@ -33,7 +34,7 @@ async function checkCommand(cwd, options = {}) {
33
34
  console.log(chalk_1.default.blue('Running Rigour checks...\n'));
34
35
  }
35
36
  const runner = new core_1.GateRunner(config);
36
- const report = await runner.run(cwd);
37
+ const report = await runner.run(cwd, files.length > 0 ? files : undefined);
37
38
  // Write machine report
38
39
  const reportPath = path_1.default.join(cwd, config.output.report_path);
39
40
  await fs_extra_1.default.writeJson(reportPath, report, { spaces: 2 });
@@ -63,6 +64,10 @@ async function checkCommand(cwd, options = {}) {
63
64
  }
64
65
  process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
65
66
  }
67
+ if (options.interactive && report.status === 'FAIL') {
68
+ await interactiveMode(report, config);
69
+ process.exit(EXIT_FAIL);
70
+ }
66
71
  // Normal human-readable output
67
72
  if (report.status === 'PASS') {
68
73
  console.log(chalk_1.default.green.bold('✔ PASS - All quality gates satisfied.'));
@@ -87,6 +92,18 @@ async function checkCommand(cwd, options = {}) {
87
92
  process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
88
93
  }
89
94
  catch (error) {
95
+ if (error.name === 'ZodError') {
96
+ if (options.json) {
97
+ console.log(JSON.stringify({ error: 'CONFIG_ERROR', details: error.issues }));
98
+ }
99
+ else {
100
+ console.error(chalk_1.default.red('\nInvalid rigour.yml configuration:'));
101
+ error.issues.forEach((issue) => {
102
+ console.error(chalk_1.default.red(` • ${issue.path.join('.')}: ${issue.message}`));
103
+ });
104
+ }
105
+ process.exit(EXIT_CONFIG_ERROR);
106
+ }
90
107
  if (options.json) {
91
108
  console.log(JSON.stringify({ error: 'INTERNAL_ERROR', message: error.message }));
92
109
  }
@@ -96,3 +113,46 @@ async function checkCommand(cwd, options = {}) {
96
113
  process.exit(EXIT_INTERNAL_ERROR);
97
114
  }
98
115
  }
116
+ async function interactiveMode(report, config) {
117
+ console.clear();
118
+ console.log(chalk_1.default.bold.blue('══ Rigour Interactive Review ══\n'));
119
+ console.log(chalk_1.default.yellow(`${report.failures.length} violations found.\n`));
120
+ const choices = report.failures.map((f, i) => ({
121
+ name: `[${f.id}] ${f.title}`,
122
+ value: i
123
+ }));
124
+ choices.push(new inquirer_1.default.Separator());
125
+ choices.push({ name: 'Exit', value: -1 });
126
+ let exit = false;
127
+ while (!exit) {
128
+ const { index } = await inquirer_1.default.prompt([
129
+ {
130
+ type: 'list',
131
+ name: 'index',
132
+ message: 'Select a violation to view details:',
133
+ choices,
134
+ pageSize: 15
135
+ }
136
+ ]);
137
+ if (index === -1) {
138
+ exit = true;
139
+ continue;
140
+ }
141
+ const failure = report.failures[index];
142
+ console.clear();
143
+ console.log(chalk_1.default.bold.red(`\nViolation: ${failure.title}`));
144
+ console.log(chalk_1.default.dim(`ID: ${failure.id}`));
145
+ console.log(`\n${chalk_1.default.bold('Details:')}\n${failure.details}`);
146
+ if (failure.files && failure.files.length > 0) {
147
+ console.log(`\n${chalk_1.default.bold('Impacted Files:')}`);
148
+ failure.files.forEach((f) => console.log(chalk_1.default.dim(` - ${f}`)));
149
+ }
150
+ if (failure.hint) {
151
+ console.log(`\n${chalk_1.default.bold.cyan('Hint:')} ${failure.hint}`);
152
+ }
153
+ console.log(chalk_1.default.dim('\n' + '─'.repeat(40)));
154
+ await inquirer_1.default.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to return to list...' }]);
155
+ console.clear();
156
+ console.log(chalk_1.default.bold.blue('══ Rigour Interactive Review ══\n'));
157
+ }
158
+ }
@@ -116,8 +116,10 @@ npx @rigour-labs/cli run -- <agent-command>
116
116
  \`\`\`
117
117
  `;
118
118
  // 1. Create Universal Instructions
119
- await fs_extra_1.default.writeFile(instructionsPath, ruleContent);
120
- console.log(chalk_1.default.green('✔ Initialized Universal Agent Handshake (docs/AGENT_INSTRUCTIONS.md)'));
119
+ if (!(await fs_extra_1.default.pathExists(instructionsPath))) {
120
+ await fs_extra_1.default.writeFile(instructionsPath, ruleContent);
121
+ console.log(chalk_1.default.green('✔ Initialized Universal Agent Handshake (docs/AGENT_INSTRUCTIONS.md)'));
122
+ }
121
123
  // 2. Create Cursor Specific Rules (.mdc)
122
124
  const cursorRulesDir = path_1.default.join(cwd, '.cursor', 'rules');
123
125
  await fs_extra_1.default.ensureDir(cursorRulesDir);
@@ -128,7 +130,28 @@ globs: **/*
128
130
  ---
129
131
 
130
132
  ${ruleContent}`;
131
- await fs_extra_1.default.writeFile(mdcPath, mdcContent);
132
- console.log(chalk_1.default.green('✔ Initialized Cursor Handshake (.cursor/rules/rigour.mdc)'));
133
+ if (!(await fs_extra_1.default.pathExists(mdcPath))) {
134
+ await fs_extra_1.default.writeFile(mdcPath, mdcContent);
135
+ console.log(chalk_1.default.green('✔ Initialized Cursor Handshake (.cursor/rules/rigour.mdc)'));
136
+ }
137
+ // 3. Update .gitignore
138
+ const gitignorePath = path_1.default.join(cwd, '.gitignore');
139
+ const ignorePatterns = ['rigour-report.json', 'rigour-fix-packet.json', '.rigour/'];
140
+ try {
141
+ let content = '';
142
+ if (await fs_extra_1.default.pathExists(gitignorePath)) {
143
+ content = await fs_extra_1.default.readFile(gitignorePath, 'utf-8');
144
+ }
145
+ const toAdd = ignorePatterns.filter(p => !content.includes(p));
146
+ if (toAdd.length > 0) {
147
+ const separator = content.endsWith('\n') ? '' : '\n';
148
+ const newContent = `${content}${separator}\n# Rigour Artifacts\n${toAdd.join('\n')}\n`;
149
+ await fs_extra_1.default.writeFile(gitignorePath, newContent);
150
+ console.log(chalk_1.default.green('✔ Updated .gitignore'));
151
+ }
152
+ }
153
+ catch (e) {
154
+ // Failing to update .gitignore isn't fatal
155
+ }
133
156
  console.log(chalk_1.default.blue('\nRigour is ready. Run `npx @rigour-labs/cli check` to verify your project.'));
134
157
  }
@@ -41,13 +41,20 @@ async function runLoop(cwd, agentArgs, options) {
41
41
  // We keep the first part of the command (the agent) but can append or wrap
42
42
  // For simplicity, we assume the agent can read the JSON file we generate
43
43
  }
44
+ const getTrackedChanges = async () => {
45
+ try {
46
+ const { stdout } = await (0, execa_1.execa)('git', ['status', '--porcelain'], { cwd });
47
+ return stdout.split('\n')
48
+ .filter(l => l.trim())
49
+ .filter(line => /M|A|D|R/.test(line.slice(0, 2)))
50
+ .map(l => l.slice(3).trim());
51
+ }
52
+ catch (e) {
53
+ return [];
54
+ }
55
+ };
44
56
  // Snapshot changed files before agent runs
45
- let beforeFiles = [];
46
- try {
47
- const { stdout } = await (0, execa_1.execa)('git', ['status', '--porcelain'], { cwd });
48
- beforeFiles = stdout.split('\n').filter(l => l.trim()).map(l => l.slice(3).trim());
49
- }
50
- catch (e) { }
57
+ const beforeFiles = await getTrackedChanges();
51
58
  // 2. Run the agent command
52
59
  if (currentArgs.length > 0) {
53
60
  console.log(chalk_1.default.cyan(`\n🚀 DEPLOYING AGENT:`));
@@ -60,12 +67,7 @@ async function runLoop(cwd, agentArgs, options) {
60
67
  }
61
68
  }
62
69
  // Snapshot changed files after agent runs
63
- let afterFiles = [];
64
- try {
65
- const { stdout } = await (0, execa_1.execa)('git', ['status', '--porcelain'], { cwd });
66
- afterFiles = stdout.split('\n').filter(l => l.trim()).map(l => l.slice(3).trim());
67
- }
68
- catch (e) { }
70
+ const afterFiles = await getTrackedChanges();
69
71
  const changedThisCycle = afterFiles.filter(f => !beforeFiles.includes(f));
70
72
  const maxFiles = config.gates.safety?.max_files_changed_per_cycle || 10;
71
73
  if (changedThisCycle.length > maxFiles) {
@@ -1,8 +1,63 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const vitest_1 = require("vitest");
7
+ const check_js_1 = require("./commands/check.js");
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
4
10
  (0, vitest_1.describe)('CLI Smoke Test', () => {
5
- (0, vitest_1.it)('should pass', async () => {
6
- (0, vitest_1.expect)(true).toBe(true);
11
+ const testDir = path_1.default.join(process.cwd(), 'temp-smoke-test');
12
+ (0, vitest_1.beforeEach)(async () => {
13
+ await fs_extra_1.default.ensureDir(testDir);
14
+ // @ts-ignore
15
+ vitest_1.vi.spyOn(process, 'exit').mockImplementation(() => { });
16
+ });
17
+ (0, vitest_1.afterEach)(async () => {
18
+ await fs_extra_1.default.remove(testDir);
19
+ vitest_1.vi.restoreAllMocks();
20
+ });
21
+ (0, vitest_1.it)('should respect ignore patterns and avoid EPERM', async () => {
22
+ const restrictedDir = path_1.default.join(testDir, '.restricted');
23
+ await fs_extra_1.default.ensureDir(restrictedDir);
24
+ await fs_extra_1.default.writeFile(path_1.default.join(restrictedDir, 'secret.js'), 'TODO: leak');
25
+ await fs_extra_1.default.writeFile(path_1.default.join(testDir, 'rigour.yml'), `
26
+ version: 1
27
+ ignore:
28
+ - ".restricted/**"
29
+ gates:
30
+ forbid_todos: true
31
+ required_files: []
32
+ `);
33
+ // Simulate EPERM by changing permissions
34
+ await fs_extra_1.default.chmod(restrictedDir, 0o000);
35
+ try {
36
+ // We need to mock process.exit or checkCommand should not exit if we want to test it easily
37
+ // For now, we'll just verify it doesn't throw before it would exit (internal logic)
38
+ // But checkCommand calls process.exit(1) on failure.
39
+ // Re-importing checkCommand to ensure it uses the latest core
40
+ await (0, vitest_1.expect)((0, check_js_1.checkCommand)(testDir, [], { ci: true })).resolves.not.toThrow();
41
+ }
42
+ finally {
43
+ await fs_extra_1.default.chmod(restrictedDir, 0o777);
44
+ }
45
+ });
46
+ (0, vitest_1.it)('should check specific files when provided', async () => {
47
+ await fs_extra_1.default.writeFile(path_1.default.join(testDir, 'bad.js'), 'TODO: fixme');
48
+ await fs_extra_1.default.writeFile(path_1.default.join(testDir, 'good.js'), 'console.log("hello")');
49
+ await fs_extra_1.default.writeFile(path_1.default.join(testDir, 'rigour.yml'), `
50
+ version: 1
51
+ gates:
52
+ forbid_todos: true
53
+ required_files: []
54
+ `);
55
+ // If we check ONLY good.js, it should PASS (exit PASS)
56
+ await (0, check_js_1.checkCommand)(testDir, [path_1.default.join(testDir, 'good.js')], { ci: true });
57
+ (0, vitest_1.expect)(process.exit).toHaveBeenCalledWith(0);
58
+ // If we check bad.js, it should FAIL (exit FAIL)
59
+ vitest_1.vi.clearAllMocks();
60
+ await (0, check_js_1.checkCommand)(testDir, [path_1.default.join(testDir, 'bad.js')], { ci: true });
61
+ (0, vitest_1.expect)(process.exit).toHaveBeenCalledWith(1);
7
62
  });
8
63
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "bin": {
5
5
  "rigour": "dist/cli.js"
6
6
  },
@@ -19,11 +19,13 @@
19
19
  "execa": "^8.0.1",
20
20
  "fs-extra": "^11.2.0",
21
21
  "globby": "^14.0.1",
22
+ "inquirer": "9.2.16",
22
23
  "yaml": "^2.8.2",
23
- "@rigour-labs/core": "2.0.0"
24
+ "@rigour-labs/core": "2.2.0"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/fs-extra": "^11.0.4",
28
+ "@types/inquirer": "9.0.7",
27
29
  "@types/node": "^25.0.3"
28
30
  },
29
31
  "scripts": {
package/src/cli.ts CHANGED
@@ -42,17 +42,20 @@ Examples:
42
42
  program
43
43
  .command('check')
44
44
  .description('Run quality gate checks')
45
+ .argument('[files...]', 'Specific files or directories to check')
45
46
  .option('--ci', 'CI mode (minimal output, non-zero exit on fail)')
46
47
  .option('--json', 'Output report in JSON format')
47
48
  .option('-i, --interactive', 'Run in interactive mode with rich output')
48
49
  .addHelpText('after', `
49
50
  Examples:
50
51
  $ rigour check # Run standard check
52
+ $ rigour check ./src # Check only the src directory
53
+ $ rigour check ./src/app.ts # Check only app.ts
51
54
  $ rigour check --interactive # Run with rich, interactive output
52
55
  $ rigour check --ci # Run in CI environment
53
56
  `)
54
- .action(async (options: any) => {
55
- await checkCommand(process.cwd(), options);
57
+ .action(async (files: string[], options: any) => {
58
+ await checkCommand(process.cwd(), files, options);
56
59
  });
57
60
 
58
61
  program
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
5
  import { GateRunner, ConfigSchema, Failure } from '@rigour-labs/core';
6
+ import inquirer from 'inquirer';
6
7
 
7
8
  // Exit codes per spec
8
9
  const EXIT_PASS = 0;
@@ -16,7 +17,7 @@ export interface CheckOptions {
16
17
  interactive?: boolean;
17
18
  }
18
19
 
19
- export async function checkCommand(cwd: string, options: CheckOptions = {}) {
20
+ export async function checkCommand(cwd: string, files: string[] = [], options: CheckOptions = {}) {
20
21
  const configPath = path.join(cwd, 'rigour.yml');
21
22
 
22
23
  if (!(await fs.pathExists(configPath))) {
@@ -38,7 +39,7 @@ export async function checkCommand(cwd: string, options: CheckOptions = {}) {
38
39
  }
39
40
 
40
41
  const runner = new GateRunner(config);
41
- const report = await runner.run(cwd);
42
+ const report = await runner.run(cwd, files.length > 0 ? files : undefined);
42
43
 
43
44
  // Write machine report
44
45
  const reportPath = path.join(cwd, config.output.report_path);
@@ -72,6 +73,11 @@ export async function checkCommand(cwd: string, options: CheckOptions = {}) {
72
73
  process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
73
74
  }
74
75
 
76
+ if (options.interactive && report.status === 'FAIL') {
77
+ await interactiveMode(report, config);
78
+ process.exit(EXIT_FAIL);
79
+ }
80
+
75
81
  // Normal human-readable output
76
82
  if (report.status === 'PASS') {
77
83
  console.log(chalk.green.bold('✔ PASS - All quality gates satisfied.'));
@@ -99,6 +105,18 @@ export async function checkCommand(cwd: string, options: CheckOptions = {}) {
99
105
  process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
100
106
 
101
107
  } catch (error: any) {
108
+ if (error.name === 'ZodError') {
109
+ if (options.json) {
110
+ console.log(JSON.stringify({ error: 'CONFIG_ERROR', details: error.issues }));
111
+ } else {
112
+ console.error(chalk.red('\nInvalid rigour.yml configuration:'));
113
+ error.issues.forEach((issue: any) => {
114
+ console.error(chalk.red(` • ${issue.path.join('.')}: ${issue.message}`));
115
+ });
116
+ }
117
+ process.exit(EXIT_CONFIG_ERROR);
118
+ }
119
+
102
120
  if (options.json) {
103
121
  console.log(JSON.stringify({ error: 'INTERNAL_ERROR', message: error.message }));
104
122
  } else if (!options.ci) {
@@ -107,3 +125,55 @@ export async function checkCommand(cwd: string, options: CheckOptions = {}) {
107
125
  process.exit(EXIT_INTERNAL_ERROR);
108
126
  }
109
127
  }
128
+
129
+ async function interactiveMode(report: any, config: any) {
130
+ console.clear();
131
+ console.log(chalk.bold.blue('══ Rigour Interactive Review ══\n'));
132
+ console.log(chalk.yellow(`${report.failures.length} violations found.\n`));
133
+
134
+ const choices = report.failures.map((f: Failure, i: number) => ({
135
+ name: `[${f.id}] ${f.title}`,
136
+ value: i
137
+ }));
138
+
139
+ choices.push(new (inquirer as any).Separator());
140
+ choices.push({ name: 'Exit', value: -1 });
141
+
142
+ let exit = false;
143
+ while (!exit) {
144
+ const { index } = await inquirer.prompt([
145
+ {
146
+ type: 'list',
147
+ name: 'index',
148
+ message: 'Select a violation to view details:',
149
+ choices,
150
+ pageSize: 15
151
+ }
152
+ ]);
153
+
154
+ if (index === -1) {
155
+ exit = true;
156
+ continue;
157
+ }
158
+
159
+ const failure = report.failures[index];
160
+ console.clear();
161
+ console.log(chalk.bold.red(`\nViolation: ${failure.title}`));
162
+ console.log(chalk.dim(`ID: ${failure.id}`));
163
+ console.log(`\n${chalk.bold('Details:')}\n${failure.details}`);
164
+
165
+ if (failure.files && failure.files.length > 0) {
166
+ console.log(`\n${chalk.bold('Impacted Files:')}`);
167
+ failure.files.forEach((f: string) => console.log(chalk.dim(` - ${f}`)));
168
+ }
169
+
170
+ if (failure.hint) {
171
+ console.log(`\n${chalk.bold.cyan('Hint:')} ${failure.hint}`);
172
+ }
173
+
174
+ console.log(chalk.dim('\n' + '─'.repeat(40)));
175
+ await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to return to list...' }]);
176
+ console.clear();
177
+ console.log(chalk.bold.blue('══ Rigour Interactive Review ══\n'));
178
+ }
179
+ }
@@ -124,8 +124,10 @@ npx @rigour-labs/cli run -- <agent-command>
124
124
  `;
125
125
 
126
126
  // 1. Create Universal Instructions
127
- await fs.writeFile(instructionsPath, ruleContent);
128
- console.log(chalk.green('✔ Initialized Universal Agent Handshake (docs/AGENT_INSTRUCTIONS.md)'));
127
+ if (!(await fs.pathExists(instructionsPath))) {
128
+ await fs.writeFile(instructionsPath, ruleContent);
129
+ console.log(chalk.green('✔ Initialized Universal Agent Handshake (docs/AGENT_INSTRUCTIONS.md)'));
130
+ }
129
131
 
130
132
  // 2. Create Cursor Specific Rules (.mdc)
131
133
  const cursorRulesDir = path.join(cwd, '.cursor', 'rules');
@@ -138,8 +140,30 @@ globs: **/*
138
140
 
139
141
  ${ruleContent}`;
140
142
 
141
- await fs.writeFile(mdcPath, mdcContent);
142
- console.log(chalk.green('✔ Initialized Cursor Handshake (.cursor/rules/rigour.mdc)'));
143
+ if (!(await fs.pathExists(mdcPath))) {
144
+ await fs.writeFile(mdcPath, mdcContent);
145
+ console.log(chalk.green('✔ Initialized Cursor Handshake (.cursor/rules/rigour.mdc)'));
146
+ }
147
+
148
+ // 3. Update .gitignore
149
+ const gitignorePath = path.join(cwd, '.gitignore');
150
+ const ignorePatterns = ['rigour-report.json', 'rigour-fix-packet.json', '.rigour/'];
151
+ try {
152
+ let content = '';
153
+ if (await fs.pathExists(gitignorePath)) {
154
+ content = await fs.readFile(gitignorePath, 'utf-8');
155
+ }
156
+
157
+ const toAdd = ignorePatterns.filter(p => !content.includes(p));
158
+ if (toAdd.length > 0) {
159
+ const separator = content.endsWith('\n') ? '' : '\n';
160
+ const newContent = `${content}${separator}\n# Rigour Artifacts\n${toAdd.join('\n')}\n`;
161
+ await fs.writeFile(gitignorePath, newContent);
162
+ console.log(chalk.green('✔ Updated .gitignore'));
163
+ }
164
+ } catch (e) {
165
+ // Failing to update .gitignore isn't fatal
166
+ }
143
167
 
144
168
  console.log(chalk.blue('\nRigour is ready. Run `npx @rigour-labs/cli check` to verify your project.'));
145
169
  }
@@ -43,12 +43,20 @@ export async function runLoop(cwd: string, agentArgs: string[], options: { itera
43
43
  // For simplicity, we assume the agent can read the JSON file we generate
44
44
  }
45
45
 
46
+ const getTrackedChanges = async () => {
47
+ try {
48
+ const { stdout } = await execa('git', ['status', '--porcelain'], { cwd });
49
+ return stdout.split('\n')
50
+ .filter(l => l.trim())
51
+ .filter(line => /M|A|D|R/.test(line.slice(0, 2)))
52
+ .map(l => l.slice(3).trim());
53
+ } catch (e) {
54
+ return [];
55
+ }
56
+ };
57
+
46
58
  // Snapshot changed files before agent runs
47
- let beforeFiles: string[] = [];
48
- try {
49
- const { stdout } = await execa('git', ['status', '--porcelain'], { cwd });
50
- beforeFiles = stdout.split('\n').filter(l => l.trim()).map(l => l.slice(3).trim());
51
- } catch (e) { }
59
+ const beforeFiles = await getTrackedChanges();
52
60
 
53
61
  // 2. Run the agent command
54
62
  if (currentArgs.length > 0) {
@@ -62,11 +70,7 @@ export async function runLoop(cwd: string, agentArgs: string[], options: { itera
62
70
  }
63
71
 
64
72
  // Snapshot changed files after agent runs
65
- let afterFiles: string[] = [];
66
- try {
67
- const { stdout } = await execa('git', ['status', '--porcelain'], { cwd });
68
- afterFiles = stdout.split('\n').filter(l => l.trim()).map(l => l.slice(3).trim());
69
- } catch (e) { }
73
+ const afterFiles = await getTrackedChanges();
70
74
 
71
75
  const changedThisCycle = afterFiles.filter(f => !beforeFiles.includes(f));
72
76
  const maxFiles = config.gates.safety?.max_files_changed_per_cycle || 10;
package/src/smoke.test.ts CHANGED
@@ -1,7 +1,68 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { checkCommand } from './commands/check.js';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
2
5
 
3
6
  describe('CLI Smoke Test', () => {
4
- it('should pass', async () => {
5
- expect(true).toBe(true);
7
+ const testDir = path.join(process.cwd(), 'temp-smoke-test');
8
+
9
+ beforeEach(async () => {
10
+ await fs.ensureDir(testDir);
11
+ // @ts-ignore
12
+ vi.spyOn(process, 'exit').mockImplementation(() => { });
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.remove(testDir);
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ it('should respect ignore patterns and avoid EPERM', async () => {
21
+ const restrictedDir = path.join(testDir, '.restricted');
22
+ await fs.ensureDir(restrictedDir);
23
+ await fs.writeFile(path.join(restrictedDir, 'secret.js'), 'TODO: leak');
24
+
25
+ await fs.writeFile(path.join(testDir, 'rigour.yml'), `
26
+ version: 1
27
+ ignore:
28
+ - ".restricted/**"
29
+ gates:
30
+ forbid_todos: true
31
+ required_files: []
32
+ `);
33
+
34
+ // Simulate EPERM by changing permissions
35
+ await fs.chmod(restrictedDir, 0o000);
36
+
37
+ try {
38
+ // We need to mock process.exit or checkCommand should not exit if we want to test it easily
39
+ // For now, we'll just verify it doesn't throw before it would exit (internal logic)
40
+ // But checkCommand calls process.exit(1) on failure.
41
+
42
+ // Re-importing checkCommand to ensure it uses the latest core
43
+ await expect(checkCommand(testDir, [], { ci: true })).resolves.not.toThrow();
44
+ } finally {
45
+ await fs.chmod(restrictedDir, 0o777);
46
+ }
47
+ });
48
+
49
+ it('should check specific files when provided', async () => {
50
+ await fs.writeFile(path.join(testDir, 'bad.js'), 'TODO: fixme');
51
+ await fs.writeFile(path.join(testDir, 'good.js'), 'console.log("hello")');
52
+ await fs.writeFile(path.join(testDir, 'rigour.yml'), `
53
+ version: 1
54
+ gates:
55
+ forbid_todos: true
56
+ required_files: []
57
+ `);
58
+
59
+ // If we check ONLY good.js, it should PASS (exit PASS)
60
+ await checkCommand(testDir, [path.join(testDir, 'good.js')], { ci: true });
61
+ expect(process.exit).toHaveBeenCalledWith(0);
62
+
63
+ // If we check bad.js, it should FAIL (exit FAIL)
64
+ vi.clearAllMocks();
65
+ await checkCommand(testDir, [path.join(testDir, 'bad.js')], { ci: true });
66
+ expect(process.exit).toHaveBeenCalledWith(1);
6
67
  });
7
68
  });