@rigstate/cli 0.7.24 → 0.7.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigstate/cli",
3
- "version": "0.7.24",
3
+ "version": "0.7.26",
4
4
  "description": "Rigstate CLI - Code audit, sync and supervision tool",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,9 +14,8 @@ import chalk from 'chalk';
14
14
  import ora from 'ora';
15
15
  import fs from 'fs/promises';
16
16
  import path from 'path';
17
- import { execSync } from 'child_process';
18
- import { fileURLToPath } from 'url';
19
17
  import { createDaemon } from '../daemon/factory.js';
18
+ import { enableDaemon, disableDaemon } from '../utils/service-manager.js';
20
19
 
21
20
  const PID_FILE = '.rigstate/daemon.pid';
22
21
  const STATE_FILE = '.rigstate/daemon.state.json';
@@ -220,116 +219,3 @@ async function showStatus(): Promise<void> {
220
219
  console.log('');
221
220
  }
222
221
 
223
- async function enableDaemon() {
224
- console.log(chalk.bold('\n⚙️ Enabling Rigstate Background Service (macOS)\n'));
225
-
226
- if (process.platform !== 'darwin') {
227
- console.error(chalk.red('❌ Currently only macOS is supported for auto-start.'));
228
- console.error(chalk.yellow('PRs welcome for Linux/Windows support!'));
229
- return;
230
- }
231
-
232
- const homeDir = process.env.HOME || '';
233
- if (!homeDir) {
234
- console.error(chalk.red('❌ Could not determine HOME directory.'));
235
- return;
236
- }
237
-
238
- const agentsDir = path.join(homeDir, 'Library/LaunchAgents');
239
- const logDir = path.join(homeDir, '.rigstate/logs');
240
- const plistPath = path.join(agentsDir, 'com.rigstate.daemon.plist');
241
-
242
- // Ensure directories exist
243
- await fs.mkdir(agentsDir, { recursive: true });
244
- await fs.mkdir(logDir, { recursive: true });
245
-
246
- // Find the executable path
247
- // We assume 'rigstate' is in the PATH, but for launchd we need absolute path
248
- // Or we use the node executable and the script path
249
-
250
- // Strategy: Use the currently executing script path
251
- // This file is likely in dist/commands/daemon.js, we need dist/index.js
252
- // Strategy: Use the currently executing script path
253
- // In the bundle, this file is merged into index.js
254
- const scriptPath = fileURLToPath(import.meta.url);
255
- const nodePath = process.execPath; // Absolute path to node executable
256
-
257
- const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
258
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
259
- <plist version="1.0">
260
- <dict>
261
- <key>Label</key>
262
- <string>com.rigstate.daemon</string>
263
- <key>ProgramArguments</key>
264
- <array>
265
- <string>${nodePath}</string>
266
- <string>${scriptPath}</string>
267
- <string>daemon</string>
268
- <string>--no-bridge</string>
269
- </array>
270
- <key>WorkingDirectory</key>
271
- <string>${process.cwd()}</string>
272
- <key>StandardOutPath</key>
273
- <string>${path.join(logDir, 'daemon.out.log')}</string>
274
- <key>StandardErrorPath</key>
275
- <string>${path.join(logDir, 'daemon.err.log')}</string>
276
- <key>RunAtLoad</key>
277
- <true/>
278
- <key>KeepAlive</key>
279
- <true/>
280
- <key>EnvironmentVariables</key>
281
- <dict>
282
- <key>PATH</key>
283
- <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${process.env.PATH}</string>
284
- </dict>
285
- </dict>
286
- </plist>`;
287
-
288
- try {
289
- await fs.writeFile(plistPath, plistContent);
290
- console.log(chalk.dim(`Created plist at: ${plistPath}`));
291
-
292
- // Load the agent
293
- try {
294
- await execShellCommand(`launchctl unload ${plistPath}`); // Unload if exists
295
- } catch (e) { } // Ignore error if not loaded
296
-
297
- await execShellCommand(`launchctl load ${plistPath}`);
298
-
299
- console.log(chalk.green('✅ Successfully enabled background daemon!'));
300
- console.log(chalk.dim(`Logs: ${logDir}`));
301
- console.log(chalk.dim('The daemon will now restart automatically if it crashes or on reboot.'));
302
-
303
- } catch (error: any) {
304
- console.error(chalk.red('❌ Failed to enable daemon:'), error.message);
305
- }
306
- }
307
-
308
- async function disableDaemon() {
309
- console.log(chalk.bold('\n⚙️ Disabling Rigstate Background Service\n'));
310
-
311
- const homeDir = process.env.HOME || '';
312
- const plistPath = path.join(homeDir, 'Library/LaunchAgents/com.rigstate.daemon.plist');
313
-
314
- try {
315
- await execShellCommand(`launchctl unload ${plistPath}`);
316
- await fs.unlink(plistPath);
317
- console.log(chalk.green('✅ Successfully disabled background daemon.'));
318
- } catch (error: any) {
319
- // If file doesn't exist, it's already disabled
320
- if (error.code === 'ENOENT') {
321
- console.log(chalk.green('✅ Daemon was not enabled.'));
322
- } else {
323
- console.error(chalk.red('❌ Failed to disable daemon:'), error.message);
324
- }
325
- }
326
- }
327
-
328
- async function execShellCommand(cmd: string) {
329
- try {
330
- const output = execSync(cmd, { stdio: 'pipe' }).toString();
331
- return output;
332
- } catch (error: any) {
333
- return error.stderr?.toString() || error.stdout?.toString() || error.message;
334
- }
335
- }
@@ -66,8 +66,14 @@ export function createLinkCommand() {
66
66
  const { syncProjectRules } = await import('./sync-rules.js');
67
67
  await syncProjectRules(projectId, apiKey, apiUrl);
68
68
 
69
- // 3. Git Hooks
70
- console.log(chalk.blue('🛡️ Checking immunity system...'));
69
+ // 3. Git Hooks (Auto-Vaccine)
70
+ console.log(chalk.blue('🛡️ Injecting Guardian hooks...'));
71
+ const { createHooksCommand } = await import('./hooks.js');
72
+ // We can't easily invoke the command action directly without refactoring,
73
+ // but we can reuse the logic. For now, let's replicate the simple install logic
74
+ // or better yet, make a shared utility.
75
+ // Actually, the simplest way for v0.7.25 is to call the install logic directly if possible.
76
+ // Let's use the helper function at the bottom of this file which I see exists.
71
77
  await installHooks(process.cwd());
72
78
 
73
79
  console.log('');
@@ -110,16 +116,57 @@ async function installHooks(cwd: string) {
110
116
  // Actually, let's just use the `hooks` command logic if possible, or a lightweight version
111
117
 
112
118
  try {
113
- // Simpler approach: Check if pre-commit exists
119
+ // Simpler approach: Check if pre-commit exists and install if missing
114
120
  const preCommitPath = path.join(cwd, '.git/hooks/pre-commit');
121
+ let shouldInstall = false;
122
+
115
123
  try {
116
124
  await fs.access(preCommitPath);
117
- console.log(chalk.green(' ✔ Git hooks already active'));
125
+ const content = await fs.readFile(preCommitPath, 'utf-8');
126
+ if (content.includes('rigstate')) {
127
+ console.log(chalk.green(' ✔ Git hooks already active'));
128
+ } else {
129
+ shouldInstall = true;
130
+ }
118
131
  } catch {
119
- console.log(chalk.yellow(' ⚠️ Git hooks missing. Run "rigstate hooks install" to secure repo.'));
132
+ shouldInstall = true;
133
+ }
134
+
135
+ if (shouldInstall) {
136
+ const PRE_COMMIT_SCRIPT = `#!/bin/sh
137
+ # Rigstate Guardian Pre-commit Hook
138
+ # Installed by: rigstate link (v0.7.25)
139
+
140
+ # 1. Silent Sentinel Check
141
+ if [ -f .rigstate/guardian.lock ]; then
142
+ echo "🛑 INTERVENTION ACTIVE: Commit blocked by Silent Sentinel."
143
+ exit 1
144
+ fi
145
+
146
+ echo "🛡️ Running Guardian checks..."
147
+ rigstate check --staged --strict=critical
148
+ exit $?
149
+ `;
150
+ // Ensure hooks dir exists
151
+ await fs.mkdir(path.dirname(preCommitPath), { recursive: true });
152
+
153
+ // Write hook
154
+ if (await fileExists(preCommitPath)) {
155
+ const existing = await fs.readFile(preCommitPath, 'utf-8');
156
+ await fs.writeFile(preCommitPath, existing + '\n\n' + PRE_COMMIT_SCRIPT.replace('#!/bin/sh\n', ''), { mode: 0o755 });
157
+ } else {
158
+ await fs.writeFile(preCommitPath, PRE_COMMIT_SCRIPT, { mode: 0o755 });
159
+ }
160
+ console.log(chalk.green(' ✔ Applied Guardian protection (git-hooks)'));
120
161
  }
121
162
 
122
- } catch (e) {
163
+ } catch (e: any) {
123
164
  // Ignore hook errors during link
165
+ console.log(chalk.dim(' (Skipped hooks: ' + e.message + ')'));
124
166
  }
125
167
  }
168
+
169
+ async function fileExists(path: string) {
170
+ const fs = await import('fs/promises');
171
+ try { await fs.access(path); return true; } catch { return false; }
172
+ }
@@ -160,6 +160,8 @@ export class GuardianDaemon extends EventEmitter {
160
160
  }
161
161
  }
162
162
 
163
+ private violationsMap = new Map<string, any[]>();
164
+
163
165
  private async runIntegrityCheck(filePath: string) {
164
166
  if (!this.guardianMonitor) return;
165
167
 
@@ -170,25 +172,37 @@ export class GuardianDaemon extends EventEmitter {
170
172
  if (result.violations.length > 0) {
171
173
  this.handleViolations(filePath, result.violations);
172
174
  } else {
173
- // Success - might need to clear previous violations for this file
174
- await this.updateViolationReport([]);
175
+ // Success - clear violations for this file
176
+ if (this.violationsMap.has(filePath)) {
177
+ this.violationsMap.delete(filePath);
178
+ this.updateViolationReport(); // Update the aggregate report
179
+ }
175
180
  }
176
181
  }
177
182
 
178
- private async updateViolationReport(violations: any[]) {
183
+ private async updateViolationReport(violations?: any[]) {
184
+ // If violations arg is passed (from handleViolations), update the map first
185
+ // But usually handleViolations calls this without args to just refresh
186
+
179
187
  const reportPath = path.join(process.cwd(), '.rigstate', 'ACTIVE_VIOLATIONS.md');
188
+ const allViolations = Array.from(this.violationsMap.entries());
189
+ const totalCount = allViolations.reduce((acc, [, v]) => acc + v.length, 0);
180
190
 
181
- // This is a simplified version. In a real build, we'd aggregate across all files.
182
- // For now, let's show the most recent or active ones.
183
- let content = `# 🛡️ Guardian Status: ${violations.length > 0 ? '⚠️ ATTENTION' : '✅ PASS'}\n\n`;
184
- content += `*Last check: ${new Date().toLocaleString()}*\n\n`;
191
+ let content = `# 🛡️ Guardian Status: ${totalCount > 0 ? '⚠️ ATTENTION' : '✅ PASS'}\n\n`;
192
+ content += `*Last check: ${new Date().toLocaleString()}*\n`;
193
+ content += `*Files with issues: ${allViolations.length}*\n\n`;
185
194
 
186
- if (violations.length === 0) {
195
+ if (totalCount === 0) {
187
196
  content += "All systems within architectural limits. Frank is satisfied. 🤫\n";
188
197
  } else {
189
198
  content += "### 🚨 Active Violations\n\n";
190
- for (const v of violations) {
191
- content += `- **[${v.severity.toUpperCase()}]**: ${v.message}\n`;
199
+ for (const [file, fileViolations] of allViolations) {
200
+ const relPath = path.relative(process.cwd(), file);
201
+ content += `#### 📄 ${relPath}\n`;
202
+ for (const v of fileViolations) {
203
+ content += `- **[${v.severity.toUpperCase()}]**: ${v.message}\n`;
204
+ }
205
+ content += '\n';
192
206
  }
193
207
  content += "\n---\n*Rigstate Daemon is watching. Fix violations to clear this report.*";
194
208
  }
@@ -202,7 +216,10 @@ export class GuardianDaemon extends EventEmitter {
202
216
  this.state.violationsFound += violations.length;
203
217
  this.emit('violation', { file: filePath, violations });
204
218
 
205
- this.updateViolationReport(violations); // Push to IDE dashboard
219
+ // Update state map
220
+ this.violationsMap.set(filePath, violations);
221
+
222
+ this.updateViolationReport(); // Push to IDE dashboard
206
223
 
207
224
  for (const v of violations) {
208
225
  const level = v.severity === 'critical' ? 'error' : v.severity === 'warning' ? 'warn' : 'info';
@@ -0,0 +1,109 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ async function execShellCommand(cmd: string) {
8
+ try {
9
+ const output = execSync(cmd, { stdio: 'pipe' }).toString();
10
+ return output;
11
+ } catch (error: any) {
12
+ return error.stderr?.toString() || error.stdout?.toString() || error.message;
13
+ }
14
+ }
15
+
16
+ export async function enableDaemon() {
17
+ console.log(chalk.bold('\n⚙️ Enabling Rigstate Background Service (macOS)\n'));
18
+
19
+ if (process.platform !== 'darwin') {
20
+ console.error(chalk.red('❌ Currently only macOS is supported for auto-start.'));
21
+ console.error(chalk.yellow('PRs welcome for Linux/Windows support!'));
22
+ return;
23
+ }
24
+
25
+ const homeDir = process.env.HOME || '';
26
+ if (!homeDir) {
27
+ console.error(chalk.red('❌ Could not determine HOME directory.'));
28
+ return;
29
+ }
30
+
31
+ const agentsDir = path.join(homeDir, 'Library/LaunchAgents');
32
+ const logDir = path.join(homeDir, '.rigstate/logs');
33
+ const plistPath = path.join(agentsDir, 'com.rigstate.daemon.plist');
34
+
35
+ // Ensure directories exist
36
+ await fs.mkdir(agentsDir, { recursive: true });
37
+ await fs.mkdir(logDir, { recursive: true });
38
+
39
+ const scriptPath = fileURLToPath(import.meta.url);
40
+ const nodePath = process.execPath;
41
+
42
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
43
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
44
+ <plist version="1.0">
45
+ <dict>
46
+ <key>Label</key>
47
+ <string>com.rigstate.daemon</string>
48
+ <key>ProgramArguments</key>
49
+ <array>
50
+ <string>${nodePath}</string>
51
+ <string>${scriptPath}</string>
52
+ <string>daemon</string>
53
+ <string>--no-bridge</string>
54
+ </array>
55
+ <key>WorkingDirectory</key>
56
+ <string>${process.cwd()}</string>
57
+ <key>StandardOutPath</key>
58
+ <string>${path.join(logDir, 'daemon.out.log')}</string>
59
+ <key>StandardErrorPath</key>
60
+ <string>${path.join(logDir, 'daemon.err.log')}</string>
61
+ <key>RunAtLoad</key>
62
+ <true/>
63
+ <key>KeepAlive</key>
64
+ <true/>
65
+ <key>EnvironmentVariables</key>
66
+ <dict>
67
+ <key>PATH</key>
68
+ <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${process.env.PATH}</string>
69
+ </dict>
70
+ </dict>
71
+ </plist>`;
72
+
73
+ try {
74
+ await fs.writeFile(plistPath, plistContent);
75
+ console.log(chalk.dim(`Created plist at: ${plistPath}`));
76
+
77
+ try {
78
+ await execShellCommand(`launchctl unload ${plistPath}`);
79
+ } catch (e) { }
80
+
81
+ await execShellCommand(`launchctl load ${plistPath}`);
82
+
83
+ console.log(chalk.green('✅ Successfully enabled background daemon!'));
84
+ console.log(chalk.dim(`Logs: ${logDir}`));
85
+ console.log(chalk.dim('The daemon will now restart automatically if it crashes or on reboot.'));
86
+
87
+ } catch (error: any) {
88
+ console.error(chalk.red('❌ Failed to enable daemon:'), error.message);
89
+ }
90
+ }
91
+
92
+ export async function disableDaemon() {
93
+ console.log(chalk.bold('\n⚙️ Disabling Rigstate Background Service\n'));
94
+
95
+ const homeDir = process.env.HOME || '';
96
+ const plistPath = path.join(homeDir, 'Library/LaunchAgents/com.rigstate.daemon.plist');
97
+
98
+ try {
99
+ await execShellCommand(`launchctl unload ${plistPath}`);
100
+ await fs.unlink(plistPath);
101
+ console.log(chalk.green('✅ Successfully disabled background daemon.'));
102
+ } catch (error: any) {
103
+ if (error.code === 'ENOENT') {
104
+ console.log(chalk.green('✅ Daemon was not enabled.'));
105
+ } else {
106
+ console.error(chalk.red('❌ Failed to disable daemon:'), error.message);
107
+ }
108
+ }
109
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "isRunning": true,
3
- "startedAt": "2026-01-26T13:11:14.337Z",
4
- "filesChecked": 4,
5
- "violationsFound": 83,
6
- "tasksProcessed": 0,
7
- "lastActivity": "2026-01-26T15:24:35.672Z"
8
- }