@rigour-labs/cli 2.22.0 → 3.0.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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for hooks init command.
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
+ import { hooksInitCommand } from './hooks.js';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import yaml from 'yaml';
10
+ describe('hooksInitCommand', () => {
11
+ let testDir;
12
+ beforeEach(() => {
13
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-test-'));
14
+ // Write minimal rigour.yml
15
+ fs.writeFileSync(path.join(testDir, 'rigour.yml'), yaml.stringify({
16
+ version: 1,
17
+ gates: { max_file_lines: 500 },
18
+ }));
19
+ vi.spyOn(console, 'log').mockImplementation(() => { });
20
+ vi.spyOn(console, 'error').mockImplementation(() => { });
21
+ });
22
+ afterEach(() => {
23
+ fs.rmSync(testDir, { recursive: true, force: true });
24
+ vi.restoreAllMocks();
25
+ });
26
+ it('should generate Claude hooks', async () => {
27
+ await hooksInitCommand(testDir, { tool: 'claude' });
28
+ const settingsPath = path.join(testDir, '.claude', 'settings.json');
29
+ expect(fs.existsSync(settingsPath)).toBe(true);
30
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
31
+ expect(settings.hooks).toBeDefined();
32
+ expect(settings.hooks.PostToolUse).toBeDefined();
33
+ });
34
+ it('should generate Cursor hooks', async () => {
35
+ await hooksInitCommand(testDir, { tool: 'cursor' });
36
+ const hooksPath = path.join(testDir, '.cursor', 'hooks.json');
37
+ expect(fs.existsSync(hooksPath)).toBe(true);
38
+ const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
39
+ expect(hooks.hooks).toBeDefined();
40
+ });
41
+ it('should generate Cline hooks', async () => {
42
+ await hooksInitCommand(testDir, { tool: 'cline' });
43
+ const hookPath = path.join(testDir, '.clinerules', 'hooks', 'PostToolUse');
44
+ expect(fs.existsSync(hookPath)).toBe(true);
45
+ });
46
+ it('should generate Windsurf hooks', async () => {
47
+ await hooksInitCommand(testDir, { tool: 'windsurf' });
48
+ const hooksPath = path.join(testDir, '.windsurf', 'hooks.json');
49
+ expect(fs.existsSync(hooksPath)).toBe(true);
50
+ });
51
+ it('should support dry-run mode', async () => {
52
+ await hooksInitCommand(testDir, { tool: 'claude', dryRun: true });
53
+ // Dry run should NOT create files
54
+ const settingsPath = path.join(testDir, '.claude', 'settings.json');
55
+ expect(fs.existsSync(settingsPath)).toBe(false);
56
+ });
57
+ it('should not overwrite without --force', async () => {
58
+ // Create existing file
59
+ const claudeDir = path.join(testDir, '.claude');
60
+ fs.mkdirSync(claudeDir, { recursive: true });
61
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), '{"existing": true}');
62
+ await hooksInitCommand(testDir, { tool: 'claude' });
63
+ // Should keep existing content
64
+ const content = fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf-8');
65
+ expect(content).toContain('existing');
66
+ });
67
+ it('should overwrite with --force', async () => {
68
+ // Create existing file
69
+ const claudeDir = path.join(testDir, '.claude');
70
+ fs.mkdirSync(claudeDir, { recursive: true });
71
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), '{"existing": true}');
72
+ await hooksInitCommand(testDir, { tool: 'claude', force: true });
73
+ // Should have new hooks content
74
+ const content = fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf-8');
75
+ expect(content).toContain('PostToolUse');
76
+ });
77
+ });
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
5
  import { DiscoveryService } from '@rigour-labs/core';
6
6
  import { CODE_QUALITY_RULES, DEBUGGING_RULES, COLLABORATION_RULES, AGNOSTIC_AI_INSTRUCTIONS } from './constants.js';
7
+ import { hooksInitCommand } from './hooks.js';
7
8
  import { randomUUID } from 'crypto';
8
9
  // Helper to log events for Rigour Studio
