@litmers/cursorflow-orchestrator 0.1.31 → 0.1.36

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 (150) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +182 -59
  3. package/commands/cursorflow-add.md +159 -0
  4. package/commands/cursorflow-doctor.md +45 -23
  5. package/commands/cursorflow-monitor.md +23 -2
  6. package/commands/cursorflow-new.md +87 -0
  7. package/commands/cursorflow-run.md +60 -111
  8. package/dist/cli/add.d.ts +7 -0
  9. package/dist/cli/add.js +377 -0
  10. package/dist/cli/add.js.map +1 -0
  11. package/dist/cli/clean.js +1 -0
  12. package/dist/cli/clean.js.map +1 -1
  13. package/dist/cli/config.d.ts +7 -0
  14. package/dist/cli/config.js +181 -0
  15. package/dist/cli/config.js.map +1 -0
  16. package/dist/cli/doctor.js +47 -4
  17. package/dist/cli/doctor.js.map +1 -1
  18. package/dist/cli/index.js +34 -30
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/logs.js +17 -34
  21. package/dist/cli/logs.js.map +1 -1
  22. package/dist/cli/monitor.js +62 -65
  23. package/dist/cli/monitor.js.map +1 -1
  24. package/dist/cli/new.d.ts +7 -0
  25. package/dist/cli/new.js +232 -0
  26. package/dist/cli/new.js.map +1 -0
  27. package/dist/cli/prepare.js +95 -193
  28. package/dist/cli/prepare.js.map +1 -1
  29. package/dist/cli/resume.js +57 -68
  30. package/dist/cli/resume.js.map +1 -1
  31. package/dist/cli/run.js +60 -30
  32. package/dist/cli/run.js.map +1 -1
  33. package/dist/cli/stop.js +6 -0
  34. package/dist/cli/stop.js.map +1 -1
  35. package/dist/cli/tasks.d.ts +5 -3
  36. package/dist/cli/tasks.js +181 -29
  37. package/dist/cli/tasks.js.map +1 -1
  38. package/dist/core/failure-policy.d.ts +9 -0
  39. package/dist/core/failure-policy.js +9 -0
  40. package/dist/core/failure-policy.js.map +1 -1
  41. package/dist/core/orchestrator.d.ts +20 -6
  42. package/dist/core/orchestrator.js +215 -334
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/runner/agent.d.ts +27 -0
  45. package/dist/core/runner/agent.js +294 -0
  46. package/dist/core/runner/agent.js.map +1 -0
  47. package/dist/core/runner/index.d.ts +5 -0
  48. package/dist/core/runner/index.js +22 -0
  49. package/dist/core/runner/index.js.map +1 -0
  50. package/dist/core/runner/pipeline.d.ts +9 -0
  51. package/dist/core/runner/pipeline.js +539 -0
  52. package/dist/core/runner/pipeline.js.map +1 -0
  53. package/dist/core/runner/prompt.d.ts +25 -0
  54. package/dist/core/runner/prompt.js +175 -0
  55. package/dist/core/runner/prompt.js.map +1 -0
  56. package/dist/core/runner/task.d.ts +26 -0
  57. package/dist/core/runner/task.js +283 -0
  58. package/dist/core/runner/task.js.map +1 -0
  59. package/dist/core/runner/utils.d.ts +37 -0
  60. package/dist/core/runner/utils.js +161 -0
  61. package/dist/core/runner/utils.js.map +1 -0
  62. package/dist/core/runner.d.ts +2 -96
  63. package/dist/core/runner.js +11 -1136
  64. package/dist/core/runner.js.map +1 -1
  65. package/dist/core/stall-detection.d.ts +326 -0
  66. package/dist/core/stall-detection.js +781 -0
  67. package/dist/core/stall-detection.js.map +1 -0
  68. package/dist/services/logging/console.js +2 -1
  69. package/dist/services/logging/console.js.map +1 -1
  70. package/dist/types/config.d.ts +6 -6
  71. package/dist/types/flow.d.ts +84 -0
  72. package/dist/types/flow.js +10 -0
  73. package/dist/types/flow.js.map +1 -0
  74. package/dist/types/index.d.ts +1 -0
  75. package/dist/types/index.js +3 -3
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/types/lane.d.ts +0 -2
  78. package/dist/types/logging.d.ts +5 -1
  79. package/dist/types/task.d.ts +7 -11
  80. package/dist/utils/config.d.ts +5 -1
  81. package/dist/utils/config.js +15 -16
  82. package/dist/utils/config.js.map +1 -1
  83. package/dist/utils/dependency.d.ts +36 -1
  84. package/dist/utils/dependency.js +256 -1
  85. package/dist/utils/dependency.js.map +1 -1
  86. package/dist/utils/doctor.js +40 -8
  87. package/dist/utils/doctor.js.map +1 -1
  88. package/dist/utils/enhanced-logger.d.ts +45 -82
  89. package/dist/utils/enhanced-logger.js +239 -844
  90. package/dist/utils/enhanced-logger.js.map +1 -1
  91. package/dist/utils/flow.d.ts +9 -0
  92. package/dist/utils/flow.js +73 -0
  93. package/dist/utils/flow.js.map +1 -0
  94. package/dist/utils/git.d.ts +29 -0
  95. package/dist/utils/git.js +115 -5
  96. package/dist/utils/git.js.map +1 -1
  97. package/dist/utils/state.js +0 -2
  98. package/dist/utils/state.js.map +1 -1
  99. package/dist/utils/task-service.d.ts +2 -2
  100. package/dist/utils/task-service.js +40 -31
  101. package/dist/utils/task-service.js.map +1 -1
  102. package/package.json +4 -3
  103. package/src/cli/add.ts +397 -0
  104. package/src/cli/clean.ts +1 -0
  105. package/src/cli/config.ts +177 -0
  106. package/src/cli/doctor.ts +48 -4
  107. package/src/cli/index.ts +36 -32
  108. package/src/cli/logs.ts +20 -33
  109. package/src/cli/monitor.ts +70 -75
  110. package/src/cli/new.ts +235 -0
  111. package/src/cli/prepare.ts +98 -205
  112. package/src/cli/resume.ts +61 -76
  113. package/src/cli/run.ts +333 -306
  114. package/src/cli/stop.ts +8 -0
  115. package/src/cli/tasks.ts +200 -21
  116. package/src/core/failure-policy.ts +9 -0
  117. package/src/core/orchestrator.ts +279 -379
  118. package/src/core/runner/agent.ts +314 -0
  119. package/src/core/runner/index.ts +6 -0
  120. package/src/core/runner/pipeline.ts +567 -0
  121. package/src/core/runner/prompt.ts +174 -0
  122. package/src/core/runner/task.ts +320 -0
  123. package/src/core/runner/utils.ts +142 -0
  124. package/src/core/runner.ts +8 -1347
  125. package/src/core/stall-detection.ts +936 -0
  126. package/src/services/logging/console.ts +2 -1
  127. package/src/types/config.ts +6 -6
  128. package/src/types/flow.ts +91 -0
  129. package/src/types/index.ts +15 -3
  130. package/src/types/lane.ts +0 -2
  131. package/src/types/logging.ts +5 -1
  132. package/src/types/task.ts +7 -11
  133. package/src/utils/config.ts +16 -17
  134. package/src/utils/dependency.ts +311 -2
  135. package/src/utils/doctor.ts +36 -8
  136. package/src/utils/enhanced-logger.ts +264 -927
  137. package/src/utils/flow.ts +42 -0
  138. package/src/utils/git.ts +145 -5
  139. package/src/utils/state.ts +0 -2
  140. package/src/utils/task-service.ts +48 -40
  141. package/commands/cursorflow-review.md +0 -56
  142. package/commands/cursorflow-runs.md +0 -59
  143. package/dist/cli/runs.d.ts +0 -5
  144. package/dist/cli/runs.js +0 -214
  145. package/dist/cli/runs.js.map +0 -1
  146. package/dist/core/reviewer.d.ts +0 -66
  147. package/dist/core/reviewer.js +0 -265
  148. package/dist/core/reviewer.js.map +0 -1
  149. package/src/cli/runs.ts +0 -212
  150. package/src/core/reviewer.ts +0 -285
