@rigstate/cli 0.6.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.
Files changed (56) hide show
  1. package/.env.example +5 -0
  2. package/IMPLEMENTATION.md +239 -0
  3. package/QUICK_START.md +220 -0
  4. package/README.md +150 -0
  5. package/dist/index.cjs +3987 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +3964 -0
  10. package/dist/index.js.map +1 -0
  11. package/install.sh +15 -0
  12. package/package.json +53 -0
  13. package/src/commands/check.ts +329 -0
  14. package/src/commands/config.ts +81 -0
  15. package/src/commands/daemon.ts +197 -0
  16. package/src/commands/env.ts +158 -0
  17. package/src/commands/fix.ts +140 -0
  18. package/src/commands/focus.ts +134 -0
  19. package/src/commands/hooks.ts +163 -0
  20. package/src/commands/init.ts +282 -0
  21. package/src/commands/link.ts +45 -0
  22. package/src/commands/login.ts +35 -0
  23. package/src/commands/mcp.ts +73 -0
  24. package/src/commands/nexus.ts +81 -0
  25. package/src/commands/override.ts +65 -0
  26. package/src/commands/scan.ts +242 -0
  27. package/src/commands/sync-rules.ts +191 -0
  28. package/src/commands/sync.ts +339 -0
  29. package/src/commands/watch.ts +283 -0
  30. package/src/commands/work.ts +172 -0
  31. package/src/daemon/bridge-listener.ts +127 -0
  32. package/src/daemon/core.ts +184 -0
  33. package/src/daemon/factory.ts +45 -0
  34. package/src/daemon/file-watcher.ts +97 -0
  35. package/src/daemon/guardian-monitor.ts +133 -0
  36. package/src/daemon/heuristic-engine.ts +203 -0
  37. package/src/daemon/intervention-protocol.ts +128 -0
  38. package/src/daemon/telemetry.ts +23 -0
  39. package/src/daemon/types.ts +18 -0
  40. package/src/hive/gateway.ts +74 -0
  41. package/src/hive/protocol.ts +29 -0
  42. package/src/hive/scrubber.ts +72 -0
  43. package/src/index.ts +85 -0
  44. package/src/nexus/council.ts +103 -0
  45. package/src/nexus/dispatcher.ts +133 -0
  46. package/src/utils/config.ts +83 -0
  47. package/src/utils/files.ts +95 -0
  48. package/src/utils/governance.ts +128 -0
  49. package/src/utils/logger.ts +66 -0
  50. package/src/utils/manifest.ts +18 -0
  51. package/src/utils/rule-engine.ts +292 -0
  52. package/src/utils/skills-provisioner.ts +153 -0
  53. package/src/utils/version.ts +1 -0
  54. package/src/utils/watchdog.ts +215 -0
  55. package/tsconfig.json +29 -0
  56. package/tsup.config.ts +11 -0