9
10
  async function logStudioEvent(cwd, event) {
@@ -322,7 +323,9 @@ ${ruleContent}`;
322
323
  console.log(chalk.green('✔ Initialized Windsurf Handshake (.windsurfrules)'));
323
324
  }
324
325
  }
325
- // 3. Update .gitignore
326
+ // 3. Auto-initialize hooks for detected AI coding tools
327
+ await initHooksForDetectedTools(cwd, detectedIDE);
328
+ // 4. Update .gitignore
326
329
  const gitignorePath = path.join(cwd, '.gitignore');
327
330
  const ignorePatterns = ['rigour-report.json', 'rigour-fix-packet.json', '.rigour/'];
328
331
  try {
@@ -368,3 +371,24 @@ ${ruleContent}`;
368
371
  content: [{ type: "text", text: `Rigour Governance Initialized` }]
369
372
  });
370
373
  }
374
+ // Maps detected IDE to hook tool name
375
+ const IDE_TO_HOOK_TOOL = {
376
+ claude: 'claude',
377
+ cursor: 'cursor',
378
+ cline: 'cline',
379
+ windsurf: 'windsurf',
380
+ };
381
+ async function initHooksForDetectedTools(cwd, detectedIDE) {
382
+ const hookTool = IDE_TO_HOOK_TOOL[detectedIDE];
383
+ if (!hookTool) {
384
+ return; // Unknown IDE or no hook support (vscode, gemini, codex)
385
+ }
386
+ try {
387
+ console.log(chalk.dim(`\n Setting up real-time hooks for ${detectedIDE}...`));
388
+ await hooksInitCommand(cwd, { tool: hookTool });
389
+ }
390
+ catch {
391
+ // Non-fatal — hooks are a bonus, not a requirement
392
+ console.log(chalk.dim(` (Hooks setup skipped — run 'rigour hooks init' manually)`));
393
+ }
394
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Tests for init command — IDE detection, config generation, auto-hook integration.
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
+ import { initCommand } from './init.js';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ describe('initCommand', () => {
10
+ let testDir;
11
+ beforeEach(() => {
12
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'init-test-'));
13
+ // Minimal package.json for discovery
14
+ fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({
15
+ name: 'test-project',
16
+ dependencies: { express: '^4.0.0' },
17
+ }));
18
+ vi.spyOn(console, 'log').mockImplementation(() => { });
19
+ vi.spyOn(console, 'error').mockImplementation(() => { });
20
+ });
21
+ afterEach(() => {
22
+ fs.rmSync(testDir, { recursive: true, force: true });
23
+ vi.restoreAllMocks();
24
+ });
25
+ it('should create rigour.yml', async () => {
26
+ await initCommand(testDir);
27
+ const configPath = path.join(testDir, 'rigour.yml');
28
+ expect(fs.existsSync(configPath)).toBe(true);
29
+ const content = fs.readFileSync(configPath, 'utf-8');
30
+ expect(content).toContain('version');
31
+ });
32
+ it('should create docs/AGENT_INSTRUCTIONS.md', async () => {
33
+ await initCommand(testDir);
34
+ const docsPath = path.join(testDir, 'docs', 'AGENT_INSTRUCTIONS.md');
35
+ expect(fs.existsSync(docsPath)).toBe(true);
36
+ const content = fs.readFileSync(docsPath, 'utf-8');
37
+ expect(content).toContain('Rigour');
38
+ });
39
+ it('should support dry-run mode', async () => {
40
+ await initCommand(testDir, { dryRun: true });
41
+ // Dry run should NOT create rigour.yml
42
+ expect(fs.existsSync(path.join(testDir, 'rigour.yml'))).toBe(false);
43
+ });
44
+ it('should not overwrite without --force', async () => {
45
+ // Create existing rigour.yml
46
+ fs.writeFileSync(path.join(testDir, 'rigour.yml'), 'version: 1\nexisting: true\n');
47
+ await initCommand(testDir);
48
+ const content = fs.readFileSync(path.join(testDir, 'rigour.yml'), 'utf-8');
49
+ expect(content).toContain('existing');
50
+ });
51
+ it('should overwrite with --force', async () => {
52
+ // Create existing rigour.yml
53
+ fs.writeFileSync(path.join(testDir, 'rigour.yml'), 'version: 1\nexisting: true\n');
54
+ await initCommand(testDir, { force: true });
55
+ // Should create backup
56
+ expect(fs.existsSync(path.join(testDir, 'rigour.yml.bak'))).toBe(true);
57
+ // New config should not contain 'existing'
58
+ const content = fs.readFileSync(path.join(testDir, 'rigour.yml'), 'utf-8');
59
+ expect(content).not.toContain('existing: true');
60
+ });
61
+ it('should detect Claude IDE and create hooks', async () => {
62
+ // Create Claude marker
63
+ fs.mkdirSync(path.join(testDir, '.claude'), { recursive: true });
64
+ await initCommand(testDir);
65
+ // Should have created .claude/settings.json (hooks)
66
+ const settingsPath = path.join(testDir, '.claude', 'settings.json');
67
+ expect(fs.existsSync(settingsPath)).toBe(true);
68
+ });
69
+ it('should detect Cursor IDE and create hooks', async () => {
70
+ // Create Cursor marker
71
+ fs.mkdirSync(path.join(testDir, '.cursor'), { recursive: true });
72
+ await initCommand(testDir);
73
+ // Should have created .cursor/hooks.json
74
+ const hooksPath = path.join(testDir, '.cursor', 'hooks.json');
75
+ expect(fs.existsSync(hooksPath)).toBe(true);
76
+ });
77
+ it('should update .gitignore with rigour patterns', async () => {
78
+ await initCommand(testDir);
79
+ const gitignorePath = path.join(testDir, '.gitignore');
80
+ expect(fs.existsSync(gitignorePath)).toBe(true);
81
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
82
+ expect(content).toContain('rigour-report.json');
83
+ expect(content).toContain('.rigour/');
84
+ });
85
+ it('should create .rigour/memory.json for Studio', async () => {
86
+ await initCommand(testDir);
87
+ const memPath = path.join(testDir, '.rigour', 'memory.json');
88
+ expect(fs.existsSync(memPath)).toBe(true);
89
+ const mem = JSON.parse(fs.readFileSync(memPath, 'utf-8'));
90
+ expect(mem.memories.project_boot).toBeDefined();
91
+ });
92
+ it('should support --ide flag to target specific IDE', async () => {
93
+ await initCommand(testDir, { ide: 'windsurf' });
94
+ // Should create windsurf rules
95
+ expect(fs.existsSync(path.join(testDir, '.windsurfrules'))).toBe(true);
96
+ });
97
+ });
@@ -1,12 +1,13 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
+ import os from 'os';
4
5
  async function getInitCommand() {
5
6
  const { initCommand } = await import('./commands/init.js');
6
7
  return initCommand;
7
8
  }
