@litmers/cursorflow-orchestrator 0.1.18 → 0.1.20
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 +16 -0
- package/README.md +25 -7
- package/dist/cli/clean.js +7 -6
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +7 -6
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +50 -42
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +15 -14
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +37 -20
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +193 -40
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +3 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +7 -7
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +2 -1
- package/dist/core/orchestrator.js +48 -91
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner.js +55 -20
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +7 -6
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +7 -6
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.js +14 -11
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.js +163 -10
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/log-formatter.d.ts +16 -0
- package/dist/utils/log-formatter.js +194 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/state.d.ts +4 -1
- package/dist/utils/state.js +11 -8
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/cli/clean.ts +7 -6
- package/src/cli/index.ts +5 -1
- package/src/cli/init.ts +7 -6
- package/src/cli/logs.ts +52 -42
- package/src/cli/monitor.ts +15 -14
- package/src/cli/prepare.ts +39 -20
- package/src/cli/resume.ts +810 -626
- package/src/cli/run.ts +3 -2
- package/src/cli/signal.ts +7 -6
- package/src/core/orchestrator.ts +62 -91
- package/src/core/runner.ts +58 -20
- package/src/utils/config.ts +7 -6
- package/src/utils/doctor.ts +7 -6
- package/src/utils/enhanced-logger.ts +14 -11
- package/src/utils/git.ts +145 -11
- package/src/utils/log-formatter.ts +162 -0
- package/src/utils/path.ts +45 -0
- package/src/utils/state.ts +16 -8
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +1 -0
- package/templates/basic.json +21 -0
package/src/cli/logs.ts
CHANGED
|
@@ -6,12 +6,14 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as logger from '../utils/logger';
|
|
8
8
|
import { loadConfig } from '../utils/config';
|
|
9
|
+
import { safeJoin } from '../utils/path';
|
|
9
10
|
import {
|
|
10
11
|
readJsonLog,
|
|
11
12
|
exportLogs,
|
|
12
13
|
stripAnsi,
|
|
13
14
|
JsonLogEntry
|
|
14
15
|
} from '../utils/enhanced-logger';
|
|
16
|
+
import { formatPotentialJsonMessage } from '../utils/log-formatter';
|
|
15
17
|
|
|
16
18
|
interface LogsOptions {
|
|
17
19
|
runDir?: string;
|
|
@@ -114,15 +116,15 @@ function parseArgs(args: string[]): LogsOptions {
|
|
|
114
116
|
* Find the latest run directory
|
|
115
117
|
*/
|
|
116
118
|
function findLatestRunDir(logsDir: string): string | null {
|
|
117
|
-
const runsDir =
|
|
119
|
+
const runsDir = safeJoin(logsDir, 'runs');
|
|
118
120
|
if (!fs.existsSync(runsDir)) return null;
|
|
119
121
|
|
|
120
122
|
const runs = fs.readdirSync(runsDir)
|
|
121
123
|
.filter(d => d.startsWith('run-'))
|
|
122
124
|
.map(d => ({
|
|
123
125
|
name: d,
|
|
124
|
-
path:
|
|
125
|
-
mtime: fs.statSync(
|
|
126
|
+
path: safeJoin(runsDir, d),
|
|
127
|
+
mtime: fs.statSync(safeJoin(runsDir, d)).mtime.getTime()
|
|
126
128
|
}))
|
|
127
129
|
.sort((a, b) => b.mtime - a.mtime);
|
|
128
130
|
|
|
@@ -133,11 +135,11 @@ function findLatestRunDir(logsDir: string): string | null {
|
|
|
133
135
|
* List lanes in a run directory
|
|
134
136
|
*/
|
|
135
137
|
function listLanes(runDir: string): string[] {
|
|
136
|
-
const lanesDir =
|
|
138
|
+
const lanesDir = safeJoin(runDir, 'lanes');
|
|
137
139
|
if (!fs.existsSync(lanesDir)) return [];
|
|
138
140
|
|
|
139
141
|
return fs.readdirSync(lanesDir)
|
|
140
|
-
.filter(d => fs.statSync(
|
|
142
|
+
.filter(d => fs.statSync(safeJoin(lanesDir, d)).isDirectory());
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
/**
|
|
@@ -148,9 +150,9 @@ function displayTextLogs(
|
|
|
148
150
|
options: LogsOptions
|
|
149
151
|
): void {
|
|
150
152
|
let logFile: string;
|
|
151
|
-
const readableLog =
|
|
152
|
-
const rawLog =
|
|
153
|
-
const cleanLog =
|
|
153
|
+
const readableLog = safeJoin(laneDir, 'terminal-readable.log');
|
|
154
|
+
const rawLog = safeJoin(laneDir, 'terminal-raw.log');
|
|
155
|
+
const cleanLog = safeJoin(laneDir, 'terminal.log');
|
|
154
156
|
|
|
155
157
|
if (options.raw) {
|
|
156
158
|
logFile = rawLog;
|
|
@@ -171,10 +173,10 @@ function displayTextLogs(
|
|
|
171
173
|
let content = fs.readFileSync(logFile, 'utf8');
|
|
172
174
|
let lines = content.split('\n');
|
|
173
175
|
|
|
174
|
-
// Apply filter (
|
|
176
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
175
177
|
if (options.filter) {
|
|
176
|
-
const
|
|
177
|
-
lines = lines.filter(line =>
|
|
178
|
+
const filterLower = options.filter.toLowerCase();
|
|
179
|
+
lines = lines.filter(line => line.toLowerCase().includes(filterLower));
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
// Apply tail
|
|
@@ -197,7 +199,7 @@ function displayJsonLogs(
|
|
|
197
199
|
laneDir: string,
|
|
198
200
|
options: LogsOptions
|
|
199
201
|
): void {
|
|
200
|
-
const logFile =
|
|
202
|
+
const logFile = safeJoin(laneDir, 'terminal.jsonl');
|
|
201
203
|
|
|
202
204
|
if (!fs.existsSync(logFile)) {
|
|
203
205
|
console.log('No JSON log file found.');
|
|
@@ -211,10 +213,13 @@ function displayJsonLogs(
|
|
|
211
213
|
entries = entries.filter(e => e.level === options.level);
|
|
212
214
|
}
|
|
213
215
|
|
|
214
|
-
// Apply
|
|
216
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
215
217
|
if (options.filter) {
|
|
216
|
-
const
|
|
217
|
-
entries = entries.filter(e =>
|
|
218
|
+
const filterLower = options.filter.toLowerCase();
|
|
219
|
+
entries = entries.filter(e =>
|
|
220
|
+
e.message.toLowerCase().includes(filterLower) ||
|
|
221
|
+
(e.task && e.task.toLowerCase().includes(filterLower))
|
|
222
|
+
);
|
|
218
223
|
}
|
|
219
224
|
|
|
220
225
|
// Apply tail
|
|
@@ -229,7 +234,8 @@ function displayJsonLogs(
|
|
|
229
234
|
for (const entry of entries) {
|
|
230
235
|
const levelColor = getLevelColor(entry.level);
|
|
231
236
|
const ts = new Date(entry.timestamp).toLocaleTimeString();
|
|
232
|
-
|
|
237
|
+
const formattedMsg = formatPotentialJsonMessage(entry.message);
|
|
238
|
+
console.log(`${levelColor}[${ts}] [${entry.level.toUpperCase().padEnd(6)}]${logger.COLORS.reset} ${formattedMsg}`);
|
|
233
239
|
}
|
|
234
240
|
}
|
|
235
241
|
}
|
|
@@ -290,8 +296,8 @@ function readAllLaneLogs(runDir: string): MergedLogEntry[] {
|
|
|
290
296
|
const allEntries: MergedLogEntry[] = [];
|
|
291
297
|
|
|
292
298
|
lanes.forEach((laneName, index) => {
|
|
293
|
-
const laneDir =
|
|
294
|
-
const jsonLogPath =
|
|
299
|
+
const laneDir = safeJoin(runDir, 'lanes', laneName);
|
|
300
|
+
const jsonLogPath = safeJoin(laneDir, 'terminal.jsonl');
|
|
295
301
|
|
|
296
302
|
if (fs.existsSync(jsonLogPath)) {
|
|
297
303
|
const entries = readJsonLog(jsonLogPath);
|
|
@@ -333,13 +339,13 @@ function displayMergedLogs(runDir: string, options: LogsOptions): void {
|
|
|
333
339
|
entries = entries.filter(e => e.level === options.level);
|
|
334
340
|
}
|
|
335
341
|
|
|
336
|
-
// Apply
|
|
342
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
337
343
|
if (options.filter) {
|
|
338
|
-
const
|
|
344
|
+
const filterLower = options.filter.toLowerCase();
|
|
339
345
|
entries = entries.filter(e =>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
346
|
+
e.message.toLowerCase().includes(filterLower) ||
|
|
347
|
+
(e.task && e.task.toLowerCase().includes(filterLower)) ||
|
|
348
|
+
e.laneName.toLowerCase().includes(filterLower)
|
|
343
349
|
);
|
|
344
350
|
}
|
|
345
351
|
|
|
@@ -387,7 +393,8 @@ function displayMergedLogs(runDir: string, options: LogsOptions): void {
|
|
|
387
393
|
continue;
|
|
388
394
|
}
|
|
389
395
|
|
|
390
|
-
|
|
396
|
+
const formattedMsg = formatPotentialJsonMessage(entry.message);
|
|
397
|
+
console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${laneColor}[${lanePad}]${logger.COLORS.reset} ${levelColor}[${levelPad}]${logger.COLORS.reset} ${formattedMsg}`);
|
|
391
398
|
}
|
|
392
399
|
|
|
393
400
|
console.log(`\n${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
|
|
@@ -426,8 +433,8 @@ function followAllLogs(runDir: string, options: LogsOptions): void {
|
|
|
426
433
|
const newEntries: MergedLogEntry[] = [];
|
|
427
434
|
|
|
428
435
|
for (const lane of lanes) {
|
|
429
|
-
const laneDir =
|
|
430
|
-
const jsonLogPath =
|
|
436
|
+
const laneDir = safeJoin(runDir, 'lanes', lane);
|
|
437
|
+
const jsonLogPath = safeJoin(laneDir, 'terminal.jsonl');
|
|
431
438
|
|
|
432
439
|
try {
|
|
433
440
|
// Use statSync directly to avoid TOCTOU race condition
|
|
@@ -473,10 +480,12 @@ function followAllLogs(runDir: string, options: LogsOptions): void {
|
|
|
473
480
|
// Apply level filter
|
|
474
481
|
if (options.level && entry.level !== options.level) continue;
|
|
475
482
|
|
|
476
|
-
// Apply
|
|
483
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
477
484
|
if (options.filter) {
|
|
478
|
-
const
|
|
479
|
-
if (!
|
|
485
|
+
const filterLower = options.filter.toLowerCase();
|
|
486
|
+
if (!entry.message.toLowerCase().includes(filterLower) &&
|
|
487
|
+
!(entry.task && entry.task.toLowerCase().includes(filterLower)) &&
|
|
488
|
+
!entry.laneName.toLowerCase().includes(filterLower)) {
|
|
480
489
|
continue;
|
|
481
490
|
}
|
|
482
491
|
}
|
|
@@ -496,7 +505,8 @@ function followAllLogs(runDir: string, options: LogsOptions): void {
|
|
|
496
505
|
continue;
|
|
497
506
|
}
|
|
498
507
|
|
|
499
|
-
|
|
508
|
+
const formattedMsg = formatPotentialJsonMessage(entry.message);
|
|
509
|
+
console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${entry.laneColor}[${lanePad}]${logger.COLORS.reset} ${levelColor}[${levelPad}]${logger.COLORS.reset} ${formattedMsg}`);
|
|
500
510
|
}
|
|
501
511
|
}, 100);
|
|
502
512
|
|
|
@@ -644,9 +654,9 @@ function escapeHtml(text: string): string {
|
|
|
644
654
|
*/
|
|
645
655
|
function followLogs(laneDir: string, options: LogsOptions): void {
|
|
646
656
|
let logFile: string;
|
|
647
|
-
const readableLog =
|
|
648
|
-
const rawLog =
|
|
649
|
-
const cleanLog =
|
|
657
|
+
const readableLog = safeJoin(laneDir, 'terminal-readable.log');
|
|
658
|
+
const rawLog = safeJoin(laneDir, 'terminal-raw.log');
|
|
659
|
+
const cleanLog = safeJoin(laneDir, 'terminal.log');
|
|
650
660
|
|
|
651
661
|
if (options.raw) {
|
|
652
662
|
logFile = rawLog;
|
|
@@ -687,11 +697,11 @@ function followLogs(laneDir: string, options: LogsOptions): void {
|
|
|
687
697
|
|
|
688
698
|
let content = buffer.toString();
|
|
689
699
|
|
|
690
|
-
// Apply filter (
|
|
700
|
+
// Apply filter (case-insensitive string match to avoid ReDoS)
|
|
691
701
|
if (options.filter) {
|
|
692
|
-
const
|
|
702
|
+
const filterLower = options.filter.toLowerCase();
|
|
693
703
|
const lines = content.split('\n');
|
|
694
|
-
content = lines.filter(line =>
|
|
704
|
+
content = lines.filter(line => line.toLowerCase().includes(filterLower)).join('\n');
|
|
695
705
|
}
|
|
696
706
|
|
|
697
707
|
// Clean ANSI if needed (unless raw mode)
|
|
@@ -734,11 +744,11 @@ function displaySummary(runDir: string): void {
|
|
|
734
744
|
}
|
|
735
745
|
|
|
736
746
|
for (const lane of lanes) {
|
|
737
|
-
const laneDir =
|
|
738
|
-
const cleanLog =
|
|
739
|
-
const rawLog =
|
|
740
|
-
const jsonLog =
|
|
741
|
-
const readableLog =
|
|
747
|
+
const laneDir = safeJoin(runDir, 'lanes', lane);
|
|
748
|
+
const cleanLog = safeJoin(laneDir, 'terminal.log');
|
|
749
|
+
const rawLog = safeJoin(laneDir, 'terminal-raw.log');
|
|
750
|
+
const jsonLog = safeJoin(laneDir, 'terminal.jsonl');
|
|
751
|
+
const readableLog = safeJoin(laneDir, 'terminal-readable.log');
|
|
742
752
|
|
|
743
753
|
console.log(` ${logger.COLORS.green}📁 ${lane}${logger.COLORS.reset}`);
|
|
744
754
|
|
|
@@ -839,7 +849,7 @@ async function logs(args: string[]): Promise<void> {
|
|
|
839
849
|
}
|
|
840
850
|
|
|
841
851
|
// Find lane directory
|
|
842
|
-
const laneDir =
|
|
852
|
+
const laneDir = safeJoin(runDir, 'lanes', options.lane);
|
|
843
853
|
if (!fs.existsSync(laneDir)) {
|
|
844
854
|
const lanes = listLanes(runDir);
|
|
845
855
|
throw new Error(`Lane not found: ${options.lane}\nAvailable lanes: ${lanes.join(', ')}`);
|
package/src/cli/monitor.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as logger from '../utils/logger';
|
|
|
9
9
|
import { loadState, readLog } from '../utils/state';
|
|
10
10
|
import { LaneState, ConversationEntry } from '../utils/types';
|
|
11
11
|
import { loadConfig } from '../utils/config';
|
|
12
|
+
import { safeJoin } from '../utils/path';
|
|
12
13
|
|
|
13
14
|
interface LaneWithDeps {
|
|
14
15
|
name: string;
|
|
@@ -339,11 +340,11 @@ class InteractiveMonitor {
|
|
|
339
340
|
if (!lane) return;
|
|
340
341
|
|
|
341
342
|
try {
|
|
342
|
-
const interventionPath =
|
|
343
|
+
const interventionPath = safeJoin(lane.path, 'intervention.txt');
|
|
343
344
|
fs.writeFileSync(interventionPath, message, 'utf8');
|
|
344
345
|
|
|
345
346
|
// Also log it to the conversation
|
|
346
|
-
const convoPath =
|
|
347
|
+
const convoPath = safeJoin(lane.path, 'conversation.jsonl');
|
|
347
348
|
const entry = {
|
|
348
349
|
timestamp: new Date().toISOString(),
|
|
349
350
|
role: 'user',
|
|
@@ -372,7 +373,7 @@ class InteractiveMonitor {
|
|
|
372
373
|
return;
|
|
373
374
|
}
|
|
374
375
|
|
|
375
|
-
const timeoutPath =
|
|
376
|
+
const timeoutPath = safeJoin(lane.path, 'timeout.txt');
|
|
376
377
|
fs.writeFileSync(timeoutPath, String(timeoutMs), 'utf8');
|
|
377
378
|
|
|
378
379
|
this.showNotification(`Timeout updated to ${Math.round(timeoutMs/1000)}s`, 'success');
|
|
@@ -385,7 +386,7 @@ class InteractiveMonitor {
|
|
|
385
386
|
if (!this.selectedLaneName) return;
|
|
386
387
|
const lane = this.lanes.find(l => l.name === this.selectedLaneName);
|
|
387
388
|
if (!lane) return;
|
|
388
|
-
const convoPath =
|
|
389
|
+
const convoPath = safeJoin(lane.path, 'conversation.jsonl');
|
|
389
390
|
this.currentLogs = readLog<ConversationEntry>(convoPath);
|
|
390
391
|
// Keep selection in bounds after refresh
|
|
391
392
|
if (this.selectedMessageIndex >= this.currentLogs.length) {
|
|
@@ -537,7 +538,7 @@ class InteractiveMonitor {
|
|
|
537
538
|
}
|
|
538
539
|
|
|
539
540
|
const status = this.getLaneStatus(lane.path, lane.name);
|
|
540
|
-
const logPath =
|
|
541
|
+
const logPath = safeJoin(lane.path, 'terminal.log');
|
|
541
542
|
let liveLog = '(No live terminal output)';
|
|
542
543
|
if (fs.existsSync(logPath)) {
|
|
543
544
|
const content = fs.readFileSync(logPath, 'utf8');
|
|
@@ -680,7 +681,7 @@ class InteractiveMonitor {
|
|
|
680
681
|
return;
|
|
681
682
|
}
|
|
682
683
|
|
|
683
|
-
const logPath =
|
|
684
|
+
const logPath = safeJoin(lane.path, 'terminal.log');
|
|
684
685
|
let logLines: string[] = [];
|
|
685
686
|
if (fs.existsSync(logPath)) {
|
|
686
687
|
const content = fs.readFileSync(logPath, 'utf8');
|
|
@@ -761,21 +762,21 @@ class InteractiveMonitor {
|
|
|
761
762
|
}
|
|
762
763
|
|
|
763
764
|
private listLanesWithDeps(runDir: string): LaneWithDeps[] {
|
|
764
|
-
const lanesDir =
|
|
765
|
+
const lanesDir = safeJoin(runDir, 'lanes');
|
|
765
766
|
if (!fs.existsSync(lanesDir)) return [];
|
|
766
767
|
|
|
767
768
|
const config = loadConfig();
|
|
768
|
-
const tasksDir =
|
|
769
|
+
const tasksDir = safeJoin(config.projectRoot, config.tasksDir);
|
|
769
770
|
|
|
770
771
|
const laneConfigs = this.listLaneFilesFromDir(tasksDir);
|
|
771
772
|
|
|
772
773
|
return fs.readdirSync(lanesDir)
|
|
773
|
-
.filter(d => fs.statSync(
|
|
774
|
+
.filter(d => fs.statSync(safeJoin(lanesDir, d)).isDirectory())
|
|
774
775
|
.map(name => {
|
|
775
776
|
const config = laneConfigs.find(c => c.name === name);
|
|
776
777
|
return {
|
|
777
778
|
name,
|
|
778
|
-
path:
|
|
779
|
+
path: safeJoin(lanesDir, name),
|
|
779
780
|
dependsOn: config?.dependsOn || [],
|
|
780
781
|
};
|
|
781
782
|
});
|
|
@@ -786,7 +787,7 @@ class InteractiveMonitor {
|
|
|
786
787
|
return fs.readdirSync(tasksDir)
|
|
787
788
|
.filter(f => f.endsWith('.json'))
|
|
788
789
|
.map(f => {
|
|
789
|
-
const filePath =
|
|
790
|
+
const filePath = safeJoin(tasksDir, f);
|
|
790
791
|
try {
|
|
791
792
|
const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
792
793
|
return { name: path.basename(f, '.json'), dependsOn: config.dependsOn || [] };
|
|
@@ -797,7 +798,7 @@ class InteractiveMonitor {
|
|
|
797
798
|
}
|
|
798
799
|
|
|
799
800
|
private getLaneStatus(lanePath: string, laneName: string) {
|
|
800
|
-
const statePath =
|
|
801
|
+
const statePath = safeJoin(lanePath, 'state.json');
|
|
801
802
|
const state = loadState<LaneState & { chatId?: string }>(statePath);
|
|
802
803
|
|
|
803
804
|
const laneInfo = this.lanes.find(l => l.name === laneName);
|
|
@@ -857,11 +858,11 @@ class InteractiveMonitor {
|
|
|
857
858
|
* Find the latest run directory
|
|
858
859
|
*/
|
|
859
860
|
function findLatestRunDir(logsDir: string): string | null {
|
|
860
|
-
const runsDir =
|
|
861
|
+
const runsDir = safeJoin(logsDir, 'runs');
|
|
861
862
|
if (!fs.existsSync(runsDir)) return null;
|
|
862
863
|
const runs = fs.readdirSync(runsDir)
|
|
863
864
|
.filter(d => d.startsWith('run-'))
|
|
864
|
-
.map(d => ({ name: d, path:
|
|
865
|
+
.map(d => ({ name: d, path: safeJoin(runsDir, d), mtime: fs.statSync(safeJoin(runsDir, d)).mtime.getTime() }))
|
|
865
866
|
.sort((a, b) => b.mtime - a.mtime);
|
|
866
867
|
return runs.length > 0 ? runs[0]!.path : null;
|
|
867
868
|
}
|
package/src/cli/prepare.ts
CHANGED
|
@@ -9,6 +9,8 @@ import * as path from 'path';
|
|
|
9
9
|
import * as logger from '../utils/logger';
|
|
10
10
|
import { loadConfig, getTasksDir } from '../utils/config';
|
|
11
11
|
import { Task, RunnerConfig } from '../utils/types';
|
|
12
|
+
import { safeJoin } from '../utils/path';
|
|
13
|
+
import { resolveTemplate } from '../utils/template';
|
|
12
14
|
|
|
13
15
|
// Preset template types
|
|
14
16
|
type PresetType = 'complex' | 'simple' | 'merge';
|
|
@@ -113,8 +115,8 @@ Prepare task files for a new feature - Terminal-first workflow.
|
|
|
113
115
|
--add-task <file> Append task(s) to existing lane JSON file
|
|
114
116
|
|
|
115
117
|
Advanced:
|
|
116
|
-
--template <path>
|
|
117
|
-
--force
|
|
118
|
+
--template <path|url|name> External template JSON file, URL, or built-in name
|
|
119
|
+
--force Overwrite existing files
|
|
118
120
|
|
|
119
121
|
═══════════════════════════════════════════════════════════════════════════════
|
|
120
122
|
|
|
@@ -612,7 +614,7 @@ function getFeatureNameFromDir(taskDir: string): string {
|
|
|
612
614
|
}
|
|
613
615
|
|
|
614
616
|
async function addLaneToDir(options: PrepareOptions): Promise<void> {
|
|
615
|
-
const taskDir = path.resolve(process.cwd(), options.addLane!);
|
|
617
|
+
const taskDir = path.resolve(process.cwd(), options.addLane!); // nosemgrep
|
|
616
618
|
|
|
617
619
|
if (!fs.existsSync(taskDir)) {
|
|
618
620
|
throw new Error(`Task directory not found: ${taskDir}`);
|
|
@@ -622,17 +624,38 @@ async function addLaneToDir(options: PrepareOptions): Promise<void> {
|
|
|
622
624
|
const laneNumber = getNextLaneNumber(taskDir);
|
|
623
625
|
const laneName = `lane-${laneNumber}`;
|
|
624
626
|
const fileName = `${laneNumber.toString().padStart(2, '0')}-${laneName}.json`;
|
|
625
|
-
const filePath =
|
|
627
|
+
const filePath = safeJoin(taskDir, fileName);
|
|
626
628
|
|
|
627
629
|
const hasDependencies = options.dependsOnLanes.length > 0;
|
|
628
630
|
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
631
|
+
// Load template if provided
|
|
632
|
+
let template = null;
|
|
633
|
+
if (options.template) {
|
|
634
|
+
template = await resolveTemplate(options.template);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let taskConfig;
|
|
638
|
+
let effectivePreset: EffectivePresetType = options.preset || (hasDependencies ? 'merge' : 'complex');
|
|
639
|
+
|
|
640
|
+
if (template) {
|
|
641
|
+
taskConfig = { ...template, laneNumber, devPort: 3000 + laneNumber };
|
|
642
|
+
effectivePreset = 'custom';
|
|
643
|
+
} else {
|
|
644
|
+
// Build tasks from options (auto-detects merge preset if has dependencies)
|
|
645
|
+
const tasks = buildTasksFromOptions(options, laneNumber, featureName, hasDependencies);
|
|
646
|
+
taskConfig = getDefaultConfig(laneNumber, featureName, tasks);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Replace placeholders
|
|
650
|
+
const processedConfig = replacePlaceholders(taskConfig, {
|
|
651
|
+
featureName,
|
|
652
|
+
laneNumber,
|
|
653
|
+
devPort: 3000 + laneNumber,
|
|
654
|
+
});
|
|
632
655
|
|
|
633
656
|
// Add dependencies if specified
|
|
634
657
|
const finalConfig = {
|
|
635
|
-
...
|
|
658
|
+
...processedConfig,
|
|
636
659
|
...(hasDependencies ? { dependsOn: options.dependsOnLanes } : {}),
|
|
637
660
|
};
|
|
638
661
|
|
|
@@ -647,9 +670,10 @@ async function addLaneToDir(options: PrepareOptions): Promise<void> {
|
|
|
647
670
|
throw err;
|
|
648
671
|
}
|
|
649
672
|
|
|
650
|
-
const
|
|
673
|
+
const tasksList = finalConfig.tasks || [];
|
|
674
|
+
const taskSummary = tasksList.map((t: any) => t.name).join(' → ');
|
|
651
675
|
const depsInfo = hasDependencies ? ` (depends: ${options.dependsOnLanes.join(', ')})` : '';
|
|
652
|
-
const presetInfo = options.preset ? ` [${options.preset}]` : (hasDependencies ? ' [merge]' : '');
|
|
676
|
+
const presetInfo = options.preset ? ` [${options.preset}]` : (hasDependencies ? ' [merge]' : (template ? ' [template]' : ''));
|
|
653
677
|
|
|
654
678
|
logger.success(`Added lane: ${fileName} [${taskSummary}]${presetInfo}${depsInfo}`);
|
|
655
679
|
logger.info(`Directory: ${taskDir}`);
|
|
@@ -660,7 +684,7 @@ async function addLaneToDir(options: PrepareOptions): Promise<void> {
|
|
|
660
684
|
}
|
|
661
685
|
|
|
662
686
|
async function addTaskToLane(options: PrepareOptions): Promise<void> {
|
|
663
|
-
const laneFile = path.resolve(process.cwd(), options.addTask!);
|
|
687
|
+
const laneFile = path.resolve(process.cwd(), options.addTask!); // nosemgrep
|
|
664
688
|
|
|
665
689
|
if (options.taskSpecs.length === 0) {
|
|
666
690
|
throw new Error('No task specified. Use --task "name|model|prompt|criteria" to define a task.');
|
|
@@ -708,7 +732,7 @@ async function createNewFeature(options: PrepareOptions): Promise<void> {
|
|
|
708
732
|
const now = new Date();
|
|
709
733
|
const timestamp = now.toISOString().replace(/[-T:]/g, '').substring(2, 12);
|
|
710
734
|
const taskDirName = `${timestamp}_${options.featureName}`;
|
|
711
|
-
const taskDir =
|
|
735
|
+
const taskDir = safeJoin(tasksBaseDir, taskDirName);
|
|
712
736
|
|
|
713
737
|
if (fs.existsSync(taskDir) && !options.force) {
|
|
714
738
|
throw new Error(`Task directory already exists: ${taskDir}. Use --force to overwrite.`);
|
|
@@ -723,12 +747,7 @@ async function createNewFeature(options: PrepareOptions): Promise<void> {
|
|
|
723
747
|
// Load template if provided (overrides --prompt/--task/--preset)
|
|
724
748
|
let template = null;
|
|
725
749
|
if (options.template) {
|
|
726
|
-
|
|
727
|
-
if (!fs.existsSync(templatePath)) {
|
|
728
|
-
throw new Error(`Template file not found: ${templatePath}`);
|
|
729
|
-
}
|
|
730
|
-
template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
|
|
731
|
-
logger.info(`Using template: ${options.template}`);
|
|
750
|
+
template = await resolveTemplate(options.template);
|
|
732
751
|
}
|
|
733
752
|
|
|
734
753
|
// Calculate dependencies
|
|
@@ -741,7 +760,7 @@ async function createNewFeature(options: PrepareOptions): Promise<void> {
|
|
|
741
760
|
for (let i = 1; i <= options.lanes; i++) {
|
|
742
761
|
const laneName = `lane-${i}`;
|
|
743
762
|
const fileName = `${i.toString().padStart(2, '0')}-${laneName}.json`;
|
|
744
|
-
const filePath =
|
|
763
|
+
const filePath = safeJoin(taskDir, fileName);
|
|
745
764
|
|
|
746
765
|
const depNums = dependencyMap.get(i) || [];
|
|
747
766
|
const dependsOn = depNums.map(n => {
|
|
@@ -788,7 +807,7 @@ async function createNewFeature(options: PrepareOptions): Promise<void> {
|
|
|
788
807
|
}
|
|
789
808
|
|
|
790
809
|
// Create README
|
|
791
|
-
const readmePath =
|
|
810
|
+
const readmePath = safeJoin(taskDir, 'README.md');
|
|
792
811
|
const readme = `# Task: ${options.featureName}
|
|
793
812
|
|
|
794
813
|
Prepared at: ${now.toISOString()}
|