maesttro 0.4.4 → 0.6.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/maesttro.js CHANGED
@@ -1,16 +1,146 @@
1
1
  #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ if (mod && typeof mod === "object" || typeof mod === "function") {
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ }
30
+ if (canCache)
31
+ cache.set(mod, to);
32
+ return to;
33
+ };
34
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
35
+
36
+ // ../node_modules/.bun/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
37
+ var require_src = __commonJS(function(exports, module) {
38
+ var ESC = "\x1B";
39
+ var CSI = `${ESC}[`;
40
+ var beep = "\x07";
41
+ var cursor = {
42
+ to(x, y) {
43
+ if (!y)
44
+ return `${CSI}${x + 1}G`;
45
+ return `${CSI}${y + 1};${x + 1}H`;
46
+ },
47
+ move(x, y) {
48
+ let ret = "";
49
+ if (x < 0)
50
+ ret += `${CSI}${-x}D`;
51
+ else if (x > 0)
52
+ ret += `${CSI}${x}C`;
53
+ if (y < 0)
54
+ ret += `${CSI}${-y}A`;
55
+ else if (y > 0)
56
+ ret += `${CSI}${y}B`;
57
+ return ret;
58
+ },
59
+ up: (count = 1) => `${CSI}${count}A`,
60
+ down: (count = 1) => `${CSI}${count}B`,
61
+ forward: (count = 1) => `${CSI}${count}C`,
62
+ backward: (count = 1) => `${CSI}${count}D`,
63
+ nextLine: (count = 1) => `${CSI}E`.repeat(count),
64
+ prevLine: (count = 1) => `${CSI}F`.repeat(count),
65
+ left: `${CSI}G`,
66
+ hide: `${CSI}?25l`,
67
+ show: `${CSI}?25h`,
68
+ save: `${ESC}7`,
69
+ restore: `${ESC}8`
70
+ };
71
+ var scroll = {
72
+ up: (count = 1) => `${CSI}S`.repeat(count),
73
+ down: (count = 1) => `${CSI}T`.repeat(count)
74
+ };
75
+ var erase = {
76
+ screen: `${CSI}2J`,
77
+ up: (count = 1) => `${CSI}1J`.repeat(count),
78
+ down: (count = 1) => `${CSI}J`.repeat(count),
79
+ line: `${CSI}2K`,
80
+ lineEnd: `${CSI}K`,
81
+ lineStart: `${CSI}1K`,
82
+ lines(count) {
83
+ let clear = "";
84
+ for (let i = 0;i < count; i++)
85
+ clear += this.line + (i < count - 1 ? cursor.up() : "");
86
+ if (count)
87
+ clear += cursor.left;
88
+ return clear;
89
+ }
90
+ };
91
+ module.exports = { cursor, scroll, erase, beep };
92
+ });
2
93
 
3
94
  // src/credential-store.ts
4
95
  import {
5
96
  chmodSync,
97
+ mkdirSync as mkdirSync2,
98
+ readFileSync as readFileSync2,
99
+ renameSync as renameSync2,
100
+ rmSync,
101
+ writeFileSync as writeFileSync2
102
+ } from "node:fs";
103
+ import { dirname, join as join2 } from "node:path";
104
+
105
+ // src/maesttro-home.ts
106
+ import {
107
+ appendFileSync,
6
108
  mkdirSync,
7
109
  readFileSync,
8
110
  renameSync,
9
- rmSync,
10
111
  writeFileSync
11
112
  } from "node:fs";
12
113
  import { homedir } from "node:os";
13
- import { dirname, join } from "node:path";
114
+ import { join } from "node:path";
115
+ function maesttroHome() {
116
+ return process.env.MAESTTRO_HOME ?? join(homedir(), ".config", "maesttro");
117
+ }
118
+ function configPath() {
119
+ return join(maesttroHome(), "config.json");
120
+ }
121
+ function readMaesttroConfig() {
122
+ try {
123
+ return JSON.parse(readFileSync(configPath(), "utf8"));
124
+ } catch {
125
+ return {};
126
+ }
127
+ }
128
+ function writeMaesttroConfig(config) {
129
+ mkdirSync(maesttroHome(), { recursive: true, mode: 448 });
130
+ const temporary = `${configPath()}.tmp`;
131
+ writeFileSync(temporary, `${JSON.stringify(config, null, 2)}
132
+ `);
133
+ renameSync(temporary, configPath());
134
+ }
135
+ function fileSessionLog(path = join(maesttroHome(), "maesttro.log")) {
136
+ return (event, detail) => {
137
+ try {
138
+ mkdirSync(maesttroHome(), { recursive: true, mode: 448 });
139
+ appendFileSync(path, `${new Date().toISOString()} ${event}${detail ? ` ${detail}` : ""}
140
+ `);
141
+ } catch {}
142
+ };
143
+ }
14
144
 
15
145
  // src/run-command.ts
16
146
  import { spawnSync } from "node:child_process";
