@litmers/cursorflow-orchestrator 0.1.13 → 0.1.14

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 (68) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +83 -2
  3. package/commands/cursorflow-clean.md +20 -6
  4. package/commands/cursorflow-prepare.md +1 -1
  5. package/commands/cursorflow-resume.md +127 -6
  6. package/commands/cursorflow-run.md +2 -2
  7. package/commands/cursorflow-signal.md +11 -4
  8. package/dist/cli/clean.js +164 -12
  9. package/dist/cli/clean.js.map +1 -1
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +6 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/logs.d.ts +8 -0
  14. package/dist/cli/logs.js +746 -0
  15. package/dist/cli/logs.js.map +1 -0
  16. package/dist/cli/monitor.js +113 -30
  17. package/dist/cli/monitor.js.map +1 -1
  18. package/dist/cli/prepare.js +1 -1
  19. package/dist/cli/resume.js +367 -18
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +2 -0
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/signal.js +34 -20
  24. package/dist/cli/signal.js.map +1 -1
  25. package/dist/core/orchestrator.d.ts +11 -1
  26. package/dist/core/orchestrator.js +257 -35
  27. package/dist/core/orchestrator.js.map +1 -1
  28. package/dist/core/reviewer.js +20 -0
  29. package/dist/core/reviewer.js.map +1 -1
  30. package/dist/core/runner.js +113 -13
  31. package/dist/core/runner.js.map +1 -1
  32. package/dist/utils/config.js +34 -0
  33. package/dist/utils/config.js.map +1 -1
  34. package/dist/utils/enhanced-logger.d.ts +209 -0
  35. package/dist/utils/enhanced-logger.js +963 -0
  36. package/dist/utils/enhanced-logger.js.map +1 -0
  37. package/dist/utils/events.d.ts +59 -0
  38. package/dist/utils/events.js +37 -0
  39. package/dist/utils/events.js.map +1 -0
  40. package/dist/utils/git.d.ts +5 -0
  41. package/dist/utils/git.js +25 -0
  42. package/dist/utils/git.js.map +1 -1
  43. package/dist/utils/types.d.ts +122 -1
  44. package/dist/utils/webhook.d.ts +5 -0
  45. package/dist/utils/webhook.js +109 -0
  46. package/dist/utils/webhook.js.map +1 -0
  47. package/examples/README.md +1 -1
  48. package/package.json +1 -1
  49. package/scripts/simple-logging-test.sh +97 -0
  50. package/scripts/test-real-logging.sh +289 -0
  51. package/scripts/test-streaming-multi-task.sh +247 -0
  52. package/src/cli/clean.ts +170 -13
  53. package/src/cli/index.ts +4 -1
  54. package/src/cli/logs.ts +848 -0
  55. package/src/cli/monitor.ts +123 -30
  56. package/src/cli/prepare.ts +1 -1
  57. package/src/cli/resume.ts +463 -22
  58. package/src/cli/run.ts +2 -0
  59. package/src/cli/signal.ts +43 -27
  60. package/src/core/orchestrator.ts +303 -37
  61. package/src/core/reviewer.ts +22 -0
  62. package/src/core/runner.ts +128 -12
  63. package/src/utils/config.ts +36 -0
  64. package/src/utils/enhanced-logger.ts +1097 -0
  65. package/src/utils/events.ts +117 -0
  66. package/src/utils/git.ts +25 -0
  67. package/src/utils/types.ts +150 -1
  68. package/src/utils/webhook.ts +85 -0
package/src/cli/clean.ts CHANGED
@@ -8,7 +8,7 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as logger from '../utils/logger';
10
10
  import * as git from '../utils/git';
11
- import { loadConfig, getLogsDir } from '../utils/config';
11
+ import { loadConfig, getLogsDir, getTasksDir } from '../utils/config';
12
12
 
