@rigour-labs/cli 2.9.4 → 2.11.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.
@@ -0,0 +1,272 @@
1
+ import { Command } from 'commander';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { execa } from 'execa';
5
+ import fs from 'fs-extra';
6
+ import http from 'http';
7
+ import { randomUUID } from 'crypto';
8
+ export const studioCommand = new Command('studio')
9
+ .description('Launch Rigour Studio (Local-First Governance UI)')
10
+ .option('-p, --port <number>', 'Port to run the studio on', '3000')
11
+ .option('--dev', 'Run in development mode', true)
12
+ .action(async (options) => {
13
+ const cwd = process.cwd();
14
+ // Calculate the workspace root where the studio package lives
15
+ // This file is in packages/rigour-cli/src/commands/studio.ts (or dist/commands/studio.js)
16
+ const workspaceRoot = path.join(path.dirname(new URL(import.meta.url).pathname), '../../../../');
17
+ console.log(chalk.bold.cyan('\nšŸ›”ļø Launching Rigour Studio...'));
18
+ console.log(chalk.gray(`Project Root: ${cwd}`));
19
+ console.log(chalk.gray(`Shadowing interactions in ${path.join(cwd, '.rigour/events.jsonl')}\n`));
20
+ // For Phase 1, we start the studio in dev mode via pnpm
21
+ // This ensures the user has a live, hot-reloading governance dashboard
22
+ try {
23
+ // Start the Studio dev server in the workspace root
24
+ const studioProcess = execa('pnpm', ['--filter', '@rigour-labs/studio', 'dev', '--port', options.port], {
25
+ stdio: 'inherit',
26
+ shell: true,
27
+ cwd: workspaceRoot
28
+ });
29
+ // Start a small API server for events on port + 1
30
+ const apiPort = parseInt(options.port) + 1;
31
+ const eventsPath = path.join(cwd, '.rigour/events.jsonl');
32
+ const apiServer = http.createServer(async (req, res) => {
33
+ const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
34
+ // Set global CORS headers
35
+ res.setHeader('Access-Control-Allow-Origin', '*');
36
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
37
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
38
+ if (req.method === 'OPTIONS') {
39
+ res.writeHead(204);
40
+ res.end();
41
+ return;
42
+ }
43
+ if (url.pathname === '/api/events') {
44
+ res.writeHead(200, {
45
+ 'Content-Type': 'text/event-stream',
46
+ 'Cache-Control': 'no-cache',
47
+ 'Connection': 'keep-alive'
48
+ });
49
+ // Send existing events first
50
+ if (await fs.pathExists(eventsPath)) {
51
+ const content = await fs.readFile(eventsPath, 'utf8');
52
+ const lines = content.split('\n').filter(l => l.trim());
53
+ for (const line of lines) {
54
+ res.write(`data: ${line}\n\n`);
55
+ }
56
+ }
57
+ // Watch for new events
58
+ await fs.ensureDir(path.dirname(eventsPath));
59
+ const watcher = fs.watch(path.dirname(eventsPath), (eventType, filename) => {
60
+ if (filename === 'events.jsonl') {
61
+ fs.readFile(eventsPath, 'utf8').then(content => {
62
+ const lines = content.split('\n').filter(l => l.trim());
63
+ const lastLine = lines[lines.length - 1];
64
+ if (lastLine) {
65
+ res.write(`data: ${lastLine}\n\n`);
66
+ }
67
+ }).catch(() => { });
68
+ }
69
+ });
70
+ req.on('close', () => watcher.close());
71
+ }
72
+ else if (url.pathname === '/api/file') {
73
+ const filePath = url.searchParams.get('path');
74
+ if (!filePath) {
75
+ res.writeHead(400);
76
+ res.end('Missing path parameter');
77
+ return;
78
+ }
79
+ const absolutePath = path.resolve(cwd, filePath);
80
+ if (!absolutePath.startsWith(cwd)) {
81
+ res.writeHead(403);
82
+ res.end('Forbidden: Path outside project root');
83
+ return;
84
+ }
85
+ try {
86
+ const stats = await fs.stat(absolutePath);
87
+ if (!stats.isFile()) {
88
+ res.writeHead(400);
89
+ res.end('Path is not a file');
90
+ return;
91
+ }
92
+ const content = await fs.readFile(absolutePath, 'utf8');
93
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
94
+ res.end(content);
95
+ }
96
+ catch (e) {
97
+ res.writeHead(404);
98
+ res.end(`File not found: ${e.message}`);
99
+ }
100
+ }
101
+ else if (url.pathname === '/api/tree') {
102
+ try {
103
+ const getTree = async (dir) => {
104
+ const entries = await fs.readdir(dir, { withFileTypes: true });
105
+ let files = [];
106
+ const exclude = [
107
+ 'node_modules', '.git', '.rigour', '.github', '.vscode', '.cursor',
108
+ 'venv', '.venv', 'dist', 'build', 'out', 'target', '__pycache__'
109
+ ];
110
+ for (const entry of entries) {
111
+ if (exclude.includes(entry.name) || entry.name.startsWith('.')) {
112
+ if (entry.name !== '.cursorrules' && entry.name !== '.cursor') {
113
+ continue;
114
+ }
115
+ }
116
+ const fullPath = path.join(dir, entry.name);
117
+ if (entry.isDirectory()) {
118
+ files = [...files, ...(await getTree(fullPath))];
119
+ }
120
+ else {
121
+ files.push(path.relative(cwd, fullPath));
122
+ }
123
+ }
124
+ return files;
125
+ };
126
+ const tree = await getTree(cwd);
127
+ res.writeHead(200, { 'Content-Type': 'application/json' });
128
+ res.end(JSON.stringify(tree));
129
+ }
130
+ catch (e) {
131
+ res.writeHead(500);
132
+ res.end(e.message);
133
+ }
134
+ }
135
+ else if (url.pathname === '/api/config') {
136
+ try {
137
+ const configPath = path.join(cwd, 'rigour.yml');
138
+ if (await fs.pathExists(configPath)) {
139
+ const content = await fs.readFile(configPath, 'utf8');
140
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
141
+ res.end(content);
142
+ }
143
+ else {
144
+ res.writeHead(404);
145
+ res.end('rigour.yml not found');
146
+ }
147
+ }
148
+ catch (e) {
149
+ res.writeHead(500);
150
+ res.end(e.message);
151
+ }
152
+ }
153
+ else if (url.pathname === '/api/memory') {
154
+ try {
155
+ const memoryPath = path.join(cwd, '.rigour/memory.json');
156
+ if (await fs.pathExists(memoryPath)) {
157
+ const content = await fs.readFile(memoryPath, 'utf8');
158
+ res.writeHead(200, { 'Content-Type': 'application/json' });
159
+ res.end(content);
160
+ }
161
+ else {
162
+ res.end(JSON.stringify({}));
163
+ }
164
+ }
165
+ catch (e) {
166
+ res.writeHead(500);
167
+ res.end(e.message);
168
+ }
169
+ }
170
+ else if (url.pathname === '/api/index-stats') {
171
+ try {
172
+ const indexPath = path.join(cwd, '.rigour/patterns.json'); // Corrected path
173
+ if (await fs.pathExists(indexPath)) {
174
+ const content = await fs.readJson(indexPath);
175
+ res.writeHead(200, { 'Content-Type': 'application/json' });
176
+ res.end(JSON.stringify(content));
177
+ }
178
+ else {
179
+ res.end(JSON.stringify({ patterns: [], stats: { totalPatterns: 0, totalFiles: 0, byType: {} } }));
180
+ }
181
+ }
182
+ catch (e) {
183
+ res.writeHead(500);
184
+ res.end(e.message);
185
+ }
186
+ }
187
+ else if (url.pathname === '/api/index-search') {
188
+ const query = url.searchParams.get('q');
189
+ if (!query) {
190
+ res.writeHead(400);
191
+ res.end('Missing query parameter');
192
+ return;
193
+ }
194
+ try {
195
+ const { generateEmbedding, semanticSearch } = await import('@rigour-labs/core/pattern-index');
196
+ const indexPath = path.join(cwd, '.rigour/patterns.json');
197
+ if (!(await fs.pathExists(indexPath))) {
198
+ res.writeHead(404);
199
+ res.end('Index not found');
200
+ return;
201
+ }
202
+ const indexData = await fs.readJson(indexPath);
203
+ const queryVector = await generateEmbedding(query);
204
+ const similarities = semanticSearch(queryVector, indexData.patterns);
205
+ // Attach similarity to patterns and sort
206
+ const results = indexData.patterns.map((p, i) => ({
207
+ ...p,
208
+ similarity: similarities[i]
209
+ }))
210
+ .filter((p) => p.similarity > 0.3) // Threshold
211
+ .sort((a, b) => b.similarity - a.similarity)
212
+ .slice(0, 20);
213
+ res.writeHead(200, { 'Content-Type': 'application/json' });
214
+ res.end(JSON.stringify(results));
215
+ }
216
+ catch (e) {
217
+ res.writeHead(500);
218
+ res.end(e.message);
219
+ }
220
+ }
221
+ else if (url.pathname === '/api/arbitrate' && req.method === 'POST') {
222
+ let body = '';
223
+ req.on('data', chunk => body += chunk);
224
+ req.on('end', async () => {
225
+ try {
226
+ const decision = JSON.parse(body);
227
+ const logEntry = JSON.stringify({
228
+ id: randomUUID(),
229
+ timestamp: new Date().toISOString(),
230
+ tool: 'human_arbitration',
231
+ requestId: decision.requestId,
232
+ decision: decision.decision,
233
+ status: decision.decision === 'approve' ? 'success' : 'error',
234
+ arbitrated: true
235
+ }) + "\n";
236
+ await fs.appendFile(eventsPath, logEntry);
237
+ res.writeHead(200);
238
+ res.end(JSON.stringify({ success: true }));
239
+ }
240
+ catch (e) {
241
+ res.writeHead(500);
242
+ res.end(e.message);
243
+ }
244
+ });
245
+ return;
246
+ }
247
+ else {
248
+ res.writeHead(404);
249
+ res.end();
250
+ }
251
+ });
252
+ apiServer.listen(apiPort, () => {
253
+ console.log(chalk.gray(`API Streamer active on port ${apiPort}`));
254
+ });
255
+ // Open browser
256
+ setTimeout(async () => {
257
+ const url = `http://localhost:${options.port}`;
258
+ console.log(chalk.green(`\nāœ… Rigour Studio is live at ${chalk.bold(url)}`));
259
+ try {
260
+ await execa('open', [url]);
261
+ }
262
+ catch {
263
+ }
264
+ }, 2000);
265
+ await studioProcess;
266
+ }
267
+ catch (error) {
268
+ console.error(chalk.red(`\nāŒ Failed to launch Rigour Studio: ${error.message}`));
269
+ console.log(chalk.yellow('Make sure to run "pnpm install" in the root directory.\n'));
270
+ process.exit(1);
271
+ }
272
+ });
@@ -1,43 +1,43 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const vitest_1 = require("vitest");
7
- const init_js_1 = require("./commands/init.js");
8
- const fs_extra_1 = __importDefault(require("fs-extra"));
9
- const path_1 = __importDefault(require("path"));
10
- (0, vitest_1.describe)('Init Command Rules Verification', () => {
11
- const testDir = path_1.default.join(process.cwd(), 'temp-init-rules-test');
12
- (0, vitest_1.beforeEach)(async () => {
13
- await fs_extra_1.default.ensureDir(testDir);
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ async function getInitCommand() {
5
+ const { initCommand } = await import('./commands/init.js');
6
+ return initCommand;
7
+ }
8
+ describe('Init Command Rules Verification', () => {
9
+ const testDir = path.join(process.cwd(), 'temp-init-rules-test');
10
+ beforeEach(async () => {
11
+ await fs.ensureDir(testDir);
14
12
  });
15
- (0, vitest_1.afterEach)(async () => {
16
- await fs_extra_1.default.remove(testDir);
13
+ afterEach(async () => {
14
+ await fs.remove(testDir);
17
15
  });
18
- (0, vitest_1.it)('should create instructions with agnostic rules and cursor rules on init', async () => {
16
+ it('should create instructions with agnostic rules and cursor rules on init', async () => {
17
+ const initCommand = await getInitCommand();
19
18
  // Run init in test directory with all IDEs to verify rules in both locations
20
- await (0, init_js_1.initCommand)(testDir, { ide: 'all' });
21
- const instructionsPath = path_1.default.join(testDir, 'docs', 'AGENT_INSTRUCTIONS.md');
22
- const mdcPath = path_1.default.join(testDir, '.cursor', 'rules', 'rigour.mdc');
23
- (0, vitest_1.expect)(await fs_extra_1.default.pathExists(instructionsPath)).toBe(true);
24
- (0, vitest_1.expect)(await fs_extra_1.default.pathExists(mdcPath)).toBe(true);
25
- const instructionsContent = await fs_extra_1.default.readFile(instructionsPath, 'utf-8');
26
- const mdcContent = await fs_extra_1.default.readFile(mdcPath, 'utf-8');
19
+ await initCommand(testDir, { ide: 'all' });
20
+ const instructionsPath = path.join(testDir, 'docs', 'AGENT_INSTRUCTIONS.md');
21
+ const mdcPath = path.join(testDir, '.cursor', 'rules', 'rigour.mdc');
22
+ expect(await fs.pathExists(instructionsPath)).toBe(true);
23
+ expect(await fs.pathExists(mdcPath)).toBe(true);
24
+ const instructionsContent = await fs.readFile(instructionsPath, 'utf-8');
25
+ const mdcContent = await fs.readFile(mdcPath, 'utf-8');
27
26
  // Check for agnostic instructions
28
- (0, vitest_1.expect)(instructionsContent).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
29
- (0, vitest_1.expect)(instructionsContent).toContain('VERIFICATION PROOF REQUIRED');
27
+ expect(instructionsContent).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
28
+ expect(instructionsContent).toContain('VERIFICATION PROOF REQUIRED');
30
29
  // Check for key sections in universal instructions
31
- (0, vitest_1.expect)(instructionsContent).toContain('# šŸ›”ļø Rigour: Engineering Excellence Protocol');
32
- (0, vitest_1.expect)(instructionsContent).toContain('# Code Quality Standards');
30
+ expect(instructionsContent).toContain('# šŸ›”ļø Rigour: Mandatory Engineering Governance Protocol');
31
+ expect(instructionsContent).toContain('# Code Quality Standards');
33
32
  // Check that MDC includes agnostic rules
34
- (0, vitest_1.expect)(mdcContent).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
33
+ expect(mdcContent).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
35
34
  });
36
- (0, vitest_1.it)('should create .clinerules when ide is cline or all', async () => {
37
- await (0, init_js_1.initCommand)(testDir, { ide: 'cline' });
38
- const clineRulesPath = path_1.default.join(testDir, '.clinerules');
39
- (0, vitest_1.expect)(await fs_extra_1.default.pathExists(clineRulesPath)).toBe(true);
40
- const content = await fs_extra_1.default.readFile(clineRulesPath, 'utf-8');
41
- (0, vitest_1.expect)(content).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
35
+ it('should create .clinerules when ide is cline or all', async () => {
36
+ const initCommand = await getInitCommand();
37
+ await initCommand(testDir, { ide: 'cline' });
38
+ const clineRulesPath = path.join(testDir, '.clinerules');
39
+ expect(await fs.pathExists(clineRulesPath)).toBe(true);
40
+ const content = await fs.readFile(clineRulesPath, 'utf-8');
41
+ expect(content).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
42
42
  });
43
43
  });
@@ -1,28 +1,26 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
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"));
10
- (0, vitest_1.describe)('CLI Smoke Test', () => {
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);
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ async function getCheckCommand() {
5
+ const { checkCommand } = await import('./commands/check.js');
6
+ return checkCommand;
7
+ }
8
+ describe('CLI Smoke Test', () => {
9
+ const testDir = path.join(process.cwd(), 'temp-smoke-test');
10
+ beforeEach(async () => {
11
+ await fs.ensureDir(testDir);
14
12
  // @ts-ignore
15
- vitest_1.vi.spyOn(process, 'exit').mockImplementation(() => { });
13
+ vi.spyOn(process, 'exit').mockImplementation(() => { });
16
14
  });
17
- (0, vitest_1.afterEach)(async () => {
18
- await fs_extra_1.default.remove(testDir);
19
- vitest_1.vi.restoreAllMocks();
15
+ afterEach(async () => {
16
+ await fs.remove(testDir);
17
+ vi.restoreAllMocks();
20
18
  });
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'), `
19
+ it('should respect ignore patterns and avoid EPERM', async () => {
20
+ const restrictedDir = path.join(testDir, '.restricted');
21
+ await fs.ensureDir(restrictedDir);
22
+ await fs.writeFile(path.join(restrictedDir, 'secret.js'), 'TODO: leak');
23
+ await fs.writeFile(path.join(testDir, 'rigour.yml'), `
26
24
  version: 1
27
25
  ignore:
28
26
  - ".restricted/**"
@@ -31,33 +29,36 @@ gates:
31
29
  required_files: []
32
30
  `);
33
31
  // Simulate EPERM by changing permissions
34
- await fs_extra_1.default.chmod(restrictedDir, 0o000);
32
+ await fs.chmod(restrictedDir, 0o000);
35
33
  try {
36
34
  // We need to mock process.exit or checkCommand should not exit if we want to test it easily
37
35
  // For now, we'll just verify it doesn't throw before it would exit (internal logic)
38
36
  // But checkCommand calls process.exit(1) on failure.
39
37
  // 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();
38
+ const checkCommand = await getCheckCommand();
39
+ await expect(checkCommand(testDir, [], { ci: true })).resolves.not.toThrow();
41
40
  }
42
41
  finally {
43
- await fs_extra_1.default.chmod(restrictedDir, 0o777);
42
+ await fs.chmod(restrictedDir, 0o777);
44
43
  }
45
44
  });
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'), `
45
+ it('should check specific files when provided', async () => {
46
+ await fs.writeFile(path.join(testDir, 'bad.js'), 'TODO: fixme');
47
+ await fs.writeFile(path.join(testDir, 'good.js'), 'console.log("hello")');
48
+ await fs.writeFile(path.join(testDir, 'rigour.yml'), `
50
49
  version: 1
51
50
  gates:
52
51
  forbid_todos: true
53
52
  required_files: []
54
53
  `);
55
54
  // 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);
55
+ const checkCommand = await getCheckCommand();
56
+ await checkCommand(testDir, [path.join(testDir, 'good.js')], { ci: true });
57
+ expect(process.exit).toHaveBeenCalledWith(0);
58
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);
59
+ vi.clearAllMocks();
60
+ const checkCommandFail = await getCheckCommand();
61
+ await checkCommandFail(testDir, [path.join(testDir, 'bad.js')], { ci: true });
62
+ expect(process.exit).toHaveBeenCalledWith(1);
62
63
  });
63
64
  });
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "2.9.4",
3
+ "version": "2.11.0",
4
+ "type": "module",
4
5
  "bin": {
5
6
  "rigour": "dist/cli.js"
6
7
  },
@@ -20,8 +21,9 @@
20
21
  "fs-extra": "^11.2.0",
21
22
  "globby": "^14.0.1",
22
23
  "inquirer": "9.2.16",
24
+ "ora": "^8.0.1",
23
25
  "yaml": "^2.8.2",
24
- "@rigour-labs/core": "2.9.4"
26
+ "@rigour-labs/core": "2.11.0"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@types/fs-extra": "^11.0.4",
package/src/cli.ts CHANGED
@@ -6,10 +6,15 @@ import { explainCommand } from './commands/explain.js';
6
6
  import { runLoop } from './commands/run.js';
7
7
  import { guideCommand } from './commands/guide.js';
8
8
  import { setupCommand } from './commands/setup.js';
9
+ import { indexCommand } from './commands/index.js';
10
+ import { studioCommand } from './commands/studio.js';
9
11
  import chalk from 'chalk';
10
12
 
11
13
  const program = new Command();
12
14
 
15
+ program.addCommand(indexCommand);
16
+ program.addCommand(studioCommand);
17
+
13
18
  program
14
19
  .name('rigour')
15
20
  .description('šŸ›”ļø Rigour: The Quality Gate Loop for AI-Assisted Engineering')
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
5
  import { GateRunner, ConfigSchema, Failure } from '@rigour-labs/core';
6
6
  import inquirer from 'inquirer';
7
+ import { randomUUID } from 'crypto';
7
8
 
8
9
  // Exit codes per spec
9
10
  const EXIT_PASS = 0;
@@ -17,6 +18,23 @@ export interface CheckOptions {
17
18
  interactive?: boolean;
18
19
  }
19
20
 
21
+ // Helper to log events for Rigour Studio
22
+ async function logStudioEvent(cwd: string, event: any) {
23
+ try {
24
+ const rigourDir = path.join(cwd, ".rigour");
25
+ await fs.ensureDir(rigourDir);
26
+ const eventsPath = path.join(rigourDir, "events.jsonl");
27
+ const logEntry = JSON.stringify({
28
+ id: randomUUID(),
29
+ timestamp: new Date().toISOString(),
30
+ ...event
31
+ }) + "\n";
32
+ await fs.appendFile(eventsPath, logEntry);
33
+ } catch {
34
+ // Silent fail
35
+ }
36
+ }
37
+
20
38
  export async function checkCommand(cwd: string, files: string[] = [], options: CheckOptions = {}) {
21
39
  const configPath = path.join(cwd, 'rigour.yml');
22
40
 
@@ -39,12 +57,30 @@ export async function checkCommand(cwd: string, files: string[] = [], options: C
39
57
  }
40
58
 
41
59
  const runner = new GateRunner(config);
60
+
61
+ const requestId = randomUUID();
62
+ await logStudioEvent(cwd, {
63
+ type: "tool_call",
64
+ requestId,
65
+ tool: "rigour_check",
66
+ arguments: { files }
67
+ });
68
+
42
69
  const report = await runner.run(cwd, files.length > 0 ? files : undefined);
43
70
 
44
71
  // Write machine report
45
72
  const reportPath = path.join(cwd, config.output.report_path);
46
73
  await fs.writeJson(reportPath, report, { spaces: 2 });
47
74
 
75
+ await logStudioEvent(cwd, {
76
+ type: "tool_response",
77
+ requestId,
78
+ tool: "rigour_check",
79
+ status: report.status === 'PASS' ? 'success' : 'error',
80
+ content: [{ type: "text", text: `Audit Result: ${report.status}` }],
81
+ _rigour_report: report
82
+ });
83
+
48
84
  // Generate Fix Packet v2 on failure
49
85
  if (report.status === 'FAIL') {
50
86
  const { FixPacketService } = await import('@rigour-labs/core');
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Index Command
3
+ *
4
+ * Builds and updates the Rigour Pattern Index to prevent code reinvention.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import path from 'path';
9
+ import chalk from 'chalk';
10
+ import ora from 'ora';
11
+ import fs from 'fs-extra';
12
+ import { randomUUID } from 'crypto';
13
+
14
+ // Helper to log events for Rigour Studio
15
+ async function logStudioEvent(cwd: string, event: any) {
16
+ try {
17
+ const rigourDir = path.join(cwd, ".rigour");
18
+ await fs.ensureDir(rigourDir);
19
+ const eventsPath = path.join(rigourDir, "events.jsonl");
20
+ const logEntry = JSON.stringify({
21
+ id: randomUUID(),
22
+ timestamp: new Date().toISOString(),
23
+ ...event
24
+ }) + "\n";
25
+ await fs.appendFile(eventsPath, logEntry);
26
+ } catch {
27
+ // Silent fail
28
+ }
29
+ }
30
+ // Dynamic imports are used inside the action handler below to prevent
31
+ // native dependency issues from affecting the rest of the CLI.
32
+
33
+ export const indexCommand = new Command('index')
34
+ .description('Build or update the pattern index for the current project')
35
+ .option('-s, --semantic', 'Generate semantic embeddings for better matching (requires Transformers.js)', false)
36
+ .option('-f, --force', 'Force a full rebuild of the index', false)
37
+ .option('-o, --output <path>', 'Custom path for the index file')
38
+ .action(async (options) => {
39
+ const cwd = process.cwd();
40
+
41
+ // Dynamic import to isolate native dependencies
42
+ const {
43
+ PatternIndexer,
44
+ savePatternIndex,
45
+ loadPatternIndex,
46
+ getDefaultIndexPath
47
+ } = await import('@rigour-labs/core/pattern-index');
48
+
49
+ const indexPath = options.output || getDefaultIndexPath(cwd);
50
+ const spinner = ora('Initializing pattern indexer...').start();
51
+
52
+ try {
53
+ const requestId = randomUUID();
54
+ await logStudioEvent(cwd, {
55
+ type: "tool_call",
56
+ requestId,
57
+ tool: "rigour_index",
58
+ arguments: options
59
+ });
60
+
61
+ const indexer = new PatternIndexer(cwd, {
62
+ useEmbeddings: options.semantic
63
+ });
64
+
65
+ let index;
66
+ const existingIndex = await loadPatternIndex(indexPath);
67
+
68
+ if (existingIndex && !options.force) {
69
+ spinner.text = 'Updating existing pattern index...';
70
+ index = await indexer.updateIndex(existingIndex);
71
+ } else {
72
+ spinner.text = 'Building fresh pattern index (this may take a while)...';
73
+ index = await indexer.buildIndex();
74
+ }
75
+
76
+ spinner.text = 'Saving index to disk...';
77
+ await savePatternIndex(index, indexPath);
78
+
79
+ spinner.succeed(chalk.green(`Pattern index built successfully!`));
80
+
81
+ await logStudioEvent(cwd, {
82
+ type: "tool_response",
83
+ requestId,
84
+ tool: "rigour_index",
85
+ status: "success",
86
+ content: [{ type: "text", text: `Index built: ${index.stats.totalPatterns} patterns` }]
87
+ });
88
+ console.log(chalk.blue(`- Total Patterns: ${index.stats.totalPatterns}`));
89
+ console.log(chalk.blue(`- Total Files: ${index.stats.totalFiles}`));
90
+ console.log(chalk.blue(`- Index Path: ${indexPath}`));
91
+
92
+ if (options.semantic) {
93
+ console.log(chalk.magenta(`- Semantic Search: Enabled (Local Transformers.js)`));
94
+ }
95
+
96
+ const byType = Object.entries(index.stats.byType)
97
+ .map(([type, count]) => `${type}: ${count}`)
98
+ .join(', ');
99
+ console.log(chalk.gray(`Types: ${byType}`));
100
+
101
+ } catch (error: any) {
102
+ spinner.fail(chalk.red(`Failed to build pattern index: ${error.message}`));
103
+ process.exit(1);
104
+ }
105
+ });