@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,172 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import axios from 'axios';
5
+ import inquirer from 'inquirer';
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
9
+
10
+ export function createWorkCommand(): Command {
11
+ return new Command('work')
12
+ .alias('start')
13
+ .description('Select and execute a Roadmap Task (fetches IDE Prompt)')
14
+ .argument('[taskId]', 'Optional Task ID (e.g., T-1021) to start immediately')
15
+ .option('--project <id>', 'Project ID')
16
+ .action(async (taskId: string | undefined, options: { project?: string }) => {
17
+ const spinner = ora();
18
+
19
+ try {
20
+ const apiKey = getApiKey();
21
+ const apiUrl = getApiUrl();
22
+ const projectId = options.project || getProjectId();
23
+
24
+ if (!projectId) {
25
+ console.log(chalk.red('❌ Project ID is required. Run `rigstate link` or pass --project <id>'));
26
+ process.exit(1);
27
+ }
28
+
29
+ if (!taskId) {
30
+ spinner.start('Fetching active roadmap tasks...');
31
+ }
32
+
33
+ // 1. Fetch Roadmap
34
+ const response = await axios.get(
35
+ `${apiUrl}/api/v1/roadmap?project_id=${projectId}`,
36
+ { headers: { 'Authorization': `Bearer ${apiKey}` }, timeout: 10000 }
37
+ );
38
+
39
+ if (!response.data.success) {
40
+ throw new Error(response.data.error || 'Failed to fetch roadmap');
41
+ }
42
+
43
+ const allTasks: any[] = response.data.data.roadmap || [];
44
+
45
+ // 2. Filter relevant tasks (ACTIVE or LOCKED or IN_PROGRESS if we had that status)
46
+ // We focus on ACTIVE (Started) and LOCKED (Next Up).
47
+ // Sort: ACTIVE first, then by step_number.
48
+ const actionableTasks = allTasks
49
+ .filter(t => ['ACTIVE', 'LOCKED'].includes(t.status))
50
+ .sort((a, b) => {
51
+ if (a.status === 'ACTIVE' && b.status !== 'ACTIVE') return -1;
52
+ if (b.status === 'ACTIVE' && a.status !== 'ACTIVE') return 1;
53
+ return a.step_number - b.step_number;
54
+ });
55
+
56
+ spinner.stop();
57
+
58
+ let selectedTask: any;
59
+
60
+ if (taskId) {
61
+ // Direct Selection via Arg
62
+ // taskId can be "T-1021" or just "1021" or the UUID
63
+ selectedTask = allTasks.find(t =>
64
+ t.id === taskId ||
65
+ `T-${t.step_number}` === taskId ||
66
+ t.step_number.toString() === taskId
67
+ );
68
+
69
+ if (!selectedTask) {
70
+ console.log(chalk.red(`❌ Task '${taskId}' not found in roadmap.`));
71
+ return;
72
+ }
73
+ } else {
74
+ // Interactive Selection
75
+ if (actionableTasks.length === 0) {
76
+ console.log(chalk.yellow('No active or locked tasks found. The Roadmap is clear! 🎉'));
77
+ return;
78
+ }
79
+
80
+ const choices = actionableTasks.map(t => {
81
+ const id = `T-${t.step_number}`;
82
+ const statusIcon = t.status === 'ACTIVE' ? '▶️' : '🔒';
83
+ const priority = t.priority === 'MVP' ? chalk.magenta('[MVP]') : chalk.blue(`[${t.priority}]`);
84
+
85
+ return {
86
+ name: `${statusIcon} ${chalk.bold(id)}: ${t.title} ${priority}`,
87
+ value: t,
88
+ short: `${id}: ${t.title}`
89
+ };
90
+ });
91
+
92
+ const answer = await inquirer.prompt([{
93
+ type: 'list',
94
+ name: 'task',
95
+ message: 'Which task are you working on?',
96
+ choices,
97
+ pageSize: 15
98
+ }]);
99
+
100
+ selectedTask = answer.task;
101
+ }
102
+
103
+ // 3. Display Task Context & Prompt
104
+ console.log('\n' + chalk.bold.underline(`🚀 WORK MODE: ${selectedTask.title}`));
105
+ console.log(chalk.dim(`ID: T-${selectedTask.step_number} | Status: ${selectedTask.status}`));
106
+
107
+ if (selectedTask.prompt_content) {
108
+ console.log(chalk.yellow.bold('\n📋 IDE EXECUTION SIGNAL (Prompt):'));
109
+ console.log(chalk.gray('--------------------------------------------------'));
110
+ console.log(selectedTask.prompt_content);
111
+ console.log(chalk.gray('--------------------------------------------------'));
112
+
113
+ // Copy to clipboard option?
114
+ // For now, let's offer to save it to a file which is more robust for Agents.
115
+
116
+ const { action } = await inquirer.prompt([{
117
+ type: 'list',
118
+ name: 'action',
119
+ message: 'What do you want to do?',
120
+ choices: [
121
+ { name: 'Copy Prompt (Print clean)', value: 'print' },
122
+ { name: 'Create .cursorrules (Agent Context)', value: 'cursorrules' },
123
+ { name: 'Mark as ACTIVE (if LOCKED)', value: 'activate' },
124
+ { name: 'Mark as COMPLETED', value: 'complete' },
125
+ { name: 'Cancel', value: 'cancel' }
126
+ ]
127
+ }]);
128
+
129
+ if (action === 'cursorrules') {
130
+ // Create a temporary .cursorrules or overwrite existing
131
+ // Warning: this ignores strict existing cursorrules, maybe append?
132
+ // For safety, let's create a .rigstate-context.md file
133
+ await fs.writeFile('.rigstate-prompt.md', selectedTask.prompt_content);
134
+ console.log(chalk.green(`✅ Prompt saved to ${chalk.bold('.rigstate-prompt.md')}`));
135
+ console.log(chalk.dim('You can now reference this file in your IDE chat (@.rigstate-prompt.md)'));
136
+ } else if (action === 'print') {
137
+ console.log('\n' + selectedTask.prompt_content + '\n');
138
+ } else if (action === 'activate' && selectedTask.status !== 'ACTIVE') {
139
+ try {
140
+ await axios.post(
141
+ `${apiUrl}/api/v1/roadmap/update-status`,
142
+ { step_id: selectedTask.id, status: 'ACTIVE', project_id: projectId },
143
+ { headers: { 'Authorization': `Bearer ${apiKey}` } }
144
+ );
145
+ console.log(chalk.green(`✅ Task marked as ACTIVE.`));
146
+ } catch (e: any) {
147
+ console.error(chalk.red(`Failed to update status: ${e.message}`));
148
+ }
149
+ } else if (action === 'complete') {
150
+ try {
151
+ await axios.post(
152
+ `${apiUrl}/api/v1/roadmap/update-status`,
153
+ { step_id: selectedTask.id, status: 'COMPLETED', project_id: projectId },
154
+ { headers: { 'Authorization': `Bearer ${apiKey}` } }
155
+ );
156
+ console.log(chalk.green(`✅ Task marked as COMPLETED. Great job!`));
157
+ } catch (e: any) {
158
+ console.error(chalk.red(`Failed to update status: ${e.message}`));
159
+ }
160
+ }
161
+
162
+ } else {
163
+ console.log(chalk.yellow('\n⚠️ No specific IDE Prompt found for this task (Legacy Task?).'));
164
+ console.log(chalk.dim('Objective: ' + (selectedTask.summary || selectedTask.description || 'Check web UI for details.')));
165
+ }
166
+
167
+ } catch (error: any) {
168
+ spinner.stop();
169
+ console.error(chalk.red(`\nCommand failed: ${error.message}`));
170
+ }
171
+ });
172
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Bridge Listener - Connects to Agent Bridge for task polling
3
+ *
4
+ * Uses polling as Supabase Realtime requires direct SDK access
5
+ * which is complex in CLI context. Polling every 5 seconds
6
+ * provides good responsiveness for task-based workflows.
7
+ */
8
+
9
+ import axios from 'axios';
10
+ import { EventEmitter } from 'events';
11
+
12
+ export interface BridgeListenerEvents {
13
+ task: (task: BridgeTask) => void;
14
+ ping: () => void;
15
+ error: (error: Error) => void;
16
+ connected: () => void;
17
+ disconnected: () => void;
18
+ }
19
+
20
+ export interface BridgeTask {
21
+ id: string;
22
+ project_id: string;
23
+ task_id: string | null;
24
+ status: string;
25
+ proposal: string | null;
26
+ summary: string | null;
27
+ created_at: string;
28
+ }
29
+
30
+ export interface BridgeListener extends EventEmitter {
31
+ connect(): Promise<void>;
32
+ disconnect(): Promise<void>;
33
+ on<K extends keyof BridgeListenerEvents>(event: K, listener: BridgeListenerEvents[K]): this;
34
+ emit<K extends keyof BridgeListenerEvents>(event: K, ...args: Parameters<BridgeListenerEvents[K]>): boolean;
35
+ }
36
+
37
+ const POLL_INTERVAL_MS = 5000; // 5 seconds
38
+
39
+ export function createBridgeListener(
40
+ projectId: string,
41
+ apiUrl: string,
42
+ apiKey: string
43
+ ): BridgeListener {
44
+ const emitter = new EventEmitter() as BridgeListener;
45
+ let pollInterval: NodeJS.Timeout | null = null;
46
+ let isConnected = false;
47
+ let lastCheckedId: string | null = null;
48
+
49
+ const checkBridge = async () => {
50
+ try {
51
+ const response = await axios.get(`${apiUrl}/api/v1/agent/bridge`, {
52
+ params: {
53
+ project_id: projectId,
54
+ action: 'check'
55
+ },
56
+ headers: { Authorization: `Bearer ${apiKey}` },
57
+ timeout: 10000
58
+ });
59
+
60
+ if (response.data.success && response.data.data?.task) {
61
+ const task = response.data.data.task;
62
+
63
+ // Check if this is a new task
64
+ if (task.id !== lastCheckedId) {
65
+ lastCheckedId = task.id;
66
+
67
+ // Check for ping/heartbeat
68
+ if (task.proposal?.startsWith('ping')) {
69
+ emitter.emit('ping');
70
+
71
+ // Auto-acknowledge ping
72
+ await acknowledgePing(task.id);
73
+ } else {
74
+ // Emit task for processing
75
+ emitter.emit('task', task);
76
+ }
77
+ }
78
+ }
79
+ } catch (error: any) {
80
+ // Don't emit error for network issues during polling
81
+ // as this is expected when offline
82
+ if (error.code !== 'ECONNREFUSED' && error.code !== 'ETIMEDOUT') {
83
+ emitter.emit('error', error);
84
+ }
85
+ }
86
+ };
87
+
88
+ const acknowledgePing = async (taskId: string) => {
89
+ try {
90
+ await axios.post(`${apiUrl}/api/v1/agent/bridge`, {
91
+ project_id: projectId,
92
+ action: 'update',
93
+ bridge_id: taskId,
94
+ status: 'COMPLETED',
95
+ summary: 'Pong! Guardian Daemon is active.'
96
+ }, {
97
+ headers: { Authorization: `Bearer ${apiKey}` },
98
+ timeout: 5000
99
+ });
100
+ } catch {
101
+ // Silently fail ping acknowledgment
102
+ }
103
+ };
104
+
105
+ emitter.connect = async () => {
106
+ if (isConnected) return;
107
+
108
+ // Initial check
109
+ await checkBridge();
110
+
111
+ // Start polling
112
+ pollInterval = setInterval(checkBridge, POLL_INTERVAL_MS);
113
+ isConnected = true;
114
+ emitter.emit('connected');
115
+ };
116
+
117
+ emitter.disconnect = async () => {
118
+ if (pollInterval) {
119
+ clearInterval(pollInterval);
120
+ pollInterval = null;
121
+ }
122
+ isConnected = false;
123
+ emitter.emit('disconnected');
124
+ };
125
+
126
+ return emitter;
127
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Daemon Core - Orchestrates all background watchers
3
+ *
4
+ * Combines:
5
+ * 1. File Watcher (chokidar) - Monitors filesystem changes
6
+ * 2. Guardian Rules - Validates files against rules on change
7
+ * 3. Agent Bridge Listener - Receives tasks from Rigstate Cloud
8
+ */
9
+
10
+ import chalk from 'chalk';
11
+ import ora from 'ora';
12
+ import * as fs from 'fs/promises';
13
+ import { EventEmitter } from 'events';
14
+ import { createFileWatcher, type FileWatcherEvents } from './file-watcher.js';
15
+ import { createHeuristicEngine } from './heuristic-engine.js';
16
+ import { createInterventionProtocol, type InterventionDecision } from './intervention-protocol.js';
17
+ import { createGuardianMonitor } from './guardian-monitor.js';
18
+ import { createBridgeListener, type BridgeListenerEvents } from './bridge-listener.js';
19
+ import { DaemonConfig, DaemonState } from './types.js';
20
+ import { trackSkillUsage } from './telemetry.js';
21
+ import { jitProvisionSkill } from '../utils/skills-provisioner.js';
22
+
23
+ export class GuardianDaemon extends EventEmitter {
24
+ private config: DaemonConfig;
25
+ private state: DaemonState;
26
+ private fileWatcher: ReturnType<typeof createFileWatcher> | null = null;
27
+ private guardianMonitor: ReturnType<typeof createGuardianMonitor> | null = null;
28
+ private heuristicEngine: ReturnType<typeof createHeuristicEngine> | null = null;
29
+ private interventionProtocol: ReturnType<typeof createInterventionProtocol> | null = null;
30
+ private bridgeListener: ReturnType<typeof createBridgeListener> | null = null;
31
+
32
+ constructor(config: DaemonConfig) {
33
+ super();
34
+ this.config = config;
35
+ this.state = {
36
+ isRunning: false,
37
+ startedAt: null,
38
+ filesChecked: 0,
39
+ violationsFound: 0,
40
+ tasksProcessed: 0,
41
+ lastActivity: null
42
+ };
43
+ }
44
+
45
+ async start(): Promise<void> {
46
+ if (this.state.isRunning) {
47
+ console.log(chalk.yellow('Daemon is already running.'));
48
+ return;
49
+ }
50
+
51
+ this.printWelcome();
52
+ this.state.isRunning = true;
53
+ this.state.startedAt = new Date().toISOString();
54
+
55
+ // 1. Initialize Engines
56
+ this.heuristicEngine = createHeuristicEngine();
57
+ this.interventionProtocol = createInterventionProtocol();
58
+ this.guardianMonitor = createGuardianMonitor(this.config.projectId, this.config.apiUrl, this.config.apiKey);
59
+
60
+ // 2. Load and Sync Rules
61
+ await this.guardianMonitor.loadRules();
62
+ console.log(chalk.green(` ✓ Loaded ${this.guardianMonitor.getRuleCount()} rules`));
63
+ await this.syncHeuristics();
64
+
65
+ // 3. Setup File Watcher
66
+ if (this.config.checkOnChange) {
67
+ this.setupFileWatcher();
68
+ }
69
+
70
+ // 4. Setup Bridge
71
+ if (this.config.bridgeEnabled) {
72
+ await this.setupBridge();
73
+ }
74
+
75
+ this.printActive();
76
+ this.emit('started', this.state);
77
+ }
78
+
79
+ private printWelcome() {
80
+ console.log(chalk.bold.blue('\n🛡️ Guardian Daemon Starting...'));
81
+ console.log(chalk.dim(`Project: ${this.config.projectId}`));
82
+ console.log(chalk.dim(`Watch Path: ${this.config.watchPath}`));
83
+ console.log(chalk.dim('─'.repeat(50)));
84
+ }
85
+
86
+ private printActive() {
87
+ console.log(chalk.dim('─'.repeat(50)));
88
+ console.log(chalk.green.bold('✅ Guardian Daemon is now active'));
89
+ console.log(chalk.dim('Press Ctrl+C to stop\n'));
90
+ }
91
+
92
+ private async syncHeuristics() {
93
+ if (!this.heuristicEngine) return;
94
+ const synced = await this.heuristicEngine.refreshRules(this.config.projectId, this.config.apiUrl, this.config.apiKey);
95
+ if (synced) console.log(chalk.green(' ✓ Synced heuristic rules'));
96
+ }
97
+
98
+ private setupFileWatcher() {
99
+ console.log(chalk.dim('📂 Starting file watcher...'));
100
+ this.fileWatcher = createFileWatcher(this.config.watchPath);
101
+ this.fileWatcher.on('change', (path) => this.handleFileChange(path));
102
+ this.fileWatcher.start();
103
+ console.log(chalk.green(' ✓ File watcher active'));
104
+ }
105
+
106
+ private async handleFileChange(filePath: string) {
107
+ this.state.lastActivity = new Date().toISOString();
108
+ if (this.config.verbose) console.log(chalk.dim(` 📝 File changed: ${filePath}`));
109
+
110
+ // 1. Calculate metrics
111
+ let lineCount = 0;
112
+ try {
113
+ const content = await fs.readFile(filePath, 'utf-8');
114
+ lineCount = content.split('\n').length;
115
+ } catch (e) { /* Deleted file or error */ }
116
+
117
+ // A. Prediction & JIT
118
+ if (this.heuristicEngine && this.interventionProtocol && this.guardianMonitor) {
119
+ const matches = await this.heuristicEngine.analyzeFile(filePath, {
120
+ lineCount,
121
+ rules: this.guardianMonitor.getRules()
122
+ });
123
+ for (const match of matches) {
124
+ console.log(chalk.magenta(` 💡 PREDICTIVE ACTIVATION: ${match.skillId}`));
125
+ console.log(chalk.dim(` Reason: ${match.reason}`));
126
+
127
+ const decision = this.interventionProtocol.evaluateTrigger(match.skillId, match.confidence);
128
+ this.interventionProtocol.enforce(decision);
129
+
130
+ await jitProvisionSkill(match.skillId, this.config.apiUrl, this.config.apiKey, this.config.projectId, process.cwd());
131
+ await trackSkillUsage(this.config.apiUrl, this.config.apiKey, this.config.projectId, match.skillId);
132
+ this.emit('skill:suggestion', match);
133
+ }
134
+ }
135
+
136
+ // B. Guardian Check
137
+ if (this.guardianMonitor) {
138
+ if (this.interventionProtocol) this.interventionProtocol.clear(filePath);
139
+ const result = await this.guardianMonitor.checkFile(filePath);
140
+ this.state.filesChecked++;
141
+
142
+ if (result.violations.length > 0) {
143
+ this.state.violationsFound += result.violations.length;
144
+ this.emit('violation', { file: filePath, violations: result.violations });
145
+ for (const v of result.violations) {
146
+ const color = v.severity === 'critical' ? chalk.red : v.severity === 'warning' ? chalk.yellow : chalk.blue;
147
+ console.log(color(` [${v.severity.toUpperCase()}] ${filePath}: ${v.message}`));
148
+ if (this.interventionProtocol) {
149
+ const decision = this.interventionProtocol.evaluateViolation(v.message, v.severity as any);
150
+ this.interventionProtocol.enforce(decision);
151
+ this.interventionProtocol.registerViolation(filePath, decision);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ private async setupBridge() {
159
+ console.log(chalk.dim('🌉 Connecting to Agent Bridge...'));
160
+ this.bridgeListener = createBridgeListener(this.config.projectId, this.config.apiUrl, this.config.apiKey);
161
+ this.bridgeListener.on('task', (task) => {
162
+ this.state.lastActivity = new Date().toISOString();
163
+ this.state.tasksProcessed++;
164
+ console.log(chalk.cyan(`\n📥 New task received: ${task.id}`));
165
+ this.emit('task', task);
166
+ });
167
+ await this.bridgeListener.connect();
168
+ console.log(chalk.green(' ✓ Agent Bridge connected'));
169
+ }
170
+
171
+ async stop(): Promise<void> {
172
+ if (!this.state.isRunning) return;
173
+ console.log(chalk.dim('\n🛑 Stopping Guardian Daemon...'));
174
+ if (this.fileWatcher) await this.fileWatcher.stop();
175
+ if (this.bridgeListener) await this.bridgeListener.disconnect();
176
+ this.state.isRunning = false;
177
+ console.log(chalk.green('✓ Daemon stopped.'));
178
+ this.emit('stopped', this.state);
179
+ }
180
+
181
+ getState(): DaemonState {
182
+ return { ...this.state };
183
+ }
184
+ }
@@ -0,0 +1,45 @@
1
+ import { GuardianDaemon } from './core.js';
2
+ import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
3
+ import { loadManifest } from '../utils/manifest.js';
4
+ import { DaemonConfig } from './types.js';
5
+
6
+ /**
7
+ * Factory for creating a GuardianDaemon with resolved configuration.
8
+ */
9
+ export async function createDaemon(options: {
10
+ project?: string;
11
+ path?: string;
12
+ noBridge?: boolean;
13
+ verbose?: boolean;
14
+ }): Promise<GuardianDaemon> {
15
+ const apiUrl = getApiUrl();
16
+ let projectId = options.project;
17
+
18
+ if (!projectId) {
19
+ const manifest = await loadManifest();
20
+ if (manifest) projectId = manifest.project_id;
21
+ }
22
+
23
+ if (!projectId) projectId = getProjectId();
24
+
25
+ if (!projectId) {
26
+ throw new Error('No project ID found. Run "rigstate link" or use --project <id>.');
27
+ }
28
+
29
+ const apiKey = getApiKey();
30
+ if (!apiKey) {
31
+ throw new Error('Not authenticated. Run "rigstate login" first.');
32
+ }
33
+
34
+ const config: DaemonConfig = {
35
+ projectId,
36
+ apiUrl,
37
+ apiKey,
38
+ watchPath: options.path || process.cwd(),
39
+ checkOnChange: true,
40
+ bridgeEnabled: !options.noBridge,
41
+ verbose: !!options.verbose
42
+ };
43
+
44
+ return new GuardianDaemon(config);
45
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * File Watcher - Monitors filesystem for changes using chokidar
3
+ */
4
+
5
+ import * as chokidar from 'chokidar';
6
+ import path from 'path';
7
+ import { EventEmitter } from 'events';
8
+
9
+ export interface FileWatcherEvents {
10
+ change: (filePath: string) => void;
11
+ add: (filePath: string) => void;
12
+ unlink: (filePath: string) => void;
13
+ error: (error: Error) => void;
14
+ ready: () => void;
15
+ }
16
+
17
+ export interface FileWatcher extends EventEmitter {
18
+ start(): void;
19
+ stop(): Promise<void>;
20
+ on<K extends keyof FileWatcherEvents>(event: K, listener: FileWatcherEvents[K]): this;
21
+ emit<K extends keyof FileWatcherEvents>(event: K, ...args: Parameters<FileWatcherEvents[K]>): boolean;
22
+ }
23
+
24
+ const CODE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
25
+
26
+ function isCodeFile(filePath: string): boolean {
27
+ const ext = path.extname(filePath).toLowerCase();
28
+ return CODE_EXTENSIONS.includes(ext);
29
+ }
30
+
31
+ export function createFileWatcher(watchPath: string): FileWatcher {
32
+ const emitter = new EventEmitter() as FileWatcher;
33
+ let watcher: chokidar.FSWatcher | null = null;
34
+
35
+ emitter.start = () => {
36
+ const absolutePath = path.resolve(process.cwd(), watchPath);
37
+
38
+ watcher = chokidar.watch(absolutePath, {
39
+ ignored: (path) => {
40
+ // Always ignore node_modules
41
+ if (path.includes('node_modules')) return true;
42
+ if (path.includes('.git')) return true;
43
+ if (path.includes('.next')) return true;
44
+ if (path.includes('dist')) return true;
45
+ if (path.includes('build')) return true;
46
+ if (path.includes('.rigstate')) return true;
47
+ if (path.includes('coverage')) return true;
48
+ return false;
49
+ },
50
+ persistent: true,
51
+ ignoreInitial: true,
52
+ depth: 15,
53
+ awaitWriteFinish: {
54
+ stabilityThreshold: 200,
55
+ pollInterval: 100
56
+ },
57
+ usePolling: false, // Use native events when possible
58
+ interval: 300,
59
+ binaryInterval: 1000
60
+ });
61
+
62
+ watcher.on('change', (filePath: string) => {
63
+ if (isCodeFile(filePath)) {
64
+ emitter.emit('change', path.relative(process.cwd(), filePath));
65
+ }
66
+ });
67
+
68
+ watcher.on('add', (filePath: string) => {
69
+ if (isCodeFile(filePath)) {
70
+ emitter.emit('add', path.relative(process.cwd(), filePath));
71
+ }
72
+ });
73
+
74
+ watcher.on('unlink', (filePath: string) => {
75
+ if (isCodeFile(filePath)) {
76
+ emitter.emit('unlink', path.relative(process.cwd(), filePath));
77
+ }
78
+ });
79
+
80
+ watcher.on('error', (error: any) => {
81
+ emitter.emit('error', error);
82
+ });
83
+
84
+ watcher.on('ready', () => {
85
+ emitter.emit('ready');
86
+ });
87
+ };
88
+
89
+ emitter.stop = async () => {
90
+ if (watcher) {
91
+ await watcher.close();
92
+ watcher = null;
93
+ }
94
+ };
95
+
96
+ return emitter;
97
+ }