@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.
- package/dist/cli.js +20 -21
- package/dist/commands/check.js +77 -50
- package/dist/commands/constants.js +4 -7
- package/dist/commands/explain.js +30 -36
- package/dist/commands/guide.js +15 -21
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +88 -0
- package/dist/commands/init.js +142 -105
- package/dist/commands/run.js +42 -48
- package/dist/commands/setup.js +21 -27
- package/dist/commands/studio.d.ts +2 -0
- package/dist/commands/studio.js +272 -0
- package/dist/init-rules.test.js +34 -34
- package/dist/smoke.test.js +35 -34
- package/package.json +4 -2
- package/src/cli.ts +5 -0
- package/src/commands/check.ts +36 -0
- package/src/commands/index.ts +105 -0
- package/src/commands/init.ts +64 -17
- package/src/commands/studio.ts +273 -0
- package/src/init-rules.test.ts +10 -2
- package/src/smoke.test.ts +10 -2
- package/src/templates/handshake.mdc +23 -26
- package/vitest.config.ts +10 -0
- package/vitest.setup.ts +30 -0
|
@@ -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
|
+
});
|
package/dist/init-rules.test.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
(
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
await
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await fs.remove(testDir);
|
|
17
15
|
});
|
|
18
|
-
|
|
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
|
|
21
|
-
const instructionsPath =
|
|
22
|
-
const mdcPath =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const instructionsContent = await
|
|
26
|
-
const mdcContent = await
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
33
|
+
expect(mdcContent).toContain('# š¤ CRITICAL INSTRUCTION FOR AI');
|
|
35
34
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
});
|
package/dist/smoke.test.js
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
(
|
|
11
|
-
|
|
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
|
-
|
|
13
|
+
vi.spyOn(process, 'exit').mockImplementation(() => { });
|
|
16
14
|
});
|
|
17
|
-
|
|
18
|
-
await
|
|
19
|
-
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.remove(testDir);
|
|
17
|
+
vi.restoreAllMocks();
|
|
20
18
|
});
|
|
21
|
-
|
|
22
|
-
const restrictedDir =
|
|
23
|
-
await
|
|
24
|
-
await
|
|
25
|
-
await
|
|
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
|
|
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
|
-
|
|
38
|
+
const checkCommand = await getCheckCommand();
|
|
39
|
+
await expect(checkCommand(testDir, [], { ci: true })).resolves.not.toThrow();
|
|
41
40
|
}
|
|
42
41
|
finally {
|
|
43
|
-
await
|
|
42
|
+
await fs.chmod(restrictedDir, 0o777);
|
|
44
43
|
}
|
|
45
44
|
});
|
|
46
|
-
|
|
47
|
-
await
|
|
48
|
-
await
|
|
49
|
-
await
|
|
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
|
-
|
|
57
|
-
(
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
(
|
|
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.
|
|
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.
|
|
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')
|
package/src/commands/check.ts
CHANGED
|
@@ -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
|
+
});
|