@@ -14,7 +14,7 @@ import * as path from 'path';
14
14
  import * as readline from 'readline';
15
15
  import { loadState, readLog } from '../utils/state';
16
16
  import { LaneState, ConversationEntry } from '../utils/types';
17
- import { loadConfig } from '../utils/config';
17
+ import { loadConfig, getLogsDir } from '../utils/config';
18
18
  import { safeJoin } from '../utils/path';
19
19
  import { getLaneProcessStatus, getFlowSummary, LaneProcessStatus } from '../services/process';
20
20
  import { LogBufferService, BufferedLogEntry } from '../services/logging/buffer';
@@ -56,13 +56,13 @@ const UI = {
56
56
  interface LaneWithDeps {
57
57
  name: string;
58
58
  path: string;
59
- dependsOn: string[];
60
59
  }
61
60
 
62
61
  interface MonitorOptions {
63
62
  runDir?: string;
64
63
  interval: number;
65
64
  help: boolean;
65
+ list: boolean;
66
66
  }
67
67
 
68
68
  function printHelp(): void {
@@ -73,8 +73,14 @@ Interactive lane dashboard to track progress and dependencies.
73
73
 
74
74
  Options:
75
75
  [run-dir] Run directory to monitor (default: latest)
76
+ --list, -l List all runs (multiple flows dashboard)
76
77
  --interval <seconds> Refresh interval (default: 2)
77
78
  --help, -h Show help
79
+
80
+ Examples:
81
+ cursorflow monitor # Monitor latest run
82
+ cursorflow monitor --list # Show all runs dashboard
83
+ cursorflow monitor run-123 # Monitor specific run
78
84
  `);
79
85
  }
80
86
 
@@ -135,16 +141,23 @@ class InteractiveMonitor {
135
141
  return process.stdout.rows || 24;
136
142
  }
137
143
 
138
- constructor(runDir: string, interval: number, logsDir?: string) {
144
+ constructor(runDir: string, interval: number, logsDir?: string, initialView: View = View.LIST) {
145
+ const config = loadConfig();
146
+
147
+ // Change current directory to project root for consistent path handling
148
+ if (config.projectRoot !== process.cwd()) {
149
+ process.chdir(config.projectRoot);
150
+ }
151
+
139
152
  this.runDir = runDir;
140
153
  this.interval = interval;
154
+ this.view = initialView;
141
155
 
142
156
  // Set logs directory for multiple flows discovery
143
157
  if (logsDir) {
144
158
  this.logsDir = logsDir;
145
159
  } else {
146
- const config = loadConfig();
147
- this.logsDir = safeJoin(config.logsDir, 'runs');
160
+ this.logsDir = safeJoin(getLogsDir(config), 'runs');
148
161
  }
149
162
 
150
163
  // Initialize unified log buffer
@@ -152,6 +165,21 @@ class InteractiveMonitor {
152
165
  }
153
166
 
154
167
  public async start() {
168
+ // Non-interactive mode for CI/pipes
169
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
170
+ this.discoverFlows();
171
+ this.refresh();
172
+ // Print summary and exit
173
+ if (this.view === View.FLOWS_DASHBOARD) {
174
+ console.log(`\nFound ${this.allFlows.length} flows`);
175
+ } else {
176
+ console.log(`\nMonitoring run: ${path.basename(this.runDir)}`);
177
+ const flowSummary = getFlowSummary(this.runDir);
178
+ console.log(`Status: ${flowSummary.running} running, ${flowSummary.completed} completed, ${flowSummary.failed} failed`);
179
+ }
180
+ return;
181
+ }
182
+
155
183
  this.setupTerminal();
156
184
 
157
185
  // Start unified log streaming
@@ -999,14 +1027,12 @@ class InteractiveMonitor {
999
1027
  // Next action
1000
1028
  let nextAction = '-';
1001
1029
  if (status.status === 'completed') {
1002
- const dependents = this.lanes.filter(l => laneStatuses[l.name]?.dependsOn?.includes(lane.name));
1003
- nextAction = dependents.length > 0 ? `→ ${dependents.map(d => d.name).join(', ')}` : '✓ Done';
1030
+ nextAction = '✓ Done';
1004
1031
  } else if (status.status === 'waiting') {
1005
1032
  if (status.waitingFor?.length > 0) {
1006
1033
  nextAction = `⏳ ${status.waitingFor.join(', ')}`;
1007
1034
  } else {
1008
- const missingDeps = status.dependsOn.filter((d: string) => laneStatuses[d]?.status !== 'completed');
1009
- nextAction = missingDeps.length > 0 ? `⏳ ${missingDeps.join(', ')}` : '⏳ waiting';
1035
+ nextAction = '⏳ waiting';
1010
1036
  }
1011
1037
  } else if (processStatus?.actualStatus === 'running') {
1012
1038
  nextAction = '🚀 working...';
@@ -1063,9 +1089,6 @@ class InteractiveMonitor {
1063
1089
  process.stdout.write(` ${UI.COLORS.dim}Duration${UI.COLORS.reset} ${this.formatDuration(processStatus?.duration || status.duration)}\n`);
1064
1090
  process.stdout.write(` ${UI.COLORS.dim}Branch${UI.COLORS.reset} ${status.pipelineBranch}\n`);
1065
1091
 
1066
- if (status.dependsOn && status.dependsOn.length > 0) {
1067
- process.stdout.write(` ${UI.COLORS.dim}Depends${UI.COLORS.reset} ${status.dependsOn.join(', ')}\n`);
1068
- }
1069
1092
  if (status.waitingFor && status.waitingFor.length > 0) {
1070
1093
  process.stdout.write(` ${UI.COLORS.yellow}Waiting${UI.COLORS.reset} ${status.waitingFor.join(', ')}\n`);
1071
1094
  }
@@ -1075,7 +1098,7 @@ class InteractiveMonitor {
1075
1098
 
1076
1099
  // Live terminal preview
1077
1100
  this.renderSectionTitle('Live Terminal', 'last 10 lines');
1078
- const logPath = safeJoin(lane.path, 'terminal.log');
1101
+ const logPath = safeJoin(lane.path, 'terminal-readable.log');
1079
1102
  if (fs.existsSync(logPath)) {
1080
1103
  const content = fs.readFileSync(logPath, 'utf8');
1081
1104
  const lines = content.split('\n').slice(-10);
@@ -1132,7 +1155,6 @@ class InteractiveMonitor {
1132
1155
  const colors: Record<string, string> = {
1133
1156
  user: UI.COLORS.yellow,
1134
1157
  assistant: UI.COLORS.green,
1135
- reviewer: UI.COLORS.magenta,
1136
1158
  intervention: UI.COLORS.red,
1137
1159
  system: UI.COLORS.cyan,
1138
1160
  };
@@ -1243,9 +1265,9 @@ class InteractiveMonitor {
1243
1265
  const nodeText = `${statusIcon} ${laneName}`;
1244
1266
  process.stdout.write(` ${statusColor}${nodeText.padEnd(20)}${UI.COLORS.reset}`);
1245
1267
 
1246
- // Render dependencies
1247
- if (status?.dependsOn?.length > 0) {
1248
- process.stdout.write(` ${UI.COLORS.dim}←${UI.COLORS.reset} ${UI.COLORS.yellow}${status.dependsOn.join(', ')}${UI.COLORS.reset}`);
1268
+ // Show task-level dependencies if waiting
1269
+ if (status?.waitingFor?.length > 0) {
1270
+ process.stdout.write(` ${UI.COLORS.dim}←${UI.COLORS.reset} ${UI.COLORS.yellow}${status.waitingFor.join(', ')}${UI.COLORS.reset}`);
1249
1271
  }
1250
1272
  process.stdout.write('\n');
1251
1273
  }
@@ -1256,54 +1278,21 @@ class InteractiveMonitor {
1256
1278
  }
1257
1279
  }
1258
1280
 
1259
- process.stdout.write(`\n ${UI.COLORS.dim}Lanes wait for dependencies to complete before starting${UI.COLORS.reset}\n`);
1281
+ process.stdout.write(`\n ${UI.COLORS.dim}Tasks can wait for other tasks using task-level dependencies${UI.COLORS.reset}\n`);
1260
1282
 
1261
1283
  this.renderFooter(['[←/Esc] Back']);
1262
1284
  }
1263
1285
 
1264
1286
  /**
1265
- * Calculate dependency levels for visualization
1287
+ * Calculate levels for visualization (all lanes run in parallel now)
1266
1288
  */
1267
1289
  private calculateDependencyLevels(): string[][] {
1268
- const levels: string[][] = [];
1269
- const assigned = new Set<string>();
1270
-
1271
- // First, find lanes with no dependencies
1272
- const noDeps = this.lanes.filter(l => !l.dependsOn || l.dependsOn.length === 0);
1273
- if (noDeps.length > 0) {
1274
- levels.push(noDeps.map(l => l.name));
1275
- noDeps.forEach(l => assigned.add(l.name));
1276
- }
1277
-
1278
- // Then assign remaining lanes by dependency completion
1279
- let maxIterations = 10;
1280
- while (assigned.size < this.lanes.length && maxIterations-- > 0) {
1281
- const nextLevel: string[] = [];
1282
-
1283
- for (const lane of this.lanes) {
1284
- if (assigned.has(lane.name)) continue;
1285
-
1286
- // Check if all dependencies are assigned
1287
- const allDepsAssigned = lane.dependsOn.every(d => assigned.has(d));
1288
- if (allDepsAssigned) {
1289
- nextLevel.push(lane.name);
1290
- }
1291
- }
1292
-
1293
- if (nextLevel.length === 0) {
1294
- // Remaining lanes have circular deps or missing deps
1295
- const remaining = this.lanes.filter(l => !assigned.has(l.name)).map(l => l.name);
1296
- if (remaining.length > 0) {
1297
- levels.push(remaining);
1298
- }
1299
- break;
1300
- }
1301
-
1302
- levels.push(nextLevel);
1303
- nextLevel.forEach(n => assigned.add(n));
1290
+ // Since lane-level dependencies are removed, all lanes can run in parallel
1291
+ // Group them into a single level
1292
+ if (this.lanes.length === 0) {
1293
+ return [];
1304
1294
  }
1305
-
1306
- return levels;
1295
+ return [this.lanes.map(l => l.name)];
1307
1296
  }
1308
1297
 
1309
1298
  private renderTerminal() {
@@ -1326,8 +1315,8 @@ class InteractiveMonitor {
1326
1315
  logLines = this.getReadableLogLines(jsonlPath, lane.name);
1327
1316
  totalLines = logLines.length;
1328
1317
  } else {
1329
- // Use raw log
1330
- const logPath = safeJoin(lane.path, 'terminal.log');
1318
+ // Use readable log
1319
+ const logPath = safeJoin(lane.path, 'terminal-readable.log');
1331
1320
  if (fs.existsSync(logPath)) {
1332
1321
  const content = fs.readFileSync(logPath, 'utf8');
1333
1322
  logLines = content.split('\n');
@@ -1689,39 +1678,33 @@ class InteractiveMonitor {
1689
1678
  return fs.readdirSync(lanesDir)
1690
1679
  .filter(d => fs.statSync(safeJoin(lanesDir, d)).isDirectory())
1691
1680
  .map(name => {
1692
- const config = laneConfigs.find(c => c.name === name);
1693
1681
  return {
1694
1682
  name,
1695
1683
  path: safeJoin(lanesDir, name),
1696
- dependsOn: config?.dependsOn || [],
1697
1684
  };
1698
1685
  });
1699
1686
  }
1700
1687
 
1701
- private listLaneFilesFromDir(tasksDir: string): { name: string; dependsOn: string[] }[] {
1688
+ private listLaneFilesFromDir(tasksDir: string): { name: string }[] {
1702
1689
  if (!fs.existsSync(tasksDir)) return [];
1703
1690
  return fs.readdirSync(tasksDir)
1704
1691
  .filter(f => f.endsWith('.json'))
1705
1692
  .map(f => {
1706
1693
  const filePath = safeJoin(tasksDir, f);
1707
1694
  try {
1708
- const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
1709
- return { name: path.basename(f, '.json'), dependsOn: config.dependsOn || [] };
1695
+ return { name: path.basename(f, '.json') };
1710
1696
  } catch {
1711
- return { name: path.basename(f, '.json'), dependsOn: [] };
1697
+ return { name: path.basename(f, '.json') };
1712
1698
  }
1713
1699
  });
1714
1700
  }
1715
1701
 
1716
- private getLaneStatus(lanePath: string, laneName: string) {
1702
+ private getLaneStatus(lanePath: string, _laneName: string) {
1717
1703
  const statePath = safeJoin(lanePath, 'state.json');
1718
1704
  const state = loadState<LaneState & { chatId?: string }>(statePath);
1719
1705
 
1720
- const laneInfo = this.lanes.find(l => l.name === laneName);
1721
- const dependsOn = state?.dependsOn || laneInfo?.dependsOn || [];
1722
-
1723
1706
  if (!state) {
1724
- return { status: 'pending', currentTask: 0, totalTasks: '?', progress: '0%', dependsOn, duration: 0, pipelineBranch: '-', chatId: '-' };
1707
+ return { status: 'pending', currentTask: 0, totalTasks: '?', progress: '0%', duration: 0, pipelineBranch: '-', chatId: '-' };
1725
1708
  }
1726
1709
 
1727
1710
  const progress = state.totalTasks > 0 ? Math.round((state.currentTaskIndex / state.totalTasks) * 100) : 0;
@@ -1737,7 +1720,6 @@ class InteractiveMonitor {
1737
1720
  progress: `${progress}%`,
1738
1721
  pipelineBranch: state.pipelineBranch || '-',
1739
1722
  chatId: state.chatId || '-',
1740
- dependsOn,
1741
1723
  duration,
1742
1724
  error: state.error,
1743
1725
  pid: state.pid,
@@ -1788,6 +1770,8 @@ function findLatestRunDir(logsDir: string): string | null {
1788
1770
  */
1789
1771
  async function monitor(args: string[]): Promise<void> {
1790
1772
  const help = args.includes('--help') || args.includes('-h');
1773
+ const list = args.includes('--list') || args.includes('-l');
1774
+
1791
1775
  if (help) {
1792
1776
  printHelp();
1793
1777
  return;
@@ -1797,17 +1781,28 @@ async function monitor(args: string[]): Promise<void> {
1797
1781
  const interval = intervalIdx >= 0 ? parseInt(args[intervalIdx + 1] || '2') || 2 : 2;
1798
1782
 
1799
1783
  const runDirArg = args.find(arg => !arg.startsWith('--') && args.indexOf(arg) !== intervalIdx + 1);
1784
+ const originalCwd = process.cwd();
1800
1785
  const config = loadConfig();
1801
1786
 
1802
1787
  let runDir = runDirArg;
1788
+ if (runDir && runDir !== 'latest' && !path.isAbsolute(runDir)) {
1789
+ runDir = path.resolve(originalCwd, runDir);
1790
+ }
1791
+
1803
1792
  if (!runDir || runDir === 'latest') {
1804
- runDir = findLatestRunDir(config.logsDir) || undefined;
1805
- if (!runDir) throw new Error('No run directories found');
1793
+ runDir = findLatestRunDir(getLogsDir(config)) || undefined;
1794
+ if (!runDir && !list) throw new Error('No run directories found');
1795
+ if (!runDir && list) {
1796
+ // Create a dummy runDir if none exists but we want to see the list (dashboard will handle empty list)
1797
+ runDir = path.join(getLogsDir(config), 'runs', 'empty');
1798
+ }
1806
1799
  }
1807
1800
 
1808
- if (!fs.existsSync(runDir)) throw new Error(`Run directory not found: ${runDir}`);
1801
+ if (runDir && !fs.existsSync(runDir) && !list) {
1802
+ throw new Error(`Run directory not found: ${runDir}`);
1803
+ }
1809
1804
 
1810
- const monitor = new InteractiveMonitor(runDir, interval);
1805
+ const monitor = new InteractiveMonitor(runDir!, interval, undefined, list ? View.FLOWS_DASHBOARD : View.LIST);
1811
1806
  await monitor.start();
1812
1807
  }
1813
1808
 
package/src/cli/new.ts ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * CursorFlow 'new' command
3
+ *
4
+ * Creates a new Flow with empty Lane files
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as logger from '../utils/logger';
10
+ import { loadConfig, findProjectRoot } from '../utils/config';
11
+ import { FlowMeta, LaneConfig } from '../types/flow';
12
+ import { safeJoin } from '../utils/path';
13
+ import * as git from '../utils/git';
14
+
15
+ interface NewOptions {
16
+ flowName: string;
17
+ lanes: string[];
18
+ help: boolean;
19
+ }
20
+
21
+ function printHelp(): void {
22
+ console.log(`
23
+ \x1b[1mcursorflow new\x1b[0m - Flow와 Lane 생성
24
+
25
+ \x1b[1m사용법:\x1b[0m
26
+ cursorflow new <FlowName> --lanes "lane1,lane2,..."
27
+
28
+ \x1b[1m설명:\x1b[0m
29
+ 새로운 Flow 디렉토리를 생성하고, 지정된 Lane 파일들의 뼈대를 만듭니다.
30
+ 각 Lane에 실제 Task를 추가하려면 'cursorflow add' 명령을 사용하세요.
31
+
32
+ \x1b[1m옵션:\x1b[0m
33
+ --lanes <names> 콤마로 구분된 레인 이름 목록 (필수)
34
+ 예: --lanes "backend,frontend,mobile"
35
+
36
+ \x1b[1m예시:\x1b[0m
37
+ # 백엔드와 프론트엔드 2개 레인 생성
38
+ cursorflow new ShopFeature --lanes "backend,frontend"
39
+
40
+ # API, Web, Mobile 3개 레인 생성
41
+ cursorflow new SearchFeature --lanes "api,web,mobile"
42
+
43
+ \x1b[1m생성 결과:\x1b[0m
44
+ _cursorflow/flows/001_ShopFeature/
45
+ ├── flow.meta.json # Flow 메타데이터
46
+ ├── 01-backend.json # Lane 1 (빈 상태)
47
+ └── 02-frontend.json # Lane 2 (빈 상태)
48
+ `);
49
+ }
50
+
51
+ function parseArgs(args: string[]): NewOptions {
52
+ const result: NewOptions = {
53
+ flowName: '',
54
+ lanes: [],
55
+ help: false,
56
+ };
57
+
58
+ let i = 0;
59
+ while (i < args.length) {
60
+ const arg = args[i];
61
+
62
+ if (arg === '--help' || arg === '-h') {
63
+ result.help = true;
64
+ } else if (arg === '--lanes' && args[i + 1]) {
65
+ result.lanes = args[++i].split(',').map(l => l.trim()).filter(l => l);
66
+ } else if (!arg.startsWith('--') && !result.flowName) {
67
+ result.flowName = arg;
68
+ }
69
+
70
+ i++;
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ /**
77
+ * Get next flow ID by scanning existing flows
78
+ */
79
+ function getNextFlowId(flowsDir: string): string {
80
+ if (!fs.existsSync(flowsDir)) {
81
+ return '001';
82
+ }
83
+
84
+ const dirs = fs.readdirSync(flowsDir)
85
+ .filter(name => {
86
+ const dirPath = safeJoin(flowsDir, name);
87
+ return fs.statSync(dirPath).isDirectory();
88
+ })
89
+ .filter(name => /^\d+_/.test(name));
90
+
91
+ if (dirs.length === 0) {
92
+ return '001';
93
+ }
94
+
95
+ const maxId = Math.max(...dirs.map(name => {
96
+ const match = name.match(/^(\d+)_/);
97
+ return match ? parseInt(match[1], 10) : 0;
98
+ }));
99
+
100
+ return String(maxId + 1).padStart(3, '0');
101
+ }
102
+
103
+ /**
104
+ * Create flow.meta.json
105
+ */
106
+ function createFlowMeta(flowId: string, flowName: string, lanes: string[], baseBranch: string): FlowMeta {
107
+ return {
108
+ id: flowId,
109
+ name: flowName,
110
+ createdAt: new Date().toISOString(),
111
+ createdBy: 'user',
112
+ baseBranch,
113
+ status: 'pending',
114
+ lanes,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Create empty lane config
120
+ */
121
+ function createEmptyLaneConfig(laneName: string): LaneConfig {
122
+ return {
123
+ laneName,
124
+ tasks: [],
125
+ };
126
+ }
127
+
128
+ async function newFlow(args: string[]): Promise<void> {
129
+ const options = parseArgs(args);
130
+
131
+ if (options.help) {
132
+ printHelp();
133
+ return;
134
+ }
135
+
136
+ // Validate inputs
137
+ if (!options.flowName) {
138
+ logger.error('Flow 이름이 필요합니다.');
139
+ console.log('\n사용법: cursorflow new <FlowName> --lanes "lane1,lane2"');
140
+ console.log('도움말: cursorflow new --help');
141
+ process.exit(1);
142
+ }
143
+
144
+ if (options.lanes.length === 0) {
145
+ logger.error('최소 하나의 레인이 필요합니다.');
146
+ console.log('\n예: cursorflow new ' + options.flowName + ' --lanes "backend,frontend"');
147
+ process.exit(1);
148
+ }
149
+
150
+ // Validate lane names (alphanumeric, dash, underscore only)
151
+ const invalidLanes = options.lanes.filter(l => !/^[a-zA-Z0-9_-]+$/.test(l));
152
+ if (invalidLanes.length > 0) {
153
+ logger.error(`잘못된 레인 이름: ${invalidLanes.join(', ')}`);
154
+ console.log('레인 이름은 영문, 숫자, 대시(-), 언더스코어(_)만 사용 가능합니다.');
155
+ process.exit(1);
156
+ }
157
+
158
+ // Check for duplicate lane names
159
+ const uniqueLanes = new Set(options.lanes);
160
+ if (uniqueLanes.size !== options.lanes.length) {
161
+ logger.error('중복된 레인 이름이 있습니다.');
162
+ process.exit(1);
163
+ }
164
+
165
+ // Load config and determine paths
166
+ const projectRoot = findProjectRoot();
167
+ const config = loadConfig(projectRoot);
168
+ const flowsDir = safeJoin(projectRoot, config.flowsDir);
169
+
170
+ // Ensure flows directory exists
171
+ if (!fs.existsSync(flowsDir)) {
172
+ fs.mkdirSync(flowsDir, { recursive: true });
173
+ }
174
+
175
+ // Get next flow ID
176
+ const flowId = getNextFlowId(flowsDir);
177
+ const flowDirName = `${flowId}_${options.flowName}`;
178
+ const flowDir = safeJoin(flowsDir, flowDirName);
179
+
180
+ // Check if flow already exists
181
+ if (fs.existsSync(flowDir)) {
182
+ logger.error(`Flow 디렉토리가 이미 존재합니다: ${flowDirName}`);
183
+ process.exit(1);
184
+ }
185
+
186
+ // Get current branch as base branch
187
+ let baseBranch = 'main';
188
+ try {
189
+ baseBranch = git.getCurrentBranch(projectRoot);
190
+ } catch {
191
+ logger.warn('현재 브랜치를 가져올 수 없어 main을 기본값으로 사용합니다.');
192
+ }
193
+
194
+ // Create flow directory
195
+ fs.mkdirSync(flowDir, { recursive: true });
196
+
197
+ // Create flow.meta.json
198
+ const flowMeta = createFlowMeta(flowId, options.flowName, options.lanes, baseBranch);
199
+ const metaPath = safeJoin(flowDir, 'flow.meta.json');
200
+ fs.writeFileSync(metaPath, JSON.stringify(flowMeta, null, 2));
201
+
202
+ // Create lane files
203
+ options.lanes.forEach((laneName, index) => {
204
+ const laneNumber = String(index + 1).padStart(2, '0');
205
+ const laneFileName = `${laneNumber}-${laneName}.json`;
206
+ const lanePath = safeJoin(flowDir, laneFileName);
207
+
208
+ const laneConfig = createEmptyLaneConfig(laneName);
209
+ fs.writeFileSync(lanePath, JSON.stringify(laneConfig, null, 2));
210
+ });
211
+
212
+ // Print success message
213
+ logger.section(`✅ Flow 생성 완료: ${flowDirName}`);
214
+ console.log('');
215
+ console.log(` 📁 ${flowDir}`);
216
+ console.log(` ├── flow.meta.json`);
217
+ options.lanes.forEach((laneName, index) => {
218
+ const laneNumber = String(index + 1).padStart(2, '0');
219
+ const isLast = index === options.lanes.length - 1;
220
+ const prefix = isLast ? '└──' : '├──';
221
+ console.log(` ${prefix} ${laneNumber}-${laneName}.json (빈 상태)`);
222
+ });
223
+
224
+ console.log('');
225
+ logger.info('다음 단계: 각 레인에 태스크를 추가하세요.');
226
+ console.log('');
227
+ options.lanes.forEach((laneName) => {
228
+ console.log(` cursorflow add ${options.flowName} ${laneName} \\`);
229
+ console.log(` --task "name=implement|model=sonnet-4.5|prompt=..."`);
230
+ console.log('');
231
+ });
232
+ }
233
+
234
+ export = newFlow;
235
+