@litmers/cursorflow-orchestrator 0.1.13 → 0.1.15
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 +37 -0
- package/README.md +83 -2
- package/commands/cursorflow-clean.md +20 -6
- package/commands/cursorflow-prepare.md +1 -1
- package/commands/cursorflow-resume.md +127 -6
- package/commands/cursorflow-run.md +2 -2
- package/commands/cursorflow-signal.md +11 -4
- package/dist/cli/clean.js +164 -12
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.d.ts +8 -0
- package/dist/cli/logs.js +759 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/monitor.js +113 -30
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +1 -1
- package/dist/cli/resume.js +367 -18
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +9 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +34 -20
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +13 -1
- package/dist/core/orchestrator.js +396 -35
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +24 -2
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +9 -3
- package/dist/core/runner.js +266 -61
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +38 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +210 -0
- package/dist/utils/enhanced-logger.js +1030 -0
- package/dist/utils/enhanced-logger.js.map +1 -0
- package/dist/utils/events.d.ts +59 -0
- package/dist/utils/events.js +37 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/git.d.ts +11 -0
- package/dist/utils/git.js +40 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +4 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/types.d.ts +132 -1
- package/dist/utils/webhook.d.ts +5 -0
- package/dist/utils/webhook.js +109 -0
- package/dist/utils/webhook.js.map +1 -0
- package/examples/README.md +1 -1
- package/package.json +2 -1
- package/scripts/patches/test-cursor-agent.js +1 -1
- package/scripts/simple-logging-test.sh +97 -0
- package/scripts/test-real-cursor-lifecycle.sh +289 -0
- package/scripts/test-real-logging.sh +289 -0
- package/scripts/test-streaming-multi-task.sh +247 -0
- package/src/cli/clean.ts +170 -13
- package/src/cli/index.ts +4 -1
- package/src/cli/logs.ts +863 -0
- package/src/cli/monitor.ts +123 -30
- package/src/cli/prepare.ts +1 -1
- package/src/cli/resume.ts +463 -22
- package/src/cli/run.ts +10 -0
- package/src/cli/signal.ts +43 -27
- package/src/core/orchestrator.ts +458 -36
- package/src/core/reviewer.ts +40 -4
- package/src/core/runner.ts +293 -60
- package/src/utils/config.ts +41 -1
- package/src/utils/enhanced-logger.ts +1166 -0
- package/src/utils/events.ts +117 -0
- package/src/utils/git.ts +40 -0
- package/src/utils/logger.ts +4 -1
- package/src/utils/types.ts +160 -1
- package/src/utils/webhook.ts +85 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Comprehensive Streaming Output Test with Multi-Task Dependencies
|
|
4
|
+
#
|
|
5
|
+
# This test verifies:
|
|
6
|
+
# 1. Streaming output from cursor-agent (not just final JSON)
|
|
7
|
+
# 2. Multiple tasks execution in sequence
|
|
8
|
+
# 3. Dependency handling between tasks
|
|
9
|
+
# 4. Raw log capture and parsed log quality
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
set -e
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
16
|
+
|
|
17
|
+
# Colors
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
YELLOW='\033[1;33m'
|
|
21
|
+
CYAN='\033[0;36m'
|
|
22
|
+
MAGENTA='\033[0;35m'
|
|
23
|
+
NC='\033[0m'
|
|
24
|
+
|
|
25
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
26
|
+
echo -e "${CYAN} 🧪 Streaming Multi-Task Test${NC}"
|
|
27
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
# Check cursor-agent
|
|
31
|
+
if ! command -v cursor-agent &> /dev/null; then
|
|
32
|
+
echo -e "${RED}❌ cursor-agent not found${NC}"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Check auth
|
|
37
|
+
echo -e "${YELLOW}Checking cursor-agent authentication...${NC}"
|
|
38
|
+
if ! cursor-agent create-chat &> /dev/null; then
|
|
39
|
+
echo -e "${RED}❌ cursor-agent authentication failed${NC}"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
echo -e "${GREEN}✓ cursor-agent authenticated${NC}"
|
|
43
|
+
|
|
44
|
+
cd "$PROJECT_ROOT"
|
|
45
|
+
|
|
46
|
+
# Build
|
|
47
|
+
echo -e "${YELLOW}Building project...${NC}"
|
|
48
|
+
npm run build > /dev/null 2>&1
|
|
49
|
+
echo -e "${GREEN}✓ Build complete${NC}"
|
|
50
|
+
|
|
51
|
+
# Setup test directory
|
|
52
|
+
TEST_DIR="$PROJECT_ROOT/_test-streaming"
|
|
53
|
+
rm -rf "$TEST_DIR"
|
|
54
|
+
mkdir -p "$TEST_DIR/lane"
|
|
55
|
+
|
|
56
|
+
# Create multi-task configuration with dependencies
|
|
57
|
+
cat > "$TEST_DIR/multi-task.json" << 'EOF'
|
|
58
|
+
{
|
|
59
|
+
"baseBranch": "main",
|
|
60
|
+
"branchPrefix": "test/stream-",
|
|
61
|
+
"timeout": 120000,
|
|
62
|
+
"dependencyPolicy": {
|
|
63
|
+
"allowDependencyChange": false,
|
|
64
|
+
"lockfileReadOnly": true
|
|
65
|
+
},
|
|
66
|
+
"tasks": [
|
|
67
|
+
{
|
|
68
|
+
"name": "task-1-analyze",
|
|
69
|
+
"prompt": "Analyze this codebase. List the top 3 most important files and briefly explain what each does. Keep your response under 200 words. Do not make any file changes.",
|
|
70
|
+
"model": "sonnet-4.5"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "task-2-summary",
|
|
74
|
+
"prompt": "Based on your previous analysis, write a 2-sentence summary of what this project does. Do not make any file changes.",
|
|
75
|
+
"model": "sonnet-4.5"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
EOF
|
|
80
|
+
|
|
81
|
+
echo -e "${GREEN}✓ Test configuration created (2 tasks)${NC}"
|
|
82
|
+
echo ""
|
|
83
|
+
|
|
84
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
85
|
+
echo -e "${CYAN} Running orchestration with STREAMING enabled...${NC}"
|
|
86
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
87
|
+
echo ""
|
|
88
|
+
|
|
89
|
+
LANE_DIR="$TEST_DIR/lane"
|
|
90
|
+
|
|
91
|
+
# Run spawnLane directly to test streaming
|
|
92
|
+
node -e "
|
|
93
|
+
const { spawnLane, waitChild } = require('./dist/core/orchestrator');
|
|
94
|
+
const fs = require('fs');
|
|
95
|
+
const path = require('path');
|
|
96
|
+
|
|
97
|
+
const tasksFile = path.join('$TEST_DIR', 'multi-task.json');
|
|
98
|
+
const laneRunDir = '$LANE_DIR';
|
|
99
|
+
|
|
100
|
+
console.log('Spawning lane with streaming output enabled...');
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
const result = spawnLane({
|
|
104
|
+
laneName: 'stream-test',
|
|
105
|
+
tasksFile,
|
|
106
|
+
laneRunDir,
|
|
107
|
+
executor: 'cursor-agent',
|
|
108
|
+
startIndex: 0,
|
|
109
|
+
enhancedLogConfig: {
|
|
110
|
+
enabled: true,
|
|
111
|
+
stripAnsi: true,
|
|
112
|
+
streamOutput: true, // Enable streaming!
|
|
113
|
+
addTimestamps: true,
|
|
114
|
+
keepRawLogs: true,
|
|
115
|
+
writeJsonLog: true,
|
|
116
|
+
timestampFormat: 'iso',
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
console.log('Lane spawned, PID:', result.child.pid);
|
|
121
|
+
console.log('Waiting for completion (may take 2-3 minutes)...');
|
|
122
|
+
console.log('');
|
|
123
|
+
|
|
124
|
+
const timeout = setTimeout(() => {
|
|
125
|
+
console.log('\\nTimeout reached (3 min)');
|
|
126
|
+
result.child.kill('SIGTERM');
|
|
127
|
+
}, 180000);
|
|
128
|
+
|
|
129
|
+
waitChild(result.child).then((code) => {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log('Lane completed with exit code:', code);
|
|
133
|
+
result.logManager?.close();
|
|
134
|
+
process.exit(code === 0 ? 0 : 1);
|
|
135
|
+
}).catch(e => {
|
|
136
|
+
clearTimeout(timeout);
|
|
137
|
+
console.error('Error:', e);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
140
|
+
" 2>&1
|
|
141
|
+
|
|
142
|
+
EXIT_CODE=$?
|
|
143
|
+
|
|
144
|
+
echo ""
|
|
145
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
146
|
+
echo -e "${CYAN} 📋 Log File Analysis${NC}"
|
|
147
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
148
|
+
echo ""
|
|
149
|
+
|
|
150
|
+
# Check files
|
|
151
|
+
echo -e "${YELLOW}Log files created:${NC}"
|
|
152
|
+
ls -la "$LANE_DIR"/*.log "$LANE_DIR"/*.jsonl 2>/dev/null || echo "No log files found"
|
|
153
|
+
echo ""
|
|
154
|
+
|
|
155
|
+
# Count streaming entries in JSON log
|
|
156
|
+
if [ -f "$LANE_DIR/terminal.jsonl" ]; then
|
|
157
|
+
TOTAL_ENTRIES=$(wc -l < "$LANE_DIR/terminal.jsonl")
|
|
158
|
+
STDOUT_ENTRIES=$(grep -c '"level":"stdout"' "$LANE_DIR/terminal.jsonl" || echo "0")
|
|
159
|
+
STREAMING_ENTRIES=$(grep -c '"type":' "$LANE_DIR/terminal.jsonl" || echo "0")
|
|
160
|
+
|
|
161
|
+
echo -e "${YELLOW}JSON Log Statistics:${NC}"
|
|
162
|
+
echo " Total entries: $TOTAL_ENTRIES"
|
|
163
|
+
echo " stdout entries: $STDOUT_ENTRIES"
|
|
164
|
+
echo " Entries with streaming 'type': $STREAMING_ENTRIES"
|
|
165
|
+
echo ""
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# Check for streaming content markers in raw log
|
|
169
|
+
if [ -f "$LANE_DIR/terminal-raw.log" ]; then
|
|
170
|
+
RAW_SIZE=$(wc -c < "$LANE_DIR/terminal-raw.log")
|
|
171
|
+
RAW_LINES=$(wc -l < "$LANE_DIR/terminal-raw.log")
|
|
172
|
+
|
|
173
|
+
# Look for streaming indicators
|
|
174
|
+
HAS_TYPE_TEXT=$(grep -c '"type":"text"' "$LANE_DIR/terminal-raw.log" 2>/dev/null || echo "0")
|
|
175
|
+
HAS_TYPE_RESULT=$(grep -c '"type":"result"' "$LANE_DIR/terminal-raw.log" 2>/dev/null || echo "0")
|
|
176
|
+
HAS_CONTENT=$(grep -c '"content":' "$LANE_DIR/terminal-raw.log" 2>/dev/null || echo "0")
|
|
177
|
+
|
|
178
|
+
echo -e "${YELLOW}Raw Log Statistics:${NC}"
|
|
179
|
+
echo " Size: $RAW_SIZE bytes"
|
|
180
|
+
echo " Lines: $RAW_LINES"
|
|
181
|
+
echo ""
|
|
182
|
+
echo -e "${YELLOW}Streaming Markers Found:${NC}"
|
|
183
|
+
echo " type:'text' entries: $HAS_TYPE_TEXT"
|
|
184
|
+
echo " type:'result' entries: $HAS_TYPE_RESULT"
|
|
185
|
+
echo " content entries: $HAS_CONTENT"
|
|
186
|
+
echo ""
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# Show sample of streaming output
|
|
190
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
191
|
+
echo -e "${CYAN} 📝 Sample Streaming Output (first 30 lines of stdout)${NC}"
|
|
192
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
193
|
+
echo ""
|
|
194
|
+
|
|
195
|
+
if [ -f "$LANE_DIR/terminal.log" ]; then
|
|
196
|
+
# Show lines containing cursor-agent output (streaming JSON)
|
|
197
|
+
echo -e "${MAGENTA}Looking for streaming JSON output...${NC}"
|
|
198
|
+
grep -E '^\[.*\] \{"type":' "$LANE_DIR/terminal.log" | head -20 || echo "No streaming JSON found"
|
|
199
|
+
echo ""
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Test the logs CLI command
|
|
203
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
204
|
+
echo -e "${CYAN} 🔍 Testing cursorflow logs command${NC}"
|
|
205
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
206
|
+
echo ""
|
|
207
|
+
|
|
208
|
+
# Create a fake runs directory structure for the CLI
|
|
209
|
+
FAKE_RUN_DIR="$TEST_DIR/runs/run-test"
|
|
210
|
+
mkdir -p "$FAKE_RUN_DIR/lanes"
|
|
211
|
+
ln -sf "$LANE_DIR" "$FAKE_RUN_DIR/lanes/stream-test"
|
|
212
|
+
|
|
213
|
+
echo -e "${YELLOW}Testing: cursorflow logs --tail 15${NC}"
|
|
214
|
+
node dist/cli/index.js logs "$FAKE_RUN_DIR" --tail 15 2>&1 || true
|
|
215
|
+
|
|
216
|
+
echo ""
|
|
217
|
+
echo -e "${YELLOW}Testing: cursorflow logs --filter 'type.*text'${NC}"
|
|
218
|
+
node dist/cli/index.js logs "$FAKE_RUN_DIR" --filter '"type":"text"' --tail 10 2>&1 || true
|
|
219
|
+
|
|
220
|
+
echo ""
|
|
221
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
222
|
+
echo -e "${CYAN} 📊 Test Summary${NC}"
|
|
223
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
224
|
+
echo ""
|
|
225
|
+
|
|
226
|
+
# Determine if streaming worked
|
|
227
|
+
if [ "$HAS_TYPE_TEXT" -gt "0" ] || [ "$HAS_CONTENT" -gt "10" ]; then
|
|
228
|
+
echo -e "${GREEN}✅ Streaming output captured successfully!${NC}"
|
|
229
|
+
echo " Found $HAS_TYPE_TEXT text stream entries"
|
|
230
|
+
echo " Found $HAS_CONTENT content entries"
|
|
231
|
+
else
|
|
232
|
+
echo -e "${YELLOW}⚠️ Limited streaming content detected${NC}"
|
|
233
|
+
echo " This may indicate:"
|
|
234
|
+
echo " - stream-json format not outputting expected data"
|
|
235
|
+
echo " - Or cursor-agent only provides final results"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
echo ""
|
|
239
|
+
echo "Log files saved at: $LANE_DIR"
|
|
240
|
+
echo ""
|
|
241
|
+
|
|
242
|
+
# Cleanup git
|
|
243
|
+
git worktree remove _cursorflow/worktrees/test/stream-* --force 2>/dev/null || true
|
|
244
|
+
git branch -D $(git branch | grep "test/stream-") 2>/dev/null || true
|
|
245
|
+
|
|
246
|
+
exit $EXIT_CODE
|
|
247
|
+
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
logger.info(`
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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';
|