@litmers/cursorflow-orchestrator 0.1.6 → 0.1.9

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.
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Direct cursor-agent spawn test script
4
+ *
5
+ * This script tests cursor-agent directly to compare behavior with CursorFlow.
6
+ * It helps identify if the issue is in how CursorFlow spawns the agent.
7
+ *
8
+ * Usage:
9
+ * node scripts/patches/test-cursor-agent.js [--stdio-mode <mode>]
10
+ *
11
+ * Modes:
12
+ * - pipe: Full pipe mode (default, same as CursorFlow)
13
+ * - inherit-stdin: Inherit stdin, pipe stdout/stderr
14
+ * - inherit-all: Inherit all (like running directly in terminal)
15
+ */
16
+
17
+ const { spawn, spawnSync } = require('child_process');
18
+ const path = require('path');
19
+
20
+ // Parse args
21
+ const args = process.argv.slice(2);
22
+ const stdioModeIdx = args.indexOf('--stdio-mode');
23
+ const stdioMode = stdioModeIdx >= 0 ? args[stdioModeIdx + 1] : 'pipe';
24
+
25
+ console.log('='.repeat(60));
26
+ console.log('cursor-agent Direct Spawn Test');
27
+ console.log('='.repeat(60));
28
+ console.log(`stdio mode: ${stdioMode}`);
29
+ console.log('');
30
+
31
+ // Step 1: Check cursor-agent is available
32
+ console.log('1. Checking cursor-agent availability...');
33
+ const whichResult = spawnSync('which', ['cursor-agent']);
34
+ if (whichResult.status !== 0) {
35
+ console.error(' ❌ cursor-agent not found in PATH');
36
+ process.exit(1);
37
+ }
38
+ console.log(` ✓ Found at: ${whichResult.stdout.toString().trim()}`);
39
+
40
+ // Step 2: Create a chat session
41
+ console.log('\n2. Creating chat session...');
42
+ const createChatResult = spawnSync('cursor-agent', ['create-chat'], {
43
+ encoding: 'utf8',
44
+ stdio: 'pipe',
45
+ timeout: 30000,
46
+ });
47
+
48
+ if (createChatResult.status !== 0) {
49
+ console.error(' ❌ Failed to create chat:');
50
+ console.error(createChatResult.stderr || createChatResult.stdout);
51
+ process.exit(1);
52
+ }
53
+
54
+ const chatIdLines = createChatResult.stdout.trim().split('\n');
55
+ const chatId = chatIdLines[chatIdLines.length - 1];
56
+ console.log(` ✓ Chat ID: ${chatId}`);
57
+
58
+ // Step 3: Configure stdio based on mode
59
+ let stdioConfig;
60
+ switch (stdioMode) {
61
+ case 'inherit-stdin':
62
+ stdioConfig = ['inherit', 'pipe', 'pipe'];
63
+ break;
64
+ case 'inherit-all':
65
+ stdioConfig = 'inherit';
66
+ break;
67
+ case 'ignore-stdin':
68
+ stdioConfig = ['ignore', 'pipe', 'pipe'];
69
+ break;
70
+ case 'pipe':
71
+ default:
72
+ stdioConfig = ['pipe', 'pipe', 'pipe'];
73
+ break;
74
+ }
75
+
76
+ console.log(`\n3. Testing with stdio: ${JSON.stringify(stdioConfig)}`);
77
+
78
+ // Step 4: Send a simple prompt
79
+ const testPrompt = 'Just respond with: Hello World. Nothing else.';
80
+ const workspace = process.cwd();
81
+
82
+ const agentArgs = [
83
+ '--print',
84
+ '--output-format', 'json',
85
+ '--workspace', workspace,
86
+ '--model', 'gemini-3-flash',
87
+ '--resume', chatId,
88
+ testPrompt,
89
+ ];
90
+
91
+ console.log(` Workspace: ${workspace}`);
92
+ console.log(` Prompt: "${testPrompt}"`);
93
+ console.log('');
94
+ console.log('4. Spawning cursor-agent...');
95
+ console.log(` Command: cursor-agent ${agentArgs.join(' ').substring(0, 80)}...`);
96
+ console.log('');
97
+
98
+ const startTime = Date.now();
99
+
100
+ const child = spawn('cursor-agent', agentArgs, {
101
+ stdio: stdioConfig,
102
+ env: {
103
+ ...process.env,
104
+ // Disable Python buffering if cursor-agent uses Python
105
+ PYTHONUNBUFFERED: '1',
106
+ // Disable Node.js experimental warnings
107
+ NODE_OPTIONS: '',
108
+ },
109
+ });
110
+
111
+ let fullStdout = '';
112
+ let fullStderr = '';
113
+ let gotData = false;
114
+
115
+ // Track first byte time
116
+ let firstByteTime = null;
117
+
118
+ if (child.stdout) {
119
+ child.stdout.on('data', (data) => {
120
+ if (!firstByteTime) {
121
+ firstByteTime = Date.now();
122
+ console.log(` [+${((firstByteTime - startTime) / 1000).toFixed(2)}s] First stdout byte received`);
123
+ }
124
+ gotData = true;
125
+ const str = data.toString();
126
+ fullStdout += str;
127
+ process.stdout.write(` [stdout] ${str}`);
128
+ });
129
+ }
130
+
131
+ if (child.stderr) {
132
+ child.stderr.on('data', (data) => {
133
+ const str = data.toString();
134
+ fullStderr += str;
135
+ process.stderr.write(` [stderr] ${str}`);
136
+ });
137
+ }
138
+
139
+ // Set timeout
140
+ const timeout = setTimeout(() => {
141
+ console.log('\n ⏰ TIMEOUT after 60 seconds');
142
+ console.log(` Got any data: ${gotData}`);
143
+ console.log(` Stdout length: ${fullStdout.length}`);
144
+ console.log(` Stderr length: ${fullStderr.length}`);
145
+ child.kill('SIGTERM');
146
+ }, 60000);
147
+
148
+ child.on('close', (code) => {
149
+ clearTimeout(timeout);
150
+ const elapsed = (Date.now() - startTime) / 1000;
151
+
152
+ console.log('\n' + '-'.repeat(60));
153
+ console.log('RESULT');
154
+ console.log('-'.repeat(60));
155
+ console.log(`Exit code: ${code}`);
156
+ console.log(`Duration: ${elapsed.toFixed(2)}s`);
157
+ console.log(`First byte: ${firstByteTime ? ((firstByteTime - startTime) / 1000).toFixed(2) + 's' : 'never'}`);
158
+ console.log(`Stdout bytes: ${fullStdout.length}`);
159
+ console.log(`Stderr bytes: ${fullStderr.length}`);
160
+
161
+ if (fullStdout.length > 0) {
162
+ console.log('\nStdout content:');
163
+ console.log(fullStdout);
164
+ }
165
+
166
+ if (fullStderr.length > 0) {
167
+ console.log('\nStderr content:');
168
+ console.log(fullStderr);
169
+ }
170
+
171
+ // Try to parse JSON
172
+ const lines = fullStdout.split('\n').filter(Boolean);
173
+ for (let i = lines.length - 1; i >= 0; i--) {
174
+ const line = lines[i].trim();
175
+ if (line.startsWith('{') && line.endsWith('}')) {
176
+ try {
177
+ const json = JSON.parse(line);
178
+ console.log('\nParsed JSON response:');
179
+ console.log(JSON.stringify(json, null, 2));
180
+ break;
181
+ } catch {
182
+ continue;
183
+ }
184
+ }
185
+ }
186
+
187
+ console.log('\n' + '='.repeat(60));
188
+ if (code === 0 && fullStdout.includes('result')) {
189
+ console.log('✓ SUCCESS - cursor-agent responded correctly');
190
+ } else if (gotData) {
191
+ console.log('⚠ PARTIAL - Got data but may not have completed');
192
+ } else {
193
+ console.log('❌ FAILURE - No data received');
194
+ }
195
+ console.log('='.repeat(60));
196
+ });
197
+
198
+ child.on('error', (err) => {
199
+ clearTimeout(timeout);
200
+ console.error(`\n❌ Spawn error: ${err.message}`);
201
+ process.exit(1);
202
+ });
203
+
package/src/cli/clean.ts CHANGED
@@ -1,36 +1,156 @@
1
1
  /**
2
- * CursorFlow clean command (stub)
2
+ * CursorFlow clean command
3
+ *
4
+ * Clean up worktrees, branches, and logs created by CursorFlow
3
5
  */
