@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.
- package/CHANGELOG.md +27 -0
- package/README.md +182 -59
- package/commands/cursorflow-add.md +159 -0
- package/commands/cursorflow-doctor.md +45 -23
- package/commands/cursorflow-monitor.md +23 -2
- package/commands/cursorflow-new.md +87 -0
- package/commands/cursorflow-run.md +60 -111
- package/dist/cli/add.d.ts +7 -0
- package/dist/cli/add.js +377 -0
- package/dist/cli/add.js.map +1 -0
- package/dist/cli/clean.js +1 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/config.d.ts +7 -0
- package/dist/cli/config.js +181 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/doctor.js +47 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +34 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +17 -34
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +62 -65
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/new.d.ts +7 -0
- package/dist/cli/new.js +232 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/prepare.js +95 -193
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +57 -68
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +60 -30
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/stop.js +6 -0
- package/dist/cli/stop.js.map +1 -1
- package/dist/cli/tasks.d.ts +5 -3
- package/dist/cli/tasks.js +181 -29
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/failure-policy.d.ts +9 -0
- package/dist/core/failure-policy.js +9 -0
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/orchestrator.d.ts +20 -6
- package/dist/core/orchestrator.js +215 -334
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +27 -0
- package/dist/core/runner/agent.js +294 -0
- package/dist/core/runner/agent.js.map +1 -0
- package/dist/core/runner/index.d.ts +5 -0
- package/dist/core/runner/index.js +22 -0
- package/dist/core/runner/index.js.map +1 -0
- package/dist/core/runner/pipeline.d.ts +9 -0
- package/dist/core/runner/pipeline.js +539 -0
- package/dist/core/runner/pipeline.js.map +1 -0
- package/dist/core/runner/prompt.d.ts +25 -0
- package/dist/core/runner/prompt.js +175 -0
- package/dist/core/runner/prompt.js.map +1 -0
- package/dist/core/runner/task.d.ts +26 -0
- package/dist/core/runner/task.js +283 -0
- package/dist/core/runner/task.js.map +1 -0
- package/dist/core/runner/utils.d.ts +37 -0
- package/dist/core/runner/utils.js +161 -0
- package/dist/core/runner/utils.js.map +1 -0
- package/dist/core/runner.d.ts +2 -96
- package/dist/core/runner.js +11 -1136
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +326 -0
- package/dist/core/stall-detection.js +781 -0
- package/dist/core/stall-detection.js.map +1 -0
- package/dist/services/logging/console.js +2 -1
- package/dist/services/logging/console.js.map +1 -1
- package/dist/types/config.d.ts +6 -6
- package/dist/types/flow.d.ts +84 -0
- package/dist/types/flow.js +10 -0
- package/dist/types/flow.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/lane.d.ts +0 -2
- package/dist/types/logging.d.ts +5 -1
- package/dist/types/task.d.ts +7 -11
- package/dist/utils/config.d.ts +5 -1
- package/dist/utils/config.js +15 -16
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/dependency.d.ts +36 -1
- package/dist/utils/dependency.js +256 -1
- package/dist/utils/dependency.js.map +1 -1
- package/dist/utils/doctor.js +40 -8
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +45 -82
- package/dist/utils/enhanced-logger.js +239 -844
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/flow.d.ts +9 -0
- package/dist/utils/flow.js +73 -0
- package/dist/utils/flow.js.map +1 -0
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +115 -5
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/state.js +0 -2
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +2 -2
- package/dist/utils/task-service.js +40 -31
- package/dist/utils/task-service.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/add.ts +397 -0
- package/src/cli/clean.ts +1 -0
- package/src/cli/config.ts +177 -0
- package/src/cli/doctor.ts +48 -4
- package/src/cli/index.ts +36 -32
- package/src/cli/logs.ts +20 -33
- package/src/cli/monitor.ts +70 -75
- package/src/cli/new.ts +235 -0
- package/src/cli/prepare.ts +98 -205
- package/src/cli/resume.ts +61 -76
- package/src/cli/run.ts +333 -306
- package/src/cli/stop.ts +8 -0
- package/src/cli/tasks.ts +200 -21
- package/src/core/failure-policy.ts +9 -0
- package/src/core/orchestrator.ts +279 -379
- package/src/core/runner/agent.ts +314 -0
- package/src/core/runner/index.ts +6 -0
- package/src/core/runner/pipeline.ts +567 -0
- package/src/core/runner/prompt.ts +174 -0
- package/src/core/runner/task.ts +320 -0
- package/src/core/runner/utils.ts +142 -0
- package/src/core/runner.ts +8 -1347
- package/src/core/stall-detection.ts +936 -0
- package/src/services/logging/console.ts +2 -1
- package/src/types/config.ts +6 -6
- package/src/types/flow.ts +91 -0
- package/src/types/index.ts +15 -3
- package/src/types/lane.ts +0 -2
- package/src/types/logging.ts +5 -1
- package/src/types/task.ts +7 -11
- package/src/utils/config.ts +16 -17
- package/src/utils/dependency.ts +311 -2
- package/src/utils/doctor.ts +36 -8
- package/src/utils/enhanced-logger.ts +264 -927
- package/src/utils/flow.ts +42 -0
- package/src/utils/git.ts +145 -5
- package/src/utils/state.ts +0 -2
- package/src/utils/task-service.ts +48 -40
- package/commands/cursorflow-review.md +0 -56
- package/commands/cursorflow-runs.md +0 -59
- package/dist/cli/runs.d.ts +0 -5
- package/dist/cli/runs.js +0 -214
- package/dist/cli/runs.js.map +0 -1
- package/dist/core/reviewer.d.ts +0 -66
- package/dist/core/reviewer.js +0 -265
- package/dist/core/reviewer.js.map +0 -1
- package/src/cli/runs.ts +0 -212
- package/src/core/reviewer.ts +0 -285
package/src/cli/monitor.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1247
|
-
if (status?.
|
|
1248
|
-
process.stdout.write(` ${UI.COLORS.dim}←${UI.COLORS.reset} ${UI.COLORS.yellow}${status.
|
|
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}
|
|
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
|
|
1287
|
+
* Calculate levels for visualization (all lanes run in parallel now)
|
|
1266
1288
|
*/
|
|
1267
1289
|
private calculateDependencyLevels(): string[][] {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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')
|
|
1697
|
+
return { name: path.basename(f, '.json') };
|
|
1712
1698
|
}
|
|
1713
1699
|
});
|
|
1714
1700
|
}
|
|
1715
1701
|
|
|
1716
|
-
private getLaneStatus(lanePath: 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%',
|
|
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
|
|
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)
|
|
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,
|
|
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
|
+
|