@orgloop/agentctl 1.1.0 → 1.2.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/README.md +145 -3
- package/dist/adapters/claude-code.js +23 -12
- package/dist/adapters/codex.d.ts +72 -0
- package/dist/adapters/codex.js +702 -0
- package/dist/adapters/openclaw.d.ts +60 -9
- package/dist/adapters/openclaw.js +195 -38
- package/dist/adapters/opencode.d.ts +143 -0
- package/dist/adapters/opencode.js +682 -0
- package/dist/adapters/pi-rust.d.ts +89 -0
- package/dist/adapters/pi-rust.js +753 -0
- package/dist/adapters/pi.d.ts +96 -0
- package/dist/adapters/pi.js +865 -0
- package/dist/cli.js +332 -60
- package/dist/core/types.d.ts +1 -0
- package/dist/daemon/server.js +152 -21
- package/dist/daemon/session-tracker.d.ts +24 -0
- package/dist/daemon/session-tracker.js +149 -4
- package/dist/daemon/state.d.ts +1 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +4 -0
- package/dist/launch-orchestrator.d.ts +60 -0
- package/dist/launch-orchestrator.js +198 -0
- package/dist/matrix-parser.d.ts +40 -0
- package/dist/matrix-parser.js +69 -0
- package/dist/utils/daemon-env.d.ts +16 -0
- package/dist/utils/daemon-env.js +85 -0
- package/dist/utils/partial-read.d.ts +20 -0
- package/dist/utils/partial-read.js +66 -0
- package/dist/utils/resolve-binary.d.ts +14 -0
- package/dist/utils/resolve-binary.js +66 -0
- package/dist/worktree.d.ts +22 -0
- package/dist/worktree.js +68 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -9,21 +9,34 @@ import path from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { ClaudeCodeAdapter } from "./adapters/claude-code.js";
|
|
12
|
+
import { CodexAdapter } from "./adapters/codex.js";
|
|
12
13
|
import { OpenClawAdapter } from "./adapters/openclaw.js";
|
|
14
|
+
import { OpenCodeAdapter } from "./adapters/opencode.js";
|
|
15
|
+
import { PiAdapter } from "./adapters/pi.js";
|
|
16
|
+
import { PiRustAdapter } from "./adapters/pi-rust.js";
|
|
13
17
|
import { DaemonClient } from "./client/daemon-client.js";
|
|
14
18
|
import { runHook } from "./hooks.js";
|
|
19
|
+
import { orchestrateLaunch, parseAdapterSlots, } from "./launch-orchestrator.js";
|
|
20
|
+
import { expandMatrix, parseMatrixFile } from "./matrix-parser.js";
|
|
15
21
|
import { mergeSession } from "./merge.js";
|
|
16
22
|
import { createWorktree } from "./worktree.js";
|
|
17
23
|
const adapters = {
|
|
18
24
|
"claude-code": new ClaudeCodeAdapter(),
|
|
25
|
+
codex: new CodexAdapter(),
|
|
19
26
|
openclaw: new OpenClawAdapter(),
|
|
27
|
+
opencode: new OpenCodeAdapter(),
|
|
28
|
+
pi: new PiAdapter(),
|
|
29
|
+
"pi-rust": new PiRustAdapter(),
|
|
20
30
|
};
|
|
21
31
|
const client = new DaemonClient();
|
|
22
32
|
/**
|
|
23
33
|
* Ensure the daemon is running. Auto-starts it if not.
|
|
24
34
|
* Returns true if daemon is available after the call.
|
|
35
|
+
* Set AGENTCTL_NO_DAEMON=1 to skip daemon and use direct adapter mode.
|
|
25
36
|
*/
|
|
26
37
|
async function ensureDaemon() {
|
|
38
|
+
if (process.env.AGENTCTL_NO_DAEMON === "1")
|
|
39
|
+
return false;
|
|
27
40
|
if (await client.isRunning())
|
|
28
41
|
return true;
|
|
29
42
|
// Auto-start daemon in background
|
|
@@ -67,27 +80,35 @@ function getAllAdapters() {
|
|
|
67
80
|
return Object.values(adapters);
|
|
68
81
|
}
|
|
69
82
|
// --- Formatters ---
|
|
70
|
-
function formatSession(s) {
|
|
71
|
-
|
|
83
|
+
function formatSession(s, showGroup) {
|
|
84
|
+
const row = {
|
|
72
85
|
ID: s.id.slice(0, 8),
|
|
73
86
|
Status: s.status,
|
|
87
|
+
Adapter: s.adapter || "-",
|
|
74
88
|
Model: s.model || "-",
|
|
75
|
-
CWD: s.cwd ? shortenPath(s.cwd) : "-",
|
|
76
|
-
PID: s.pid?.toString() || "-",
|
|
77
|
-
Started: timeAgo(s.startedAt),
|
|
78
|
-
Prompt: (s.prompt || "-").slice(0, 60),
|
|
79
89
|
};
|
|
90
|
+
if (showGroup)
|
|
91
|
+
row.Group = s.group || "-";
|
|
92
|
+
row.CWD = s.cwd ? shortenPath(s.cwd) : "-";
|
|
93
|
+
row.PID = s.pid?.toString() || "-";
|
|
94
|
+
row.Started = timeAgo(s.startedAt);
|
|
95
|
+
row.Prompt = (s.prompt || "-").slice(0, 60);
|
|
96
|
+
return row;
|
|
80
97
|
}
|
|
81
|
-
function formatRecord(s) {
|
|
82
|
-
|
|
98
|
+
function formatRecord(s, showGroup) {
|
|
99
|
+
const row = {
|
|
83
100
|
ID: s.id.slice(0, 8),
|
|
84
101
|
Status: s.status,
|
|
102
|
+
Adapter: s.adapter || "-",
|
|
85
103
|
Model: s.model || "-",
|
|
86
|
-
CWD: s.cwd ? shortenPath(s.cwd) : "-",
|
|
87
|
-
PID: s.pid?.toString() || "-",
|
|
88
|
-
Started: timeAgo(new Date(s.startedAt)),
|
|
89
|
-
Prompt: (s.prompt || "-").slice(0, 60),
|
|
90
104
|
};
|
|
105
|
+
if (showGroup)
|
|
106
|
+
row.Group = s.group || "-";
|
|
107
|
+
row.CWD = s.cwd ? shortenPath(s.cwd) : "-";
|
|
108
|
+
row.PID = s.pid?.toString() || "-";
|
|
109
|
+
row.Started = timeAgo(new Date(s.startedAt));
|
|
110
|
+
row.Prompt = (s.prompt || "-").slice(0, 60);
|
|
111
|
+
return row;
|
|
91
112
|
}
|
|
92
113
|
function shortenPath(p) {
|
|
93
114
|
const home = process.env.HOME || "";
|
|
@@ -153,6 +174,7 @@ function sessionToJson(s) {
|
|
|
153
174
|
tokens: s.tokens,
|
|
154
175
|
cost: s.cost,
|
|
155
176
|
pid: s.pid,
|
|
177
|
+
group: s.group,
|
|
156
178
|
meta: s.meta,
|
|
157
179
|
};
|
|
158
180
|
}
|
|
@@ -168,20 +190,27 @@ program
|
|
|
168
190
|
.description("List agent sessions")
|
|
169
191
|
.option("--adapter <name>", "Filter by adapter")
|
|
170
192
|
.option("--status <status>", "Filter by status (running|stopped|idle|error)")
|
|
193
|
+
.option("--group <id>", "Filter by launch group (e.g. g-a1b2c3)")
|
|
171
194
|
.option("-a, --all", "Include stopped sessions (last 7 days)")
|
|
172
195
|
.option("--json", "Output as JSON")
|
|
173
196
|
.action(async (opts) => {
|
|
174
197
|
const daemonRunning = await ensureDaemon();
|
|
175
198
|
if (daemonRunning) {
|
|
176
|
-
|
|
199
|
+
let sessions = await client.call("session.list", {
|
|
177
200
|
status: opts.status,
|
|
178
201
|
all: opts.all,
|
|
202
|
+
adapter: opts.adapter,
|
|
203
|
+
group: opts.group,
|
|
179
204
|
});
|
|
205
|
+
if (opts.adapter) {
|
|
206
|
+
sessions = sessions.filter((s) => s.adapter === opts.adapter);
|
|
207
|
+
}
|
|
180
208
|
if (opts.json) {
|
|
181
209
|
printJson(sessions);
|
|
182
210
|
}
|
|
183
211
|
else {
|
|
184
|
-
|
|
212
|
+
const hasGroups = sessions.some((s) => s.group);
|
|
213
|
+
printTable(sessions.map((s) => formatRecord(s, hasGroups)));
|
|
185
214
|
}
|
|
186
215
|
return;
|
|
187
216
|
}
|
|
@@ -202,7 +231,8 @@ program
|
|
|
202
231
|
printJson(sessions.map(sessionToJson));
|
|
203
232
|
}
|
|
204
233
|
else {
|
|
205
|
-
|
|
234
|
+
const hasGroups = sessions.some((s) => s.group);
|
|
235
|
+
printTable(sessions.map((s) => formatSession(s, hasGroups)));
|
|
206
236
|
}
|
|
207
237
|
});
|
|
208
238
|
// status
|
|
@@ -222,7 +252,7 @@ program
|
|
|
222
252
|
printJson(session);
|
|
223
253
|
}
|
|
224
254
|
else {
|
|
225
|
-
const fmt = formatRecord(session);
|
|
255
|
+
const fmt = formatRecord(session, !!session.group);
|
|
226
256
|
for (const [k, v] of Object.entries(fmt)) {
|
|
227
257
|
console.log(`${k.padEnd(10)} ${v}`);
|
|
228
258
|
}
|
|
@@ -232,32 +262,37 @@ program
|
|
|
232
262
|
}
|
|
233
263
|
return;
|
|
234
264
|
}
|
|
235
|
-
catch
|
|
236
|
-
|
|
237
|
-
process.exit(1);
|
|
265
|
+
catch {
|
|
266
|
+
// Daemon failed — fall through to direct adapter lookup
|
|
238
267
|
}
|
|
239
268
|
}
|
|
240
|
-
// Direct fallback
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
for (const [k, v] of Object.entries(fmt)) {
|
|
250
|
-
console.log(`${k.padEnd(10)} ${v}`);
|
|
269
|
+
// Direct fallback: try specified adapter, or search all adapters
|
|
270
|
+
const statusAdapters = opts.adapter
|
|
271
|
+
? [getAdapter(opts.adapter)]
|
|
272
|
+
: getAllAdapters();
|
|
273
|
+
for (const adapter of statusAdapters) {
|
|
274
|
+
try {
|
|
275
|
+
const session = await adapter.status(id);
|
|
276
|
+
if (opts.json) {
|
|
277
|
+
printJson(sessionToJson(session));
|
|
251
278
|
}
|
|
252
|
-
|
|
253
|
-
|
|
279
|
+
else {
|
|
280
|
+
const fmt = formatSession(session, !!session.group);
|
|
281
|
+
for (const [k, v] of Object.entries(fmt)) {
|
|
282
|
+
console.log(`${k.padEnd(10)} ${v}`);
|
|
283
|
+
}
|
|
284
|
+
if (session.tokens) {
|
|
285
|
+
console.log(`Tokens in: ${session.tokens.in}, out: ${session.tokens.out}`);
|
|
286
|
+
}
|
|
254
287
|
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// Try next adapter
|
|
255
292
|
}
|
|
256
293
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
process.exit(1);
|
|
260
|
-
}
|
|
294
|
+
console.error(`Session not found: ${id}`);
|
|
295
|
+
process.exit(1);
|
|
261
296
|
});
|
|
262
297
|
// peek
|
|
263
298
|
program
|
|
@@ -276,22 +311,39 @@ program
|
|
|
276
311
|
console.log(output);
|
|
277
312
|
return;
|
|
278
313
|
}
|
|
314
|
+
catch {
|
|
315
|
+
// Daemon failed — fall through to direct adapter lookup
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Direct fallback: try specified adapter, or search all adapters
|
|
319
|
+
if (opts.adapter) {
|
|
320
|
+
const adapter = getAdapter(opts.adapter);
|
|
321
|
+
try {
|
|
322
|
+
const output = await adapter.peek(id, {
|
|
323
|
+
lines: Number.parseInt(opts.lines, 10),
|
|
324
|
+
});
|
|
325
|
+
console.log(output);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
279
328
|
catch (err) {
|
|
280
329
|
console.error(err.message);
|
|
281
330
|
process.exit(1);
|
|
282
331
|
}
|
|
283
332
|
}
|
|
284
|
-
const adapter
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
333
|
+
for (const adapter of getAllAdapters()) {
|
|
334
|
+
try {
|
|
335
|
+
const output = await adapter.peek(id, {
|
|
336
|
+
lines: Number.parseInt(opts.lines, 10),
|
|
337
|
+
});
|
|
338
|
+
console.log(output);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// Try next adapter
|
|
343
|
+
}
|
|
294
344
|
}
|
|
345
|
+
console.error(`Session not found: ${id}`);
|
|
346
|
+
process.exit(1);
|
|
295
347
|
});
|
|
296
348
|
// stop
|
|
297
349
|
program
|
|
@@ -356,7 +408,7 @@ program
|
|
|
356
408
|
// launch
|
|
357
409
|
program
|
|
358
410
|
.command("launch [adapter]")
|
|
359
|
-
.description("Launch a new agent session")
|
|
411
|
+
.description("Launch a new agent session (or multiple with --adapter flags)")
|
|
360
412
|
.requiredOption("-p, --prompt <text>", "Prompt to send")
|
|
361
413
|
.option("--spec <path>", "Spec file path")
|
|
362
414
|
.option("--cwd <dir>", "Working directory")
|
|
@@ -364,13 +416,108 @@ program
|
|
|
364
416
|
.option("--force", "Override directory locks")
|
|
365
417
|
.option("--worktree <repo>", "Auto-create git worktree from this repo before launch")
|
|
366
418
|
.option("--branch <name>", "Branch name for --worktree")
|
|
419
|
+
.option("--adapter <name>", "Adapter to launch (repeatable for parallel launch)", collectAdapter, [])
|
|
420
|
+
.option("--matrix <file>", "YAML matrix file for advanced sweep launch")
|
|
367
421
|
.option("--on-create <script>", "Hook: run after session is created")
|
|
368
422
|
.option("--on-complete <script>", "Hook: run after session completes")
|
|
369
423
|
.option("--pre-merge <script>", "Hook: run before merge")
|
|
370
424
|
.option("--post-merge <script>", "Hook: run after merge")
|
|
425
|
+
.allowUnknownOption() // Allow interleaved --adapter/--model for parseAdapterSlots
|
|
371
426
|
.action(async (adapterName, opts) => {
|
|
372
427
|
let cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();
|
|
373
|
-
|
|
428
|
+
// Collect hooks
|
|
429
|
+
const hooks = opts.onCreate || opts.onComplete || opts.preMerge || opts.postMerge
|
|
430
|
+
? {
|
|
431
|
+
onCreate: opts.onCreate,
|
|
432
|
+
onComplete: opts.onComplete,
|
|
433
|
+
preMerge: opts.preMerge,
|
|
434
|
+
postMerge: opts.postMerge,
|
|
435
|
+
}
|
|
436
|
+
: undefined;
|
|
437
|
+
// --- Multi-adapter / matrix detection ---
|
|
438
|
+
let slots = [];
|
|
439
|
+
if (opts.matrix) {
|
|
440
|
+
// Matrix file mode
|
|
441
|
+
try {
|
|
442
|
+
const matrixFile = await parseMatrixFile(opts.matrix);
|
|
443
|
+
slots = expandMatrix(matrixFile);
|
|
444
|
+
// Matrix can override cwd and prompt
|
|
445
|
+
if (matrixFile.cwd)
|
|
446
|
+
cwd = path.resolve(matrixFile.cwd);
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
console.error(`Failed to parse matrix file: ${err.message}`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
// Check for multi-adapter via raw argv parsing
|
|
455
|
+
// We need raw argv because commander can't handle interleaved
|
|
456
|
+
// --adapter A --model M1 --adapter B --model M2
|
|
457
|
+
const rawArgs = process.argv.slice(2);
|
|
458
|
+
const adapterCount = rawArgs.filter((a) => a === "--adapter" || a === "-A").length;
|
|
459
|
+
if (adapterCount > 1) {
|
|
460
|
+
// Multi-adapter mode: parse from raw args
|
|
461
|
+
slots = parseAdapterSlots(rawArgs);
|
|
462
|
+
}
|
|
463
|
+
else if (adapterCount === 1 && opts.adapter?.length === 1) {
|
|
464
|
+
// Single --adapter flag — could still be multi if model is specified
|
|
465
|
+
// but this is the normal single-adapter path via --adapter flag
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// --- Parallel launch path ---
|
|
469
|
+
if (slots.length > 1) {
|
|
470
|
+
const daemonRunning = await ensureDaemon();
|
|
471
|
+
try {
|
|
472
|
+
let groupId = "";
|
|
473
|
+
const result = await orchestrateLaunch({
|
|
474
|
+
slots,
|
|
475
|
+
prompt: opts.prompt,
|
|
476
|
+
spec: opts.spec,
|
|
477
|
+
cwd,
|
|
478
|
+
hooks,
|
|
479
|
+
adapters,
|
|
480
|
+
onSessionLaunched: (slotResult) => {
|
|
481
|
+
// Track in daemon if available
|
|
482
|
+
if (daemonRunning && !slotResult.error) {
|
|
483
|
+
client
|
|
484
|
+
.call("session.launch.track", {
|
|
485
|
+
id: slotResult.sessionId,
|
|
486
|
+
adapter: slotResult.slot.adapter,
|
|
487
|
+
cwd: slotResult.cwd,
|
|
488
|
+
group: groupId,
|
|
489
|
+
})
|
|
490
|
+
.catch(() => {
|
|
491
|
+
// Best effort — session will be picked up by poll
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
onGroupCreated: (id) => {
|
|
496
|
+
groupId = id;
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
console.log(`\nLaunched ${result.results.length} sessions (group: ${result.groupId}):`);
|
|
500
|
+
for (const r of result.results) {
|
|
501
|
+
const label = r.slot.model
|
|
502
|
+
? `${r.slot.adapter} (${r.slot.model})`
|
|
503
|
+
: r.slot.adapter;
|
|
504
|
+
if (r.error) {
|
|
505
|
+
console.log(` ✗ ${label} — ${r.error}`);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
console.log(` ${label} → ${shortenPath(r.cwd)} (${r.sessionId.slice(0, 8)})`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
console.error(`Parallel launch failed: ${err.message}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// --- Single adapter launch path (original behavior) ---
|
|
519
|
+
const name = slots.length === 1 ? slots[0].adapter : adapterName || "claude-code";
|
|
520
|
+
const model = slots.length === 1 && slots[0].model ? slots[0].model : opts.model;
|
|
374
521
|
// FEAT-1: Worktree lifecycle
|
|
375
522
|
let worktreeInfo;
|
|
376
523
|
if (opts.worktree) {
|
|
@@ -391,15 +538,6 @@ program
|
|
|
391
538
|
process.exit(1);
|
|
392
539
|
}
|
|
393
540
|
}
|
|
394
|
-
// Collect hooks
|
|
395
|
-
const hooks = opts.onCreate || opts.onComplete || opts.preMerge || opts.postMerge
|
|
396
|
-
? {
|
|
397
|
-
onCreate: opts.onCreate,
|
|
398
|
-
onComplete: opts.onComplete,
|
|
399
|
-
preMerge: opts.preMerge,
|
|
400
|
-
postMerge: opts.postMerge,
|
|
401
|
-
}
|
|
402
|
-
: undefined;
|
|
403
541
|
const daemonRunning = await ensureDaemon();
|
|
404
542
|
if (daemonRunning) {
|
|
405
543
|
try {
|
|
@@ -408,7 +546,7 @@ program
|
|
|
408
546
|
prompt: opts.prompt,
|
|
409
547
|
cwd,
|
|
410
548
|
spec: opts.spec,
|
|
411
|
-
model
|
|
549
|
+
model,
|
|
412
550
|
force: opts.force,
|
|
413
551
|
worktree: worktreeInfo
|
|
414
552
|
? { repo: worktreeInfo.repo, branch: worktreeInfo.branch }
|
|
@@ -443,7 +581,7 @@ program
|
|
|
443
581
|
prompt: opts.prompt,
|
|
444
582
|
spec: opts.spec,
|
|
445
583
|
cwd,
|
|
446
|
-
model
|
|
584
|
+
model,
|
|
447
585
|
hooks,
|
|
448
586
|
});
|
|
449
587
|
console.log(`Launched session ${session.id.slice(0, 8)} (PID: ${session.pid})`);
|
|
@@ -462,6 +600,10 @@ program
|
|
|
462
600
|
process.exit(1);
|
|
463
601
|
}
|
|
464
602
|
});
|
|
603
|
+
/** Commander collect callback for repeatable --adapter */
|
|
604
|
+
function collectAdapter(value, previous) {
|
|
605
|
+
return previous.concat([value]);
|
|
606
|
+
}
|
|
465
607
|
// events
|
|
466
608
|
program
|
|
467
609
|
.command("events")
|
|
@@ -560,6 +702,84 @@ program
|
|
|
560
702
|
});
|
|
561
703
|
}
|
|
562
704
|
});
|
|
705
|
+
// --- Worktree subcommand ---
|
|
706
|
+
const worktreeCmd = new Command("worktree").description("Manage agentctl-created worktrees");
|
|
707
|
+
worktreeCmd
|
|
708
|
+
.command("list")
|
|
709
|
+
.description("List git worktrees for a repo")
|
|
710
|
+
.argument("<repo>", "Path to the main repo")
|
|
711
|
+
.option("--json", "Output as JSON")
|
|
712
|
+
.action(async (repo, opts) => {
|
|
713
|
+
const { listWorktrees } = await import("./worktree.js");
|
|
714
|
+
try {
|
|
715
|
+
const entries = await listWorktrees(repo);
|
|
716
|
+
// Filter to only non-bare worktrees (exclude the main worktree)
|
|
717
|
+
const worktrees = entries.filter((e) => !e.bare);
|
|
718
|
+
if (opts.json) {
|
|
719
|
+
printJson(worktrees);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (worktrees.length === 0) {
|
|
723
|
+
console.log("No worktrees found.");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
printTable(worktrees.map((e) => ({
|
|
727
|
+
Path: shortenPath(e.path),
|
|
728
|
+
Branch: e.branch || "-",
|
|
729
|
+
HEAD: e.head?.slice(0, 8) || "-",
|
|
730
|
+
})));
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
console.error(err.message);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
worktreeCmd
|
|
738
|
+
.command("clean")
|
|
739
|
+
.description("Remove a worktree and optionally its branch")
|
|
740
|
+
.argument("<path>", "Path to the worktree to remove")
|
|
741
|
+
.option("--repo <path>", "Main repo path (auto-detected if omitted)")
|
|
742
|
+
.option("--delete-branch", "Also delete the worktree's branch")
|
|
743
|
+
.action(async (worktreePath, opts) => {
|
|
744
|
+
const { cleanWorktree } = await import("./worktree.js");
|
|
745
|
+
const absPath = path.resolve(worktreePath);
|
|
746
|
+
let repo = opts.repo;
|
|
747
|
+
// Auto-detect repo from the worktree's .git file
|
|
748
|
+
if (!repo) {
|
|
749
|
+
try {
|
|
750
|
+
const gitFile = await fs.readFile(path.join(absPath, ".git"), "utf-8");
|
|
751
|
+
// .git file contains: gitdir: /path/to/repo/.git/worktrees/<name>
|
|
752
|
+
const match = gitFile.match(/gitdir:\s*(.+)/);
|
|
753
|
+
if (match) {
|
|
754
|
+
const gitDir = match[1].trim();
|
|
755
|
+
// Navigate up from .git/worktrees/<name> to the repo root
|
|
756
|
+
repo = path.resolve(gitDir, "..", "..", "..");
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch {
|
|
760
|
+
console.error("Cannot auto-detect repo. Use --repo to specify the main repository.");
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (!repo) {
|
|
765
|
+
console.error("Cannot determine repo path. Use --repo.");
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
const result = await cleanWorktree(repo, absPath, {
|
|
770
|
+
deleteBranch: opts.deleteBranch,
|
|
771
|
+
});
|
|
772
|
+
console.log(`Removed worktree: ${shortenPath(result.removedPath)}`);
|
|
773
|
+
if (result.deletedBranch) {
|
|
774
|
+
console.log(`Deleted branch: ${result.deletedBranch}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
console.error(err.message);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
program.addCommand(worktreeCmd);
|
|
563
783
|
// --- Lock commands ---
|
|
564
784
|
program
|
|
565
785
|
.command("lock <directory>")
|
|
@@ -650,6 +870,25 @@ program
|
|
|
650
870
|
process.exit(1);
|
|
651
871
|
}
|
|
652
872
|
});
|
|
873
|
+
// --- Prune command (#40) ---
|
|
874
|
+
program
|
|
875
|
+
.command("prune")
|
|
876
|
+
.description("Remove dead and stale sessions from daemon state")
|
|
877
|
+
.action(async () => {
|
|
878
|
+
const daemonRunning = await ensureDaemon();
|
|
879
|
+
if (!daemonRunning) {
|
|
880
|
+
console.error("Daemon not running. Start with: agentctl daemon start");
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
try {
|
|
884
|
+
const result = await client.call("session.prune");
|
|
885
|
+
console.log(`Pruned ${result.pruned} dead/stale sessions`);
|
|
886
|
+
}
|
|
887
|
+
catch (err) {
|
|
888
|
+
console.error(err.message);
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
653
892
|
// --- Daemon subcommand ---
|
|
654
893
|
const daemonCmd = new Command("daemon").description("Manage the agentctl daemon");
|
|
655
894
|
daemonCmd
|
|
@@ -727,8 +966,9 @@ daemonCmd
|
|
|
727
966
|
});
|
|
728
967
|
daemonCmd
|
|
729
968
|
.command("status")
|
|
730
|
-
.description("Show daemon status")
|
|
969
|
+
.description("Show daemon status and all daemon-related processes")
|
|
731
970
|
.action(async () => {
|
|
971
|
+
// Show daemon status
|
|
732
972
|
try {
|
|
733
973
|
const status = await client.call("daemon.status");
|
|
734
974
|
console.log(`Daemon running (PID ${status.pid})`);
|
|
@@ -740,6 +980,38 @@ daemonCmd
|
|
|
740
980
|
catch {
|
|
741
981
|
console.log("Daemon not running");
|
|
742
982
|
}
|
|
983
|
+
// Show all daemon-related processes (#39)
|
|
984
|
+
const configDir = path.join(os.homedir(), ".agentctl");
|
|
985
|
+
const { getSupervisorPid } = await import("./daemon/supervisor.js");
|
|
986
|
+
const supPid = await getSupervisorPid();
|
|
987
|
+
let daemonPid = null;
|
|
988
|
+
try {
|
|
989
|
+
const raw = await fs.readFile(path.join(configDir, "agentctl.pid"), "utf-8");
|
|
990
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
991
|
+
try {
|
|
992
|
+
process.kill(pid, 0);
|
|
993
|
+
daemonPid = pid;
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
// PID file is stale
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
// No PID file
|
|
1001
|
+
}
|
|
1002
|
+
console.log("\nDaemon-related processes:");
|
|
1003
|
+
if (supPid) {
|
|
1004
|
+
console.log(` Supervisor: PID ${supPid} (alive)`);
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
console.log(" Supervisor: not running");
|
|
1008
|
+
}
|
|
1009
|
+
if (daemonPid) {
|
|
1010
|
+
console.log(` Daemon: PID ${daemonPid} (alive)`);
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
console.log(" Daemon: not running");
|
|
1014
|
+
}
|
|
743
1015
|
});
|
|
744
1016
|
daemonCmd
|
|
745
1017
|
.command("restart")
|