@rigour-labs/cli 4.0.4 โ 4.1.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/README.md +8 -6
- package/dist/cli.js +24 -1
- package/dist/commands/check.js +15 -3
- package/dist/commands/demo-scenarios.js +4 -4
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +127 -0
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/doctor.test.js +20 -0
- package/dist/commands/guide.d.ts +1 -1
- package/dist/commands/guide.js +1 -1
- package/dist/commands/hooks.d.ts +7 -0
- package/dist/commands/hooks.js +99 -17
- package/dist/commands/hooks.test.js +46 -1
- package/dist/commands/init.js +2 -2
- package/dist/commands/settings.d.ts +7 -7
- package/dist/commands/settings.js +7 -7
- package/dist/commands/setup.js +9 -3
- package/dist/commands/studio.js +16 -4
- package/dist/smoke.test.js +1 -1
- package/package.json +2 -2
- package/studio-dist/assets/{index-B2pKEIQ0.js โ index-D14slH7l.js} +17 -17
- package/studio-dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
**Local-first quality gates for AI-generated code.**
|
|
8
8
|
Rigour forces AI agents to meet strict engineering standards before marking tasks "Done".
|
|
9
9
|
|
|
10
|
-
>
|
|
10
|
+
> Core gates run locally. Deep analysis can run local or cloud provider mode.
|
|
11
11
|
|
|
12
12
|
## ๐ Quick Start
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npx rigour scan # Zero-config scan (auto-detect stack)
|
|
16
|
-
npx rigour init # Initialize quality gates
|
|
17
|
-
npx rigour check # Verify code quality
|
|
18
|
-
npx rigour run -- claude "Build feature X" # Agent loop
|
|
15
|
+
npx @rigour-labs/cli scan # Zero-config scan (auto-detect stack)
|
|
16
|
+
npx @rigour-labs/cli init # Initialize quality gates
|
|
17
|
+
npx @rigour-labs/cli check # Verify code quality
|
|
18
|
+
npx @rigour-labs/cli run -- claude "Build feature X" # Agent loop
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## ๐บ Homebrew
|
|
@@ -74,6 +74,8 @@ All gates support **TypeScript, JavaScript, Python, Go, Ruby, and C#/.NET**.
|
|
|
74
74
|
| `rigour init` | Setup Rigour in your project |
|
|
75
75
|
| `rigour check` | Validate code against quality gates |
|
|
76
76
|
| `rigour check --ci` | CI mode with appropriate output |
|
|
77
|
+
| `rigour hooks init` | Install real-time hooks for supported tools |
|
|
78
|
+
| `rigour hooks check --files ...` | Run fast hook gates on specific files |
|
|
77
79
|
| `rigour explain` | Detailed explanation of validation results |
|
|
78
80
|
| `rigour run` | Supervisor loop for iterative refinement |
|
|
79
81
|
| `rigour studio` | Dashboard for monitoring |
|
|
@@ -98,7 +100,7 @@ All gates support **TypeScript, JavaScript, Python, Go, Ruby, and C#/.NET**.
|
|
|
98
100
|
## ๐งช CI Integration
|
|
99
101
|
|
|
100
102
|
```yaml
|
|
101
|
-
- run: npx rigour check --ci
|
|
103
|
+
- run: npx @rigour-labs/cli check --ci
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
## ๐ License
|
package/dist/cli.js
CHANGED
|
@@ -11,8 +11,9 @@ import { indexCommand } from './commands/index.js';
|
|
|
11
11
|
import { studioCommand } from './commands/studio.js';
|
|
12
12
|
import { exportAuditCommand } from './commands/export-audit.js';
|
|
13
13
|
import { demoCommand } from './commands/demo.js';
|
|
14
|
-
import { hooksInitCommand } from './commands/hooks.js';
|
|
14
|
+
import { hooksInitCommand, hooksCheckCommand } from './commands/hooks.js';
|
|
15
15
|
import { settingsShowCommand, settingsSetKeyCommand, settingsRemoveKeyCommand, settingsSetCommand, settingsGetCommand, settingsResetCommand, settingsPathCommand } from './commands/settings.js';
|
|
16
|
+
import { doctorCommand } from './commands/doctor.js';
|
|
16
17
|
import { checkForUpdates } from './utils/version.js';
|
|
17
18
|
import { getCliVersion } from './utils/cli-version.js';
|
|
18
19
|
import chalk from 'chalk';
|
|
@@ -176,6 +177,12 @@ program
|
|
|
176
177
|
.action(async () => {
|
|
177
178
|
await setupCommand();
|
|
178
179
|
});
|
|
180
|
+
program
|
|
181
|
+
.command('doctor')
|
|
182
|
+
.description('Diagnose install conflicts and deep-mode readiness')
|
|
183
|
+
.action(async () => {
|
|
184
|
+
await doctorCommand();
|
|
185
|
+
});
|
|
179
186
|
const hooksCmd = program
|
|
180
187
|
.command('hooks')
|
|
181
188
|
.description('Manage AI coding tool hook integrations');
|
|
@@ -197,6 +204,22 @@ Examples:
|
|
|
197
204
|
.action(async (options) => {
|
|
198
205
|
await hooksInitCommand(process.cwd(), options);
|
|
199
206
|
});
|
|
207
|
+
hooksCmd
|
|
208
|
+
.command('check')
|
|
209
|
+
.description('Run fast hook checks for one or more files')
|
|
210
|
+
.option('--files <paths>', 'Comma-separated file paths')
|
|
211
|
+
.option('--stdin', 'Read hook payload from stdin (Cursor/Windsurf/Cline format)')
|
|
212
|
+
.option('--block', 'Exit code 2 on failures (for blocking hooks)')
|
|
213
|
+
.option('--timeout <ms>', 'Timeout in milliseconds (default: 5000)')
|
|
214
|
+
.addHelpText('after', `
|
|
215
|
+
Examples:
|
|
216
|
+
$ rigour hooks check --files src/app.ts
|
|
217
|
+
$ rigour hooks check --files src/a.ts,src/b.ts --block
|
|
218
|
+
$ echo '{"file_path":"src/app.ts"}' | rigour hooks check --stdin
|
|
219
|
+
`)
|
|
220
|
+
.action(async (options) => {
|
|
221
|
+
await hooksCheckCommand(process.cwd(), options);
|
|
222
|
+
});
|
|
200
223
|
// Settings management (like Claude Code's settings.json)
|
|
201
224
|
const settingsCmd = program
|
|
202
225
|
.command('settings')
|
package/dist/commands/check.js
CHANGED
|
@@ -91,6 +91,18 @@ export async function checkCommand(cwd, files = [], options = {}) {
|
|
|
91
91
|
isLocal: !hasApiKey || deepOpts.provider === 'local',
|
|
92
92
|
provider: deepOpts.provider || 'cloud',
|
|
93
93
|
};
|
|
94
|
+
if (!isSilent) {
|
|
95
|
+
if (!resolvedDeepMode.isLocal && !options.provider && !options.apiKey) {
|
|
96
|
+
console.log(chalk.yellow(`Deep execution defaulted to cloud (${resolvedDeepMode.provider}) from settings.`));
|
|
97
|
+
console.log(chalk.dim('Use `--provider local` to force local sidecar execution.\n'));
|
|
98
|
+
}
|
|
99
|
+
else if (options.provider === 'local' && hasApiKey) {
|
|
100
|
+
console.log(chalk.green('Deep execution forced to local (`--provider local`) even though an API key is configured.\n'));
|
|
101
|
+
}
|
|
102
|
+
else if (options.provider && options.provider !== 'local' && !hasApiKey) {
|
|
103
|
+
console.log(chalk.yellow(`Provider "${options.provider}" requested, but no API key was resolved. Falling back to local execution.\n`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
94
106
|
}
|
|
95
107
|
const report = await runner.run(cwd, files.length > 0 ? files : undefined, deepOpts);
|
|
96
108
|
// Write machine report
|
|
@@ -228,7 +240,7 @@ export async function checkCommand(cwd, files = [], options = {}) {
|
|
|
228
240
|
*/
|
|
229
241
|
function renderDeepOutput(report, config, options, resolvedDeepMode) {
|
|
230
242
|
const stats = report.stats;
|
|
231
|
-
const isLocal = stats.deep?.tier ? stats.deep.tier !== 'cloud' :
|
|
243
|
+
const isLocal = resolvedDeepMode?.isLocal ?? (stats.deep?.tier ? stats.deep.tier !== 'cloud' : !options.apiKey);
|
|
232
244
|
const provider = resolvedDeepMode?.provider || options.provider || 'cloud';
|
|
233
245
|
console.log('');
|
|
234
246
|
if (report.status === 'PASS') {
|
|
@@ -245,10 +257,10 @@ function renderDeepOutput(report, config, options, resolvedDeepMode) {
|
|
|
245
257
|
console.log('');
|
|
246
258
|
// Privacy badge โ this IS the marketing
|
|
247
259
|
if (isLocal) {
|
|
248
|
-
console.log(chalk.green(' ๐
|
|
260
|
+
console.log(chalk.green(' ๐ Local sidecar/model execution. Code remains on this machine.'));
|
|
249
261
|
}
|
|
250
262
|
else {
|
|
251
|
-
console.log(chalk.yellow(` โ๏ธ Code
|
|
263
|
+
console.log(chalk.yellow(` โ๏ธ Cloud provider execution. Code context may be sent to ${provider} API.`));
|
|
252
264
|
}
|
|
253
265
|
// Deep stats
|
|
254
266
|
if (stats.deep) {
|
|
@@ -285,16 +285,16 @@ export function loadData(raw: unknown) {
|
|
|
285
285
|
return parsed;
|
|
286
286
|
}
|
|
287
287
|
`.trim());
|
|
288
|
-
// Issue 4:
|
|
288
|
+
// Issue 4: Placeholder markers
|
|
289
289
|
await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
|
|
290
|
-
//
|
|
291
|
-
//
|
|
290
|
+
// NOTE: Claude suggested this but I need to review
|
|
291
|
+
// NOTE: This function has edge cases
|
|
292
292
|
export function formatDate(date: Date): string {
|
|
293
293
|
return date.toISOString().split('T')[0];
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
export function sanitizeInput(input: string): string {
|
|
297
|
-
//
|
|
297
|
+
// NOTE: Add proper sanitization
|
|
298
298
|
return input.trim();
|
|
299
299
|
}
|
|
300
300
|
`.trim());
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
|
+
import { loadSettings, resolveDeepOptions, isModelCached, createProvider } from '@rigour-labs/core';
|
|
7
|
+
function runText(command, args) {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync(command, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function listRigourPaths() {
|
|
16
|
+
if (process.platform === 'win32') {
|
|
17
|
+
const output = runText('where', ['rigour']);
|
|
18
|
+
return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
const output = runText('which', ['-a', 'rigour']);
|
|
21
|
+
return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
function getRigourVersionFromPath(binaryPath) {
|
|
24
|
+
try {
|
|
25
|
+
const output = execFileSync(binaryPath, ['--version'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
26
|
+
return output.split(/\r?\n/)[0]?.trim() || 'unknown';
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function detectInstallKind(binaryPath) {
|
|
33
|
+
const normalizedInput = binaryPath.replace(/\\/g, '/');
|
|
34
|
+
let resolved = binaryPath;
|
|
35
|
+
try {
|
|
36
|
+
resolved = fs.realpathSync(binaryPath);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Keep original path
|
|
40
|
+
}
|
|
41
|
+
const normalizedResolved = resolved.replace(/\\/g, '/');
|
|
42
|
+
const homebrewSignals = [
|
|
43
|
+
'/Cellar/rigour/',
|
|
44
|
+
'/opt/rigour/',
|
|
45
|
+
'/opt/homebrew/bin/rigour',
|
|
46
|
+
'/usr/local/bin/rigour',
|
|
47
|
+
];
|
|
48
|
+
if (homebrewSignals.some((signal) => normalizedInput.includes(signal) || normalizedResolved.includes(signal))) {
|
|
49
|
+
return 'homebrew';
|
|
50
|
+
}
|
|
51
|
+
if (normalizedInput.includes('@rigour-labs') || normalizedInput.includes('node_modules') ||
|
|
52
|
+
normalizedResolved.includes('@rigour-labs') || normalizedResolved.includes('node_modules')) {
|
|
53
|
+
return 'npm';
|
|
54
|
+
}
|
|
55
|
+
return 'unknown';
|
|
56
|
+
}
|
|
57
|
+
export function hasVersionShadowing(versions) {
|
|
58
|
+
const normalized = versions.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
59
|
+
return new Set(normalized).size > 1;
|
|
60
|
+
}
|
|
61
|
+
export async function doctorCommand() {
|
|
62
|
+
console.log(chalk.bold.cyan('\nRigour Doctor\n'));
|
|
63
|
+
const paths = Array.from(new Set(listRigourPaths()));
|
|
64
|
+
if (paths.length === 0) {
|
|
65
|
+
console.log(chalk.red('โ rigour not found in PATH'));
|
|
66
|
+
console.log(chalk.dim(' Install with: npm i -g @rigour-labs/cli OR brew install rigour-labs/tap/rigour\n'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(chalk.bold('CLI Path Check'));
|
|
70
|
+
const entries = paths.map((p) => ({
|
|
71
|
+
path: p,
|
|
72
|
+
version: getRigourVersionFromPath(p),
|
|
73
|
+
kind: detectInstallKind(p),
|
|
74
|
+
}));
|
|
75
|
+
entries.forEach((entry, index) => {
|
|
76
|
+
const active = index === 0 ? chalk.green(' (active)') : '';
|
|
77
|
+
console.log(` - ${entry.path} ${chalk.dim(`[${entry.kind}] v${entry.version}`)}${active}`);
|
|
78
|
+
});
|
|
79
|
+
const distinctVersions = Array.from(new Set(entries.map((entry) => entry.version)));
|
|
80
|
+
if (entries.length > 1 && hasVersionShadowing(distinctVersions)) {
|
|
81
|
+
console.log(chalk.yellow('\nโ Multiple rigour binaries with different versions detected.'));
|
|
82
|
+
console.log(chalk.dim(' This can shadow upgrades and cause "still old version" confusion.'));
|
|
83
|
+
if (process.platform === 'win32') {
|
|
84
|
+
console.log(chalk.dim(' Run: where rigour'));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log(chalk.dim(' Run: which -a rigour'));
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.dim(' Keep one install channel active (brew or npm global), then relink PATH order.\n'));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(chalk.green(' โ PATH order/version state looks consistent.\n'));
|
|
93
|
+
}
|
|
94
|
+
console.log(chalk.bold('Deep Mode Readiness'));
|
|
95
|
+
const settings = loadSettings();
|
|
96
|
+
const resolved = resolveDeepOptions({});
|
|
97
|
+
const defaultProvider = resolved.provider || settings.deep?.defaultProvider || 'anthropic';
|
|
98
|
+
const defaultIsCloud = !!resolved.apiKey && defaultProvider !== 'local';
|
|
99
|
+
const hasAnyApiKey = !!(settings.providers && Object.keys(settings.providers).some((k) => !!settings.providers?.[k]));
|
|
100
|
+
console.log(` - API keys configured: ${hasAnyApiKey ? chalk.green('yes') : chalk.yellow('no')}`);
|
|
101
|
+
console.log(` - Deep default provider: ${chalk.cyan(defaultProvider)}`);
|
|
102
|
+
if (defaultIsCloud) {
|
|
103
|
+
console.log(chalk.yellow(` โ Deep defaults to cloud (${defaultProvider}) when you run \`rigour check --deep\`.`));
|
|
104
|
+
console.log(chalk.dim(' Force local any time with: rigour check --deep --provider local'));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(chalk.green(' โ Deep defaults to local execution.'));
|
|
108
|
+
}
|
|
109
|
+
const provider = createProvider({ enabled: true, provider: 'local' });
|
|
110
|
+
const sidecarAvailable = await provider.isAvailable();
|
|
111
|
+
provider.dispose();
|
|
112
|
+
const deepModelCached = await isModelCached('deep');
|
|
113
|
+
const proModelCached = await isModelCached('pro');
|
|
114
|
+
console.log(` - Local inference binary: ${sidecarAvailable ? chalk.green('ready') : chalk.yellow('missing')}`);
|
|
115
|
+
console.log(` - Local deep model cache: ${deepModelCached ? chalk.green('ready') : chalk.yellow('not cached')}`);
|
|
116
|
+
console.log(` - Local pro model cache: ${proModelCached ? chalk.green('ready') : chalk.dim('not cached')}`);
|
|
117
|
+
if (!sidecarAvailable || !deepModelCached) {
|
|
118
|
+
console.log(chalk.dim('\n Local bootstrap command: rigour check --deep --provider local'));
|
|
119
|
+
}
|
|
120
|
+
const rigourHome = path.join(os.homedir(), '.rigour');
|
|
121
|
+
console.log(chalk.dim(` Rigour home: ${rigourHome}\n`));
|
|
122
|
+
console.log(chalk.bold('Recommended Baseline'));
|
|
123
|
+
console.log(chalk.dim(' 1) rigour doctor'));
|
|
124
|
+
console.log(chalk.dim(' 2) rigour check --deep --provider local'));
|
|
125
|
+
console.log(chalk.dim(' 3) rigour check --deep -k <KEY> --provider <name>'));
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { detectInstallKind, hasVersionShadowing } from './doctor.js';
|
|
3
|
+
describe('doctor helpers', () => {
|
|
4
|
+
it('classifies Homebrew paths', () => {
|
|
5
|
+
expect(detectInstallKind('/opt/homebrew/bin/rigour')).toBe('homebrew');
|
|
6
|
+
expect(detectInstallKind('/usr/local/bin/rigour')).toBe('homebrew');
|
|
7
|
+
expect(detectInstallKind('/opt/homebrew/Cellar/rigour/4.0.5/bin/rigour')).toBe('homebrew');
|
|
8
|
+
});
|
|
9
|
+
it('classifies npm/global node_modules paths', () => {
|
|
10
|
+
expect(detectInstallKind('/opt/homebrew/lib/node_modules/@rigour-labs/cli/dist/cli.js')).toBe('npm');
|
|
11
|
+
expect(detectInstallKind('/Users/test/.nvm/versions/node/v22.0.0/lib/node_modules/@rigour-labs/cli/dist/cli.js')).toBe('npm');
|
|
12
|
+
});
|
|
13
|
+
it('returns unknown for unrelated paths', () => {
|
|
14
|
+
expect(detectInstallKind('/usr/bin/rigour')).toBe('unknown');
|
|
15
|
+
});
|
|
16
|
+
it('detects version shadowing only when versions differ', () => {
|
|
17
|
+
expect(hasVersionShadowing(['4.0.5', '4.0.5'])).toBe(false);
|
|
18
|
+
expect(hasVersionShadowing(['4.0.5', '2.0.0'])).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
});
|
package/dist/commands/guide.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function guideCommand():
|
|
1
|
+
export declare function guideCommand(): void;
|
package/dist/commands/guide.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
export
|
|
2
|
+
export function guideCommand() {
|
|
3
3
|
console.log(chalk.bold.cyan('\n๐ก๏ธ Rigour Labs | The Engineering Guide\n'));
|
|
4
4
|
console.log(chalk.bold('Getting Started:'));
|
|
5
5
|
console.log(chalk.dim(' 1. Run ') + chalk.cyan('rigour init') + chalk.dim(' to detect your project role and apply standards.'));
|
package/dist/commands/hooks.d.ts
CHANGED
|
@@ -19,4 +19,11 @@ export interface HooksOptions {
|
|
|
19
19
|
force?: boolean;
|
|
20
20
|
block?: boolean;
|
|
21
21
|
}
|
|
22
|
+
export interface HooksCheckOptions {
|
|
23
|
+
files?: string;
|
|
24
|
+
stdin?: boolean;
|
|
25
|
+
block?: boolean;
|
|
26
|
+
timeout?: string;
|
|
27
|
+
}
|
|
22
28
|
export declare function hooksInitCommand(cwd: string, options?: HooksOptions): Promise<void>;
|
|
29
|
+
export declare function hooksCheckCommand(cwd: string, options?: HooksCheckOptions): Promise<void>;
|
package/dist/commands/hooks.js
CHANGED
|
@@ -17,6 +17,7 @@ import fs from 'fs-extra';
|
|
|
17
17
|
import path from 'path';
|
|
18
18
|
import chalk from 'chalk';
|
|
19
19
|
import { randomUUID } from 'crypto';
|
|
20
|
+
import { runHookChecker } from '@rigour-labs/core';
|
|
20
21
|
// โโ Studio event logging โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
21
22
|
async function logStudioEvent(cwd, event) {
|
|
22
23
|
try {
|
|
@@ -53,12 +54,21 @@ function detectTools(cwd) {
|
|
|
53
54
|
}
|
|
54
55
|
return detected;
|
|
55
56
|
}
|
|
56
|
-
function
|
|
57
|
+
function resolveCheckerCommand(cwd) {
|
|
57
58
|
const localPath = path.join(cwd, 'node_modules', '@rigour-labs', 'core', 'dist', 'hooks', 'standalone-checker.js');
|
|
58
59
|
if (fs.existsSync(localPath)) {
|
|
59
|
-
return localPath;
|
|
60
|
+
return { command: 'node', args: [localPath] };
|
|
60
61
|
}
|
|
61
|
-
return '
|
|
62
|
+
return { command: 'rigour', args: ['hooks', 'check'] };
|
|
63
|
+
}
|
|
64
|
+
function shellEscape(arg) {
|
|
65
|
+
if (/^[A-Za-z0-9_/@%+=:,.-]+$/.test(arg)) {
|
|
66
|
+
return arg;
|
|
67
|
+
}
|
|
68
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
69
|
+
}
|
|
70
|
+
function checkerToShellCommand(spec) {
|
|
71
|
+
return [spec.command, ...spec.args].map(shellEscape).join(' ');
|
|
62
72
|
}
|
|
63
73
|
// โโ Tool resolution (from --tool flag or auto-detect) โโโโโโโโโโโโโโโโ
|
|
64
74
|
const ALL_TOOLS = ['claude', 'cursor', 'cline', 'windsurf'];
|
|
@@ -86,15 +96,16 @@ function resolveTools(cwd, toolFlag) {
|
|
|
86
96
|
return detected;
|
|
87
97
|
}
|
|
88
98
|
// โโ Per-tool hook generators โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
89
|
-
function generateClaudeHooks(
|
|
99
|
+
function generateClaudeHooks(checker, block) {
|
|
90
100
|
const blockFlag = block ? ' --block' : '';
|
|
101
|
+
const checkerCommand = checkerToShellCommand(checker);
|
|
91
102
|
const settings = {
|
|
92
103
|
hooks: {
|
|
93
104
|
PostToolUse: [{
|
|
94
105
|
matcher: "Write|Edit|MultiEdit",
|
|
95
106
|
hooks: [{
|
|
96
107
|
type: "command",
|
|
97
|
-
command:
|
|
108
|
+
command: `${checkerCommand} --files "$TOOL_INPUT_file_path"${blockFlag}`,
|
|
98
109
|
}]
|
|
99
110
|
}]
|
|
100
111
|
}
|
|
@@ -105,10 +116,12 @@ function generateClaudeHooks(checkerPath, block) {
|
|
|
105
116
|
description: 'Claude Code PostToolUse hook',
|
|
106
117
|
}];
|
|
107
118
|
}
|
|
108
|
-
function generateCursorHooks(
|
|
119
|
+
function generateCursorHooks(checker, block) {
|
|
120
|
+
const blockFlag = block ? ' --block' : '';
|
|
121
|
+
const checkerCommand = checkerToShellCommand(checker);
|
|
109
122
|
const hooks = {
|
|
110
123
|
version: 1,
|
|
111
|
-
hooks: { afterFileEdit: [{ command:
|
|
124
|
+
hooks: { afterFileEdit: [{ command: `${checkerCommand} --stdin${blockFlag}` }] }
|
|
112
125
|
};
|
|
113
126
|
return [{
|
|
114
127
|
path: '.cursor/hooks.json',
|
|
@@ -116,8 +129,8 @@ function generateCursorHooks(checkerPath, _block) {
|
|
|
116
129
|
description: 'Cursor afterFileEdit hook config',
|
|
117
130
|
}];
|
|
118
131
|
}
|
|
119
|
-
function generateClineHooks(
|
|
120
|
-
const script = buildClineScript(
|
|
132
|
+
function generateClineHooks(checker, block) {
|
|
133
|
+
const script = buildClineScript(checker, block);
|
|
121
134
|
return [{
|
|
122
135
|
path: '.clinerules/hooks/PostToolUse',
|
|
123
136
|
content: script,
|
|
@@ -125,7 +138,8 @@ function generateClineHooks(checkerPath, _block) {
|
|
|
125
138
|
description: 'Cline PostToolUse executable hook',
|
|
126
139
|
}];
|
|
127
140
|
}
|
|
128
|
-
function buildClineScript(
|
|
141
|
+
function buildClineScript(checker, block) {
|
|
142
|
+
const blockArgLiteral = block ? `, '--block'` : '';
|
|
129
143
|
return `#!/usr/bin/env node
|
|
130
144
|
/**
|
|
131
145
|
* Cline PostToolUse hook for Rigour.
|
|
@@ -148,11 +162,21 @@ process.stdin.on('end', async () => {
|
|
|
148
162
|
return;
|
|
149
163
|
}
|
|
150
164
|
|
|
151
|
-
const {
|
|
152
|
-
const
|
|
153
|
-
|
|
165
|
+
const { spawnSync } = require('child_process');
|
|
166
|
+
const command = ${JSON.stringify(checker.command)};
|
|
167
|
+
const baseArgs = ${JSON.stringify(checker.args)};
|
|
168
|
+
const proc = spawnSync(
|
|
169
|
+
command,
|
|
170
|
+
[...baseArgs, '--files', filePath${blockArgLiteral}],
|
|
154
171
|
{ encoding: 'utf-8', timeout: 5000 }
|
|
155
172
|
);
|
|
173
|
+
if (proc.error) {
|
|
174
|
+
throw proc.error;
|
|
175
|
+
}
|
|
176
|
+
const raw = (proc.stdout || '').trim();
|
|
177
|
+
if (!raw) {
|
|
178
|
+
throw new Error(proc.stderr || 'Rigour hook checker returned no output');
|
|
179
|
+
}
|
|
156
180
|
const result = JSON.parse(raw);
|
|
157
181
|
if (result.status === 'fail') {
|
|
158
182
|
const msgs = result.failures
|
|
@@ -171,10 +195,12 @@ process.stdin.on('end', async () => {
|
|
|
171
195
|
});
|
|
172
196
|
`;
|
|
173
197
|
}
|
|
174
|
-
function generateWindsurfHooks(
|
|
198
|
+
function generateWindsurfHooks(checker, block) {
|
|
199
|
+
const blockFlag = block ? ' --block' : '';
|
|
200
|
+
const checkerCommand = checkerToShellCommand(checker);
|
|
175
201
|
const hooks = {
|
|
176
202
|
version: 1,
|
|
177
|
-
hooks: { post_write_code: [{ command:
|
|
203
|
+
hooks: { post_write_code: [{ command: `${checkerCommand} --stdin${blockFlag}` }] }
|
|
178
204
|
};
|
|
179
205
|
return [{
|
|
180
206
|
path: '.windsurf/hooks.json',
|
|
@@ -245,12 +271,12 @@ export async function hooksInitCommand(cwd, options = {}) {
|
|
|
245
271
|
arguments: { tool: options.tool, dryRun: options.dryRun },
|
|
246
272
|
});
|
|
247
273
|
const tools = resolveTools(cwd, options.tool);
|
|
248
|
-
const
|
|
274
|
+
const checker = resolveCheckerCommand(cwd);
|
|
249
275
|
const block = !!options.block;
|
|
250
276
|
// Collect generated files from all tools
|
|
251
277
|
const allFiles = [];
|
|
252
278
|
for (const tool of tools) {
|
|
253
|
-
allFiles.push(...GENERATORS[tool](
|
|
279
|
+
allFiles.push(...GENERATORS[tool](checker, block));
|
|
254
280
|
}
|
|
255
281
|
if (options.dryRun) {
|
|
256
282
|
printDryRun(allFiles);
|
|
@@ -272,3 +298,59 @@ export async function hooksInitCommand(cwd, options = {}) {
|
|
|
272
298
|
content: [{ type: 'text', text: `Generated hooks for: ${tools.join(', ')}` }],
|
|
273
299
|
});
|
|
274
300
|
}
|
|
301
|
+
async function readStdin() {
|
|
302
|
+
const chunks = [];
|
|
303
|
+
for await (const chunk of process.stdin) {
|
|
304
|
+
chunks.push(chunk);
|
|
305
|
+
}
|
|
306
|
+
return Buffer.concat(chunks).toString('utf-8').trim();
|
|
307
|
+
}
|
|
308
|
+
function parseStdinFiles(input) {
|
|
309
|
+
if (!input) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const payload = JSON.parse(input);
|
|
314
|
+
if (Array.isArray(payload.files)) {
|
|
315
|
+
return payload.files;
|
|
316
|
+
}
|
|
317
|
+
if (payload.file_path) {
|
|
318
|
+
return [payload.file_path];
|
|
319
|
+
}
|
|
320
|
+
if (payload.toolInput?.path) {
|
|
321
|
+
return [payload.toolInput.path];
|
|
322
|
+
}
|
|
323
|
+
if (payload.toolInput?.file_path) {
|
|
324
|
+
return [payload.toolInput.file_path];
|
|
325
|
+
}
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return input.split('\n').map(l => l.trim()).filter(Boolean);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
export async function hooksCheckCommand(cwd, options = {}) {
|
|
333
|
+
const timeout = options.timeout ? Number(options.timeout) : 5000;
|
|
334
|
+
const files = options.stdin
|
|
335
|
+
? parseStdinFiles(await readStdin())
|
|
336
|
+
: (options.files ?? '').split(',').map(f => f.trim()).filter(Boolean);
|
|
337
|
+
if (files.length === 0) {
|
|
338
|
+
process.stdout.write(JSON.stringify({ status: 'pass', failures: [], duration_ms: 0 }));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const result = await runHookChecker({
|
|
342
|
+
cwd,
|
|
343
|
+
files,
|
|
344
|
+
timeout_ms: Number.isFinite(timeout) ? timeout : 5000,
|
|
345
|
+
});
|
|
346
|
+
process.stdout.write(JSON.stringify(result));
|
|
347
|
+
if (result.status === 'fail') {
|
|
348
|
+
for (const failure of result.failures) {
|
|
349
|
+
const loc = failure.line ? `:${failure.line}` : '';
|
|
350
|
+
process.stderr.write(`[rigour/${failure.gate}] ${failure.file}${loc}: ${failure.message}\n`);
|
|
351
|
+
}
|
|
352
|
+
if (options.block) {
|
|
353
|
+
process.exitCode = 2;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for hooks init command.
|
|
3
3
|
*/
|
|
4
4
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
import { hooksInitCommand } from './hooks.js';
|
|
5
|
+
import { hooksInitCommand, hooksCheckCommand } from './hooks.js';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as os from 'os';
|
|
@@ -30,6 +30,7 @@ describe('hooksInitCommand', () => {
|
|
|
30
30
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
31
31
|
expect(settings.hooks).toBeDefined();
|
|
32
32
|
expect(settings.hooks.PostToolUse).toBeDefined();
|
|
33
|
+
expect(settings.hooks.PostToolUse[0].hooks[0].command).toContain('rigour hooks check');
|
|
33
34
|
});
|
|
34
35
|
it('should generate Cursor hooks', async () => {
|
|
35
36
|
await hooksInitCommand(testDir, { tool: 'cursor' });
|
|
@@ -74,4 +75,48 @@ describe('hooksInitCommand', () => {
|
|
|
74
75
|
const content = fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf-8');
|
|
75
76
|
expect(content).toContain('PostToolUse');
|
|
76
77
|
});
|
|
78
|
+
it('should propagate --block to generated hook commands', async () => {
|
|
79
|
+
await hooksInitCommand(testDir, { tool: 'all', force: true, block: true });
|
|
80
|
+
const claude = JSON.parse(fs.readFileSync(path.join(testDir, '.claude', 'settings.json'), 'utf-8'));
|
|
81
|
+
const cursor = JSON.parse(fs.readFileSync(path.join(testDir, '.cursor', 'hooks.json'), 'utf-8'));
|
|
82
|
+
const windsurf = JSON.parse(fs.readFileSync(path.join(testDir, '.windsurf', 'hooks.json'), 'utf-8'));
|
|
83
|
+
const clineScript = fs.readFileSync(path.join(testDir, '.clinerules', 'hooks', 'PostToolUse'), 'utf-8');
|
|
84
|
+
expect(claude.hooks.PostToolUse[0].hooks[0].command).toContain('--block');
|
|
85
|
+
expect(cursor.hooks.afterFileEdit[0].command).toContain('--block');
|
|
86
|
+
expect(windsurf.hooks.post_write_code[0].command).toContain('--block');
|
|
87
|
+
expect(clineScript).toContain('--block');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('hooksCheckCommand', () => {
|
|
91
|
+
let testDir;
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-check-test-'));
|
|
94
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
95
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
96
|
+
});
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
99
|
+
vi.restoreAllMocks();
|
|
100
|
+
});
|
|
101
|
+
it('should return pass JSON when file is clean', async () => {
|
|
102
|
+
const filePath = path.join(testDir, 'ok.ts');
|
|
103
|
+
fs.writeFileSync(filePath, 'export const x = 1;\n');
|
|
104
|
+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
105
|
+
await hooksCheckCommand(testDir, { files: 'ok.ts' });
|
|
106
|
+
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
|
|
107
|
+
expect(output).toContain('"status":"pass"');
|
|
108
|
+
});
|
|
109
|
+
it('should return fail JSON and set exit code 2 in block mode', async () => {
|
|
110
|
+
const filePath = path.join(testDir, 'bad.ts');
|
|
111
|
+
fs.writeFileSync(filePath, "const password = 'abcdefghijklmnopqrstuvwxyz12345';\n");
|
|
112
|
+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
113
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
114
|
+
const originalExitCode = process.exitCode;
|
|
115
|
+
await hooksCheckCommand(testDir, { files: 'bad.ts', block: true });
|
|
116
|
+
const output = stdoutSpy.mock.calls.map(call => String(call[0])).join('');
|
|
117
|
+
expect(output).toContain('"status":"fail"');
|
|
118
|
+
expect(stderrSpy).toHaveBeenCalled();
|
|
119
|
+
expect(process.exitCode).toBe(2);
|
|
120
|
+
process.exitCode = originalExitCode;
|
|
121
|
+
});
|
|
77
122
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -393,8 +393,8 @@ async function checkPrerequisites() {
|
|
|
393
393
|
console.log(chalk.yellow(' โ No API keys configured'));
|
|
394
394
|
}
|
|
395
395
|
// Check 2: Local model availability
|
|
396
|
-
const hasDeepModel = isModelCached('deep');
|
|
397
|
-
const hasProModel = isModelCached('pro');
|
|
396
|
+
const hasDeepModel = await isModelCached('deep');
|
|
397
|
+
const hasProModel = await isModelCached('pro');
|
|
398
398
|
if (hasDeepModel || hasProModel) {
|
|
399
399
|
const models = [];
|
|
400
400
|
if (hasDeepModel)
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Like Claude Code's settings.json or Gemini CLI's config.
|
|
5
5
|
* Stores API keys, default provider, multi-agent config, CLI preferences.
|
|
6
6
|
*/
|
|
7
|
-
export declare function settingsShowCommand():
|
|
8
|
-
export declare function settingsSetKeyCommand(provider: string, apiKey: string):
|
|
9
|
-
export declare function settingsRemoveKeyCommand(provider: string):
|
|
10
|
-
export declare function settingsSetCommand(key: string, value: string):
|
|
11
|
-
export declare function settingsGetCommand(key: string):
|
|
12
|
-
export declare function settingsResetCommand():
|
|
13
|
-
export declare function settingsPathCommand():
|
|
7
|
+
export declare function settingsShowCommand(): void;
|
|
8
|
+
export declare function settingsSetKeyCommand(provider: string, apiKey: string): void;
|
|
9
|
+
export declare function settingsRemoveKeyCommand(provider: string): void;
|
|
10
|
+
export declare function settingsSetCommand(key: string, value: string): void;
|
|
11
|
+
export declare function settingsGetCommand(key: string): void;
|
|
12
|
+
export declare function settingsResetCommand(): void;
|
|
13
|
+
export declare function settingsPathCommand(): void;
|