@litmers/cursorflow-orchestrator 0.1.0
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 +78 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/commands/cursorflow-clean.md +162 -0
- package/commands/cursorflow-init.md +67 -0
- package/commands/cursorflow-monitor.md +131 -0
- package/commands/cursorflow-prepare.md +134 -0
- package/commands/cursorflow-resume.md +181 -0
- package/commands/cursorflow-review.md +220 -0
- package/commands/cursorflow-run.md +129 -0
- package/package.json +52 -0
- package/scripts/postinstall.js +27 -0
- package/src/cli/clean.js +30 -0
- package/src/cli/index.js +93 -0
- package/src/cli/init.js +235 -0
- package/src/cli/monitor.js +29 -0
- package/src/cli/resume.js +31 -0
- package/src/cli/run.js +51 -0
- package/src/cli/setup-commands.js +210 -0
- package/src/core/orchestrator.js +185 -0
- package/src/core/reviewer.js +233 -0
- package/src/core/runner.js +343 -0
- package/src/utils/config.js +195 -0
- package/src/utils/cursor-agent.js +190 -0
- package/src/utils/git.js +311 -0
- package/src/utils/logger.js +178 -0
- package/src/utils/state.js +207 -0
package/src/utils/git.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git utilities for CursorFlow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { execSync, spawnSync } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run git command and return output
|
|
11
|
+
*/
|
|
12
|
+
function runGit(args, options = {}) {
|
|
13
|
+
const { cwd, silent = false } = options;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = execSync(`git ${args.join(' ')}`, {
|
|
17
|
+
cwd: cwd || process.cwd(),
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
stdio: silent ? 'pipe' : 'inherit',
|
|
20
|
+
});
|
|
21
|
+
return result ? result.trim() : '';
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (silent) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run git command and return result object
|
|
32
|
+
*/
|
|
33
|
+
function runGitResult(args, options = {}) {
|
|
34
|
+
const { cwd } = options;
|
|
35
|
+
|
|
36
|
+
const result = spawnSync('git', args, {
|
|
37
|
+
cwd: cwd || process.cwd(),
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
stdio: 'pipe',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
exitCode: result.status ?? 1,
|
|
44
|
+
stdout: (result.stdout || '').trim(),
|
|
45
|
+
stderr: (result.stderr || '').trim(),
|
|
46
|
+
success: result.status === 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get current branch name
|
|
52
|
+
*/
|
|
53
|
+
function getCurrentBranch(cwd) {
|
|
54
|
+
return runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, silent: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get repository root directory
|
|
59
|
+
*/
|
|
60
|
+
function getRepoRoot(cwd) {
|
|
61
|
+
return runGit(['rev-parse', '--show-toplevel'], { cwd, silent: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if directory is a git repository
|
|
66
|
+
*/
|
|
67
|
+
function isGitRepo(cwd) {
|
|
68
|
+
const result = runGitResult(['rev-parse', '--git-dir'], { cwd });
|
|
69
|
+
return result.success;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if worktree exists
|
|
74
|
+
*/
|
|
75
|
+
function worktreeExists(worktreePath, cwd) {
|
|
76
|
+
const result = runGitResult(['worktree', 'list'], { cwd });
|
|
77
|
+
if (!result.success) return false;
|
|
78
|
+
|
|
79
|
+
return result.stdout.includes(worktreePath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create worktree
|
|
84
|
+
*/
|
|
85
|
+
function createWorktree(worktreePath, branchName, options = {}) {
|
|
86
|
+
const { cwd, baseBranch = 'main' } = options;
|
|
87
|
+
|
|
88
|
+
// Check if branch already exists
|
|
89
|
+
const branchExists = runGitResult(['rev-parse', '--verify', branchName], { cwd }).success;
|
|
90
|
+
|
|
91
|
+
if (branchExists) {
|
|
92
|
+
// Branch exists, checkout to worktree
|
|
93
|
+
runGit(['worktree', 'add', worktreePath, branchName], { cwd });
|
|
94
|
+
} else {
|
|
95
|
+
// Create new branch from base
|
|
96
|
+
runGit(['worktree', 'add', '-b', branchName, worktreePath, baseBranch], { cwd });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return worktreePath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove worktree
|
|
104
|
+
*/
|
|
105
|
+
function removeWorktree(worktreePath, options = {}) {
|
|
106
|
+
const { cwd, force = false } = options;
|
|
107
|
+
|
|
108
|
+
const args = ['worktree', 'remove', worktreePath];
|
|
109
|
+
if (force) {
|
|
110
|
+
args.push('--force');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
runGit(args, { cwd });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* List all worktrees
|
|
118
|
+
*/
|
|
119
|
+
function listWorktrees(cwd) {
|
|
120
|
+
const result = runGitResult(['worktree', 'list', '--porcelain'], { cwd });
|
|
121
|
+
if (!result.success) return [];
|
|
122
|
+
|
|
123
|
+
const worktrees = [];
|
|
124
|
+
const lines = result.stdout.split('\n');
|
|
125
|
+
let current = {};
|
|
126
|
+
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
if (line.startsWith('worktree ')) {
|
|
129
|
+
if (current.path) {
|
|
130
|
+
worktrees.push(current);
|
|
131
|
+
}
|
|
132
|
+
current = { path: line.slice(9) };
|
|
133
|
+
} else if (line.startsWith('branch ')) {
|
|
134
|
+
current.branch = line.slice(7);
|
|
135
|
+
} else if (line.startsWith('HEAD ')) {
|
|
136
|
+
current.head = line.slice(5);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (current.path) {
|
|
141
|
+
worktrees.push(current);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return worktrees;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if there are uncommitted changes
|
|
149
|
+
*/
|
|
150
|
+
function hasUncommittedChanges(cwd) {
|
|
151
|
+
const result = runGitResult(['status', '--porcelain'], { cwd });
|
|
152
|
+
return result.success && result.stdout.length > 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get list of changed files
|
|
157
|
+
*/
|
|
158
|
+
function getChangedFiles(cwd) {
|
|
159
|
+
const result = runGitResult(['status', '--porcelain'], { cwd });
|
|
160
|
+
if (!result.success) return [];
|
|
161
|
+
|
|
162
|
+
return result.stdout
|
|
163
|
+
.split('\n')
|
|
164
|
+
.filter(line => line.trim())
|
|
165
|
+
.map(line => {
|
|
166
|
+
const status = line.slice(0, 2);
|
|
167
|
+
const file = line.slice(3);
|
|
168
|
+
return { status, file };
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create commit
|
|
174
|
+
*/
|
|
175
|
+
function commit(message, options = {}) {
|
|
176
|
+
const { cwd, addAll = true } = options;
|
|
177
|
+
|
|
178
|
+
if (addAll) {
|
|
179
|
+
runGit(['add', '-A'], { cwd });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
runGit(['commit', '-m', message], { cwd });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Push to remote
|
|
187
|
+
*/
|
|
188
|
+
function push(branchName, options = {}) {
|
|
189
|
+
const { cwd, force = false, setUpstream = false } = options;
|
|
190
|
+
|
|
191
|
+
const args = ['push'];
|
|
192
|
+
|
|
193
|
+
if (force) {
|
|
194
|
+
args.push('--force');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (setUpstream) {
|
|
198
|
+
args.push('-u', 'origin', branchName);
|
|
199
|
+
} else {
|
|
200
|
+
args.push('origin', branchName);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
runGit(args, { cwd });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Fetch from remote
|
|
208
|
+
*/
|
|
209
|
+
function fetch(options = {}) {
|
|
210
|
+
const { cwd, prune = true } = options;
|
|
211
|
+
|
|
212
|
+
const args = ['fetch', 'origin'];
|
|
213
|
+
if (prune) {
|
|
214
|
+
args.push('--prune');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
runGit(args, { cwd });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if branch exists (local or remote)
|
|
222
|
+
*/
|
|
223
|
+
function branchExists(branchName, options = {}) {
|
|
224
|
+
const { cwd, remote = false } = options;
|
|
225
|
+
|
|
226
|
+
if (remote) {
|
|
227
|
+
const result = runGitResult(['ls-remote', '--heads', 'origin', branchName], { cwd });
|
|
228
|
+
return result.success && result.stdout.length > 0;
|
|
229
|
+
} else {
|
|
230
|
+
const result = runGitResult(['rev-parse', '--verify', branchName], { cwd });
|
|
231
|
+
return result.success;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Delete branch
|
|
237
|
+
*/
|
|
238
|
+
function deleteBranch(branchName, options = {}) {
|
|
239
|
+
const { cwd, force = false, remote = false } = options;
|
|
240
|
+
|
|
241
|
+
if (remote) {
|
|
242
|
+
runGit(['push', 'origin', '--delete', branchName], { cwd });
|
|
243
|
+
} else {
|
|
244
|
+
const args = ['branch', force ? '-D' : '-d', branchName];
|
|
245
|
+
runGit(args, { cwd });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Merge branch
|
|
251
|
+
*/
|
|
252
|
+
function merge(branchName, options = {}) {
|
|
253
|
+
const { cwd, noFf = false, message = null } = options;
|
|
254
|
+
|
|
255
|
+
const args = ['merge'];
|
|
256
|
+
|
|
257
|
+
if (noFf) {
|
|
258
|
+
args.push('--no-ff');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (message) {
|
|
262
|
+
args.push('-m', message);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
args.push(branchName);
|
|
266
|
+
|
|
267
|
+
runGit(args, { cwd });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get commit info
|
|
272
|
+
*/
|
|
273
|
+
function getCommitInfo(commitHash, options = {}) {
|
|
274
|
+
const { cwd } = options;
|
|
275
|
+
|
|
276
|
+
const format = '--format=%H%n%h%n%an%n%ae%n%at%n%s';
|
|
277
|
+
const result = runGitResult(['show', '-s', format, commitHash], { cwd });
|
|
278
|
+
|
|
279
|
+
if (!result.success) return null;
|
|
280
|
+
|
|
281
|
+
const lines = result.stdout.split('\n');
|
|
282
|
+
return {
|
|
283
|
+
hash: lines[0],
|
|
284
|
+
shortHash: lines[1],
|
|
285
|
+
author: lines[2],
|
|
286
|
+
authorEmail: lines[3],
|
|
287
|
+
timestamp: parseInt(lines[4]),
|
|
288
|
+
subject: lines[5],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
runGit,
|
|
294
|
+
runGitResult,
|
|
295
|
+
getCurrentBranch,
|
|
296
|
+
getRepoRoot,
|
|
297
|
+
isGitRepo,
|
|
298
|
+
worktreeExists,
|
|
299
|
+
createWorktree,
|
|
300
|
+
removeWorktree,
|
|
301
|
+
listWorktrees,
|
|
302
|
+
hasUncommittedChanges,
|
|
303
|
+
getChangedFiles,
|
|
304
|
+
commit,
|
|
305
|
+
push,
|
|
306
|
+
fetch,
|
|
307
|
+
branchExists,
|
|
308
|
+
deleteBranch,
|
|
309
|
+
merge,
|
|
310
|
+
getCommitInfo,
|
|
311
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Logging utilities for CursorFlow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const LOG_LEVELS = {
|
|
7
|
+
error: 0,
|
|
8
|
+
warn: 1,
|
|
9
|
+
info: 2,
|
|
10
|
+
debug: 3,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const COLORS = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
red: '\x1b[31m',
|
|
16
|
+
yellow: '\x1b[33m',
|
|
17
|
+
green: '\x1b[32m',
|
|
18
|
+
blue: '\x1b[34m',
|
|
19
|
+
cyan: '\x1b[36m',
|
|
20
|
+
gray: '\x1b[90m',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let currentLogLevel = LOG_LEVELS.info;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set log level
|
|
27
|
+
*/
|
|
28
|
+
function setLogLevel(level) {
|
|
29
|
+
if (typeof level === 'string') {
|
|
30
|
+
currentLogLevel = LOG_LEVELS[level] ?? LOG_LEVELS.info;
|
|
31
|
+
} else {
|
|
32
|
+
currentLogLevel = level;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format message with timestamp
|
|
38
|
+
*/
|
|
39
|
+
function formatMessage(level, message, emoji = '') {
|
|
40
|
+
const timestamp = new Date().toISOString();
|
|
41
|
+
const prefix = emoji ? `${emoji} ` : '';
|
|
42
|
+
return `[${timestamp}] [${level.toUpperCase()}] ${prefix}${message}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Log with color
|
|
47
|
+
*/
|
|
48
|
+
function logWithColor(color, level, message, emoji = '') {
|
|
49
|
+
if (LOG_LEVELS[level] > currentLogLevel) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const formatted = formatMessage(level, message, emoji);
|
|
54
|
+
console.log(`${color}${formatted}${COLORS.reset}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Error log
|
|
59
|
+
*/
|
|
60
|
+
function error(message, emoji = '❌') {
|
|
61
|
+
logWithColor(COLORS.red, 'error', message, emoji);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Warning log
|
|
66
|
+
*/
|
|
67
|
+
function warn(message, emoji = '⚠️') {
|
|
68
|
+
logWithColor(COLORS.yellow, 'warn', message, emoji);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Info log
|
|
73
|
+
*/
|
|
74
|
+
function info(message, emoji = 'ℹ️') {
|
|
75
|
+
logWithColor(COLORS.cyan, 'info', message, emoji);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Success log
|
|
80
|
+
*/
|
|
81
|
+
function success(message, emoji = '✅') {
|
|
82
|
+
logWithColor(COLORS.green, 'info', message, emoji);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Debug log
|
|
87
|
+
*/
|
|
88
|
+
function debug(message, emoji = '🔍') {
|
|
89
|
+
logWithColor(COLORS.gray, 'debug', message, emoji);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Progress log
|
|
94
|
+
*/
|
|
95
|
+
function progress(message, emoji = '🔄') {
|
|
96
|
+
logWithColor(COLORS.blue, 'info', message, emoji);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Section header
|
|
101
|
+
*/
|
|
102
|
+
function section(message) {
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(`${COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
105
|
+
console.log(`${COLORS.cyan} ${message}${COLORS.reset}`);
|
|
106
|
+
console.log(`${COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Simple log without formatting
|
|
112
|
+
*/
|
|
113
|
+
function log(message) {
|
|
114
|
+
console.log(message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Log JSON data (pretty print in debug mode)
|
|
119
|
+
*/
|
|
120
|
+
function json(data) {
|
|
121
|
+
if (currentLogLevel >= LOG_LEVELS.debug) {
|
|
122
|
+
console.log(JSON.stringify(data, null, 2));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create spinner (simple implementation)
|
|
128
|
+
*/
|
|
129
|
+
function createSpinner(message) {
|
|
130
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
131
|
+
let i = 0;
|
|
132
|
+
let interval = null;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
start() {
|
|
136
|
+
process.stdout.write(`${message} ${frames[0]}`);
|
|
137
|
+
interval = setInterval(() => {
|
|
138
|
+
i = (i + 1) % frames.length;
|
|
139
|
+
process.stdout.write(`\r${message} ${frames[i]}`);
|
|
140
|
+
}, 80);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
stop(finalMessage = null) {
|
|
144
|
+
if (interval) {
|
|
145
|
+
clearInterval(interval);
|
|
146
|
+
interval = null;
|
|
147
|
+
}
|
|
148
|
+
process.stdout.write('\r\x1b[K'); // Clear line
|
|
149
|
+
if (finalMessage) {
|
|
150
|
+
console.log(finalMessage);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
succeed(message) {
|
|
155
|
+
this.stop(`${COLORS.green}✓${COLORS.reset} ${message}`);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
fail(message) {
|
|
159
|
+
this.stop(`${COLORS.red}✗${COLORS.reset} ${message}`);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
setLogLevel,
|
|
166
|
+
error,
|
|
167
|
+
warn,
|
|
168
|
+
info,
|
|
169
|
+
success,
|
|
170
|
+
debug,
|
|
171
|
+
progress,
|
|
172
|
+
section,
|
|
173
|
+
log,
|
|
174
|
+
json,
|
|
175
|
+
createSpinner,
|
|
176
|
+
COLORS,
|
|
177
|
+
LOG_LEVELS,
|
|
178
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* State management utilities for CursorFlow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Save state to JSON file
|
|
11
|
+
*/
|
|
12
|
+
function saveState(statePath, state) {
|
|
13
|
+
const stateDir = path.dirname(statePath);
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(stateDir)) {
|
|
16
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load state from JSON file
|
|
24
|
+
*/
|
|
25
|
+
function loadState(statePath) {
|
|
26
|
+
if (!fs.existsSync(statePath)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const content = fs.readFileSync(statePath, 'utf8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.warn(`Warning: Failed to parse state file ${statePath}: ${error.message}`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Append to JSONL log file
|
|
41
|
+
*/
|
|
42
|
+
function appendLog(logPath, entry) {
|
|
43
|
+
const logDir = path.dirname(logPath);
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(logDir)) {
|
|
46
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const line = JSON.stringify(entry) + '\n';
|
|
50
|
+
fs.appendFileSync(logPath, line, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read JSONL log file
|
|
55
|
+
*/
|
|
56
|
+
function readLog(logPath) {
|
|
57
|
+
if (!fs.existsSync(logPath)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
63
|
+
return content
|
|
64
|
+
.split('\n')
|
|
65
|
+
.filter(line => line.trim())
|
|
66
|
+
.map(line => JSON.parse(line));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn(`Warning: Failed to parse log file ${logPath}: ${error.message}`);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create initial lane state
|
|
75
|
+
*/
|
|
76
|
+
function createLaneState(laneName, config) {
|
|
77
|
+
return {
|
|
78
|
+
label: laneName,
|
|
79
|
+
status: 'pending',
|
|
80
|
+
currentTaskIndex: 0,
|
|
81
|
+
totalTasks: config.tasks ? config.tasks.length : 0,
|
|
82
|
+
worktreeDir: null,
|
|
83
|
+
pipelineBranch: null,
|
|
84
|
+
startTime: Date.now(),
|
|
85
|
+
endTime: null,
|
|
86
|
+
error: null,
|
|
87
|
+
dependencyRequest: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Update lane state
|
|
93
|
+
*/
|
|
94
|
+
function updateLaneState(state, updates) {
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
...updates,
|
|
98
|
+
updatedAt: Date.now(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create conversation log entry
|
|
104
|
+
*/
|
|
105
|
+
function createConversationEntry(role, text, options = {}) {
|
|
106
|
+
return {
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
role, // 'user' | 'assistant' | 'reviewer' | 'system'
|
|
109
|
+
task: options.task || null,
|
|
110
|
+
fullText: text,
|
|
111
|
+
textLength: text.length,
|
|
112
|
+
model: options.model || null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create git operation log entry
|
|
118
|
+
*/
|
|
119
|
+
function createGitLogEntry(operation, details = {}) {
|
|
120
|
+
return {
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
operation, // 'commit' | 'push' | 'merge' | 'worktree-add' | etc.
|
|
123
|
+
...details,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create event log entry
|
|
129
|
+
*/
|
|
130
|
+
function createEventEntry(event, data = {}) {
|
|
131
|
+
return {
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
event,
|
|
134
|
+
...data,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get latest run directory
|
|
140
|
+
*/
|
|
141
|
+
function getLatestRunDir(logsDir) {
|
|
142
|
+
if (!fs.existsSync(logsDir)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const runs = fs.readdirSync(logsDir)
|
|
147
|
+
.filter(f => fs.statSync(path.join(logsDir, f)).isDirectory())
|
|
148
|
+
.sort()
|
|
149
|
+
.reverse();
|
|
150
|
+
|
|
151
|
+
if (runs.length === 0) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return path.join(logsDir, runs[0]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* List all lanes in a run directory
|
|
160
|
+
*/
|
|
161
|
+
function listLanesInRun(runDir) {
|
|
162
|
+
if (!fs.existsSync(runDir)) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return fs.readdirSync(runDir)
|
|
167
|
+
.filter(f => fs.statSync(path.join(runDir, f)).isDirectory())
|
|
168
|
+
.map(laneName => ({
|
|
169
|
+
name: laneName,
|
|
170
|
+
dir: path.join(runDir, laneName),
|
|
171
|
+
statePath: path.join(runDir, laneName, 'state.json'),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get lane state summary
|
|
177
|
+
*/
|
|
178
|
+
function getLaneStateSummary(statePath) {
|
|
179
|
+
const state = loadState(statePath);
|
|
180
|
+
if (!state) {
|
|
181
|
+
return { status: 'unknown', progress: '-' };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const progress = `${(state.currentTaskIndex || 0) + 1}/${state.totalTasks || '?'}`;
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
status: state.status || 'unknown',
|
|
188
|
+
progress,
|
|
189
|
+
label: state.label,
|
|
190
|
+
error: state.error,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
saveState,
|
|
196
|
+
loadState,
|
|
197
|
+
appendLog,
|
|
198
|
+
readLog,
|
|
199
|
+
createLaneState,
|
|
200
|
+
updateLaneState,
|
|
201
|
+
createConversationEntry,
|
|
202
|
+
createGitLogEntry,
|
|
203
|
+
createEventEntry,
|
|
204
|
+
getLatestRunDir,
|
|
205
|
+
listLanesInRun,
|
|
206
|
+
getLaneStateSummary,
|
|
207
|
+
};
|