@rigstate/cli 0.7.25 → 0.7.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigstate/cli",
3
- "version": "0.7.25",
3
+ "version": "0.7.29",
4
4
  "description": "Rigstate CLI - Code audit, sync and supervision tool",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,9 +9,9 @@ import { getApiUrl, getApiKey } from '../utils/config.js';
9
9
  export function createLinkCommand() {
10
10
  return new Command('link')
11
11
  .description('Link current directory to a Rigstate project')
12
- .argument('<projectId>', 'Project ID to link')
12
+ .argument('[projectId]', 'Project ID to link')
13
13
  .action(async (projectId) => {
14
- // Check Global Override (Rigstate v2.7)
14
+ // Check Global Override first
15
15
  try {
16
16
  const globalPath = path.join(os.homedir(), '.rigstate', 'config.json');
17
17
  const globalData = await fs.readFile(globalPath, 'utf-8').catch(() => null);
@@ -20,14 +20,60 @@ export function createLinkCommand() {
20
20
  const cwd = process.cwd();
21
21
  if (config.overrides && config.overrides[cwd]) {
22
22
  const overrideId = config.overrides[cwd];
23
- if (overrideId !== projectId) {
24
- console.warn(chalk.yellow(`Global override detected. Enforcing project ID: ${overrideId}`));
23
+ console.warn(chalk.yellow(`Global override detected. Enforcing project ID: ${overrideId}`));
24
+ if (!projectId) projectId = overrideId;
25
+ else if (projectId !== overrideId) {
26
+ console.warn(chalk.red(`Ignoring provided ID ${projectId}. Using override.`));
25
27
  projectId = overrideId;
26
28
  }
27
29
  }
28
30
  }
29
31
  } catch (e) { }
30
32
 
33
+ // Interactive Selection if no ID
34
+ if (!projectId) {
35
+ try {
36
+ const inquirer = (await import('inquirer')).default;
37
+ const { getApiKey: _getApiKey, getApiUrl: _getApiUrl } = await import('../utils/config.js');
38
+ const apiKey = getApiKey();
39
+ const apiUrl = getApiUrl();
40
+
41
+ if (!apiKey) {
42
+ console.error(chalk.red('Not authenticated. Please run "rigstate login" or provide a Project ID.'));
43
+ process.exit(1);
44
+ }
45
+
46
+ console.log(chalk.dim('Fetching your projects...'));
47
+ const axios = (await import('axios')).default;
48
+ const response = await axios.get(`${apiUrl}/api/v1/projects`, {
49
+ headers: { Authorization: `Bearer ${apiKey}` }
50
+ });
51
+
52
+ if (!response.data.success || !response.data.data.projects?.length) {
53
+ console.error(chalk.yellow('No projects found. Create one at https://app.rigstate.com'));
54
+ process.exit(1);
55
+ }
56
+
57
+ const choices = response.data.data.projects.map((p: any) => ({
58
+ name: `${p.name} (${p.id})`,
59
+ value: p.id
60
+ }));
61
+
62
+ const answer = await inquirer.prompt([{
63
+ type: 'list',
64
+ name: 'id',
65
+ message: 'Select project to link:',
66
+ choices
67
+ }]);
68
+ projectId = answer.id;
69
+
70
+ } catch (e: any) {
71
+ console.error(chalk.red(`Failed to fetch projects: ${e.message}`));
72
+ console.error('Please provide project ID manually: rigstate link <id>');
73
+ process.exit(1);
74
+ }
75
+ }
76
+
31
77
  const manifestPath = path.join(process.cwd(), '.rigstate');
32
78
 
33
79
  const content: any = {
@@ -42,6 +88,9 @@ export function createLinkCommand() {
42
88
  }
43
89
 
44
90
  try {
91
+ // Ensure .rigstate dir exists
92
+ await fs.mkdir(path.dirname(manifestPath), { recursive: true });
93
+
45
94
  await fs.writeFile(manifestPath, JSON.stringify(content, null, 2), 'utf-8');
46
95
  console.log(chalk.green(`✔ Linked to project ID: ${projectId}`));
47
96
  console.log(chalk.dim(`Created local context manifest at .rigstate`));
@@ -52,7 +101,7 @@ export function createLinkCommand() {
52
101
  console.log('');
53
102
 
54
103
  const { getApiKey: _getApiKey, getApiUrl: _getApiUrl } = await import('../utils/config.js');
55
- const apiKey = getApiKey(); // Might throw if not logged in
104
+ const apiKey = getApiKey();
56
105
  const apiUrl = getApiUrl();
57
106
 
58
107
  if (apiKey) {
@@ -67,19 +116,14 @@ export function createLinkCommand() {
67
116
  await syncProjectRules(projectId, apiKey, apiUrl);
68
117
 
69
118
  // 3. Git Hooks (Auto-Vaccine)
70
- console.log(chalk.blue('🛡️ Injecting Guardian hooks...'));
71
- const { createHooksCommand } = await import('./hooks.js');
72
- // We can't easily invoke the command action directly without refactoring,
73
- // but we can reuse the logic. For now, let's replicate the simple install logic
74
- // or better yet, make a shared utility.
75
- // Actually, the simplest way for v0.7.25 is to call the install logic directly if possible.
76
- // Let's use the helper function at the bottom of this file which I see exists.
119
+ console.log(chalk.blue('🛡️ Injecting Guardian hooks & Safety nets...'));
77
120
  await installHooks(process.cwd());
121
+ await hardenGitIgnore(process.cwd());
78
122
 
79
123
  console.log('');
80
124
  console.log(chalk.bold.green('🚀 Link Complete! Your environment is ready.'));
81
125
 
82
- // 4. Tactical Suggestion (The First Move)
126
+ // 4. Tactical Suggestion
83
127
  const { suggestNextMove } = await import('./suggest.js');
84
128
  await suggestNextMove(projectId, apiKey, apiUrl);
85
129
  } else {
@@ -88,7 +132,7 @@ export function createLinkCommand() {
88
132
  }
89
133
 
90
134
  } catch (error: any) {
91
- if (error.message.includes('Not authenticated')) {
135
+ if (error.message?.includes('Not authenticated')) {
92
136
  console.warn(chalk.yellow('⚠️ Not authenticated. Run "rigstate login" to enable automation features.'));
93
137
  } else {
94
138
  console.error(chalk.red(`Failed to link project: ${error.message}`));
@@ -97,6 +141,47 @@ export function createLinkCommand() {
97
141
  });
98
142
  }
99
143
 
144
+ async function hardenGitIgnore(cwd: string) {
145
+ const fs = await import('fs/promises');
146
+ const path = await import('path');
147
+ const ignorePath = path.join(cwd, '.gitignore');
148
+
149
+ const REQUIRED_IGNORES = [
150
+ '# Rigstate - Runtime Artifacts (Do not commit)',
151
+ '.rigstate/ACTIVE_VIOLATIONS.md',
152
+ '.rigstate/CURRENT_CONTEXT.md',
153
+ '.rigstate/daemon.pid',
154
+ '.rigstate/daemon.state.json',
155
+ '.rigstate/*.log',
156
+ '.rigstate/*.bak',
157
+ '# Keep identity tracked',
158
+ '!.rigstate/identity.json'
159
+ ];
160
+
161
+ try {
162
+ let content = '';
163
+ try {
164
+ content = await fs.readFile(ignorePath, 'utf-8');
165
+ } catch {
166
+ // No .gitignore, start fresh
167
+ content = '';
168
+ }
169
+
170
+ const missing = REQUIRED_IGNORES.filter(line => !content.includes(line) && !line.startsWith('#'));
171
+
172
+ if (missing.length > 0) {
173
+ console.log(chalk.dim(' Configuring .gitignore for Rigstate safety...'));
174
+ const toAppend = '\n\n' + REQUIRED_IGNORES.join('\n') + '\n';
175
+ await fs.writeFile(ignorePath, content + toAppend, 'utf-8');
176
+ console.log(chalk.green(' ✔ .gitignore updated (Artifacts protected)'));
177
+ } else {
178
+ console.log(chalk.green(' ✔ .gitignore already hardened'));
179
+ }
180
+ } catch (e: any) {
181
+ console.warn(chalk.yellow(` Could not update .gitignore: ${e.message}`));
182
+ }
183
+ }
184
+
100
185
  async function installHooks(cwd: string) {
101
186
  const fs = await import('fs/promises');
102
187
  const path = await import('path');
@@ -0,0 +1,165 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import axios from 'axios';
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import inquirer from 'inquirer';
8
+ import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
9
+
10
+ export function createPlanCommand(): Command {
11
+ const plan = new Command('plan');
12
+
13
+ plan
14
+ .description('Generate an implementation plan for a roadmap task')
15
+ .argument('[taskId]', 'Task ID (e.g. T-10) or UUID')
16
+ .action(async (taskId) => {
17
+ await executePlan(taskId);
18
+ });
19
+
20
+ return plan;
21
+ }
22
+
23
+ export async function executePlan(taskId?: string) {
24
+ const spinner = ora('Initializing Planning Mode...').start();
25
+
26
+ try {
27
+ const { projectId, apiKey, apiUrl } = getContext();
28
+
29
+ // 1. Resolve Task ID if missing or short
30
+ let realId = taskId;
31
+ let taskTitle = '';
32
+ let taskDescription = '';
33
+
34
+ if (!taskId) {
35
+ spinner.text = 'Fetching actionable tasks...';
36
+ // Interactive selection
37
+ const response = await axios.get(
38
+ `${apiUrl}/api/v1/roadmap?project_id=${projectId}`,
39
+ { headers: { 'Authorization': `Bearer ${apiKey}` } }
40
+ );
41
+
42
+ if (!response.data.success) throw new Error('Failed to fetch roadmap');
43
+ const tasks: any[] = response.data.data.roadmap || [];
44
+
45
+ const choices = tasks
46
+ .filter(t => ['ACTIVE', 'IN_PROGRESS', 'PENDING'].includes(t.status))
47
+ .map(t => ({
48
+ name: `T-${t.step_number}: ${t.title}`,
49
+ value: t
50
+ }));
51
+
52
+ if (choices.length === 0) {
53
+ spinner.fail('No actionable tasks found in roadmap.');
54
+ return;
55
+ }
56
+
57
+ spinner.stop();
58
+ const answer = await inquirer.prompt([{
59
+ type: 'list',
60
+ name: 'task',
61
+ message: 'Select a task to plan:',
62
+ choices
63
+ }]);
64
+
65
+ realId = answer.task.id;
66
+ taskTitle = answer.task.title;
67
+ taskDescription = answer.task.description;
68
+ taskId = `T-${answer.task.step_number}`;
69
+ } else {
70
+ // Fetch specific task details
71
+ spinner.text = `Fetching details for ${taskId}...`;
72
+ const response = await axios.get(
73
+ `${apiUrl}/api/v1/roadmap?project_id=${projectId}`,
74
+ { headers: { 'Authorization': `Bearer ${apiKey}` } }
75
+ );
76
+ const task = response.data.data.roadmap.find((t: any) =>
77
+ t.id === taskId ||
78
+ `T-${t.step_number}` === taskId ||
79
+ t.step_number.toString() === taskId
80
+ );
81
+
82
+ if (!task) throw new Error(`Task ${taskId} not found.`);
83
+ realId = task.id;
84
+ taskTitle = task.title;
85
+ taskDescription = task.description;
86
+ }
87
+
88
+ // 2. Generate Context File
89
+ spinner.start('Generating Context for Frank...');
90
+ const contextPath = path.join(process.cwd(), '.rigstate', 'CURRENT_CONTEXT.md');
91
+ const contextContent = `
92
+ # 🎯 Active Mission: ${taskTitle}
93
+ **ID:** ${taskId}
94
+
95
+ ## 📝 Description
96
+ ${taskDescription}
97
+
98
+ ## 🛡️ Architectural Constraints
99
+ - Follow strictly the rules in .cursor/rules/
100
+ - Ensure zero violations in ACTIVE_VIOLATIONS.md
101
+ - Update IMPLEMENTATION_PLAN.md before writing code.
102
+
103
+ *Generated by Rigstate CLI at ${new Date().toLocaleString()}*
104
+ `;
105
+ await fs.mkdir(path.dirname(contextPath), { recursive: true });
106
+ await fs.writeFile(contextPath, contextContent.trim());
107
+
108
+ // 3. Generate Plan Template
109
+ const planPath = path.join(process.cwd(), 'IMPLEMENTATION_PLAN.md');
110
+ const planExists = await fs.stat(planPath).then(() => true).catch(() => false);
111
+
112
+ if (!planExists) {
113
+ const planTemplate = `
114
+ # 📋 Implementation Plan: ${taskTitle}
115
+
116
+ ## 1. 🔍 Analysis
117
+ - [ ] Understand the requirements in .rigstate/CURRENT_CONTEXT.md
118
+ - [ ] Check for existing architectural patterns
119
+
120
+ ## 2. 🏗️ Proposed Changes
121
+ [Frank: List the files you intend to modify and the nature of the changes]
122
+
123
+ ## 3. ✅ Verification
124
+ - [ ] Run tests
125
+ - [ ] Verification Step 1...
126
+
127
+ ## 4. 🚀 Execution
128
+ [Frank: Log your progress here]
129
+ `;
130
+ await fs.writeFile(planPath, planTemplate.trim());
131
+ spinner.succeed(chalk.green('Created new IMPLEMENTATION_PLAN.md'));
132
+ } else {
133
+ spinner.info(chalk.yellow('IMPLEMENTATION_PLAN.md already exists. Preserving it.'));
134
+ // Optionally append or just leave it
135
+ }
136
+
137
+ // 4. Update Status (Optional - maybe set to IN_PROGRESS?)
138
+ // For now, let's keep it pure planning.
139
+
140
+ console.log('');
141
+ console.log(chalk.bold.blue('🚀 Planning Mode Activated'));
142
+ console.log(chalk.dim('────────────────────────────────────────'));
143
+ console.log(`1. Context loaded into: ${chalk.bold('.rigstate/CURRENT_CONTEXT.md')}`);
144
+ console.log(`2. Plan template ready: ${chalk.bold('IMPLEMENTATION_PLAN.md')}`);
145
+ console.log('');
146
+ console.log(chalk.yellow('👉 NEXT STEP:'));
147
+ console.log(` Open ${chalk.bold('IMPLEMENTATION_PLAN.md')} in your IDE.`);
148
+ console.log(` Tell Frank: ${chalk.italic('"Read the context and draft the plan."')}`);
149
+ console.log('');
150
+
151
+ } catch (e: any) {
152
+ spinner.fail(chalk.red(`Planning failed: ${e.message}`));
153
+ }
154
+ }
155
+
156
+ function getContext() {
157
+ const apiKey = getApiKey();
158
+ const apiUrl = getApiUrl();
159
+ const projectId = getProjectId();
160
+
161
+ if (!projectId) {
162
+ throw new Error('Project ID missing. Run rigstate link.');
163
+ }
164
+ return { projectId, apiKey, apiUrl };
165
+ }
@@ -6,6 +6,7 @@ import inquirer from 'inquirer';
6
6
  import fs from 'fs/promises';
7
7
  import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
8
8
  import { suggestNextMove } from './suggest.js';
9
+ import { executePlan } from './plan.js';
9
10
 
10
11
  export function createWorkCommand(): Command {
11
12
  const work = new Command('work');
@@ -92,12 +93,17 @@ async function listInteractive() {
92
93
  name: 'action',
93
94
  message: 'Action:',
94
95
  choices: [
96
+ { name: 'Plan (Draft Blueprint - RECOMMENDED)', value: 'plan' },
95
97
  { name: 'Start (Set IN_PROGRESS)', value: 'start' },
96
98
  { name: 'Finish (Audit & Complete)', value: 'finish' },
97
99
  { name: 'Cancel', value: 'cancel' }
98
100
  ]
99
101
  }]);
100
102
 
103
+ if (action === 'plan') {
104
+ await executePlan(taskId);
105
+ // After planning, maybe ask to start? For now, just exit as plan command does sufficient logging.
106
+ }
101
107
  if (action === 'start') await setTaskStatus(taskId, 'IN_PROGRESS');
102
108
  if (action === 'finish') await finishTask(taskId);
103
109
 
@@ -160,6 +160,8 @@ export class GuardianDaemon extends EventEmitter {
160
160
  }
161
161
  }
162
162
 
163
+ private violationsMap = new Map<string, any[]>();
164
+
163
165
  private async runIntegrityCheck(filePath: string) {
164
166
  if (!this.guardianMonitor) return;
165
167
 
@@ -170,25 +172,37 @@ export class GuardianDaemon extends EventEmitter {
170
172
  if (result.violations.length > 0) {
171
173
  this.handleViolations(filePath, result.violations);
172
174
  } else {
173
- // Success - might need to clear previous violations for this file
174
- await this.updateViolationReport([]);
175
+ // Success - clear violations for this file
176
+ if (this.violationsMap.has(filePath)) {
177
+ this.violationsMap.delete(filePath);
178
+ this.updateViolationReport(); // Update the aggregate report
179
+ }
175
180
  }
176
181
  }
177
182
 
178
- private async updateViolationReport(violations: any[]) {
183
+ private async updateViolationReport(violations?: any[]) {
184
+ // If violations arg is passed (from handleViolations), update the map first
185
+ // But usually handleViolations calls this without args to just refresh
186
+
179
187
  const reportPath = path.join(process.cwd(), '.rigstate', 'ACTIVE_VIOLATIONS.md');
188
+ const allViolations = Array.from(this.violationsMap.entries());
189
+ const totalCount = allViolations.reduce((acc, [, v]) => acc + v.length, 0);
180
190
 
181
- // This is a simplified version. In a real build, we'd aggregate across all files.
182
- // For now, let's show the most recent or active ones.
183
- let content = `# 🛡️ Guardian Status: ${violations.length > 0 ? '⚠️ ATTENTION' : '✅ PASS'}\n\n`;
184
- content += `*Last check: ${new Date().toLocaleString()}*\n\n`;
191
+ let content = `# 🛡️ Guardian Status: ${totalCount > 0 ? '⚠️ ATTENTION' : '✅ PASS'}\n\n`;
192
+ content += `*Last check: ${new Date().toLocaleString()}*\n`;
193
+ content += `*Files with issues: ${allViolations.length}*\n\n`;
185
194
 
186
- if (violations.length === 0) {
195
+ if (totalCount === 0) {
187
196
  content += "All systems within architectural limits. Frank is satisfied. 🤫\n";
188
197
  } else {
189
198
  content += "### 🚨 Active Violations\n\n";
190
- for (const v of violations) {
191
- content += `- **[${v.severity.toUpperCase()}]**: ${v.message}\n`;
199
+ for (const [file, fileViolations] of allViolations) {
200
+ const relPath = path.relative(process.cwd(), file);
201
+ content += `#### 📄 ${relPath}\n`;
202
+ for (const v of fileViolations) {
203
+ content += `- **[${v.severity.toUpperCase()}]**: ${v.message}\n`;
204
+ }
205
+ content += '\n';
192
206
  }
193
207
  content += "\n---\n*Rigstate Daemon is watching. Fix violations to clear this report.*";
194
208
  }
@@ -202,7 +216,10 @@ export class GuardianDaemon extends EventEmitter {
202
216
  this.state.violationsFound += violations.length;
203
217
  this.emit('violation', { file: filePath, violations });
204
218
 
205
- this.updateViolationReport(violations); // Push to IDE dashboard
219
+ // Update state map
220
+ this.violationsMap.set(filePath, violations);
221
+
222
+ this.updateViolationReport(); // Push to IDE dashboard
206
223
 
207
224
  for (const v of violations) {
208
225
  const level = v.severity === 'critical' ? 'error' : v.severity === 'warning' ? 'warn' : 'info';
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ import { createIdeaCommand } from './commands/idea.js';
22
22
  import { createReleaseCommand } from './commands/release.js';
23
23
  import { createRoadmapCommand } from './commands/roadmap.js';
24
24
  import { createCouncilCommand } from './commands/council.js';
25
+ import { createPlanCommand } from './commands/plan.js';
25
26
  import { checkVersion } from './utils/version.js';
26
27
  import dotenv from 'dotenv';
27
28
 
@@ -60,7 +61,9 @@ program.addCommand(createOverrideCommand());
60
61
  program.addCommand(createIdeaCommand());
61
62
  program.addCommand(createReleaseCommand());
62
63
  program.addCommand(createRoadmapCommand());
64
+ program.addCommand(createRoadmapCommand());
63
65
  program.addCommand(createCouncilCommand());
66
+ program.addCommand(createPlanCommand());
64
67
 
65
68
  program.hook('preAction', async () => {
66
69
  await checkVersion();
@@ -1,8 +0,0 @@
1
- {
2
- "isRunning": true,
3
- "startedAt": "2026-01-26T13:11:14.337Z",
4
- "filesChecked": 4,
5
- "violationsFound": 83,
6
- "tasksProcessed": 0,
7
- "lastActivity": "2026-01-26T15:24:35.672Z"
8
- }