@supaku/agentfactory-cli 0.3.0 → 0.4.1
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/dist/src/analyze-logs.d.ts +2 -2
- package/dist/src/analyze-logs.js +23 -194
- package/dist/src/cleanup.d.ts +2 -6
- package/dist/src/cleanup.d.ts.map +1 -1
- package/dist/src/cleanup.js +24 -225
- package/dist/src/lib/analyze-logs-runner.d.ts +47 -0
- package/dist/src/lib/analyze-logs-runner.d.ts.map +1 -0
- package/dist/src/lib/analyze-logs-runner.js +216 -0
- package/dist/src/lib/cleanup-runner.d.ts +28 -0
- package/dist/src/lib/cleanup-runner.d.ts.map +1 -0
- package/dist/src/lib/cleanup-runner.js +224 -0
- package/dist/src/lib/orchestrator-runner.d.ts +45 -0
- package/dist/src/lib/orchestrator-runner.d.ts.map +1 -0
- package/dist/src/lib/orchestrator-runner.js +144 -0
- package/dist/src/lib/queue-admin-runner.d.ts +30 -0
- package/dist/src/lib/queue-admin-runner.d.ts.map +1 -0
- package/dist/src/lib/queue-admin-runner.js +378 -0
- package/dist/src/lib/worker-fleet-runner.d.ts +28 -0
- package/dist/src/lib/worker-fleet-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-fleet-runner.js +224 -0
- package/dist/src/lib/worker-runner.d.ts +31 -0
- package/dist/src/lib/worker-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-runner.js +735 -0
- package/dist/src/orchestrator.d.ts +1 -1
- package/dist/src/orchestrator.js +42 -106
- package/dist/src/queue-admin.d.ts +3 -2
- package/dist/src/queue-admin.d.ts.map +1 -1
- package/dist/src/queue-admin.js +38 -360
- package/dist/src/worker-fleet.d.ts +1 -1
- package/dist/src/worker-fleet.js +23 -162
- package/dist/src/worker.d.ts +1 -0
- package/dist/src/worker.d.ts.map +1 -1
- package/dist/src/worker.js +33 -702
- package/package.json +28 -4
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Analyzer Runner -- Programmatic API for the log analyzer CLI.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the core logic from the analyze-logs bin script so that it can be
|
|
5
|
+
* invoked programmatically (e.g. from a Next.js route handler or test) without
|
|
6
|
+
* process.exit / dotenv / argv coupling.
|
|
7
|
+
*/
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { createLogAnalyzer } from '@supaku/agentfactory';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const DEFAULT_POLL_INTERVAL = 5000;
|
|
14
|
+
/** Detect the git repository root. Falls back to cwd. */
|
|
15
|
+
export function getGitRoot() {
|
|
16
|
+
try {
|
|
17
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
}).trim();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return process.cwd();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function formatTime() {
|
|
27
|
+
return new Date().toLocaleTimeString();
|
|
28
|
+
}
|
|
29
|
+
function emptyStats() {
|
|
30
|
+
return {
|
|
31
|
+
sessionsAnalyzed: 0,
|
|
32
|
+
totalErrors: 0,
|
|
33
|
+
totalPatterns: 0,
|
|
34
|
+
issuesCreated: 0,
|
|
35
|
+
issuesUpdated: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function analyzeAndPrintSession(analyzer, sessionId, options, stats) {
|
|
39
|
+
console.log(`\nAnalyzing session: ${sessionId}`);
|
|
40
|
+
console.log('-'.repeat(50));
|
|
41
|
+
const result = analyzer.analyzeSession(sessionId);
|
|
42
|
+
if (!result) {
|
|
43
|
+
console.log(' [SKIP] Session not found or incomplete');
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
console.log(` Issue: ${result.metadata.issueIdentifier}`);
|
|
47
|
+
console.log(` Work Type: ${result.metadata.workType}`);
|
|
48
|
+
console.log(` Status: ${result.metadata.status}`);
|
|
49
|
+
console.log(` Events: ${result.eventsAnalyzed}`);
|
|
50
|
+
console.log(` Errors: ${result.errorsFound}`);
|
|
51
|
+
console.log(` Patterns: ${result.patterns.length}`);
|
|
52
|
+
stats.sessionsAnalyzed++;
|
|
53
|
+
stats.totalErrors += result.errorsFound;
|
|
54
|
+
stats.totalPatterns += result.patterns.length;
|
|
55
|
+
if (options.verbose && result.patterns.length > 0) {
|
|
56
|
+
console.log('\n Detected Patterns:');
|
|
57
|
+
for (const pattern of result.patterns) {
|
|
58
|
+
console.log(` - [${pattern.severity}] ${pattern.title}`);
|
|
59
|
+
console.log(` Type: ${pattern.type}, Occurrences: ${pattern.occurrences}`);
|
|
60
|
+
if (pattern.tool) {
|
|
61
|
+
console.log(` Tool: ${pattern.tool}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (result.suggestedIssues.length > 0) {
|
|
66
|
+
console.log(`\n Suggested Issues: ${result.suggestedIssues.length}`);
|
|
67
|
+
if (options.verbose) {
|
|
68
|
+
for (const issue of result.suggestedIssues) {
|
|
69
|
+
console.log(` - ${issue.title}`);
|
|
70
|
+
console.log(` Signature: ${issue.signature}`);
|
|
71
|
+
console.log(` Labels: ${issue.labels.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const issueResults = await analyzer.createIssues(result.suggestedIssues, sessionId, options.dryRun);
|
|
76
|
+
for (const issueResult of issueResults) {
|
|
77
|
+
if (issueResult.created) {
|
|
78
|
+
console.log(` [${options.dryRun ? 'WOULD CREATE' : 'CREATED'}] ${issueResult.identifier}`);
|
|
79
|
+
stats.issuesCreated++;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(` [${options.dryRun ? 'WOULD UPDATE' : 'UPDATED'}] ${issueResult.identifier}`);
|
|
83
|
+
stats.issuesUpdated++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.log(` [ERROR] Failed to create issues: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!options.dryRun) {
|
|
92
|
+
analyzer.markProcessed(sessionId, result);
|
|
93
|
+
console.log(' [PROCESSED]');
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Summary printer
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
export function printSummary(stats, dryRun) {
|
|
101
|
+
console.log('\n' + '='.repeat(50));
|
|
102
|
+
console.log('=== Summary ===\n');
|
|
103
|
+
console.log(` Sessions analyzed: ${stats.sessionsAnalyzed}`);
|
|
104
|
+
console.log(` Total errors found: ${stats.totalErrors}`);
|
|
105
|
+
console.log(` Total patterns detected: ${stats.totalPatterns}`);
|
|
106
|
+
console.log(` Issues created: ${stats.issuesCreated}${dryRun ? ' (dry run)' : ''}`);
|
|
107
|
+
console.log(` Issues updated: ${stats.issuesUpdated}${dryRun ? ' (dry run)' : ''}`);
|
|
108
|
+
console.log('');
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Follow mode (watch)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
async function runFollowMode(analyzer, options, interval, signal) {
|
|
114
|
+
const stats = emptyStats();
|
|
115
|
+
const processedInSession = new Set();
|
|
116
|
+
console.log(`[${formatTime()}] Watching for new sessions (poll interval: ${interval}ms)`);
|
|
117
|
+
console.log(`[${formatTime()}] Press Ctrl+C to stop\n`);
|
|
118
|
+
const isAborted = () => signal?.aborted ?? false;
|
|
119
|
+
// Initial check for existing unprocessed sessions
|
|
120
|
+
const initialSessions = analyzer.getUnprocessedSessions();
|
|
121
|
+
if (initialSessions.length > 0) {
|
|
122
|
+
console.log(`[${formatTime()}] Found ${initialSessions.length} existing unprocessed session(s)`);
|
|
123
|
+
for (const sid of initialSessions) {
|
|
124
|
+
if (isAborted())
|
|
125
|
+
break;
|
|
126
|
+
await analyzeAndPrintSession(analyzer, sid, options, stats);
|
|
127
|
+
processedInSession.add(sid);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Poll loop
|
|
131
|
+
while (!isAborted()) {
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
const timer = setTimeout(resolve, interval);
|
|
134
|
+
// If an abort signal fires while waiting, resolve immediately
|
|
135
|
+
if (signal) {
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
resolve();
|
|
139
|
+
};
|
|
140
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (isAborted())
|
|
144
|
+
break;
|
|
145
|
+
const sessions = analyzer.getUnprocessedSessions();
|
|
146
|
+
const newSessions = sessions.filter((s) => !processedInSession.has(s));
|
|
147
|
+
if (newSessions.length > 0) {
|
|
148
|
+
console.log(`[${formatTime()}] Found ${newSessions.length} new session(s) ready for analysis`);
|
|
149
|
+
for (const sid of newSessions) {
|
|
150
|
+
if (isAborted())
|
|
151
|
+
break;
|
|
152
|
+
const analyzed = await analyzeAndPrintSession(analyzer, sid, options, stats);
|
|
153
|
+
if (analyzed) {
|
|
154
|
+
processedInSession.add(sid);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.log(`\n[${formatTime()}] Stopping...`);
|
|
160
|
+
return stats;
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Public entry point
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
/**
|
|
166
|
+
* Run the log analyzer programmatically.
|
|
167
|
+
*
|
|
168
|
+
* For one-shot mode (default) the returned promise resolves with the analysis
|
|
169
|
+
* result once all sessions have been processed.
|
|
170
|
+
*
|
|
171
|
+
* For follow mode (`config.follow = true`) the analyzer keeps running until
|
|
172
|
+
* the optional `signal` is aborted. When the signal fires the current poll
|
|
173
|
+
* cycle finishes and the accumulated stats are returned.
|
|
174
|
+
*/
|
|
175
|
+
export async function runLogAnalyzer(config = {}, signal) {
|
|
176
|
+
const gitRoot = config.gitRoot ?? getGitRoot();
|
|
177
|
+
const logsDir = config.logsDir ?? process.env.AGENT_LOGS_DIR ?? `${gitRoot}/.agent-logs`;
|
|
178
|
+
const dryRun = config.dryRun ?? false;
|
|
179
|
+
const verbose = config.verbose ?? false;
|
|
180
|
+
const interval = config.interval ?? DEFAULT_POLL_INTERVAL;
|
|
181
|
+
console.log('\n=== AgentFactory Log Analyzer ===\n');
|
|
182
|
+
if (dryRun) {
|
|
183
|
+
console.log('[DRY RUN MODE - No issues will be created]\n');
|
|
184
|
+
}
|
|
185
|
+
const analyzer = createLogAnalyzer({ logsDir });
|
|
186
|
+
// Cleanup mode
|
|
187
|
+
if (config.cleanup) {
|
|
188
|
+
console.log('Cleaning up old logs...\n');
|
|
189
|
+
const deleted = analyzer.cleanupOldLogs();
|
|
190
|
+
console.log(`Deleted ${deleted} old log entries.\n`);
|
|
191
|
+
return emptyStats();
|
|
192
|
+
}
|
|
193
|
+
// Follow (watch) mode
|
|
194
|
+
if (config.follow) {
|
|
195
|
+
return runFollowMode(analyzer, { dryRun, verbose }, interval, signal);
|
|
196
|
+
}
|
|
197
|
+
// Standard one-shot mode
|
|
198
|
+
const stats = emptyStats();
|
|
199
|
+
let sessionsToAnalyze;
|
|
200
|
+
if (config.sessionId) {
|
|
201
|
+
sessionsToAnalyze = [config.sessionId];
|
|
202
|
+
console.log(`Analyzing session: ${config.sessionId}\n`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
sessionsToAnalyze = analyzer.getUnprocessedSessions();
|
|
206
|
+
console.log(`Found ${sessionsToAnalyze.length} unprocessed session(s)\n`);
|
|
207
|
+
}
|
|
208
|
+
if (sessionsToAnalyze.length === 0) {
|
|
209
|
+
console.log('No sessions to analyze.\n');
|
|
210
|
+
return stats;
|
|
211
|
+
}
|
|
212
|
+
for (const sid of sessionsToAnalyze) {
|
|
213
|
+
await analyzeAndPrintSession(analyzer, sid, { dryRun, verbose }, stats);
|
|
214
|
+
}
|
|
215
|
+
return stats;
|
|
216
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup Runner -- Programmatic API for the worktree cleanup CLI.
|
|
3
|
+
*
|
|
4
|
+
* Exports `runCleanup()` so worktree cleanup can be invoked from code
|
|
5
|
+
* without going through process.argv / process.env / process.exit.
|
|
6
|
+
*/
|
|
7
|
+
export interface CleanupRunnerConfig {
|
|
8
|
+
/** Show what would be cleaned up without removing (default: false) */
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
/** Force removal even if worktree appears active (default: false) */
|
|
11
|
+
force?: boolean;
|
|
12
|
+
/** Custom worktrees directory (default: {gitRoot}/.worktrees) */
|
|
13
|
+
worktreePath?: string;
|
|
14
|
+
/** Git root for default worktree path (default: auto-detect) */
|
|
15
|
+
gitRoot?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CleanupResult {
|
|
18
|
+
scanned: number;
|
|
19
|
+
orphaned: number;
|
|
20
|
+
cleaned: number;
|
|
21
|
+
errors: Array<{
|
|
22
|
+
path: string;
|
|
23
|
+
error: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export declare function getGitRoot(): string;
|
|
27
|
+
export declare function runCleanup(config?: CleanupRunnerConfig): CleanupResult;
|
|
28
|
+
//# sourceMappingURL=cleanup-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/cleanup-runner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,qEAAqE;IACrE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/C;AAuBD,wBAAgB,UAAU,IAAI,MAAM,CASnC;AAuND,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,aAAa,CAUtE"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup Runner -- Programmatic API for the worktree cleanup CLI.
|
|
3
|
+
*
|
|
4
|
+
* Exports `runCleanup()` so worktree cleanup can be invoked from code
|
|
5
|
+
* without going through process.argv / process.env / process.exit.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
9
|
+
import { resolve, basename } from 'path';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export function getGitRoot() {
|
|
14
|
+
try {
|
|
15
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
16
|
+
encoding: 'utf-8',
|
|
17
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
18
|
+
}).trim();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return process.cwd();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get list of git worktrees from 'git worktree list'
|
|
26
|
+
*/
|
|
27
|
+
function getGitWorktrees() {
|
|
28
|
+
const worktrees = new Map();
|
|
29
|
+
try {
|
|
30
|
+
const output = execSync('git worktree list --porcelain', {
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
+
});
|
|
34
|
+
let currentPath = '';
|
|
35
|
+
for (const line of output.split('\n')) {
|
|
36
|
+
if (line.startsWith('worktree ')) {
|
|
37
|
+
currentPath = line.substring(9);
|
|
38
|
+
}
|
|
39
|
+
else if (line.startsWith('branch ')) {
|
|
40
|
+
const branch = line.substring(7).replace('refs/heads/', '');
|
|
41
|
+
worktrees.set(currentPath, branch);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('Failed to list git worktrees:', error);
|
|
47
|
+
}
|
|
48
|
+
return worktrees;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if a git branch exists
|
|
52
|
+
*/
|
|
53
|
+
function branchExists(branchName) {
|
|
54
|
+
try {
|
|
55
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, {
|
|
56
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
|
+
});
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Scan the worktrees directory and identify orphaned worktrees
|
|
66
|
+
*/
|
|
67
|
+
function scanWorktrees(options) {
|
|
68
|
+
const worktreesDir = resolve(options.worktreePath);
|
|
69
|
+
const result = [];
|
|
70
|
+
if (!existsSync(worktreesDir)) {
|
|
71
|
+
console.log(`Worktrees directory not found: ${worktreesDir}`);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
const gitWorktrees = getGitWorktrees();
|
|
75
|
+
const entries = readdirSync(worktreesDir);
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const entryPath = resolve(worktreesDir, entry);
|
|
78
|
+
try {
|
|
79
|
+
if (!statSync(entryPath).isDirectory()) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const info = {
|
|
87
|
+
path: entryPath,
|
|
88
|
+
branch: entry,
|
|
89
|
+
isOrphaned: false,
|
|
90
|
+
};
|
|
91
|
+
const isKnownWorktree = gitWorktrees.has(entryPath);
|
|
92
|
+
const branchName = isKnownWorktree ? gitWorktrees.get(entryPath) : entry;
|
|
93
|
+
if (options.force) {
|
|
94
|
+
info.isOrphaned = true;
|
|
95
|
+
info.reason = 'force cleanup requested';
|
|
96
|
+
}
|
|
97
|
+
else if (!isKnownWorktree) {
|
|
98
|
+
info.isOrphaned = true;
|
|
99
|
+
info.reason = 'not registered with git worktree';
|
|
100
|
+
}
|
|
101
|
+
else if (!branchExists(branchName)) {
|
|
102
|
+
info.isOrphaned = true;
|
|
103
|
+
info.reason = `branch '${branchName}' no longer exists`;
|
|
104
|
+
}
|
|
105
|
+
result.push(info);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Remove a single worktree
|
|
111
|
+
*/
|
|
112
|
+
function removeWorktree(worktreePath) {
|
|
113
|
+
try {
|
|
114
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
117
|
+
});
|
|
118
|
+
return { success: true };
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
try {
|
|
122
|
+
execSync(`rm -rf "${worktreePath}"`, {
|
|
123
|
+
encoding: 'utf-8',
|
|
124
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
125
|
+
});
|
|
126
|
+
execSync('git worktree prune', {
|
|
127
|
+
encoding: 'utf-8',
|
|
128
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
129
|
+
});
|
|
130
|
+
return { success: true };
|
|
131
|
+
}
|
|
132
|
+
catch (rmError) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: rmError instanceof Error ? rmError.message : String(rmError),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Core cleanup logic
|
|
142
|
+
*/
|
|
143
|
+
function cleanup(options) {
|
|
144
|
+
const result = {
|
|
145
|
+
scanned: 0,
|
|
146
|
+
orphaned: 0,
|
|
147
|
+
cleaned: 0,
|
|
148
|
+
errors: [],
|
|
149
|
+
};
|
|
150
|
+
console.log('Scanning worktrees...\n');
|
|
151
|
+
try {
|
|
152
|
+
execSync('git worktree prune', {
|
|
153
|
+
encoding: 'utf-8',
|
|
154
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
console.log('Note: Could not prune git worktree metadata');
|
|
159
|
+
}
|
|
160
|
+
const worktrees = scanWorktrees(options);
|
|
161
|
+
result.scanned = worktrees.length;
|
|
162
|
+
if (worktrees.length === 0) {
|
|
163
|
+
console.log('No worktrees found.\n');
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
console.log(`Found ${worktrees.length} worktree(s) in ${options.worktreePath}/\n`);
|
|
167
|
+
for (const wt of worktrees) {
|
|
168
|
+
const status = wt.isOrphaned ? ' orphaned' : ' active';
|
|
169
|
+
const reason = wt.reason ? ` (${wt.reason})` : '';
|
|
170
|
+
console.log(` ${status}: ${basename(wt.path)}${reason}`);
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
const orphaned = worktrees.filter((wt) => wt.isOrphaned);
|
|
174
|
+
result.orphaned = orphaned.length;
|
|
175
|
+
if (orphaned.length === 0) {
|
|
176
|
+
console.log('No orphaned worktrees to clean up.\n');
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
if (options.dryRun) {
|
|
180
|
+
console.log(`[DRY RUN] Would clean up ${orphaned.length} orphaned worktree(s):\n`);
|
|
181
|
+
for (const wt of orphaned) {
|
|
182
|
+
console.log(` Would remove: ${wt.path}`);
|
|
183
|
+
}
|
|
184
|
+
console.log('');
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
console.log(`Cleaning up ${orphaned.length} orphaned worktree(s)...\n`);
|
|
188
|
+
for (const wt of orphaned) {
|
|
189
|
+
process.stdout.write(` Removing ${basename(wt.path)}... `);
|
|
190
|
+
const removal = removeWorktree(wt.path);
|
|
191
|
+
if (removal.success) {
|
|
192
|
+
console.log('done');
|
|
193
|
+
result.cleaned++;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(`FAILED: ${removal.error}`);
|
|
197
|
+
result.errors.push({ path: wt.path, error: removal.error || 'Unknown error' });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
console.log('');
|
|
201
|
+
try {
|
|
202
|
+
execSync('git worktree prune', {
|
|
203
|
+
encoding: 'utf-8',
|
|
204
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
205
|
+
});
|
|
206
|
+
console.log('Pruned git worktree metadata.\n');
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Ignore prune errors
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Runner
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
export function runCleanup(config) {
|
|
217
|
+
const gitRoot = config?.gitRoot ?? getGitRoot();
|
|
218
|
+
const options = {
|
|
219
|
+
dryRun: config?.dryRun ?? false,
|
|
220
|
+
force: config?.force ?? false,
|
|
221
|
+
worktreePath: config?.worktreePath ?? resolve(gitRoot, '.worktrees'),
|
|
222
|
+
};
|
|
223
|
+
return cleanup(options);
|
|
224
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator Runner -- Programmatic API for the orchestrator CLI.
|
|
3
|
+
*
|
|
4
|
+
* Exports `runOrchestrator()` so the orchestrator can be invoked from code
|
|
5
|
+
* (e.g. Next.js route handlers, tests, or custom scripts) without going
|
|
6
|
+
* through process.argv / process.env / process.exit.
|
|
7
|
+
*/
|
|
8
|
+
import { type AgentProcess, type OrchestratorIssue } from '@supaku/agentfactory';
|
|
9
|
+
export interface OrchestratorRunnerConfig {
|
|
10
|
+
/** Linear API key for authentication */
|
|
11
|
+
linearApiKey: string;
|
|
12
|
+
/** Filter issues by project name */
|
|
13
|
+
project?: string;
|
|
14
|
+
/** Maximum concurrent agents (default: 3) */
|
|
15
|
+
max?: number;
|
|
16
|
+
/** Process a single issue by ID */
|
|
17
|
+
single?: string;
|
|
18
|
+
/** Wait for agents to complete (default: true) */
|
|
19
|
+
wait?: boolean;
|
|
20
|
+
/** Show what would be done without executing (default: false) */
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
/** Git repository root (default: auto-detect) */
|
|
23
|
+
gitRoot?: string;
|
|
24
|
+
/** Callbacks for agent lifecycle events */
|
|
25
|
+
callbacks?: OrchestratorCallbacks;
|
|
26
|
+
}
|
|
27
|
+
export interface OrchestratorCallbacks {
|
|
28
|
+
onIssueSelected?: (issue: OrchestratorIssue) => void;
|
|
29
|
+
onAgentStart?: (agent: AgentProcess) => void;
|
|
30
|
+
onAgentComplete?: (agent: AgentProcess) => void;
|
|
31
|
+
onAgentError?: (agent: AgentProcess, error: Error) => void;
|
|
32
|
+
onAgentIncomplete?: (agent: AgentProcess) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface OrchestratorRunnerResult {
|
|
35
|
+
agentsSpawned: number;
|
|
36
|
+
errors: Array<{
|
|
37
|
+
issueId: string;
|
|
38
|
+
error: Error;
|
|
39
|
+
}>;
|
|
40
|
+
completed: AgentProcess[];
|
|
41
|
+
}
|
|
42
|
+
export declare function getGitRoot(): string;
|
|
43
|
+
export declare function formatDuration(ms: number): string;
|
|
44
|
+
export declare function runOrchestrator(config: OrchestratorRunnerConfig): Promise<OrchestratorRunnerResult>;
|
|
45
|
+
//# sourceMappingURL=orchestrator-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/orchestrator-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAA;AAM7B,MAAM,WAAW,wBAAwB;IACvC,wCAAwC;IACxC,YAAY,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kDAAkD;IAClD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,qBAAqB,CAAA;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACpD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IAC/C,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC1D,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;CAClD;AAED,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAA;IAChD,SAAS,EAAE,YAAY,EAAE,CAAA;CAC1B;AAMD,wBAAgB,UAAU,IAAI,MAAM,CASnC;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAYjD;AA0CD,wBAAsB,eAAe,CACnC,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,wBAAwB,CAAC,CAqFnC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator Runner -- Programmatic API for the orchestrator CLI.
|
|
3
|
+
*
|
|
4
|
+
* Exports `runOrchestrator()` so the orchestrator can be invoked from code
|
|
5
|
+
* (e.g. Next.js route handlers, tests, or custom scripts) without going
|
|
6
|
+
* through process.argv / process.env / process.exit.
|
|
7
|
+
*/
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { createOrchestrator, } from '@supaku/agentfactory';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export function getGitRoot() {
|
|
15
|
+
try {
|
|
16
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
19
|
+
}).trim();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return process.cwd();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function formatDuration(ms) {
|
|
26
|
+
const seconds = Math.floor(ms / 1000);
|
|
27
|
+
const minutes = Math.floor(seconds / 60);
|
|
28
|
+
const hours = Math.floor(minutes / 60);
|
|
29
|
+
if (hours > 0) {
|
|
30
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
31
|
+
}
|
|
32
|
+
if (minutes > 0) {
|
|
33
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
34
|
+
}
|
|
35
|
+
return `${seconds}s`;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Default callbacks (console.log-based, matching the original CLI output)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
function defaultCallbacks() {
|
|
41
|
+
return {
|
|
42
|
+
onIssueSelected: (issue) => {
|
|
43
|
+
console.log(`Selected: ${issue.identifier} - ${issue.title}`);
|
|
44
|
+
console.log(` URL: ${issue.url}`);
|
|
45
|
+
console.log(` Labels: ${issue.labels.join(', ') || 'none'}`);
|
|
46
|
+
},
|
|
47
|
+
onAgentStart: (agent) => {
|
|
48
|
+
console.log(`Agent started: ${agent.identifier} (PID: ${agent.pid})`);
|
|
49
|
+
console.log(` Worktree: ${agent.worktreePath}`);
|
|
50
|
+
},
|
|
51
|
+
onAgentComplete: (agent) => {
|
|
52
|
+
const duration = agent.completedAt
|
|
53
|
+
? formatDuration(agent.completedAt.getTime() - agent.startedAt.getTime())
|
|
54
|
+
: 'unknown';
|
|
55
|
+
console.log(`Agent completed: ${agent.identifier} (${duration})`);
|
|
56
|
+
},
|
|
57
|
+
onAgentError: (_agent, error) => {
|
|
58
|
+
console.error(`Agent failed: ${_agent.identifier}`);
|
|
59
|
+
console.error(` Error: ${error.message}`);
|
|
60
|
+
},
|
|
61
|
+
onAgentIncomplete: (agent) => {
|
|
62
|
+
const duration = agent.completedAt
|
|
63
|
+
? formatDuration(agent.completedAt.getTime() - agent.startedAt.getTime())
|
|
64
|
+
: 'unknown';
|
|
65
|
+
console.warn(`Agent incomplete: ${agent.identifier} (${duration})`);
|
|
66
|
+
console.warn(` Reason: ${agent.incompleteReason ?? 'unknown'}`);
|
|
67
|
+
console.warn(` Worktree preserved: ${agent.worktreePath}`);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Runner
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
export async function runOrchestrator(config) {
|
|
75
|
+
const maxConcurrent = config.max ?? 3;
|
|
76
|
+
const wait = config.wait ?? true;
|
|
77
|
+
const dryRun = config.dryRun ?? false;
|
|
78
|
+
const gitRoot = config.gitRoot ?? getGitRoot();
|
|
79
|
+
const cb = config.callbacks ?? defaultCallbacks();
|
|
80
|
+
const orchestrator = createOrchestrator({
|
|
81
|
+
project: config.project,
|
|
82
|
+
maxConcurrent,
|
|
83
|
+
worktreePath: path.resolve(gitRoot, '.worktrees'),
|
|
84
|
+
linearApiKey: config.linearApiKey,
|
|
85
|
+
}, {
|
|
86
|
+
onIssueSelected: cb.onIssueSelected,
|
|
87
|
+
onAgentStart: cb.onAgentStart,
|
|
88
|
+
onAgentComplete: cb.onAgentComplete,
|
|
89
|
+
onAgentError: cb.onAgentError,
|
|
90
|
+
onAgentIncomplete: cb.onAgentIncomplete,
|
|
91
|
+
});
|
|
92
|
+
const result = {
|
|
93
|
+
agentsSpawned: 0,
|
|
94
|
+
errors: [],
|
|
95
|
+
completed: [],
|
|
96
|
+
};
|
|
97
|
+
// --single mode ----------------------------------------------------------
|
|
98
|
+
if (config.single) {
|
|
99
|
+
if (dryRun) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
await orchestrator.spawnAgentForIssue(config.single);
|
|
103
|
+
result.agentsSpawned = 1;
|
|
104
|
+
if (wait) {
|
|
105
|
+
// Wire up SIGINT so callers running from a terminal can stop agents
|
|
106
|
+
const sigintHandler = () => {
|
|
107
|
+
orchestrator.stopAll();
|
|
108
|
+
};
|
|
109
|
+
process.on('SIGINT', sigintHandler);
|
|
110
|
+
try {
|
|
111
|
+
const completed = await orchestrator.waitForAll();
|
|
112
|
+
result.completed = completed;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
// --dry-run mode ---------------------------------------------------------
|
|
121
|
+
if (dryRun) {
|
|
122
|
+
await orchestrator.getBacklogIssues();
|
|
123
|
+
// Nothing to spawn in dry-run; caller can inspect issues via callbacks
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
// Normal run -------------------------------------------------------------
|
|
127
|
+
const runResult = await orchestrator.run();
|
|
128
|
+
result.agentsSpawned = runResult.agents.length;
|
|
129
|
+
result.errors = runResult.errors;
|
|
130
|
+
if (wait && runResult.agents.length > 0) {
|
|
131
|
+
const sigintHandler = () => {
|
|
132
|
+
orchestrator.stopAll();
|
|
133
|
+
};
|
|
134
|
+
process.on('SIGINT', sigintHandler);
|
|
135
|
+
try {
|
|
136
|
+
const completed = await orchestrator.waitForAll();
|
|
137
|
+
result.completed = completed;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|