@litmers/cursorflow-orchestrator 0.1.3 → 0.1.6

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 (90) hide show
  1. package/CHANGELOG.md +17 -7
  2. package/README.md +33 -2
  3. package/commands/cursorflow-doctor.md +24 -0
  4. package/commands/cursorflow-signal.md +19 -0
  5. package/dist/cli/clean.d.ts +5 -0
  6. package/dist/cli/clean.js +57 -0
  7. package/dist/cli/clean.js.map +1 -0
  8. package/dist/cli/doctor.d.ts +15 -0
  9. package/dist/cli/doctor.js +139 -0
  10. package/dist/cli/doctor.js.map +1 -0
  11. package/dist/cli/index.d.ts +6 -0
  12. package/dist/cli/index.js +125 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/cli/init.d.ts +7 -0
  15. package/dist/cli/init.js +302 -0
  16. package/dist/cli/init.js.map +1 -0
  17. package/dist/cli/monitor.d.ts +8 -0
  18. package/dist/cli/monitor.js +210 -0
  19. package/dist/cli/monitor.js.map +1 -0
  20. package/dist/cli/resume.d.ts +5 -0
  21. package/dist/cli/resume.js +128 -0
  22. package/dist/cli/resume.js.map +1 -0
  23. package/dist/cli/run.d.ts +5 -0
  24. package/dist/cli/run.js +128 -0
  25. package/dist/cli/run.js.map +1 -0
  26. package/dist/cli/setup-commands.d.ts +23 -0
  27. package/dist/cli/setup-commands.js +234 -0
  28. package/dist/cli/setup-commands.js.map +1 -0
  29. package/dist/cli/signal.d.ts +7 -0
  30. package/dist/cli/signal.js +99 -0
  31. package/dist/cli/signal.js.map +1 -0
  32. package/dist/core/orchestrator.d.ts +47 -0
  33. package/dist/core/orchestrator.js +192 -0
  34. package/dist/core/orchestrator.js.map +1 -0
  35. package/dist/core/reviewer.d.ts +60 -0
  36. package/dist/core/reviewer.js +239 -0
  37. package/dist/core/reviewer.js.map +1 -0
  38. package/dist/core/runner.d.ts +51 -0
  39. package/dist/core/runner.js +499 -0
  40. package/dist/core/runner.js.map +1 -0
  41. package/dist/utils/config.d.ts +31 -0
  42. package/dist/utils/config.js +198 -0
  43. package/dist/utils/config.js.map +1 -0
  44. package/dist/utils/cursor-agent.d.ts +61 -0
  45. package/dist/utils/cursor-agent.js +263 -0
  46. package/dist/utils/cursor-agent.js.map +1 -0
  47. package/dist/utils/doctor.d.ts +63 -0
  48. package/dist/utils/doctor.js +280 -0
  49. package/dist/utils/doctor.js.map +1 -0
  50. package/dist/utils/git.d.ts +131 -0
  51. package/dist/utils/git.js +272 -0
  52. package/dist/utils/git.js.map +1 -0
  53. package/dist/utils/logger.d.ts +68 -0
  54. package/dist/utils/logger.js +158 -0
  55. package/dist/utils/logger.js.map +1 -0
  56. package/dist/utils/state.d.ts +65 -0
  57. package/dist/utils/state.js +216 -0
  58. package/dist/utils/state.js.map +1 -0
  59. package/dist/utils/types.d.ts +118 -0
  60. package/dist/utils/types.js +6 -0
  61. package/dist/utils/types.js.map +1 -0
  62. package/examples/README.md +155 -0
  63. package/examples/demo-project/README.md +262 -0
  64. package/examples/demo-project/_cursorflow/tasks/demo-test/01-create-utils.json +18 -0
  65. package/examples/demo-project/_cursorflow/tasks/demo-test/02-add-tests.json +18 -0
  66. package/examples/demo-project/_cursorflow/tasks/demo-test/README.md +109 -0
  67. package/package.json +71 -61
  68. package/scripts/ai-security-check.js +11 -4
  69. package/scripts/local-security-gate.sh +76 -0
  70. package/src/cli/{clean.js → clean.ts} +11 -5
  71. package/src/cli/doctor.ts +127 -0
  72. package/src/cli/{index.js → index.ts} +27 -16
  73. package/src/cli/{init.js → init.ts} +26 -18
  74. package/src/cli/{monitor.js → monitor.ts} +57 -44
  75. package/src/cli/resume.ts +119 -0
  76. package/src/cli/run.ts +109 -0
  77. package/src/cli/{setup-commands.js → setup-commands.ts} +38 -18
  78. package/src/cli/signal.ts +89 -0
  79. package/src/core/{orchestrator.js → orchestrator.ts} +44 -26
  80. package/src/core/{reviewer.js → reviewer.ts} +36 -29
  81. package/src/core/{runner.js → runner.ts} +125 -76
  82. package/src/utils/{config.js → config.ts} +17 -25
  83. package/src/utils/{cursor-agent.js → cursor-agent.ts} +38 -47
  84. package/src/utils/doctor.ts +312 -0
  85. package/src/utils/{git.js → git.ts} +70 -56
  86. package/src/utils/{logger.js → logger.ts} +170 -178
  87. package/src/utils/{state.js → state.ts} +30 -38
  88. package/src/utils/types.ts +134 -0
  89. package/src/cli/resume.js +0 -31
  90. package/src/cli/run.js +0 -51