8
9
  describe('Init Command Rules Verification', () => {
9
- const testDir = path.join(process.cwd(), 'temp-init-rules-test');
10
+ const testDir = path.join(os.tmpdir(), 'rigour-temp-init-rules-test-' + process.pid);
10
11
  beforeEach(async () => {
11
12
  await fs.ensureDir(testDir);
12
13
  });
@@ -1,12 +1,13 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
+ import os from 'os';
4
5
  async function getCheckCommand() {
5
6
  const { checkCommand } = await import('./commands/check.js');
6
7
  return checkCommand;
7
8
  }
8
9
  describe('CLI Smoke Test', () => {
9
- const testDir = path.join(process.cwd(), 'temp-smoke-test');
10
+ const testDir = path.join(os.tmpdir(), 'rigour-temp-smoke-test-' + process.pid);
10
11
  beforeEach(async () => {
11
12
  await fs.ensureDir(testDir);
12
13
  // @ts-ignore
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "2.22.0",
3
+ "version": "3.0.1",
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
+ "license": "MIT",
6
+ "homepage": "https://rigour.run",
7
+ "keywords": [
8
+ "quality-gates",
9
+ "ai-code-quality",
10
+ "cli",
11
+ "linter",
12
+ "static-analysis",
13
+ "claude",
14
+ "cursor",
15
+ "copilot",
16
+ "mcp",
17
+ "code-review",
18
+ "ci-cd"
19
+ ],
4
20
  "type": "module",
5
21
  "bin": {
6
22
  "rigour": "dist/cli.js"
@@ -28,7 +44,7 @@
28
44
  "inquirer": "9.2.16",
29
45
  "ora": "^8.0.1",
30
46
  "yaml": "^2.8.2",
31
- "@rigour-labs/core": "2.22.0"
47
+ "@rigour-labs/core": "3.0.1"
32
48
  },
33
49
  "devDependencies": {
34
50
  "@types/fs-extra": "^11.0.4",