@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
|
@@ -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
|
+
}
|