@rigour-labs/cli 2.11.0 → 2.13.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.
@@ -1,273 +0,0 @@
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
-
9
- export const studioCommand = new Command('studio')
10
- .description('Launch Rigour Studio (Local-First Governance UI)')
11
- .option('-p, --port <number>', 'Port to run the studio on', '3000')
12
- .option('--dev', 'Run in development mode', true)
13
- .action(async (options) => {
14
- const cwd = process.cwd();
15
- // Calculate the workspace root where the studio package lives
16
- // This file is in packages/rigour-cli/src/commands/studio.ts (or dist/commands/studio.js)
17
- const workspaceRoot = path.join(path.dirname(new URL(import.meta.url).pathname), '../../../../');
18
-
19
- console.log(chalk.bold.cyan('\nšŸ›”ļø Launching Rigour Studio...'));
20
- console.log(chalk.gray(`Project Root: ${cwd}`));
21
- console.log(chalk.gray(`Shadowing interactions in ${path.join(cwd, '.rigour/events.jsonl')}\n`));
22
-
23
- // For Phase 1, we start the studio in dev mode via pnpm
24
- // This ensures the user has a live, hot-reloading governance dashboard
25
- try {
26
- // Start the Studio dev server in the workspace root
27
- const studioProcess = execa('pnpm', ['--filter', '@rigour-labs/studio', 'dev', '--port', options.port], {
28
- stdio: 'inherit',
29
- shell: true,
30
- cwd: workspaceRoot
31
- });
32
-
33
- // Start a small API server for events on port + 1
34
- const apiPort = parseInt(options.port) + 1;
35
- const eventsPath = path.join(cwd, '.rigour/events.jsonl');
36
-
37
- const apiServer = http.createServer(async (req, res) => {
38
- const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
39
-
40
- // Set global CORS headers
41
- res.setHeader('Access-Control-Allow-Origin', '*');
42
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
43
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
44
-
45
- if (req.method === 'OPTIONS') {
46
- res.writeHead(204);
47
- res.end();
48
- return;
49
- }
50
-
51
- if (url.pathname === '/api/events') {
52
- res.writeHead(200, {
53
- 'Content-Type': 'text/event-stream',
54
- 'Cache-Control': 'no-cache',
55
- 'Connection': 'keep-alive'
56
- });
57
-
58
- // Send existing events first
59
- if (await fs.pathExists(eventsPath)) {
60
- const content = await fs.readFile(eventsPath, 'utf8');
61
- const lines = content.split('\n').filter(l => l.trim());
62
- for (const line of lines) {
63
- res.write(`data: ${line}\n\n`);
64
- }
65
- }
66
-
67
- // Watch for new events
68
- await fs.ensureDir(path.dirname(eventsPath));
69
- const watcher = fs.watch(path.dirname(eventsPath), (eventType, filename) => {
70
- if (filename === 'events.jsonl') {
71
- fs.readFile(eventsPath, 'utf8').then(content => {
72
- const lines = content.split('\n').filter(l => l.trim());
73
- const lastLine = lines[lines.length - 1];
74
- if (lastLine) {
75
- res.write(`data: ${lastLine}\n\n`);
76
- }
77
- }).catch(() => { });
78
- }
79
- });
80
-
81
- req.on('close', () => watcher.close());
82
- } else if (url.pathname === '/api/file') {
83
- const filePath = url.searchParams.get('path');
84
- if (!filePath) {
85
- res.writeHead(400);
86
- res.end('Missing path parameter');
87
- return;
88
- }
89
-
90
- const absolutePath = path.resolve(cwd, filePath);
91
- if (!absolutePath.startsWith(cwd)) {
92
- res.writeHead(403);
93
- res.end('Forbidden: Path outside project root');
94
- return;
95
- }
96
-
97
- try {
98
- const stats = await fs.stat(absolutePath);
99
- if (!stats.isFile()) {
100
- res.writeHead(400);
101
- res.end('Path is not a file');
102
- return;
103
- }
104
-
105
- const content = await fs.readFile(absolutePath, 'utf8');
106
- res.writeHead(200, { 'Content-Type': 'text/plain' });
107
- res.end(content);
108
- } catch (e: any) {
109
- res.writeHead(404);
110
- res.end(`File not found: ${e.message}`);
111
- }
112
- } else if (url.pathname === '/api/tree') {
113
- try {
114
- const getTree = async (dir: string): Promise<string[]> => {
115
- const entries = await fs.readdir(dir, { withFileTypes: true });
116
- let files: string[] = [];
117
- const exclude = [
118
- 'node_modules', '.git', '.rigour', '.github', '.vscode', '.cursor',
119
- 'venv', '.venv', 'dist', 'build', 'out', 'target', '__pycache__'
120
- ];
121
- for (const entry of entries) {
122
- if (exclude.includes(entry.name) || entry.name.startsWith('.')) {
123
- if (entry.name !== '.cursorrules' && entry.name !== '.cursor') {
124
- continue;
125
- }
126
- }
127
- const fullPath = path.join(dir, entry.name);
128
- if (entry.isDirectory()) {
129
- files = [...files, ...(await getTree(fullPath))];
130
- } else {
131
- files.push(path.relative(cwd, fullPath));
132
- }
133
- }
134
- return files;
135
- };
136
- const tree = await getTree(cwd);
137
- res.writeHead(200, { 'Content-Type': 'application/json' });
138
- res.end(JSON.stringify(tree));
139
- } catch (e: any) {
140
- res.writeHead(500);
141
- res.end(e.message);
142
- }
143
- } else if (url.pathname === '/api/config') {
144
- try {
145
- const configPath = path.join(cwd, 'rigour.yml');
146
- if (await fs.pathExists(configPath)) {
147
- const content = await fs.readFile(configPath, 'utf8');
148
- res.writeHead(200, { 'Content-Type': 'text/plain' });
149
- res.end(content);
150
- } else {
151
- res.writeHead(404);
152
- res.end('rigour.yml not found');
153
- }
154
- } catch (e: any) {
155
- res.writeHead(500);
156
- res.end(e.message);
157
- }
158
- } else if (url.pathname === '/api/memory') {
159
- try {
160
- const memoryPath = path.join(cwd, '.rigour/memory.json');
161
- if (await fs.pathExists(memoryPath)) {
162
- const content = await fs.readFile(memoryPath, 'utf8');
163
- res.writeHead(200, { 'Content-Type': 'application/json' });
164
- res.end(content);
165
- } else {
166
- res.end(JSON.stringify({}));
167
- }
168
- } catch (e: any) {
169
- res.writeHead(500);
170
- res.end(e.message);
171
- }
172
- } else if (url.pathname === '/api/index-stats') {
173
- try {
174
- const indexPath = path.join(cwd, '.rigour/patterns.json'); // Corrected path
175
- if (await fs.pathExists(indexPath)) {
176
- const content = await fs.readJson(indexPath);
177
- res.writeHead(200, { 'Content-Type': 'application/json' });
178
- res.end(JSON.stringify(content));
179
- } else {
180
- res.end(JSON.stringify({ patterns: [], stats: { totalPatterns: 0, totalFiles: 0, byType: {} } }));
181
- }
182
- } catch (e: any) {
183
- res.writeHead(500);
184
- res.end(e.message);
185
- }
186
- } else if (url.pathname === '/api/index-search') {
187
- const query = url.searchParams.get('q');
188
- if (!query) {
189
- res.writeHead(400);
190
- res.end('Missing query parameter');
191
- return;
192
- }
193
-
194
- try {
195
- const { generateEmbedding, semanticSearch } = await import('@rigour-labs/core/pattern-index');
196
- const indexPath = path.join(cwd, '.rigour/patterns.json');
197
-
198
- if (!(await fs.pathExists(indexPath))) {
199
- res.writeHead(404);
200
- res.end('Index not found');
201
- return;
202
- }
203
-
204
- const indexData = await fs.readJson(indexPath);
205
- const queryVector = await generateEmbedding(query);
206
- const similarities = semanticSearch(queryVector, indexData.patterns);
207
-
208
- // Attach similarity to patterns and sort
209
- const results = indexData.patterns.map((p: any, i: number) => ({
210
- ...p,
211
- similarity: similarities[i]
212
- }))
213
- .filter((p: any) => p.similarity > 0.3) // Threshold
214
- .sort((a: any, b: any) => b.similarity - a.similarity)
215
- .slice(0, 20);
216
-
217
- res.writeHead(200, { 'Content-Type': 'application/json' });
218
- res.end(JSON.stringify(results));
219
- } catch (e: any) {
220
- res.writeHead(500);
221
- res.end(e.message);
222
- }
223
- } else if (url.pathname === '/api/arbitrate' && req.method === 'POST') {
224
- let body = '';
225
- req.on('data', chunk => body += chunk);
226
- req.on('end', async () => {
227
- try {
228
- const decision = JSON.parse(body);
229
- const logEntry = JSON.stringify({
230
- id: randomUUID(),
231
- timestamp: new Date().toISOString(),
232
- tool: 'human_arbitration',
233
- requestId: decision.requestId,
234
- decision: decision.decision,
235
- status: decision.decision === 'approve' ? 'success' : 'error',
236
- arbitrated: true
237
- }) + "\n";
238
- await fs.appendFile(eventsPath, logEntry);
239
- res.writeHead(200);
240
- res.end(JSON.stringify({ success: true }));
241
- } catch (e: any) {
242
- res.writeHead(500);
243
- res.end(e.message);
244
- }
245
- });
246
- return;
247
- } else {
248
- res.writeHead(404);
249
- res.end();
250
- }
251
- });
252
-
253
- apiServer.listen(apiPort, () => {
254
- console.log(chalk.gray(`API Streamer active on port ${apiPort}`));
255
- });
256
-
257
- // Open browser
258
- setTimeout(async () => {
259
- const url = `http://localhost:${options.port}`;
260
- console.log(chalk.green(`\nāœ… Rigour Studio is live at ${chalk.bold(url)}`));
261
- try {
262
- await execa('open', [url]);
263
- } catch {
264
- }
265
- }, 2000);
266
-
267
- await studioProcess;
268
- } catch (error: any) {
269
- console.error(chalk.red(`\nāŒ Failed to launch Rigour Studio: ${error.message}`));
270
- console.log(chalk.yellow('Make sure to run "pnpm install" in the root directory.\n'));
271
- process.exit(1);
272
- }
273
- });
@@ -1,59 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
-
3
-
4
- import fs from 'fs-extra';
5
- import path from 'path';
6
-
7
- async function getInitCommand() {
8
- const { initCommand } = await import('./commands/init.js');
9
- return initCommand;
10
- }
11
-
12
- describe('Init Command Rules Verification', () => {
13
- const testDir = path.join(process.cwd(), 'temp-init-rules-test');
14
-
15
- beforeEach(async () => {
16
- await fs.ensureDir(testDir);
17
- });
18
-
19
- afterEach(async () => {
20
- await fs.remove(testDir);
21
- });
22
-
23
- it('should create instructions with agnostic rules and cursor rules on init', async () => {
24
- const initCommand = await getInitCommand();
25
- // Run init in test directory with all IDEs to verify rules in both locations
26
- await initCommand(testDir, { ide: 'all' });
27
-
28
- const instructionsPath = path.join(testDir, 'docs', 'AGENT_INSTRUCTIONS.md');
29
- const mdcPath = path.join(testDir, '.cursor', 'rules', 'rigour.mdc');
30
-
31
- expect(await fs.pathExists(instructionsPath)).toBe(true);
32
- expect(await fs.pathExists(mdcPath)).toBe(true);
33
-
34
- const instructionsContent = await fs.readFile(instructionsPath, 'utf-8');
35
- const mdcContent = await fs.readFile(mdcPath, 'utf-8');
36
-
37
- // Check for agnostic instructions
38
- expect(instructionsContent).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
39
- expect(instructionsContent).toContain('VERIFICATION PROOF REQUIRED');
40
-
41
- // Check for key sections in universal instructions
42
- expect(instructionsContent).toContain('# šŸ›”ļø Rigour: Mandatory Engineering Governance Protocol');
43
- expect(instructionsContent).toContain('# Code Quality Standards');
44
-
45
- // Check that MDC includes agnostic rules
46
- expect(mdcContent).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
47
- });
48
-
49
- it('should create .clinerules when ide is cline or all', async () => {
50
- const initCommand = await getInitCommand();
51
- await initCommand(testDir, { ide: 'cline' });
52
- const clineRulesPath = path.join(testDir, '.clinerules');
53
- expect(await fs.pathExists(clineRulesPath)).toBe(true);
54
-
55
- const content = await fs.readFile(clineRulesPath, 'utf-8');
56
- expect(content).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
57
- });
58
-
59
- });
package/src/smoke.test.ts DELETED
@@ -1,76 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
-
3
- import fs from 'fs-extra';
4
- import path from 'path';
5
-
6
- async function getCheckCommand() {
7
- const { checkCommand } = await import('./commands/check.js');
8
- return checkCommand;
9
- }
10
-
11
- describe('CLI Smoke Test', () => {
12
- const testDir = path.join(process.cwd(), 'temp-smoke-test');
13
-
14
- beforeEach(async () => {
15
- await fs.ensureDir(testDir);
16
- // @ts-ignore
17
- vi.spyOn(process, 'exit').mockImplementation(() => { });
18
- });
19
-
20
- afterEach(async () => {
21
- await fs.remove(testDir);
22
- vi.restoreAllMocks();
23
- });
24
-
25
- it('should respect ignore patterns and avoid EPERM', async () => {
26
- const restrictedDir = path.join(testDir, '.restricted');
27
- await fs.ensureDir(restrictedDir);
28
- await fs.writeFile(path.join(restrictedDir, 'secret.js'), 'TODO: leak');
29
-
30
- await fs.writeFile(path.join(testDir, 'rigour.yml'), `
31
- version: 1
32
- ignore:
33
- - ".restricted/**"
34
- gates:
35
- forbid_todos: true
36
- required_files: []
37
- `);
38
-
39
- // Simulate EPERM by changing permissions
40
- await fs.chmod(restrictedDir, 0o000);
41
-
42
- try {
43
- // We need to mock process.exit or checkCommand should not exit if we want to test it easily
44
- // For now, we'll just verify it doesn't throw before it would exit (internal logic)
45
- // But checkCommand calls process.exit(1) on failure.
46
-
47
- // Re-importing checkCommand to ensure it uses the latest core
48
- const checkCommand = await getCheckCommand();
49
- await expect(checkCommand(testDir, [], { ci: true })).resolves.not.toThrow();
50
- } finally {
51
- await fs.chmod(restrictedDir, 0o777);
52
- }
53
- });
54
-
55
- it('should check specific files when provided', async () => {
56
- await fs.writeFile(path.join(testDir, 'bad.js'), 'TODO: fixme');
57
- await fs.writeFile(path.join(testDir, 'good.js'), 'console.log("hello")');
58
- await fs.writeFile(path.join(testDir, 'rigour.yml'), `
59
- version: 1
60
- gates:
61
- forbid_todos: true
62
- required_files: []
63
- `);
64
-
65
- // If we check ONLY good.js, it should PASS (exit PASS)
66
- const checkCommand = await getCheckCommand();
67
- await checkCommand(testDir, [path.join(testDir, 'good.js')], { ci: true });
68
- expect(process.exit).toHaveBeenCalledWith(0);
69
-
70
- // If we check bad.js, it should FAIL (exit FAIL)
71
- vi.clearAllMocks();
72
- const checkCommandFail = await getCheckCommand();
73
- await checkCommandFail(testDir, [path.join(testDir, 'bad.js')], { ci: true });
74
- expect(process.exit).toHaveBeenCalledWith(1);
75
- });
76
- });
@@ -1,36 +0,0 @@
1
- ---
2
- description: Mandatory Rigour Governance Protocol - Automated engineering enforcement (SOLID, DRY, Patterns).
3
- globs: **/*
4
- ---
5
-
6
- # šŸ›”ļø Rigour: Mandatory Engineering Governance
7
-
8
- **You are an AI Engineering Agent governed by Rigour.**
9
-
10
- Your access to this codebase is conditional upon your strict adherence to the **Rigour Governance Protocol**. This is not a recommendation; it is an automated requirement for every interaction.
11
-
12
- ## 🚦 The Mandatory Governance Loop
13
- You MUST execute this loop for every engineering task. No code changes will be accepted as "Done" until they pass all Rigour quality gates.
14
-
15
- 1. **Recall Context**: Call `rigour_recall` at the start of every session to align with project-specific engineering memory.
16
- 2. **Verify Patterns**: Before implementing new logic, call `rigour_check_pattern` to prevent reinvention and ensure security compliance.
17
- 3. **Audit Enforcement**: Before finishing, you MUST call `rigour_check`.
18
- 4. **Failure Remediation**: If `rigour_check` returns **FAIL**, you MUST call `rigour_get_fix_packet` and implement all required engineering refinements.
19
- 5. **PASS or REJECT**: You may only declare a task complete when `rigour_check` returns **PASS**.
20
-
21
- ## 🧩 Governance Standards
22
- Your logic is automatically audited against these core engineering gates:
23
-
24
- ### 1. Architectural Integrity (SOLID)
25
- - **SRP**: Every component must have one reason to change.
26
- - **DIP**: Depend on abstractions, not concretions.
27
-
28
- ### 2. Efficiency & Security (DRY + CVE)
29
- - **No Reinvention**: Reuse existing patterns identified by Rigour.
30
- - **Safety**: No vulnerable libraries or deprecated patterns allowed.
31
-
32
- ## āš–ļø Transparency & Accountability
33
- **Every action, tool call, and logic shift you perform is automatically logged to the project's local audit trail.** This data is visible in real-time to the human governor in **Rigour Studio**. Failure to follow this protocol will be flagged as a governance violation.
34
-
35
- **GOVERNANCE STATUS: ACTIVE**
36
- **ENFORCEMENT: MANDATORY**
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": [
8
- "src/**/*"
9
- ]
10
- }
package/vitest.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- setupFiles: ['./vitest.setup.ts'],
6
- deps: {
7
- external: ['@xenova/transformers', 'sharp'],
8
- },
9
- },
10
- });
package/vitest.setup.ts DELETED
@@ -1,30 +0,0 @@
1
- import { vi } from 'vitest';
2
-
3
- // Mock Transformers.js to avoid native binary dependency issues and speed up tests
4
- vi.mock('@xenova/transformers', () => ({
5
- pipeline: async () => {
6
- // Return a mock extractor that produces deterministic "embeddings"
7
- return async (text: string) => {
8
- // Create a fake vector based on the text length or hash
9
- const vector = new Array(384).fill(0);
10
- for (let i = 0; i < Math.min(text.length, 384); i++) {
11
- vector[i] = text.charCodeAt(i) / 255;
12
- }
13
- return { data: new Float32Array(vector) };
14
- };
15
- },
16
- env: {
17
- allowImageProcessors: false,
18
- },
19
- }));
20
-
21
- // Also mock sharp just in case something else pulls it in
22
- vi.mock('sharp', () => ({
23
- default: () => ({
24
- resize: () => ({
25
- toFormat: () => ({
26
- toBuffer: async () => Buffer.from([]),
27
- }),
28
- }),
29
- }),
30
- }));