@@ -0,0 +1,242 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import axios from 'axios';
5
+ import { glob } from 'glob';
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
9
+ import { readGitignore, shouldIgnore, isCodeFile } from '../utils/files.js';
10
+
11
+ interface ScanResult {
12
+ id: string;
13
+ file_path: string;
14
+ issues: Array<{
15
+ type: string;
16
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
17
+ message: string;
18
+ line?: number;
19
+ }>;
20
+ }
21
+
22
+ interface ApiResponse {
23
+ results: ScanResult[];
24
+ summary: {
25
+ total_files: number;
26
+ total_issues: number;
27
+ by_severity: Record<string, number>;
28
+ };
29
+ }
30
+
31
+ export function createScanCommand(): Command {
32
+ return new Command('scan')
33
+ .description('Scan code files for security and quality issues')
34
+ .argument('[path]', 'Directory or file to scan', '.')
35
+ .option('--json', 'Output results as JSON')
36
+ .option('--project <id>', 'Project ID to associate with this scan')
37
+ .action(async (targetPath: string, options: { json?: boolean; project?: string }) => {
38
+ const spinner = ora();
39
+
40
+ try {
41
+ // Get API credentials
42
+ const apiKey = getApiKey();
43
+ const apiUrl = getApiUrl();
44
+ const projectId = options.project || getProjectId();
45
+
46
+ if (!projectId) {
47
+ console.warn(
48
+ chalk.yellow(
49
+ 'âš ī¸ No project ID specified. Use --project <id> or set a default.'
50
+ )
51
+ );
52
+ }
53
+
54
+ // Resolve target path
55
+ const scanPath = path.resolve(process.cwd(), targetPath);
56
+
57
+ spinner.start(`Scanning ${chalk.cyan(scanPath)}...`);
58
+
59
+ // Read .gitignore patterns
60
+ const gitignorePatterns = await readGitignore(scanPath);
61
+
62
+ // Find all code files
63
+ const pattern = path.join(scanPath, '**/*');
64
+ const allFiles = await glob(pattern, {
65
+ nodir: true,
66
+ dot: false,
67
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
68
+ });
69
+
70
+ // Filter files
71
+ const codeFiles = allFiles.filter((file) => {
72
+ const relativePath = path.relative(scanPath, file);
73
+ return isCodeFile(file) && !shouldIgnore(relativePath, gitignorePatterns);
74
+ });
75
+
76
+ if (codeFiles.length === 0) {
77
+ spinner.warn(chalk.yellow('No code files found to scan.'));
78
+ return;
79
+ }
80
+
81
+ spinner.text = `Found ${codeFiles.length} files. Scanning...`;
82
+
83
+ // Scan each file individually
84
+ const results: ScanResult[] = [];
85
+ let totalIssues = 0;
86
+ const severityCounts: Record<string, number> = {};
87
+
88
+ for (let i = 0; i < codeFiles.length; i++) {
89
+ const filePath = codeFiles[i];
90
+ const relativePath = path.relative(scanPath, filePath);
91
+
92
+ spinner.text = `Scanning ${i + 1}/${codeFiles.length}: ${relativePath}`;
93
+
94
+ try {
95
+ const content = await fs.readFile(filePath, 'utf-8');
96
+
97
+ // Call the API for this file
98
+ const response = await axios.post(
99
+ `${apiUrl}/api/v1/audit`,
100
+ {
101
+ content,
102
+ file_path: relativePath,
103
+ project_id: projectId,
104
+ },
105
+ {
106
+ headers: {
107
+ 'Authorization': `Bearer ${apiKey}`,
108
+ 'Content-Type': 'application/json',
109
+ },
110
+ timeout: 60000, // 1 minute per file
111
+ }
112
+ );
113
+
114
+ // Aggregate results
115
+ const vulnerabilities = response.data.vulnerabilities || [];
116
+ if (vulnerabilities.length > 0) {
117
+ results.push({
118
+ id: response.data.id || relativePath,
119
+ file_path: relativePath,
120
+ issues: vulnerabilities.map((v: any) => ({
121
+ type: v.type,
122
+ severity: v.severity,
123
+ message: v.description || v.title,
124
+ line: v.line_number,
125
+ })),
126
+ });
127
+
128
+ totalIssues += vulnerabilities.length;
129
+
130
+ vulnerabilities.forEach((v: any) => {
131
+ severityCounts[v.severity] = (severityCounts[v.severity] || 0) + 1;
132
+ });
133
+ }
134
+ } catch (fileError) {
135
+ if (axios.isAxiosError(fileError)) {
136
+ console.warn(chalk.yellow(`\nâš ī¸ Skipping ${relativePath}: ${fileError.message}`));
137
+ } else {
138
+ console.warn(chalk.yellow(`\nâš ī¸ Error reading ${relativePath}`));
139
+ }
140
+ }
141
+ }
142
+
143
+ spinner.succeed(chalk.green('✅ Scan completed!'));
144
+
145
+ // Build aggregated response
146
+ const aggregatedResponse: ApiResponse = {
147
+ results,
148
+ summary: {
149
+ total_files: codeFiles.length,
150
+ total_issues: totalIssues,
151
+ by_severity: severityCounts,
152
+ },
153
+ };
154
+
155
+ // Output results
156
+ if (options.json) {
157
+ console.log(JSON.stringify(aggregatedResponse, null, 2));
158
+ } else {
159
+ printPrettyResults(aggregatedResponse);
160
+ }
161
+ } catch (error) {
162
+ spinner.fail(chalk.red('❌ Scan failed'));
163
+
164
+ if (axios.isAxiosError(error)) {
165
+ if (error.response) {
166
+ console.error(chalk.red('API Error:'), error.response.data);
167
+ } else if (error.request) {
168
+ console.error(
169
+ chalk.red('Network Error:'),
170
+ 'Could not reach the API. Is the server running?'
171
+ );
172
+ } else {
173
+ console.error(chalk.red('Error:'), error.message);
174
+ }
175
+ } else {
176
+ console.error(
177
+ chalk.red('Error:'),
178
+ error instanceof Error ? error.message : 'Unknown error'
179
+ );
180
+ }
181
+
182
+ process.exit(1);
183
+ }
184
+ });
185
+ }
186
+
187
+ function printPrettyResults(data: ApiResponse): void {
188
+ const { results, summary } = data;
189
+
190
+ console.log('\n' + chalk.bold('📊 Scan Summary'));
191
+ console.log(chalk.dim('─'.repeat(60)));
192
+ console.log(`Total Files Scanned: ${chalk.cyan(summary.total_files)}`);
193
+ console.log(`Total Issues Found: ${chalk.yellow(summary.total_issues)}`);
194
+
195
+ if (summary.by_severity) {
196
+ console.log('\nIssues by Severity:');
197
+ Object.entries(summary.by_severity).forEach(([severity, count]) => {
198
+ const color = getSeverityColor(severity as any);
199
+ console.log(` ${color(`${severity}:`)} ${count}`);
200
+ });
201
+ }
202
+
203
+ if (results && results.length > 0) {
204
+ console.log('\n' + chalk.bold('🔍 Detailed Results'));
205
+ console.log(chalk.dim('─'.repeat(60)));
206
+
207
+ results.forEach((result) => {
208
+ if (result.issues && result.issues.length > 0) {
209
+ console.log(`\n${chalk.bold(result.file_path)}`);
210
+
211
+ result.issues.forEach((issue) => {
212
+ const severityColor = getSeverityColor(issue.severity);
213
+ const lineInfo = issue.line ? chalk.dim(`:${issue.line}`) : '';
214
+
215
+ console.log(
216
+ ` ${severityColor(`[${issue.severity.toUpperCase()}]`)} ${issue.type}${lineInfo}`
217
+ );
218
+ console.log(` ${chalk.dim(issue.message)}`);
219
+ });
220
+ }
221
+ });
222
+ }
223
+
224
+ console.log('\n' + chalk.dim('─'.repeat(60)));
225
+ }
226
+
227
+ function getSeverityColor(severity: string): (str: string) => string {
228
+ switch (severity.toLowerCase()) {
229
+ case 'critical':
230
+ return chalk.red.bold;
231
+ case 'high':
232
+ return chalk.red;
233
+ case 'medium':
234
+ return chalk.yellow;
235
+ case 'low':
236
+ return chalk.blue;
237
+ case 'info':
238
+ return chalk.gray;
239
+ default:
240
+ return chalk.white;
241
+ }
242
+ }
@@ -0,0 +1,191 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getApiKey, getApiUrl } from '../utils/config.js';
5
+ import axios from 'axios';
6
+
7
+ interface SyncResult {
8
+ projectId: string;
9
+ projectName: string;
10
+ status: 'success' | 'failed';
11
+ error?: string;
12
+ }
13
+
14
+ export function createSyncRulesCommand() {
15
+ const syncRules = new Command('sync-rules');
16
+
17
+ syncRules
18
+ .description('đŸ›Ąī¸ Push Frank Protocol v1.0 to all existing projects')
19
+ .option('--dry-run', 'Preview changes without pushing to GitHub')
20
+ .option('--project <id>', 'Sync a specific project only')
21
+ .action(async (options) => {
22
+ const spinner = ora('đŸ›Ąī¸ Frank Protocol: Initializing retroactive sync...').start();
23
+ const results: SyncResult[] = [];
24
+
25
+ // Get config
26
+ let apiKey: string;
27
+ try {
28
+ apiKey = getApiKey();
29
+ } catch (e) {
30
+ spinner.fail(chalk.red('Not authenticated. Run "rigstate login" first.'));
31
+ return;
32
+ }
33
+
34
+ const apiUrl = getApiUrl();
35
+
36
+ try {
37
+ // Fetch projects via API
38
+ spinner.text = 'Fetching projects...';
39
+
40
+ const projectsResponse = await axios.get(`${apiUrl}/api/v1/projects`, {
41
+ params: options.project ? { project_id: options.project } : {},
42
+ headers: { Authorization: `Bearer ${apiKey}` }
43
+ });
44
+
45
+ if (!projectsResponse.data.success) {
46
+ throw new Error(projectsResponse.data.error || 'Failed to fetch projects');
47
+ }
48
+
49
+ let projects = projectsResponse.data.data.projects || [];
50
+
51
+ if (projects.length === 0) {
52
+ spinner.fail(chalk.red('No projects found.'));
53
+ return;
54
+ }
55
+
56
+ // If multiple projects found and no specific project flag, ask user
57
+ if (projects.length > 1 && !options.project) {
58
+ spinner.stop(); // Stop spinner to allow interaction
59
+
60
+ const inquirer = (await import('inquirer')).default;
61
+ const { selectedProjectId } = await inquirer.prompt([{
62
+ type: 'list',
63
+ name: 'selectedProjectId',
64
+ message: 'Multiple projects found. Which one do you want to sync?',
65
+ choices: projects.map((p: any) => ({
66
+ name: `${p.name} [${p.id}]`,
67
+ value: p.id
68
+ }))
69
+ }]);
70
+
71
+ projects = projects.filter((p: any) => p.id === selectedProjectId);
72
+ options.project = selectedProjectId; // Set this so we know we are in a targeted context for file writing
73
+
74
+ // Try to save this preference to .env for future
75
+ try {
76
+ const fs = await import('fs/promises');
77
+ const path = await import('path');
78
+ const envPath = path.join(process.cwd(), '.env');
79
+ const envLocalPath = path.join(process.cwd(), '.env.local');
80
+
81
+ // Check if .env.local exists, otherwise check .env
82
+ let targetEnv = envLocalPath;
83
+ try {
84
+ await fs.access(envLocalPath);
85
+ } catch {
86
+ try {
87
+ await fs.access(envPath);
88
+ targetEnv = envPath;
89
+ } catch {
90
+ // Neither exist, create .env
91
+ targetEnv = envPath;
92
+ }
93
+ }
94
+
95
+ let content = '';
96
+ try {
97
+ content = await fs.readFile(targetEnv, 'utf-8');
98
+ } catch { }
99
+
100
+ if (!content.includes('RIGSTATE_PROJECT_ID')) {
101
+ const newContent = content.endsWith('\n') || content === ''
102
+ ? `${content}RIGSTATE_PROJECT_ID=${selectedProjectId}\n`
103
+ : `${content}\nRIGSTATE_PROJECT_ID=${selectedProjectId}\n`;
104
+
105
+ await fs.writeFile(targetEnv, newContent, 'utf-8');
106
+ console.log(chalk.dim(` 💾 Saved default project to ${path.basename(targetEnv)}`));
107
+ }
108
+ } catch (e) {
109
+ // Ignore error saving env
110
+ }
111
+ }
112
+
113
+ spinner.succeed(`Syncing project: ${projects[0].name}`);
114
+
115
+ // Process each project
116
+ for (const project of projects) {
117
+ const projectSpinner = ora(` Syncing: ${project.name}...`).start();
118
+
119
+ try {
120
+ if (options.dryRun) {
121
+ projectSpinner.succeed(chalk.yellow(` [DRY-RUN] Would sync: ${project.name}`));
122
+ results.push({ projectId: project.id, projectName: project.name, status: 'success' });
123
+ continue;
124
+ }
125
+
126
+ // Call API to regenerate and sync rules
127
+ const syncResponse = await axios.post(`${apiUrl}/api/v1/rules/sync`, {
128
+ project_id: project.id
129
+ }, {
130
+ headers: { Authorization: `Bearer ${apiKey}` }
131
+ });
132
+
133
+ if (syncResponse.data.success) {
134
+ if (syncResponse.data.data.github_synced) {
135
+ projectSpinner.succeed(chalk.green(` ✅ ${project.name} [${project.id}] → GitHub synced`));
136
+ } else {
137
+ projectSpinner.info(chalk.blue(` â„šī¸ ${project.name} [${project.id}] → Rules generated (no GitHub)`));
138
+ }
139
+
140
+ // Write files locally if we are syncing a single project or if inferred context matches
141
+ // For safety, if user didn't specify project and we found multiple, we only write if we can be sure.
142
+ // But usually, if you run this in a repo, you want the files.
143
+ // Let's write files if they are returned and we are arguably in the right place.
144
+ // To be safe: Only write if projects.length === 1 OR options.project is set.
145
+
146
+ const files = syncResponse.data.data.files;
147
+ if (files && Array.isArray(files) && (projects.length === 1 || options.project)) {
148
+ const fs = await import('fs/promises');
149
+ const path = await import('path');
150
+
151
+ for (const file of files) {
152
+ const filePath = path.join(process.cwd(), file.path);
153
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
154
+ await fs.writeFile(filePath, file.content, 'utf-8');
155
+ // projectSpinner.text = `Wrote ${file.path}`; // Don't spam spinner
156
+ }
157
+ console.log(chalk.dim(` 💾 Wrote ${files.length} rule files to local .cursor/rules/`));
158
+ }
159
+
160
+ results.push({ projectId: project.id, projectName: project.name, status: 'success' });
161
+ } else {
162
+ projectSpinner.warn(chalk.yellow(` âš ī¸ ${project.name} → ${syncResponse.data.error || 'Unknown error'}`));
163
+ results.push({ projectId: project.id, projectName: project.name, status: 'failed', error: syncResponse.data.error });
164
+ }
165
+
166
+ } catch (e: any) {
167
+ projectSpinner.fail(chalk.red(` ❌ ${project.name}: ${e.message}`));
168
+ results.push({ projectId: project.id, projectName: project.name, status: 'failed', error: e.message });
169
+ }
170
+ }
171
+
172
+ // Summary
173
+ console.log('');
174
+ console.log(chalk.bold('📊 Sync Summary:'));
175
+ const successful = results.filter(r => r.status === 'success').length;
176
+ const failed = results.filter(r => r.status === 'failed').length;
177
+ console.log(chalk.green(` ✅ Successful: ${successful}`));
178
+ if (failed > 0) {
179
+ console.log(chalk.red(` ❌ Failed: ${failed}`));
180
+ }
181
+ console.log('');
182
+ console.log(chalk.cyan('đŸ›Ąī¸ Frank Protocol v1.0 has been injected into the rules engine.'));
183
+ console.log(chalk.dim(' All new chats will now boot with mandatory governance checks.'));
184
+
185
+ } catch (e: any) {
186
+ spinner.fail(chalk.red('Sync failed: ' + e.message));
187
+ }
188
+ });
189
+
190
+ return syncRules;
191
+ }