@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,339 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getApiKey, getProjectId, getApiUrl, setProjectId } from '../utils/config.js';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
export function createSyncCommand() {
|
|
10
|
+
const sync = new Command('sync');
|
|
11
|
+
|
|
12
|
+
sync
|
|
13
|
+
.description('Synchronize local state with Rigstate Cloud')
|
|
14
|
+
.option('-p, --project <id>', 'Specify Project ID (saves to config automatically)')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
const spinner = ora('Synchronizing project state...').start();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// 1. Authentication Check
|
|
20
|
+
let apiKey;
|
|
21
|
+
try {
|
|
22
|
+
apiKey = getApiKey();
|
|
23
|
+
} catch (e) {
|
|
24
|
+
spinner.fail('Not authenticated. Run "rigstate login" first.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Project Context Resolution
|
|
29
|
+
let projectId = options.project;
|
|
30
|
+
|
|
31
|
+
// Check local .rigstate manifest
|
|
32
|
+
if (!projectId) {
|
|
33
|
+
try {
|
|
34
|
+
const manifestPath = path.join(process.cwd(), '.rigstate');
|
|
35
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
36
|
+
const manifest = JSON.parse(manifestContent);
|
|
37
|
+
if (manifest.project_id) projectId = manifest.project_id;
|
|
38
|
+
} catch (e) { }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check global config
|
|
42
|
+
if (!projectId) projectId = getProjectId();
|
|
43
|
+
|
|
44
|
+
if (options.project) {
|
|
45
|
+
// Persistence: Save project ID for future use
|
|
46
|
+
setProjectId(options.project);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!projectId) {
|
|
50
|
+
spinner.fail('No project context found.\n Run with --project <id> once to save context.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const apiUrl = getApiUrl();
|
|
55
|
+
|
|
56
|
+
// 3. API Execution
|
|
57
|
+
const response = await axios.get(`${apiUrl}/api/v1/roadmap`, {
|
|
58
|
+
params: { project_id: projectId },
|
|
59
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Parse Standardized Response ({ success, data, ... })
|
|
63
|
+
if (!response.data.success) {
|
|
64
|
+
throw new Error(response.data.error || 'Unknown API failure');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { roadmap, project } = response.data.data;
|
|
68
|
+
const timestamp = response.data.timestamp;
|
|
69
|
+
|
|
70
|
+
// 4. Write Artifacts
|
|
71
|
+
const targetPath = path.join(process.cwd(), 'roadmap.json');
|
|
72
|
+
const fileContent = JSON.stringify({
|
|
73
|
+
project,
|
|
74
|
+
last_synced: timestamp,
|
|
75
|
+
roadmap
|
|
76
|
+
}, null, 2);
|
|
77
|
+
|
|
78
|
+
await fs.writeFile(targetPath, fileContent, 'utf-8');
|
|
79
|
+
|
|
80
|
+
// 4b. Write Context Manifest (.rigstate) - CONTEXT GUARD
|
|
81
|
+
try {
|
|
82
|
+
const manifestPath = path.join(process.cwd(), '.rigstate');
|
|
83
|
+
const manifestContent = {
|
|
84
|
+
project_id: projectId,
|
|
85
|
+
project_name: project,
|
|
86
|
+
last_synced: timestamp,
|
|
87
|
+
api_url: apiUrl
|
|
88
|
+
};
|
|
89
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifestContent, null, 2), 'utf-8');
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Fail silently
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4c. Provision Agent Skills (Skills Dominion)
|
|
95
|
+
console.log(chalk.bold('\n🧠 Agent Skills Provisioning...'));
|
|
96
|
+
try {
|
|
97
|
+
const { provisionSkills, generateSkillsDiscoveryBlock } = await import('../utils/skills-provisioner.js');
|
|
98
|
+
const skills = await provisionSkills(apiUrl, apiKey, projectId, process.cwd());
|
|
99
|
+
|
|
100
|
+
// Update .cursorrules with skills discovery block (if file exists)
|
|
101
|
+
const cursorRulesPath = path.join(process.cwd(), '.cursorrules');
|
|
102
|
+
try {
|
|
103
|
+
let rulesContent = await fs.readFile(cursorRulesPath, 'utf-8');
|
|
104
|
+
const skillsBlock = generateSkillsDiscoveryBlock(skills);
|
|
105
|
+
|
|
106
|
+
// Replace existing skills block or insert after PROJECT CONTEXT
|
|
107
|
+
if (rulesContent.includes('<available_skills>')) {
|
|
108
|
+
rulesContent = rulesContent.replace(
|
|
109
|
+
/<available_skills>[\s\S]*?<\/available_skills>/,
|
|
110
|
+
skillsBlock
|
|
111
|
+
);
|
|
112
|
+
} else if (rulesContent.includes('## 🧠 PROJECT CONTEXT')) {
|
|
113
|
+
// Insert after PROJECT CONTEXT section
|
|
114
|
+
const insertPoint = rulesContent.indexOf('---', rulesContent.indexOf('## 🧠 PROJECT CONTEXT'));
|
|
115
|
+
if (insertPoint !== -1) {
|
|
116
|
+
rulesContent = rulesContent.slice(0, insertPoint + 3) +
|
|
117
|
+
'\n\n' + skillsBlock + '\n' +
|
|
118
|
+
rulesContent.slice(insertPoint + 3);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await fs.writeFile(cursorRulesPath, rulesContent, 'utf-8');
|
|
123
|
+
console.log(chalk.dim(` Updated .cursorrules with skills discovery block`));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// .cursorrules doesn't exist or couldn't be updated
|
|
126
|
+
}
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
console.log(chalk.yellow(` ⚠ Skills provisioning skipped: ${e.message}`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 5. Process Execution Logs (MISSION REPORTING)
|
|
132
|
+
try {
|
|
133
|
+
const logPath = path.join(process.cwd(), '.rigstate', 'logs', 'last_execution.json');
|
|
134
|
+
try {
|
|
135
|
+
const logContent = await fs.readFile(logPath, 'utf-8');
|
|
136
|
+
const logData = JSON.parse(logContent);
|
|
137
|
+
|
|
138
|
+
if (logData.task_summary) {
|
|
139
|
+
await axios.post(`${apiUrl}/api/v1/execution-logs`, {
|
|
140
|
+
project_id: projectId,
|
|
141
|
+
...logData,
|
|
142
|
+
agent_role: process.env.RIGSTATE_MODE === 'SUPERVISOR' ? 'SUPERVISOR' : 'WORKER'
|
|
143
|
+
}, {
|
|
144
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await fs.unlink(logPath);
|
|
148
|
+
console.log(chalk.dim(`✔ Mission Report uploaded.`));
|
|
149
|
+
}
|
|
150
|
+
} catch (e: any) {
|
|
151
|
+
// Ignore ENOENT (file not found), log errors if API fails
|
|
152
|
+
if (e.code !== 'ENOENT') {
|
|
153
|
+
// console.log(chalk.yellow('Log upload skipped: ' + e.message));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (e) { }
|
|
157
|
+
|
|
158
|
+
// 6. User Feedback
|
|
159
|
+
spinner.succeed(chalk.green(`Synced ${roadmap.length} roadmap steps for project "${project}"`));
|
|
160
|
+
console.log(chalk.dim(`Local files updated: roadmap.json`));
|
|
161
|
+
|
|
162
|
+
const { runGuardianWatchdog } = await import('../utils/watchdog.js');
|
|
163
|
+
const settings = response.data.data.settings || {};
|
|
164
|
+
await runGuardianWatchdog(process.cwd(), settings, projectId);
|
|
165
|
+
|
|
166
|
+
// 8. Bridge Heartbeat & Pending Tasks
|
|
167
|
+
console.log(chalk.bold('\n📡 Agent Bridge Heartbeat...'));
|
|
168
|
+
try {
|
|
169
|
+
const bridgeResponse = await axios.get(`${apiUrl}/api/v1/agent/bridge`, {
|
|
170
|
+
params: { project_id: projectId },
|
|
171
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (bridgeResponse.data.success) {
|
|
175
|
+
const tasks = bridgeResponse.data.tasks;
|
|
176
|
+
const pending = tasks.filter((t: any) => t.status === 'PENDING');
|
|
177
|
+
const approved = tasks.filter((t: any) => t.status === 'APPROVED');
|
|
178
|
+
|
|
179
|
+
if (pending.length > 0 || approved.length > 0) {
|
|
180
|
+
console.log(chalk.yellow(`⚠ Bridge Alert: ${pending.length} pending, ${approved.length} approved tasks found.`));
|
|
181
|
+
console.log(chalk.dim('Run "rigstate fix" to process these tasks or ensure your IDE MCP server is active.'));
|
|
182
|
+
} else {
|
|
183
|
+
console.log(chalk.green('✔ Heartbeat healthy. No pending bridge tasks.'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Acknowledge Pings if any
|
|
187
|
+
const pings = pending.filter((t: any) => t.proposal?.startsWith('ping'));
|
|
188
|
+
for (const ping of pings) {
|
|
189
|
+
await axios.post(`${apiUrl}/api/v1/agent/bridge`, {
|
|
190
|
+
bridge_id: ping.id,
|
|
191
|
+
status: 'COMPLETED',
|
|
192
|
+
summary: 'Pong! CLI Sync Heartbeat confirmed.'
|
|
193
|
+
}, {
|
|
194
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
195
|
+
});
|
|
196
|
+
console.log(chalk.cyan(`🏓 Pong! Acknowledged heartbeat signal [${ping.id}]`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (e: any) {
|
|
200
|
+
console.log(chalk.yellow(`⚠ Could not verify Bridge status: ${e.message}`));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (options.project) {
|
|
204
|
+
console.log(chalk.blue(`Project context saved. Future commands will use this project.`));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 9. Migration Guard (The Firewall)
|
|
208
|
+
try {
|
|
209
|
+
const migrationDir = path.join(process.cwd(), 'supabase', 'migrations');
|
|
210
|
+
const files = await fs.readdir(migrationDir);
|
|
211
|
+
const sqlFiles = files.filter(f => f.endsWith('.sql')).sort();
|
|
212
|
+
|
|
213
|
+
if (sqlFiles.length > 0) {
|
|
214
|
+
const latestMigration = sqlFiles[sqlFiles.length - 1];
|
|
215
|
+
console.log(chalk.dim(`\n🛡 Migration Guard:`));
|
|
216
|
+
console.log(chalk.dim(` Latest Local: ${latestMigration}`));
|
|
217
|
+
console.log(chalk.yellow(` ⚠ Ensure DB schema matches this version. CLI cannot verify Remote RLS policies directly.`));
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// No migrations folder, or error reading - ignore
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 10. Sovereign Foundation (Vault Sync)
|
|
224
|
+
try {
|
|
225
|
+
const vaultResponse = await axios.post(`${apiUrl}/api/v1/vault/sync`,
|
|
226
|
+
{ project_id: projectId },
|
|
227
|
+
{ headers: { Authorization: `Bearer ${apiKey}` } }
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (vaultResponse.data.success) {
|
|
231
|
+
const vaultContent: string = vaultResponse.data.data.content || '';
|
|
232
|
+
const localEnvPath = path.join(process.cwd(), '.env.local');
|
|
233
|
+
let localContent = '';
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
localContent = await fs.readFile(localEnvPath, 'utf-8');
|
|
237
|
+
} catch (e) { /* File doesn't exist */ }
|
|
238
|
+
|
|
239
|
+
// Normalize for comparison (trim, ignore comments?) - Simple trim for now
|
|
240
|
+
if (vaultContent.trim() !== localContent.trim()) {
|
|
241
|
+
console.log(chalk.bold('\n🔐 Sovereign Foundation (Vault):'));
|
|
242
|
+
console.log(chalk.yellow(' Status: Drift Detected / Update Available'));
|
|
243
|
+
|
|
244
|
+
const { syncVault } = await import('inquirer').then(m => m.default.prompt([{
|
|
245
|
+
type: 'confirm',
|
|
246
|
+
name: 'syncVault',
|
|
247
|
+
message: 'Synchronize local .env.local with Vault secrets?',
|
|
248
|
+
default: false
|
|
249
|
+
}]));
|
|
250
|
+
|
|
251
|
+
if (syncVault) {
|
|
252
|
+
await fs.writeFile(localEnvPath, vaultContent, 'utf-8');
|
|
253
|
+
console.log(chalk.green(' ✅ .env.local synchronized with Vault.'));
|
|
254
|
+
} else {
|
|
255
|
+
console.log(chalk.dim(' Skipped vault sync.'));
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
console.log(chalk.dim('\n🔐 Sovereign Foundation: Synced.'));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (e: any) {
|
|
262
|
+
// Fail silently or warn if vault access denied (expected for some users)
|
|
263
|
+
// console.log(chalk.dim(` (Vault check skipped: ${e.message})`));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 11. System Integrity Checks (The Firewall)
|
|
267
|
+
console.log(chalk.dim('\n🛡️ System Integrity Check...'));
|
|
268
|
+
await checkSystemIntegrity(apiUrl, apiKey, projectId);
|
|
269
|
+
|
|
270
|
+
} catch (error: any) {
|
|
271
|
+
if (axios.isAxiosError(error)) {
|
|
272
|
+
const message = error.response?.data?.error || error.message;
|
|
273
|
+
spinner.fail(chalk.red(`Sync failed: ${message}`));
|
|
274
|
+
} else {
|
|
275
|
+
spinner.fail(chalk.red('Sync failed: ' + (error.message || 'Unknown error')));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return sync;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* System Integrity Checks
|
|
285
|
+
* Verifies Migration Sync and RLS Status via API
|
|
286
|
+
*/
|
|
287
|
+
async function checkSystemIntegrity(apiUrl: string, apiKey: string, projectId: string) {
|
|
288
|
+
try {
|
|
289
|
+
// Call System Integrity API
|
|
290
|
+
const response = await axios.get(`${apiUrl}/api/v1/system/integrity`, {
|
|
291
|
+
params: { project_id: projectId },
|
|
292
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (response.data.success) {
|
|
296
|
+
const { migrations, rls, guardian_violations } = response.data.data;
|
|
297
|
+
|
|
298
|
+
// Migration Status
|
|
299
|
+
if (migrations) {
|
|
300
|
+
if (migrations.in_sync) {
|
|
301
|
+
console.log(chalk.green(` ✅ Migrations synced (${migrations.count} versions)`));
|
|
302
|
+
} else {
|
|
303
|
+
console.log(chalk.red(` 🛑 CRITICAL: DB Schema out of sync! ${migrations.missing?.length || 0} migrations not applied.`));
|
|
304
|
+
if (migrations.missing?.length > 0) {
|
|
305
|
+
console.log(chalk.dim(` Missing: ${migrations.missing.slice(0, 3).join(', ')}${migrations.missing.length > 3 ? '...' : ''}`));
|
|
306
|
+
}
|
|
307
|
+
console.log(chalk.yellow(` Run 'supabase db push' or apply migrations immediately.`));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// RLS Status
|
|
312
|
+
if (rls) {
|
|
313
|
+
if (rls.all_secured) {
|
|
314
|
+
console.log(chalk.green(` ✅ RLS Audit Passed (${rls.table_count} tables secured)`));
|
|
315
|
+
} else {
|
|
316
|
+
console.log(chalk.red(` 🛑 CRITICAL: Security Vulnerability! ${rls.unsecured?.length || 0} tables have RLS disabled.`));
|
|
317
|
+
rls.unsecured?.forEach((table: string) => {
|
|
318
|
+
console.log(chalk.red(` - ${table}`));
|
|
319
|
+
});
|
|
320
|
+
console.log(chalk.yellow(' Enable RLS immediately: ALTER TABLE "table" ENABLE ROW LEVEL SECURITY;'));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Guardian Violations
|
|
325
|
+
if (guardian_violations) {
|
|
326
|
+
if (guardian_violations.count === 0) {
|
|
327
|
+
console.log(chalk.green(' ✅ Guardian: No active violations'));
|
|
328
|
+
} else {
|
|
329
|
+
console.log(chalk.yellow(` ⚠️ Guardian: ${guardian_violations.count} active violations`));
|
|
330
|
+
console.log(chalk.dim(' Run "rigstate check" for details.'));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch (e: any) {
|
|
335
|
+
// API might not have this endpoint yet - fail silently
|
|
336
|
+
console.log(chalk.dim(' (System integrity check skipped - API endpoint not available)'));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chokidar from 'chokidar';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { getApiKey, getProjectId, getApiUrl } from '../utils/config.js';
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
|
|
11
|
+
interface VerificationCriteria {
|
|
12
|
+
type: 'file_exists' | 'file_content' | 'content_match';
|
|
13
|
+
path: string;
|
|
14
|
+
pattern?: string;
|
|
15
|
+
match?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createWatchCommand() {
|
|
19
|
+
const watch = new Command('watch');
|
|
20
|
+
|
|
21
|
+
watch
|
|
22
|
+
.description('Watch for changes and auto-verify roadmap tasks')
|
|
23
|
+
.option('--no-auto-commit', 'Disable auto-commit on verification')
|
|
24
|
+
.option('--no-auto-push', 'Disable auto-push after commit')
|
|
25
|
+
.option('--run-tests', 'Run tests before committing')
|
|
26
|
+
.option('--test-command <cmd>', 'Custom test command (default: npm test)')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
console.log(chalk.bold.blue('🔭 Rigstate Watch Mode'));
|
|
29
|
+
console.log(chalk.dim('Monitoring for task completion...'));
|
|
30
|
+
console.log('');
|
|
31
|
+
|
|
32
|
+
// Get config
|
|
33
|
+
let apiKey: string;
|
|
34
|
+
let projectId: string | undefined;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
apiKey = getApiKey();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.log(chalk.red('Not authenticated. Run "rigstate login" first.'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
projectId = getProjectId();
|
|
44
|
+
if (!projectId) {
|
|
45
|
+
// Try to read from local manifest
|
|
46
|
+
try {
|
|
47
|
+
const manifestPath = path.join(process.cwd(), '.rigstate');
|
|
48
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
49
|
+
const manifest = JSON.parse(content);
|
|
50
|
+
projectId = manifest.project_id;
|
|
51
|
+
} catch (e) { }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!projectId) {
|
|
55
|
+
console.log(chalk.red('No project context. Run "rigstate link" or "rigstate sync --project <id>" first.'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const apiUrl = getApiUrl();
|
|
60
|
+
|
|
61
|
+
// Settings
|
|
62
|
+
const config = {
|
|
63
|
+
autoCommit: options.autoCommit !== false,
|
|
64
|
+
autoPush: options.autoPush !== false,
|
|
65
|
+
runTests: options.runTests || false,
|
|
66
|
+
testCommand: options.testCommand || 'npm test'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
console.log(chalk.dim(`Auto-commit: ${config.autoCommit ? 'ON' : 'OFF'}`));
|
|
70
|
+
console.log(chalk.dim(`Auto-push: ${config.autoPush ? 'ON' : 'OFF'}`));
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
// Fetch active task
|
|
74
|
+
const fetchActiveTask = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const response = await axios.get(`${apiUrl}/api/v1/roadmap`, {
|
|
77
|
+
params: { project_id: projectId },
|
|
78
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!response.data.success) return null;
|
|
82
|
+
|
|
83
|
+
const roadmap = response.data.data.roadmap || [];
|
|
84
|
+
|
|
85
|
+
// Priority: IN_PROGRESS > ACTIVE > LOCKED
|
|
86
|
+
const statusPriority: Record<string, number> = {
|
|
87
|
+
'IN_PROGRESS': 0,
|
|
88
|
+
'ACTIVE': 1,
|
|
89
|
+
'LOCKED': 2
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const activeTasks = roadmap
|
|
93
|
+
.filter((t: any) => ['IN_PROGRESS', 'ACTIVE', 'LOCKED'].includes(t.status))
|
|
94
|
+
.sort((a: any, b: any) => {
|
|
95
|
+
const pA = statusPriority[a.status] ?? 99;
|
|
96
|
+
const pB = statusPriority[b.status] ?? 99;
|
|
97
|
+
if (pA !== pB) return pA - pB;
|
|
98
|
+
return (a.step_number || 0) - (b.step_number || 0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return activeTasks[0] || null;
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Check verification criteria
|
|
108
|
+
const checkCriteria = async (criteria: VerificationCriteria): Promise<boolean> => {
|
|
109
|
+
try {
|
|
110
|
+
const fullPath = path.resolve(process.cwd(), criteria.path);
|
|
111
|
+
|
|
112
|
+
switch (criteria.type) {
|
|
113
|
+
case 'file_exists':
|
|
114
|
+
await fs.access(fullPath);
|
|
115
|
+
return true;
|
|
116
|
+
|
|
117
|
+
case 'file_content':
|
|
118
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
119
|
+
return content.length > 0;
|
|
120
|
+
|
|
121
|
+
case 'content_match':
|
|
122
|
+
if (!criteria.match) return false;
|
|
123
|
+
const fileContent = await fs.readFile(fullPath, 'utf-8');
|
|
124
|
+
return fileContent.includes(criteria.match);
|
|
125
|
+
|
|
126
|
+
default:
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Complete task
|
|
135
|
+
const completeTask = async (taskId: string, task: any) => {
|
|
136
|
+
const spinner = ora('Completing task...').start();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// Run tests if enabled
|
|
140
|
+
if (config.runTests) {
|
|
141
|
+
spinner.text = 'Running tests...';
|
|
142
|
+
try {
|
|
143
|
+
execSync(config.testCommand, { stdio: 'pipe' });
|
|
144
|
+
spinner.text = 'Tests passed!';
|
|
145
|
+
} catch (e) {
|
|
146
|
+
spinner.fail('Tests failed. Task not completed.');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Update status via API
|
|
152
|
+
await axios.post(`${apiUrl}/api/v1/roadmap/update-status`, {
|
|
153
|
+
project_id: projectId,
|
|
154
|
+
chunk_id: taskId,
|
|
155
|
+
status: 'COMPLETED'
|
|
156
|
+
}, {
|
|
157
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
spinner.succeed(chalk.green(`✅ Task #${task.step_number} completed: ${task.title}`));
|
|
161
|
+
|
|
162
|
+
// Auto-commit
|
|
163
|
+
if (config.autoCommit) {
|
|
164
|
+
spinner.start('Committing changes...');
|
|
165
|
+
try {
|
|
166
|
+
execSync('git add -A', { stdio: 'pipe' });
|
|
167
|
+
const commitMsg = `feat: Complete task #${task.step_number} - ${task.title}`;
|
|
168
|
+
execSync(`git commit -m "${commitMsg}"`, { stdio: 'pipe' });
|
|
169
|
+
spinner.succeed('Changes committed');
|
|
170
|
+
|
|
171
|
+
// Auto-push
|
|
172
|
+
if (config.autoPush) {
|
|
173
|
+
spinner.start('Pushing to remote...');
|
|
174
|
+
try {
|
|
175
|
+
execSync('git push', { stdio: 'pipe' });
|
|
176
|
+
spinner.succeed('Pushed to remote');
|
|
177
|
+
} catch (e) {
|
|
178
|
+
spinner.warn('Push failed (no remote or conflict)');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (e: any) {
|
|
182
|
+
spinner.warn('Nothing to commit or commit failed');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(chalk.blue('Watching for next task...'));
|
|
188
|
+
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
spinner.fail(`Failed to complete task: ${e.message}`);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Main watch loop
|
|
195
|
+
let currentTask: any = null;
|
|
196
|
+
let isProcessing = false;
|
|
197
|
+
|
|
198
|
+
const processActiveTask = async () => {
|
|
199
|
+
if (isProcessing) return;
|
|
200
|
+
isProcessing = true;
|
|
201
|
+
|
|
202
|
+
const task = await fetchActiveTask();
|
|
203
|
+
|
|
204
|
+
if (!task) {
|
|
205
|
+
if (currentTask) {
|
|
206
|
+
console.log(chalk.green('🎉 All tasks completed! Watching for new tasks...'));
|
|
207
|
+
currentTask = null;
|
|
208
|
+
}
|
|
209
|
+
isProcessing = false;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!currentTask || currentTask.id !== task.id) {
|
|
214
|
+
currentTask = task;
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(chalk.bold.yellow(`📌 Active Task #${task.step_number}: ${task.title}`));
|
|
217
|
+
console.log(chalk.dim(`Status: ${task.status}`));
|
|
218
|
+
|
|
219
|
+
if (task.verification_criteria) {
|
|
220
|
+
console.log(chalk.dim('Verification: Auto-checking criteria...'));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check verification criteria if present
|
|
225
|
+
if (task.verification_criteria && Array.isArray(task.verification_criteria)) {
|
|
226
|
+
let allPassed = true;
|
|
227
|
+
for (const criteria of task.verification_criteria) {
|
|
228
|
+
const passed = await checkCriteria(criteria);
|
|
229
|
+
if (!passed) {
|
|
230
|
+
allPassed = false;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (allPassed) {
|
|
236
|
+
console.log(chalk.green('✓ All verification criteria passed!'));
|
|
237
|
+
await completeTask(task.id, task);
|
|
238
|
+
currentTask = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
isProcessing = false;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Initial check
|
|
246
|
+
await processActiveTask();
|
|
247
|
+
|
|
248
|
+
// Set up file watcher
|
|
249
|
+
const watcher = chokidar.watch('.', {
|
|
250
|
+
ignored: [
|
|
251
|
+
/(^|[\/\\])\../, // dotfiles
|
|
252
|
+
'**/node_modules/**',
|
|
253
|
+
'**/.git/**',
|
|
254
|
+
'**/.next/**',
|
|
255
|
+
'**/dist/**'
|
|
256
|
+
],
|
|
257
|
+
persistent: true,
|
|
258
|
+
ignoreInitial: true
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
watcher.on('all', async (event, filePath) => {
|
|
262
|
+
if (['add', 'change', 'unlink'].includes(event)) {
|
|
263
|
+
// Debounce - wait a bit for multiple rapid changes
|
|
264
|
+
setTimeout(() => processActiveTask(), 500);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
console.log(chalk.dim('Watching for file changes... (Ctrl+C to exit)'));
|
|
269
|
+
|
|
270
|
+
// Periodic check every 30 seconds
|
|
271
|
+
setInterval(() => processActiveTask(), 30000);
|
|
272
|
+
|
|
273
|
+
// Keep process alive
|
|
274
|
+
process.on('SIGINT', () => {
|
|
275
|
+
console.log('');
|
|
276
|
+
console.log(chalk.dim('Watch mode stopped.'));
|
|
277
|
+
watcher.close();
|
|
278
|
+
process.exit(0);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return watch;
|
|
283
|
+
}
|