@supaku/agentfactory-cli 0.1.2 → 0.3.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/dist/src/analyze-logs.d.ts +26 -0
- package/dist/src/analyze-logs.d.ts.map +1 -0
- package/dist/src/analyze-logs.js +317 -0
- package/dist/src/cleanup.d.ts +21 -0
- package/dist/src/cleanup.d.ts.map +1 -0
- package/dist/src/cleanup.js +309 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +16 -0
- package/dist/src/queue-admin.d.ts +24 -0
- package/dist/src/queue-admin.d.ts.map +1 -0
- package/dist/src/queue-admin.js +418 -0
- package/dist/src/worker-fleet.d.ts +23 -0
- package/dist/src/worker-fleet.d.ts.map +1 -0
- package/dist/src/worker-fleet.js +256 -0
- package/dist/src/worker.d.ts +10 -10
- package/dist/src/worker.js +734 -50
- package/package.json +9 -4
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentFactory Log Analyzer CLI
|
|
4
|
+
*
|
|
5
|
+
* Analyzes agent session logs for errors and improvement opportunities.
|
|
6
|
+
* Can automatically create deduplicated Linear issues in the backlog.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* af-analyze-logs [options]
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --session <id> Analyze a specific session
|
|
13
|
+
* --follow, -f Watch for new sessions and analyze as they complete
|
|
14
|
+
* --interval <ms> Poll interval in milliseconds (default: 5000)
|
|
15
|
+
* --dry-run Show what would be created without creating issues
|
|
16
|
+
* --cleanup Cleanup old logs based on retention policy
|
|
17
|
+
* --verbose Show detailed analysis output
|
|
18
|
+
* --help, -h Show this help message
|
|
19
|
+
*
|
|
20
|
+
* Environment (loaded from .env.local in CWD):
|
|
21
|
+
* LINEAR_API_KEY Required for issue creation
|
|
22
|
+
* AGENT_LOG_RETENTION_DAYS Days before cleanup (default: 7)
|
|
23
|
+
* AGENT_LOGS_DIR Base directory for logs (default: .agent-logs)
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=analyze-logs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-logs.d.ts","sourceRoot":"","sources":["../../src/analyze-logs.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentFactory Log Analyzer CLI
|
|
4
|
+
*
|
|
5
|
+
* Analyzes agent session logs for errors and improvement opportunities.
|
|
6
|
+
* Can automatically create deduplicated Linear issues in the backlog.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* af-analyze-logs [options]
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --session <id> Analyze a specific session
|
|
13
|
+
* --follow, -f Watch for new sessions and analyze as they complete
|
|
14
|
+
* --interval <ms> Poll interval in milliseconds (default: 5000)
|
|
15
|
+
* --dry-run Show what would be created without creating issues
|
|
16
|
+
* --cleanup Cleanup old logs based on retention policy
|
|
17
|
+
* --verbose Show detailed analysis output
|
|
18
|
+
* --help, -h Show this help message
|
|
19
|
+
*
|
|
20
|
+
* Environment (loaded from .env.local in CWD):
|
|
21
|
+
* LINEAR_API_KEY Required for issue creation
|
|
22
|
+
* AGENT_LOG_RETENTION_DAYS Days before cleanup (default: 7)
|
|
23
|
+
* AGENT_LOGS_DIR Base directory for logs (default: .agent-logs)
|
|
24
|
+
*/
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { execSync } from 'child_process';
|
|
27
|
+
import { config } from 'dotenv';
|
|
28
|
+
// Load environment variables from .env.local in CWD
|
|
29
|
+
config({ path: path.resolve(process.cwd(), '.env.local') });
|
|
30
|
+
import { createLogAnalyzer } from '@supaku/agentfactory';
|
|
31
|
+
/**
|
|
32
|
+
* Get the git repository root directory
|
|
33
|
+
*/
|
|
34
|
+
function getGitRoot() {
|
|
35
|
+
try {
|
|
36
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
37
|
+
encoding: 'utf-8',
|
|
38
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
|
+
}).trim();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return process.cwd();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const DEFAULT_POLL_INTERVAL = 5000;
|
|
46
|
+
function parseArgs() {
|
|
47
|
+
const args = process.argv.slice(2);
|
|
48
|
+
const result = {
|
|
49
|
+
follow: false,
|
|
50
|
+
interval: DEFAULT_POLL_INTERVAL,
|
|
51
|
+
dryRun: false,
|
|
52
|
+
cleanup: false,
|
|
53
|
+
verbose: false,
|
|
54
|
+
showHelp: false,
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
switch (arg) {
|
|
59
|
+
case '--session':
|
|
60
|
+
result.sessionId = args[++i];
|
|
61
|
+
break;
|
|
62
|
+
case '--follow':
|
|
63
|
+
case '-f':
|
|
64
|
+
result.follow = true;
|
|
65
|
+
break;
|
|
66
|
+
case '--interval':
|
|
67
|
+
result.interval = parseInt(args[++i], 10) || DEFAULT_POLL_INTERVAL;
|
|
68
|
+
break;
|
|
69
|
+
case '--dry-run':
|
|
70
|
+
result.dryRun = true;
|
|
71
|
+
break;
|
|
72
|
+
case '--cleanup':
|
|
73
|
+
result.cleanup = true;
|
|
74
|
+
break;
|
|
75
|
+
case '--verbose':
|
|
76
|
+
result.verbose = true;
|
|
77
|
+
break;
|
|
78
|
+
case '--help':
|
|
79
|
+
case '-h':
|
|
80
|
+
result.showHelp = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function printHelp() {
|
|
87
|
+
console.log(`
|
|
88
|
+
AgentFactory Log Analyzer - Analyze agent session logs for errors and improvements
|
|
89
|
+
|
|
90
|
+
Usage:
|
|
91
|
+
af-analyze-logs [options]
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
--session <id> Analyze a specific session
|
|
95
|
+
--follow, -f Watch for new sessions and analyze as they complete
|
|
96
|
+
--interval <ms> Poll interval in milliseconds (default: 5000)
|
|
97
|
+
--dry-run Show what would be created without creating issues
|
|
98
|
+
--cleanup Cleanup old logs based on retention policy
|
|
99
|
+
--verbose Show detailed analysis output
|
|
100
|
+
--help, -h Show this help message
|
|
101
|
+
|
|
102
|
+
Environment Variables:
|
|
103
|
+
LINEAR_API_KEY Required for creating Linear issues
|
|
104
|
+
AGENT_LOG_RETENTION_DAYS Days before log cleanup (default: 7)
|
|
105
|
+
AGENT_LOGS_DIR Base directory for logs (default: .agent-logs)
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
# Analyze all unprocessed sessions
|
|
109
|
+
af-analyze-logs
|
|
110
|
+
|
|
111
|
+
# Analyze a specific session
|
|
112
|
+
af-analyze-logs --session abc123
|
|
113
|
+
|
|
114
|
+
# Watch for new sessions and analyze continuously
|
|
115
|
+
af-analyze-logs --follow
|
|
116
|
+
|
|
117
|
+
# Watch with custom poll interval (10 seconds)
|
|
118
|
+
af-analyze-logs -f --interval 10000
|
|
119
|
+
|
|
120
|
+
# Preview what issues would be created (no actual changes)
|
|
121
|
+
af-analyze-logs --dry-run
|
|
122
|
+
|
|
123
|
+
# Clean up logs older than retention period
|
|
124
|
+
af-analyze-logs --cleanup
|
|
125
|
+
|
|
126
|
+
# Show detailed output during analysis
|
|
127
|
+
af-analyze-logs --verbose
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Analyze a single session and print results
|
|
132
|
+
*/
|
|
133
|
+
async function analyzeAndPrintSession(analyzer, sessionId, options, stats) {
|
|
134
|
+
console.log(`\nAnalyzing session: ${sessionId}`);
|
|
135
|
+
console.log('-'.repeat(50));
|
|
136
|
+
const result = analyzer.analyzeSession(sessionId);
|
|
137
|
+
if (!result) {
|
|
138
|
+
console.log(' [SKIP] Session not found or incomplete');
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
console.log(` Issue: ${result.metadata.issueIdentifier}`);
|
|
142
|
+
console.log(` Work Type: ${result.metadata.workType}`);
|
|
143
|
+
console.log(` Status: ${result.metadata.status}`);
|
|
144
|
+
console.log(` Events: ${result.eventsAnalyzed}`);
|
|
145
|
+
console.log(` Errors: ${result.errorsFound}`);
|
|
146
|
+
console.log(` Patterns: ${result.patterns.length}`);
|
|
147
|
+
stats.sessionsAnalyzed++;
|
|
148
|
+
stats.totalErrors += result.errorsFound;
|
|
149
|
+
stats.totalPatterns += result.patterns.length;
|
|
150
|
+
if (options.verbose && result.patterns.length > 0) {
|
|
151
|
+
console.log('\n Detected Patterns:');
|
|
152
|
+
for (const pattern of result.patterns) {
|
|
153
|
+
console.log(` - [${pattern.severity}] ${pattern.title}`);
|
|
154
|
+
console.log(` Type: ${pattern.type}, Occurrences: ${pattern.occurrences}`);
|
|
155
|
+
if (pattern.tool) {
|
|
156
|
+
console.log(` Tool: ${pattern.tool}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (result.suggestedIssues.length > 0) {
|
|
161
|
+
console.log(`\n Suggested Issues: ${result.suggestedIssues.length}`);
|
|
162
|
+
if (options.verbose) {
|
|
163
|
+
for (const issue of result.suggestedIssues) {
|
|
164
|
+
console.log(` - ${issue.title}`);
|
|
165
|
+
console.log(` Signature: ${issue.signature}`);
|
|
166
|
+
console.log(` Labels: ${issue.labels.join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const issueResults = await analyzer.createIssues(result.suggestedIssues, sessionId, options.dryRun);
|
|
171
|
+
for (const issueResult of issueResults) {
|
|
172
|
+
if (issueResult.created) {
|
|
173
|
+
console.log(` [${options.dryRun ? 'WOULD CREATE' : 'CREATED'}] ${issueResult.identifier}`);
|
|
174
|
+
stats.issuesCreated++;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(` [${options.dryRun ? 'WOULD UPDATE' : 'UPDATED'}] ${issueResult.identifier}`);
|
|
178
|
+
stats.issuesUpdated++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.log(` [ERROR] Failed to create issues: ${error instanceof Error ? error.message : String(error)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!options.dryRun) {
|
|
187
|
+
analyzer.markProcessed(sessionId, result);
|
|
188
|
+
console.log(' [PROCESSED]');
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Print summary statistics
|
|
194
|
+
*/
|
|
195
|
+
function printSummary(stats, dryRun) {
|
|
196
|
+
console.log('\n' + '='.repeat(50));
|
|
197
|
+
console.log('=== Summary ===\n');
|
|
198
|
+
console.log(` Sessions analyzed: ${stats.sessionsAnalyzed}`);
|
|
199
|
+
console.log(` Total errors found: ${stats.totalErrors}`);
|
|
200
|
+
console.log(` Total patterns detected: ${stats.totalPatterns}`);
|
|
201
|
+
console.log(` Issues created: ${stats.issuesCreated}${dryRun ? ' (dry run)' : ''}`);
|
|
202
|
+
console.log(` Issues updated: ${stats.issuesUpdated}${dryRun ? ' (dry run)' : ''}`);
|
|
203
|
+
console.log('');
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Format time for display
|
|
207
|
+
*/
|
|
208
|
+
function formatTime() {
|
|
209
|
+
return new Date().toLocaleTimeString();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Watch mode - continuously poll for new sessions
|
|
213
|
+
*/
|
|
214
|
+
async function runFollowMode(analyzer, options) {
|
|
215
|
+
const stats = {
|
|
216
|
+
sessionsAnalyzed: 0,
|
|
217
|
+
totalErrors: 0,
|
|
218
|
+
totalPatterns: 0,
|
|
219
|
+
issuesCreated: 0,
|
|
220
|
+
issuesUpdated: 0,
|
|
221
|
+
};
|
|
222
|
+
const processedInSession = new Set();
|
|
223
|
+
console.log(`[${formatTime()}] Watching for new sessions (poll interval: ${options.interval}ms)`);
|
|
224
|
+
console.log(`[${formatTime()}] Press Ctrl+C to stop\n`);
|
|
225
|
+
let running = true;
|
|
226
|
+
const shutdown = () => {
|
|
227
|
+
console.log(`\n[${formatTime()}] Stopping...`);
|
|
228
|
+
running = false;
|
|
229
|
+
printSummary(stats, options.dryRun);
|
|
230
|
+
process.exit(0);
|
|
231
|
+
};
|
|
232
|
+
process.on('SIGINT', shutdown);
|
|
233
|
+
process.on('SIGTERM', shutdown);
|
|
234
|
+
// Initial check for existing unprocessed sessions
|
|
235
|
+
const initialSessions = analyzer.getUnprocessedSessions();
|
|
236
|
+
if (initialSessions.length > 0) {
|
|
237
|
+
console.log(`[${formatTime()}] Found ${initialSessions.length} existing unprocessed session(s)`);
|
|
238
|
+
for (const sid of initialSessions) {
|
|
239
|
+
if (!running)
|
|
240
|
+
break;
|
|
241
|
+
await analyzeAndPrintSession(analyzer, sid, options, stats);
|
|
242
|
+
processedInSession.add(sid);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Poll loop
|
|
246
|
+
while (running) {
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, options.interval));
|
|
248
|
+
if (!running)
|
|
249
|
+
break;
|
|
250
|
+
const sessions = analyzer.getUnprocessedSessions();
|
|
251
|
+
const newSessions = sessions.filter((s) => !processedInSession.has(s));
|
|
252
|
+
if (newSessions.length > 0) {
|
|
253
|
+
console.log(`[${formatTime()}] Found ${newSessions.length} new session(s) ready for analysis`);
|
|
254
|
+
for (const sid of newSessions) {
|
|
255
|
+
if (!running)
|
|
256
|
+
break;
|
|
257
|
+
const analyzed = await analyzeAndPrintSession(analyzer, sid, options, stats);
|
|
258
|
+
if (analyzed) {
|
|
259
|
+
processedInSession.add(sid);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function main() {
|
|
266
|
+
const options = parseArgs();
|
|
267
|
+
if (options.showHelp) {
|
|
268
|
+
printHelp();
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
const gitRoot = getGitRoot();
|
|
272
|
+
const logsDir = process.env.AGENT_LOGS_DIR ?? `${gitRoot}/.agent-logs`;
|
|
273
|
+
console.log('\n=== AgentFactory Log Analyzer ===\n');
|
|
274
|
+
if (options.dryRun) {
|
|
275
|
+
console.log('[DRY RUN MODE - No issues will be created]\n');
|
|
276
|
+
}
|
|
277
|
+
const analyzer = createLogAnalyzer({ logsDir });
|
|
278
|
+
if (options.cleanup) {
|
|
279
|
+
console.log('Cleaning up old logs...\n');
|
|
280
|
+
const deleted = analyzer.cleanupOldLogs();
|
|
281
|
+
console.log(`Deleted ${deleted} old log entries.\n`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (options.follow) {
|
|
285
|
+
await runFollowMode(analyzer, options);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Standard one-shot mode
|
|
289
|
+
const stats = {
|
|
290
|
+
sessionsAnalyzed: 0,
|
|
291
|
+
totalErrors: 0,
|
|
292
|
+
totalPatterns: 0,
|
|
293
|
+
issuesCreated: 0,
|
|
294
|
+
issuesUpdated: 0,
|
|
295
|
+
};
|
|
296
|
+
let sessionsToAnalyze;
|
|
297
|
+
if (options.sessionId) {
|
|
298
|
+
sessionsToAnalyze = [options.sessionId];
|
|
299
|
+
console.log(`Analyzing session: ${options.sessionId}\n`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
sessionsToAnalyze = analyzer.getUnprocessedSessions();
|
|
303
|
+
console.log(`Found ${sessionsToAnalyze.length} unprocessed session(s)\n`);
|
|
304
|
+
}
|
|
305
|
+
if (sessionsToAnalyze.length === 0) {
|
|
306
|
+
console.log('No sessions to analyze.\n');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
for (const sid of sessionsToAnalyze) {
|
|
310
|
+
await analyzeAndPrintSession(analyzer, sid, options, stats);
|
|
311
|
+
}
|
|
312
|
+
printSummary(stats, options.dryRun);
|
|
313
|
+
}
|
|
314
|
+
main().catch((error) => {
|
|
315
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentFactory Worktree Cleanup
|
|
4
|
+
*
|
|
5
|
+
* Cleans up orphaned git worktrees in the .worktrees/ directory.
|
|
6
|
+
* Run this script periodically to remove worktrees from:
|
|
7
|
+
* - Crashed agent sessions
|
|
8
|
+
* - Completed work where cleanup failed
|
|
9
|
+
* - Stale branches that have been merged/deleted
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* af-cleanup [options]
|
|
13
|
+
*
|
|
14
|
+
* Options:
|
|
15
|
+
* --dry-run Show what would be cleaned up without removing anything
|
|
16
|
+
* --force Force removal even if worktree appears active
|
|
17
|
+
* --path <dir> Custom worktrees directory (default: .worktrees)
|
|
18
|
+
* --help, -h Show this help message
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../src/cleanup.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;GAiBG"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentFactory Worktree Cleanup
|
|
4
|
+
*
|
|
5
|
+
* Cleans up orphaned git worktrees in the .worktrees/ directory.
|
|
6
|
+
* Run this script periodically to remove worktrees from:
|
|
7
|
+
* - Crashed agent sessions
|
|
8
|
+
* - Completed work where cleanup failed
|
|
9
|
+
* - Stale branches that have been merged/deleted
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* af-cleanup [options]
|
|
13
|
+
*
|
|
14
|
+
* Options:
|
|
15
|
+
* --dry-run Show what would be cleaned up without removing anything
|
|
16
|
+
* --force Force removal even if worktree appears active
|
|
17
|
+
* --path <dir> Custom worktrees directory (default: .worktrees)
|
|
18
|
+
* --help, -h Show this help message
|
|
19
|
+
*/
|
|
20
|
+
import { execSync } from 'child_process';
|
|
21
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
22
|
+
import { resolve, basename } from 'path';
|
|
23
|
+
/**
|
|
24
|
+
* Get the git repository root directory
|
|
25
|
+
*/
|
|
26
|
+
function getGitRoot() {
|
|
27
|
+
try {
|
|
28
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
29
|
+
encoding: 'utf-8',
|
|
30
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
+
}).trim();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return process.cwd();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function parseArgs() {
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const gitRoot = getGitRoot();
|
|
40
|
+
const result = {
|
|
41
|
+
dryRun: false,
|
|
42
|
+
force: false,
|
|
43
|
+
worktreePath: resolve(gitRoot, '.worktrees'),
|
|
44
|
+
};
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
const arg = args[i];
|
|
47
|
+
switch (arg) {
|
|
48
|
+
case '--dry-run':
|
|
49
|
+
result.dryRun = true;
|
|
50
|
+
break;
|
|
51
|
+
case '--force':
|
|
52
|
+
result.force = true;
|
|
53
|
+
break;
|
|
54
|
+
case '--path':
|
|
55
|
+
result.worktreePath = args[++i];
|
|
56
|
+
break;
|
|
57
|
+
case '--help':
|
|
58
|
+
case '-h':
|
|
59
|
+
printHelp();
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function printHelp() {
|
|
66
|
+
console.log(`
|
|
67
|
+
AgentFactory Worktree Cleanup - Remove orphaned git worktrees
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
af-cleanup [options]
|
|
71
|
+
|
|
72
|
+
Options:
|
|
73
|
+
--dry-run Show what would be cleaned up without removing
|
|
74
|
+
--force Force removal even if worktree appears active
|
|
75
|
+
--path <dir> Custom worktrees directory (default: .worktrees)
|
|
76
|
+
--help, -h Show this help message
|
|
77
|
+
|
|
78
|
+
Orphaned worktrees are identified by:
|
|
79
|
+
- Branch no longer exists (merged/deleted)
|
|
80
|
+
- Not listed in 'git worktree list' (stale directory)
|
|
81
|
+
- Lock file exists but is stale
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
# Preview what would be cleaned up
|
|
85
|
+
af-cleanup --dry-run
|
|
86
|
+
|
|
87
|
+
# Clean up orphaned worktrees
|
|
88
|
+
af-cleanup
|
|
89
|
+
|
|
90
|
+
# Force cleanup all worktrees (use with caution)
|
|
91
|
+
af-cleanup --force
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get list of git worktrees from 'git worktree list'
|
|
96
|
+
*/
|
|
97
|
+
function getGitWorktrees() {
|
|
98
|
+
const worktrees = new Map();
|
|
99
|
+
try {
|
|
100
|
+
const output = execSync('git worktree list --porcelain', {
|
|
101
|
+
encoding: 'utf-8',
|
|
102
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
|
+
});
|
|
104
|
+
let currentPath = '';
|
|
105
|
+
for (const line of output.split('\n')) {
|
|
106
|
+
if (line.startsWith('worktree ')) {
|
|
107
|
+
currentPath = line.substring(9);
|
|
108
|
+
}
|
|
109
|
+
else if (line.startsWith('branch ')) {
|
|
110
|
+
const branch = line.substring(7).replace('refs/heads/', '');
|
|
111
|
+
worktrees.set(currentPath, branch);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error('Failed to list git worktrees:', error);
|
|
117
|
+
}
|
|
118
|
+
return worktrees;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if a git branch exists
|
|
122
|
+
*/
|
|
123
|
+
function branchExists(branchName) {
|
|
124
|
+
try {
|
|
125
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, {
|
|
126
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
});
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Scan the worktrees directory and identify orphaned worktrees
|
|
136
|
+
*/
|
|
137
|
+
function scanWorktrees(options) {
|
|
138
|
+
const worktreesDir = resolve(options.worktreePath);
|
|
139
|
+
const result = [];
|
|
140
|
+
if (!existsSync(worktreesDir)) {
|
|
141
|
+
console.log(`Worktrees directory not found: ${worktreesDir}`);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
const gitWorktrees = getGitWorktrees();
|
|
145
|
+
const entries = readdirSync(worktreesDir);
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const entryPath = resolve(worktreesDir, entry);
|
|
148
|
+
try {
|
|
149
|
+
if (!statSync(entryPath).isDirectory()) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const info = {
|
|
157
|
+
path: entryPath,
|
|
158
|
+
branch: entry,
|
|
159
|
+
isOrphaned: false,
|
|
160
|
+
};
|
|
161
|
+
const isKnownWorktree = gitWorktrees.has(entryPath);
|
|
162
|
+
const branchName = isKnownWorktree ? gitWorktrees.get(entryPath) : entry;
|
|
163
|
+
if (options.force) {
|
|
164
|
+
info.isOrphaned = true;
|
|
165
|
+
info.reason = 'force cleanup requested';
|
|
166
|
+
}
|
|
167
|
+
else if (!isKnownWorktree) {
|
|
168
|
+
info.isOrphaned = true;
|
|
169
|
+
info.reason = 'not registered with git worktree';
|
|
170
|
+
}
|
|
171
|
+
else if (!branchExists(branchName)) {
|
|
172
|
+
info.isOrphaned = true;
|
|
173
|
+
info.reason = `branch '${branchName}' no longer exists`;
|
|
174
|
+
}
|
|
175
|
+
result.push(info);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Remove a single worktree
|
|
181
|
+
*/
|
|
182
|
+
function removeWorktree(worktreePath) {
|
|
183
|
+
try {
|
|
184
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
185
|
+
encoding: 'utf-8',
|
|
186
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
187
|
+
});
|
|
188
|
+
return { success: true };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
try {
|
|
192
|
+
execSync(`rm -rf "${worktreePath}"`, {
|
|
193
|
+
encoding: 'utf-8',
|
|
194
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
195
|
+
});
|
|
196
|
+
execSync('git worktree prune', {
|
|
197
|
+
encoding: 'utf-8',
|
|
198
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
199
|
+
});
|
|
200
|
+
return { success: true };
|
|
201
|
+
}
|
|
202
|
+
catch (rmError) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: rmError instanceof Error ? rmError.message : String(rmError),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Main cleanup function
|
|
212
|
+
*/
|
|
213
|
+
function cleanup(options) {
|
|
214
|
+
const result = {
|
|
215
|
+
scanned: 0,
|
|
216
|
+
orphaned: 0,
|
|
217
|
+
cleaned: 0,
|
|
218
|
+
errors: [],
|
|
219
|
+
};
|
|
220
|
+
console.log('Scanning worktrees...\n');
|
|
221
|
+
try {
|
|
222
|
+
execSync('git worktree prune', {
|
|
223
|
+
encoding: 'utf-8',
|
|
224
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
console.log('Note: Could not prune git worktree metadata');
|
|
229
|
+
}
|
|
230
|
+
const worktrees = scanWorktrees(options);
|
|
231
|
+
result.scanned = worktrees.length;
|
|
232
|
+
if (worktrees.length === 0) {
|
|
233
|
+
console.log('No worktrees found.\n');
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
console.log(`Found ${worktrees.length} worktree(s) in ${options.worktreePath}/\n`);
|
|
237
|
+
for (const wt of worktrees) {
|
|
238
|
+
const status = wt.isOrphaned ? ' orphaned' : ' active';
|
|
239
|
+
const reason = wt.reason ? ` (${wt.reason})` : '';
|
|
240
|
+
console.log(` ${status}: ${basename(wt.path)}${reason}`);
|
|
241
|
+
}
|
|
242
|
+
console.log('');
|
|
243
|
+
const orphaned = worktrees.filter((wt) => wt.isOrphaned);
|
|
244
|
+
result.orphaned = orphaned.length;
|
|
245
|
+
if (orphaned.length === 0) {
|
|
246
|
+
console.log('No orphaned worktrees to clean up.\n');
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
if (options.dryRun) {
|
|
250
|
+
console.log(`[DRY RUN] Would clean up ${orphaned.length} orphaned worktree(s):\n`);
|
|
251
|
+
for (const wt of orphaned) {
|
|
252
|
+
console.log(` Would remove: ${wt.path}`);
|
|
253
|
+
}
|
|
254
|
+
console.log('');
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
console.log(`Cleaning up ${orphaned.length} orphaned worktree(s)...\n`);
|
|
258
|
+
for (const wt of orphaned) {
|
|
259
|
+
process.stdout.write(` Removing ${basename(wt.path)}... `);
|
|
260
|
+
const removal = removeWorktree(wt.path);
|
|
261
|
+
if (removal.success) {
|
|
262
|
+
console.log('done');
|
|
263
|
+
result.cleaned++;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
console.log(`FAILED: ${removal.error}`);
|
|
267
|
+
result.errors.push({ path: wt.path, error: removal.error || 'Unknown error' });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
console.log('');
|
|
271
|
+
try {
|
|
272
|
+
execSync('git worktree prune', {
|
|
273
|
+
encoding: 'utf-8',
|
|
274
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
275
|
+
});
|
|
276
|
+
console.log('Pruned git worktree metadata.\n');
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Ignore prune errors
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
// Main execution
|
|
284
|
+
function main() {
|
|
285
|
+
const options = parseArgs();
|
|
286
|
+
console.log('\n=== AgentFactory Worktree Cleanup ===\n');
|
|
287
|
+
if (options.dryRun) {
|
|
288
|
+
console.log('[DRY RUN MODE - No changes will be made]\n');
|
|
289
|
+
}
|
|
290
|
+
if (options.force) {
|
|
291
|
+
console.log('[FORCE MODE - All worktrees will be removed]\n');
|
|
292
|
+
}
|
|
293
|
+
const result = cleanup(options);
|
|
294
|
+
console.log('=== Summary ===\n');
|
|
295
|
+
console.log(` Scanned: ${result.scanned} worktree(s)`);
|
|
296
|
+
console.log(` Orphaned: ${result.orphaned}`);
|
|
297
|
+
console.log(` Cleaned: ${result.cleaned}`);
|
|
298
|
+
if (result.errors.length > 0) {
|
|
299
|
+
console.log(` Errors: ${result.errors.length}`);
|
|
300
|
+
for (const err of result.errors) {
|
|
301
|
+
console.log(` - ${basename(err.path)}: ${err.error}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
console.log('');
|
|
305
|
+
if (result.errors.length > 0) {
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
main();
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,QAAA,MAAM,OAAO,QAAkB,CAAA;AAE/B,iBAAS,SAAS,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,QAAA,MAAM,OAAO,QAAkB,CAAA;AAE/B,iBAAS,SAAS,IAAI,IAAI,CAoBzB"}
|