@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,158 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ import { getApiKey, getProjectId, getApiUrl } from '../utils/config.js';
7
+ import axios from 'axios';
8
+
9
+ export function createEnvPullCommand() {
10
+ const envPull = new Command('env');
11
+
12
+ envPull
13
+ .command('pull')
14
+ .description('Pull environment variables from project vault')
15
+ .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
+ // Get config
25
+ let apiKey: string;
26
+ let projectId: string | undefined;
27
+
28
+ try {
29
+ apiKey = getApiKey();
30
+ } catch (e) {
31
+ spinner.fail(chalk.red('Not authenticated. Run "rigstate login" first.'));
32
+ return;
33
+ }
34
+ spinner.succeed('Authenticated');
35
+
36
+ // Get project context
37
+ spinner.start('Reading project configuration...');
38
+ projectId = getProjectId();
39
+
40
+ if (!projectId) {
41
+ try {
42
+ const manifestPath = path.join(process.cwd(), '.rigstate');
43
+ const content = await fs.readFile(manifestPath, 'utf-8');
44
+ const manifest = JSON.parse(content);
45
+ projectId = manifest.project_id;
46
+ } catch (e) { }
47
+ }
48
+
49
+ if (!projectId) {
50
+ spinner.fail(chalk.red('No project context. Run "rigstate link" first.'));
51
+ return;
52
+ }
53
+
54
+ spinner.succeed(`Project: ${chalk.cyan(projectId.substring(0, 8))}...`);
55
+
56
+ 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
+ }
155
+ });
156
+
157
+ return envPull;
158
+ }
@@ -0,0 +1,140 @@
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 inquirer from 'inquirer';
9
+ import * as Diff from 'diff';
10
+ import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
11
+ import { readGitignore, shouldIgnore, isCodeFile } from '../utils/files.js';
12
+
13
+ export function createFixCommand(): Command {
14
+ return new Command('fix')
15
+ .description('Scan and interactively FIX detected issues using Rigstate AI')
16
+ .argument('[path]', 'Directory or file to scan', '.')
17
+ .option('--project <id>', 'Project ID to context-aware audit')
18
+ .action(async (targetPath: string, options: { project?: string }) => {
19
+ const spinner = ora();
20
+
21
+ try {
22
+ const apiKey = getApiKey();
23
+ const apiUrl = getApiUrl();
24
+ const projectId = options.project || getProjectId();
25
+
26
+ if (!projectId) {
27
+ console.log(chalk.yellow('⚠️ Project ID is required for fixing. Using default or pass --project <id>'));
28
+ // We can proceed without it, but fix quality drops. Let's warn.
29
+ }
30
+
31
+ const scanPath = path.resolve(process.cwd(), targetPath);
32
+
33
+ // --- Reuse Scan Logic (Simplified) ---
34
+ const gitignorePatterns = await readGitignore(scanPath);
35
+ const pattern = path.join(scanPath, '**/*');
36
+ const allFiles = await glob(pattern, { nodir: true, dot: false, ignore: ['**/node_modules/**', '**/.git/**'] });
37
+ const codeFiles = allFiles.filter(file => {
38
+ const relativePath = path.relative(scanPath, file);
39
+ return isCodeFile(file) && !shouldIgnore(relativePath, gitignorePatterns);
40
+ });
41
+
42
+ if (codeFiles.length === 0) {
43
+ console.log(chalk.yellow('No code files found.'));
44
+ return;
45
+ }
46
+
47
+ console.log(chalk.bold(`\n🧠 Rigstate Fix Mode`));
48
+ console.log(chalk.dim(`Scanning ${codeFiles.length} files with Project Context...\n`));
49
+
50
+ let fixedCount = 0;
51
+
52
+ for (let i = 0; i < codeFiles.length; i++) {
53
+ const filePath = codeFiles[i];
54
+ const relativePath = path.relative(scanPath, filePath);
55
+
56
+ spinner.start(`Analyzing ${relativePath}...`);
57
+
58
+ try {
59
+ const content = await fs.readFile(filePath, 'utf-8');
60
+ const response = await axios.post(
61
+ `${apiUrl}/api/v1/audit`,
62
+ { content, file_path: relativePath, project_id: projectId },
63
+ { headers: { 'Authorization': `Bearer ${apiKey}` }, timeout: 120000 }
64
+ );
65
+
66
+ const vulnerabilities = response.data.vulnerabilities || [];
67
+ const fixableIssues = vulnerabilities.filter((v: any) => v.fixed_content);
68
+
69
+ if (fixableIssues.length > 0) {
70
+ spinner.stop();
71
+ console.log(`\n${chalk.bold(relativePath)}: Found ${fixableIssues.length} fixable issues.`);
72
+
73
+ for (const issue of fixableIssues) {
74
+ console.log(chalk.red(`\n[${issue.type}] ${issue.title}`));
75
+ console.log(chalk.dim(issue.suggestion || issue.message));
76
+
77
+ // Show Diff
78
+ const diff = Diff.createTwoFilesPatch(relativePath, relativePath, content, issue.fixed_content, 'Current', 'Fixed');
79
+ console.log('\n' + diff.split('\n').slice(0, 15).join('\n') + (diff.split('\n').length > 15 ? '\n...' : ''));
80
+
81
+ const { apply } = await inquirer.prompt([{
82
+ type: 'confirm',
83
+ name: 'apply',
84
+ message: `Apply this fix to ${chalk.cyan(relativePath)}?`,
85
+ default: true
86
+ }]);
87
+
88
+ if (apply) {
89
+ await fs.writeFile(filePath, issue.fixed_content);
90
+ console.log(chalk.green(`✅ Fixed applied!`));
91
+ fixedCount++;
92
+
93
+ if (issue.related_step_id) {
94
+ const { completeStep } = await inquirer.prompt([{
95
+ type: 'confirm',
96
+ name: 'completeStep',
97
+ message: `Frank thinks this fix completes a Roadmap Step. Mark as COMPLETED in Rigstate?`,
98
+ default: true
99
+ }]);
100
+
101
+ if (completeStep) {
102
+ try {
103
+ await axios.post(
104
+ `${apiUrl}/api/v1/roadmap/update-status`,
105
+ { step_id: issue.related_step_id, status: 'COMPLETED', project_id: projectId },
106
+ { headers: { 'Authorization': `Bearer ${apiKey}` } }
107
+ );
108
+ console.log(chalk.green(`🚀 Roadmap updated! Mission Control is in sync.`));
109
+ } catch (err: any) {
110
+ console.error(chalk.yellow(`Failed to update roadmap: ${err.message}`));
111
+ }
112
+ }
113
+ }
114
+
115
+ // Stop processing this file as content changed
116
+ break;
117
+ } else {
118
+ console.log(chalk.dim('Skipped.'));
119
+ }
120
+ }
121
+ } else {
122
+ spinner.text = `Checked ${relativePath} (No auto-fixes)`;
123
+ }
124
+
125
+ } catch (e: any) {
126
+ // silent fail for file read/network errors in fix mode to keep flow
127
+ }
128
+ }
129
+
130
+ spinner.stop();
131
+ console.log(chalk.bold.green(`\n\n🚀 Fix session complete!`));
132
+ console.log(`Frank fixed ${fixedCount} detected issues.`);
133
+ console.log(chalk.dim(`Run 'rigstate scan' to verify remaining issues.`));
134
+
135
+ } catch (error: any) {
136
+ spinner.fail('Fix session failed');
137
+ console.error(error.message);
138
+ }
139
+ });
140
+ }
@@ -0,0 +1,134 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getApiKey, getProjectId, getApiUrl } from '../utils/config.js';
5
+ import axios from 'axios';
6
+ import { execSync } from 'child_process';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+
10
+ export function createFocusCommand() {
11
+ const focus = new Command('focus');
12
+
13
+ focus
14
+ .alias('task')
15
+ .description('Get the next active roadmap task and copy its prompt to clipboard')
16
+ .option('--no-copy', 'Do not copy to clipboard')
17
+ .action(async (options) => {
18
+ const spinner = ora('Fetching next objective...').start();
19
+
20
+ // Get config
21
+ let apiKey: string;
22
+ let projectId: string | undefined;
23
+
24
+ try {
25
+ apiKey = getApiKey();
26
+ } catch (e) {
27
+ spinner.fail(chalk.red('Not authenticated. Run "rigstate login" first.'));
28
+ return;
29
+ }
30
+
31
+ projectId = getProjectId();
32
+ if (!projectId) {
33
+ try {
34
+ const manifestPath = path.join(process.cwd(), '.rigstate');
35
+ const content = await fs.readFile(manifestPath, 'utf-8');
36
+ const manifest = JSON.parse(content);
37
+ projectId = manifest.project_id;
38
+ } catch (e) { }
39
+ }
40
+
41
+ if (!projectId) {
42
+ spinner.fail(chalk.red('No project context. Run "rigstate link" first.'));
43
+ return;
44
+ }
45
+
46
+ const apiUrl = getApiUrl();
47
+
48
+ try {
49
+ // Fetch roadmap
50
+ const response = await axios.get(`${apiUrl}/api/v1/roadmap`, {
51
+ params: { project_id: projectId },
52
+ headers: { Authorization: `Bearer ${apiKey}` }
53
+ });
54
+
55
+ if (!response.data.success) {
56
+ throw new Error(response.data.error || 'Failed to fetch roadmap');
57
+ }
58
+
59
+ const roadmap = response.data.data.roadmap || [];
60
+
61
+ // Priority: IN_PROGRESS > ACTIVE > LOCKED
62
+ const statusPriority: Record<string, number> = {
63
+ 'IN_PROGRESS': 0,
64
+ 'ACTIVE': 1,
65
+ 'LOCKED': 2
66
+ };
67
+
68
+ const activeTasks = roadmap
69
+ .filter((t: any) => ['IN_PROGRESS', 'ACTIVE', 'LOCKED'].includes(t.status))
70
+ .sort((a: any, b: any) => {
71
+ const pA = statusPriority[a.status] ?? 99;
72
+ const pB = statusPriority[b.status] ?? 99;
73
+ if (pA !== pB) return pA - pB;
74
+ return (a.step_number || 0) - (b.step_number || 0);
75
+ });
76
+
77
+ if (activeTasks.length === 0) {
78
+ spinner.succeed('All caught up! No active tasks found.');
79
+ return;
80
+ }
81
+
82
+ const nextTask = activeTasks[0];
83
+ spinner.stop();
84
+
85
+ // Display
86
+ console.log('');
87
+ console.log(chalk.bold.blue(`📌 Task #${nextTask.step_number || '?'}: ${nextTask.title}`));
88
+
89
+ const statusColor = nextTask.status === 'IN_PROGRESS' ? chalk.yellow :
90
+ nextTask.status === 'ACTIVE' ? chalk.green : chalk.dim;
91
+
92
+ console.log(chalk.dim('Status: ') + statusColor(nextTask.status));
93
+ console.log(chalk.dim('─'.repeat(60)));
94
+
95
+ if (nextTask.prompt_content) {
96
+ console.log(chalk.white(nextTask.prompt_content));
97
+ console.log(chalk.dim('─'.repeat(60)));
98
+
99
+ // Auto-copy for Mac
100
+ if (options.copy !== false) {
101
+ try {
102
+ if (process.platform === 'darwin') {
103
+ execSync('pbcopy', { input: nextTask.prompt_content });
104
+ console.log(chalk.green('✅ Prompt copied to clipboard! Ready to paste (Cmd+V).'));
105
+ } else if (process.platform === 'linux') {
106
+ try {
107
+ execSync('xclip -selection clipboard', { input: nextTask.prompt_content });
108
+ console.log(chalk.green('✅ Prompt copied to clipboard!'));
109
+ } catch (e) {
110
+ console.log(chalk.yellow('ℹ️ Copy prompt manually (xclip not available)'));
111
+ }
112
+ } else {
113
+ console.log(chalk.yellow('ℹ️ Copy prompt manually (Auto-copy not supported on this OS)'));
114
+ }
115
+ } catch (e) {
116
+ // ignore copy error
117
+ }
118
+ }
119
+ } else {
120
+ console.log(chalk.yellow('No prompt instructions available.'));
121
+ if (nextTask.architectural_brief) {
122
+ console.log(chalk.bold('Brief:'));
123
+ console.log(nextTask.architectural_brief);
124
+ }
125
+ }
126
+ console.log('');
127
+
128
+ } catch (e: any) {
129
+ spinner.fail(chalk.red(`Failed to fetch task: ${e.message}`));
130
+ }
131
+ });
132
+
133
+ return focus;
134
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * rigstate hooks - Manage git hooks for Guardian integration
3
+ *
4
+ * Usage:
5
+ * rigstate hooks install # Install pre-commit hook
6
+ * rigstate hooks uninstall # Remove pre-commit hook
7
+ */
8
+
9
+ import { Command } from 'commander';
10
+ import chalk from 'chalk';
11
+ import fs from 'fs/promises';
12
+ import path from 'path';
13
+
14
+ const PRE_COMMIT_SCRIPT = `#!/bin/sh
15
+ # Rigstate Guardian Pre-commit Hook
16
+ # Installed by: rigstate hooks install
17
+
18
+ # 1. Silent Sentinel Check (Phase 5)
19
+ if [ -f .rigstate/guardian.lock ]; then
20
+ echo "🛑 INTERVENTION ACTIVE: Commit blocked by Silent Sentinel."
21
+ echo " A critical violation ('HARD_LOCK') was detected by the Guardian Daemon."
22
+ echo " Please fix the violation to unlock the repo."
23
+ echo ""
24
+ if grep -q "HARD_LOCK_ACTIVE" .rigstate/guardian.lock; then
25
+ cat .rigstate/guardian.lock
26
+ fi
27
+ exit 1
28
+ fi
29
+
30
+ echo "🛡️ Running Guardian checks..."
31
+
32
+ # Run check with strict mode for critical violations
33
+ rigstate check --staged --strict=critical
34
+
35
+ # Exit with the same code as rigstate check
36
+ exit $?
37
+ `;
38
+
39
+ export function createHooksCommand(): Command {
40
+ const hooks = new Command('hooks')
41
+ .description('Manage git hooks for Guardian integration');
42
+
43
+ hooks
44
+ .command('install')
45
+ .description('Install pre-commit hook to run Guardian checks')
46
+ .option('--strict [level]', 'Strict level: "all" or "critical" (default)', 'critical')
47
+ .action(async (options: { strict: string }) => {
48
+ try {
49
+ // 1. Find .git directory
50
+ const gitDir = path.join(process.cwd(), '.git');
51
+ try {
52
+ await fs.access(gitDir);
53
+ } catch {
54
+ console.log(chalk.red('❌ Not a git repository.'));
55
+ console.log(chalk.dim(' Initialize with "git init" first.'));
56
+ process.exit(1);
57
+ }
58
+
59
+ // 2. Ensure hooks directory exists
60
+ const hooksDir = path.join(gitDir, 'hooks');
61
+ await fs.mkdir(hooksDir, { recursive: true });
62
+
63
+ // 3. Check for existing pre-commit
64
+ const preCommitPath = path.join(hooksDir, 'pre-commit');
65
+ let existingContent = '';
66
+ try {
67
+ existingContent = await fs.readFile(preCommitPath, 'utf-8');
68
+ if (existingContent.includes('rigstate')) {
69
+ console.log(chalk.yellow('⚠ Rigstate pre-commit hook already installed.'));
70
+ console.log(chalk.dim(' Use "rigstate hooks uninstall" to remove first.'));
71
+ return;
72
+ }
73
+ } catch {
74
+ // File doesn't exist, that's fine
75
+ }
76
+
77
+ // 4. Create hook script with configurable strict level
78
+ let script = PRE_COMMIT_SCRIPT;
79
+ if (options.strict === 'all') {
80
+ script = script.replace('--strict=critical', '--strict');
81
+ }
82
+
83
+ // 5. If existing hook, append to it
84
+ if (existingContent && !existingContent.includes('rigstate')) {
85
+ // Append our hook to existing
86
+ const combinedScript = existingContent + '\n\n' + script.replace('#!/bin/sh\n', '');
87
+ await fs.writeFile(preCommitPath, combinedScript, { mode: 0o755 });
88
+ console.log(chalk.green('✅ Rigstate hook appended to existing pre-commit.'));
89
+ } else {
90
+ // Create new hook
91
+ await fs.writeFile(preCommitPath, script, { mode: 0o755 });
92
+ console.log(chalk.green('✅ Pre-commit hook installed!'));
93
+ }
94
+
95
+ console.log(chalk.dim(` Path: ${preCommitPath}`));
96
+ console.log(chalk.dim(` Strict level: ${options.strict}`));
97
+ console.log('');
98
+ console.log(chalk.cyan('Guardian will now check your code before each commit.'));
99
+ console.log(chalk.dim('Use "rigstate hooks uninstall" to remove the hook.'));
100
+
101
+ } catch (error: any) {
102
+ console.error(chalk.red('Failed to install hook:'), error.message);
103
+ process.exit(1);
104
+ }
105
+ });
106
+
107
+ hooks
108
+ .command('uninstall')
109
+ .description('Remove Rigstate pre-commit hook')
110
+ .action(async () => {
111
+ try {
112
+ const preCommitPath = path.join(process.cwd(), '.git', 'hooks', 'pre-commit');
113
+
114
+ try {
115
+ const content = await fs.readFile(preCommitPath, 'utf-8');
116
+
117
+ if (!content.includes('rigstate')) {
118
+ console.log(chalk.yellow('⚠ No Rigstate hook found in pre-commit.'));
119
+ return;
120
+ }
121
+
122
+ // Check if it's our hook exclusively
123
+ if (content.includes('# Rigstate Guardian Pre-commit Hook') &&
124
+ content.trim().split('\n').filter(l => l && !l.startsWith('#')).length <= 4) {
125
+ // It's only our hook, remove the file
126
+ await fs.unlink(preCommitPath);
127
+ console.log(chalk.green('✅ Pre-commit hook removed.'));
128
+ } else {
129
+ // There's other content, just remove our section
130
+ const lines = content.split('\n');
131
+ const filteredLines = [];
132
+ let inRigstateSection = false;
133
+
134
+ for (const line of lines) {
135
+ if (line.includes('Rigstate Guardian Pre-commit Hook')) {
136
+ inRigstateSection = true;
137
+ continue;
138
+ }
139
+ if (inRigstateSection && line.includes('exit $?')) {
140
+ inRigstateSection = false;
141
+ continue;
142
+ }
143
+ if (!inRigstateSection && !line.includes('rigstate check')) {
144
+ filteredLines.push(line);
145
+ }
146
+ }
147
+
148
+ await fs.writeFile(preCommitPath, filteredLines.join('\n'), { mode: 0o755 });
149
+ console.log(chalk.green('✅ Rigstate section removed from pre-commit hook.'));
150
+ }
151
+
152
+ } catch {
153
+ console.log(chalk.yellow('⚠ No pre-commit hook found.'));
154
+ }
155
+
156
+ } catch (error: any) {
157
+ console.error(chalk.red('Failed to uninstall hook:'), error.message);
158
+ process.exit(1);
159
+ }
160
+ });
161
+
162
+ return hooks;
163
+ }