4
6
 
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
5
9
  import * as logger from '../utils/logger';
10
+ import * as git from '../utils/git';
11
+ import { loadConfig, getLogsDir } from '../utils/config';
6
12
 
7
13
  interface CleanOptions {
8
14
  type?: string;
9
15
  pattern: string | null;
10
16
  dryRun: boolean;
11
17
  force: boolean;
18
+ all: boolean;
12
19
  }
13
20
 
14
21
  function parseArgs(args: string[]): CleanOptions {
15
22
  return {
16
- type: args[0], // branches | worktrees | logs | all
23
+ type: args.find(a => ['branches', 'worktrees', 'logs', 'all'].includes(a)),
17
24
  pattern: null,
18
25
  dryRun: args.includes('--dry-run'),
19
26
  force: args.includes('--force'),
27
+ all: args.includes('--all'),
20
28
  };
21
29
  }
22
30
 
23
31
  async function clean(args: string[]): Promise<void> {
24
- logger.section('🧹 Cleaning CursorFlow Resources');
25
-
26
32
  const options = parseArgs(args);
33
+ const config = loadConfig();
34
+ const repoRoot = git.getRepoRoot();
35
+
36
+ logger.section('🧹 Cleaning CursorFlow Resources');
37
+
38
+ const type = options.type || 'all';
39
+
40
+ if (type === 'all') {
41
+ await cleanWorktrees(config, repoRoot, options);
42
+ await cleanBranches(config, repoRoot, options);
43
+ await cleanLogs(config, options);
44
+ } else if (type === 'worktrees') {
45
+ await cleanWorktrees(config, repoRoot, options);
46
+ } else if (type === 'branches') {
47
+ await cleanBranches(config, repoRoot, options);
48
+ } else if (type === 'logs') {
49
+ await cleanLogs(config, options);
50
+ }
51
+
52
+ logger.success('\n✨ Cleaning complete!');
53
+ }
54
+
55
+ async function cleanWorktrees(config: any, repoRoot: string, options: CleanOptions) {
56
+ logger.info('\nChecking worktrees...');
57
+ const worktrees = git.listWorktrees(repoRoot);
27
58
 
28
- logger.info('This command will be fully implemented in the next phase');
29
- logger.info(`Clean type: ${options.type}`);
30
- logger.info(`Dry run: ${options.dryRun}`);
59
+ const worktreeRoot = path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees');
60
+ const toRemove = worktrees.filter(wt => {
61
+ // Skip main worktree
62
+ if (wt.path === repoRoot) return false;
63
+
64
+ const isInsideRoot = wt.path.startsWith(worktreeRoot);
65
+ const hasPrefix = path.basename(wt.path).startsWith(config.worktreePrefix || 'cursorflow-');
66
+
67
+ return isInsideRoot || hasPrefix;
68
+ });
69
+
70
+ if (toRemove.length === 0) {
71
+ logger.info(' No worktrees found to clean.');
72
+ return;
73
+ }
74
+
75
+ for (const wt of toRemove) {
76
+ if (options.dryRun) {
77
+ logger.info(` [DRY RUN] Would remove worktree: ${wt.path} (${wt.branch || 'no branch'})`);
78
+ } else {
79
+ try {
80
+ logger.info(` Removing worktree: ${wt.path}...`);
81
+ git.removeWorktree(wt.path, { cwd: repoRoot, force: options.force });
82
+
83
+ // Git worktree remove might leave the directory if it has untracked files
84
+ if (fs.existsSync(wt.path)) {
85
+ if (options.force) {
86
+ fs.rmSync(wt.path, { recursive: true, force: true });
87
+ logger.info(` (Forced removal of directory)`);
88
+ } else {
89
+ logger.warn(` Directory still exists: ${wt.path} (contains untracked files). Use --force to delete anyway.`);
90
+ }
91
+ }
92
+ } catch (e: any) {
93
+ logger.error(` Failed to remove worktree ${wt.path}: ${e.message}`);
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ async function cleanBranches(config: any, repoRoot: string, options: CleanOptions) {
100
+ logger.info('\nChecking branches...');
31
101
 
32
- logger.warn('\n⚠️ Implementation pending');
33
- logger.info('This will clean branches, worktrees, and logs');
102
+ // List all local branches
103
+ const result = git.runGitResult(['branch', '--list'], { cwd: repoRoot });
104
+ if (!result.success) return;
105
+
106
+ const branches = result.stdout
107
+ .split('\n')
108
+ .map(b => b.replace('*', '').trim())
109
+ .filter(b => b && b !== 'main' && b !== 'master');
110
+
111
+ const prefix = config.branchPrefix || 'feature/';
112
+ const toDelete = branches.filter(b => b.startsWith(prefix));
113
+
114
+ if (toDelete.length === 0) {
115
+ logger.info(' No branches found to clean.');
116
+ return;
117
+ }
118
+
119
+ for (const branch of toDelete) {
120
+ if (options.dryRun) {
121
+ logger.info(` [DRY RUN] Would delete branch: ${branch}`);
122
+ } else {
123
+ try {
124
+ logger.info(` Deleting branch: ${branch}...`);
125
+ git.deleteBranch(branch, { cwd: repoRoot, force: options.force || options.all });
126
+ } catch (e: any) {
127
+ logger.warn(` Could not delete branch ${branch}: ${e.message}. Use --force if it's not merged.`);
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ async function cleanLogs(config: any, options: CleanOptions) {
134
+ const logsDir = getLogsDir(config);
135
+ logger.info(`\nChecking logs in ${logsDir}...`);
136
+
137
+ if (!fs.existsSync(logsDir)) {
138
+ logger.info(' Logs directory does not exist.');
139
+ return;
140
+ }
141
+
142
+ if (options.dryRun) {
143
+ logger.info(` [DRY RUN] Would remove logs directory: ${logsDir}`);
144
+ } else {
145
+ try {
146
+ logger.info(` Removing logs...`);
147
+ fs.rmSync(logsDir, { recursive: true, force: true });
148
+ fs.mkdirSync(logsDir, { recursive: true });
149
+ logger.info(` Logs cleared.`);
150
+ } catch (e: any) {
151
+ logger.error(` Failed to clean logs: ${e.message}`);
152
+ }
153
+ }
34
154
  }
35
155
 
36
156
  export = clean;
package/src/cli/index.ts CHANGED
@@ -20,45 +20,45 @@ const COMMANDS: Record<string, CommandFn> = {
20
20
 
21
21
  function printHelp(): void {
22
22
  console.log(`
23
- CursorFlow - Git worktree-based parallel AI agent orchestration
23
+ \x1b[1m\x1b[36mCursorFlow\x1b[0m - Git worktree-based parallel AI agent orchestration
24
24
 
25
- Usage: cursorflow <command> [options]
25
+ \x1b[1mUSAGE\x1b[0m
26
+ $ \x1b[32mcursorflow\x1b[0m <command> [options]
26
27
 
27
- Commands:
28
- init [options] Initialize CursorFlow in project
29
- run <tasks-dir> [options] Run orchestration
30
- monitor [run-dir] [options] Monitor lane execution
31
- clean <type> [options] Clean branches/worktrees/logs
32
- resume <lane> [options] Resume interrupted lane
33
- doctor [options] Check environment and preflight
34
- signal <lane> <msg> Directly intervene in a running lane
28
+ \x1b[1mCOMMANDS\x1b[0m
29
+ \x1b[33minit\x1b[0m [options] Initialize CursorFlow in project
30
+ \x1b[33mrun\x1b[0m <tasks-dir> [options] Run orchestration (DAG-based)
31
+ \x1b[33mmonitor\x1b[0m [run-dir] [options] \x1b[36mInteractive\x1b[0m lane dashboard
32
+ \x1b[33mclean\x1b[0m <type> [options] Clean branches/worktrees/logs
33
+ \x1b[33mresume\x1b[0m <lane> [options] Resume interrupted lane
34
+ \x1b[33mdoctor\x1b[0m [options] Check environment and preflight
35
+ \x1b[33msignal\x1b[0m <lane> <msg> Directly intervene in a running lane
35
36
 
36
- Global Options:
37
+ \x1b[1mGLOBAL OPTIONS\x1b[0m
37
38
  --config <path> Config file path
38
39
  --help, -h Show help
39
40
  --version, -v Show version
40
41
 
41
- Examples:
42
- cursorflow init --example
43
- cursorflow run _cursorflow/tasks/MyFeature/
44
- cursorflow monitor --watch
45
- cursorflow clean branches --all
46
- cursorflow doctor
42
+ \x1b[1mEXAMPLES\x1b[0m
43
+ $ \x1b[32mcursorflow init --example\x1b[0m
44
+ $ \x1b[32mcursorflow run _cursorflow/tasks/MyFeature/\x1b[0m
45
+ $ \x1b[32mcursorflow monitor latest\x1b[0m
46
+ $ \x1b[32mcursorflow signal lane-1 "Please use pnpm instead of npm"\x1b[0m
47
47
 
48
- Documentation:
48
+ \x1b[1mDOCUMENTATION\x1b[0m
49
49
  https://github.com/eungjin-cigro/cursorflow#readme
50
50
  `);
51
51
  }
52
52
 
53
53
  function printVersion(): void {
54
54
  const pkg = require('../../package.json');
55
- console.log(`CursorFlow v${pkg.version}`);
55
+ console.log(`\x1b[1m\x1b[36mCursorFlow\x1b[0m v${pkg.version}`);
56
56
  }
57
57
 
58
58
  async function main(): Promise<void> {
59
59
  const args = process.argv.slice(2);
60
60
 
61
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
61
+ if (args.length === 0 || args.includes('--help') || args.includes('-h') || args[0] === 'help') {
62
62
  printHelp();
63
63
  return;
64
64
  }