@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.
@@ -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
+ };