@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.
- package/.env.example +5 -0
- package/IMPLEMENTATION.md +239 -0
- package/QUICK_START.md +220 -0
- package/README.md +150 -0
- package/dist/index.cjs +3987 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3964 -0
- package/dist/index.js.map +1 -0
- package/install.sh +15 -0
- package/package.json +53 -0
- package/src/commands/check.ts +329 -0
- package/src/commands/config.ts +81 -0
- package/src/commands/daemon.ts +197 -0
- package/src/commands/env.ts +158 -0
- package/src/commands/fix.ts +140 -0
- package/src/commands/focus.ts +134 -0
- package/src/commands/hooks.ts +163 -0
- package/src/commands/init.ts +282 -0
- package/src/commands/link.ts +45 -0
- package/src/commands/login.ts +35 -0
- package/src/commands/mcp.ts +73 -0
- package/src/commands/nexus.ts +81 -0
- package/src/commands/override.ts +65 -0
- package/src/commands/scan.ts +242 -0
- package/src/commands/sync-rules.ts +191 -0
- package/src/commands/sync.ts +339 -0
- package/src/commands/watch.ts +283 -0
- package/src/commands/work.ts +172 -0
- package/src/daemon/bridge-listener.ts +127 -0
- package/src/daemon/core.ts +184 -0
- package/src/daemon/factory.ts +45 -0
- package/src/daemon/file-watcher.ts +97 -0
- package/src/daemon/guardian-monitor.ts +133 -0
- package/src/daemon/heuristic-engine.ts +203 -0
- package/src/daemon/intervention-protocol.ts +128 -0
- package/src/daemon/telemetry.ts +23 -0
- package/src/daemon/types.ts +18 -0
- package/src/hive/gateway.ts +74 -0
- package/src/hive/protocol.ts +29 -0
- package/src/hive/scrubber.ts +72 -0
- package/src/index.ts +85 -0
- package/src/nexus/council.ts +103 -0
- package/src/nexus/dispatcher.ts +133 -0
- package/src/utils/config.ts +83 -0
- package/src/utils/files.ts +95 -0
- package/src/utils/governance.ts +128 -0
- package/src/utils/logger.ts +66 -0
- package/src/utils/manifest.ts +18 -0
- package/src/utils/rule-engine.ts +292 -0
- package/src/utils/skills-provisioner.ts +153 -0
- package/src/utils/version.ts +1 -0
- package/src/utils/watchdog.ts +215 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +11 -0
package/install.sh
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Build the CLI
|
|
4
|
+
echo "šØ Building @rigstate/cli..."
|
|
5
|
+
npm run build
|
|
6
|
+
|
|
7
|
+
# Install globally
|
|
8
|
+
echo "š¦ Installing globally..."
|
|
9
|
+
echo "Note: This may require sudo/administrator permissions"
|
|
10
|
+
npm install -g .
|
|
11
|
+
|
|
12
|
+
echo "ā
Installation complete!"
|
|
13
|
+
echo ""
|
|
14
|
+
echo "Try running:"
|
|
15
|
+
echo " rigstate --help"
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rigstate/cli",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Rigstate CLI - Code audit, sync and supervision tool",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"rigstate": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"lint": "tsc --noEmit",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"test": "node dist/index.js --help"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@rigstate/rules-engine": "*",
|
|
19
|
+
"@rigstate/shared": "*",
|
|
20
|
+
"uuid": "^9.0.1",
|
|
21
|
+
"@types/diff": "^7.0.2",
|
|
22
|
+
"@types/inquirer": "^9.0.9",
|
|
23
|
+
"axios": "^1.6.5",
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"chokidar": "^3.6.0",
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"conf": "^12.0.0",
|
|
28
|
+
"diff": "^4.0.2",
|
|
29
|
+
"dotenv": "^16.4.1",
|
|
30
|
+
"glob": "^10.3.10",
|
|
31
|
+
"inquirer": "^9.3.8",
|
|
32
|
+
"ora": "^8.0.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.11.5",
|
|
36
|
+
"@types/uuid": "^10.0.0",
|
|
37
|
+
"tsup": "^8.0.1",
|
|
38
|
+
"typescript": "^5.3.3"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"rigstate",
|
|
45
|
+
"cli",
|
|
46
|
+
"audit",
|
|
47
|
+
"security",
|
|
48
|
+
"code-quality",
|
|
49
|
+
"supervisor"
|
|
50
|
+
],
|
|
51
|
+
"author": "Rigstate",
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rigstate check - Validate code against Guardian rules
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* rigstate check # Check current directory
|
|
6
|
+
* rigstate check ./src # Check specific path
|
|
7
|
+
* rigstate check --strict # Exit 1 on any violation
|
|
8
|
+
* rigstate check --strict=critical # Exit 1 only on critical
|
|
9
|
+
* rigstate check --staged # Only check staged files (for pre-commit)
|
|
10
|
+
* rigstate check --json # Output as JSON
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import ora from 'ora';
|
|
16
|
+
import axios from 'axios';
|
|
17
|
+
import { glob } from 'glob';
|
|
18
|
+
import fs from 'fs/promises';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { execSync } from 'child_process';
|
|
21
|
+
import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
|
|
22
|
+
import { loadManifest } from '../utils/manifest.js';
|
|
23
|
+
import {
|
|
24
|
+
checkFile,
|
|
25
|
+
formatViolations,
|
|
26
|
+
summarizeResults,
|
|
27
|
+
type EffectiveRule,
|
|
28
|
+
type CheckResult,
|
|
29
|
+
type Violation
|
|
30
|
+
} from '../utils/rule-engine.js';
|
|
31
|
+
|
|
32
|
+
// Cache settings
|
|
33
|
+
const CACHE_FILE = '.rigstate/rules-cache.json';
|
|
34
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
35
|
+
const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours (offline limit)
|
|
36
|
+
|
|
37
|
+
interface CachedRules {
|
|
38
|
+
timestamp: string;
|
|
39
|
+
projectId: string;
|
|
40
|
+
rules: EffectiveRule[];
|
|
41
|
+
settings: { lmax: number; lmax_warning: number };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createCheckCommand(): Command {
|
|
45
|
+
return new Command('check')
|
|
46
|
+
.description('Validate code against Guardian architectural rules')
|
|
47
|
+
.argument('[path]', 'Directory or file to check', '.')
|
|
48
|
+
.option('--project <id>', 'Project ID (or use .rigstate manifest)')
|
|
49
|
+
.option('--strict [level]', 'Exit 1 on violations. Level: "all" (default) or "critical"')
|
|
50
|
+
.option('--staged', 'Only check git staged files (for pre-commit hooks)')
|
|
51
|
+
.option('--json', 'Output results as JSON')
|
|
52
|
+
.option('--no-cache', 'Skip rule cache and fetch fresh from API')
|
|
53
|
+
.action(async (targetPath: string, options: {
|
|
54
|
+
project?: string;
|
|
55
|
+
strict?: boolean | string;
|
|
56
|
+
staged?: boolean;
|
|
57
|
+
json?: boolean;
|
|
58
|
+
cache?: boolean;
|
|
59
|
+
}) => {
|
|
60
|
+
const spinner = ora();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// 1. Resolve project context
|
|
64
|
+
let projectId = options.project;
|
|
65
|
+
let apiUrl = getApiUrl();
|
|
66
|
+
|
|
67
|
+
if (!projectId) {
|
|
68
|
+
const manifest = await loadManifest();
|
|
69
|
+
if (manifest) {
|
|
70
|
+
projectId = manifest.project_id;
|
|
71
|
+
if (manifest.api_url) apiUrl = manifest.api_url;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!projectId) {
|
|
76
|
+
projectId = getProjectId();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!projectId) {
|
|
80
|
+
console.log(chalk.red('ā No project context found.'));
|
|
81
|
+
console.log(chalk.dim(' Run "rigstate link" or pass --project <id>'));
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Get API key
|
|
86
|
+
let apiKey: string;
|
|
87
|
+
try {
|
|
88
|
+
apiKey = getApiKey();
|
|
89
|
+
} catch {
|
|
90
|
+
console.log(chalk.red('ā Not authenticated. Run "rigstate login" first.'));
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Fetch rules (with caching)
|
|
95
|
+
spinner.start('Fetching Guardian rules...');
|
|
96
|
+
let rules: EffectiveRule[];
|
|
97
|
+
let settings: { lmax: number; lmax_warning: number };
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const cached = options.cache !== false ? await loadCachedRules(projectId) : null;
|
|
101
|
+
|
|
102
|
+
if (cached && !isStale(cached.timestamp, CACHE_TTL_MS)) {
|
|
103
|
+
rules = cached.rules;
|
|
104
|
+
settings = cached.settings;
|
|
105
|
+
spinner.text = 'Using cached rules...';
|
|
106
|
+
} else {
|
|
107
|
+
// Fetch from API
|
|
108
|
+
const response = await axios.get(`${apiUrl}/api/v1/guardian/rules`, {
|
|
109
|
+
params: { project_id: projectId },
|
|
110
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
111
|
+
timeout: 10000
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!response.data.success) {
|
|
115
|
+
throw new Error(response.data.error || 'Unknown API error');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
rules = response.data.data.rules;
|
|
119
|
+
settings = response.data.data.settings;
|
|
120
|
+
|
|
121
|
+
// Save to cache
|
|
122
|
+
await saveCachedRules(projectId, rules, settings);
|
|
123
|
+
}
|
|
124
|
+
} catch (apiError: any) {
|
|
125
|
+
// Fallback to cache if API fails
|
|
126
|
+
const cached = await loadCachedRules(projectId);
|
|
127
|
+
|
|
128
|
+
if (cached && !isStale(cached.timestamp, CACHE_MAX_AGE_MS)) {
|
|
129
|
+
spinner.warn(chalk.yellow('Using cached rules (API unavailable)'));
|
|
130
|
+
rules = cached.rules;
|
|
131
|
+
settings = cached.settings;
|
|
132
|
+
} else {
|
|
133
|
+
spinner.fail(chalk.red('Failed to fetch rules and no valid cache'));
|
|
134
|
+
console.log(chalk.dim(` Error: ${apiError.message}`));
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
spinner.succeed(`Loaded ${rules.length} Guardian rules`);
|
|
140
|
+
|
|
141
|
+
// 4. Get files to check
|
|
142
|
+
const scanPath = path.resolve(process.cwd(), targetPath);
|
|
143
|
+
let filesToCheck: string[];
|
|
144
|
+
|
|
145
|
+
if (options.staged) {
|
|
146
|
+
// Only staged files
|
|
147
|
+
spinner.start('Getting staged files...');
|
|
148
|
+
try {
|
|
149
|
+
const stagedOutput = execSync('git diff --cached --name-only --diff-filter=ACMR', {
|
|
150
|
+
encoding: 'utf-8',
|
|
151
|
+
cwd: process.cwd()
|
|
152
|
+
});
|
|
153
|
+
filesToCheck = stagedOutput
|
|
154
|
+
.split('\n')
|
|
155
|
+
.filter(f => f.trim())
|
|
156
|
+
.filter(f => isCodeFile(f))
|
|
157
|
+
.map(f => path.resolve(process.cwd(), f));
|
|
158
|
+
} catch {
|
|
159
|
+
spinner.fail('Not a git repository or no staged files');
|
|
160
|
+
process.exit(2);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// All code files in path
|
|
164
|
+
spinner.start(`Scanning ${chalk.cyan(targetPath)}...`);
|
|
165
|
+
const pattern = path.join(scanPath, '**/*');
|
|
166
|
+
const allFiles = await glob(pattern, {
|
|
167
|
+
nodir: true,
|
|
168
|
+
dot: false,
|
|
169
|
+
ignore: [
|
|
170
|
+
'**/node_modules/**',
|
|
171
|
+
'**/.git/**',
|
|
172
|
+
'**/dist/**',
|
|
173
|
+
'**/build/**',
|
|
174
|
+
'**/.next/**',
|
|
175
|
+
'**/coverage/**'
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
filesToCheck = allFiles.filter(f => isCodeFile(f));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (filesToCheck.length === 0) {
|
|
182
|
+
spinner.warn(chalk.yellow('No code files found to check.'));
|
|
183
|
+
outputResults([], !!options.json);
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
spinner.succeed(`Found ${filesToCheck.length} files to check`);
|
|
188
|
+
|
|
189
|
+
// 5. Run checks
|
|
190
|
+
spinner.start('Running Guardian checks...');
|
|
191
|
+
const results: CheckResult[] = [];
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < filesToCheck.length; i++) {
|
|
194
|
+
const file = filesToCheck[i];
|
|
195
|
+
spinner.text = `Checking ${i + 1}/${filesToCheck.length}: ${path.basename(file)}`;
|
|
196
|
+
const result = await checkFile(file, rules, process.cwd());
|
|
197
|
+
results.push(result);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
spinner.stop();
|
|
201
|
+
|
|
202
|
+
// 6. Output results
|
|
203
|
+
const summary = summarizeResults(results);
|
|
204
|
+
|
|
205
|
+
if (options.json) {
|
|
206
|
+
outputResults(results, true);
|
|
207
|
+
} else {
|
|
208
|
+
outputResults(results, false);
|
|
209
|
+
|
|
210
|
+
// Summary
|
|
211
|
+
console.log('\n' + chalk.bold('š Summary'));
|
|
212
|
+
console.log(chalk.dim('ā'.repeat(50)));
|
|
213
|
+
console.log(`Files checked: ${chalk.cyan(summary.totalFiles)}`);
|
|
214
|
+
console.log(`Total violations: ${summary.totalViolations > 0 ? chalk.red(summary.totalViolations) : chalk.green(0)}`);
|
|
215
|
+
|
|
216
|
+
if (summary.totalViolations > 0) {
|
|
217
|
+
console.log(` ${chalk.red('Critical:')} ${summary.criticalCount}`);
|
|
218
|
+
console.log(` ${chalk.yellow('Warning:')} ${summary.warningCount}`);
|
|
219
|
+
console.log(` ${chalk.blue('Info:')} ${summary.infoCount}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(chalk.dim('ā'.repeat(50)));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 7. Exit code logic
|
|
226
|
+
if (options.strict !== undefined) {
|
|
227
|
+
const strictLevel = typeof options.strict === 'string' ? options.strict : 'all';
|
|
228
|
+
|
|
229
|
+
if (strictLevel === 'critical' && summary.criticalCount > 0) {
|
|
230
|
+
console.log(chalk.red('\nā Check failed: Critical violations found'));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
} else if (strictLevel === 'all' && summary.totalViolations > 0) {
|
|
233
|
+
console.log(chalk.red('\nā Check failed: Violations found'));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (summary.totalViolations === 0) {
|
|
239
|
+
console.log(chalk.green('\nā
All checks passed!'));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
process.exit(0);
|
|
243
|
+
|
|
244
|
+
} catch (error: any) {
|
|
245
|
+
spinner.fail(chalk.red('Check failed'));
|
|
246
|
+
console.error(chalk.red('Error:'), error.message);
|
|
247
|
+
process.exit(2);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Helper functions
|
|
253
|
+
|
|
254
|
+
function isCodeFile(filePath: string): boolean {
|
|
255
|
+
const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
256
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
257
|
+
return codeExtensions.includes(ext);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function loadCachedRules(projectId: string): Promise<CachedRules | null> {
|
|
261
|
+
try {
|
|
262
|
+
const cachePath = path.join(process.cwd(), CACHE_FILE);
|
|
263
|
+
const content = await fs.readFile(cachePath, 'utf-8');
|
|
264
|
+
const cached: CachedRules = JSON.parse(content);
|
|
265
|
+
|
|
266
|
+
if (cached.projectId !== projectId) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return cached;
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function saveCachedRules(
|
|
277
|
+
projectId: string,
|
|
278
|
+
rules: EffectiveRule[],
|
|
279
|
+
settings: { lmax: number; lmax_warning: number }
|
|
280
|
+
): Promise<void> {
|
|
281
|
+
try {
|
|
282
|
+
const cacheDir = path.join(process.cwd(), '.rigstate');
|
|
283
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
284
|
+
|
|
285
|
+
const cached: CachedRules = {
|
|
286
|
+
timestamp: new Date().toISOString(),
|
|
287
|
+
projectId,
|
|
288
|
+
rules,
|
|
289
|
+
settings
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
await fs.writeFile(
|
|
293
|
+
path.join(cacheDir, 'rules-cache.json'),
|
|
294
|
+
JSON.stringify(cached, null, 2)
|
|
295
|
+
);
|
|
296
|
+
} catch {
|
|
297
|
+
// Silently fail cache write
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isStale(timestamp: string, maxAge: number): boolean {
|
|
302
|
+
const age = Date.now() - new Date(timestamp).getTime();
|
|
303
|
+
return age > maxAge;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function outputResults(results: CheckResult[], json: boolean): void {
|
|
307
|
+
if (json) {
|
|
308
|
+
console.log(JSON.stringify({
|
|
309
|
+
results,
|
|
310
|
+
summary: summarizeResults(results)
|
|
311
|
+
}, null, 2));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const hasViolations = results.some(r => r.violations.length > 0);
|
|
316
|
+
|
|
317
|
+
if (!hasViolations) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
console.log('\n' + chalk.bold('š Violations Found'));
|
|
322
|
+
console.log(chalk.dim('ā'.repeat(50)));
|
|
323
|
+
|
|
324
|
+
for (const result of results) {
|
|
325
|
+
if (result.violations.length > 0) {
|
|
326
|
+
formatViolations(result.violations);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getApiKey, setApiKey, getProjectId, setProjectId, getApiUrl } from '../utils/config.js';
|
|
4
|
+
|
|
5
|
+
export function createConfigCommand() {
|
|
6
|
+
const config = new Command('config');
|
|
7
|
+
|
|
8
|
+
config
|
|
9
|
+
.description('View or modify Rigstate configuration')
|
|
10
|
+
.argument('[key]', 'Configuration key to view/set (api_key, project_id, api_url)')
|
|
11
|
+
.argument('[value]', 'Value to set')
|
|
12
|
+
.action(async (key?: string, value?: string) => {
|
|
13
|
+
// No arguments - show all config
|
|
14
|
+
if (!key) {
|
|
15
|
+
console.log(chalk.bold('Rigstate Configuration'));
|
|
16
|
+
console.log(chalk.dim('ā'.repeat(40)));
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const apiKey = getApiKey();
|
|
20
|
+
console.log(`${chalk.cyan('api_key')}: ${apiKey.substring(0, 20)}...`);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.log(`${chalk.cyan('api_key')}: ${chalk.dim('(not set)')}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const projectId = getProjectId();
|
|
26
|
+
console.log(`${chalk.cyan('project_id')}: ${projectId || chalk.dim('(not set)')}`);
|
|
27
|
+
|
|
28
|
+
const apiUrl = getApiUrl();
|
|
29
|
+
console.log(`${chalk.cyan('api_url')}: ${apiUrl}`);
|
|
30
|
+
|
|
31
|
+
console.log('');
|
|
32
|
+
console.log(chalk.dim('Use "rigstate config <key> <value>" to set a value.'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get specific key
|
|
37
|
+
if (!value) {
|
|
38
|
+
switch (key) {
|
|
39
|
+
case 'api_key':
|
|
40
|
+
try {
|
|
41
|
+
const apiKey = getApiKey();
|
|
42
|
+
console.log(apiKey);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.log(chalk.dim('(not set)'));
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case 'project_id':
|
|
48
|
+
console.log(getProjectId() || chalk.dim('(not set)'));
|
|
49
|
+
break;
|
|
50
|
+
case 'api_url':
|
|
51
|
+
console.log(getApiUrl());
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
console.log(chalk.red(`Unknown config key: ${key}`));
|
|
55
|
+
console.log(chalk.dim('Valid keys: api_key, project_id, api_url'));
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Set value
|
|
61
|
+
switch (key) {
|
|
62
|
+
case 'api_key':
|
|
63
|
+
setApiKey(value);
|
|
64
|
+
console.log(chalk.green(`ā
api_key updated`));
|
|
65
|
+
break;
|
|
66
|
+
case 'project_id':
|
|
67
|
+
setProjectId(value);
|
|
68
|
+
console.log(chalk.green(`ā
project_id updated`));
|
|
69
|
+
break;
|
|
70
|
+
case 'api_url':
|
|
71
|
+
// api_url is not settable via this command for now
|
|
72
|
+
console.log(chalk.yellow('api_url is set via RIGSTATE_API_URL environment variable'));
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
console.log(chalk.red(`Unknown config key: ${key}`));
|
|
76
|
+
console.log(chalk.dim('Valid keys: api_key, project_id'));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rigstate daemon - Unified Guardian Daemon
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* rigstate daemon # Start daemon in foreground
|
|
6
|
+
* rigstate daemon status # Check if daemon is running
|
|
7
|
+
* rigstate daemon --no-bridge # Disable Agent Bridge
|
|
8
|
+
* rigstate daemon --verbose # Enable verbose output
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import fs from 'fs/promises';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { createDaemon } from '../daemon/factory.js';
|
|
17
|
+
|
|
18
|
+
const PID_FILE = '.rigstate/daemon.pid';
|
|
19
|
+
const STATE_FILE = '.rigstate/daemon.state.json';
|
|
20
|
+
|
|
21
|
+
export function createDaemonCommand(): Command {
|
|
22
|
+
const daemon = new Command('daemon')
|
|
23
|
+
.description('Start the Guardian daemon for continuous monitoring');
|
|
24
|
+
|
|
25
|
+
// Main daemon command (start in foreground)
|
|
26
|
+
daemon
|
|
27
|
+
.argument('[action]', 'Action: start (default) or status', 'start')
|
|
28
|
+
.option('--project <id>', 'Project ID (or use .rigstate manifest)')
|
|
29
|
+
.option('--path <path>', 'Path to watch', '.')
|
|
30
|
+
.option('--no-bridge', 'Disable Agent Bridge connection')
|
|
31
|
+
.option('--verbose', 'Enable verbose output')
|
|
32
|
+
.action(async (action: string, options: {
|
|
33
|
+
project?: string;
|
|
34
|
+
path?: string;
|
|
35
|
+
bridge?: boolean;
|
|
36
|
+
verbose?: boolean;
|
|
37
|
+
}) => {
|
|
38
|
+
if (action === 'status') {
|
|
39
|
+
await showStatus();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const spinner = ora();
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Check if already running
|
|
47
|
+
if (await isRunning()) {
|
|
48
|
+
console.log(chalk.yellow('ā Another daemon instance may be running.'));
|
|
49
|
+
console.log(chalk.dim(` Check ${PID_FILE} or run "rigstate daemon status"`));
|
|
50
|
+
console.log(chalk.dim(' Use Ctrl+C to stop the running daemon first.\n'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Create daemon
|
|
54
|
+
spinner.start('Initializing Guardian Daemon...');
|
|
55
|
+
const daemonInstance = await createDaemon({
|
|
56
|
+
project: options.project,
|
|
57
|
+
path: options.path,
|
|
58
|
+
noBridge: options.bridge === false,
|
|
59
|
+
verbose: options.verbose
|
|
60
|
+
});
|
|
61
|
+
spinner.stop();
|
|
62
|
+
|
|
63
|
+
// Write PID file
|
|
64
|
+
await writePidFile();
|
|
65
|
+
|
|
66
|
+
// Handle shutdown gracefully
|
|
67
|
+
process.on('SIGINT', async () => {
|
|
68
|
+
console.log(chalk.dim('\n\nShutting down...'));
|
|
69
|
+
await daemonInstance.stop();
|
|
70
|
+
await cleanupPidFile();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
process.on('SIGTERM', async () => {
|
|
75
|
+
await daemonInstance.stop();
|
|
76
|
+
await cleanupPidFile();
|
|
77
|
+
process.exit(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Update state file periodically
|
|
81
|
+
const stateInterval = setInterval(async () => {
|
|
82
|
+
await writeStateFile(daemonInstance.getState());
|
|
83
|
+
}, 5000);
|
|
84
|
+
|
|
85
|
+
daemonInstance.on('stopped', () => {
|
|
86
|
+
clearInterval(stateInterval);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Start the daemon
|
|
90
|
+
await daemonInstance.start();
|
|
91
|
+
|
|
92
|
+
// Keep the process alive
|
|
93
|
+
await new Promise(() => { });
|
|
94
|
+
|
|
95
|
+
} catch (error: any) {
|
|
96
|
+
spinner.fail(chalk.red('Failed to start daemon'));
|
|
97
|
+
console.error(chalk.red('Error:'), error.message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return daemon;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function isRunning(): Promise<boolean> {
|
|
106
|
+
try {
|
|
107
|
+
const pidPath = path.join(process.cwd(), PID_FILE);
|
|
108
|
+
const content = await fs.readFile(pidPath, 'utf-8');
|
|
109
|
+
const pid = parseInt(content.trim(), 10);
|
|
110
|
+
|
|
111
|
+
// Check if process exists
|
|
112
|
+
try {
|
|
113
|
+
process.kill(pid, 0);
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
// Process doesn't exist, clean up stale PID file
|
|
117
|
+
await fs.unlink(pidPath);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function writePidFile(): Promise<void> {
|
|
126
|
+
try {
|
|
127
|
+
const dir = path.join(process.cwd(), '.rigstate');
|
|
128
|
+
await fs.mkdir(dir, { recursive: true });
|
|
129
|
+
await fs.writeFile(path.join(dir, 'daemon.pid'), process.pid.toString());
|
|
130
|
+
} catch {
|
|
131
|
+
// Silently fail
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function cleanupPidFile(): Promise<void> {
|
|
136
|
+
try {
|
|
137
|
+
await fs.unlink(path.join(process.cwd(), PID_FILE));
|
|
138
|
+
await fs.unlink(path.join(process.cwd(), STATE_FILE));
|
|
139
|
+
} catch {
|
|
140
|
+
// Silently fail
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function writeStateFile(state: any): Promise<void> {
|
|
145
|
+
try {
|
|
146
|
+
const dir = path.join(process.cwd(), '.rigstate');
|
|
147
|
+
await fs.mkdir(dir, { recursive: true });
|
|
148
|
+
await fs.writeFile(
|
|
149
|
+
path.join(dir, 'daemon.state.json'),
|
|
150
|
+
JSON.stringify(state, null, 2)
|
|
151
|
+
);
|
|
152
|
+
} catch {
|
|
153
|
+
// Silently fail
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function showStatus(): Promise<void> {
|
|
158
|
+
console.log(chalk.bold('\nš”ļø Guardian Daemon Status\n'));
|
|
159
|
+
|
|
160
|
+
const running = await isRunning();
|
|
161
|
+
|
|
162
|
+
if (!running) {
|
|
163
|
+
console.log(chalk.yellow('Status: Not running'));
|
|
164
|
+
console.log(chalk.dim('Use "rigstate daemon" to start.\n'));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(chalk.green('Status: Running'));
|
|
169
|
+
|
|
170
|
+
// Read state file
|
|
171
|
+
try {
|
|
172
|
+
const statePath = path.join(process.cwd(), STATE_FILE);
|
|
173
|
+
const content = await fs.readFile(statePath, 'utf-8');
|
|
174
|
+
const state = JSON.parse(content);
|
|
175
|
+
|
|
176
|
+
console.log(chalk.dim('ā'.repeat(40)));
|
|
177
|
+
console.log(`Started at: ${state.startedAt || 'Unknown'}`);
|
|
178
|
+
console.log(`Files checked: ${state.filesChecked || 0}`);
|
|
179
|
+
console.log(`Violations: ${state.violationsFound || 0}`);
|
|
180
|
+
console.log(`Tasks processed: ${state.tasksProcessed || 0}`);
|
|
181
|
+
console.log(`Last activity: ${state.lastActivity || 'None'}`);
|
|
182
|
+
console.log(chalk.dim('ā'.repeat(40)));
|
|
183
|
+
} catch {
|
|
184
|
+
console.log(chalk.dim('(State file not found)'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Read PID
|
|
188
|
+
try {
|
|
189
|
+
const pidPath = path.join(process.cwd(), PID_FILE);
|
|
190
|
+
const pid = await fs.readFile(pidPath, 'utf-8');
|
|
191
|
+
console.log(chalk.dim(`PID: ${pid.trim()}`));
|
|
192
|
+
} catch {
|
|
193
|
+
// No PID file
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log('');
|
|
197
|
+
}
|