@rigstate/cli 0.6.3 → 0.6.7

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.6.3",
3
+ "version": "0.6.7",
4
4
  "description": "Rigstate CLI - Code audit, sync and supervision tool",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -7,6 +7,7 @@
7
7
  * rigstate daemon --no-bridge # Disable Agent Bridge
8
8
  * rigstate daemon --verbose # Enable verbose output
9
9
  */
10
+ /* eslint-disable no-console */
10
11
 
11
12
  import { Command } from 'commander';
12
13
  import chalk from 'chalk';
@@ -40,6 +41,16 @@ export function createDaemonCommand(): Command {
40
41
  return;
41
42
  }
42
43
 
44
+ if (action === 'enable') {
45
+ await enableDaemon();
46
+ return;
47
+ }
48
+
49
+ if (action === 'disable') {
50
+ await disableDaemon();
51
+ return;
52
+ }
53
+
43
54
  const spinner = ora();
44
55
 
45
56
  try {
@@ -195,3 +206,121 @@ async function showStatus(): Promise<void> {
195
206
 
196
207
  console.log('');
197
208
  }
209
+
210
+ async function enableDaemon() {
211
+ console.log(chalk.bold('\n⚙️ Enabling Rigstate Background Service (macOS)\n'));
212
+
213
+ if (process.platform !== 'darwin') {
214
+ console.error(chalk.red('❌ Currently only macOS is supported for auto-start.'));
215
+ console.error(chalk.yellow('PRs welcome for Linux/Windows support!'));
216
+ return;
217
+ }
218
+
219
+ const homeDir = process.env.HOME || '';
220
+ if (!homeDir) {
221
+ console.error(chalk.red('❌ Could not determine HOME directory.'));
222
+ return;
223
+ }
224
+
225
+ const agentsDir = path.join(homeDir, 'Library/LaunchAgents');
226
+ const logDir = path.join(homeDir, '.rigstate/logs');
227
+ const plistPath = path.join(agentsDir, 'com.rigstate.daemon.plist');
228
+
229
+ // Ensure directories exist
230
+ await fs.mkdir(agentsDir, { recursive: true });
231
+ await fs.mkdir(logDir, { recursive: true });
232
+
233
+ // Find the executable path
234
+ // We assume 'rigstate' is in the PATH, but for launchd we need absolute path
235
+ // Or we use the node executable and the script path
236
+
237
+ // Strategy: Use the currently executing script path
238
+ // This file is likely in dist/commands/daemon.js, we need dist/index.js
239
+ const scriptPath = path.resolve(__dirname, '../index.js');
240
+ const nodePath = process.execPath; // Absolute path to node executable
241
+
242
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
243
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
244
+ <plist version="1.0">
245
+ <dict>
246
+ <key>Label</key>
247
+ <string>com.rigstate.daemon</string>
248
+ <key>ProgramArguments</key>
249
+ <array>
250
+ <string>${nodePath}</string>
251
+ <string>${scriptPath}</string>
252
+ <string>daemon</string>
253
+ <string>--no-bridge</string>
254
+ </array>
255
+ <key>WorkingDirectory</key>
256
+ <string>${process.cwd()}</string>
257
+ <key>StandardOutPath</key>
258
+ <string>${path.join(logDir, 'daemon.out.log')}</string>
259
+ <key>StandardErrorPath</key>
260
+ <string>${path.join(logDir, 'daemon.err.log')}</string>
261
+ <key>RunAtLoad</key>
262
+ <true/>
263
+ <key>KeepAlive</key>
264
+ <true/>
265
+ <key>EnvironmentVariables</key>
266
+ <dict>
267
+ <key>PATH</key>
268
+ <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${process.env.PATH}</string>
269
+ </dict>
270
+ </dict>
271
+ </plist>`;
272
+
273
+ try {
274
+ await fs.writeFile(plistPath, plistContent);
275
+ console.log(chalk.dim(`Created plist at: ${plistPath}`));
276
+
277
+ // Load the agent
278
+ try {
279
+ await execShellCommand(`launchctl unload ${plistPath}`); // Unload if exists
280
+ } catch (e) { } // Ignore error if not loaded
281
+
282
+ await execShellCommand(`launchctl load ${plistPath}`);
283
+
284
+ console.log(chalk.green('✅ Successfully enabled background daemon!'));
285
+ console.log(chalk.dim(`Logs: ${logDir}`));
286
+ console.log(chalk.dim('The daemon will now restart automatically if it crashes or on reboot.'));
287
+
288
+ } catch (error: any) {
289
+ console.error(chalk.red('❌ Failed to enable daemon:'), error.message);
290
+ }
291
+ }
292
+
293
+ async function disableDaemon() {
294
+ console.log(chalk.bold('\n⚙️ Disabling Rigstate Background Service\n'));
295
+
296
+ const homeDir = process.env.HOME || '';
297
+ const plistPath = path.join(homeDir, 'Library/LaunchAgents/com.rigstate.daemon.plist');
298
+
299
+ try {
300
+ await execShellCommand(`launchctl unload ${plistPath}`);
301
+ await fs.unlink(plistPath);
302
+ console.log(chalk.green('✅ Successfully disabled background daemon.'));
303
+ } catch (error: any) {
304
+ // If file doesn't exist, it's already disabled
305
+ if (error.code === 'ENOENT') {
306
+ console.log(chalk.green('✅ Daemon was not enabled.'));
307
+ } else {
308
+ console.error(chalk.red('❌ Failed to disable daemon:'), error.message);
309
+ }
310
+ }
311
+ }
312
+
313
+ function execShellCommand(cmd: string) {
314
+ const exec = require('child_process').exec;
315
+ return new Promise((resolve, reject) => {
316
+ exec(cmd, (error: any, stdout: string, stderr: string) => {
317
+ if (error) {
318
+ // launchctl returns non-zero if already loaded/unloaded sometimes,
319
+ // but we might want to check stderr
320
+ // For now simple wrapper
321
+ // reject(error);
322
+ }
323
+ resolve(stdout ? stdout : stderr);
324
+ });
325
+ });
326
+ }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  import { Command } from 'commander';
2
3
  import chalk from 'chalk';
3
4
  import ora from 'ora';
@@ -6,6 +7,118 @@ import path from 'path';
6
7
  import { getApiKey, getProjectId, getApiUrl } from '../utils/config.js';
7
8
  import axios from 'axios';
8
9
 
10
+ // Core Logic (Exported for re-use)
11
+ export async function syncEnv(projectId: string, apiKey: string, apiUrl: string, silent = false): Promise<boolean> {
12
+ if (!silent) {
13
+ console.log('');
14
+ console.log(chalk.bold.yellow('╔══════════════════════════════════════════╗'));
15
+ console.log(chalk.bold.yellow('║') + chalk.bold.white(' 🛡️ RIGSTATE SOVEREIGN VAULT SYNC 🛡️ ') + chalk.bold.yellow('║'));
16
+ console.log(chalk.bold.yellow('╚══════════════════════════════════════════╝'));
17
+ console.log('');
18
+ }
19
+
20
+ const spinner = ora('Fetching secrets from Vault...').start();
21
+
22
+ try {
23
+ const response = await axios.post(`${apiUrl}/api/v1/vault/sync`, {
24
+ project_id: projectId
25
+ }, {
26
+ headers: { Authorization: `Bearer ${apiKey}` }
27
+ });
28
+
29
+ if (!response.data.success) {
30
+ throw new Error(response.data.error || 'Failed to fetch secrets');
31
+ }
32
+
33
+ const vaultContent = response.data.data.content || '';
34
+ const secretCount = response.data.data.count || 0;
35
+
36
+ if (secretCount === 0) {
37
+ spinner.info('No secrets found in Vault for this project.');
38
+ if (!silent) console.log(chalk.dim(' Add secrets via the Rigstate web interface.'));
39
+ return true;
40
+ }
41
+
42
+ spinner.succeed(`Retrieved ${chalk.bold(secretCount)} secret(s)`);
43
+
44
+ // Read existing .env.local for comparison
45
+ const envFile = path.resolve(process.cwd(), '.env.local');
46
+ let existingContent = '';
47
+ let existingKeys: Set<string> = new Set();
48
+
49
+ try {
50
+ existingContent = await fs.readFile(envFile, 'utf-8');
51
+ // Parse existing keys
52
+ existingContent.split('\n').forEach(line => {
53
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
54
+ if (match) existingKeys.add(match[1]);
55
+ });
56
+ } catch (e) {
57
+ // File doesn't exist
58
+ }
59
+
60
+ // Parse vault keys
61
+ const vaultKeys: Set<string> = new Set();
62
+ vaultContent.split('\n').forEach((line: string) => {
63
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
64
+ if (match) vaultKeys.add(match[1]);
65
+ });
66
+
67
+ // Calculate changes
68
+ let newCount = 0;
69
+ let updatedCount = 0;
70
+
71
+ vaultKeys.forEach(key => {
72
+ if (!existingKeys.has(key)) {
73
+ newCount++;
74
+ } else {
75
+ updatedCount++;
76
+ }
77
+ });
78
+
79
+ const unchangedCount = existingKeys.size - updatedCount;
80
+
81
+ // Write new .env.local
82
+ spinner.start('Writing .env.local...');
83
+
84
+ const header = [
85
+ '# ==========================================',
86
+ '# RIGSTATE SOVEREIGN FOUNDATION',
87
+ '# Authenticated Environment Configuration',
88
+ `# Synced at: ${new Date().toISOString()}`,
89
+ `# Project: ${projectId}`,
90
+ '# ==========================================',
91
+ ''
92
+ ].join('\n');
93
+
94
+ await fs.writeFile(envFile, header + vaultContent + '\n');
95
+ spinner.succeed('Written to .env.local');
96
+
97
+ if (!silent) {
98
+ // Summary
99
+ console.log('');
100
+ console.log(chalk.bold.green('✅ Environment synchronized successfully'));
101
+ console.log('');
102
+ console.log(chalk.dim(' Summary:'));
103
+ console.log(chalk.green(` + ${newCount} new`));
104
+ console.log(chalk.yellow(` ~ ${updatedCount} updated`));
105
+ console.log(chalk.dim(` = ${unchangedCount} unchanged`));
106
+ console.log('');
107
+
108
+ // Security reminder
109
+ console.log(chalk.bold.yellow('⚠️ Security Reminder:'));
110
+ console.log(chalk.dim(' - Never commit .env.local to version control.'));
111
+ console.log(chalk.dim(' - Ensure .gitignore includes .env.local'));
112
+ console.log('');
113
+ }
114
+ return true;
115
+
116
+ } catch (e: any) {
117
+ spinner.fail(chalk.red(`Failed to fetch secrets: ${e.message}`));
118
+ return false;
119
+ }
120
+ }
121
+
9
122
  export function createEnvPullCommand() {
10
123
  const envPull = new Command('env');
11
124
 
@@ -13,14 +126,6 @@ export function createEnvPullCommand() {
13
126
  .command('pull')
14
127
  .description('Pull environment variables from project vault')
15
128
  .action(async () => {
16
- console.log('');
17
- console.log(chalk.bold.yellow('╔══════════════════════════════════════════╗'));
18
- console.log(chalk.bold.yellow('║') + chalk.bold.white(' 🛡️ RIGSTATE SOVEREIGN VAULT SYNC 🛡️ ') + chalk.bold.yellow('║'));
19
- console.log(chalk.bold.yellow('╚══════════════════════════════════════════╝'));
20
- console.log('');
21
-
22
- const spinner = ora('Authenticating with Vault...').start();
23
-
24
129
  // Get config
25
130
  let apiKey: string;
26
131
  let projectId: string | undefined;
@@ -28,13 +133,11 @@ export function createEnvPullCommand() {
28
133
  try {
29
134
  apiKey = getApiKey();
30
135
  } catch (e) {
31
- spinner.fail(chalk.red('Not authenticated. Run "rigstate login" first.'));
136
+ console.error(chalk.red('Not authenticated. Run "rigstate login" first.'));
32
137
  return;
33
138
  }
34
- spinner.succeed('Authenticated');
35
139
 
36
140
  // Get project context
37
- spinner.start('Reading project configuration...');
38
141
  projectId = getProjectId();
39
142
 
40
143
  if (!projectId) {
@@ -47,111 +150,12 @@ export function createEnvPullCommand() {
47
150
  }
48
151
 
49
152
  if (!projectId) {
50
- spinner.fail(chalk.red('No project context. Run "rigstate link" first.'));
153
+ console.error(chalk.red('No project context. Run "rigstate link" first.'));
51
154
  return;
52
155
  }
53
156
 
54
- spinner.succeed(`Project: ${chalk.cyan(projectId.substring(0, 8))}...`);
55
-
56
157
  const apiUrl = getApiUrl();
57
-
58
- // Fetch secrets from Vault API
59
- spinner.start('Fetching secrets from Vault...');
60
-
61
- try {
62
- const response = await axios.post(`${apiUrl}/api/v1/vault/sync`, {
63
- project_id: projectId
64
- }, {
65
- headers: { Authorization: `Bearer ${apiKey}` }
66
- });
67
-
68
- if (!response.data.success) {
69
- throw new Error(response.data.error || 'Failed to fetch secrets');
70
- }
71
-
72
- const vaultContent = response.data.data.content || '';
73
- const secretCount = response.data.data.count || 0;
74
-
75
- if (secretCount === 0) {
76
- spinner.info('No secrets found in Vault for this project.');
77
- console.log(chalk.dim(' Add secrets via the Rigstate web interface.'));
78
- return;
79
- }
80
-
81
- spinner.succeed(`Retrieved ${chalk.bold(secretCount)} secret(s)`);
82
-
83
- // Read existing .env.local for comparison
84
- const envFile = path.resolve(process.cwd(), '.env.local');
85
- let existingContent = '';
86
- let existingKeys: Set<string> = new Set();
87
-
88
- try {
89
- existingContent = await fs.readFile(envFile, 'utf-8');
90
- // Parse existing keys
91
- existingContent.split('\n').forEach(line => {
92
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
93
- if (match) existingKeys.add(match[1]);
94
- });
95
- } catch (e) {
96
- // File doesn't exist
97
- }
98
-
99
- // Parse vault keys
100
- const vaultKeys: Set<string> = new Set();
101
- vaultContent.split('\n').forEach((line: string) => {
102
- const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
103
- if (match) vaultKeys.add(match[1]);
104
- });
105
-
106
- // Calculate changes
107
- let newCount = 0;
108
- let updatedCount = 0;
109
-
110
- vaultKeys.forEach(key => {
111
- if (!existingKeys.has(key)) {
112
- newCount++;
113
- } else {
114
- updatedCount++;
115
- }
116
- });
117
-
118
- const unchangedCount = existingKeys.size - updatedCount;
119
-
120
- // Write new .env.local
121
- spinner.start('Writing .env.local...');
122
-
123
- const header = [
124
- '# ==========================================',
125
- '# RIGSTATE SOVEREIGN FOUNDATION',
126
- '# Authenticated Environment Configuration',
127
- `# Synced at: ${new Date().toISOString()}`,
128
- `# Project: ${projectId}`,
129
- '# ==========================================',
130
- ''
131
- ].join('\n');
132
-
133
- await fs.writeFile(envFile, header + vaultContent + '\n');
134
- spinner.succeed('Written to .env.local');
135
-
136
- // Summary
137
- console.log('');
138
- console.log(chalk.bold.green('✅ Environment synchronized successfully'));
139
- console.log('');
140
- console.log(chalk.dim(' Summary:'));
141
- console.log(chalk.green(` + ${newCount} new`));
142
- console.log(chalk.yellow(` ~ ${updatedCount} updated`));
143
- console.log(chalk.dim(` = ${unchangedCount} unchanged`));
144
- console.log('');
145
-
146
- // Security reminder
147
- console.log(chalk.bold.yellow('⚠️ Security Reminder:'));
148
- console.log(chalk.dim(' - Never commit .env.local to version control.'));
149
- console.log(chalk.dim(' - Ensure .gitignore includes .env.local'));
150
- console.log('');
151
-
152
- } catch (e: any) {
153
- spinner.fail(chalk.red(`Failed to fetch secrets: ${e.message}`));
154
- }
158
+ await syncEnv(projectId, apiKey, apiUrl);
155
159
  });
156
160
 
157
161
  return envPull;
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  import { Command } from 'commander';
2
3
  import fs from 'fs/promises';
3
4
  import path from 'path';
@@ -38,8 +39,78 @@ export function createLinkCommand() {
38
39
  await fs.writeFile(manifestPath, JSON.stringify(content, null, 2), 'utf-8');
39
40
  console.log(chalk.green(`✔ Linked to project ID: ${projectId}`));
40
41
  console.log(chalk.dim(`Created local context manifest at .rigstate`));
42
+
43
+ // === SMART AUTOMATION ===
44
+ console.log('');
45
+ console.log(chalk.bold('🤖 Rigstate Automation Detected'));
46
+ console.log('');
47
+
48
+ const { getApiKey, getApiUrl } = await import('../utils/config.js');
49
+ const apiKey = getApiKey(); // Might throw if not logged in
50
+ const apiUrl = getApiUrl();
51
+
52
+ if (apiKey) {
53
+ // 1. Env Sync
54
+ console.log(chalk.blue('🔐 Checking Vault for secrets...'));
55
+ const { syncEnv } = await import('./env.js');
56
+ await syncEnv(projectId, apiKey, apiUrl, true);
57
+
58
+ // 2. Rules Sync
59
+ console.log(chalk.blue('🧠 Syncing neural instructions...'));
60
+ const { syncProjectRules } = await import('./sync-rules.js');
61
+ await syncProjectRules(projectId, apiKey, apiUrl);
62
+
63
+ // 3. Git Hooks
64
+ console.log(chalk.blue('🛡️ Checking immunity system...'));
65
+ await installHooks(process.cwd());
66
+ }
67
+
68
+ console.log('');
69
+ console.log(chalk.bold.green('🚀 Link Complete! Your environment is ready.'));
70
+
41
71
  } catch (error: any) {
42
- console.error(chalk.red(`Failed to link project: ${error.message}`));
72
+ if (error.message.includes('Not authenticated')) {
73
+ console.warn(chalk.yellow('⚠️ Not authenticated. Run "rigstate login" to enable automation features.'));
74
+ } else {
75
+ console.error(chalk.red(`Failed to link project: ${error.message}`));
76
+ }
43
77
  }
44
78
  });
45
79
  }
80
+
81
+ async function installHooks(cwd: string) {
82
+ const fs = await import('fs/promises');
83
+ const path = await import('path');
84
+
85
+ // Check if git repo
86
+ try {
87
+ await fs.access(path.join(cwd, '.git'));
88
+ } catch {
89
+ console.log(chalk.dim(' (Not a git repository, skipping hooks)'));
90
+ return;
91
+ }
92
+
93
+ const hooksDir = path.join(cwd, '.husky');
94
+
95
+ // Check if simple husky setup exists or just do a manual pre-commit script
96
+ // For now, let's look for a basic pre-commit file in .git/hooks if husky isn't there
97
+ // Actually, let's just use the `hooks` command logic if possible, or a lightweight version
98
+
99
+ try {
100
+ const { installHooks: runInstall } = await import('./hooks.js');
101
+ // We need to mock the command context or extract logic.
102
+ // For simplicity/robustness in this iteration, let's just suggest it or verify
103
+
104
+ // Simpler approach: Check if pre-commit exists
105
+ const preCommitPath = path.join(cwd, '.git/hooks/pre-commit');
106
+ try {
107
+ await fs.access(preCommitPath);
108
+ console.log(chalk.green(' ✔ Git hooks already active'));
109
+ } catch {
110
+ console.log(chalk.yellow(' ⚠️ Git hooks missing. Run "rigstate hooks install" to secure repo.'));
111
+ }
112
+
113
+ } catch (e) {
114
+ // Ignore hook errors during link
115
+ }
116
+ }
@@ -1,35 +1,54 @@
1
+ /* eslint-disable no-console */
1
2
  import { Command } from 'commander';
2
3
  import chalk from 'chalk';
3
4
  import { setApiKey } from '../utils/config.js';
4
5
 
5
6
  export function createLoginCommand(): Command {
6
- return new Command('login')
7
- .description('Authenticate with your Rigstate API key')
8
- .argument('<api-key>', 'Your Rigstate API key (starts with sk_)')
9
- .action(async (apiKey: string) => {
10
- try {
11
- // Basic validation
12
- if (!apiKey || !apiKey.startsWith('sk_rigstate_')) {
13
- console.error(chalk.red('❌ Invalid API key format'));
14
- console.error(chalk.dim('API keys must start with "sk_rigstate_"'));
15
- process.exit(1);
16
- }
7
+ return new Command('login')
8
+ .description('Authenticate with your Rigstate API key')
9
+ .argument('<api-key>', 'Your Rigstate API key (starts with sk_)')
10
+ .action(async (apiKey: string) => {
11
+ try {
12
+ // Basic validation
13
+ if (!apiKey || !apiKey.startsWith('sk_rigstate_')) {
14
+ console.error(chalk.red('❌ Invalid API key format'));
15
+ console.error(chalk.dim('API keys must start with "sk_rigstate_"'));
16
+ process.exit(1);
17
+ }
17
18
 
18
- // Store the API key
19
- setApiKey(apiKey);
19
+ // Store the API key
20
+ setApiKey(apiKey);
20
21
 
21
- console.log(chalk.green('✅ Successfully logged in!'));
22
- console.log(
23
- chalk.dim(
24
- `\nYour API key has been securely stored. You can now use "rigstate scan" to audit your code.`
25
- )
26
- );
27
- } catch (error) {
28
- console.error(
29
- chalk.red(' Login failed:'),
30
- error instanceof Error ? error.message : 'Unknown error'
31
- );
32
- process.exit(1);
33
- }
34
- });
22
+ console.log(chalk.green('✅ Successfully logged in!'));
23
+ console.log(
24
+ chalk.dim(
25
+ `\nYour API key has been securely stored. You can now use "rigstate scan" to audit your code.`
26
+ )
27
+ );
28
+
29
+ console.log(chalk.bold('\n🤖 Cursor MCP Configuration'));
30
+ console.log(chalk.dim('Copy and paste this into Cursor Settings -> Features -> MCP:'));
31
+ console.log(chalk.cyan(`
32
+ {
33
+ "mcpServers": {
34
+ "rigstate": {
35
+ "command": "npx",
36
+ "args": [
37
+ "-y",
38
+ "@rigstate/mcp@latest"
39
+ ],
40
+ "env": {
41
+ "RIGSTATE_API_KEY": "${apiKey}"
42
+ }
43
+ }
44
+ }
45
+ }`));
46
+ } catch (error) {
47
+ console.error(
48
+ chalk.red('❌ Login failed:'),
49
+ error instanceof Error ? error.message : 'Unknown error'
50
+ );
51
+ process.exit(1);
52
+ }
53
+ });
35
54
  }