13
13
  interface CleanOptions {
14
14
  type?: string;
@@ -17,6 +17,8 @@ interface CleanOptions {
17
17
  force: boolean;
18
18
  all: boolean;
19
19
  help: boolean;
20
+ keepLatest: boolean;
21
+ includeLatest: boolean;
20
22
  }
21
23
 
22
24
  function printHelp(): void {
@@ -29,26 +31,43 @@ Types:
29
31
  branches Remove local feature branches
30
32
  worktrees Remove temporary Git worktrees
31
33
  logs Clear log directories
34
+ tasks Clear task directories
32
35
  all Remove all of the above (default)
33
36
 
34
37
  Options:
35
38
  --dry-run Show what would be removed without deleting
36
39
  --force Force removal (ignore uncommitted changes)
40
+ --include-latest Also remove the most recent item (by default, latest is kept)
37
41
  --help, -h Show help
38
42
  `);
39
43
  }
40
44
 
41
45
  function parseArgs(args: string[]): CleanOptions {
46
+ const includeLatest = args.includes('--include-latest');
42
47
  return {
43
- type: args.find(a => ['branches', 'worktrees', 'logs', 'all'].includes(a)),
48
+ type: args.find(a => ['branches', 'worktrees', 'logs', 'tasks', 'all'].includes(a)),
44
49
  pattern: null,
45
50
  dryRun: args.includes('--dry-run'),
46
51
  force: args.includes('--force'),
47
52
  all: args.includes('--all'),
48
53
  help: args.includes('--help') || args.includes('-h'),
54
+ keepLatest: !includeLatest, // Default: keep latest, unless --include-latest is specified
55
+ includeLatest,
49
56
  };
50
57
  }
51
58
 
59
+ /**
60
+ * Get the modification time of a path (directory or file)
61
+ */
62
+ function getModTime(targetPath: string): number {
63
+ try {
64
+ const stat = fs.statSync(targetPath);
65
+ return stat.mtimeMs;
66
+ } catch {
67
+ return 0;
68
+ }
69
+ }
70
+
52
71
  async function clean(args: string[]): Promise<void> {
53
72
  const options = parseArgs(args);
54
73
 
@@ -68,12 +87,15 @@ async function clean(args: string[]): Promise<void> {
68
87
  await cleanWorktrees(config, repoRoot, options);
69
88
  await cleanBranches(config, repoRoot, options);
70
89
  await cleanLogs(config, options);
90
+ await cleanTasks(config, options);
71
91
  } else if (type === 'worktrees') {
72
92
  await cleanWorktrees(config, repoRoot, options);
73
93
  } else if (type === 'branches') {
74
94
  await cleanBranches(config, repoRoot, options);
75
95
  } else if (type === 'logs') {
76
96
  await cleanLogs(config, options);
97
+ } else if (type === 'tasks') {
98
+ await cleanTasks(config, options);
77
99
  }
78
100
 
79
101
  logger.success('\n✨ Cleaning complete!');
@@ -84,7 +106,7 @@ async function cleanWorktrees(config: any, repoRoot: string, options: CleanOptio
84
106
  const worktrees = git.listWorktrees(repoRoot);
85
107
 
86
108
  const worktreeRoot = path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees');
87
- const toRemove = worktrees.filter(wt => {
109
+ let toRemove = worktrees.filter(wt => {
88
110
  // Skip main worktree
89
111
  if (wt.path === repoRoot) return false;
90
112
 
@@ -99,6 +121,15 @@ async function cleanWorktrees(config: any, repoRoot: string, options: CleanOptio
99
121
  return;
100
122
  }
101
123
 
124
+ // If keepLatest is set, keep the most recent worktree
125
+ if (options.keepLatest && toRemove.length > 1) {
126
+ // Sort by modification time (newest first)
127
+ toRemove.sort((a, b) => getModTime(b.path) - getModTime(a.path));
128
+ const kept = toRemove[0];
129
+ toRemove = toRemove.slice(1);
130
+ logger.info(` Keeping latest worktree: ${kept.path} (${kept.branch || 'no branch'})`);
131
+ }
132
+
102
133
  for (const wt of toRemove) {
103
134
  if (options.dryRun) {
104
135
  logger.info(` [DRY RUN] Would remove worktree: ${wt.path} (${wt.branch || 'no branch'})`);
@@ -123,6 +154,21 @@ async function cleanWorktrees(config: any, repoRoot: string, options: CleanOptio
123
154
  }
124
155
  }
125
156
 
157
+ /**
158
+ * Get the commit timestamp of a branch
159
+ */
160
+ function getBranchCommitTime(branch: string, repoRoot: string): number {
161
+ try {
162
+ const result = git.runGitResult(['log', '-1', '--format=%ct', branch], { cwd: repoRoot });
163
+ if (result.success && result.stdout.trim()) {
164
+ return parseInt(result.stdout.trim(), 10) * 1000; // Convert to milliseconds
165
+ }
166
+ } catch {
167
+ // Ignore errors
168
+ }
169
+ return 0;
170
+ }
171
+
126
172
  async function cleanBranches(config: any, repoRoot: string, options: CleanOptions) {
127
173
  logger.info('\nChecking branches...');
128
174
 
@@ -136,13 +182,22 @@ async function cleanBranches(config: any, repoRoot: string, options: CleanOption
136
182
  .filter(b => b && b !== 'main' && b !== 'master');
137
183
 
138
184
  const prefix = config.branchPrefix || 'feature/';
139
- const toDelete = branches.filter(b => b.startsWith(prefix));
185
+ let toDelete = branches.filter(b => b.startsWith(prefix));
140
186
 
141
187
  if (toDelete.length === 0) {
142
188
  logger.info(' No branches found to clean.');
143
189
  return;
144
190
  }
145
191
 
192
+ // If keepLatest is set, keep the most recent branch
193
+ if (options.keepLatest && toDelete.length > 1) {
194
+ // Sort by commit time (newest first)
195
+ toDelete.sort((a, b) => getBranchCommitTime(b, repoRoot) - getBranchCommitTime(a, repoRoot));
196
+ const kept = toDelete[0];
197
+ toDelete = toDelete.slice(1);
198
+ logger.info(` Keeping latest branch: ${kept}`);
199
+ }
200
+
146
201
  for (const branch of toDelete) {
147
202
  if (options.dryRun) {
148
203
  logger.info(` [DRY RUN] Would delete branch: ${branch}`);
@@ -166,16 +221,118 @@ async function cleanLogs(config: any, options: CleanOptions) {
166
221
  return;
167
222
  }
168
223
 
169
- if (options.dryRun) {
170
- logger.info(` [DRY RUN] Would remove logs directory: ${logsDir}`);
224
+ // If keepLatest is set, keep the most recent log directory/file
225
+ if (options.keepLatest) {
226
+ const entries = fs.readdirSync(logsDir, { withFileTypes: true });
227
+ let items = entries.map(entry => ({
228
+ name: entry.name,
229
+ path: path.join(logsDir, entry.name),
230
+ isDir: entry.isDirectory(),
231
+ mtime: getModTime(path.join(logsDir, entry.name))
232
+ }));
233
+
234
+ if (items.length <= 1) {
235
+ logger.info(' Only one or no log entries found, nothing to clean.');
236
+ return;
237
+ }
238
+
239
+ // Sort by modification time (newest first)
240
+ items.sort((a, b) => b.mtime - a.mtime);
241
+ const kept = items[0];
242
+ const toRemove = items.slice(1);
243
+
244
+ logger.info(` Keeping latest log: ${kept.name}`);
245
+
246
+ for (const item of toRemove) {
247
+ if (options.dryRun) {
248
+ logger.info(` [DRY RUN] Would remove log: ${item.name}`);
249
+ } else {
250
+ try {
251
+ logger.info(` Removing log: ${item.name}...`);
252
+ fs.rmSync(item.path, { recursive: true, force: true });
253
+ } catch (e: any) {
254
+ logger.error(` Failed to remove log ${item.name}: ${e.message}`);
255
+ }
256
+ }
257
+ }
171
258
  } else {
172
- try {
173
- logger.info(` Removing logs...`);
174
- fs.rmSync(logsDir, { recursive: true, force: true });
175
- fs.mkdirSync(logsDir, { recursive: true });
176
- logger.info(` Logs cleared.`);
177
- } catch (e: any) {
178
- logger.error(` Failed to clean logs: ${e.message}`);
259
+ if (options.dryRun) {
260
+ logger.info(` [DRY RUN] Would remove logs directory: ${logsDir}`);
261
+ } else {
262
+ try {
263
+ logger.info(` Removing logs...`);
264
+ fs.rmSync(logsDir, { recursive: true, force: true });
265
+ fs.mkdirSync(logsDir, { recursive: true });
266
+ logger.info(` Logs cleared.`);
267
+ } catch (e: any) {
268
+ logger.error(` Failed to clean logs: ${e.message}`);
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ async function cleanTasks(config: any, options: CleanOptions) {
275
+ const tasksDir = getTasksDir(config);
276
+ logger.info(`\nChecking tasks in ${tasksDir}...`);
277
+
278
+ if (!fs.existsSync(tasksDir)) {
279
+ logger.info(' Tasks directory does not exist.');
280
+ return;
281
+ }
282
+
283
+ // If keepLatest is set, keep the most recent task directory/file
284
+ if (options.keepLatest) {
285
+ const entries = fs.readdirSync(tasksDir, { withFileTypes: true });
286
+ // Skip example task if it exists and there are other tasks
287
+ let items = entries
288
+ .filter(entry => entry.name !== 'example')
289
+ .map(entry => ({
290
+ name: entry.name,
291
+ path: path.join(tasksDir, entry.name),
292
+ isDir: entry.isDirectory(),
293
+ mtime: getModTime(path.join(tasksDir, entry.name))
294
+ }));
295
+
296
+ if (items.length <= 1) {
297
+ logger.info(' Only one or no user task entries found, nothing to clean.');
298
+ return;
299
+ }
300
+
301
+ // Sort by modification time (newest first)
302
+ items.sort((a, b) => b.mtime - a.mtime);
303
+ const kept = items[0];
304
+ const toRemove = items.slice(1);
305
+
306
+ logger.info(` Keeping latest task: ${kept.name}`);
307
+
308
+ for (const item of toRemove) {
309
+ if (options.dryRun) {
310
+ logger.info(` [DRY RUN] Would remove task: ${item.name}`);
311
+ } else {
312
+ try {
313
+ logger.info(` Removing task: ${item.name}...`);
314
+ fs.rmSync(item.path, { recursive: true, force: true });
315
+ } catch (e: any) {
316
+ logger.error(` Failed to remove task ${item.name}: ${e.message}`);
317
+ }
318
+ }
319
+ }
320
+ } else {
321
+ if (options.dryRun) {
322
+ logger.info(` [DRY RUN] Would remove tasks in directory: ${tasksDir} (except example)`);
323
+ } else {
324
+ try {
325
+ const entries = fs.readdirSync(tasksDir, { withFileTypes: true });
326
+ for (const entry of entries) {
327
+ if (entry.name === 'example') continue;
328
+ const itemPath = path.join(tasksDir, entry.name);
329
+ logger.info(` Removing task: ${entry.name}...`);
330
+ fs.rmSync(itemPath, { recursive: true, force: true });
331
+ }
332
+ logger.info(` Tasks cleared.`);
333
+ } catch (e: any) {
334
+ logger.error(` Failed to clean tasks: ${e.message}`);
335
+ }
179
336
  }
180
337
  }
181
338
  }
package/src/cli/index.ts CHANGED
@@ -20,6 +20,7 @@ const COMMANDS: Record<string, CommandFn> = {
20
20
  doctor: require('./doctor'),
21
21
  signal: require('./signal'),
22
22
  models: require('./models'),
23
+ logs: require('./logs'),
23
24
  setup: require('./setup-commands').main,
24
25
  'setup-commands': require('./setup-commands').main,
25
26
  };
@@ -37,10 +38,11 @@ function printHelp(): void {
37
38
  \x1b[33mrun\x1b[0m <tasks-dir> [options] Run orchestration (DAG-based)
38
39
  \x1b[33mmonitor\x1b[0m [run-dir] [options] \x1b[36mInteractive\x1b[0m lane dashboard
39
40
  \x1b[33mclean\x1b[0m <type> [options] Clean branches/worktrees/logs
40
- \x1b[33mresume\x1b[0m <lane> [options] Resume interrupted lane
41
+ \x1b[33mresume\x1b[0m [lane] [options] Resume lane(s) - use --all for batch resume
41
42
  \x1b[33mdoctor\x1b[0m [options] Check environment and preflight
42
43
  \x1b[33msignal\x1b[0m <lane> <msg> Directly intervene in a running lane
43
44
  \x1b[33mmodels\x1b[0m [options] List available AI models
45
+ \x1b[33mlogs\x1b[0m [run-dir] [options] View, export, and follow logs
44
46
 
45
47
  \x1b[1mGLOBAL OPTIONS\x1b[0m
46
48
  --config <path> Config file path
@@ -114,3 +116,4 @@ if (require.main === module) {
114
116
 
115
117
  export default main;
116
118
  export { main };
119
+ export { events } from '../utils/events';