@rigour-labs/cli 4.0.5 โ†’ 4.1.1

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 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
- > **Zero cloud. Zero telemetry. PASS/FAIL is always free.**
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')
@@ -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' : (resolvedDeepMode?.isLocal ?? !options.apiKey);
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(' ๐Ÿ”’ 100% local. Your code never left this machine.'));
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 was sent to ${provider} API.`));
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: TODO markers
288
+ // Issue 4: Placeholder markers
289
289
  await fs.writeFile(path.join(dir, 'src', 'utils.ts'), `
290
- // TODO: Claude suggested this but I need to review
291
- // FIXME: This function has edge cases
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
- // TODO: Add proper sanitization
297
+ // NOTE: Add proper sanitization
298
298
  return input.trim();
299
299
  }
300
300
  `.trim());
@@ -0,0 +1,3 @@
1
+ export declare function detectInstallKind(binaryPath: string): string;
2
+ export declare function hasVersionShadowing(versions: string[]): boolean;
3
+ export declare function doctorCommand(): Promise<void>;
@@ -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
+ });
@@ -1 +1 @@
1
- export declare function guideCommand(): Promise<void>;
1
+ export declare function guideCommand(): void;
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- export async function guideCommand() {
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.'));
@@ -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>;
@@ -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 resolveCheckerPath(cwd) {
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 'npx rigour-hook-check';
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(checkerPath, block) {
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: `node ${checkerPath} --files "$TOOL_INPUT_file_path"${blockFlag}`,
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(checkerPath, _block) {
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: `node ${checkerPath} --stdin` }] }
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(checkerPath, _block) {
120
- const script = buildClineScript(checkerPath);
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(checkerPath) {
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 { execSync } = require('child_process');
152
- const raw = execSync(
153
- \`node ${checkerPath} --files "\${filePath}"\`,
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(checkerPath, _block) {
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: `node ${checkerPath} --stdin` }] }
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 checkerPath = resolveCheckerPath(cwd);
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](checkerPath, block));
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
  });
@@ -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(): 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>;
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;