@@ -1,18 +1,24 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * CursorFlow monitor command
4
3
  */
5
4
 
6
- const fs = require('fs');
7
- const path = require('path');
8
- const logger = require('../utils/logger');
9
- const { loadState } = require('../utils/state');
10
- const { loadConfig } = require('../utils/config');
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as logger from '../utils/logger';
8
+ import { loadState } from '../utils/state';
9
+ import { LaneState } from '../utils/types';
10
+ import { loadConfig } from '../utils/config';
11
11
 
12
- function parseArgs(args) {
12
+ interface MonitorOptions {
13
+ runDir?: string;
14
+ watch: boolean;
15
+ interval: number;
16
+ }
17
+
18
+ function parseArgs(args: string[]): MonitorOptions {
13
19
  const watch = args.includes('--watch');
14
20
  const intervalIdx = args.indexOf('--interval');
15
- const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1]) || 2 : 2;
21
+ const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
16
22
 
17
23
  // Find run directory (first non-option argument)
18
24
  const runDir = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
@@ -27,7 +33,7 @@ function parseArgs(args) {
27
33
  /**
28
34
  * Find the latest run directory
29
35
  */
30
- function findLatestRunDir(logsDir) {
36
+ function findLatestRunDir(logsDir: string): string | null {
31
37
  const runsDir = path.join(logsDir, 'runs');
32
38
 
33
39
  if (!fs.existsSync(runsDir)) {
@@ -43,13 +49,13 @@ function findLatestRunDir(logsDir) {
43
49
  }))
44
50
  .sort((a, b) => b.mtime - a.mtime);
45
51
 
46
- return runs.length > 0 ? runs[0].path : null;
52
+ return runs.length > 0 ? runs[0]!.path : null;
47
53
  }
48
54
 
49
55
  /**
50
56
  * List all lanes in a run directory
51
57
  */
52
- function listLanes(runDir) {
58
+ function listLanes(runDir: string): { name: string; path: string }[] {
53
59
  const lanesDir = path.join(runDir, 'lanes');
54
60
 
55
61
  if (!fs.existsSync(lanesDir)) {
@@ -70,9 +76,16 @@ function listLanes(runDir) {
70
76
  /**
71
77
  * Get lane status
72
78
  */
73
- function getLaneStatus(lanePath) {
79
+ function getLaneStatus(lanePath: string): {
80
+ status: string;
81
+ currentTask: number | string;
82
+ totalTasks: number | string;
83
+ progress: string;
84
+ pipelineBranch?: string;
85
+ chatId?: string;
86
+ } {
74
87
  const statePath = path.join(lanePath, 'state.json');
75
- const state = loadState(statePath);
88
+ const state = loadState<LaneState & { chatId?: string }>(statePath);
76
89
 
77
90
  if (!state) {
78
91
  return {
@@ -89,7 +102,7 @@ function getLaneStatus(lanePath) {
89
102
 
90
103
  return {
91
104
  status: state.status || 'unknown',
92
- currentTask: state.currentTaskIndex + 1,
105
+ currentTask: (state.currentTaskIndex || 0) + 1,
93
106
  totalTasks: state.totalTasks || '?',
94
107
  progress: `${progress}%`,
95
108
  pipelineBranch: state.pipelineBranch || '-',
@@ -97,10 +110,25 @@ function getLaneStatus(lanePath) {
97
110
  };
98
111
  }
99
112
 
113
+ /**
114
+ * Get status icon
115
+ */
116
+ function getStatusIcon(status: string): string {
117
+ const icons: Record<string, string> = {
118
+ 'running': '🔄',
119
+ 'completed': '✅',
120
+ 'failed': '❌',
121
+ 'blocked_dependency': '🚫',
122
+ 'no state': '⚪',
123
+ };
124
+
125
+ return icons[status] || '❓';
126
+ }
127
+
100
128
  /**
101
129
  * Display lane status table
102
130
  */
103
- function displayStatus(runDir, lanes) {
131
+ function displayStatus(runDir: string, lanes: { name: string; path: string }[]): void {
104
132
  console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
105
133
  console.log(`📊 Run: ${path.basename(runDir)}`);
106
134
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
@@ -131,25 +159,10 @@ function displayStatus(runDir, lanes) {
131
159
  console.log();
132
160
  }
133
161
 
134
- /**
135
- * Get status icon
136
- */
137
- function getStatusIcon(status) {
138
- const icons = {
139
- 'running': '🔄',
140
- 'completed': '✅',
141
- 'failed': '❌',
142
- 'blocked_dependency': '🚫',
143
- 'no state': '⚪',
144
- };
145
-
146
- return icons[status] || '❓';
147
- }
148
-
149
162
  /**
150
163
  * Monitor lanes
151
164
  */
152
- async function monitor(args) {
165
+ async function monitor(args: string[]): Promise<void> {
153
166
  logger.section('📡 Monitoring Lane Execution');
154
167
 
155
168
  const options = parseArgs(args);
@@ -159,20 +172,18 @@ async function monitor(args) {
159
172
  let runDir = options.runDir;
160
173
 
161
174
  if (!runDir || runDir === 'latest') {
162
- runDir = findLatestRunDir(config.logsDir);
175
+ runDir = findLatestRunDir(config.logsDir) || undefined;
163
176
 
164
177
  if (!runDir) {
165
- logger.error('No run directories found');
166
- logger.info(`Runs directory: ${path.join(config.logsDir, 'runs')}`);
167
- process.exit(1);
178
+ logger.error(`Runs directory: ${path.join(config.logsDir, 'runs')}`);
179
+ throw new Error('No run directories found');
168
180
  }
169
181
 
170
182
  logger.info(`Using latest run: ${path.basename(runDir)}`);
171
183
  }
172
184
 
173
185
  if (!fs.existsSync(runDir)) {
174
- logger.error(`Run directory not found: ${runDir}`);
175
- process.exit(1);
186
+ throw new Error(`Run directory not found: ${runDir}`);
176
187
  }
177
188
 
178
189
  // Watch mode
@@ -187,8 +198,8 @@ async function monitor(args) {
187
198
  process.stdout.write('\x1Bc');
188
199
  }
189
200
 
190
- const lanes = listLanes(runDir);
191
- displayStatus(runDir, lanes);
201
+ const lanes = listLanes(runDir!);
202
+ displayStatus(runDir!, lanes);
192
203
 
193
204
  iteration++;
194
205
  };
@@ -200,10 +211,12 @@ async function monitor(args) {
200
211
  const intervalId = setInterval(refresh, options.interval * 1000);
201
212
 
202
213
  // Handle Ctrl+C
203
- process.on('SIGINT', () => {
204
- clearInterval(intervalId);
205
- console.log('\n👋 Monitoring stopped\n');
206
- process.exit(0);
214
+ return new Promise((_, reject) => {
215
+ process.on('SIGINT', () => {
216
+ clearInterval(intervalId);
217
+ console.log('\n👋 Monitoring stopped\n');
218
+ process.exit(0);
219
+ });
207
220
  });
208
221
 
209
222
  } else {
@@ -213,4 +226,4 @@ async function monitor(args) {
213
226
  }
214
227
  }
215
228
 
216
- module.exports = monitor;
229
+ export = monitor;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * CursorFlow resume command
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import * as fs from 'fs';
7
+ import { spawn } from 'child_process';
8
+ import * as logger from '../utils/logger';
9
+ import { loadConfig, getLogsDir } from '../utils/config';
10
+ import { loadState } from '../utils/state';
11
+ import { LaneState } from '../utils/types';
12
+
13
+ interface ResumeOptions {
14
+ lane: string | null;
15
+ runDir: string | null;
16
+ clean: boolean;
17
+ restart: boolean;
18
+ }
19
+
20
+ function parseArgs(args: string[]): ResumeOptions {
21
+ const runDirIdx = args.indexOf('--run-dir');
22
+
23
+ return {
24
+ lane: args.find(a => !a.startsWith('--')) || null,
25
+ runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
26
+ clean: args.includes('--clean'),
27
+ restart: args.includes('--restart'),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Find the latest run directory
33
+ */
34
+ function findLatestRunDir(logsDir: string): string | null {
35
+ const runsDir = path.join(logsDir, 'runs');
36
+ if (!fs.existsSync(runsDir)) return null;
37
+
38
+ const runs = fs.readdirSync(runsDir)
39
+ .filter(d => d.startsWith('run-'))
40
+ .sort()
41
+ .reverse();
42
+
43
+ return runs.length > 0 ? path.join(runsDir, runs[0]!) : null;
44
+ }
45
+
46
+ async function resume(args: string[]): Promise<void> {
47
+ const options = parseArgs(args);
48
+ const config = loadConfig();
49
+ const logsDir = getLogsDir(config);
50
+
51
+ if (!options.lane) {
52
+ throw new Error('Lane name required (e.g., cursorflow resume lane-1)');
53
+ }
54
+
55
+ let runDir = options.runDir;
56
+ if (!runDir) {
57
+ runDir = findLatestRunDir(logsDir);
58
+ }
59
+
60
+ if (!runDir || !fs.existsSync(runDir)) {
61
+ throw new Error(`Run directory not found: ${runDir || 'latest'}`);
62
+ }
63
+
64
+ const laneDir = path.join(runDir, 'lanes', options.lane);
65
+ const statePath = path.join(laneDir, 'state.json');
66
+
67
+ if (!fs.existsSync(statePath)) {
68
+ throw new Error(`Lane state not found at ${statePath}. Is the lane name correct?`);
69
+ }
70
+
71
+ const state = loadState<LaneState>(statePath);
72
+ if (!state) {
73
+ throw new Error(`Failed to load state from ${statePath}`);
74
+ }
75
+
76
+ if (!state.tasksFile || !fs.existsSync(state.tasksFile)) {
77
+ throw new Error(`Original tasks file not found: ${state.tasksFile}. Resume impossible without task definition.`);
78
+ }
79
+
80
+ logger.section(`🔁 Resuming Lane: ${options.lane}`);
81
+ logger.info(`Run: ${path.basename(runDir)}`);
82
+ logger.info(`Tasks: ${state.tasksFile}`);
83
+ logger.info(`Starting from task index: ${options.restart ? 0 : state.currentTaskIndex}`);
84
+
85
+ const runnerPath = require.resolve('../core/runner');
86
+ const runnerArgs = [
87
+ runnerPath,
88
+ state.tasksFile,
89
+ '--run-dir', laneDir,
90
+ '--start-index', options.restart ? '0' : String(state.currentTaskIndex),
91
+ ];
92
+
93
+ logger.info(`Spawning runner process...`);
94
+
95
+ const child = spawn('node', runnerArgs, {
96
+ stdio: 'inherit',
97
+ env: process.env,
98
+ });
99
+
100
+ return new Promise((resolve, reject) => {
101
+ child.on('exit', (code) => {
102
+ if (code === 0) {
103
+ logger.success(`Lane ${options.lane} completed successfully`);
104
+ resolve();
105
+ } else if (code === 2) {
106
+ logger.warn(`Lane ${options.lane} blocked on dependency change`);
107
+ resolve();
108
+ } else {
109
+ reject(new Error(`Lane ${options.lane} failed with exit code ${code}`));
110
+ }
111
+ });
112
+
113
+ child.on('error', (error) => {
114
+ reject(new Error(`Failed to start runner: ${error.message}`));
115
+ });
116
+ });
117
+ }
118
+
119
+ export = resume;
package/src/cli/run.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * CursorFlow run command
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import * as fs from 'fs';
7
+ import * as logger from '../utils/logger';
8
+ import { orchestrate } from '../core/orchestrator';
9
+ import { getLogsDir, loadConfig } from '../utils/config';
10
+ import { runDoctor } from '../utils/doctor';
11
+ import { areCommandsInstalled, setupCommands } from './setup-commands';
12
+
13
+ interface RunOptions {
14
+ tasksDir?: string;
15
+ dryRun: boolean;
16
+ executor: string | null;
17
+ skipDoctor: boolean;
18
+ }
19
+
20
+ function parseArgs(args: string[]): RunOptions {
21
+ const tasksDir = args.find(a => !a.startsWith('--'));
22
+ const executorIdx = args.indexOf('--executor');
23
+
24
+ return {
25
+ tasksDir,
26
+ dryRun: args.includes('--dry-run'),
27
+ executor: executorIdx >= 0 ? args[executorIdx + 1] || null : null,
28
+ skipDoctor: args.includes('--skip-doctor') || args.includes('--no-doctor'),
29
+ };
30
+ }
31
+
32
+ async function run(args: string[]): Promise<void> {
33
+ const options = parseArgs(args);
34
+
35
+ // Auto-setup Cursor commands if missing or outdated
36
+ if (!areCommandsInstalled()) {
37
+ logger.info('Installing missing or outdated Cursor IDE commands...');
38
+ try {
39
+ setupCommands({ silent: true });
40
+ } catch (e) {
41
+ // Non-blocking
42
+ }
43
+ }
44
+
45
+ if (!options.tasksDir) {
46
+ console.log('\nUsage: cursorflow run <tasks-dir> [options]');
47
+ throw new Error('Tasks directory required');
48
+ }
49
+
50
+ const config = loadConfig();
51
+ const logsDir = getLogsDir(config);
52
+
53
+ // Resolve tasks dir:
54
+ // - Prefer the exact path if it exists relative to cwd
55
+ // - Otherwise, fall back to projectRoot-relative path for better ergonomics
56
+ const tasksDir =
57
+ path.isAbsolute(options.tasksDir)
58
+ ? options.tasksDir
59
+ : (fs.existsSync(options.tasksDir)
60
+ ? path.resolve(process.cwd(), options.tasksDir)
61
+ : path.join(config.projectRoot, options.tasksDir));
62
+
63
+ if (!fs.existsSync(tasksDir)) {
64
+ throw new Error(`Tasks directory not found: ${tasksDir}`);
65
+ }
66
+
67
+ // Preflight checks (doctor)
68
+ if (!options.skipDoctor) {
69
+ const report = runDoctor({
70
+ cwd: process.cwd(),
71
+ tasksDir,
72
+ executor: options.executor || config.executor,
73
+ includeCursorAgentChecks: true,
74
+ });
75
+
76
+ if (!report.ok) {
77
+ logger.section('🛑 Pre-flight check failed');
78
+ for (const issue of report.issues) {
79
+ const header = `${issue.title} (${issue.id})`;
80
+ if (issue.severity === 'error') {
81
+ logger.error(header, '❌');
82
+ } else {
83
+ logger.warn(header, '⚠️');
84
+ }
85
+ console.log(` ${issue.message}`);
86
+ if (issue.details) console.log(` Details: ${issue.details}`);
87
+ if (issue.fixes?.length) {
88
+ console.log(' Fix:');
89
+ for (const fix of issue.fixes) console.log(` - ${fix}`);
90
+ }
91
+ console.log('');
92
+ }
93
+ throw new Error('Pre-flight checks failed. Run `cursorflow doctor` for details.');
94
+ }
95
+ }
96
+
97
+ try {
98
+ await orchestrate(tasksDir, {
99
+ executor: options.executor || config.executor,
100
+ pollInterval: config.pollInterval * 1000,
101
+ runDir: path.join(logsDir, 'runs', `run-${Date.now()}`),
102
+ });
103
+ } catch (error: any) {
104
+ // Re-throw to be handled by the main entry point
105
+ throw new Error(`Orchestration failed: ${error.message}`);
106
+ }
107
+ }
108
+
109
+ export = run;
@@ -1,17 +1,22 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * Setup Cursor commands
4
3
  *
5
4
  * Installs CursorFlow commands to .cursor/commands/cursorflow/
6
5
  */
7
6
 
8
- const fs = require('fs');
9
- const path = require('path');
10
- const logger = require('../utils/logger');
11
- const { findProjectRoot } = require('../utils/config');
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as logger from '../utils/logger';
10
+ import { findProjectRoot } from '../utils/config';
12
11
 
13
- function parseArgs(args) {
14
- const options = {
12
+ interface SetupOptions {
13
+ force?: boolean;
14
+ uninstall?: boolean;
15
+ silent?: boolean;
16
+ }
17
+
18
+ function parseArgs(args: string[]): SetupOptions {
19
+ const options: SetupOptions = {
15
20
  force: false,
16
21
  uninstall: false,
17
22
  silent: false,
@@ -39,7 +44,7 @@ function parseArgs(args) {
39
44
  return options;
40
45
  }
41
46
 
42
- function printHelp() {
47
+ function printHelp(): void {
43
48
  console.log(`
44
49
  Usage: cursorflow-setup [options]
45
50
 
@@ -58,12 +63,12 @@ Examples:
58
63
  `);
59
64
  }
60
65
 
61
- function getCommandsSourceDir() {
66
+ function getCommandsSourceDir(): string {
62
67
  // Commands are in the package directory
63
68
  return path.join(__dirname, '..', '..', 'commands');
64
69
  }
65
70
 
66
- function setupCommands(options = {}) {
71
+ export function setupCommands(options: SetupOptions = {}): { installed: number; backed: number; skipped: number } {
67
72
  const projectRoot = findProjectRoot();
68
73
  const targetDir = path.join(projectRoot, '.cursor', 'commands', 'cursorflow');
69
74
  const sourceDir = getCommandsSourceDir();
@@ -139,7 +144,7 @@ function setupCommands(options = {}) {
139
144
  return { installed, backed, skipped };
140
145
  }
141
146
 
142
- function uninstallCommands(options = {}) {
147
+ export function uninstallCommands(options: SetupOptions = {}): { removed: number } {
143
148
  const projectRoot = findProjectRoot();
144
149
  const targetDir = path.join(projectRoot, '.cursor', 'commands', 'cursorflow');
145
150
 
@@ -178,7 +183,26 @@ function uninstallCommands(options = {}) {
178
183
  return { removed };
179
184
  }
180
185
 
181
- async function main(args) {
186
+ /**
187
+ * Check if commands are already installed
188
+ */
189
+ export function areCommandsInstalled(): boolean {
190
+ const projectRoot = findProjectRoot();
191
+ const targetDir = path.join(projectRoot, '.cursor', 'commands', 'cursorflow');
192
+ const sourceDir = getCommandsSourceDir();
193
+
194
+ if (!fs.existsSync(targetDir) || !fs.existsSync(sourceDir)) {
195
+ return false;
196
+ }
197
+
198
+ const sourceFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith('.md'));
199
+ const targetFiles = fs.readdirSync(targetDir).filter(f => f.endsWith('.md'));
200
+
201
+ // Basic check: do we have all the files from source in target?
202
+ return sourceFiles.every(f => targetFiles.includes(f));
203
+ }
204
+
205
+ async function main(args: string[]): Promise<any> {
182
206
  const options = parseArgs(args);
183
207
 
184
208
  try {
@@ -187,7 +211,7 @@ async function main(args) {
187
211
  } else {
188
212
  return setupCommands(options);
189
213
  }
190
- } catch (error) {
214
+ } catch (error: any) {
191
215
  if (!options.silent) {
192
216
  logger.error(error.message);
193
217
  }
@@ -198,13 +222,9 @@ async function main(args) {
198
222
  if (require.main === module) {
199
223
  main(process.argv.slice(2)).catch(error => {
200
224
  console.error('❌ Error:', error.message);
201
- if (process.env.DEBUG) {
225
+ if (process.env['DEBUG']) {
202
226
  console.error(error.stack);
203
227
  }
204
228
  process.exit(1);
205
229
  });
206
230
  }
207
-
208
- module.exports = setupCommands;
209
- module.exports.setupCommands = setupCommands;
210
- module.exports.uninstallCommands = uninstallCommands;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * CursorFlow signal command
3
+ *
4
+ * Send a direct message to a running lane
5
+ */
6
+
7
+ import * as path from 'path';
8
+ import * as fs from 'fs';
9
+ import * as logger from '../utils/logger';
10
+ import { loadConfig, getLogsDir } from '../utils/config';
11
+ import { appendLog, createConversationEntry } from '../utils/state';
12
+
13
+ interface SignalOptions {
14
+ lane: string | null;
15
+ message: string | null;
16
+ runDir: string | null;
17
+ }
18
+
19
+ function parseArgs(args: string[]): SignalOptions {
20
+ const runDirIdx = args.indexOf('--run-dir');
21
+
22
+ // First non-option is lane, second (or rest joined) is message
23
+ const nonOptions = args.filter(a => !a.startsWith('--'));
24
+
25
+ return {
26
+ lane: nonOptions[0] || null,
27
+ message: nonOptions.slice(1).join(' ') || null,
28
+ runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
29
+ };
30
+ }
31
+
32
+ function findLatestRunDir(logsDir: string): string | null {
33
+ const runsDir = path.join(logsDir, 'runs');
34
+ if (!fs.existsSync(runsDir)) return null;
35
+
36
+ const runs = fs.readdirSync(runsDir)
37
+ .filter(d => d.startsWith('run-'))
38
+ .sort()
39
+ .reverse();
40
+
41
+ return runs.length > 0 ? path.join(runsDir, runs[0]!) : null;
42
+ }
43
+
44
+ async function signal(args: string[]): Promise<void> {
45
+ const options = parseArgs(args);
46
+ const config = loadConfig();
47
+ const logsDir = getLogsDir(config);
48
+
49
+ if (!options.lane) {
50
+ throw new Error('Lane name required: cursorflow signal <lane> "<message>"');
51
+ }
52
+
53
+ if (!options.message) {
54
+ throw new Error('Message required: cursorflow signal <lane> "<message>"');
55
+ }
56
+
57
+ let runDir = options.runDir;
58
+ if (!runDir) {
59
+ runDir = findLatestRunDir(logsDir);
60
+ }
61
+
62
+ if (!runDir || !fs.existsSync(runDir)) {
63
+ throw new Error(`Run directory not found: ${runDir || 'latest'}`);
64
+ }
65
+
66
+ const convoPath = path.join(runDir, 'lanes', options.lane, 'conversation.jsonl');
67
+
68
+ if (!fs.existsSync(convoPath)) {
69
+ throw new Error(`Conversation log not found at ${convoPath}. Is the lane running?`);
70
+ }
71
+
72
+ logger.info(`Sending signal to lane: ${options.lane}`);
73
+ logger.info(`Message: "${options.message}"`);
74
+
75
+ // Append as a "commander" role message
76
+ // Note: We cast to 'system' or similar if 'commander' isn't in the enum,
77
+ // but let's use 'reviewer' or 'system' which agents usually respect,
78
+ // or update the type definition.
79
+ const entry = createConversationEntry('system', `[COMMANDER INTERVENTION]\n${options.message}`, {
80
+ task: 'DIRECT_SIGNAL'
81
+ });
82
+
83
+ appendLog(convoPath, entry);
84
+
85
+ logger.success('Signal sent successfully. The agent will see this message in its next turn or via file monitoring.');
86
+ }
87
+
88
+ export = signal;
89
+