@@ -31,20 +161,30 @@ function commandExists(run, command) {
31
161
 
32
162
  // src/credential-store.ts
33
163
  var SERVICE = "maesttro";
34
- var ACCOUNT = "github";
35
- function encode(token) {
36
- return Buffer.from(JSON.stringify(token)).toString("base64");
164
+ var GITHUB_CODEC = {
165
+ account: "github",
166
+ accepts: (value) => typeof value?.access_token === "string"
167
+ };
168
+ function encode(value) {
169
+ return Buffer.from(JSON.stringify(value)).toString("base64");
37
170
  }
38
- function decode(encoded) {
39
- try {
40
- const token = JSON.parse(Buffer.from(encoded.trim(), "base64").toString("utf8"));
41
- return typeof token.access_token === "string" ? token : undefined;
42
- } catch {
43
- return;
44
- }
171
+ function decoderFor(codec) {
172
+ return (encoded) => {
173
+ try {
174
+ const value = JSON.parse(Buffer.from(encoded.trim(), "base64").toString("utf8"));
175
+ return codec.accepts(value) ? value : undefined;
176
+ } catch {
177
+ return;
178
+ }
179
+ };
45
180
  }
46
- function keychainStore(run = runCommand) {
47
- const identity = ["-s", SERVICE, "-a", ACCOUNT];
181
+ function defaultFilePath(account) {
182
+ const name = account === "github" ? "credentials.json" : `credentials-${account}.json`;
183
+ return join2(maesttroHome(), name);
184
+ }
185
+ function keychainStore(run = runCommand, codec = GITHUB_CODEC) {
186
+ const identity = ["-s", SERVICE, "-a", codec.account];
187
+ const decode = decoderFor(codec);
48
188
  return {
49
189
  kind: "macOS keychain",
50
190
  load: () => {
@@ -55,8 +195,8 @@ function keychainStore(run = runCommand) {
55
195
  ]);
56
196
  return result.status === 0 ? decode(result.stdout) : undefined;
57
197
  },
58
- save: (token) => {
59
- const command = `add-generic-password -U ${identity.join(" ")} -w ${encode(token)}
198
+ save: (value) => {
199
+ const command = `add-generic-password -U ${identity.join(" ")} -w ${encode(value)}
60
200
  `;
61
201
  const result = run("security", ["-i"], command);
62
202
  if (result.status !== 0)
@@ -67,61 +207,1328 @@ function keychainStore(run = runCommand) {
67
207
  }
68
208
  };
69
209
  }
70
- function secretToolStore(run = runCommand) {
71
- const attributes = ["service", SERVICE, "account", ACCOUNT];
210
+ function secretToolStore(run = runCommand, codec = GITHUB_CODEC) {
211
+ const attributes = ["service", SERVICE, "account", codec.account];
212
+ const decode = decoderFor(codec);
213
+ return {
214
+ kind: "Secret Service (secret-tool)",
215
+ load: () => {
216
+ const result = run("secret-tool", ["lookup", ...attributes]);
217
+ return result.status === 0 ? decode(result.stdout) : undefined;
218
+ },
219
+ save: (value) => {
220
+ const result = run("secret-tool", ["store", `--label=${SERVICE}`, ...attributes], encode(value));
221
+ if (result.status !== 0) {
222
+ throw new Error("Could not write to the Secret Service");
223
+ }
224
+ },
225
+ clear: () => {
226
+ run("secret-tool", ["clear", ...attributes]);
227
+ }
228
+ };
229
+ }
230
+ function fileStore(path, codec = GITHUB_CODEC) {
231
+ const resolvedPath = path ?? defaultFilePath(codec.account);
232
+ const decode = decoderFor(codec);
233
+ return {
234
+ kind: `file (${resolvedPath})`,
235
+ load: () => {
236
+ try {
237
+ return decode(readFileSync2(resolvedPath, "utf8"));
238
+ } catch {
239
+ return;
240
+ }
241
+ },
242
+ save: (value) => {
243
+ mkdirSync2(dirname(resolvedPath), { recursive: true, mode: 448 });
244
+ const temporary = `${resolvedPath}.tmp`;
245
+ writeFileSync2(temporary, encode(value), { mode: 384 });
246
+ chmodSync(temporary, 384);
247
+ renameSync2(temporary, resolvedPath);
248
+ },
249
+ clear: () => {
250
+ rmSync(resolvedPath, { force: true });
251
+ }
252
+ };
253
+ }
254
+ function selectCredentialStore({
255
+ platform = process.platform,
256
+ env = process.env,
257
+ run = runCommand,
258
+ codec = GITHUB_CODEC
259
+ } = {}) {
260
+ if (env.MAESTTRO_CREDENTIAL_STORE === "file")
261
+ return fileStore(undefined, codec);
262
+ if (platform === "darwin")
263
+ return keychainStore(run, codec);
264
+ if (platform === "linux" && commandExists(run, "secret-tool")) {
265
+ return secretToolStore(run, codec);
266
+ }
267
+ return fileStore(undefined, codec);
268
+ }
269
+
270
+ // src/claude-credentials.ts
271
+ function oauthOf(value) {
272
+ const oauth = value?.claudeAiOauth;
273
+ return oauth && typeof oauth === "object" ? oauth : undefined;
274
+ }
275
+ function nonEmpty(value) {
276
+ return typeof value === "string" && value.length > 0;
277
+ }
278
+ function isClaudeCredentials(value) {
279
+ const oauth = oauthOf(value);
280
+ return oauth !== undefined && nonEmpty(oauth.accessToken) && typeof oauth.expiresAt === "number" && oauth.expiresAt > 0;
281
+ }
282
+ function isMasterLogin(value) {
283
+ return isClaudeCredentials(value) && nonEmpty(oauthOf(value)?.refreshToken);
284
+ }
285
+ function carriesRefreshToken(value) {
286
+ return nonEmpty(oauthOf(value)?.refreshToken);
287
+ }
288
+ function isBlankedLogin(value) {
289
+ const oauth = oauthOf(value);
290
+ if (!oauth)
291
+ return false;
292
+ return oauth.accessToken === "" || oauth.refreshToken === "" || typeof oauth.expiresAt === "number" && oauth.expiresAt <= 0;
293
+ }
294
+ function withoutRefreshToken(credentials) {
295
+ const { refreshToken: _, ...oauth } = credentials.claudeAiOauth;
296
+ return { claudeAiOauth: oauth };
297
+ }
298
+ function markedExpired({
299
+ credentials,
300
+ now
301
+ }) {
302
+ return {
303
+ claudeAiOauth: { ...credentials.claudeAiOauth, expiresAt: now - 60000 }
304
+ };
305
+ }
306
+ function acceptRefreshed({
307
+ previous,
308
+ candidate,
309
+ now
310
+ }) {
311
+ if (!isMasterLogin(candidate))
312
+ return;
313
+ if (candidate.claudeAiOauth.expiresAt <= now)
314
+ return;
315
+ if (candidate.claudeAiOauth.accessToken === previous.claudeAiOauth.accessToken)
316
+ return;
317
+ return candidate;
318
+ }
319
+ function markedRejected({
320
+ credentials,
321
+ now
322
+ }) {
323
+ return { ...credentials, rejectedAt: now };
324
+ }
325
+ function claudeLoginState({
326
+ credentials,
327
+ now
328
+ }) {
329
+ if (!credentials)
330
+ return "missing";
331
+ if (credentials.rejectedAt !== undefined)
332
+ return "expired";
333
+ const { refreshTokenExpiresAt } = credentials.claudeAiOauth;
334
+ if (refreshTokenExpiresAt !== undefined && refreshTokenExpiresAt <= now) {
335
+ return "expired";
336
+ }
337
+ return "active";
338
+ }
339
+ var LOGIN_WARNING_MS = 5 * 24 * 60 * 60 * 1000;
340
+ var DAY_MS = 24 * 60 * 60 * 1000;
341
+ function claudeLoginWarning({
342
+ credentials,
343
+ now
344
+ }) {
345
+ if (claudeLoginState({ credentials, now }) !== "active")
346
+ return;
347
+ const expiresAt = credentials?.claudeAiOauth.refreshTokenExpiresAt;
348
+ if (expiresAt === undefined || expiresAt - now > LOGIN_WARNING_MS) {
349
+ return;
350
+ }
351
+ const days = Math.floor((expiresAt - now) / DAY_MS);
352
+ const within = days === 0 ? "within a day" : `in ${days} day${days === 1 ? "" : "s"}`;
353
+ return `Claude login expires ${within} (${new Date(expiresAt).toLocaleString()}): run \`maesttro login claude\` before then`;
354
+ }
355
+ var CLAUDE_CODEC = {
356
+ account: "claude",
357
+ accepts: isMasterLogin
358
+ };
359
+ function claudeStore() {
360
+ return selectCredentialStore({ codec: CLAUDE_CODEC });
361
+ }
362
+
363
+ // src/claude-keeper.ts
364
+ import { spawn, spawnSync as spawnSync2 } from "node:child_process";
365
+ import { randomBytes } from "node:crypto";
366
+ import {
367
+ chmodSync as chmodSync2,
368
+ mkdirSync as mkdirSync3,
369
+ mkdtempSync,
370
+ readFileSync as readFileSync3,
371
+ rmSync as rmSync2,
372
+ writeFileSync as writeFileSync3
373
+ } from "node:fs";
374
+ import { tmpdir } from "node:os";
375
+ import { join as join4 } from "node:path";
376
+
377
+ // src/host-identity.ts
378
+ function hostIdentityEnv() {
379
+ if (typeof process.getuid !== "function" || typeof process.getgid !== "function") {
380
+ return {};
381
+ }
382
+ return {
383
+ MAESTTRO_HOST_UID: String(process.getuid()),
384
+ MAESTTRO_HOST_GID: String(process.getgid())
385
+ };
386
+ }
387
+ function hostIdentityFlags() {
388
+ return Object.entries(hostIdentityEnv()).map(([name, value]) => `-e ${name}=${value}`).join(" ");
389
+ }
390
+
391
+ // src/paths.ts
392
+ import { dirname as dirname2, join as join3, resolve } from "node:path";
393
+ import { fileURLToPath } from "node:url";
394
+ var PACKAGE_ROOT = resolve(dirname2(fileURLToPath(import.meta.url)), "..");
395
+ var DOCKERFILE_DIR = join3(PACKAGE_ROOT, "docker");
396
+ var IMAGE = "maesttro-runner:latest";
397
+ var LEGACY_VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
398
+
399
+ // src/claude-keeper.ts
400
+ var KEEPER_COMMAND = 'claude -p --model haiku "ok"';
401
+ var KEEPER_TIMEOUT_MS = 75 * 1000;
402
+ var KEEPER_REMOVE_TIMEOUT_MS = 10 * 1000;
403
+ var KEEPER_EXIT_GRACE_MS = 5 * 1000;
404
+ var LOGIN_COMMAND = "claude auth login";
405
+ var REASON_MAX_LENGTH = 200;
406
+ var dockerCli = (args, { interactive }) => {
407
+ const result = spawnSync2("docker", args, {
408
+ stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"]
409
+ });
410
+ return {
411
+ status: result.status ?? null,
412
+ stderr: result.stderr?.toString() ?? "",
413
+ error: result.error
414
+ };
415
+ };
416
+ function collect(child) {
417
+ let stdout = "";
418
+ let stderr = "";
419
+ child.stdout?.on("data", (chunk) => {
420
+ stdout += String(chunk);
421
+ });
422
+ child.stderr?.on("data", (chunk) => {
423
+ stderr += String(chunk);
424
+ });
425
+ return new Promise((resolve) => {
426
+ child.on("error", (error) => resolve({ status: null, stdout, stderr, error }));
427
+ child.on("close", (status) => resolve({ status, stdout, stderr }));
428
+ });
429
+ }
430
+ var keeperDockerCli = {
431
+ start: (args) => {
432
+ const child = spawn("docker", args, {
433
+ stdio: ["ignore", "ignore", "pipe"],
434
+ detached: true
435
+ });
436
+ return { exited: collect(child), kill: () => child.kill("SIGKILL") };
437
+ },
438
+ exec: (args, timeoutMs) => collect(spawn("docker", args, {
439
+ stdio: ["ignore", "pipe", "pipe"],
440
+ detached: true,
441
+ timeout: timeoutMs,
442
+ killSignal: "SIGKILL"
443
+ }))
444
+ };
445
+ function keeperNamePrefix(uid = process.getuid?.()) {
446
+ return `claude-keeper-${uid ?? "user"}-`;
447
+ }
448
+ function keeperContainerName() {
449
+ return `${keeperNamePrefix()}${randomBytes(4).toString("hex")}`;
450
+ }
451
+ function keeperWorkDir() {
452
+ return join4(maesttroHome(), "claude-keeper");
453
+ }
454
+ function runArgs({
455
+ claudeDir,
456
+ identity,
457
+ interactive,
458
+ command,
459
+ name
460
+ }) {
461
+ return [
462
+ "run",
463
+ ...interactive ? ["-it"] : [],
464
+ "--rm",
465
+ ...Object.entries(identity).flatMap(([envName, value]) => [
466
+ "-e",
467
+ `${envName}=${value}`
468
+ ]),
469
+ ...name ? ["--name", name] : [],
470
+ "--mount",
471
+ `type=bind,source=${claudeDir},target=/home/agent/.claude`,
472
+ IMAGE,
473
+ command
474
+ ];
475
+ }
476
+ function keeperArgs({
477
+ claudeDir,
478
+ identity,
479
+ name
480
+ }) {
481
+ return runArgs({
482
+ claudeDir,
483
+ identity,
484
+ interactive: false,
485
+ command: KEEPER_COMMAND,
486
+ name
487
+ });
488
+ }
489
+ function loginArgs({
490
+ claudeDir,
491
+ identity
492
+ }) {
493
+ return runArgs({
494
+ claudeDir,
495
+ identity,
496
+ interactive: true,
497
+ command: LOGIN_COMMAND
498
+ });
499
+ }
500
+ function privateDir(parent = tmpdir()) {
501
+ mkdirSync3(parent, { recursive: true, mode: 448 });
502
+ const dir = mkdtempSync(join4(parent, "maesttro-claude-"));
503
+ chmodSync2(dir, 448);
504
+ return dir;
505
+ }
506
+ function readKeeperCredentials(dir) {
507
+ try {
508
+ return JSON.parse(readFileSync3(join4(dir, ".credentials.json"), "utf8"));
509
+ } catch {
510
+ return;
511
+ }
512
+ }
513
+ function inPrivateDir(makeDir, use) {
514
+ const dir = makeDir();
515
+ try {
516
+ return use(dir);
517
+ } finally {
518
+ rmSync2(dir, { recursive: true, force: true });
519
+ }
520
+ }
521
+ function lastLine(text) {
522
+ const lines = text.split(`
523
+ `).map((line) => line.trim()).filter(Boolean);
524
+ return (lines.at(-1) ?? "").slice(-REASON_MAX_LENGTH);
525
+ }
526
+ function failureReason({
527
+ result,
528
+ timedOut,
529
+ name
530
+ }) {
531
+ if (result.error && !timedOut) {
532
+ return result.error.message.replaceAll(name, "claude-keeper");
533
+ }
534
+ const summary = timedOut ? "the keeper timed out" : result.status === 0 ? "the keeper left the login unchanged" : `the keeper exited with status ${result.status}`;
535
+ const detail = lastLine(result.stderr.replaceAll(name, "claude-keeper"));
536
+ return detail ? `${summary}: ${detail}` : summary;
537
+ }
538
+ var TIMED_OUT = Symbol("timed out");
539
+ function sleepFor(ms) {
540
+ return new Promise((resolve) => setTimeout(resolve, ms));
541
+ }
542
+ async function runBounded({
543
+ docker,
544
+ args,
545
+ name,
546
+ sleep
547
+ }) {
548
+ const run = docker.start(args);
549
+ const first = await Promise.race([
550
+ run.exited,
551
+ sleep(KEEPER_TIMEOUT_MS).then(() => TIMED_OUT)
552
+ ]);
553
+ if (first !== TIMED_OUT)
554
+ return { result: first, timedOut: false, gone: true };
555
+ const removal = await docker.exec(["rm", "-f", name], KEEPER_REMOVE_TIMEOUT_MS);
556
+ const settled = await Promise.race([
557
+ run.exited,
558
+ sleep(KEEPER_EXIT_GRACE_MS).then(() => TIMED_OUT)
559
+ ]);
560
+ if (settled !== TIMED_OUT) {
561
+ return { result: settled, timedOut: true, gone: true };
562
+ }
563
+ run.kill();
564
+ return {
565
+ result: await run.exited,
566
+ timedOut: true,
567
+ gone: removal.status === 0
568
+ };
569
+ }
570
+ async function removeLingeringKeepers({
571
+ docker = keeperDockerCli
572
+ } = {}) {
573
+ const listed = await docker.exec(["ps", "-aq", "--filter", `name=^${keeperNamePrefix()}`], KEEPER_REMOVE_TIMEOUT_MS);
574
+ if (listed.status !== 0) {
575
+ throw new Error(lastLine(listed.stderr) || "docker ps failed");
576
+ }
577
+ const ids = (listed.stdout ?? "").split(`
578
+ `).filter(Boolean);
579
+ if (ids.length === 0)
580
+ return;
581
+ const removed = await docker.exec(["rm", "-f", ...ids], KEEPER_REMOVE_TIMEOUT_MS);
582
+ if (removed.status !== 0) {
583
+ throw new Error(lastLine(removed.stderr) || "docker rm failed");
584
+ }
585
+ }
586
+ async function refreshWithKeeper({
587
+ master,
588
+ now,
589
+ workDir = keeperWorkDir(),
590
+ docker = keeperDockerCli,
591
+ identity = hostIdentityEnv(),
592
+ sleep = sleepFor
593
+ }) {
594
+ const claudeDir = privateDir(workDir);
595
+ let outcome = {
596
+ kind: "inconclusive",
597
+ reason: "the keeper never ran"
598
+ };
599
+ let keepDir = false;
600
+ try {
601
+ writeFileSync3(join4(claudeDir, ".credentials.json"), JSON.stringify(markedExpired({ credentials: master, now })), { mode: 384 });
602
+ const name = keeperContainerName();
603
+ const { result, timedOut, gone } = await runBounded({
604
+ docker,
605
+ args: keeperArgs({ claudeDir, identity, name }),
606
+ name,
607
+ sleep
608
+ });
609
+ const candidate = readKeeperCredentials(claudeDir);
610
+ const refreshed = acceptRefreshed({ previous: master, candidate, now });
611
+ if (refreshed) {
612
+ outcome = { kind: "refreshed", credentials: refreshed };
613
+ return outcome;
614
+ }
615
+ if (!gone) {
616
+ keepDir = true;
617
+ return {
618
+ kind: "inconclusive",
619
+ reason: "the keeper couldn't be stopped; its result is kept for the next refresh"
620
+ };
621
+ }
622
+ outcome = isBlankedLogin(candidate) ? { kind: "rejected" } : {
623
+ kind: "inconclusive",
624
+ reason: failureReason({ result, timedOut, name })
625
+ };
626
+ return outcome;
627
+ } finally {
628
+ if (outcome.kind !== "refreshed" && !keepDir) {
629
+ rmSync2(claudeDir, { recursive: true, force: true });
630
+ }
631
+ }
632
+ }
633
+ function captureLogin({
634
+ docker = dockerCli,
635
+ makeDir = () => privateDir(),
636
+ identity = hostIdentityEnv()
637
+ } = {}) {
638
+ return inPrivateDir(makeDir, (claudeDir) => {
639
+ const { status } = docker(loginArgs({ claudeDir, identity }), {
640
+ interactive: true
641
+ });
642
+ if (status !== 0)
643
+ return;
644
+ const credentials = readKeeperCredentials(claudeDir);
645
+ return isMasterLogin(credentials) ? credentials : undefined;
646
+ });
647
+ }
648
+
649
+ // src/claude-refresher.ts
650
+ import { randomBytes as randomBytes4 } from "node:crypto";
651
+ import {
652
+ closeSync as closeSync2,
653
+ existsSync,
654
+ linkSync,
655
+ lstatSync as lstatSync3,
656
+ mkdirSync as mkdirSync7,
657
+ openSync as openSync2,
658
+ readdirSync as readdirSync2,
659
+ readFileSync as readFileSync7,
660
+ renameSync as renameSync5,
661
+ rmSync as rmSync5,
662
+ statSync,
663
+ utimesSync,
664
+ writeSync
665
+ } from "node:fs";
666
+ import { dirname as dirname4, join as join7 } from "node:path";
667
+
668
+ // src/held-signals.ts
669
+ var HELD_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
670
+ var processSignals = {
671
+ listenerCount: (signal) => process.listenerCount(signal),
672
+ on: (signal, handler) => {
673
+ process.on(signal, handler);
674
+ },
675
+ off: (signal, handler) => {
676
+ process.off(signal, handler);
677
+ },
678
+ raise: (signal) => {
679
+ process.kill(process.pid, signal);
680
+ }
681
+ };
682
+ function holdSignals({
683
+ host = processSignals,
684
+ onHeld = () => {}
685
+ }) {
686
+ let received;
687
+ let released = false;
688
+ const handlers = [];
689
+ const detach = () => {
690
+ for (const { signal, handler } of handlers)
691
+ host.off(signal, handler);
692
+ handlers.length = 0;
693
+ };
694
+ for (const signal of HELD_SIGNALS) {
695
+ if (host.listenerCount(signal) > 0)
696
+ continue;
697
+ const handler = () => {
698
+ if (received) {
699
+ released = true;
700
+ detach();
701
+ host.raise(signal);
702
+ return;
703
+ }
704
+ received = signal;
705
+ onHeld();
706
+ };
707
+ host.on(signal, handler);
708
+ handlers.push({ signal, handler });
709
+ }
710
+ return {
711
+ pending: () => received !== undefined,
712
+ release: () => {
713
+ if (released)
714
+ return;
715
+ released = true;
716
+ detach();
717
+ if (received)
718
+ host.raise(received);
719
+ }
720
+ };
721
+ }
722
+ function signalRouter({
723
+ base = processSignals,
724
+ onUnrouted
725
+ }) {
726
+ const routed = new Map;
727
+ const listeners = HELD_SIGNALS.map((signal) => {
728
+ const listener = () => {
729
+ const handlers = [...routed.get(signal) ?? []];
730
+ if (handlers.length === 0)
731
+ return onUnrouted(signal);
732
+ for (const handler of handlers)
733
+ handler();
734
+ };
735
+ base.on(signal, listener);
736
+ return { signal, listener };
737
+ });
738
+ return {
739
+ listenerCount: (signal) => routed.get(signal)?.size ?? 0,
740
+ on: (signal, handler) => {
741
+ routed.set(signal, (routed.get(signal) ?? new Set).add(handler));
742
+ },
743
+ off: (signal, handler) => {
744
+ routed.get(signal)?.delete(handler);
745
+ },
746
+ raise: (signal) => onUnrouted(signal),
747
+ dispose: () => {
748
+ for (const { signal, listener } of listeners)
749
+ base.off(signal, listener);
750
+ }
751
+ };
752
+ }
753
+
754
+ // src/refresher.ts
755
+ var REFRESH_LEAD_MS = 10 * 60 * 1000;
756
+ var WALL_CLOCK_TICK_MS = 60 * 1000;
757
+ var FIRST_BACKOFF_MS = 5 * 1000;
758
+ var MAX_BACKOFF_MS = 5 * 60 * 1000;
759
+ var nodeTimers = {
760
+ setTimeout: (callback, ms) => setTimeout(callback, ms).unref(),
761
+ clearTimeout: (handle) => clearTimeout(handle),
762
+ setInterval: (callback, ms) => setInterval(callback, ms).unref(),
763
+ clearInterval: (handle) => clearInterval(handle)
764
+ };
765
+ function refreshDueAt({
766
+ expiresAt,
767
+ now
768
+ }) {
769
+ const halfRemaining = (expiresAt - now) / 2;
770
+ return expiresAt - Math.min(REFRESH_LEAD_MS, Math.max(halfRemaining, 0));
771
+ }
772
+ function startRefresher({
773
+ initial,
774
+ expiresAtOf,
775
+ firstAttemptAt,
776
+ refresh,
777
+ onRefreshed,
778
+ onFailed,
779
+ now = Date.now,
780
+ timers = nodeTimers
781
+ }) {
782
+ let current = initial;
783
+ let nextAttemptAt = 0;
784
+ let backoffMs = FIRST_BACKOFF_MS;
785
+ let inFlight;
786
+ let stopped = false;
787
+ let pending;
788
+ function dueAt(token) {
789
+ return refreshDueAt({ expiresAt: expiresAtOf(token), now: now() });
790
+ }
791
+ nextAttemptAt = firstAttemptAt ?? dueAt(initial);
792
+ function scheduleAt(at) {
793
+ timers.clearTimeout(pending);
794
+ pending = timers.setTimeout(check, Math.max(at - now(), 0));
795
+ }
796
+ async function attempt() {
797
+ try {
798
+ const token = await refresh();
799
+ if (stopped)
800
+ return;
801
+ current = token;
802
+ backoffMs = FIRST_BACKOFF_MS;
803
+ nextAttemptAt = dueAt(token);
804
+ onRefreshed(token);
805
+ } catch (error) {
806
+ if (stopped)
807
+ return;
808
+ nextAttemptAt = now() + backoffMs;
809
+ onFailed(error, backoffMs);
810
+ backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
811
+ }
812
+ scheduleAt(nextAttemptAt);
813
+ }
814
+ function check() {
815
+ if (stopped)
816
+ return Promise.resolve();
817
+ if (inFlight)
818
+ return inFlight;
819
+ if (now() < nextAttemptAt)
820
+ return Promise.resolve();
821
+ inFlight = attempt().finally(() => {
822
+ inFlight = undefined;
823
+ });
824
+ return inFlight;
825
+ }
826
+ const tick = timers.setInterval(check, WALL_CLOCK_TICK_MS);
827
+ scheduleAt(nextAttemptAt);
828
+ return {
829
+ current: () => current,
830
+ check,
831
+ stop: () => {
832
+ stopped = true;
833
+ timers.clearTimeout(pending);
834
+ timers.clearInterval(tick);
835
+ return inFlight ?? Promise.resolve();
836
+ },
837
+ busy: () => inFlight !== undefined
838
+ };
839
+ }
840
+
841
+ // src/session-home.ts
842
+ import { randomBytes as randomBytes3 } from "node:crypto";
843
+ import {
844
+ closeSync,
845
+ constants,
846
+ fchmodSync,
847
+ fstatSync,
848
+ lstatSync as lstatSync2,
849
+ mkdirSync as mkdirSync6,
850
+ openSync,
851
+ readFileSync as readFileSync6,
852
+ renameSync as renameSync4,
853
+ rmSync as rmSync4,
854
+ unlinkSync,
855
+ writeFileSync as writeFileSync6
856
+ } from "node:fs";
857
+ import { join as join6 } from "node:path";
858
+
859
+ // src/json-file.ts
860
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
861
+ import { dirname as dirname3 } from "node:path";
862
+ function isObject(value) {
863
+ return typeof value === "object" && value !== null && !Array.isArray(value);
864
+ }
865
+ function mergeJson(existing, curated) {
866
+ const merged = { ...existing };
867
+ for (const [key, value] of Object.entries(curated)) {
868
+ const current = merged[key];
869
+ merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
870
+ }
871
+ return merged;
872
+ }
873
+ function readJson(path) {
874
+ try {
875
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
876
+ return isObject(parsed) ? parsed : {};
877
+ } catch {
878
+ return {};
879
+ }
880
+ }
881
+ function writeJson(path, value) {
882
+ mkdirSync4(dirname3(path), { recursive: true });
883
+ writeFileSync4(path, `${JSON.stringify(value, null, 2)}
884
+ `);
885
+ }
886
+ function skipInsignificant(text, from) {
887
+ let index = from;
888
+ while (index < text.length) {
889
+ if (/\s/.test(text[index] ?? "")) {
890
+ index++;
891
+ } else if (text.startsWith("//", index)) {
892
+ while (index < text.length && text[index] !== `
893
+ `)
894
+ index++;
895
+ } else if (text.startsWith("/*", index)) {
896
+ const end = text.indexOf("*/", index + 2);
897
+ index = end === -1 ? text.length : end + 2;
898
+ } else {
899
+ return index;
900
+ }
901
+ }
902
+ return index;
903
+ }
904
+ function stripJsonc(text) {
905
+ let output = "";
906
+ let index = 0;
907
+ while (index < text.length) {
908
+ const char = text[index];
909
+ if (char === '"') {
910
+ const start = index;
911
+ index++;
912
+ while (index < text.length && text[index] !== '"') {
913
+ index += text[index] === "\\" ? 2 : 1;
914
+ }
915
+ index++;
916
+ output += text.slice(start, index);
917
+ continue;
918
+ }
919
+ const significant = skipInsignificant(text, index);
920
+ if (significant > index) {
921
+ output += " ";
922
+ index = significant;
923
+ continue;
924
+ }
925
+ const trailingComma = char === "," && /[}\]]/.test(text[skipInsignificant(text, index + 1)] ?? "");
926
+ if (!trailingComma)
927
+ output += char;
928
+ index++;
929
+ }
930
+ return output;
931
+ }
932
+ function parseJsonc(text) {
933
+ const parsed = JSON.parse(stripJsonc(text));
934
+ if (!isObject(parsed))
935
+ throw new Error("expected a JSON object");
936
+ return parsed;
937
+ }
938
+
939
+ // src/sessions.ts
940
+ import { randomBytes as randomBytes2 } from "node:crypto";
941
+ import {
942
+ chmodSync as chmodSync3,
943
+ lstatSync,
944
+ mkdirSync as mkdirSync5,
945
+ readdirSync,
946
+ readFileSync as readFileSync5,
947
+ renameSync as renameSync3,
948
+ rmSync as rmSync3,
949
+ writeFileSync as writeFileSync5
950
+ } from "node:fs";
951
+ import { join as join5 } from "node:path";
952
+ function newSessionId() {
953
+ return `maesttro-${randomBytes2(4).toString("hex")}`;
954
+ }
955
+ function sessionsDir(home = maesttroHome()) {
956
+ return join5(home, "sessions");
957
+ }
958
+ function saveSession(record, dir = sessionsDir()) {
959
+ mkdirSync5(dir, { recursive: true, mode: 448 });
960
+ const path = join5(dir, `${record.id}.json`);
961
+ writeFileSync5(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
962
+ `);
963
+ renameSync3(`${path}.tmp`, path);
964
+ }
965
+ function readSession(id, dir = sessionsDir()) {
966
+ try {
967
+ return JSON.parse(readFileSync5(join5(dir, `${id}.json`), "utf8"));
968
+ } catch {
969
+ return;
970
+ }
971
+ }
972
+ function unlockTree(path) {
973
+ let entry;
974
+ try {
975
+ entry = lstatSync(path);
976
+ } catch {
977
+ return;
978
+ }
979
+ if (!entry.isDirectory())
980
+ return;
981
+ chmodSync3(path, 448);
982
+ for (const name of readdirSync(path))
983
+ unlockTree(join5(path, name));
984
+ }
985
+ function removeTree(path) {
986
+ try {
987
+ rmSync3(path, { recursive: true, force: true });
988
+ } catch {
989
+ unlockTree(path);
990
+ rmSync3(path, { recursive: true, force: true });
991
+ }
992
+ }
993
+ function deleteSession(id, dir = sessionsDir()) {
994
+ removeTree(join5(dir, id));
995
+ rmSync3(join5(dir, `${id}.json`), { force: true });
996
+ }
997
+ function sessionsForRepo(repoRoot, dir = sessionsDir()) {
998
+ let names;
999
+ try {
1000
+ names = readdirSync(dir).filter((name) => name.endsWith(".json"));
1001
+ } catch {
1002
+ return [];
1003
+ }
1004
+ return names.map((name) => readSession(name.slice(0, -".json".length), dir)).filter((record) => record?.repoRoot === repoRoot).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1005
+ }
1006
+
1007
+ // src/session-home.ts
1008
+ var OPEN_DIRECTORY_NO_FOLLOW = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
1009
+ var CREATE_NEW_NO_FOLLOW = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW;
1010
+ var READ_NO_FOLLOW_NO_BLOCK = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
1011
+ function sessionHome({
1012
+ id,
1013
+ dir = sessionsDir()
1014
+ }) {
1015
+ return join6(dir, id, "home");
1016
+ }
1017
+ function sessionClaudeDir({
1018
+ id,
1019
+ dir = sessionsDir()
1020
+ }) {
1021
+ return join6(dir, id, "claude");
1022
+ }
1023
+ function credentialsPath(claudeDir) {
1024
+ return join6(claudeDir, ".credentials.json");
1025
+ }
1026
+ function entryAt(path) {
1027
+ try {
1028
+ return lstatSync2(path);
1029
+ } catch (error) {
1030
+ if (error.code === "ENOENT")
1031
+ return;
1032
+ throw error;
1033
+ }
1034
+ }
1035
+ function requireRealDirectory(path) {
1036
+ if (!lstatSync2(path).isDirectory()) {
1037
+ throw new Error(`Not a directory: ${path}`);
1038
+ }
1039
+ }
1040
+ function withFd(fd, use) {
1041
+ try {
1042
+ return use(fd);
1043
+ } finally {
1044
+ closeSync(fd);
1045
+ }
1046
+ }
1047
+ function ensurePrivateDir(path) {
1048
+ const entry = entryAt(path);
1049
+ if (!entry?.isDirectory()) {
1050
+ if (entry)
1051
+ unlinkSync(path);
1052
+ mkdirSync6(path, { recursive: true, mode: 448 });
1053
+ }
1054
+ withFd(openSync(path, OPEN_DIRECTORY_NO_FOLLOW), (fd) => fchmodSync(fd, 448));
1055
+ }
1056
+ function ensureSessionHome({
1057
+ id,
1058
+ dir = sessionsDir()
1059
+ }) {
1060
+ const home = sessionHome({ id, dir });
1061
+ const claudeDir = sessionClaudeDir({ id, dir });
1062
+ ensurePrivateDir(home);
1063
+ ensurePrivateDir(claudeDir);
1064
+ ensurePrivateDir(join6(home, ".claude"));
1065
+ return { home, claudeDir };
1066
+ }
1067
+ function supersedes({
1068
+ credentials,
1069
+ current
1070
+ }) {
1071
+ if (carriesRefreshToken(current))
1072
+ return false;
1073
+ if (!isClaudeCredentials(current))
1074
+ return true;
1075
+ return credentials.claudeAiOauth.expiresAt > current.claudeAiOauth.expiresAt;
1076
+ }
1077
+ function writeSessionCredentials({
1078
+ claudeDir,
1079
+ credentials
1080
+ }) {
1081
+ requireRealDirectory(claudeDir);
1082
+ const current = readSessionCredentials({ claudeDir });
1083
+ if (!supersedes({ credentials, current }))
1084
+ return false;
1085
+ const temporary = join6(claudeDir, `.credentials.json.maesttro-${randomBytes3(8).toString("hex")}`);
1086
+ const fd = openSync(temporary, CREATE_NEW_NO_FOLLOW, 384);
1087
+ try {
1088
+ withFd(fd, () => {
1089
+ writeFileSync6(fd, JSON.stringify({
1090
+ ...isObject(current) ? current : {},
1091
+ ...withoutRefreshToken(credentials)
1092
+ }));
1093
+ fchmodSync(fd, 384);
1094
+ });
1095
+ renameSync4(temporary, credentialsPath(claudeDir));
1096
+ } catch (error) {
1097
+ rmSync4(temporary, { force: true });
1098
+ throw error;
1099
+ }
1100
+ return true;
1101
+ }
1102
+ function readSessionCredentials({
1103
+ claudeDir
1104
+ }) {
1105
+ try {
1106
+ return withFd(openSync(credentialsPath(claudeDir), READ_NO_FOLLOW_NO_BLOCK), (fd) => fstatSync(fd).isFile() ? JSON.parse(readFileSync6(fd, "utf8")) : undefined);
1107
+ } catch {
1108
+ return;
1109
+ }
1110
+ }
1111
+ function removeHostWrittenCredentials({
1112
+ claudeDir
1113
+ }) {
1114
+ requireRealDirectory(claudeDir);
1115
+ const path = credentialsPath(claudeDir);
1116
+ const entry = entryAt(path);
1117
+ if (!entry)
1118
+ return;
1119
+ const inSessionLogin = entry.isFile() && carriesRefreshToken(readSessionCredentials({ claudeDir }));
1120
+ if (!inSessionLogin)
1121
+ unlinkSync(path);
1122
+ }
1123
+
1124
+ // src/claude-refresher.ts
1125
+ var LOCK_STALE_MS = 2 * 60 * 1000;
1126
+ var LOCK_HEARTBEAT_MS = 20 * 1000;
1127
+ var SESSION_SYNC_MS = 60 * 1000;
1128
+ var LAUNCH_RETRY_MS = 60 * 1000;
1129
+ var WAITING_FOR_REFRESH = "Waiting for another maesttro's Claude refresh…";
1130
+
1131
+ class ClaudeLoginExpired extends Error {
1132
+ constructor() {
1133
+ super("Claude login expired: run `maesttro login claude`");
1134
+ }
1135
+ }
1136
+
1137
+ class ClaudeRefreshFailed extends Error {
1138
+ constructor(reason) {
1139
+ super(`Couldn't refresh the Claude login (will retry): ${reason}`);
1140
+ }
1141
+ }
1142
+
1143
+ class ClaudeLoggedOut extends Error {
1144
+ constructor() {
1145
+ super("Not logged in to Claude: run `maesttro login claude`");
1146
+ }
1147
+ }
1148
+ function claudeLockPath() {
1149
+ return join7(maesttroHome(), "claude-refresh.lock");
1150
+ }
1151
+ function lockToken() {
1152
+ return `${process.pid}-${randomBytes4(8).toString("hex")}`;
1153
+ }
1154
+ function tryAcquire(path, token) {
1155
+ let fd;
1156
+ try {
1157
+ fd = openSync2(path, "wx", 384);
1158
+ } catch (error) {
1159
+ if (error.code === "EEXIST")
1160
+ return false;
1161
+ throw error;
1162
+ }
1163
+ try {
1164
+ writeSync(fd, token);
1165
+ } catch (error) {
1166
+ rmSync5(path, { force: true });
1167
+ throw error;
1168
+ } finally {
1169
+ closeSync2(fd);
1170
+ }
1171
+ return true;
1172
+ }
1173
+ function isStale(path, now, staleMs) {
1174
+ try {
1175
+ return now - statSync(path).mtimeMs > staleMs;
1176
+ } catch {
1177
+ return false;
1178
+ }
1179
+ }
1180
+ function reclaimStaleLock({
1181
+ path,
1182
+ now,
1183
+ staleMs
1184
+ }) {
1185
+ const staging = `${path}.stale-${lockToken()}`;
1186
+ try {
1187
+ renameSync5(path, staging);
1188
+ } catch (error) {
1189
+ if (error.code === "ENOENT")
1190
+ return;
1191
+ throw error;
1192
+ }
1193
+ if (!isStale(staging, now, staleMs)) {
1194
+ try {
1195
+ linkSync(staging, path);
1196
+ } catch {}
1197
+ }
1198
+ rmSync5(staging, { force: true });
1199
+ }
1200
+ function touchIfOwned(path, token) {
1201
+ try {
1202
+ if (readFileSync7(path, "utf8") === token) {
1203
+ const now = new Date;
1204
+ utimesSync(path, now, now);
1205
+ }
1206
+ } catch {}
1207
+ }
1208
+ function releaseIfOwned(path, token) {
1209
+ try {
1210
+ if (readFileSync7(path, "utf8") === token)
1211
+ rmSync5(path, { force: true });
1212
+ } catch {}
1213
+ }
1214
+ function sleepFor2(ms) {
1215
+ return new Promise((resolve) => setTimeout(resolve, ms));
1216
+ }
1217
+ async function withRefreshLock({
1218
+ path,
1219
+ now = Date.now,
1220
+ staleMs = LOCK_STALE_MS,
1221
+ pollMs = 500,
1222
+ sleep = sleepFor2,
1223
+ onWait = () => {},
1224
+ timers = nodeTimers
1225
+ }, action) {
1226
+ mkdirSync7(dirname4(path), { recursive: true, mode: 448 });
1227
+ const token = lockToken();
1228
+ let waited = false;
1229
+ while (!tryAcquire(path, token)) {
1230
+ if (isStale(path, now(), staleMs)) {
1231
+ reclaimStaleLock({ path, now: now(), staleMs });
1232
+ } else {
1233
+ if (!waited)
1234
+ onWait();
1235
+ waited = true;
1236
+ await sleep(pollMs);
1237
+ }
1238
+ }
1239
+ const heartbeat = timers.setInterval(() => touchIfOwned(path, token), LOCK_HEARTBEAT_MS);
1240
+ try {
1241
+ return await action();
1242
+ } finally {
1243
+ timers.clearInterval(heartbeat);
1244
+ releaseIfOwned(path, token);
1245
+ }
1246
+ }
1247
+ function isDue({
1248
+ credentials,
1249
+ now
1250
+ }) {
1251
+ return credentials.claudeAiOauth.expiresAt - now <= REFRESH_LEAD_MS;
1252
+ }
1253
+ var FINISHING_REFRESH = "Finishing the Claude refresh before exiting… (press Ctrl-C again to abort)";
1254
+ var FINISHING_BACKGROUND_REFRESH = "Finishing a Claude refresh before exiting…";
1255
+ var runKeeper = ({ master, now, workDir }) => refreshWithKeeper({ master, now, workDir });
1256
+ function loadMaster(store) {
1257
+ const master = store.load();
1258
+ if (!master)
1259
+ throw new ClaudeLoggedOut;
1260
+ return master;
1261
+ }
1262
+ function keeperLeftovers(workDir) {
1263
+ try {
1264
+ return readdirSync2(workDir).map((name) => join7(workDir, name)).filter((path) => lstatSync3(path).isDirectory());
1265
+ } catch {
1266
+ return [];
1267
+ }
1268
+ }
1269
+ function discardLeftovers(workDir) {
1270
+ for (const dir of keeperLeftovers(workDir)) {
1271
+ rmSync5(dir, { recursive: true, force: true });
1272
+ }
1273
+ }
1274
+ function discardKeeperLeftovers({
1275
+ workDir = keeperWorkDir()
1276
+ } = {}) {
1277
+ discardLeftovers(workDir);
1278
+ }
1279
+ function saveRotated({
1280
+ store,
1281
+ credentials,
1282
+ workDir,
1283
+ onRotated
1284
+ }) {
1285
+ try {
1286
+ store.save(credentials);
1287
+ } catch (error) {
1288
+ onRotated(credentials);
1289
+ throw new ClaudeRefreshFailed(`couldn't save the refreshed login: ${messageOf(error)}`);
1290
+ }
1291
+ discardLeftovers(workDir);
1292
+ onRotated(credentials);
1293
+ }
1294
+ function adoptLeftover({
1295
+ master,
1296
+ workDir
1297
+ }) {
1298
+ let best;
1299
+ for (const dir of keeperLeftovers(workDir)) {
1300
+ const candidate = readKeeperCredentials(dir);
1301
+ if (!isMasterLogin(candidate))
1302
+ continue;
1303
+ const { accessToken, expiresAt } = candidate.claudeAiOauth;
1304
+ const newest = best ?? master;
1305
+ if (accessToken !== master.claudeAiOauth.accessToken && expiresAt > newest.claudeAiOauth.expiresAt) {
1306
+ best = { claudeAiOauth: candidate.claudeAiOauth };
1307
+ }
1308
+ }
1309
+ return best;
1310
+ }
1311
+ function sameLogin(current, expected) {
1312
+ return current?.claudeAiOauth.accessToken === expected.claudeAiOauth.accessToken;
1313
+ }
1314
+ function replacedMeanwhile({
1315
+ store,
1316
+ handed,
1317
+ workDir
1318
+ }) {
1319
+ const current = store.load();
1320
+ if (sameLogin(current, handed))
1321
+ return;
1322
+ if (!current)
1323
+ throw new ClaudeLoggedOut;
1324
+ discardLeftovers(workDir);
1325
+ return current;
1326
+ }
1327
+ async function clearEarlierKeepers(clearLingering) {
1328
+ try {
1329
+ await clearLingering();
1330
+ } catch (error) {
1331
+ throw new ClaudeRefreshFailed(`couldn't stop an earlier keeper: ${messageOf(error)}`);
1332
+ }
1333
+ }
1334
+ async function refreshMaster({
1335
+ store,
1336
+ keeper = runKeeper,
1337
+ lockPath = claudeLockPath(),
1338
+ workDir = keeperWorkDir(),
1339
+ clearLingering = () => removeLingeringKeepers(),
1340
+ now = Date.now,
1341
+ announce,
1342
+ sleep,
1343
+ signals,
1344
+ onRotated = () => {}
1345
+ }) {
1346
+ const loaded = loadMaster(store);
1347
+ if (keeperLeftovers(workDir).length === 0) {
1348
+ if (loaded.rejectedAt !== undefined)
1349
+ throw new ClaudeLoginExpired;
1350
+ if (!isDue({ credentials: loaded, now: now() }))
1351
+ return loaded;
1352
+ }
1353
+ const onWait = announce ? () => announce(WAITING_FOR_REFRESH) : undefined;
1354
+ const onHeld = announce ? () => announce(FINISHING_REFRESH) : undefined;
1355
+ let hold;
1356
+ try {
1357
+ return await withRefreshLock({ path: lockPath, now, sleep, onWait }, async () => {
1358
+ if (signals)
1359
+ hold = holdSignals({ host: signals, onHeld });
1360
+ await clearEarlierKeepers(clearLingering);
1361
+ let stored = store.load();
1362
+ if (!stored)
1363
+ throw new ClaudeLoggedOut;
1364
+ const adopted = adoptLeftover({ master: stored, workDir });
1365
+ if (adopted) {
1366
+ saveRotated({ store, credentials: adopted, workDir, onRotated });
1367
+ stored = adopted;
1368
+ }
1369
+ discardLeftovers(workDir);
1370
+ if (stored.rejectedAt !== undefined)
1371
+ throw new ClaudeLoginExpired;
1372
+ if (!isDue({ credentials: stored, now: now() }))
1373
+ return stored;
1374
+ if (hold?.pending()) {
1375
+ throw new ClaudeRefreshFailed("interrupted before the keeper ran");
1376
+ }
1377
+ const outcome = await keeper({ master: stored, now: now(), workDir });
1378
+ if (outcome.kind === "inconclusive") {
1379
+ throw new ClaudeRefreshFailed(outcome.reason);
1380
+ }
1381
+ const newer = replacedMeanwhile({ store, handed: stored, workDir });
1382
+ if (newer)
1383
+ return newer;
1384
+ if (outcome.kind === "rejected") {
1385
+ try {
1386
+ store.save(markedRejected({ credentials: stored, now: now() }));
1387
+ } catch {}
1388
+ throw new ClaudeLoginExpired;
1389
+ }
1390
+ saveRotated({
1391
+ store,
1392
+ credentials: outcome.credentials,
1393
+ workDir,
1394
+ onRotated
1395
+ });
1396
+ return outcome.credentials;
1397
+ });
1398
+ } finally {
1399
+ hold?.release();
1400
+ }
1401
+ }
1402
+ function runningSessionIds({
1403
+ run = runCommand
1404
+ } = {}) {
1405
+ const result = run("docker", ["ps", "--format", "{{.Names}}"]);
1406
+ if (result.status !== 0)
1407
+ return [];
1408
+ return result.stdout.split(`
1409
+ `).map((name) => name.trim()).filter((name) => name.startsWith("maesttro-"));
1410
+ }
1411
+ function messageOf(error) {
1412
+ return error instanceof Error ? error.message : String(error);
1413
+ }
1414
+ function pushCredentials({
1415
+ credentials,
1416
+ claudeDirs,
1417
+ announce
1418
+ }) {
1419
+ for (const claudeDir of new Set(claudeDirs)) {
1420
+ if (!existsSync(claudeDir))
1421
+ continue;
1422
+ try {
1423
+ writeSessionCredentials({ claudeDir, credentials });
1424
+ } catch (error) {
1425
+ announce(`Couldn't update the Claude login in ${claudeDir}: ${messageOf(error)}`);
1426
+ }
1427
+ }
1428
+ }
1429
+ function announcingOnce(announce, alreadySaid) {
1430
+ let last = alreadySaid;
72
1431
  return {
73
- kind: "Secret Service (secret-tool)",
74
- load: () => {
75
- const result = run("secret-tool", ["lookup", ...attributes]);
76
- return result.status === 0 ? decode(result.stdout) : undefined;
77
- },
78
- save: (token) => {
79
- const result = run("secret-tool", ["store", `--label=${SERVICE}`, ...attributes], encode(token));
80
- if (result.status !== 0) {
81
- throw new Error("Could not write to the Secret Service");
82
- }
1432
+ say: (message) => {
1433
+ if (message === last)
1434
+ return;
1435
+ last = message;
1436
+ announce(message);
83
1437
  },
84
- clear: () => {
85
- run("secret-tool", ["clear", ...attributes]);
1438
+ reset: () => {
1439
+ last = undefined;
86
1440
  }
87
1441
  };
88
1442
  }
89
- function fileStore(path = join(homedir(), ".config", "maesttro", "credentials.json")) {
90
- return {
91
- kind: `file (${path})`,
92
- load: () => {
93
- try {
94
- return decode(readFileSync(path, "utf8"));
95
- } catch {
96
- return;
97
- }
98
- },
99
- save: (token) => {
100
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
101
- const temporary = `${path}.tmp`;
102
- writeFileSync(temporary, encode(token), { mode: 384 });
103
- chmodSync(temporary, 384);
104
- renameSync(temporary, path);
1443
+ function syncSessionCredentials({
1444
+ store,
1445
+ claudeDir,
1446
+ now = Date.now
1447
+ }) {
1448
+ const master = store.load();
1449
+ if (!master || master.claudeAiOauth.expiresAt <= now())
1450
+ return;
1451
+ writeSessionCredentials({ claudeDir, credentials: master });
1452
+ }
1453
+ function pushToRunningSessions({
1454
+ credentials,
1455
+ claudeDir,
1456
+ run = runCommand,
1457
+ claudeDirOf = (id) => sessionClaudeDir({ id }),
1458
+ announce
1459
+ }) {
1460
+ pushCredentials({
1461
+ credentials,
1462
+ claudeDirs: [claudeDir, ...runningSessionIds({ run }).map(claudeDirOf)],
1463
+ announce
1464
+ });
1465
+ }
1466
+ function startClaudeRefresher({
1467
+ claudeDir,
1468
+ store,
1469
+ keeper,
1470
+ lockPath,
1471
+ workDir,
1472
+ clearLingering,
1473
+ run = runCommand,
1474
+ claudeDirOf = (id) => sessionClaudeDir({ id }),
1475
+ announce,
1476
+ launchFailure,
1477
+ now = Date.now,
1478
+ timers = nodeTimers
1479
+ }) {
1480
+ const initial = store.load();
1481
+ if (!initial)
1482
+ return;
1483
+ const refreshNotices = announcingOnce(announce, launchFailure);
1484
+ const syncNotices = announcingOnce(announce);
1485
+ const refresher = startRefresher({
1486
+ initial,
1487
+ expiresAtOf: (credentials) => credentials.claudeAiOauth.expiresAt,
1488
+ firstAttemptAt: launchFailure === undefined ? undefined : now() + LAUNCH_RETRY_MS,
1489
+ refresh: () => refreshMaster({
1490
+ store,
1491
+ keeper,
1492
+ lockPath,
1493
+ workDir,
1494
+ clearLingering,
1495
+ now,
1496
+ onRotated: (credentials) => pushToRunningSessions({
1497
+ credentials,
1498
+ claudeDir,
1499
+ run,
1500
+ claudeDirOf,
1501
+ announce: refreshNotices.say
1502
+ })
1503
+ }),
1504
+ onRefreshed: (credentials) => {
1505
+ refreshNotices.reset();
1506
+ pushCredentials({
1507
+ credentials,
1508
+ claudeDirs: [claudeDir],
1509
+ announce: refreshNotices.say
1510
+ });
105
1511
  },
106
- clear: () => {
107
- rmSync(path, { force: true });
1512
+ onFailed: (error) => refreshNotices.say(messageOf(error)),
1513
+ now,
1514
+ timers
1515
+ });
1516
+ const sync = timers.setInterval(() => {
1517
+ try {
1518
+ syncSessionCredentials({ store, claudeDir, now });
1519
+ syncNotices.reset();
1520
+ } catch (error) {
1521
+ syncNotices.say(messageOf(error));
108
1522
  }
1523
+ }, SESSION_SYNC_MS);
1524
+ return {
1525
+ stop: () => {
1526
+ timers.clearInterval(sync);
1527
+ return refresher.stop();
1528
+ },
1529
+ busy: refresher.busy
109
1530
  };
110
1531
  }
111
- function selectCredentialStore({
112
- platform = process.platform,
113
- env = process.env,
114
- run = runCommand
115
- } = {}) {
116
- if (env.MAESTTRO_CREDENTIAL_STORE === "file")
117
- return fileStore();
118
- if (platform === "darwin")
119
- return keychainStore(run);
120
- if (platform === "linux" && commandExists(run, "secret-tool")) {
121
- return secretToolStore(run);
122
- }
123
- return fileStore();
124
- }
125
1532
 
126
1533
  // src/github-api.ts
127
1534
  function githubApiUrl(env = process.env) {
@@ -312,7 +1719,7 @@ function loginDependencies() {
312
1719
  function describeExpiry(iso) {
313
1720
  return iso ? new Date(iso).toLocaleString() : "never";
314
1721
  }
315
- async function login() {
1722
+ async function loginGitHub() {
316
1723
  const dependencies = loginDependencies();
317
1724
  const token = await deviceLogin(dependencies);
318
1725
  const name = await githubLogin(token.access_token);
@@ -322,24 +1729,24 @@ async function ensureLogin() {
322
1729
  const dependencies = loginDependencies();
323
1730
  const token = await currentUserToken({ ...dependencies, interactive: true });
324
1731
  const name = token && await githubLogin(token.access_token);
325
- console.log(name ? `Logged in to GitHub as ${name}. Stored in ${dependencies.store.kind}.` : "GitHub login did not complete. Run `maesttro login` to retry.");
1732
+ console.log(name ? `Logged in to GitHub as ${name}. Stored in ${dependencies.store.kind}.` : "GitHub login did not complete. Run `maesttro login github` to retry.");
326
1733
  }
327
- function logout() {
1734
+ function logoutGitHub() {
328
1735
  const { store } = loginDependencies();
329
1736
  store.clear();
330
1737
  console.log(`Logged out. Removed GitHub credentials from ${store.kind}.`);
331
1738
  }
332
- async function status() {
1739
+ async function statusGitHub() {
333
1740
  const dependencies = loginDependencies();
334
1741
  const token = await currentUserToken({ ...dependencies, interactive: false });
335
1742
  if (!token) {
336
- console.log("Not logged in to GitHub. Run `maesttro login`.");
1743
+ console.log("Not logged in to GitHub. Run `maesttro login github`.");
337
1744
  process.exitCode = 1;
338
1745
  return;
339
1746
  }
340
1747
  const name = await githubLogin(token.access_token);
341
1748
  console.log([
342
- `GitHub: ${name ? `logged in as ${name}` : "token rejected — run `maesttro login`"}`,
1749
+ `GitHub: ${name ? `logged in as ${name}` : "token rejected — run `maesttro login github`"}`,
343
1750
  `Store: ${dependencies.store.kind}`,
344
1751
  `Access token expires: ${describeExpiry(token.expires_at)}`,
345
1752
  `Refresh token expires: ${describeExpiry(token.refresh_expires_at)}`
@@ -348,112 +1755,186 @@ async function status() {
348
1755
  if (!name)
349
1756
  process.exitCode = 1;
350
1757
  }
1758
+ function parseLoginTargets(args) {
1759
+ const [target, ...rest] = args;
1760
+ if (rest.length > 0)
1761
+ return;
1762
+ if (target === undefined)
1763
+ return ["github", "claude"];
1764
+ return target === "github" || target === "claude" ? [target] : undefined;
1765
+ }
1766
+ function claudeLogin({
1767
+ capture = () => captureLogin(),
1768
+ store = claudeStore(),
1769
+ forgetLeftovers = () => discardKeeperLeftovers(),
1770
+ announce = (message) => console.log(message)
1771
+ } = {}) {
1772
+ announce("Logging in to Claude in a throwaway runner (opens a URL; paste the code back)...");
1773
+ const credentials = capture();
1774
+ if (!credentials) {
1775
+ announce("Claude login did not complete. Run `maesttro login claude` to retry.");
1776
+ return false;
1777
+ }
1778
+ store.save(credentials);
1779
+ forgetLeftovers();
1780
+ announce(`Logged in to Claude. Stored in ${store.kind}.`);
1781
+ return true;
1782
+ }
1783
+ function describeClaudeStatus({
1784
+ credentials,
1785
+ now
1786
+ }) {
1787
+ if (!credentials) {
1788
+ return {
1789
+ loggedIn: false,
1790
+ lines: ["Claude: not logged in — run `maesttro login claude`"]
1791
+ };
1792
+ }
1793
+ const { expiresAt, refreshTokenExpiresAt } = credentials.claudeAiOauth;
1794
+ const loggedIn = claudeLoginState({ credentials, now }) === "active";
1795
+ const warning = claudeLoginWarning({ credentials, now });
1796
+ return {
1797
+ loggedIn,
1798
+ lines: [
1799
+ loggedIn ? "Claude: logged in" : "Claude: login expired — run `maesttro login claude`",
1800
+ ...warning ? [`Warning: ${warning}`] : [],
1801
+ `Access token expires: ${new Date(expiresAt).toLocaleString()}`,
1802
+ `Refresh token expires: ${refreshTokenExpiresAt ? new Date(refreshTokenExpiresAt).toLocaleString() : "unknown"}`
1803
+ ]
1804
+ };
1805
+ }
1806
+ function unknownTarget(command) {
1807
+ console.log(`Usage: maesttro ${command} [github|claude]`);
1808
+ process.exitCode = 1;
1809
+ }
1810
+ async function login({ args }) {
1811
+ const targets = parseLoginTargets(args);
1812
+ if (!targets)
1813
+ return unknownTarget("login");
1814
+ if (targets.includes("github"))
1815
+ await loginGitHub();
1816
+ if (targets.includes("claude") && !claudeLogin())
1817
+ process.exitCode = 1;
1818
+ }
1819
+ function logout({ args }) {
1820
+ const targets = parseLoginTargets(args);
1821
+ if (!targets) {
1822
+ unknownTarget("logout");
1823
+ return;
1824
+ }
1825
+ if (targets.includes("github"))
1826
+ logoutGitHub();
1827
+ if (targets.includes("claude")) {
1828
+ const store = claudeStore();
1829
+ store.clear();
1830
+ discardKeeperLeftovers();
1831
+ console.log(`Logged out. Removed Claude credentials from ${store.kind}.`);
1832
+ }
1833
+ }
1834
+ async function status({ args }) {
1835
+ const targets = parseLoginTargets(args);
1836
+ if (!targets)
1837
+ return unknownTarget("status");
1838
+ if (targets.includes("github"))
1839
+ await statusGitHub();
1840
+ if (targets.includes("claude")) {
1841
+ const { loggedIn, lines } = describeClaudeStatus({
1842
+ credentials: claudeStore().load(),
1843
+ now: Date.now()
1844
+ });
1845
+ console.log(lines.join(`
1846
+ `));
1847
+ if (!loggedIn)
1848
+ process.exitCode = 1;
1849
+ }
1850
+ }
1851
+
1852
+ // src/clients.ts
1853
+ var CLIENTS = {
1854
+ claude: {
1855
+ label: "Claude Code",
1856
+ npmPackage: "@anthropic-ai/claude-code",
1857
+ binary: "claude",
1858
+ configDir: "/home/agent/.claude"
1859
+ },
1860
+ opencode: {
1861
+ label: "opencode",
1862
+ npmPackage: "opencode-ai",
1863
+ binary: "opencode",
1864
+ configDir: "/home/agent/.config/opencode"
1865
+ }
1866
+ };
1867
+ var DEFAULT_CLIENT = "opencode";
1868
+ function isClientKey(value) {
1869
+ return Object.hasOwn(CLIENTS, value);
1870
+ }
351
1871
 
352
1872
  // src/configure.ts
353
1873
  import {
354
1874
  cpSync as cpSync2,
355
- existsSync as existsSync2,
356
- mkdirSync as mkdirSync4,
357
- readdirSync,
358
- readFileSync as readFileSync4,
359
- writeFileSync as writeFileSync4
1875
+ existsSync as existsSync3,
1876
+ mkdirSync as mkdirSync9,
1877
+ readdirSync as readdirSync3,
1878
+ readFileSync as readFileSync10,
1879
+ writeFileSync as writeFileSync9
360
1880
  } from "node:fs";
361
1881
  import { homedir as homedir2 } from "node:os";
362
- import { dirname as dirname5, join as join4 } from "node:path";
1882
+ import { dirname as dirname6, join as join10 } from "node:path";
363
1883
 
364
- // src/client-configs.ts
365
- import {
366
- chmodSync as chmodSync2,
367
- cpSync,
368
- existsSync,
369
- mkdirSync as mkdirSync3,
370
- mkdtempSync,
371
- readFileSync as readFileSync3,
372
- rmSync as rmSync2,
373
- writeFileSync as writeFileSync3
374
- } from "node:fs";
375
- import { tmpdir } from "node:os";
376
- import { dirname as dirname3, join as join2 } from "node:path";
1884
+ // src/claude-state.ts
1885
+ import { readFileSync as readFileSync8, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "node:fs";
1886
+ import { join as join8 } from "node:path";
377
1887
 
378
- // src/json-file.ts
379
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
380
- import { dirname as dirname2 } from "node:path";
381
- function isObject(value) {
382
- return typeof value === "object" && value !== null && !Array.isArray(value);
383
- }
384
- function mergeJson(existing, curated) {
385
- const merged = { ...existing };
386
- for (const [key, value] of Object.entries(curated)) {
387
- const current = merged[key];
388
- merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
389
- }
390
- return merged;
1888
+ // src/runner-paths.ts
1889
+ var CONTAINER_REPO_PATH = "/workspace";
1890
+
1891
+ // src/claude-state.ts
1892
+ function seededClaudeState({
1893
+ existing,
1894
+ claudeVersion
1895
+ }) {
1896
+ const projects = isObject(existing.projects) ? existing.projects : {};
1897
+ return {
1898
+ ...existing,
1899
+ hasCompletedOnboarding: true,
1900
+ ...claudeVersion ? { lastOnboardingVersion: claudeVersion } : {},
1901
+ theme: existing.theme ?? "dark",
1902
+ projects: mergeJson(projects, {
1903
+ [CONTAINER_REPO_PATH]: { hasTrustDialogAccepted: true }
1904
+ })
1905
+ };
391
1906
  }
392
- function readJson(path) {
1907
+ function installedClaudeVersion(root = "/usr/local/lib/node_modules/@anthropic-ai/claude-code") {
393
1908
  try {
394
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
395
- return isObject(parsed) ? parsed : {};
1909
+ return JSON.parse(readFileSync8(join8(root, "package.json"), "utf8")).version;
396
1910
  } catch {
397
- return {};
1911
+ return;
398
1912
  }
399
1913
  }
400
- function writeJson(path, value) {
401
- mkdirSync2(dirname2(path), { recursive: true });
402
- writeFileSync2(path, `${JSON.stringify(value, null, 2)}
1914
+ function seedClaudeState({
1915
+ home,
1916
+ claudeVersion = installedClaudeVersion()
1917
+ }) {
1918
+ const path = join8(home, ".claude.json");
1919
+ const state = seededClaudeState({ existing: readJson(path), claudeVersion });
1920
+ writeFileSync7(`${path}.tmp`, `${JSON.stringify(state, null, 2)}
403
1921
  `);
1922
+ renameSync6(`${path}.tmp`, path);
404
1923
  }
405
- function skipInsignificant(text, from) {
406
- let index = from;
407
- while (index < text.length) {
408
- if (/\s/.test(text[index] ?? "")) {
409
- index++;
410
- } else if (text.startsWith("//", index)) {
411
- while (index < text.length && text[index] !== `
412
- `)
413
- index++;
414
- } else if (text.startsWith("/*", index)) {
415
- const end = text.indexOf("*/", index + 2);
416
- index = end === -1 ? text.length : end + 2;
417
- } else {
418
- return index;
419
- }
420
- }
421
- return index;
422
- }
423
- function stripJsonc(text) {
424
- let output = "";
425
- let index = 0;
426
- while (index < text.length) {
427
- const char = text[index];
428
- if (char === '"') {
429
- const start = index;
430
- index++;
431
- while (index < text.length && text[index] !== '"') {
432
- index += text[index] === "\\" ? 2 : 1;
433
- }
434
- index++;
435
- output += text.slice(start, index);
436
- continue;
437
- }
438
- const significant = skipInsignificant(text, index);
439
- if (significant > index) {
440
- output += " ";
441
- index = significant;
442
- continue;
443
- }
444
- const trailingComma = char === "," && /[}\]]/.test(text[skipInsignificant(text, index + 1)] ?? "");
445
- if (!trailingComma)
446
- output += char;
447
- index++;
448
- }
449
- return output;
450
- }
451
- function parseJsonc(text) {
452
- const parsed = JSON.parse(stripJsonc(text));
453
- if (!isObject(parsed))
454
- throw new Error("expected a JSON object");
455
- return parsed;
456
- }
1924
+
1925
+ // src/client-configs.ts
1926
+ import {
1927
+ chmodSync as chmodSync4,
1928
+ cpSync,
1929
+ existsSync as existsSync2,
1930
+ mkdirSync as mkdirSync8,
1931
+ mkdtempSync as mkdtempSync2,
1932
+ readFileSync as readFileSync9,
1933
+ rmSync as rmSync6,
1934
+ writeFileSync as writeFileSync8
1935
+ } from "node:fs";
1936
+ import { tmpdir as tmpdir2 } from "node:os";
1937
+ import { dirname as dirname5, join as join9 } from "node:path";
457
1938
 
458
1939
  // src/maesttro-endpoint.ts
459
1940
  var DEFAULT_MAESTTRO_URL = "http://100.64.0.2";
@@ -667,11 +2148,11 @@ function readHostFile({
667
2148
  parse,
668
2149
  warn
669
2150
  }) {
670
- const path = join2(home, file);
671
- if (!existsSync(path))
2151
+ const path = join9(home, file);
2152
+ if (!existsSync2(path))
672
2153
  return {};
673
2154
  try {
674
- return parse(readFileSync3(path, "utf8"));
2155
+ return parse(readFileSync9(path, "utf8"));
675
2156
  } catch {
676
2157
  warn(`maesttro: ignoring your ~/${file}, it doesn't parse`);
677
2158
  return {};
@@ -703,66 +2184,46 @@ function buildClientConfigs({
703
2184
  removed: opencode.removed.map((path) => `opencode ${path}`)
704
2185
  };
705
2186
  }
706
- function writeClientConfigDir(files, parent = tmpdir()) {
707
- const root = mkdtempSync(join2(parent, "maesttro-clients-"));
708
- chmodSync2(root, 448);
709
- const dir = join2(root, "home");
2187
+ function writeClientConfigDir(files, parent = tmpdir2()) {
2188
+ const root = mkdtempSync2(join9(parent, "maesttro-clients-"));
2189
+ chmodSync4(root, 448);
2190
+ const dir = join9(root, "home");
710
2191
  for (const [file, content] of Object.entries(files)) {
711
- const path = join2(dir, file);
712
- mkdirSync3(dirname3(path), { recursive: true, mode: 493 });
713
- writeFileSync3(path, content);
714
- chmodSync2(path, 420);
2192
+ const path = join9(dir, file);
2193
+ mkdirSync8(dirname5(path), { recursive: true, mode: 493 });
2194
+ writeFileSync8(path, content);
2195
+ chmodSync4(path, 420);
715
2196
  }
716
- chmodSync2(dir, 493);
717
- return { dir, remove: () => rmSync2(root, { recursive: true, force: true }) };
2197
+ chmodSync4(dir, 493);
2198
+ return { dir, remove: () => rmSync6(root, { recursive: true, force: true }) };
718
2199
  }
719
2200
  function installClientConfigs({
720
2201
  home,
721
2202
  source = CONTAINER_CLIENT_CONFIGS
722
2203
  }) {
723
- if (!existsSync(source))
2204
+ if (!existsSync2(source))
724
2205
  return false;
725
- const merged = MERGED_INTO_HOME.map((file) => join2(source, file));
2206
+ const merged = MERGED_INTO_HOME.map((file) => join9(source, file));
726
2207
  cpSync(source, home, {
727
2208
  recursive: true,
728
2209
  force: true,
729
2210
  filter: (path) => !merged.includes(path)
730
2211
  });
731
2212
  for (const file of MERGED_INTO_HOME) {
732
- const staged = join2(source, file);
733
- if (!existsSync(staged))
2213
+ const staged = join9(source, file);
2214
+ if (!existsSync2(staged))
734
2215
  continue;
735
- const target = join2(home, file);
2216
+ const target = join9(home, file);
736
2217
  writeJson(target, mergeJson(readJson(target), readJson(staged)));
737
2218
  }
738
2219
  return true;
739
2220
  }
740
2221
  function injectOpencodeTuiState({ home }) {
741
- const path = join2(home, OPENCODE_TUI_STATE);
2222
+ const path = join9(home, OPENCODE_TUI_STATE);
742
2223
  writeJson(path, mergeJson(readJson(path), CURATED_OPENCODE_TUI_STATE));
743
2224
  return path;
744
2225
  }
745
2226
 
746
- // src/clients.ts
747
- var CLIENTS = {
748
- claude: {
749
- label: "Claude Code",
750
- npmPackage: "@anthropic-ai/claude-code",
751
- binary: "claude",
752
- configDir: "/home/agent/.claude"
753
- },
754
- opencode: {
755
- label: "opencode",
756
- npmPackage: "opencode-ai",
757
- binary: "opencode",
758
- configDir: "/home/agent/.config/opencode"
759
- }
760
- };
761
- var DEFAULT_CLIENT = "opencode";
762
- function isClientKey(value) {
763
- return Object.hasOwn(CLIENTS, value);
764
- }
765
-
766
2227
  // src/host-instructions.ts
767
2228
  var HOST_STAGING_DIR = "/opt/maesttro-host";
768
2229
  var STAGED_DIRECTORIES = [
@@ -776,14 +2237,6 @@ var INSTRUCTION_FILES = [
776
2237
  ];
777
2238
  var STAGED_PATHS = [...INSTRUCTION_FILES, ...STAGED_DIRECTORIES];
778
2239
 
779
- // src/paths.ts
780
- import { dirname as dirname4, join as join3, resolve } from "node:path";
781
- import { fileURLToPath } from "node:url";
782
- var PACKAGE_ROOT = resolve(dirname4(fileURLToPath(import.meta.url)), "..");
783
- var DOCKERFILE_DIR = join3(PACKAGE_ROOT, "docker");
784
- var IMAGE = "maesttro-runner:latest";
785
- var VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
786
-
787
2240
  // src/runner-context.ts
788
2241
  function githubMode(value) {
789
2242
  return value === "on" || value === "strict" ? value : "off";
@@ -832,6 +2285,13 @@ function runnerInstructions(context) {
832
2285
  "- Work in this directory. Anything outside it is not persisted.",
833
2286
  "- Commit on a branch you create here. The user pushes it from their machine with `maesttro push`; the runner cannot push, merge, or delete remote branches.",
834
2287
  "",
2288
+ "## Installing tools",
2289
+ "",
2290
+ "- Preinstalled: Node 24 (npm, pnpm, yarn), TypeScript (`tsc`), Bun, Python 3.14 and uv (`uv venv`, `uv pip`, `uv tool`; plain `pip` and `python -m venv` work too), the .NET 10 SDK, a C/C++ build toolchain, and common CLI tools (jq, ripgrep, fd, unzip, zip, sqlite3).",
2291
+ "- You can install more yourself without root: `npm i -g`, `bun add -g`, `pip install`, `uv tool install`, `uv python install`, `dotnet tool install -g`. They work as usual and are on `PATH` right away.",
2292
+ "- Installs last only until this container exits; install again when a new session needs them. Your home directory belongs to this session: it survives `maesttro resume` but no other session sees it.",
2293
+ "- System packages are unavailable: there is no root, `sudo` or `apt`. If a task needs one, tell the user which package and why.",
2294
+ "",
835
2295
  "## Pushing",
836
2296
  "",
837
2297
  "- `git push` does not work here: the runner has read-only access to the remote. This is deliberate.",
@@ -856,24 +2316,24 @@ function withoutComments(value) {
856
2316
  }
857
2317
  function applyProviderConfigs({
858
2318
  home,
859
- configsDir = join4(DOCKERFILE_DIR, "configs")
2319
+ configsDir = join10(DOCKERFILE_DIR, "configs")
860
2320
  }) {
861
2321
  const written = [];
862
2322
  for (const [key, client] of Object.entries(CLIENTS)) {
863
- const source = join4(configsDir, key);
864
- if (!existsSync2(source))
2323
+ const source = join10(configsDir, key);
2324
+ if (!existsSync3(source))
865
2325
  continue;
866
- const targetDir = join4(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
867
- for (const name of readdirSync(source)) {
2326
+ const targetDir = join10(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
2327
+ for (const name of readdirSync3(source)) {
868
2328
  if (!name.endsWith(".json"))
869
2329
  continue;
870
- const curated = withoutComments(readJson(join4(source, name)));
2330
+ const curated = withoutComments(readJson(join10(source, name)));
871
2331
  if (Object.keys(curated).length === 0)
872
2332
  continue;
873
- const target = join4(targetDir, name);
874
- mkdirSync4(targetDir, { recursive: true });
2333
+ const target = join10(targetDir, name);
2334
+ mkdirSync9(targetDir, { recursive: true });
875
2335
  const merged = mergeJson(readJson(target), curated);
876
- writeFileSync4(target, `${JSON.stringify(merged, null, 2)}
2336
+ writeFileSync9(target, `${JSON.stringify(merged, null, 2)}
877
2337
  `);
878
2338
  written.push(target);
879
2339
  }
@@ -886,27 +2346,27 @@ function stageHostInstructions({
886
2346
  context
887
2347
  }) {
888
2348
  for (const directory of STAGED_DIRECTORIES) {
889
- const source = join4(staging, directory);
890
- if (!existsSync2(source))
2349
+ const source = join10(staging, directory);
2350
+ if (!existsSync3(source))
891
2351
  continue;
892
2352
  try {
893
- cpSync2(source, join4(home, directory), { recursive: true, force: true });
2353
+ cpSync2(source, join10(home, directory), { recursive: true, force: true });
894
2354
  } catch {}
895
2355
  }
896
2356
  const instructions = runnerInstructions(context);
897
2357
  for (const file of INSTRUCTION_FILES) {
898
- const source = join4(staging, file);
899
- const hostCopy = existsSync2(source) ? readFileSync4(source, "utf8") : "";
900
- const target = join4(home, file);
901
- mkdirSync4(dirname5(target), { recursive: true });
902
- writeFileSync4(target, hostCopy ? `${hostCopy.trimEnd()}
2358
+ const source = join10(staging, file);
2359
+ const hostCopy = existsSync3(source) ? readFileSync10(source, "utf8") : "";
2360
+ const target = join10(home, file);
2361
+ mkdirSync9(dirname6(target), { recursive: true });
2362
+ writeFileSync9(target, hostCopy ? `${hostCopy.trimEnd()}
903
2363
 
904
2364
  ${instructions}` : instructions);
905
2365
  }
906
2366
  }
907
2367
  function configure({
908
2368
  home = homedir2(),
909
- inRunner = existsSync2(RUNNER_MARKER),
2369
+ inRunner = existsSync3(RUNNER_MARKER),
910
2370
  env = process.env,
911
2371
  staging = HOST_STAGING_DIR,
912
2372
  clientConfigs
@@ -915,6 +2375,7 @@ function configure({
915
2375
  if (!inRunner)
916
2376
  return;
917
2377
  installClientConfigs({ home, source: clientConfigs });
2378
+ seedClaudeState({ home });
918
2379
  injectOpencodeTuiState({ home });
919
2380
  stageHostInstructions({
920
2381
  home,
@@ -988,64 +2449,42 @@ async function revokeSessionToken(token, fetchImpl = fetch, apiUrl = githubApiUr
988
2449
  await fetchImpl(new URL("/installation/token", apiUrl), {
989
2450
  method: "DELETE",
990
2451
  headers: {
991
- Accept: "application/vnd.github+json",
992
- Authorization: `Bearer ${token}`,
993
- "User-Agent": "maesttro"
994
- },
995
- signal: AbortSignal.timeout(5000)
996
- });
997
- } catch {}
998
- }
999
-
1000
- // src/host-identity.ts
1001
- function hostIdentityEnv() {
1002
- if (typeof process.getuid !== "function" || typeof process.getgid !== "function") {
1003
- return {};
1004
- }
1005
- return {
1006
- MAESTTRO_HOST_UID: String(process.getuid()),
1007
- MAESTTRO_HOST_GID: String(process.getgid())
1008
- };
1009
- }
1010
- function hostIdentityFlags() {
1011
- return Object.entries(hostIdentityEnv()).map(([name, value]) => `-e ${name}=${value}`).join(" ");
1012
- }
1013
-
1014
- // src/legacy-token.ts
1015
- import { execSync } from "node:child_process";
1016
- var LEGACY_HOSTS = "~/.config/gh/hosts.yml";
1017
- var PURGE_LEGACY_TOKEN = [
1018
- `if [ -e ${LEGACY_HOSTS} ]; then rm -f ${LEGACY_HOSTS}; echo FOUND; fi`,
1019
- "git config --global --remove-section credential.https://github.com 2>/dev/null || true",
1020
- "git config --global --remove-section credential.https://gist.github.com 2>/dev/null || true"
1021
- ].join("; ");
1022
- var DETECT_LEGACY_TOKEN = `if [ -e ${LEGACY_HOSTS} ]; then echo FOUND; else echo CLEAN; fi`;
1023
- function inVolume(command) {
1024
- return execSync(`docker run --rm ${hostIdentityFlags()} -v ${VOLUME}:/home/agent ${IMAGE} "${command}"`).toString();
1025
- }
1026
- function purgeLegacyToken() {
1027
- return inVolume(PURGE_LEGACY_TOKEN).includes("FOUND");
1028
- }
1029
- function hasLegacyToken() {
1030
- return inVolume(DETECT_LEGACY_TOKEN).includes("FOUND");
2452
+ Accept: "application/vnd.github+json",
2453
+ Authorization: `Bearer ${token}`,
2454
+ "User-Agent": "maesttro"
2455
+ },
2456
+ signal: AbortSignal.timeout(5000)
2457
+ });
2458
+ } catch {}
1031
2459
  }
1032
- var REVOKE_NOTICE = "Removed a long-lived GitHub token that an older maesttro stored in the runner volume. Revoke it on GitHub too: https://github.com/settings/tokens?type=beta";
1033
- function purgeLegacyTokenOnce({
1034
- config,
1035
- saveConfig,
1036
- purge = purgeLegacyToken,
1037
- announce
1038
- }) {
1039
- if (config.legacyTokenPurged)
1040
- return;
1041
- if (purge())
1042
- announce(REVOKE_NOTICE);
1043
- saveConfig({ ...config, legacyTokenPurged: true });
2460
+
2461
+ // src/network-probe.ts
2462
+ import { connect } from "node:net";
2463
+ async function reachable(host, port, {
2464
+ timeoutMs = 2000,
2465
+ connectImpl = connect
2466
+ } = {}) {
2467
+ return new Promise((resolve) => {
2468
+ const socket = connectImpl(port, host);
2469
+ const timer = setTimeout(() => {
2470
+ socket.destroy();
2471
+ resolve(false);
2472
+ }, timeoutMs);
2473
+ socket.once("connect", () => {
2474
+ clearTimeout(timer);
2475
+ socket.destroy();
2476
+ resolve(true);
2477
+ });
2478
+ socket.once("error", () => {
2479
+ clearTimeout(timer);
2480
+ resolve(false);
2481
+ });
2482
+ });
1044
2483
  }
1045
2484
 
1046
2485
  // src/repo.ts
1047
- import { appendFileSync, mkdirSync as mkdirSync5, realpathSync } from "node:fs";
1048
- import { join as join5 } from "node:path";
2486
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync10, realpathSync } from "node:fs";
2487
+ import { join as join11 } from "node:path";
1049
2488
  var MINIMUM_GIT = [2, 48];
1050
2489
  var WORKTREES_DIR = ".worktrees";
1051
2490
 
@@ -1088,7 +2527,7 @@ function repoRoot(run, cwd) {
1088
2527
  "--git-common-dir"
1089
2528
  ]).out;
1090
2529
  const root = realpathSync(top.out);
1091
- if (realpathSync(commonDir) !== join5(root, ".git")) {
2530
+ if (realpathSync(commonDir) !== join11(root, ".git")) {
1092
2531
  throw new LaunchError("This is a git worktree. Run maesttro from the main checkout; each session gets its own isolated clone.");
1093
2532
  }
1094
2533
  if (realpathSync(cwd) !== root) {
@@ -1115,12 +2554,12 @@ function defaultBranch(run, root) {
1115
2554
  throw new LaunchError("Can't find the default branch of origin. Run `git remote set-head origin --auto`.");
1116
2555
  }
1117
2556
  function ensureWorktreesDir(run, root) {
1118
- mkdirSync5(join5(root, WORKTREES_DIR), { recursive: true });
2557
+ mkdirSync10(join11(root, WORKTREES_DIR), { recursive: true });
1119
2558
  if (git(run, root, ["check-ignore", "-q", `${WORKTREES_DIR}/`]).ok)
1120
2559
  return;
1121
- const info = join5(root, ".git", "info");
1122
- mkdirSync5(info, { recursive: true });
1123
- appendFileSync(join5(info, "exclude"), `
2560
+ const info = join11(root, ".git", "info");
2561
+ mkdirSync10(info, { recursive: true });
2562
+ appendFileSync2(join11(info, "exclude"), `
1124
2563
  ${WORKTREES_DIR}/
1125
2564
  `);
1126
2565
  }
@@ -1150,21 +2589,108 @@ function revision(run, root, rev) {
1150
2589
  return parsed.ok ? parsed.out : undefined;
1151
2590
  }
1152
2591
 
2592
+ // src/volume-migration.ts
2593
+ import { execSync } from "node:child_process";
2594
+ var LEGACY_HOSTS = "~/.config/gh/hosts.yml";
2595
+ var PURGE_LEGACY_TOKEN = [
2596
+ `if [ -e ${LEGACY_HOSTS} ]; then rm -f ${LEGACY_HOSTS}; echo FOUND; fi`,
2597
+ "git config --global --remove-section credential.https://github.com 2>/dev/null || true",
2598
+ "git config --global --remove-section credential.https://gist.github.com 2>/dev/null || true"
2599
+ ].join("; ");
2600
+ var REVOKE_NOTICE = "Removed a long-lived GitHub token that an older maesttro stored in the runner volume. Revoke it on GitHub too: https://github.com/settings/tokens?type=beta";
2601
+ var OLD_SESSIONS_RUNNING = `Old sessions still use ${LEGACY_VOLUME}; will move the Claude login once they exit.`;
2602
+ function dockerExec(command) {
2603
+ try {
2604
+ return execSync(command, { stdio: ["ignore", "pipe", "pipe"] }).toString();
2605
+ } catch (error) {
2606
+ const stderr = error?.stderr;
2607
+ throw new Error(stderr ? stderr.toString().trim() : error.message);
2608
+ }
2609
+ }
2610
+ function legacyVolumeExists({
2611
+ exec = dockerExec
2612
+ } = {}) {
2613
+ try {
2614
+ exec(`docker volume inspect ${LEGACY_VOLUME}`);
2615
+ return true;
2616
+ } catch (error) {
2617
+ if (error instanceof Error && /no such volume/i.test(error.message)) {
2618
+ return false;
2619
+ }
2620
+ throw error;
2621
+ }
2622
+ }
2623
+ function dockerVolume({
2624
+ exec = dockerExec
2625
+ } = {}) {
2626
+ const inVolume = (command) => exec(`docker run --rm ${hostIdentityFlags()} -v ${LEGACY_VOLUME}:/home/agent ${IMAGE} "${command}"`);
2627
+ return {
2628
+ exists: () => legacyVolumeExists({ exec }),
2629
+ inUse: () => exec(`docker ps -q --filter volume=${LEGACY_VOLUME}`).trim().length > 0,
2630
+ readClaudeCredentials: () => {
2631
+ const output = inVolume("cat ~/.claude/.credentials.json 2>/dev/null || echo null");
2632
+ try {
2633
+ return JSON.parse(output);
2634
+ } catch {
2635
+ return;
2636
+ }
2637
+ },
2638
+ purgeGitHubToken: () => inVolume(PURGE_LEGACY_TOKEN).includes("FOUND")
2639
+ };
2640
+ }
2641
+ function migrateFromVolumeOnce({
2642
+ config,
2643
+ saveConfig,
2644
+ store,
2645
+ volume = dockerVolume(),
2646
+ announce
2647
+ }) {
2648
+ if (config.volumeMigrated)
2649
+ return;
2650
+ try {
2651
+ if (volume.exists()) {
2652
+ if (!config.legacyTokenPurged && volume.purgeGitHubToken()) {
2653
+ announce(REVOKE_NOTICE);
2654
+ }
2655
+ if (volume.inUse()) {
2656
+ announce(OLD_SESSIONS_RUNNING);
2657
+ saveConfig({ ...config, legacyTokenPurged: true });
2658
+ return;
2659
+ }
2660
+ const credentials = volume.readClaudeCredentials();
2661
+ if (!store.load() && isMasterLogin(credentials)) {
2662
+ store.save(credentials);
2663
+ announce(`Moved the runner's Claude login into ${store.kind}.`);
2664
+ }
2665
+ announce(`Sessions no longer use the shared ${LEGACY_VOLUME} volume. Remove it with \`docker volume rm ${LEGACY_VOLUME}\` once nothing runs.`);
2666
+ }
2667
+ } catch {
2668
+ announce(`Couldn't check the legacy ${LEGACY_VOLUME} volume; will retry next launch.`);
2669
+ return;
2670
+ }
2671
+ saveConfig({ ...config, volumeMigrated: true });
2672
+ }
2673
+
1153
2674
  // src/doctor.ts
1154
- async function check(label, probe) {
2675
+ class DoctorWarning extends Error {
2676
+ }
2677
+ async function check(label, probe, print = console.log) {
1155
2678
  try {
1156
2679
  const detail = await probe();
1157
- console.log(` [ok] ${label}${detail ? ` — ${detail}` : ""}`);
2680
+ print(` [ok] ${label}${detail ? ` — ${detail}` : ""}`);
1158
2681
  return true;
1159
2682
  } catch (error) {
2683
+ if (error instanceof DoctorWarning) {
2684
+ print(` [warn] ${label} — ${error.message}`);
2685
+ return true;
2686
+ }
1160
2687
  const reason = error instanceof Error && error.message ? error.message : "";
1161
- console.log(` [fail] ${label}${reason ? ` — ${reason}` : ""}`);
2688
+ print(` [fail] ${label}${reason ? ` — ${reason}` : ""}`);
1162
2689
  return false;
1163
2690
  }
1164
2691
  }
1165
- function inRunner(command, { withVolume = true } = {}) {
1166
- const volume = withVolume ? `-v ${VOLUME}:/home/agent ` : "";
1167
- return execSync2(`docker run --rm ${hostIdentityFlags()} ${volume}${IMAGE} "${command}"`).toString();
2692
+ function inRunner(command) {
2693
+ return execSync2(`docker run --rm ${hostIdentityFlags()} ${IMAGE} "${command}"`).toString();
1168
2694
  }
1169
2695
  async function doctor() {
1170
2696
  console.log(`== maesttro doctor ==
@@ -1188,19 +2714,6 @@ async function doctor() {
1188
2714
  "Runner image built",
1189
2715
  () => `built ${execSync2(`docker image inspect ${IMAGE} --format "{{.Created}}"`).toString().trim()}`
1190
2716
  ],
1191
- [
1192
- "Persistent volume exists",
1193
- () => {
1194
- execSync2(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
1195
- }
1196
- ],
1197
- [
1198
- "No long-lived GitHub token in the volume",
1199
- () => {
1200
- if (hasLegacyToken())
1201
- throw new Error("run `maesttro setup` to remove it");
1202
- }
1203
- ],
1204
2717
  [
1205
2718
  "Logged in to GitHub",
1206
2719
  async () => {
@@ -1211,13 +2724,36 @@ async function doctor() {
1211
2724
  interactive: false
1212
2725
  });
1213
2726
  if (!token)
1214
- throw new Error("run `maesttro login`");
2727
+ throw new Error("run `maesttro login github`");
1215
2728
  const name = await githubLogin(token.access_token);
1216
2729
  if (!name)
1217
- throw new Error("token rejected, run `maesttro login`");
2730
+ throw new Error("token rejected, run `maesttro login github`");
1218
2731
  return name;
1219
2732
  }
1220
2733
  ],
2734
+ [
2735
+ "Logged in to Claude",
2736
+ () => {
2737
+ const credentials = claudeStore().load();
2738
+ if (!credentials)
2739
+ throw new Error("run `maesttro login claude`");
2740
+ if (claudeLoginState({ credentials, now: Date.now() }) === "expired") {
2741
+ throw new ClaudeLoginExpired;
2742
+ }
2743
+ const warning = claudeLoginWarning({ credentials, now: Date.now() });
2744
+ if (warning)
2745
+ throw new DoctorWarning(warning);
2746
+ return `access token until ${new Date(credentials.claudeAiOauth.expiresAt).toLocaleString()}`;
2747
+ }
2748
+ ],
2749
+ [
2750
+ "Old shared volume removed",
2751
+ () => {
2752
+ if (legacyVolumeExists()) {
2753
+ throw new Error(`docker volume rm ${LEGACY_VOLUME}`);
2754
+ }
2755
+ }
2756
+ ],
1221
2757
  [
1222
2758
  "Token broker reachable",
1223
2759
  async () => {
@@ -1227,6 +2763,16 @@ async function doctor() {
1227
2763
  return broker;
1228
2764
  }
1229
2765
  ],
2766
+ [
2767
+ "dgx-spark reachable",
2768
+ async () => {
2769
+ const { hostname, port } = new URL(DGX_SPARK_URL);
2770
+ if (!await reachable(hostname, Number(port))) {
2771
+ throw new Error(`${DGX_SPARK_URL} (on the tailnet?)`);
2772
+ }
2773
+ return DGX_SPARK_URL;
2774
+ }
2775
+ ],
1230
2776
  [
1231
2777
  "Personal SSH key NOT visible inside runner (isolation intact)",
1232
2778
  () => {
@@ -1239,7 +2785,7 @@ async function doctor() {
1239
2785
  ...Object.values(CLIENTS).map((client) => [
1240
2786
  `Client available: ${client.label}`,
1241
2787
  () => {
1242
- inRunner(`which ${client.binary}`, { withVolume: false });
2788
+ inRunner(`which ${client.binary}`);
1243
2789
  }
1244
2790
  ])
1245
2791
  ];
@@ -1263,7 +2809,9 @@ Usage:
1263
2809
  maesttro <client> Same, with a specific client — see \`maesttro list\`
1264
2810
  maesttro --strict [client]
1265
2811
  Same, in strict mode: no GitHub token reaches the runner, so \`gh\` is off
1266
- maesttro resume <id> Continue a kept session in its clone, in the mode it started with
2812
+ maesttro resume [client] <id>
2813
+ Continue a kept session in its clone, in the mode it started with,
2814
+ with its last client unless one is given
1267
2815
  (\`--strict\` or \`--no-strict\` after the id changes it)
1268
2816
  maesttro sessions List this repo's sessions
1269
2817
  maesttro sessions remove <id>
@@ -1274,74 +2822,35 @@ Usage:
1274
2822
  maesttro push [branch] [--force]
1275
2823
  Push a branch a session created, after showing what goes out
1276
2824
  maesttro list Show available clients
1277
- maesttro setup One-time: build image, create volume, log in to GitHub
1278
- maesttro login Log in to GitHub (device flow) so sessions get repo access
1279
- maesttro logout Remove the stored GitHub credentials
1280
- maesttro status Show GitHub login status
2825
+ maesttro setup One-time: build image, move off the old shared volume, log in to GitHub and Claude
2826
+ maesttro login [github|claude]
2827
+ Log in to GitHub (device flow) and/or Claude; without a target, does both
2828
+ maesttro logout [github|claude]
2829
+ Remove the stored credentials; without a target, does both
2830
+ maesttro status [github|claude]
2831
+ Show login status; without a target, does both
1281
2832
  maesttro configure Apply maesttro's client configs (also runs inside the runner)
1282
2833
  maesttro update Update the package and rebuild the runner image with refreshed client configs
1283
- maesttro doctor Health check — Docker, image, volume, GitHub login, broker, isolation, clients
2834
+ maesttro doctor Health check — Docker, image, GitHub and Claude logins, old volume gone, broker, isolation, clients
1284
2835
  maesttro help Show this message
1285
2836
  `;
1286
2837
 
1287
2838
  // src/launch.ts
1288
- import { execSync as execSync3, spawn, spawnSync as spawnSync2 } from "node:child_process";
1289
- import { existsSync as existsSync4, rmSync as rmSync6 } from "node:fs";
1290
- import { constants, homedir as homedir4 } from "node:os";
1291
- import { join as join11 } from "node:path";
2839
+ import { execSync as execSync3, spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
2840
+ import { existsSync as existsSync5, rmSync as rmSync9 } from "node:fs";
2841
+ import { constants as constants2, homedir as homedir3 } from "node:os";
2842
+ import { join as join15 } from "node:path";
1292
2843
 
1293
2844
  // src/backup.ts
1294
2845
  import { createHash } from "node:crypto";
1295
- import { mkdirSync as mkdirSync7, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1296
- import { basename, join as join7 } from "node:path";
1297
-
1298
- // src/maesttro-home.ts
1299
- import {
1300
- appendFileSync as appendFileSync2,
1301
- mkdirSync as mkdirSync6,
1302
- readFileSync as readFileSync5,
1303
- renameSync as renameSync2,
1304
- writeFileSync as writeFileSync5
1305
- } from "node:fs";
1306
- import { homedir as homedir3 } from "node:os";
1307
- import { join as join6 } from "node:path";
1308
- function maesttroHome() {
1309
- return join6(homedir3(), ".config", "maesttro");
1310
- }
1311
- function configPath() {
1312
- return join6(maesttroHome(), "config.json");
1313
- }
1314
- function readMaesttroConfig() {
1315
- try {
1316
- return JSON.parse(readFileSync5(configPath(), "utf8"));
1317
- } catch {
1318
- return {};
1319
- }
1320
- }
1321
- function writeMaesttroConfig(config) {
1322
- mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
1323
- const temporary = `${configPath()}.tmp`;
1324
- writeFileSync5(temporary, `${JSON.stringify(config, null, 2)}
1325
- `);
1326
- renameSync2(temporary, configPath());
1327
- }
1328
- function fileSessionLog(path = join6(maesttroHome(), "maesttro.log")) {
1329
- return (event, detail) => {
1330
- try {
1331
- mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
1332
- appendFileSync2(path, `${new Date().toISOString()} ${event}${detail ? ` ${detail}` : ""}
1333
- `);
1334
- } catch {}
1335
- };
1336
- }
1337
-
1338
- // src/backup.ts
2846
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync4, rmSync as rmSync7 } from "node:fs";
2847
+ import { basename, join as join12 } from "node:path";
1339
2848
  var BACKUPS_KEPT = 5;
1340
2849
  function shortHash(value) {
1341
2850
  return createHash("sha256").update(value).digest("hex").slice(0, 8);
1342
2851
  }
1343
2852
  function backupDir(repoRoot, home = maesttroHome()) {
1344
- return join7(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
2853
+ return join12(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
1345
2854
  }
1346
2855
  function refsFingerprint(refs) {
1347
2856
  const sorted = Object.keys(refs).sort().map((ref) => `${ref} ${refs[ref]}`);
@@ -1359,119 +2868,41 @@ function backupRepo({
1359
2868
  }) {
1360
2869
  if (Object.keys(refs).length === 0)
1361
2870
  return;
1362
- mkdirSync7(dir, { recursive: true, mode: 448 });
2871
+ mkdirSync11(dir, { recursive: true, mode: 448 });
1363
2872
  const fingerprint = refsFingerprint(refs);
1364
- const existing = readdirSync2(dir).filter((name) => name.endsWith(".bundle")).sort();
2873
+ const existing = readdirSync4(dir).filter((name) => name.endsWith(".bundle")).sort();
1365
2874
  const latest = existing.at(-1);
1366
2875
  if (latest?.endsWith(`-${fingerprint}.bundle`)) {
1367
- return { path: join7(dir, latest), reused: true };
2876
+ return { path: join12(dir, latest), reused: true };
1368
2877
  }
1369
2878
  const stamp = now.toISOString().replace(/[:.]/g, "-");
1370
- const path = join7(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
2879
+ const path = join12(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
1371
2880
  if (!git(run, repoRoot, ["bundle", "create", "--quiet", path, "--all"]).ok) {
1372
2881
  return;
1373
2882
  }
1374
2883
  for (const stale of [...existing, basename(path)].sort().slice(0, -keep)) {
1375
- rmSync3(join7(dir, stale), { force: true });
2884
+ rmSync7(join12(dir, stale), { force: true });
1376
2885
  }
1377
2886
  return { path, reused: false };
1378
2887
  }
1379
2888
 
1380
- // src/refresher.ts
1381
- var REFRESH_LEAD_MS = 10 * 60 * 1000;
1382
- var WALL_CLOCK_TICK_MS = 60 * 1000;
1383
- var FIRST_BACKOFF_MS = 5 * 1000;
1384
- var MAX_BACKOFF_MS = 5 * 60 * 1000;
1385
- var nodeTimers = {
1386
- setTimeout: (callback, ms) => setTimeout(callback, ms).unref(),
1387
- clearTimeout: (handle) => clearTimeout(handle),
1388
- setInterval: (callback, ms) => setInterval(callback, ms).unref(),
1389
- clearInterval: (handle) => clearInterval(handle)
1390
- };
1391
- function startRefresher({
1392
- initial,
1393
- refresh,
1394
- onRefreshed,
1395
- onFailed,
1396
- now = Date.now,
1397
- timers = nodeTimers
1398
- }) {
1399
- let current = initial;
1400
- let nextAttemptAt = 0;
1401
- let backoffMs = FIRST_BACKOFF_MS;
1402
- let inFlight;
1403
- let stopped = false;
1404
- let pending;
1405
- function refreshDueAt(token) {
1406
- const expiresAt = Date.parse(token.expires_at);
1407
- const halfRemaining = (expiresAt - now()) / 2;
1408
- return expiresAt - Math.min(REFRESH_LEAD_MS, Math.max(halfRemaining, 0));
1409
- }
1410
- nextAttemptAt = refreshDueAt(initial);
1411
- function scheduleAt(at) {
1412
- timers.clearTimeout(pending);
1413
- pending = timers.setTimeout(check, Math.max(at - now(), 0));
1414
- }
1415
- async function attempt() {
1416
- try {
1417
- const token = await refresh();
1418
- if (stopped)
1419
- return;
1420
- current = token;
1421
- backoffMs = FIRST_BACKOFF_MS;
1422
- nextAttemptAt = refreshDueAt(token);
1423
- onRefreshed(token);
1424
- } catch (error) {
1425
- if (stopped)
1426
- return;
1427
- nextAttemptAt = now() + backoffMs;
1428
- onFailed(error, backoffMs);
1429
- backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
1430
- }
1431
- scheduleAt(nextAttemptAt);
1432
- }
1433
- function check() {
1434
- if (stopped)
1435
- return Promise.resolve();
1436
- if (inFlight)
1437
- return inFlight;
1438
- if (now() < nextAttemptAt)
1439
- return Promise.resolve();
1440
- inFlight = attempt().finally(() => {
1441
- inFlight = undefined;
1442
- });
1443
- return inFlight;
1444
- }
1445
- const tick = timers.setInterval(check, WALL_CLOCK_TICK_MS);
1446
- scheduleAt(nextAttemptAt);
1447
- return {
1448
- current: () => current,
1449
- check,
1450
- stop: () => {
1451
- stopped = true;
1452
- timers.clearTimeout(pending);
1453
- timers.clearInterval(tick);
1454
- }
1455
- };
1456
- }
1457
-
1458
2889
  // src/session-dir.ts
1459
2890
  import {
1460
- chmodSync as chmodSync3,
1461
- mkdirSync as mkdirSync8,
1462
- mkdtempSync as mkdtempSync2,
1463
- renameSync as renameSync3,
1464
- rmSync as rmSync4,
1465
- writeFileSync as writeFileSync6
2891
+ chmodSync as chmodSync5,
2892
+ mkdirSync as mkdirSync12,
2893
+ mkdtempSync as mkdtempSync3,
2894
+ renameSync as renameSync7,
2895
+ rmSync as rmSync8,
2896
+ writeFileSync as writeFileSync10
1466
2897
  } from "node:fs";
1467
- import { tmpdir as tmpdir2 } from "node:os";
1468
- import { join as join8 } from "node:path";
2898
+ import { tmpdir as tmpdir3 } from "node:os";
2899
+ import { join as join13 } from "node:path";
1469
2900
  var CONTAINER_GH_CONFIG_DIR = "/home/agent/.config/gh-maesttro";
1470
2901
  function writeAtomically(path, content) {
1471
2902
  const temporary = `${path}.tmp`;
1472
- writeFileSync6(temporary, content, { mode: 420 });
1473
- chmodSync3(temporary, 420);
1474
- renameSync3(temporary, path);
2903
+ writeFileSync10(temporary, content, { mode: 420 });
2904
+ chmodSync5(temporary, 420);
2905
+ renameSync7(temporary, path);
1475
2906
  }
1476
2907
  function hostsYaml(token, user) {
1477
2908
  return [
@@ -1483,20 +2914,20 @@ function hostsYaml(token, user) {
1483
2914
  ].join(`
1484
2915
  `);
1485
2916
  }
1486
- function createSessionDir(parent = tmpdir2()) {
1487
- const root = mkdtempSync2(join8(parent, "maesttro-"));
1488
- chmodSync3(root, 448);
1489
- const ghDir = join8(root, "gh");
1490
- mkdirSync8(ghDir);
1491
- chmodSync3(ghDir, 493);
1492
- writeAtomically(join8(ghDir, "config.yml"), `version: "1"
2917
+ function createSessionDir(parent = tmpdir3()) {
2918
+ const root = mkdtempSync3(join13(parent, "maesttro-"));
2919
+ chmodSync5(root, 448);
2920
+ const ghDir = join13(root, "gh");
2921
+ mkdirSync12(ghDir);
2922
+ chmodSync5(ghDir, 493);
2923
+ writeAtomically(join13(ghDir, "config.yml"), `version: "1"
1493
2924
  `);
1494
2925
  return {
1495
2926
  ghDir,
1496
- writeToken: (token, user) => writeAtomically(join8(ghDir, "hosts.yml"), hostsYaml(token, user)),
1497
- writeStatus: (status) => writeAtomically(join8(ghDir, "status"), `${status}
2927
+ writeToken: (token, user) => writeAtomically(join13(ghDir, "hosts.yml"), hostsYaml(token, user)),
2928
+ writeStatus: (status) => writeAtomically(join13(ghDir, "status"), `${status}
1498
2929
  `),
1499
- remove: () => rmSync4(root, { recursive: true, force: true })
2930
+ remove: () => rmSync8(root, { recursive: true, force: true })
1500
2931
  };
1501
2932
  }
1502
2933
 
@@ -1514,7 +2945,7 @@ function refusalNotice(code, repo) {
1514
2945
  return `${UNAVAILABLE}: your GitHub account cannot see ${repo} — ${WITHOUT}`;
1515
2946
  case "user_token_invalid":
1516
2947
  case "not_logged_in":
1517
- return `${UNAVAILABLE}: GitHub login expired, run \`maesttro login\` — ${WITHOUT}`;
2948
+ return `${UNAVAILABLE}: GitHub login expired, run \`maesttro login github\` — ${WITHOUT}`;
1518
2949
  default:
1519
2950
  return `${UNAVAILABLE} (${code}) — ${WITHOUT}`;
1520
2951
  }
@@ -1601,6 +3032,7 @@ async function openGitHubSession({
1601
3032
  log("session.token_minted", `${repo} expires ${initial.expires_at}`);
1602
3033
  const refresher = startRefresher({
1603
3034
  initial,
3035
+ expiresAtOf: (token) => Date.parse(token.expires_at),
1604
3036
  refresh: () => mint({ interactive: false }),
1605
3037
  onRefreshed: (token) => {
1606
3038
  publish(token);
@@ -1647,14 +3079,14 @@ function ask(question) {
1647
3079
  // src/session-clone.ts
1648
3080
  import {
1649
3081
  copyFileSync,
1650
- existsSync as existsSync3,
1651
- lstatSync,
1652
- mkdirSync as mkdirSync9,
1653
- readdirSync as readdirSync3,
1654
- readFileSync as readFileSync6,
1655
- writeFileSync as writeFileSync7
3082
+ existsSync as existsSync4,
3083
+ lstatSync as lstatSync4,
3084
+ mkdirSync as mkdirSync13,
3085
+ readdirSync as readdirSync5,
3086
+ readFileSync as readFileSync11,
3087
+ writeFileSync as writeFileSync11
1656
3088
  } from "node:fs";
1657
- import { join as join9 } from "node:path";
3089
+ import { join as join14 } from "node:path";
1658
3090
 
1659
3091
  // src/sanitize.ts
1660
3092
  function stripControl(text) {
@@ -1666,6 +3098,14 @@ function stripControl(text) {
1666
3098
  }
1667
3099
  return out;
1668
3100
  }
3101
+ var ESC = String.fromCharCode(27);
3102
+ var BEL = String.fromCharCode(7);
3103
+ var ANSI = new RegExp(`${ESC}(?:\\[[0-9;?]*[a-zA-Z]|\\][^${BEL}]*${BEL})?`, "g");
3104
+ function cleanOutput(text) {
3105
+ return text.split(`
3106
+ `).map((line) => stripControl(line.replace(ANSI, "")).replace(/\r+$/, "")).join(`
3107
+ `).trim();
3108
+ }
1669
3109
  var VALID_BRANCH = /^[A-Za-z0-9](?:[A-Za-z0-9._/-]*[A-Za-z0-9_-])?$/;
1670
3110
  function isSafeBranchName(name) {
1671
3111
  if (name.length === 0 || name.length > 255)
@@ -1733,27 +3173,27 @@ function createSessionClone(run, root, clonePath, baseSha) {
1733
3173
  }
1734
3174
  function realDir(path) {
1735
3175
  try {
1736
- return lstatSync(path).isDirectory();
3176
+ return lstatSync4(path).isDirectory();
1737
3177
  } catch {
1738
3178
  return false;
1739
3179
  }
1740
3180
  }
1741
3181
  function dirents(dir) {
1742
3182
  try {
1743
- return readdirSync3(dir, { withFileTypes: true });
3183
+ return readdirSync5(dir, { withFileTypes: true });
1744
3184
  } catch {
1745
3185
  return [];
1746
3186
  }
1747
3187
  }
1748
3188
  function cloneGitDir(clonePath) {
1749
- const gitDir = join9(clonePath, ".git");
3189
+ const gitDir = join14(clonePath, ".git");
1750
3190
  return realDir(gitDir) ? gitDir : undefined;
1751
3191
  }
1752
3192
  function readSha(path) {
1753
3193
  try {
1754
- if (!lstatSync(path).isFile())
3194
+ if (!lstatSync4(path).isFile())
1755
3195
  return;
1756
- const text = readFileSync6(path, "utf8").trim();
3196
+ const text = readFileSync11(path, "utf8").trim();
1757
3197
  return SHA.test(text) ? text : undefined;
1758
3198
  } catch {
1759
3199
  return;
@@ -1763,7 +3203,7 @@ function packedRefs(gitDir) {
1763
3203
  const refs = new Map;
1764
3204
  let text;
1765
3205
  try {
1766
- text = readFileSync6(join9(gitDir, "packed-refs"), "utf8");
3206
+ text = readFileSync11(join14(gitDir, "packed-refs"), "utf8");
1767
3207
  } catch {
1768
3208
  return refs;
1769
3209
  }
@@ -1788,12 +3228,12 @@ function cloneBranches(clonePath) {
1788
3228
  continue;
1789
3229
  const name = prefix ? `${prefix}/${entry.name}` : entry.name;
1790
3230
  if (entry.isDirectory())
1791
- walk(join9(dir, entry.name), name);
3231
+ walk(join14(dir, entry.name), name);
1792
3232
  else if (entry.isFile())
1793
3233
  names.add(name);
1794
3234
  }
1795
3235
  };
1796
- walk(join9(gitDir, "refs", "heads"), "");
3236
+ walk(join14(gitDir, "refs", "heads"), "");
1797
3237
  for (const ref of packedRefs(gitDir).keys()) {
1798
3238
  if (ref.startsWith(HEADS_PREFIX))
1799
3239
  names.add(ref.slice(HEADS_PREFIX.length));
@@ -1806,7 +3246,7 @@ function branchTip(clonePath, branch) {
1806
3246
  const gitDir = cloneGitDir(clonePath);
1807
3247
  if (!gitDir)
1808
3248
  return;
1809
- return readSha(join9(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
3249
+ return readSha(join14(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
1810
3250
  }
1811
3251
  function cloneHead(clonePath) {
1812
3252
  const gitDir = cloneGitDir(clonePath);
@@ -1814,9 +3254,9 @@ function cloneHead(clonePath) {
1814
3254
  return;
1815
3255
  let text;
1816
3256
  try {
1817
- if (!lstatSync(join9(gitDir, "HEAD")).isFile())
3257
+ if (!lstatSync4(join14(gitDir, "HEAD")).isFile())
1818
3258
  return;
1819
- text = readFileSync6(join9(gitDir, "HEAD"), "utf8").trim();
3259
+ text = readFileSync11(join14(gitDir, "HEAD"), "utf8").trim();
1820
3260
  } catch {
1821
3261
  return;
1822
3262
  }
@@ -1837,29 +3277,29 @@ function copyObjectFiles(fromDir, intoDir) {
1837
3277
  for (const entry of dirents(fromDir)) {
1838
3278
  if (!entry.isFile())
1839
3279
  continue;
1840
- const dest = join9(intoDir, entry.name);
1841
- if (existsSync3(dest))
3280
+ const dest = join14(intoDir, entry.name);
3281
+ if (existsSync4(dest))
1842
3282
  continue;
1843
3283
  if (!created) {
1844
- mkdirSync9(intoDir, { recursive: true });
3284
+ mkdirSync13(intoDir, { recursive: true });
1845
3285
  created = true;
1846
3286
  }
1847
- copyFileSync(join9(fromDir, entry.name), dest);
3287
+ copyFileSync(join14(fromDir, entry.name), dest);
1848
3288
  }
1849
3289
  }
1850
3290
  function pinAlternates(clonePath, containerObjects) {
1851
3291
  const gitDir = cloneGitDir(clonePath);
1852
3292
  if (!gitDir)
1853
3293
  return;
1854
- const objects = join9(gitDir, "objects");
1855
- const info = join9(objects, "info");
3294
+ const objects = join14(gitDir, "objects");
3295
+ const info = join14(objects, "info");
1856
3296
  if (!realDir(objects) || !realDir(info)) {
1857
3297
  throw new LaunchError(`Unsafe objects tree in the session clone: ${clonePath}`);
1858
3298
  }
1859
- const alternates = join9(info, "alternates");
3299
+ const alternates = join14(info, "alternates");
1860
3300
  let stat;
1861
3301
  try {
1862
- stat = lstatSync(alternates);
3302
+ stat = lstatSync4(alternates);
1863
3303
  } catch {
1864
3304
  return;
1865
3305
  }
@@ -1868,73 +3308,29 @@ function pinAlternates(clonePath, containerObjects) {
1868
3308
  }
1869
3309
  const desired = `${containerObjects}
1870
3310
  `;
1871
- if (readFileSync6(alternates, "utf8") !== desired) {
1872
- writeFileSync7(alternates, desired);
3311
+ if (readFileSync11(alternates, "utf8") !== desired) {
3312
+ writeFileSync11(alternates, desired);
1873
3313
  }
1874
3314
  }
1875
3315
  function importObjects(run, root, clonePath, tip) {
1876
3316
  const gitDir = cloneGitDir(clonePath);
1877
3317
  if (!gitDir)
1878
3318
  return false;
1879
- const objects = join9(gitDir, "objects");
3319
+ const objects = join14(gitDir, "objects");
1880
3320
  if (!realDir(objects))
1881
3321
  return false;
1882
- const into = join9(root, ".git", "objects");
3322
+ const into = join14(root, ".git", "objects");
1883
3323
  for (const entry of dirents(objects)) {
1884
3324
  if (entry.name === "info" || entry.isSymbolicLink() || !entry.isDirectory()) {
1885
3325
  continue;
1886
3326
  }
1887
3327
  if (!OBJECT_SUBDIR.test(entry.name) && entry.name !== "pack")
1888
3328
  continue;
1889
- copyObjectFiles(join9(objects, entry.name), join9(into, entry.name));
3329
+ copyObjectFiles(join14(objects, entry.name), join14(into, entry.name));
1890
3330
  }
1891
3331
  return git(run, root, ["cat-file", "-e", `${tip}^{commit}`]).ok;
1892
3332
  }
1893
3333
 
1894
- // src/sessions.ts
1895
- import { randomBytes } from "node:crypto";
1896
- import {
1897
- mkdirSync as mkdirSync10,
1898
- readdirSync as readdirSync4,
1899
- readFileSync as readFileSync7,
1900
- renameSync as renameSync4,
1901
- rmSync as rmSync5,
1902
- writeFileSync as writeFileSync8
1903
- } from "node:fs";
1904
- import { join as join10 } from "node:path";
1905
- function newSessionId() {
1906
- return `maesttro-${randomBytes(4).toString("hex")}`;
1907
- }
1908
- function sessionsDir(home = maesttroHome()) {
1909
- return join10(home, "sessions");
1910
- }
1911
- function saveSession(record, dir = sessionsDir()) {
1912
- mkdirSync10(dir, { recursive: true, mode: 448 });
1913
- const path = join10(dir, `${record.id}.json`);
1914
- writeFileSync8(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
1915
- `);
1916
- renameSync4(`${path}.tmp`, path);
1917
- }
1918
- function readSession(id, dir = sessionsDir()) {
1919
- try {
1920
- return JSON.parse(readFileSync7(join10(dir, `${id}.json`), "utf8"));
1921
- } catch {
1922
- return;
1923
- }
1924
- }
1925
- function deleteSession(id, dir = sessionsDir()) {
1926
- rmSync5(join10(dir, `${id}.json`), { force: true });
1927
- }
1928
- function sessionsForRepo(repoRoot, dir = sessionsDir()) {
1929
- let names;
1930
- try {
1931
- names = readdirSync4(dir).filter((name) => name.endsWith(".json"));
1932
- } catch {
1933
- return [];
1934
- }
1935
- return names.map((name) => readSession(name.slice(0, -".json".length), dir)).filter((record) => record?.repoRoot === repoRoot).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1936
- }
1937
-
1938
3334
  // src/workspace-git.ts
1939
3335
  var GITHUB_REMOTE = /^(?:https:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/;
1940
3336
  function githubRepoFromRemote(remote) {
@@ -1962,9 +3358,18 @@ function workspaceAuthor(run, workspace) {
1962
3358
  }
1963
3359
 
1964
3360
  // src/launch.ts
1965
- var CONTAINER_REPO = "/workspace";
3361
+ var CONTAINER_REPO = CONTAINER_REPO_PATH;
1966
3362
  var CONTAINER_PACKAGE = "/opt/maesttro";
1967
3363
  var CONTAINER_GIT_OBJECTS = "/opt/maesttro-objects";
3364
+ function sessionClientArgs({
3365
+ args,
3366
+ session
3367
+ }) {
3368
+ const chosen = args.length > 0 ? args : session.client ? [session.client] : [];
3369
+ const [first] = chosen;
3370
+ const client = first === undefined ? DEFAULT_CLIENT : isClientKey(first) ? first : undefined;
3371
+ return { args: chosen, client };
3372
+ }
1968
3373
  function resolveCommand(args) {
1969
3374
  const [first, ...rest] = args;
1970
3375
  if (first === undefined)
@@ -1983,16 +3388,19 @@ function sessionMounts({
1983
3388
  hostHome,
1984
3389
  stagedPaths,
1985
3390
  clientConfigs,
1986
- sharedSkills
3391
+ sharedSkills,
3392
+ home,
3393
+ claudeDir
1987
3394
  }) {
1988
- const objects = join11(repoRoot, ".git", "objects");
3395
+ const objects = join15(repoRoot, ".git", "objects");
1989
3396
  const mounts = [
1990
- { host: VOLUME, container: "/home/agent", type: "volume" },
3397
+ { host: home, container: "/home/agent" },
3398
+ { host: claudeDir, container: "/home/agent/.claude" },
1991
3399
  { host: objects, container: CONTAINER_GIT_OBJECTS, readOnly: true },
1992
3400
  { host: sessionDir, container: CONTAINER_REPO },
1993
3401
  { host: packageRoot, container: CONTAINER_PACKAGE, readOnly: true },
1994
3402
  ...stagedPaths.map((path) => ({
1995
- host: join11(hostHome, path),
3403
+ host: join15(hostHome, path),
1996
3404
  container: `${HOST_STAGING_DIR}/${path}`,
1997
3405
  readOnly: true
1998
3406
  })),
@@ -2018,9 +3426,9 @@ function dockerRunArgs({
2018
3426
  extra = [],
2019
3427
  name
2020
3428
  }) {
2021
- const volumes = mounts.flatMap(({ host, container, readOnly, type = "bind" }) => [
3429
+ const volumes = mounts.flatMap(({ host, container, readOnly }) => [
2022
3430
  "--mount",
2023
- `type=${type},source=${host},target=${container}${readOnly ? ",readonly" : ""}`
3431
+ `type=bind,source=${host},target=${container}${readOnly ? ",readonly" : ""}`
2024
3432
  ]);
2025
3433
  return [
2026
3434
  "run",
@@ -2049,17 +3457,54 @@ Default: ${DEFAULT_CLIENT}. Run \`maesttro <client>\` to pick one, or \`maesttro
2049
3457
  function ensureReady() {
2050
3458
  try {
2051
3459
  execSync3(`docker image inspect ${IMAGE}`, { stdio: "ignore" });
2052
- execSync3(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
2053
3460
  } catch {
2054
3461
  throw new LaunchError("Runner isn't set up yet. Run `maesttro setup` first.");
2055
3462
  }
2056
3463
  }
2057
- function clearStaleContainer(id) {
2058
- spawnSync2("docker", ["rm", "-f", id], { stdio: "ignore" });
2059
- }
3464
+ function clearStaleContainer(id) {
3465
+ spawnSync3("docker", ["rm", "-f", id], { stdio: "ignore" });
3466
+ }
3467
+ async function establishClaudeCredentials({
3468
+ claudeDir,
3469
+ store,
3470
+ refresh = refreshMaster,
3471
+ onRotated = (credentials) => pushToRunningSessions({ credentials, claudeDir, announce }),
3472
+ signals = processSignals,
3473
+ announce,
3474
+ now = Date.now
3475
+ }) {
3476
+ const credentials = store.load();
3477
+ if (credentials && claudeLoginState({ credentials, now: now() }) === "active" && isDue({ credentials, now: now() })) {
3478
+ announce("Refreshing Claude login…");
3479
+ }
3480
+ try {
3481
+ writeSessionCredentials({
3482
+ claudeDir,
3483
+ credentials: await refresh({ store, announce, onRotated, signals })
3484
+ });
3485
+ return {};
3486
+ } catch (error) {
3487
+ if (error instanceof ClaudeLoggedOut) {
3488
+ announce(error.message);
3489
+ removeHostWrittenCredentials({ claudeDir });
3490
+ return { failure: error.message };
3491
+ }
3492
+ if (error instanceof ClaudeLoginExpired || error instanceof ClaudeRefreshFailed) {
3493
+ announce(error.message);
3494
+ const master = store.load();
3495
+ if (master && master.claudeAiOauth.expiresAt > now()) {
3496
+ writeSessionCredentials({ claudeDir, credentials: master });
3497
+ } else if (error instanceof ClaudeLoginExpired) {
3498
+ removeHostWrittenCredentials({ claudeDir });
3499
+ }
3500
+ return { failure: error.message };
3501
+ }
3502
+ throw error;
3503
+ }
3504
+ }
2060
3505
  function sharedSkillsDir() {
2061
- const dir = process.env.AGENT_SHARED_SKILLS ?? join11(homedir4(), "agent-shared", "skills");
2062
- return existsSync4(dir) ? dir : undefined;
3506
+ const dir = process.env.AGENT_SHARED_SKILLS ?? join15(homedir3(), "agent-shared", "skills");
3507
+ return existsSync5(dir) ? dir : undefined;
2063
3508
  }
2064
3509
  async function resolveAuthor(workspace) {
2065
3510
  const fromRepo = workspaceAuthor(runCommand, workspace);
@@ -2088,7 +3533,7 @@ function startSession(run, root) {
2088
3533
  if (!baseSha)
2089
3534
  throw new LaunchError(`Can't resolve ${base}.`);
2090
3535
  const id = newSessionId();
2091
- const sessionDir = join11(root, WORKTREES_DIR, id);
3536
+ const sessionDir = join15(root, WORKTREES_DIR, id);
2092
3537
  createSessionClone(run, root, sessionDir, baseSha);
2093
3538
  return {
2094
3539
  id,
@@ -2105,7 +3550,7 @@ function resumeSession(root, id) {
2105
3550
  if (!session || session.repoRoot !== root) {
2106
3551
  throw new LaunchError(`No session ${id} in this repo. See \`maesttro sessions\`.`);
2107
3552
  }
2108
- if (!existsSync4(session.worktree)) {
3553
+ if (!existsSync5(session.worktree)) {
2109
3554
  throw new LaunchError(`The clone for ${id} is gone: ${session.worktree}`);
2110
3555
  }
2111
3556
  return session;
@@ -2128,13 +3573,13 @@ function preserveBranches(run, session) {
2128
3573
  return preserved;
2129
3574
  }
2130
3575
  function finishSession(run, session) {
2131
- if (!existsSync4(session.worktree)) {
3576
+ if (!existsSync5(session.worktree)) {
2132
3577
  deleteSession(session.id);
2133
3578
  return;
2134
3579
  }
2135
3580
  const preserved = preserveBranches(run, session);
2136
3581
  if (!cloneHasWork(session.worktree, session.baseSha)) {
2137
- rmSync6(session.worktree, { recursive: true, force: true });
3582
+ rmSync9(session.worktree, { recursive: true, force: true });
2138
3583
  deleteSession(session.id);
2139
3584
  return;
2140
3585
  }
@@ -2163,9 +3608,45 @@ function runnerGitHubMode({
2163
3608
  function exitCodeOf(code, signal) {
2164
3609
  if (code !== null)
2165
3610
  return code;
2166
- return signal ? 128 + (constants.signals[signal] ?? 0) : 1;
3611
+ return signal ? 128 + (constants2.signals[signal] ?? 0) : 1;
2167
3612
  }
2168
3613
  var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
3614
+ var ABANDON_GRACE_MS = 3000;
3615
+ function closeSession({
3616
+ run,
3617
+ session
3618
+ }) {
3619
+ try {
3620
+ finishSession(run, session);
3621
+ } catch (error) {
3622
+ console.log(`
3623
+ Couldn't clean up session ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
3624
+ }
3625
+ }
3626
+ function claudeNoticeSink({
3627
+ log,
3628
+ print = (message) => console.log(message)
3629
+ }) {
3630
+ let quiet = false;
3631
+ let unseen;
3632
+ return {
3633
+ announce: (message) => {
3634
+ if (!quiet)
3635
+ return print(message);
3636
+ log("claude.notice", message);
3637
+ unseen = message;
3638
+ },
3639
+ quiet: () => {
3640
+ quiet = true;
3641
+ },
3642
+ replay: () => {
3643
+ if (unseen)
3644
+ print(`
3645
+ ${unseen}`);
3646
+ unseen = undefined;
3647
+ }
3648
+ };
3649
+ }
2169
3650
  async function launch({
2170
3651
  args,
2171
3652
  resume,
@@ -2175,9 +3656,10 @@ async function launch({
2175
3656
  ensureReady();
2176
3657
  requireGit(run);
2177
3658
  const root = repoRoot(run, process.cwd());
2178
- purgeLegacyTokenOnce({
3659
+ migrateFromVolumeOnce({
2179
3660
  config: readMaesttroConfig(),
2180
3661
  saveConfig: writeMaesttroConfig,
3662
+ store: claudeStore(),
2181
3663
  announce: (message) => console.log(message)
2182
3664
  });
2183
3665
  const started = resume ? resumeSession(root, resume) : startSession(run, root);
@@ -2188,13 +3670,55 @@ async function launch({
2188
3670
  sessionId: started.id
2189
3671
  });
2190
3672
  const strict = isStrict(requestedStrict, started);
3673
+ const client = sessionClientArgs({ args, session: started });
2191
3674
  const saved = {
2192
3675
  ...started,
2193
3676
  backup: started.backup ?? backup?.path,
2194
- strict
3677
+ strict,
3678
+ client: client.client ?? started.client
2195
3679
  };
2196
3680
  saveSession(saved);
3681
+ let github;
3682
+ let claudeRefresher;
3683
+ let clientConfigDir;
3684
+ let abandoning = false;
3685
+ const interrupts = signalRouter({
3686
+ onUnrouted: async (signal) => {
3687
+ const code = exitCodeOf(null, signal);
3688
+ if (abandoning)
3689
+ process.exit(code);
3690
+ abandoning = true;
3691
+ console.log(`
3692
+ Interrupted; cleaning up session ${saved.id}…`);
3693
+ claudeRefresher?.stop();
3694
+ clientConfigDir?.remove();
3695
+ if (github) {
3696
+ await Promise.race([
3697
+ github.close(),
3698
+ new Promise((resolve) => setTimeout(resolve, ABANDON_GRACE_MS))
3699
+ ]);
3700
+ }
3701
+ closeSession({ run, session: saved });
3702
+ process.exit(code);
3703
+ }
3704
+ });
2197
3705
  console.log(`Session ${saved.id}${strict ? " (strict)" : ""}: clone .worktrees/${saved.id} at ${saved.base}${backup ? `, backup ${backup.reused ? "reused" : "written"}` : ""}.`);
3706
+ const { home, claudeDir } = ensureSessionHome({ id: saved.id });
3707
+ const claude = claudeStore();
3708
+ const sessionLog = fileSessionLog();
3709
+ const claudeNotices = claudeNoticeSink({ log: sessionLog });
3710
+ const { failure } = await establishClaudeCredentials({
3711
+ claudeDir,
3712
+ store: claude,
3713
+ announce: claudeNotices.announce,
3714
+ signals: interrupts
3715
+ });
3716
+ claudeRefresher = startClaudeRefresher({
3717
+ claudeDir,
3718
+ store: claude,
3719
+ announce: claudeNotices.announce,
3720
+ launchFailure: failure
3721
+ });
2198
3722
  const repo = workspaceRepo(run, root);
2199
3723
  if (repo && !resume) {
2200
3724
  git(run, saved.worktree, [
@@ -2204,7 +3728,7 @@ async function launch({
2204
3728
  `https://github.com/${repo}.git`
2205
3729
  ]);
2206
3730
  }
2207
- const github = await openGitHubSession({
3731
+ github = await openGitHubSession({
2208
3732
  repo,
2209
3733
  author: await resolveAuthor(root),
2210
3734
  strict,
@@ -2214,11 +3738,11 @@ async function launch({
2214
3738
  store: selectCredentialStore(),
2215
3739
  announce: (message) => console.log(message)
2216
3740
  },
2217
- log: fileSessionLog()
3741
+ log: sessionLog
2218
3742
  });
2219
3743
  if (github.notice)
2220
3744
  console.log(github.notice);
2221
- const hostHome = homedir4();
3745
+ const hostHome = homedir3();
2222
3746
  const url = maesttroUrl();
2223
3747
  const orchestratorWindow = await fetchOrchestratorWindow({ url });
2224
3748
  if (orchestratorWindow === undefined) {
@@ -2232,15 +3756,17 @@ async function launch({
2232
3756
  if (clientConfigs.removed.length > 0) {
2233
3757
  console.log(`Kept out of the runner as secrets: ${clientConfigs.removed.join(", ")}`);
2234
3758
  }
2235
- const clientConfigDir = writeClientConfigDir(clientConfigs.files);
3759
+ clientConfigDir = writeClientConfigDir(clientConfigs.files);
2236
3760
  const { mounts, workdir } = sessionMounts({
2237
3761
  repoRoot: root,
2238
3762
  sessionDir: saved.worktree,
2239
3763
  packageRoot: PACKAGE_ROOT,
2240
3764
  hostHome,
2241
- stagedPaths: STAGED_PATHS.filter((path) => existsSync4(join11(hostHome, path))),
3765
+ stagedPaths: STAGED_PATHS.filter((path) => existsSync5(join15(hostHome, path))),
2242
3766
  clientConfigs: clientConfigDir.dir,
2243
- sharedSkills: sharedSkillsDir()
3767
+ sharedSkills: sharedSkillsDir(),
3768
+ home,
3769
+ claudeDir
2244
3770
  });
2245
3771
  const sessionEnv = {
2246
3772
  ...github.env,
@@ -2259,13 +3785,15 @@ async function launch({
2259
3785
  ].concat(repo ? ["MAESTTRO_REPO"] : []).flatMap((name) => ["-e", name]);
2260
3786
  clearStaleContainer(saved.id);
2261
3787
  pinAlternates(saved.worktree, CONTAINER_GIT_OBJECTS);
2262
- const child = spawn("docker", dockerRunArgs({
2263
- command: containerCommand(resolveCommand(args)),
3788
+ const child = spawn2("docker", dockerRunArgs({
3789
+ command: containerCommand(resolveCommand(client.args)),
2264
3790
  mounts,
2265
3791
  workdir,
2266
3792
  name: saved.id,
2267
3793
  extra: [...github.dockerArgs, ...envFlags]
2268
3794
  }), { stdio: "inherit", env: { ...process.env, ...sessionEnv } });
3795
+ interrupts.dispose();
3796
+ claudeNotices.quiet();
2269
3797
  const forward = (signal) => child.kill(signal);
2270
3798
  for (const signal of FORWARDED_SIGNALS)
2271
3799
  process.on(signal, forward);
@@ -2276,13 +3804,1035 @@ async function launch({
2276
3804
  for (const signal of FORWARDED_SIGNALS)
2277
3805
  process.off(signal, forward);
2278
3806
  await github.close();
3807
+ if (claudeRefresher?.busy())
3808
+ console.log(`
3809
+ ${FINISHING_BACKGROUND_REFRESH}`);
3810
+ await claudeRefresher?.stop();
3811
+ claudeNotices.replay();
2279
3812
  clientConfigDir.remove();
2280
- finishSession(run, saved);
3813
+ closeSession({ run, session: saved });
2281
3814
  process.exit(exitCode);
2282
3815
  }
2283
3816
 
2284
3817
  // src/push.ts
2285
- import { spawnSync as spawnSync3 } from "node:child_process";
3818
+ import { spawnSync as spawnSync4 } from "node:child_process";
3819
+
3820
+ // ../node_modules/.bun/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
3821
+ import { stdout, stdin } from "node:process";
3822
+ import * as l from "node:readline";
3823
+ import l__default from "node:readline";
3824
+
3825
+ // ../node_modules/.bun/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
3826
+ var getCodePointsLength = (() => {
3827
+ const SURROGATE_PAIR_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
3828
+ return (input) => {
3829
+ let surrogatePairsNr = 0;
3830
+ SURROGATE_PAIR_RE.lastIndex = 0;
3831
+ while (SURROGATE_PAIR_RE.test(input)) {
3832
+ surrogatePairsNr += 1;
3833
+ }
3834
+ return input.length - surrogatePairsNr;
3835
+ };
3836
+ })();
3837
+ var isFullWidth = (x) => {
3838
+ return x === 12288 || x >= 65281 && x <= 65376 || x >= 65504 && x <= 65510;
3839
+ };
3840
+ var isWideNotCJKTNotEmoji = (x) => {
3841
+ return x === 8987 || x === 9001 || x >= 12272 && x <= 12287 || x >= 12289 && x <= 12350 || x >= 12441 && x <= 12543 || x >= 12549 && x <= 12591 || x >= 12593 && x <= 12686 || x >= 12688 && x <= 12771 || x >= 12783 && x <= 12830 || x >= 12832 && x <= 12871 || x >= 12880 && x <= 19903 || x >= 65040 && x <= 65049 || x >= 65072 && x <= 65106 || x >= 65108 && x <= 65126 || x >= 65128 && x <= 65131 || x >= 127488 && x <= 127490 || x >= 127504 && x <= 127547 || x >= 127552 && x <= 127560 || x >= 131072 && x <= 196605 || x >= 196608 && x <= 262141;
3842
+ };
3843
+
3844
+ // ../node_modules/.bun/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/index.js
3845
+ var ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y;
3846
+ var CONTROL_RE = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y;
3847
+ var CJKT_WIDE_RE = /(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu;
3848
+ var TAB_RE = /\t{1,1000}/y;
3849
+ var EMOJI_RE = /[\u{1F1E6}-\u{1F1FF}]{2}|\u{1F3F4}[\u{E0061}-\u{E007A}]{2}[\u{E0030}-\u{E0039}\u{E0061}-\u{E007A}]{1,3}\u{E007F}|(?:\p{Emoji}\uFE0F\u20E3?|\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation})(?:\u200D(?:\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F\u20E3?))*/yu;
3850
+ var LATIN_RE = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y;
3851
+ var MODIFIER_RE = /\p{M}+/gu;
3852
+ var NO_TRUNCATION = { limit: Infinity, ellipsis: "" };
3853
+ var getStringTruncatedWidth = (input, truncationOptions = {}, widthOptions = {}) => {
3854
+ const LIMIT = truncationOptions.limit ?? Infinity;
3855
+ const ELLIPSIS = truncationOptions.ellipsis ?? "";
3856
+ const ELLIPSIS_WIDTH = truncationOptions?.ellipsisWidth ?? (ELLIPSIS ? getStringTruncatedWidth(ELLIPSIS, NO_TRUNCATION, widthOptions).width : 0);
3857
+ const ANSI_WIDTH = 0;
3858
+ const CONTROL_WIDTH = widthOptions.controlWidth ?? 0;
3859
+ const TAB_WIDTH = widthOptions.tabWidth ?? 8;
3860
+ const EMOJI_WIDTH = widthOptions.emojiWidth ?? 2;
3861
+ const FULL_WIDTH_WIDTH = 2;
3862
+ const REGULAR_WIDTH = widthOptions.regularWidth ?? 1;
3863
+ const WIDE_WIDTH = widthOptions.wideWidth ?? FULL_WIDTH_WIDTH;
3864
+ const PARSE_BLOCKS = [
3865
+ [LATIN_RE, REGULAR_WIDTH],
3866
+ [ANSI_RE, ANSI_WIDTH],
3867
+ [CONTROL_RE, CONTROL_WIDTH],
3868
+ [TAB_RE, TAB_WIDTH],
3869
+ [EMOJI_RE, EMOJI_WIDTH],
3870
+ [CJKT_WIDE_RE, WIDE_WIDTH]
3871
+ ];
3872
+ let indexPrev = 0;
3873
+ let index = 0;
3874
+ let length = input.length;
3875
+ let lengthExtra = 0;
3876
+ let truncationEnabled = false;
3877
+ let truncationIndex = length;
3878
+ let truncationLimit = Math.max(0, LIMIT - ELLIPSIS_WIDTH);
3879
+ let unmatchedStart = 0;
3880
+ let unmatchedEnd = 0;
3881
+ let width = 0;
3882
+ let widthExtra = 0;
3883
+ outer:
3884
+ while (true) {
3885
+ if (unmatchedEnd > unmatchedStart || index >= length && index > indexPrev) {
3886
+ const unmatched = input.slice(unmatchedStart, unmatchedEnd) || input.slice(indexPrev, index);
3887
+ lengthExtra = 0;
3888
+ for (const char of unmatched.replaceAll(MODIFIER_RE, "")) {
3889
+ const codePoint = char.codePointAt(0) || 0;
3890
+ if (isFullWidth(codePoint)) {
3891
+ widthExtra = FULL_WIDTH_WIDTH;
3892
+ } else if (isWideNotCJKTNotEmoji(codePoint)) {
3893
+ widthExtra = WIDE_WIDTH;
3894
+ } else {
3895
+ widthExtra = REGULAR_WIDTH;
3896
+ }
3897
+ if (width + widthExtra > truncationLimit) {
3898
+ truncationIndex = Math.min(truncationIndex, Math.max(unmatchedStart, indexPrev) + lengthExtra);
3899
+ }
3900
+ if (width + widthExtra > LIMIT) {
3901
+ truncationEnabled = true;
3902
+ break outer;
3903
+ }
3904
+ lengthExtra += char.length;
3905
+ width += widthExtra;
3906
+ }
3907
+ unmatchedStart = unmatchedEnd = 0;
3908
+ }
3909
+ if (index >= length) {
3910
+ break outer;
3911
+ }
3912
+ for (let i = 0, l = PARSE_BLOCKS.length;i < l; i++) {
3913
+ const [BLOCK_RE, BLOCK_WIDTH] = PARSE_BLOCKS[i];
3914
+ BLOCK_RE.lastIndex = index;
3915
+ if (BLOCK_RE.test(input)) {
3916
+ lengthExtra = BLOCK_RE === CJKT_WIDE_RE ? getCodePointsLength(input.slice(index, BLOCK_RE.lastIndex)) : BLOCK_RE === EMOJI_RE ? 1 : BLOCK_RE.lastIndex - index;
3917
+ widthExtra = lengthExtra * BLOCK_WIDTH;
3918
+ if (width + widthExtra > truncationLimit) {
3919
+ truncationIndex = Math.min(truncationIndex, index + Math.floor((truncationLimit - width) / BLOCK_WIDTH));
3920
+ }
3921
+ if (width + widthExtra > LIMIT) {
3922
+ truncationEnabled = true;
3923
+ break outer;
3924
+ }
3925
+ width += widthExtra;
3926
+ unmatchedStart = indexPrev;
3927
+ unmatchedEnd = index;
3928
+ index = indexPrev = BLOCK_RE.lastIndex;
3929
+ continue outer;
3930
+ }
3931
+ }
3932
+ index += 1;
3933
+ }
3934
+ return {
3935
+ width: truncationEnabled ? truncationLimit : width,
3936
+ index: truncationEnabled ? truncationIndex : length,
3937
+ truncated: truncationEnabled,
3938
+ ellipsed: truncationEnabled && LIMIT >= ELLIPSIS_WIDTH
3939
+ };
3940
+ };
3941
+ var dist_default = getStringTruncatedWidth;
3942
+
3943
+ // ../node_modules/.bun/fast-string-width@3.0.2/node_modules/fast-string-width/dist/index.js
3944
+ var NO_TRUNCATION2 = {
3945
+ limit: Infinity,
3946
+ ellipsis: "",
3947
+ ellipsisWidth: 0
3948
+ };
3949
+ var fastStringWidth = (input, options = {}) => {
3950
+ return dist_default(input, NO_TRUNCATION2, options).width;
3951
+ };
3952
+ var dist_default2 = fastStringWidth;
3953
+
3954
+ // ../node_modules/.bun/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js
3955
+ var ESC2 = "\x1B";
3956
+ var CSI = "›";
3957
+ var END_CODE = 39;
3958
+ var ANSI_ESCAPE_BELL = "\x07";
3959
+ var ANSI_CSI = "[";
3960
+ var ANSI_OSC = "]";
3961
+ var ANSI_SGR_TERMINATOR = "m";
3962
+ var ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;
3963
+ var GROUP_REGEX = new RegExp(`(?:\\${ANSI_CSI}(?<code>\\d+)m|\\${ANSI_ESCAPE_LINK}(?<uri>.*)${ANSI_ESCAPE_BELL})`, "y");
3964
+ var getClosingCode = (openingCode) => {
3965
+ if (openingCode >= 30 && openingCode <= 37)
3966
+ return 39;
3967
+ if (openingCode >= 90 && openingCode <= 97)
3968
+ return 39;
3969
+ if (openingCode >= 40 && openingCode <= 47)
3970
+ return 49;
3971
+ if (openingCode >= 100 && openingCode <= 107)
3972
+ return 49;
3973
+ if (openingCode === 1 || openingCode === 2)
3974
+ return 22;
3975
+ if (openingCode === 3)
3976
+ return 23;
3977
+ if (openingCode === 4)
3978
+ return 24;
3979
+ if (openingCode === 7)
3980
+ return 27;
3981
+ if (openingCode === 8)
3982
+ return 28;
3983
+ if (openingCode === 9)
3984
+ return 29;
3985
+ if (openingCode === 0)
3986
+ return 0;
3987
+ return;
3988
+ };
3989
+ var wrapAnsiCode = (code) => `${ESC2}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
3990
+ var wrapAnsiHyperlink = (url) => `${ESC2}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`;
3991
+ var wrapWord = (rows, word, columns) => {
3992
+ const characters = word[Symbol.iterator]();
3993
+ let isInsideEscape = false;
3994
+ let isInsideLinkEscape = false;
3995
+ let lastRow = rows.at(-1);
3996
+ let visible = lastRow === undefined ? 0 : dist_default2(lastRow);
3997
+ let currentCharacter = characters.next();
3998
+ let nextCharacter = characters.next();
3999
+ let rawCharacterIndex = 0;
4000
+ while (!currentCharacter.done) {
4001
+ const character = currentCharacter.value;
4002
+ const characterLength = dist_default2(character);
4003
+ if (visible + characterLength <= columns) {
4004
+ rows[rows.length - 1] += character;
4005
+ } else {
4006
+ rows.push(character);
4007
+ visible = 0;
4008
+ }
4009
+ if (character === ESC2 || character === CSI) {
4010
+ isInsideEscape = true;
4011
+ isInsideLinkEscape = word.startsWith(ANSI_ESCAPE_LINK, rawCharacterIndex + 1);
4012
+ }
4013
+ if (isInsideEscape) {
4014
+ if (isInsideLinkEscape) {
4015
+ if (character === ANSI_ESCAPE_BELL) {
4016
+ isInsideEscape = false;
4017
+ isInsideLinkEscape = false;
4018
+ }
4019
+ } else if (character === ANSI_SGR_TERMINATOR) {
4020
+ isInsideEscape = false;
4021
+ }
4022
+ } else {
4023
+ visible += characterLength;
4024
+ if (visible === columns && !nextCharacter.done) {
4025
+ rows.push("");
4026
+ visible = 0;
4027
+ }
4028
+ }
4029
+ currentCharacter = nextCharacter;
4030
+ nextCharacter = characters.next();
4031
+ rawCharacterIndex += character.length;
4032
+ }
4033
+ lastRow = rows.at(-1);
4034
+ if (!visible && lastRow !== undefined && lastRow.length && rows.length > 1) {
4035
+ rows[rows.length - 2] += rows.pop();
4036
+ }
4037
+ };
4038
+ var stringVisibleTrimSpacesRight = (string) => {
4039
+ const words = string.split(" ");
4040
+ let last = words.length;
4041
+ while (last) {
4042
+ if (dist_default2(words[last - 1])) {
4043
+ break;
4044
+ }
4045
+ last--;
4046
+ }
4047
+ if (last === words.length) {
4048
+ return string;
4049
+ }
4050
+ return words.slice(0, last).join(" ") + words.slice(last).join("");
4051
+ };
4052
+ var exec = (string, columns, options = {}) => {
4053
+ if (options.trim !== false && string.trim() === "") {
4054
+ return "";
4055
+ }
4056
+ let returnValue = "";
4057
+ let escapeCode;
4058
+ let escapeUrl;
4059
+ const words = string.split(" ");
4060
+ let rows = [""];
4061
+ let rowLength = 0;
4062
+ for (let index = 0;index < words.length; index++) {
4063
+ const word = words[index];
4064
+ if (options.trim !== false) {
4065
+ const row = rows.at(-1) ?? "";
4066
+ const trimmed = row.trimStart();
4067
+ if (row.length !== trimmed.length) {
4068
+ rows[rows.length - 1] = trimmed;
4069
+ rowLength = dist_default2(trimmed);
4070
+ }
4071
+ }
4072
+ if (index !== 0) {
4073
+ if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
4074
+ rows.push("");
4075
+ rowLength = 0;
4076
+ }
4077
+ if (rowLength || options.trim === false) {
4078
+ rows[rows.length - 1] += " ";
4079
+ rowLength++;
4080
+ }
4081
+ }
4082
+ const wordLength = dist_default2(word);
4083
+ if (options.hard && wordLength > columns) {
4084
+ const remainingColumns = columns - rowLength;
4085
+ const breaksStartingThisLine = 1 + Math.floor((wordLength - remainingColumns - 1) / columns);
4086
+ const breaksStartingNextLine = Math.floor((wordLength - 1) / columns);
4087
+ if (breaksStartingNextLine < breaksStartingThisLine) {
4088
+ rows.push("");
4089
+ }
4090
+ wrapWord(rows, word, columns);
4091
+ rowLength = dist_default2(rows.at(-1) ?? "");
4092
+ continue;
4093
+ }
4094
+ if (rowLength + wordLength > columns && rowLength && wordLength) {
4095
+ if (options.wordWrap === false && rowLength < columns) {
4096
+ wrapWord(rows, word, columns);
4097
+ rowLength = dist_default2(rows.at(-1) ?? "");
4098
+ continue;
4099
+ }
4100
+ rows.push("");
4101
+ rowLength = 0;
4102
+ }
4103
+ if (rowLength + wordLength > columns && options.wordWrap === false) {
4104
+ wrapWord(rows, word, columns);
4105
+ rowLength = dist_default2(rows.at(-1) ?? "");
4106
+ continue;
4107
+ }
4108
+ rows[rows.length - 1] += word;
4109
+ rowLength += wordLength;
4110
+ }
4111
+ if (options.trim !== false) {
4112
+ rows = rows.map((row) => stringVisibleTrimSpacesRight(row));
4113
+ }
4114
+ const preString = rows.join(`
4115
+ `);
4116
+ let inSurrogate = false;
4117
+ for (let i = 0;i < preString.length; i++) {
4118
+ const character = preString[i];
4119
+ returnValue += character;
4120
+ if (!inSurrogate) {
4121
+ inSurrogate = character >= "\uD800" && character <= "\uDBFF";
4122
+ if (inSurrogate) {
4123
+ continue;
4124
+ }
4125
+ } else {
4126
+ inSurrogate = false;
4127
+ }
4128
+ if (character === ESC2 || character === CSI) {
4129
+ GROUP_REGEX.lastIndex = i + 1;
4130
+ const groupsResult = GROUP_REGEX.exec(preString);
4131
+ const groups = groupsResult?.groups;
4132
+ if (groups?.code !== undefined) {
4133
+ const code = Number.parseFloat(groups.code);
4134
+ escapeCode = code === END_CODE ? undefined : code;
4135
+ } else if (groups?.uri !== undefined) {
4136
+ escapeUrl = groups.uri.length === 0 ? undefined : groups.uri;
4137
+ }
4138
+ }
4139
+ if (preString[i + 1] === `
4140
+ `) {
4141
+ if (escapeUrl) {
4142
+ returnValue += wrapAnsiHyperlink("");
4143
+ }
4144
+ const closingCode = escapeCode ? getClosingCode(escapeCode) : undefined;
4145
+ if (escapeCode && closingCode) {
4146
+ returnValue += wrapAnsiCode(closingCode);
4147
+ }
4148
+ } else if (character === `
4149
+ `) {
4150
+ if (escapeCode && getClosingCode(escapeCode)) {
4151
+ returnValue += wrapAnsiCode(escapeCode);
4152
+ }
4153
+ if (escapeUrl) {
4154
+ returnValue += wrapAnsiHyperlink(escapeUrl);
4155
+ }
4156
+ }
4157
+ }
4158
+ return returnValue;
4159
+ };
4160
+ var CRLF_OR_LF = /\r?\n/;
4161
+ function wrapAnsi(string, columns, options) {
4162
+ return String(string).normalize().split(CRLF_OR_LF).map((line) => exec(line, columns, options)).join(`
4163
+ `);
4164
+ }
4165
+
4166
+ // ../node_modules/.bun/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
4167
+ var import_sisteransi = __toESM(require_src(), 1);
4168
+ import { ReadStream } from "node:tty";
4169
+ function findCursor(s, o, l) {
4170
+ if (!l.some((r) => !r.disabled))
4171
+ return s;
4172
+ const t = s + o, n = Math.max(l.length - 1, 0), e = t < 0 ? n : t > n ? 0 : t;
4173
+ return l[e]?.disabled ? findCursor(e, o < 0 ? -1 : 1, l) : e;
4174
+ }
4175
+ var a$1 = ["up", "down", "left", "right", "space", "enter", "cancel"];
4176
+ var t = [
4177
+ "January",
4178
+ "February",
4179
+ "March",
4180
+ "April",
4181
+ "May",
4182
+ "June",
4183
+ "July",
4184
+ "August",
4185
+ "September",
4186
+ "October",
4187
+ "November",
4188
+ "December"
4189
+ ];
4190
+ var settings = {
4191
+ actions: new Set(a$1),
4192
+ aliases: /* @__PURE__ */ new Map([
4193
+ ["k", "up"],
4194
+ ["j", "down"],
4195
+ ["h", "left"],
4196
+ ["l", "right"],
4197
+ ["\x03", "cancel"],
4198
+ ["escape", "cancel"]
4199
+ ]),
4200
+ messages: {
4201
+ cancel: "Canceled",
4202
+ error: "Something went wrong"
4203
+ },
4204
+ withGuide: true,
4205
+ accessible: undefined,
4206
+ date: {
4207
+ monthNames: [...t],
4208
+ messages: {
4209
+ required: "Please enter a valid date",
4210
+ invalidMonth: "There are only 12 months in a year",
4211
+ invalidDay: (n, e) => `There are only ${n} days in ${e}`,
4212
+ afterMin: (n) => `Date must be on or after ${n.toISOString().slice(0, 10)}`,
4213
+ beforeMax: (n) => `Date must be on or before ${n.toISOString().slice(0, 10)}`
4214
+ }
4215
+ }
4216
+ };
4217
+ function isAccessible(n) {
4218
+ if (n !== undefined)
4219
+ return n;
4220
+ if (settings.accessible !== undefined)
4221
+ return settings.accessible;
4222
+ const e = process.env.ACCESSIBLE;
4223
+ return e !== undefined && e !== "" && e !== "0" && e !== "false";
4224
+ }
4225
+ function isActionKey(n, e) {
4226
+ if (typeof n == "string")
4227
+ return settings.aliases.get(n) === e;
4228
+ for (const s of n)
4229
+ if (s !== undefined && isActionKey(s, e))
4230
+ return true;
4231
+ return false;
4232
+ }
4233
+ function diffLines(i, s) {
4234
+ if (i === s)
4235
+ return;
4236
+ const e = i.split(`
4237
+ `), t = s.split(`
4238
+ `), r = Math.max(e.length, t.length), f = [];
4239
+ for (let n = 0;n < r; n++)
4240
+ e[n] !== t[n] && f.push(n);
4241
+ return {
4242
+ lines: f,
4243
+ numLinesBefore: e.length,
4244
+ numLinesAfter: t.length,
4245
+ numLines: r
4246
+ };
4247
+ }
4248
+ var R = globalThis.process.platform.startsWith("win");
4249
+ var CANCEL_SYMBOL = Symbol("clack:cancel");
4250
+ function isCancel(e) {
4251
+ return e === CANCEL_SYMBOL;
4252
+ }
4253
+ function setRawMode(e, r) {
4254
+ const o = e;
4255
+ o.isTTY && o.setRawMode(r);
4256
+ }
4257
+ function block({
4258
+ input: e = stdin,
4259
+ output: r = stdout,
4260
+ overwrite: o = true,
4261
+ hideCursor: n = true
4262
+ } = {}) {
4263
+ const s = l.createInterface({
4264
+ input: e,
4265
+ output: r,
4266
+ prompt: "",
4267
+ tabSize: 1
4268
+ });
4269
+ l.emitKeypressEvents(e, s), e instanceof ReadStream && e.isTTY && e.setRawMode(true);
4270
+ const t = (f, { name: a, sequence: w }) => {
4271
+ const c = String(f);
4272
+ if (isActionKey([c, a, w], "cancel")) {
4273
+ n && r.write(import_sisteransi.cursor.show), process.exit(0);
4274
+ return;
4275
+ }
4276
+ if (!o)
4277
+ return;
4278
+ const i = a === "return" ? 0 : -1, m = a === "return" ? -1 : 0;
4279
+ l.moveCursor(r, i, m, () => {
4280
+ l.clearLine(r, 1, () => {
4281
+ e.once("keypress", t);
4282
+ });
4283
+ });
4284
+ };
4285
+ return n && r.write(import_sisteransi.cursor.hide), e.once("keypress", t), () => {
4286
+ e.off("keypress", t), n && r.write(import_sisteransi.cursor.show), e instanceof ReadStream && e.isTTY && !R && e.setRawMode(false), s.terminal = false, s.close();
4287
+ };
4288
+ }
4289
+ var getColumns = (e) => ("columns" in e) && typeof e.columns == "number" ? e.columns : 80;
4290
+ var getRows = (e) => ("rows" in e) && typeof e.rows == "number" ? e.rows : 20;
4291
+ function wrapTextWithPrefix(e, r, o, n = o, s = o, t) {
4292
+ const f = getColumns(e ?? stdout);
4293
+ return wrapAnsi(r, f - o.length, {
4294
+ hard: true,
4295
+ trim: false
4296
+ }).split(`
4297
+ `).map((c, i, m) => {
4298
+ const d = t ? t(c, i) : c;
4299
+ return i === 0 ? `${n}${d}` : i === m.length - 1 ? `${s}${d}` : `${o}${d}`;
4300
+ }).join(`
4301
+ `);
4302
+ }
4303
+ function runValidation(e, a) {
4304
+ if ("~standard" in e) {
4305
+ const n = e["~standard"].validate(a);
4306
+ return n instanceof Promise ? n.then((r) => r.issues?.at(0)?.message) : n.issues?.at(0)?.message;
4307
+ }
4308
+ return e(a);
4309
+ }
4310
+
4311
+ class y {
4312
+ input;
4313
+ output;
4314
+ _abortSignal;
4315
+ rl;
4316
+ opts;
4317
+ _render;
4318
+ _track = false;
4319
+ _prevFrame = "";
4320
+ _subscribers = /* @__PURE__ */ new Map;
4321
+ _cursor = 0;
4322
+ state = "initial";
4323
+ error = "";
4324
+ value;
4325
+ userInput = "";
4326
+ get accessible() {
4327
+ return isAccessible(this.opts.accessible);
4328
+ }
4329
+ constructor(t, e = true) {
4330
+ const { input: i = stdin, output: s = stdout, render: r, signal: n, ...o } = t;
4331
+ this.opts = o, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = r.bind(this), this._track = e, this._abortSignal = n, this.input = i, this.output = s;
4332
+ }
4333
+ unsubscribe() {
4334
+ this._subscribers.clear();
4335
+ }
4336
+ setSubscriber(t, e) {
4337
+ const i = this._subscribers.get(t) ?? [];
4338
+ i.push(e), this._subscribers.set(t, i);
4339
+ }
4340
+ on(t, e) {
4341
+ this.setSubscriber(t, { cb: e });
4342
+ }
4343
+ once(t, e) {
4344
+ this.setSubscriber(t, { cb: e, once: true });
4345
+ }
4346
+ emit(t, ...e) {
4347
+ const i = this._subscribers.get(t) ?? [], s = [];
4348
+ for (const r of i)
4349
+ r.cb(...e), r.once && s.push(() => i.splice(i.indexOf(r), 1));
4350
+ for (const r of s)
4351
+ r();
4352
+ }
4353
+ prompt() {
4354
+ return new Promise((t) => {
4355
+ if (this._abortSignal) {
4356
+ if (this._abortSignal.aborted)
4357
+ return this.state = "cancel", this.close(), t(CANCEL_SYMBOL);
4358
+ this._abortSignal.addEventListener("abort", () => {
4359
+ this.state = "cancel", this.close();
4360
+ }, { once: true });
4361
+ }
4362
+ this.rl = l__default.createInterface({
4363
+ input: this.input,
4364
+ tabSize: 2,
4365
+ prompt: "",
4366
+ escapeCodeTimeout: 50,
4367
+ terminal: true
4368
+ }), this.rl.prompt(), this.opts.initialUserInput !== undefined && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), setRawMode(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
4369
+ this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(this.value);
4370
+ }), this.once("cancel", () => {
4371
+ this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(CANCEL_SYMBOL);
4372
+ });
4373
+ });
4374
+ }
4375
+ _isActionKey(t, e) {
4376
+ return t === "\t";
4377
+ }
4378
+ _shouldSubmit(t, e) {
4379
+ return true;
4380
+ }
4381
+ _setValue(t) {
4382
+ this.value = t, this.emit("value", this.value);
4383
+ }
4384
+ _setUserInput(t, e) {
4385
+ this.userInput = t ?? "", this.emit("userInput", this.userInput), e && this._track && this.rl && (this.rl.write(this.userInput), this._cursor = this.rl.cursor);
4386
+ }
4387
+ _clearUserInput() {
4388
+ this.rl?.write(null, { ctrl: true, name: "u" }), this._setUserInput("");
4389
+ }
4390
+ async onKeypress(t, e) {
4391
+ if (this.state !== "validating") {
4392
+ if (this._track && e.name !== "return" && (e.name && this._isActionKey(t, e) && this.rl?.write(null, { ctrl: true, name: "h" }), this._cursor = this.rl?.cursor ?? 0, this._setUserInput(this.rl?.line)), this.state === "error" && (this.state = "active"), e?.name && (!this._track && settings.aliases.has(e.name) && this.emit("cursor", settings.aliases.get(e.name)), settings.actions.has(e.name) && this.emit("cursor", e.name)), t && (t.toLowerCase() === "y" || t.toLowerCase() === "n") && this.emit("confirm", t.toLowerCase() === "y"), this.emit("key", t, e), e?.name === "return" && this._shouldSubmit(t, e)) {
4393
+ if (this.opts.validate) {
4394
+ const i = runValidation(this.opts.validate, this.value);
4395
+ let s;
4396
+ i instanceof Promise ? (this.state = "validating", this.render(), s = await i) : s = i, s && (this.error = s instanceof Error ? s.message : s, this.state = "error", this.rl?.write(this.userInput));
4397
+ }
4398
+ this.state !== "error" && (this.state = "submit");
4399
+ }
4400
+ isActionKey([t, e?.name, e?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close();
4401
+ }
4402
+ }
4403
+ close() {
4404
+ this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(`
4405
+ `), setRawMode(this.input, false), this.rl?.close(), this.rl = undefined, this.emit(`${this.state}`, this.value), this.unsubscribe();
4406
+ }
4407
+ restoreCursor() {
4408
+ const t = wrapAnsi(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split(`
4409
+ `).length - 1;
4410
+ this.output.write(import_sisteransi.cursor.move(-999, t * -1));
4411
+ }
4412
+ render() {
4413
+ const t = wrapAnsi(this._render(this) ?? "", process.stdout.columns, {
4414
+ hard: true,
4415
+ trim: false
4416
+ });
4417
+ if (t !== this._prevFrame) {
4418
+ if (this.state === "initial")
4419
+ this.output.write(import_sisteransi.cursor.hide);
4420
+ else {
4421
+ const e = diffLines(this._prevFrame, t), i = getRows(this.output);
4422
+ if (this.restoreCursor(), e) {
4423
+ const s = Math.max(0, e.numLinesAfter - i), r = Math.max(0, e.numLinesBefore - i);
4424
+ let n = e.lines.find((o) => o >= s);
4425
+ if (n === undefined) {
4426
+ this._prevFrame = t;
4427
+ return;
4428
+ }
4429
+ if (e.lines.length === 1) {
4430
+ this.output.write(import_sisteransi.cursor.move(0, n - r)), this.output.write(import_sisteransi.erase.lines(1));
4431
+ const o = t.split(`
4432
+ `);
4433
+ this.output.write(o[n]), this._prevFrame = t, this.output.write(import_sisteransi.cursor.move(0, o.length - n - 1));
4434
+ return;
4435
+ } else if (e.lines.length > 1) {
4436
+ if (s < r)
4437
+ n = s;
4438
+ else {
4439
+ const h = n - r;
4440
+ h > 0 && this.output.write(import_sisteransi.cursor.move(0, h));
4441
+ }
4442
+ this.output.write(import_sisteransi.erase.down());
4443
+ const f = t.split(`
4444
+ `).slice(n);
4445
+ this.output.write(f.join(`
4446
+ `)), this._prevFrame = t;
4447
+ return;
4448
+ }
4449
+ }
4450
+ this.output.write(import_sisteransi.erase.down());
4451
+ }
4452
+ this.output.write(t), this.state === "initial" && (this.state = "active"), this._prevFrame = t;
4453
+ }
4454
+ }
4455
+ }
4456
+ class r extends y {
4457
+ get cursor() {
4458
+ return this.value ? 0 : 1;
4459
+ }
4460
+ get _value() {
4461
+ return this.cursor === 0;
4462
+ }
4463
+ constructor(t) {
4464
+ super(t, false), this.value = !!t.initialValue, this.on("userInput", () => {
4465
+ this.value = this._value;
4466
+ }), this.on("confirm", (i) => {
4467
+ this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
4468
+ }), this.on("cursor", () => {
4469
+ this.value = !this.value;
4470
+ });
4471
+ }
4472
+ }
4473
+ var n$1 = class n extends y {
4474
+ options;
4475
+ cursor = 0;
4476
+ get _selectedValue() {
4477
+ return this.options[this.cursor];
4478
+ }
4479
+ changeValue() {
4480
+ const e = this._selectedValue;
4481
+ this.value = e === undefined ? undefined : e.value;
4482
+ }
4483
+ constructor(e) {
4484
+ super(e, false), this.options = e.options;
4485
+ const o = this.options.findIndex(({ value: s }) => s === e.initialValue), t = o === -1 ? 0 : o;
4486
+ this.cursor = this.options[t]?.disabled ? findCursor(t, 1, this.options) : t, this.changeValue(), this.on("cursor", (s) => {
4487
+ switch (s) {
4488
+ case "left":
4489
+ case "up":
4490
+ this.cursor = findCursor(this.cursor, -1, this.options);
4491
+ break;
4492
+ case "down":
4493
+ case "right":
4494
+ this.cursor = findCursor(this.cursor, 1, this.options);
4495
+ break;
4496
+ }
4497
+ this.changeValue();
4498
+ });
4499
+ }
4500
+ };
4501
+
4502
+ // ../node_modules/.bun/@clack+prompts@1.8.1/node_modules/@clack/prompts/dist/index.mjs
4503
+ import { styleText, stripVTControlCharacters } from "node:util";
4504
+ import process$1 from "node:process";
4505
+ var import_sisteransi2 = __toESM(require_src(), 1);
4506
+ function isUnicodeSupported() {
4507
+ if (process$1.platform !== "win32") {
4508
+ return process$1.env.TERM !== "linux";
4509
+ }
4510
+ return Boolean(process$1.env.CI) || Boolean(process$1.env.WT_SESSION) || Boolean(process$1.env.TERMINUS_SUBLIME) || process$1.env.ConEmuTask === "{cmd::Cmder}" || process$1.env.TERM_PROGRAM === "Terminus-Sublime" || process$1.env.TERM_PROGRAM === "vscode" || process$1.env.TERM === "xterm-256color" || process$1.env.TERM === "alacritty" || process$1.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
4511
+ }
4512
+ var unicode = isUnicodeSupported();
4513
+ var isCI = () => process.env.CI === "true";
4514
+ var unicodeOr = (o, e) => unicode ? o : e;
4515
+ var S_STEP_ACTIVE = unicodeOr("◆", "*");
4516
+ var S_STEP_CANCEL = unicodeOr("■", "x");
4517
+ var S_STEP_ERROR = unicodeOr("▲", "x");
4518
+ var S_STEP_SUBMIT = unicodeOr("◇", "o");
4519
+ var S_BAR_START = unicodeOr("┌", "T");
4520
+ var S_BAR = unicodeOr("│", "|");
4521
+ var S_BAR_END = unicodeOr("└", "—");
4522
+ var S_BAR_START_RIGHT = unicodeOr("┐", "T");
4523
+ var S_BAR_END_RIGHT = unicodeOr("┘", "—");
4524
+ var S_RADIO_ACTIVE = unicodeOr("●", ">");
4525
+ var S_RADIO_INACTIVE = unicodeOr("○", " ");
4526
+ var S_CHECKBOX_ACTIVE = unicodeOr("◻", "[•]");
4527
+ var S_CHECKBOX_SELECTED = unicodeOr("◼", "[+]");
4528
+ var S_CHECKBOX_INACTIVE = unicodeOr("◻", "[ ]");
4529
+ var S_PASSWORD_MASK = unicodeOr("▪", "•");
4530
+ var S_BAR_H = unicodeOr("─", "-");
4531
+ var S_CORNER_TOP_RIGHT = unicodeOr("╮", "+");
4532
+ var S_CONNECT_LEFT = unicodeOr("├", "+");
4533
+ var S_CORNER_BOTTOM_RIGHT = unicodeOr("╯", "+");
4534
+ var S_CORNER_BOTTOM_LEFT = unicodeOr("╰", "+");
4535
+ var S_CORNER_TOP_LEFT = unicodeOr("╭", "+");
4536
+ var S_INFO = unicodeOr("●", "•");
4537
+ var S_SUCCESS = unicodeOr("◆", "*");
4538
+ var S_WARN = unicodeOr("▲", "!");
4539
+ var S_ERROR = unicodeOr("■", "x");
4540
+ var symbol = (o) => {
4541
+ switch (o) {
4542
+ case "initial":
4543
+ case "active":
4544
+ return styleText("cyan", S_STEP_ACTIVE);
4545
+ case "cancel":
4546
+ return styleText("red", S_STEP_CANCEL);
4547
+ case "error":
4548
+ return styleText("yellow", S_STEP_ERROR);
4549
+ case "submit":
4550
+ return styleText("green", S_STEP_SUBMIT);
4551
+ case "validating":
4552
+ return styleText("dim", S_STEP_ACTIVE);
4553
+ }
4554
+ };
4555
+ var symbolBar = (o) => {
4556
+ switch (o) {
4557
+ case "initial":
4558
+ case "active":
4559
+ return styleText("cyan", S_BAR);
4560
+ case "cancel":
4561
+ return styleText("red", S_BAR);
4562
+ case "error":
4563
+ return styleText("yellow", S_BAR);
4564
+ case "submit":
4565
+ return styleText("green", S_BAR);
4566
+ }
4567
+ };
4568
+ function formatInstructionFooter(o, e) {
4569
+ const r = [`${e ? `${styleText("cyan", S_BAR)} ` : ""}${o.join(" • ")}`];
4570
+ return e && r.push(styleText("cyan", S_BAR_END)), r;
4571
+ }
4572
+ var I = (l, e, w, p, b, C = false) => {
4573
+ let r = e, O = 0;
4574
+ if (C)
4575
+ for (let i = p - 1;i >= w; i--) {
4576
+ const m = l[i];
4577
+ if (m && (r -= m.length), O++, r <= b)
4578
+ break;
4579
+ }
4580
+ else
4581
+ for (let i = w;i < p; i++) {
4582
+ const m = l[i];
4583
+ if (m && (r -= m.length), O++, r <= b)
4584
+ break;
4585
+ }
4586
+ return { lineCount: r, removals: O };
4587
+ };
4588
+ var limitOptions = ({
4589
+ cursor: l,
4590
+ options: e,
4591
+ style: w,
4592
+ output: p = process.stdout,
4593
+ maxItems: b = Number.POSITIVE_INFINITY,
4594
+ columnPadding: C = 0,
4595
+ rowPadding: r = 4
4596
+ }) => {
4597
+ const i = getColumns(p) - C, m = getRows(p), M = styleText("dim", "..."), v = Math.max(m - r, 0), a = Math.max(Math.min(b, v), 5);
4598
+ let f = 0;
4599
+ l >= a - 3 && (f = Math.max(Math.min(l - a + 3, e.length - a), 0));
4600
+ let d = a < e.length && f > 0, c = a < e.length && f + a < e.length;
4601
+ const W = Math.min(f + a, e.length), s = [];
4602
+ let g = 0;
4603
+ d && g++, c && g++;
4604
+ const T = f + (d ? 1 : 0), y = W - (c ? 1 : 0);
4605
+ for (let t = T;t < y; t++) {
4606
+ const n = e[t], o = n ? w(n, t === l) : "", h = wrapAnsi(o, i, {
4607
+ hard: true,
4608
+ trim: false
4609
+ }).split(`
4610
+ `);
4611
+ s.push(h), g += h.length;
4612
+ }
4613
+ if (g > v) {
4614
+ let t = 0, n = 0, o = g;
4615
+ const h = l - T;
4616
+ let u = v;
4617
+ const L = () => I(s, o, 0, h, u), E = () => I(s, o, h + 1, s.length, u, true);
4618
+ d ? ({ lineCount: o, removals: t } = L(), o > u && (c || (u -= 1), { lineCount: o, removals: n } = E())) : (c || (u -= 1), { lineCount: o, removals: n } = E(), o > u && (u -= 1, { lineCount: o, removals: t } = L())), t > 0 && (d = true, s.splice(0, t)), n > 0 && (c = true, s.splice(s.length - n, n));
4619
+ }
4620
+ const x = [];
4621
+ d && x.push(M);
4622
+ for (const t of s)
4623
+ for (const n of t)
4624
+ x.push(n);
4625
+ return c && x.push(M), x;
4626
+ };
4627
+ var confirm = (e) => {
4628
+ const a = e.active ?? "Yes", o = e.inactive ?? "No";
4629
+ return new r({
4630
+ active: a,
4631
+ inactive: o,
4632
+ signal: e.signal,
4633
+ input: e.input,
4634
+ output: e.output,
4635
+ initialValue: e.initialValue ?? true,
4636
+ render() {
4637
+ const i = e.withGuide ?? settings.withGuide, u = `${symbol(this.state)} `, l = i ? `${styleText("gray", S_BAR)} ` : "", f = wrapTextWithPrefix(e.output, e.message, l, u), s = `${i ? `${styleText("gray", S_BAR)}
4638
+ ` : ""}${f}
4639
+ `, c = this.value ? a : o;
4640
+ switch (this.state) {
4641
+ case "submit": {
4642
+ const r = i ? `${styleText("gray", S_BAR)} ` : "";
4643
+ return `${s}${r}${styleText("dim", c)}`;
4644
+ }
4645
+ case "cancel": {
4646
+ const r = i ? `${styleText("gray", S_BAR)} ` : "";
4647
+ return `${s}${r}${styleText(["strikethrough", "dim"], c)}${i ? `
4648
+ ${styleText("gray", S_BAR)}` : ""}`;
4649
+ }
4650
+ default: {
4651
+ const r = i ? `${styleText("cyan", S_BAR)} ` : "", g = i ? styleText("cyan", S_BAR_END) : "";
4652
+ return `${s}${r}${this.value ? `${styleText("green", S_RADIO_ACTIVE)} ${a}` : `${styleText("dim", S_RADIO_INACTIVE)} ${styleText("dim", a)}`}${e.vertical ? i ? `
4653
+ ${styleText("cyan", S_BAR)} ` : `
4654
+ ` : ` ${styleText("dim", "/")} `}${this.value ? `${styleText("dim", S_RADIO_INACTIVE)} ${styleText("dim", o)}` : `${styleText("green", S_RADIO_ACTIVE)} ${o}`}
4655
+ ${g}
4656
+ `;
4657
+ }
4658
+ }
4659
+ }
4660
+ }).prompt();
4661
+ };
4662
+ var MULTISELECT_INSTRUCTIONS = [
4663
+ `${styleText("dim", "↑/↓")} to navigate`,
4664
+ `${styleText("dim", "Space:")} select`,
4665
+ `${styleText("dim", "Enter:")} confirm`
4666
+ ];
4667
+ var intro = (o = "", t) => {
4668
+ const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText("gray", S_BAR_START)} ` : "";
4669
+ i.write(`${e}${o}
4670
+ `);
4671
+ };
4672
+ var outro = (o = "", t) => {
4673
+ const i = t?.output ?? process.stdout, e = t?.withGuide ?? settings.withGuide ? `${styleText("gray", S_BAR)}
4674
+ ${styleText("gray", S_BAR_END)} ` : "";
4675
+ i.write(`${e}${o}
4676
+
4677
+ `);
4678
+ };
4679
+ var W = (l) => styleText("magenta", l);
4680
+ var spinner = ({
4681
+ indicator: l = "dots",
4682
+ onCancel: h,
4683
+ output: n = process.stdout,
4684
+ cancelMessage: G,
4685
+ errorMessage: O,
4686
+ frames: E = unicode ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"],
4687
+ delay: F = unicode ? 80 : 120,
4688
+ signal: m,
4689
+ ...I
4690
+ } = {}) => {
4691
+ const u = isCI();
4692
+ let M, T, d = false, S = false, s = "", p, w = performance.now();
4693
+ const x = getColumns(n), k = I?.styleFrame ?? W, g = (e) => {
4694
+ const r = e > 1 ? O ?? settings.messages.error : G ?? settings.messages.cancel;
4695
+ S = e === 1, d && (a(r, e), S && typeof h == "function" && h());
4696
+ }, f = () => g(2), i = () => g(1), A = () => {
4697
+ process.on("uncaughtExceptionMonitor", f), process.on("unhandledRejection", f), process.on("SIGINT", i), process.on("SIGTERM", i), process.on("exit", g), m && m.addEventListener("abort", i);
4698
+ }, H = () => {
4699
+ process.removeListener("uncaughtExceptionMonitor", f), process.removeListener("unhandledRejection", f), process.removeListener("SIGINT", i), process.removeListener("SIGTERM", i), process.removeListener("exit", g), m && m.removeEventListener("abort", i);
4700
+ }, y = () => {
4701
+ if (p === undefined)
4702
+ return;
4703
+ u && n.write(`
4704
+ `);
4705
+ const r = wrapAnsi(p, x, {
4706
+ hard: true,
4707
+ trim: false
4708
+ }).split(`
4709
+ `);
4710
+ r.length > 1 && n.write(import_sisteransi2.cursor.up(r.length - 1)), n.write(import_sisteransi2.cursor.to(0)), n.write(import_sisteransi2.erase.down());
4711
+ }, C = (e) => e.replace(/\.+$/, ""), _ = (e) => {
4712
+ const r = (performance.now() - e) / 1000, t = Math.floor(r / 60), o = Math.floor(r % 60);
4713
+ return t > 0 ? `[${t}m ${o}s]` : `[${o}s]`;
4714
+ }, N = I.withGuide ?? settings.withGuide, P = (e = "") => {
4715
+ d = true, M = block({ output: n }), s = C(e), w = performance.now(), N && n.write(`${styleText("gray", S_BAR)}
4716
+ `);
4717
+ let r = 0, t = 0;
4718
+ A(), T = setInterval(() => {
4719
+ if (u && s === p)
4720
+ return;
4721
+ y(), p = s;
4722
+ const o = k(E[r]);
4723
+ let v;
4724
+ if (u)
4725
+ v = `${o} ${s}...`;
4726
+ else if (l === "timer")
4727
+ v = `${o} ${s} ${_(w)}`;
4728
+ else {
4729
+ const B = ".".repeat(Math.floor(t)).slice(0, 3);
4730
+ v = `${o} ${s}${B}`;
4731
+ }
4732
+ const j = wrapAnsi(v, x, {
4733
+ hard: true,
4734
+ trim: false
4735
+ });
4736
+ n.write(j), r = r + 1 < E.length ? r + 1 : 0, t = t < 4 ? t + 0.125 : 0;
4737
+ }, F);
4738
+ }, a = (e = "", r = 0, t = false) => {
4739
+ if (!d)
4740
+ return;
4741
+ d = false, clearInterval(T), y();
4742
+ const o = r === 0 ? styleText("green", S_STEP_SUBMIT) : r === 1 ? styleText("red", S_STEP_CANCEL) : styleText("red", S_STEP_ERROR);
4743
+ s = e ?? s, t || (l === "timer" ? n.write(`${o} ${s} ${_(w)}
4744
+ `) : n.write(`${o} ${s}
4745
+ `)), H(), M();
4746
+ };
4747
+ return {
4748
+ start: P,
4749
+ stop: (e = "") => a(e, 0),
4750
+ message: (e = "") => {
4751
+ s = C(e ?? s);
4752
+ },
4753
+ cancel: (e = "") => a(e, 1),
4754
+ error: (e = "") => a(e, 2),
4755
+ clear: () => a("", 0, true),
4756
+ get isCancelled() {
4757
+ return S;
4758
+ }
4759
+ };
4760
+ };
4761
+ var u2 = {
4762
+ light: unicodeOr("─", "-"),
4763
+ heavy: unicodeOr("━", "="),
4764
+ block: unicodeOr("█", "#")
4765
+ };
4766
+ var SELECT_INSTRUCTIONS = [
4767
+ `${styleText("dim", "↑/↓")} to navigate`,
4768
+ `${styleText("dim", "Enter:")} confirm`
4769
+ ];
4770
+ var c = (t, o) => t.includes(`
4771
+ `) ? t.split(`
4772
+ `).map((d) => o(d)).join(`
4773
+ `) : o(t);
4774
+ var select = (t) => {
4775
+ const o = (n, m) => {
4776
+ if (n === undefined)
4777
+ return "";
4778
+ const s = n.label ?? String(n.value);
4779
+ switch (m) {
4780
+ case "disabled":
4781
+ return `${styleText("gray", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("gray", i))}${n.hint ? ` ${styleText("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
4782
+ case "selected":
4783
+ return `${c(s, (i) => styleText("dim", i))}`;
4784
+ case "active":
4785
+ return `${styleText("green", S_RADIO_ACTIVE)} ${s}${n.hint ? ` ${styleText("dim", `(${n.hint})`)}` : ""}`;
4786
+ case "cancelled":
4787
+ return `${c(s, (i) => styleText(["strikethrough", "dim"], i))}`;
4788
+ default:
4789
+ return `${styleText("dim", S_RADIO_INACTIVE)} ${c(s, (i) => styleText("dim", i))}`;
4790
+ }
4791
+ }, d = t.showInstructions ?? true;
4792
+ return new n$1({
4793
+ options: t.options,
4794
+ signal: t.signal,
4795
+ input: t.input,
4796
+ output: t.output,
4797
+ initialValue: t.initialValue,
4798
+ render() {
4799
+ const n = t.withGuide ?? settings.withGuide, m = `${symbol(this.state)} `, s = `${symbolBar(this.state)} `, i = wrapTextWithPrefix(t.output, t.message, s, m), u = `${n ? `${styleText("gray", S_BAR)}
4800
+ ` : ""}${i}
4801
+ `;
4802
+ switch (this.state) {
4803
+ case "submit": {
4804
+ const r = n ? `${styleText("gray", S_BAR)} ` : "", a = wrapTextWithPrefix(t.output, o(this.options[this.cursor], "selected"), r);
4805
+ return `${u}${a}`;
4806
+ }
4807
+ case "cancel": {
4808
+ const r = n ? `${styleText("gray", S_BAR)} ` : "", a = wrapTextWithPrefix(t.output, o(this.options[this.cursor], "cancelled"), r);
4809
+ return `${u}${a}${n ? `
4810
+ ${styleText("gray", S_BAR)}` : ""}`;
4811
+ }
4812
+ default: {
4813
+ const r = n ? `${styleText("cyan", S_BAR)} ` : "", a = u.split(`
4814
+ `).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS, n) : n ? [styleText("cyan", S_BAR_END)] : [], f = p.join(`
4815
+ `), b = p.length + 1;
4816
+ return `${u}${r}${limitOptions({
4817
+ output: t.output,
4818
+ cursor: this.cursor,
4819
+ options: this.options,
4820
+ maxItems: t.maxItems,
4821
+ columnPadding: r.length,
4822
+ rowPadding: a + b,
4823
+ style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive")
4824
+ }).join(`
4825
+ ${r}`)}
4826
+ ${f}
4827
+ `;
4828
+ }
4829
+ }
4830
+ }
4831
+ }).prompt();
4832
+ };
4833
+ var i = `${styleText("gray", S_BAR)} `;
4834
+
4835
+ // src/push.ts
2286
4836
  var CONFIRM_FLAGS = new Set(["--yes", "-y"]);
2287
4837
  var FORWARDED_GIT_FLAGS = new Set(["--force"]);
2288
4838
  function parsePushArgs(args) {
@@ -2352,26 +4902,46 @@ function planPush({
2352
4902
  reason: names.length === 0 ? "No session in this repo has a branch ready to push." : "Several session branches are ready; pick one."
2353
4903
  };
2354
4904
  }
2355
- async function choose(candidates) {
2356
- if (!process.stdin.isTTY)
2357
- return;
2358
- console.log(candidates.map((name, index) => ` ${index + 1}) ${name}`).join(`
2359
- `));
2360
- const answer = await ask("Branch to push: ");
2361
- return candidates[Number(answer) - 1];
4905
+ function branchHint(run, candidate) {
4906
+ const count = git(run, candidate.dir, [
4907
+ "rev-list",
4908
+ "--count",
4909
+ `${candidate.baseSha}..${candidate.branch}`
4910
+ ]).out;
4911
+ const stat = cleanOutput(git(run, candidate.dir, [
4912
+ "diff",
4913
+ "--no-color",
4914
+ "--stat",
4915
+ `${candidate.baseSha}...${candidate.branch}`
4916
+ ]).out);
4917
+ const files = stat.split(`
4918
+ `).pop() ?? "";
4919
+ return [
4920
+ `${count || "0"} commit${count === "1" ? "" : "s"}`,
4921
+ files || "no file changes"
4922
+ ].join(" | ");
4923
+ }
4924
+ async function chooseInteractive(run, names, candidates) {
4925
+ return select({
4926
+ message: "Branch to push",
4927
+ options: names.map((name) => ({
4928
+ value: name,
4929
+ label: name,
4930
+ hint: branchHint(run, candidates.get(name))
4931
+ }))
4932
+ });
2362
4933
  }
2363
4934
  function describe(run, root, branch, base, tip, gitFlags) {
2364
- const commits = git(run, root, [
2365
- "log",
2366
- "--format= %h %s (%an / %cn)",
2367
- `${base}..${tip}`
2368
- ]).out;
2369
- const stat = git(run, root, ["diff", "--stat", `${base}...${tip}`]).out;
4935
+ const commits = cleanOutput(git(run, root, ["log", "--format= %h %s (%an / %cn)", `${base}..${tip}`]).out);
4936
+ const stat = cleanOutput(git(run, root, ["diff", "--no-color", "--stat", `${base}...${tip}`]).out);
2370
4937
  return [
2371
4938
  `Pushing ${branch} to origin${gitFlags.length ? ` with ${gitFlags.join(" ")}` : ""} (compared with ${base}):`,
2372
- stripControl(commits) || " (no new commits)",
4939
+ `
4940
+ Commits:`,
4941
+ commits || " (no new commits)",
2373
4942
  stat ? `
2374
- ${stripControl(stat)}` : ""
4943
+ Changed files:
4944
+ ${stat}` : ""
2375
4945
  ].join(`
2376
4946
  `);
2377
4947
  }
@@ -2390,10 +4960,20 @@ async function push({ args }) {
2390
4960
  } catch (error) {
2391
4961
  throw error instanceof LaunchError ? error : new LaunchError(String(error));
2392
4962
  }
4963
+ const tty = process.stdin.isTTY === true;
2393
4964
  const candidates = pushCandidates(sessionsForRepo(root));
2394
4965
  let plan = planPush({ requested, candidates });
2395
4966
  if (!plan.ok && !requested && plan.candidates.length > 1) {
2396
- const picked = await choose(plan.candidates);
4967
+ let picked;
4968
+ if (tty) {
4969
+ intro("maesttro push");
4970
+ const chosen = await chooseInteractive(run, plan.candidates, candidates);
4971
+ if (isCancel(chosen)) {
4972
+ console.log("Not pushed.");
4973
+ return;
4974
+ }
4975
+ picked = chosen;
4976
+ }
2397
4977
  if (picked)
2398
4978
  plan = planPush({ requested: picked, candidates });
2399
4979
  }
@@ -2403,36 +4983,43 @@ async function push({ args }) {
2403
4983
  return;
2404
4984
  }
2405
4985
  const { branch, base, baseSha, dir, sessionId } = plan.candidate;
4986
+ const prepare = tty ? spinner() : undefined;
4987
+ prepare?.start(`Preparing ${branch}...`);
2406
4988
  const tip = branchTip(dir, branch);
2407
4989
  if (!tip || !importObjects(run, root, dir, tip)) {
4990
+ prepare?.stop(`Could not prepare ${branch}.`);
2408
4991
  console.error(`Could not read ${branch} from session ${sessionId}.`);
2409
4992
  process.exitCode = 1;
2410
4993
  return;
2411
4994
  }
2412
4995
  git(run, root, ["update-ref", `refs/maesttro/${sessionId}/${branch}`, tip]);
4996
+ prepare?.stop(`Prepared ${branch}.`);
2413
4997
  console.log(describe(run, root, branch, base, tip, gitFlags));
2414
4998
  if (!yes) {
2415
- if (!process.stdin.isTTY) {
4999
+ if (!tty) {
2416
5000
  console.error("Not a terminal; re-run with --yes to push without confirming.");
2417
5001
  process.exitCode = 1;
2418
5002
  return;
2419
5003
  }
2420
- const answer = await ask(`
2421
- Push? [y/N] `);
2422
- if (!/^y(es)?$/i.test(answer)) {
5004
+ const answer = await confirm({ message: `Push ${branch} to origin?` });
5005
+ if (isCancel(answer) || !answer) {
2423
5006
  console.log("Not pushed.");
2424
5007
  return;
2425
5008
  }
2426
5009
  }
2427
- const pushed = spawnSync3("git", ["-C", root, "push", ...gitFlags, "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
5010
+ const pushed = spawnSync4("git", ["-C", root, "push", ...gitFlags, "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
2428
5011
  if (pushed.status === 0) {
2429
- console.log(`Pushed ${branch}. Base for comparison was ${baseSha.slice(0, 8)}.`);
5012
+ const done = `Pushed ${branch}. Base for comparison was ${baseSha.slice(0, 8)}.`;
5013
+ if (tty && !yes)
5014
+ outro(done);
5015
+ else
5016
+ console.log(done);
2430
5017
  }
2431
5018
  process.exitCode = pushed.status ?? 1;
2432
5019
  }
2433
5020
 
2434
5021
  // src/sessions-command.ts
2435
- import { existsSync as existsSync5, rmSync as rmSync7 } from "node:fs";
5022
+ import { existsSync as existsSync6, rmSync as rmSync10 } from "node:fs";
2436
5023
  function parseRemoveArgs(args) {
2437
5024
  let all = false;
2438
5025
  let force = false;
@@ -2448,7 +5035,7 @@ function parseRemoveArgs(args) {
2448
5035
  return { all, force, id };
2449
5036
  }
2450
5037
  async function removeSession(run, session, force, ask, interactive) {
2451
- if (existsSync5(session.worktree)) {
5038
+ if (existsSync6(session.worktree)) {
2452
5039
  const preserved = preserveBranches(run, session);
2453
5040
  const head = cloneHead(session.worktree);
2454
5041
  const unbranched = preserved.length === 0 && head?.detached !== undefined && head.detached !== session.baseSha;
@@ -2466,7 +5053,7 @@ async function removeSession(run, session, force, ask, interactive) {
2466
5053
  return "kept";
2467
5054
  }
2468
5055
  }
2469
- rmSync7(session.worktree, { recursive: true, force: true });
5056
+ rmSync10(session.worktree, { recursive: true, force: true });
2470
5057
  const kept = preserved.length > 0 ? ` Its branches are kept as refs/maesttro/${session.id}/*.` : "";
2471
5058
  console.log(`Removed session ${session.id}.${kept}`);
2472
5059
  } else {
@@ -2523,7 +5110,7 @@ async function sessionsCommand({
2523
5110
  return;
2524
5111
  }
2525
5112
  for (const session of sessions) {
2526
- const branches = existsSync5(session.worktree) ? cloneBranches(session.worktree) : undefined;
5113
+ const branches = existsSync6(session.worktree) ? cloneBranches(session.worktree) : undefined;
2527
5114
  const where = branches === undefined ? "clone missing" : branches.length > 0 ? `branches: ${branches.join(", ")}` : "no branches yet";
2528
5115
  const mode = session.strict ? " strict" : "";
2529
5116
  console.log(` ${session.id} ${session.createdAt.slice(0, 16)} ${where}${mode}`);
@@ -2551,23 +5138,31 @@ async function setup({ args }) {
2551
5138
  console.log("-> Building runner image (this pulls node:22-slim + installs CLIs, first run only)...");
2552
5139
  run(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`);
2553
5140
  console.log(`
2554
- -> Ensuring persistent volume exists...`);
2555
- execSync4(`docker volume create ${VOLUME}`, { stdio: "ignore" });
2556
- console.log(`
2557
- -> Removing any long-lived GitHub token from the volume...`);
2558
- if (purgeLegacyToken())
2559
- console.log(` ${REVOKE_NOTICE}`);
2560
- writeMaesttroConfig({ ...readMaesttroConfig(), legacyTokenPurged: true });
5141
+ -> Checking for the old shared runner volume...`);
5142
+ migrateFromVolumeOnce({
5143
+ config: readMaesttroConfig(),
5144
+ saveConfig: writeMaesttroConfig,
5145
+ store: claudeStore(),
5146
+ announce: (message) => console.log(` ${message}`)
5147
+ });
2561
5148
  if (args[0]) {
2562
- console.log(" Ignoring the token argument: sessions now get short-lived tokens through `maesttro login`.");
5149
+ console.log(" Ignoring the token argument: sessions now get short-lived tokens through `maesttro login github`.");
2563
5150
  }
2564
5151
  console.log(`
2565
5152
  -> Logging in to GitHub...`);
2566
5153
  await ensureLogin();
2567
5154
  console.log(`
5155
+ -> Logging in to Claude...`);
5156
+ const claude = claudeStore();
5157
+ if (claudeLoginState({ credentials: claude.load(), now: Date.now() }) === "active") {
5158
+ console.log(` Already logged in. Stored in ${claude.kind}.`);
5159
+ } else if (!claudeLogin({ announce: (message) => console.log(` ${message}`) })) {
5160
+ console.log(" Setup continues without it; Claude Code sessions will show their own login screen.");
5161
+ }
5162
+ console.log(`
2568
5163
  -> Verifying isolation...`);
2569
5164
  console.log(" personal SSH key should NOT be visible:");
2570
- run(`docker run --rm ${hostIdentityFlags()} -v ${VOLUME}:/home/agent ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || true"`);
5165
+ run(`docker run --rm ${hostIdentityFlags()} ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || true"`);
2571
5166
  console.log("\nSetup complete. Run `maesttro` from any project directory to start a session,");
2572
5167
  console.log("or `maesttro list` to see available clients.");
2573
5168
  }
@@ -2575,17 +5170,89 @@ async function setup({ args }) {
2575
5170
  // src/update.ts
2576
5171
  import { execSync as execSync5 } from "node:child_process";
2577
5172
 
5173
+ // src/runner-images.ts
5174
+ var RUNNER_LABEL = "org.maesttro.runner";
5175
+ var RUNNER_ENTRYPOINT = "/usr/local/bin/maesttro-entrypoint";
5176
+ var INSPECT_FORMAT = "{{.Id}}\t{{json .Config}}\t{{json .RepoTags}}";
5177
+ function parseJson(text, fallback) {
5178
+ try {
5179
+ return JSON.parse(text ?? "") ?? fallback;
5180
+ } catch {
5181
+ return fallback;
5182
+ }
5183
+ }
5184
+ function parseImageRows(stdout) {
5185
+ return stdout.split(`
5186
+ `).filter((line) => line.trim() !== "").map((line) => {
5187
+ const [id = "", config, tags] = line.split("\t");
5188
+ const { Entrypoint, Labels } = parseJson(config, {});
5189
+ return {
5190
+ id,
5191
+ entrypoint: Entrypoint ?? [],
5192
+ labels: Labels ?? {},
5193
+ tags: parseJson(tags, [])
5194
+ };
5195
+ });
5196
+ }
5197
+ function isRunnerImage(row) {
5198
+ return row.labels[RUNNER_LABEL] !== undefined || row.entrypoint.includes(RUNNER_ENTRYPOINT);
5199
+ }
5200
+ function staleRunnerImages({
5201
+ rows,
5202
+ latestId
5203
+ }) {
5204
+ if (!latestId)
5205
+ return [];
5206
+ return rows.filter((row) => row.id !== latestId && isRunnerImage(row));
5207
+ }
5208
+ function currentLatestId(run) {
5209
+ const result = run("docker", [
5210
+ "image",
5211
+ "inspect",
5212
+ "--format",
5213
+ "{{.Id}}",
5214
+ IMAGE
5215
+ ]);
5216
+ return result.status === 0 ? result.stdout.trim() || undefined : undefined;
5217
+ }
5218
+ function removeStaleRunnerImages({
5219
+ run = runCommand,
5220
+ latestId = () => currentLatestId(run)
5221
+ } = {}) {
5222
+ const listed = run("docker", ["image", "ls", "--quiet", "--no-trunc"]);
5223
+ if (listed.status !== 0)
5224
+ return 0;
5225
+ const ids = [
5226
+ ...new Set(listed.stdout.split(`
5227
+ `).map((id) => id.trim()).filter(Boolean))
5228
+ ];
5229
+ if (ids.length === 0)
5230
+ return 0;
5231
+ const inspected = run("docker", [
5232
+ "image",
5233
+ "inspect",
5234
+ "--format",
5235
+ INSPECT_FORMAT,
5236
+ ...ids
5237
+ ]);
5238
+ const stale = staleRunnerImages({
5239
+ rows: parseImageRows(inspected.stdout),
5240
+ latestId: latestId()
5241
+ });
5242
+ return stale.filter((image) => run("docker", ["rmi", ...image.tags, image.id]).status === 0).length;
5243
+ }
5244
+
2578
5245
  // src/version-check.ts
2579
5246
  import { execFile } from "node:child_process";
2580
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
2581
- import { join as join12 } from "node:path";
5247
+ import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
5248
+ import { join as join16 } from "node:path";
2582
5249
  var LOOKUP_TIMEOUT_MS = 3000;
2583
5250
  function installedManifest() {
2584
- return JSON.parse(readFileSync8(join12(PACKAGE_ROOT, "package.json"), "utf8"));
5251
+ return JSON.parse(readFileSync12(join16(PACKAGE_ROOT, "package.json"), "utf8"));
2585
5252
  }
2586
5253
  function canOfferUpdate({
2587
5254
  interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
2588
- inRunner = existsSync6(RUNNER_MARKER)
5255
+ inRunner = existsSync7(RUNNER_MARKER)
2589
5256
  } = {}) {
2590
5257
  return interactive && !inRunner;
2591
5258
  }
@@ -2634,6 +5301,10 @@ function update() {
2634
5301
  -> Rebuilding runner image with refreshed client configs...`);
2635
5302
  execSync5(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`, { stdio: "inherit" });
2636
5303
  console.log(`
5304
+ -> Removing older runner images...`);
5305
+ const removed = removeStaleRunnerImages();
5306
+ console.log(removed > 0 ? ` Removed ${removed} older runner image${removed === 1 ? "" : "s"}.` : " None to remove.");
5307
+ console.log(`
2637
5308
  Update complete. Bot identity and your project files are untouched —`);
2638
5309
  console.log("only the runner image (client versions + curated configs) was refreshed.");
2639
5310
  }
@@ -2675,6 +5346,13 @@ var MODE_FLAGS = {
2675
5346
  "--strict": true,
2676
5347
  "--no-strict": false
2677
5348
  };
5349
+ function clientFirst(args) {
5350
+ const [first, second, ...rest] = args;
5351
+ if (first !== undefined && second !== undefined && isClientKey(first)) {
5352
+ return [second, first, ...rest];
5353
+ }
5354
+ return args;
5355
+ }
2678
5356
  function takeModeFlags(args) {
2679
5357
  let strict;
2680
5358
  let index = 0;
@@ -2726,13 +5404,13 @@ async function main(argv, commands = defaultCommands) {
2726
5404
  await commands.doctor();
2727
5405
  return;
2728
5406
  case "login":
2729
- await commands.login();
5407
+ await commands.login({ args: rest });
2730
5408
  return;
2731
5409
  case "logout":
2732
- await commands.logout();
5410
+ await commands.logout({ args: rest });
2733
5411
  return;
2734
5412
  case "status":
2735
- await commands.status();
5413
+ await commands.status({ args: rest });
2736
5414
  return;
2737
5415
  case "push":
2738
5416
  await commands.push({ args: rest });
@@ -2742,11 +5420,11 @@ async function main(argv, commands = defaultCommands) {
2742
5420
  return;
2743
5421
  case "resume": {
2744
5422
  const beforeId = takeModeFlags(rest);
2745
- const [id, ...afterId] = beforeId.rest;
5423
+ const [id, ...afterId] = clientFirst(beforeId.rest);
2746
5424
  const afterIdFlags = takeModeFlags(afterId);
2747
5425
  const strict = afterIdFlags.strict ?? beforeId.strict ?? leading.strict;
2748
5426
  if (!id) {
2749
- commands.print("Usage: maesttro resume <session-id> [--strict|--no-strict] [client] [args...]");
5427
+ commands.print("Usage: maesttro resume [client] <session-id> [--strict|--no-strict] [client] [args...]");
2750
5428
  return;
2751
5429
  }
2752
5430
  await commands.launch({