maesttro 0.5.0 → 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/README.md +15 -9
- package/dist/maesttro.js +2048 -640
- package/docker/Dockerfile +4 -8
- package/docker/entrypoint.sh +6 -3
- package/package.json +1 -1
package/dist/maesttro.js
CHANGED
|
@@ -94,14 +94,53 @@ var require_src = __commonJS(function(exports, module) {
|
|
|
94
94
|
// src/credential-store.ts
|
|
95
95
|
import {
|
|
96
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,
|
|
97
108
|
mkdirSync,
|
|
98
109
|
readFileSync,
|
|
99
110
|
renameSync,
|
|
100
|
-
rmSync,
|
|
101
111
|
writeFileSync
|
|
102
112
|
} from "node:fs";
|
|
103
113
|
import { homedir } from "node:os";
|
|
104
|
-
import {
|
|
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
|
+
}
|
|
105
144
|
|
|
106
145
|
// src/run-command.ts
|
|
107
146
|
import { spawnSync } from "node:child_process";
|
|
@@ -120,98 +159,1375 @@ function commandExists(run, command) {
|
|
|
120
159
|
return run("sh", ["-c", `command -v ${command}`]).status === 0;
|
|
121
160
|
}
|
|
122
161
|
|
|
123
|
-
// src/credential-store.ts
|
|
124
|
-
var SERVICE = "maesttro";
|
|
125
|
-
var
|
|
126
|
-
|
|
127
|
-
|
|
162
|
+
// src/credential-store.ts
|
|
163
|
+
var SERVICE = "maesttro";
|
|
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");
|
|
170
|
+
}
|
|
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
|
+
};
|
|
180
|
+
}
|
|
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);
|
|
188
|
+
return {
|
|
189
|
+
kind: "macOS keychain",
|
|
190
|
+
load: () => {
|
|
191
|
+
const result = run("security", [
|
|
192
|
+
"find-generic-password",
|
|
193
|
+
...identity,
|
|
194
|
+
"-w"
|
|
195
|
+
]);
|
|
196
|
+
return result.status === 0 ? decode(result.stdout) : undefined;
|
|
197
|
+
},
|
|
198
|
+
save: (value) => {
|
|
199
|
+
const command = `add-generic-password -U ${identity.join(" ")} -w ${encode(value)}
|
|
200
|
+
`;
|
|
201
|
+
const result = run("security", ["-i"], command);
|
|
202
|
+
if (result.status !== 0)
|
|
203
|
+
throw new Error("Could not write to keychain");
|
|
204
|
+
},
|
|
205
|
+
clear: () => {
|
|
206
|
+
run("security", ["delete-generic-password", ...identity]);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
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;
|
|
128
1101
|
}
|
|
129
|
-
function
|
|
1102
|
+
function readSessionCredentials({
|
|
1103
|
+
claudeDir
|
|
1104
|
+
}) {
|
|
130
1105
|
try {
|
|
131
|
-
|
|
132
|
-
return typeof token.access_token === "string" ? token : undefined;
|
|
1106
|
+
return withFd(openSync(credentialsPath(claudeDir), READ_NO_FOLLOW_NO_BLOCK), (fd) => fstatSync(fd).isFile() ? JSON.parse(readFileSync6(fd, "utf8")) : undefined);
|
|
133
1107
|
} catch {
|
|
134
1108
|
return;
|
|
135
1109
|
}
|
|
136
1110
|
}
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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);
|
|
158
1205
|
}
|
|
159
|
-
}
|
|
1206
|
+
} catch {}
|
|
160
1207
|
}
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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;
|
|
173
1368
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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)}`);
|
|
177
1426
|
}
|
|
178
|
-
}
|
|
1427
|
+
}
|
|
179
1428
|
}
|
|
180
|
-
function
|
|
1429
|
+
function announcingOnce(announce, alreadySaid) {
|
|
1430
|
+
let last = alreadySaid;
|
|
181
1431
|
return {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
return decode(readFileSync(path, "utf8"));
|
|
186
|
-
} catch {
|
|
1432
|
+
say: (message) => {
|
|
1433
|
+
if (message === last)
|
|
187
1434
|
return;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
save: (token) => {
|
|
191
|
-
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
192
|
-
const temporary = `${path}.tmp`;
|
|
193
|
-
writeFileSync(temporary, encode(token), { mode: 384 });
|
|
194
|
-
chmodSync(temporary, 384);
|
|
195
|
-
renameSync(temporary, path);
|
|
1435
|
+
last = message;
|
|
1436
|
+
announce(message);
|
|
196
1437
|
},
|
|
197
|
-
|
|
198
|
-
|
|
1438
|
+
reset: () => {
|
|
1439
|
+
last = undefined;
|
|
199
1440
|
}
|
|
200
1441
|
};
|
|
201
1442
|
}
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
});
|
|
1511
|
+
},
|
|
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));
|
|
1522
|
+
}
|
|
1523
|
+
}, SESSION_SYNC_MS);
|
|
1524
|
+
return {
|
|
1525
|
+
stop: () => {
|
|
1526
|
+
timers.clearInterval(sync);
|
|
1527
|
+
return refresher.stop();
|
|
1528
|
+
},
|
|
1529
|
+
busy: refresher.busy
|
|
1530
|
+
};
|
|
215
1531
|
}
|
|
216
1532
|
|
|
217
1533
|
// src/github-api.ts
|
|
@@ -403,7 +1719,7 @@ function loginDependencies() {
|
|
|
403
1719
|
function describeExpiry(iso) {
|
|
404
1720
|
return iso ? new Date(iso).toLocaleString() : "never";
|
|
405
1721
|
}
|
|
406
|
-
async function
|
|
1722
|
+
async function loginGitHub() {
|
|
407
1723
|
const dependencies = loginDependencies();
|
|
408
1724
|
const token = await deviceLogin(dependencies);
|
|
409
1725
|
const name = await githubLogin(token.access_token);
|
|
@@ -413,139 +1729,213 @@ async function ensureLogin() {
|
|
|
413
1729
|
const dependencies = loginDependencies();
|
|
414
1730
|
const token = await currentUserToken({ ...dependencies, interactive: true });
|
|
415
1731
|
const name = token && await githubLogin(token.access_token);
|
|
416
|
-
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.");
|
|
417
1733
|
}
|
|
418
|
-
function
|
|
1734
|
+
function logoutGitHub() {
|
|
419
1735
|
const { store } = loginDependencies();
|
|
420
1736
|
store.clear();
|
|
421
1737
|
console.log(`Logged out. Removed GitHub credentials from ${store.kind}.`);
|
|
422
1738
|
}
|
|
423
|
-
async function
|
|
424
|
-
const dependencies = loginDependencies();
|
|
425
|
-
const token = await currentUserToken({ ...dependencies, interactive: false });
|
|
426
|
-
if (!token) {
|
|
427
|
-
console.log("Not logged in to GitHub. Run `maesttro login`.");
|
|
1739
|
+
async function statusGitHub() {
|
|
1740
|
+
const dependencies = loginDependencies();
|
|
1741
|
+
const token = await currentUserToken({ ...dependencies, interactive: false });
|
|
1742
|
+
if (!token) {
|
|
1743
|
+
console.log("Not logged in to GitHub. Run `maesttro login github`.");
|
|
1744
|
+
process.exitCode = 1;
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
const name = await githubLogin(token.access_token);
|
|
1748
|
+
console.log([
|
|
1749
|
+
`GitHub: ${name ? `logged in as ${name}` : "token rejected — run `maesttro login github`"}`,
|
|
1750
|
+
`Store: ${dependencies.store.kind}`,
|
|
1751
|
+
`Access token expires: ${describeExpiry(token.expires_at)}`,
|
|
1752
|
+
`Refresh token expires: ${describeExpiry(token.refresh_expires_at)}`
|
|
1753
|
+
].join(`
|
|
1754
|
+
`));
|
|
1755
|
+
if (!name)
|
|
1756
|
+
process.exitCode = 1;
|
|
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())
|
|
428
1817
|
process.exitCode = 1;
|
|
1818
|
+
}
|
|
1819
|
+
function logout({ args }) {
|
|
1820
|
+
const targets = parseLoginTargets(args);
|
|
1821
|
+
if (!targets) {
|
|
1822
|
+
unknownTarget("logout");
|
|
429
1823
|
return;
|
|
430
1824
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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(`
|
|
438
1846
|
`));
|
|
439
|
-
|
|
440
|
-
|
|
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);
|
|
441
1870
|
}
|
|
442
1871
|
|
|
443
1872
|
// src/configure.ts
|
|
444
1873
|
import {
|
|
445
1874
|
cpSync as cpSync2,
|
|
446
|
-
existsSync as
|
|
447
|
-
mkdirSync as
|
|
448
|
-
readdirSync,
|
|
449
|
-
readFileSync as
|
|
450
|
-
writeFileSync as
|
|
1875
|
+
existsSync as existsSync3,
|
|
1876
|
+
mkdirSync as mkdirSync9,
|
|
1877
|
+
readdirSync as readdirSync3,
|
|
1878
|
+
readFileSync as readFileSync10,
|
|
1879
|
+
writeFileSync as writeFileSync9
|
|
451
1880
|
} from "node:fs";
|
|
452
1881
|
import { homedir as homedir2 } from "node:os";
|
|
453
|
-
import { dirname as
|
|
1882
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
454
1883
|
|
|
455
|
-
// src/
|
|
456
|
-
import {
|
|
457
|
-
|
|
458
|
-
cpSync,
|
|
459
|
-
existsSync,
|
|
460
|
-
mkdirSync as mkdirSync3,
|
|
461
|
-
mkdtempSync,
|
|
462
|
-
readFileSync as readFileSync3,
|
|
463
|
-
rmSync as rmSync2,
|
|
464
|
-
writeFileSync as writeFileSync3
|
|
465
|
-
} from "node:fs";
|
|
466
|
-
import { tmpdir } from "node:os";
|
|
467
|
-
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";
|
|
468
1887
|
|
|
469
|
-
// src/
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
};
|
|
482
1906
|
}
|
|
483
|
-
function
|
|
1907
|
+
function installedClaudeVersion(root = "/usr/local/lib/node_modules/@anthropic-ai/claude-code") {
|
|
484
1908
|
try {
|
|
485
|
-
|
|
486
|
-
return isObject(parsed) ? parsed : {};
|
|
1909
|
+
return JSON.parse(readFileSync8(join8(root, "package.json"), "utf8")).version;
|
|
487
1910
|
} catch {
|
|
488
|
-
return
|
|
1911
|
+
return;
|
|
489
1912
|
}
|
|
490
1913
|
}
|
|
491
|
-
function
|
|
492
|
-
|
|
493
|
-
|
|
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)}
|
|
494
1921
|
`);
|
|
495
|
-
}
|
|
496
|
-
function skipInsignificant(text, from) {
|
|
497
|
-
let index = from;
|
|
498
|
-
while (index < text.length) {
|
|
499
|
-
if (/\s/.test(text[index] ?? "")) {
|
|
500
|
-
index++;
|
|
501
|
-
} else if (text.startsWith("//", index)) {
|
|
502
|
-
while (index < text.length && text[index] !== `
|
|
503
|
-
`)
|
|
504
|
-
index++;
|
|
505
|
-
} else if (text.startsWith("/*", index)) {
|
|
506
|
-
const end = text.indexOf("*/", index + 2);
|
|
507
|
-
index = end === -1 ? text.length : end + 2;
|
|
508
|
-
} else {
|
|
509
|
-
return index;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return index;
|
|
513
|
-
}
|
|
514
|
-
function stripJsonc(text) {
|
|
515
|
-
let output = "";
|
|
516
|
-
let index = 0;
|
|
517
|
-
while (index < text.length) {
|
|
518
|
-
const char = text[index];
|
|
519
|
-
if (char === '"') {
|
|
520
|
-
const start = index;
|
|
521
|
-
index++;
|
|
522
|
-
while (index < text.length && text[index] !== '"') {
|
|
523
|
-
index += text[index] === "\\" ? 2 : 1;
|
|
524
|
-
}
|
|
525
|
-
index++;
|
|
526
|
-
output += text.slice(start, index);
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
const significant = skipInsignificant(text, index);
|
|
530
|
-
if (significant > index) {
|
|
531
|
-
output += " ";
|
|
532
|
-
index = significant;
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
const trailingComma = char === "," && /[}\]]/.test(text[skipInsignificant(text, index + 1)] ?? "");
|
|
536
|
-
if (!trailingComma)
|
|
537
|
-
output += char;
|
|
538
|
-
index++;
|
|
539
|
-
}
|
|
540
|
-
return output;
|
|
541
|
-
}
|
|
542
|
-
function parseJsonc(text) {
|
|
543
|
-
const parsed = JSON.parse(stripJsonc(text));
|
|
544
|
-
if (!isObject(parsed))
|
|
545
|
-
throw new Error("expected a JSON object");
|
|
546
|
-
return parsed;
|
|
1922
|
+
renameSync6(`${path}.tmp`, path);
|
|
547
1923
|
}
|
|
548
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";
|
|
1938
|
+
|
|
549
1939
|
// src/maesttro-endpoint.ts
|
|
550
1940
|
var DEFAULT_MAESTTRO_URL = "http://100.64.0.2";
|
|
551
1941
|
var ORCHESTRATOR_MODEL = "maesttro";
|
|
@@ -758,11 +2148,11 @@ function readHostFile({
|
|
|
758
2148
|
parse,
|
|
759
2149
|
warn
|
|
760
2150
|
}) {
|
|
761
|
-
const path =
|
|
762
|
-
if (!
|
|
2151
|
+
const path = join9(home, file);
|
|
2152
|
+
if (!existsSync2(path))
|
|
763
2153
|
return {};
|
|
764
2154
|
try {
|
|
765
|
-
return parse(
|
|
2155
|
+
return parse(readFileSync9(path, "utf8"));
|
|
766
2156
|
} catch {
|
|
767
2157
|
warn(`maesttro: ignoring your ~/${file}, it doesn't parse`);
|
|
768
2158
|
return {};
|
|
@@ -794,66 +2184,46 @@ function buildClientConfigs({
|
|
|
794
2184
|
removed: opencode.removed.map((path) => `opencode ${path}`)
|
|
795
2185
|
};
|
|
796
2186
|
}
|
|
797
|
-
function writeClientConfigDir(files, parent =
|
|
798
|
-
const root =
|
|
799
|
-
|
|
800
|
-
const dir =
|
|
2187
|
+
function writeClientConfigDir(files, parent = tmpdir2()) {
|
|
2188
|
+
const root = mkdtempSync2(join9(parent, "maesttro-clients-"));
|
|
2189
|
+
chmodSync4(root, 448);
|
|
2190
|
+
const dir = join9(root, "home");
|
|
801
2191
|
for (const [file, content] of Object.entries(files)) {
|
|
802
|
-
const path =
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
2192
|
+
const path = join9(dir, file);
|
|
2193
|
+
mkdirSync8(dirname5(path), { recursive: true, mode: 493 });
|
|
2194
|
+
writeFileSync8(path, content);
|
|
2195
|
+
chmodSync4(path, 420);
|
|
806
2196
|
}
|
|
807
|
-
|
|
808
|
-
return { dir, remove: () =>
|
|
2197
|
+
chmodSync4(dir, 493);
|
|
2198
|
+
return { dir, remove: () => rmSync6(root, { recursive: true, force: true }) };
|
|
809
2199
|
}
|
|
810
2200
|
function installClientConfigs({
|
|
811
2201
|
home,
|
|
812
2202
|
source = CONTAINER_CLIENT_CONFIGS
|
|
813
2203
|
}) {
|
|
814
|
-
if (!
|
|
2204
|
+
if (!existsSync2(source))
|
|
815
2205
|
return false;
|
|
816
|
-
const merged = MERGED_INTO_HOME.map((file) =>
|
|
2206
|
+
const merged = MERGED_INTO_HOME.map((file) => join9(source, file));
|
|
817
2207
|
cpSync(source, home, {
|
|
818
2208
|
recursive: true,
|
|
819
2209
|
force: true,
|
|
820
2210
|
filter: (path) => !merged.includes(path)
|
|
821
2211
|
});
|
|
822
2212
|
for (const file of MERGED_INTO_HOME) {
|
|
823
|
-
const staged =
|
|
824
|
-
if (!
|
|
2213
|
+
const staged = join9(source, file);
|
|
2214
|
+
if (!existsSync2(staged))
|
|
825
2215
|
continue;
|
|
826
|
-
const target =
|
|
2216
|
+
const target = join9(home, file);
|
|
827
2217
|
writeJson(target, mergeJson(readJson(target), readJson(staged)));
|
|
828
2218
|
}
|
|
829
2219
|
return true;
|
|
830
2220
|
}
|
|
831
2221
|
function injectOpencodeTuiState({ home }) {
|
|
832
|
-
const path =
|
|
2222
|
+
const path = join9(home, OPENCODE_TUI_STATE);
|
|
833
2223
|
writeJson(path, mergeJson(readJson(path), CURATED_OPENCODE_TUI_STATE));
|
|
834
2224
|
return path;
|
|
835
2225
|
}
|
|
836
2226
|
|
|
837
|
-
// src/clients.ts
|
|
838
|
-
var CLIENTS = {
|
|
839
|
-
claude: {
|
|
840
|
-
label: "Claude Code",
|
|
841
|
-
npmPackage: "@anthropic-ai/claude-code",
|
|
842
|
-
binary: "claude",
|
|
843
|
-
configDir: "/home/agent/.claude"
|
|
844
|
-
},
|
|
845
|
-
opencode: {
|
|
846
|
-
label: "opencode",
|
|
847
|
-
npmPackage: "opencode-ai",
|
|
848
|
-
binary: "opencode",
|
|
849
|
-
configDir: "/home/agent/.config/opencode"
|
|
850
|
-
}
|
|
851
|
-
};
|
|
852
|
-
var DEFAULT_CLIENT = "opencode";
|
|
853
|
-
function isClientKey(value) {
|
|
854
|
-
return Object.hasOwn(CLIENTS, value);
|
|
855
|
-
}
|
|
856
|
-
|
|
857
2227
|
// src/host-instructions.ts
|
|
858
2228
|
var HOST_STAGING_DIR = "/opt/maesttro-host";
|
|
859
2229
|
var STAGED_DIRECTORIES = [
|
|
@@ -867,14 +2237,6 @@ var INSTRUCTION_FILES = [
|
|
|
867
2237
|
];
|
|
868
2238
|
var STAGED_PATHS = [...INSTRUCTION_FILES, ...STAGED_DIRECTORIES];
|
|
869
2239
|
|
|
870
|
-
// src/paths.ts
|
|
871
|
-
import { dirname as dirname4, join as join3, resolve } from "node:path";
|
|
872
|
-
import { fileURLToPath } from "node:url";
|
|
873
|
-
var PACKAGE_ROOT = resolve(dirname4(fileURLToPath(import.meta.url)), "..");
|
|
874
|
-
var DOCKERFILE_DIR = join3(PACKAGE_ROOT, "docker");
|
|
875
|
-
var IMAGE = "maesttro-runner:latest";
|
|
876
|
-
var VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
|
|
877
|
-
|
|
878
2240
|
// src/runner-context.ts
|
|
879
2241
|
function githubMode(value) {
|
|
880
2242
|
return value === "on" || value === "strict" ? value : "off";
|
|
@@ -927,7 +2289,7 @@ function runnerInstructions(context) {
|
|
|
927
2289
|
"",
|
|
928
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).",
|
|
929
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.",
|
|
930
|
-
"- Installs last only until this container exits; install again when a new session needs them.
|
|
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.",
|
|
931
2293
|
"- System packages are unavailable: there is no root, `sudo` or `apt`. If a task needs one, tell the user which package and why.",
|
|
932
2294
|
"",
|
|
933
2295
|
"## Pushing",
|
|
@@ -954,24 +2316,24 @@ function withoutComments(value) {
|
|
|
954
2316
|
}
|
|
955
2317
|
function applyProviderConfigs({
|
|
956
2318
|
home,
|
|
957
|
-
configsDir =
|
|
2319
|
+
configsDir = join10(DOCKERFILE_DIR, "configs")
|
|
958
2320
|
}) {
|
|
959
2321
|
const written = [];
|
|
960
2322
|
for (const [key, client] of Object.entries(CLIENTS)) {
|
|
961
|
-
const source =
|
|
962
|
-
if (!
|
|
2323
|
+
const source = join10(configsDir, key);
|
|
2324
|
+
if (!existsSync3(source))
|
|
963
2325
|
continue;
|
|
964
|
-
const targetDir =
|
|
965
|
-
for (const name of
|
|
2326
|
+
const targetDir = join10(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
|
|
2327
|
+
for (const name of readdirSync3(source)) {
|
|
966
2328
|
if (!name.endsWith(".json"))
|
|
967
2329
|
continue;
|
|
968
|
-
const curated = withoutComments(readJson(
|
|
2330
|
+
const curated = withoutComments(readJson(join10(source, name)));
|
|
969
2331
|
if (Object.keys(curated).length === 0)
|
|
970
2332
|
continue;
|
|
971
|
-
const target =
|
|
972
|
-
|
|
2333
|
+
const target = join10(targetDir, name);
|
|
2334
|
+
mkdirSync9(targetDir, { recursive: true });
|
|
973
2335
|
const merged = mergeJson(readJson(target), curated);
|
|
974
|
-
|
|
2336
|
+
writeFileSync9(target, `${JSON.stringify(merged, null, 2)}
|
|
975
2337
|
`);
|
|
976
2338
|
written.push(target);
|
|
977
2339
|
}
|
|
@@ -984,27 +2346,27 @@ function stageHostInstructions({
|
|
|
984
2346
|
context
|
|
985
2347
|
}) {
|
|
986
2348
|
for (const directory of STAGED_DIRECTORIES) {
|
|
987
|
-
const source =
|
|
988
|
-
if (!
|
|
2349
|
+
const source = join10(staging, directory);
|
|
2350
|
+
if (!existsSync3(source))
|
|
989
2351
|
continue;
|
|
990
2352
|
try {
|
|
991
|
-
cpSync2(source,
|
|
2353
|
+
cpSync2(source, join10(home, directory), { recursive: true, force: true });
|
|
992
2354
|
} catch {}
|
|
993
2355
|
}
|
|
994
2356
|
const instructions = runnerInstructions(context);
|
|
995
2357
|
for (const file of INSTRUCTION_FILES) {
|
|
996
|
-
const source =
|
|
997
|
-
const hostCopy =
|
|
998
|
-
const target =
|
|
999
|
-
|
|
1000
|
-
|
|
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()}
|
|
1001
2363
|
|
|
1002
2364
|
${instructions}` : instructions);
|
|
1003
2365
|
}
|
|
1004
2366
|
}
|
|
1005
2367
|
function configure({
|
|
1006
2368
|
home = homedir2(),
|
|
1007
|
-
inRunner =
|
|
2369
|
+
inRunner = existsSync3(RUNNER_MARKER),
|
|
1008
2370
|
env = process.env,
|
|
1009
2371
|
staging = HOST_STAGING_DIR,
|
|
1010
2372
|
clientConfigs
|
|
@@ -1013,6 +2375,7 @@ function configure({
|
|
|
1013
2375
|
if (!inRunner)
|
|
1014
2376
|
return;
|
|
1015
2377
|
installClientConfigs({ home, source: clientConfigs });
|
|
2378
|
+
seedClaudeState({ home });
|
|
1016
2379
|
injectOpencodeTuiState({ home });
|
|
1017
2380
|
stageHostInstructions({
|
|
1018
2381
|
home,
|
|
@@ -1095,52 +2458,6 @@ async function revokeSessionToken(token, fetchImpl = fetch, apiUrl = githubApiUr
|
|
|
1095
2458
|
} catch {}
|
|
1096
2459
|
}
|
|
1097
2460
|
|
|
1098
|
-
// src/host-identity.ts
|
|
1099
|
-
function hostIdentityEnv() {
|
|
1100
|
-
if (typeof process.getuid !== "function" || typeof process.getgid !== "function") {
|
|
1101
|
-
return {};
|
|
1102
|
-
}
|
|
1103
|
-
return {
|
|
1104
|
-
MAESTTRO_HOST_UID: String(process.getuid()),
|
|
1105
|
-
MAESTTRO_HOST_GID: String(process.getgid())
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
function hostIdentityFlags() {
|
|
1109
|
-
return Object.entries(hostIdentityEnv()).map(([name, value]) => `-e ${name}=${value}`).join(" ");
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
// src/legacy-token.ts
|
|
1113
|
-
import { execSync } from "node:child_process";
|
|
1114
|
-
var LEGACY_HOSTS = "~/.config/gh/hosts.yml";
|
|
1115
|
-
var PURGE_LEGACY_TOKEN = [
|
|
1116
|
-
`if [ -e ${LEGACY_HOSTS} ]; then rm -f ${LEGACY_HOSTS}; echo FOUND; fi`,
|
|
1117
|
-
"git config --global --remove-section credential.https://github.com 2>/dev/null || true",
|
|
1118
|
-
"git config --global --remove-section credential.https://gist.github.com 2>/dev/null || true"
|
|
1119
|
-
].join("; ");
|
|
1120
|
-
var DETECT_LEGACY_TOKEN = `if [ -e ${LEGACY_HOSTS} ]; then echo FOUND; else echo CLEAN; fi`;
|
|
1121
|
-
function inVolume(command) {
|
|
1122
|
-
return execSync(`docker run --rm ${hostIdentityFlags()} -v ${VOLUME}:/home/agent ${IMAGE} "${command}"`).toString();
|
|
1123
|
-
}
|
|
1124
|
-
function purgeLegacyToken() {
|
|
1125
|
-
return inVolume(PURGE_LEGACY_TOKEN).includes("FOUND");
|
|
1126
|
-
}
|
|
1127
|
-
function hasLegacyToken() {
|
|
1128
|
-
return inVolume(DETECT_LEGACY_TOKEN).includes("FOUND");
|
|
1129
|
-
}
|
|
1130
|
-
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";
|
|
1131
|
-
function purgeLegacyTokenOnce({
|
|
1132
|
-
config,
|
|
1133
|
-
saveConfig,
|
|
1134
|
-
purge = purgeLegacyToken,
|
|
1135
|
-
announce
|
|
1136
|
-
}) {
|
|
1137
|
-
if (config.legacyTokenPurged)
|
|
1138
|
-
return;
|
|
1139
|
-
if (purge())
|
|
1140
|
-
announce(REVOKE_NOTICE);
|
|
1141
|
-
saveConfig({ ...config, legacyTokenPurged: true });
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
2461
|
// src/network-probe.ts
|
|
1145
2462
|
import { connect } from "node:net";
|
|
1146
2463
|
async function reachable(host, port, {
|
|
@@ -1166,8 +2483,8 @@ async function reachable(host, port, {
|
|
|
1166
2483
|
}
|
|
1167
2484
|
|
|
1168
2485
|
// src/repo.ts
|
|
1169
|
-
import { appendFileSync, mkdirSync as
|
|
1170
|
-
import { join as
|
|
2486
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync10, realpathSync } from "node:fs";
|
|
2487
|
+
import { join as join11 } from "node:path";
|
|
1171
2488
|
var MINIMUM_GIT = [2, 48];
|
|
1172
2489
|
var WORKTREES_DIR = ".worktrees";
|
|
1173
2490
|
|
|
@@ -1210,7 +2527,7 @@ function repoRoot(run, cwd) {
|
|
|
1210
2527
|
"--git-common-dir"
|
|
1211
2528
|
]).out;
|
|
1212
2529
|
const root = realpathSync(top.out);
|
|
1213
|
-
if (realpathSync(commonDir) !==
|
|
2530
|
+
if (realpathSync(commonDir) !== join11(root, ".git")) {
|
|
1214
2531
|
throw new LaunchError("This is a git worktree. Run maesttro from the main checkout; each session gets its own isolated clone.");
|
|
1215
2532
|
}
|
|
1216
2533
|
if (realpathSync(cwd) !== root) {
|
|
@@ -1236,57 +2553,144 @@ function defaultBranch(run, root) {
|
|
|
1236
2553
|
}
|
|
1237
2554
|
throw new LaunchError("Can't find the default branch of origin. Run `git remote set-head origin --auto`.");
|
|
1238
2555
|
}
|
|
1239
|
-
function ensureWorktreesDir(run, root) {
|
|
1240
|
-
|
|
1241
|
-
if (git(run, root, ["check-ignore", "-q", `${WORKTREES_DIR}/`]).ok)
|
|
2556
|
+
function ensureWorktreesDir(run, root) {
|
|
2557
|
+
mkdirSync10(join11(root, WORKTREES_DIR), { recursive: true });
|
|
2558
|
+
if (git(run, root, ["check-ignore", "-q", `${WORKTREES_DIR}/`]).ok)
|
|
2559
|
+
return;
|
|
2560
|
+
const info = join11(root, ".git", "info");
|
|
2561
|
+
mkdirSync10(info, { recursive: true });
|
|
2562
|
+
appendFileSync2(join11(info, "exclude"), `
|
|
2563
|
+
${WORKTREES_DIR}/
|
|
2564
|
+
`);
|
|
2565
|
+
}
|
|
2566
|
+
function snapshotRefs(run, root) {
|
|
2567
|
+
const listed = git(run, root, [
|
|
2568
|
+
"for-each-ref",
|
|
2569
|
+
"--format=%(refname) %(objectname)",
|
|
2570
|
+
"refs/heads",
|
|
2571
|
+
"refs/tags"
|
|
2572
|
+
]);
|
|
2573
|
+
const refs = {};
|
|
2574
|
+
for (const line of listed.out.split(`
|
|
2575
|
+
`)) {
|
|
2576
|
+
const [name, sha] = line.split(" ");
|
|
2577
|
+
if (name && sha)
|
|
2578
|
+
refs[name] = sha;
|
|
2579
|
+
}
|
|
2580
|
+
return refs;
|
|
2581
|
+
}
|
|
2582
|
+
function revision(run, root, rev) {
|
|
2583
|
+
const parsed = git(run, root, [
|
|
2584
|
+
"rev-parse",
|
|
2585
|
+
"--verify",
|
|
2586
|
+
"--quiet",
|
|
2587
|
+
`${rev}^{commit}`
|
|
2588
|
+
]);
|
|
2589
|
+
return parsed.ok ? parsed.out : undefined;
|
|
2590
|
+
}
|
|
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.`);
|
|
1242
2669
|
return;
|
|
1243
|
-
const info = join5(root, ".git", "info");
|
|
1244
|
-
mkdirSync5(info, { recursive: true });
|
|
1245
|
-
appendFileSync(join5(info, "exclude"), `
|
|
1246
|
-
${WORKTREES_DIR}/
|
|
1247
|
-
`);
|
|
1248
|
-
}
|
|
1249
|
-
function snapshotRefs(run, root) {
|
|
1250
|
-
const listed = git(run, root, [
|
|
1251
|
-
"for-each-ref",
|
|
1252
|
-
"--format=%(refname) %(objectname)",
|
|
1253
|
-
"refs/heads",
|
|
1254
|
-
"refs/tags"
|
|
1255
|
-
]);
|
|
1256
|
-
const refs = {};
|
|
1257
|
-
for (const line of listed.out.split(`
|
|
1258
|
-
`)) {
|
|
1259
|
-
const [name, sha] = line.split(" ");
|
|
1260
|
-
if (name && sha)
|
|
1261
|
-
refs[name] = sha;
|
|
1262
2670
|
}
|
|
1263
|
-
|
|
1264
|
-
}
|
|
1265
|
-
function revision(run, root, rev) {
|
|
1266
|
-
const parsed = git(run, root, [
|
|
1267
|
-
"rev-parse",
|
|
1268
|
-
"--verify",
|
|
1269
|
-
"--quiet",
|
|
1270
|
-
`${rev}^{commit}`
|
|
1271
|
-
]);
|
|
1272
|
-
return parsed.ok ? parsed.out : undefined;
|
|
2671
|
+
saveConfig({ ...config, volumeMigrated: true });
|
|
1273
2672
|
}
|
|
1274
2673
|
|
|
1275
2674
|
// src/doctor.ts
|
|
1276
|
-
|
|
2675
|
+
class DoctorWarning extends Error {
|
|
2676
|
+
}
|
|
2677
|
+
async function check(label, probe, print = console.log) {
|
|
1277
2678
|
try {
|
|
1278
2679
|
const detail = await probe();
|
|
1279
|
-
|
|
2680
|
+
print(` [ok] ${label}${detail ? ` — ${detail}` : ""}`);
|
|
1280
2681
|
return true;
|
|
1281
2682
|
} catch (error) {
|
|
2683
|
+
if (error instanceof DoctorWarning) {
|
|
2684
|
+
print(` [warn] ${label} — ${error.message}`);
|
|
2685
|
+
return true;
|
|
2686
|
+
}
|
|
1282
2687
|
const reason = error instanceof Error && error.message ? error.message : "";
|
|
1283
|
-
|
|
2688
|
+
print(` [fail] ${label}${reason ? ` — ${reason}` : ""}`);
|
|
1284
2689
|
return false;
|
|
1285
2690
|
}
|
|
1286
2691
|
}
|
|
1287
|
-
function inRunner(command
|
|
1288
|
-
|
|
1289
|
-
return execSync2(`docker run --rm ${hostIdentityFlags()} ${volume}${IMAGE} "${command}"`).toString();
|
|
2692
|
+
function inRunner(command) {
|
|
2693
|
+
return execSync2(`docker run --rm ${hostIdentityFlags()} ${IMAGE} "${command}"`).toString();
|
|
1290
2694
|
}
|
|
1291
2695
|
async function doctor() {
|
|
1292
2696
|
console.log(`== maesttro doctor ==
|
|
@@ -1310,19 +2714,6 @@ async function doctor() {
|
|
|
1310
2714
|
"Runner image built",
|
|
1311
2715
|
() => `built ${execSync2(`docker image inspect ${IMAGE} --format "{{.Created}}"`).toString().trim()}`
|
|
1312
2716
|
],
|
|
1313
|
-
[
|
|
1314
|
-
"Persistent volume exists",
|
|
1315
|
-
() => {
|
|
1316
|
-
execSync2(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
|
|
1317
|
-
}
|
|
1318
|
-
],
|
|
1319
|
-
[
|
|
1320
|
-
"No long-lived GitHub token in the volume",
|
|
1321
|
-
() => {
|
|
1322
|
-
if (hasLegacyToken())
|
|
1323
|
-
throw new Error("run `maesttro setup` to remove it");
|
|
1324
|
-
}
|
|
1325
|
-
],
|
|
1326
2717
|
[
|
|
1327
2718
|
"Logged in to GitHub",
|
|
1328
2719
|
async () => {
|
|
@@ -1333,13 +2724,36 @@ async function doctor() {
|
|
|
1333
2724
|
interactive: false
|
|
1334
2725
|
});
|
|
1335
2726
|
if (!token)
|
|
1336
|
-
throw new Error("run `maesttro login`");
|
|
2727
|
+
throw new Error("run `maesttro login github`");
|
|
1337
2728
|
const name = await githubLogin(token.access_token);
|
|
1338
2729
|
if (!name)
|
|
1339
|
-
throw new Error("token rejected, run `maesttro login`");
|
|
2730
|
+
throw new Error("token rejected, run `maesttro login github`");
|
|
1340
2731
|
return name;
|
|
1341
2732
|
}
|
|
1342
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
|
+
],
|
|
1343
2757
|
[
|
|
1344
2758
|
"Token broker reachable",
|
|
1345
2759
|
async () => {
|
|
@@ -1371,7 +2785,7 @@ async function doctor() {
|
|
|
1371
2785
|
...Object.values(CLIENTS).map((client) => [
|
|
1372
2786
|
`Client available: ${client.label}`,
|
|
1373
2787
|
() => {
|
|
1374
|
-
inRunner(`which ${client.binary}
|
|
2788
|
+
inRunner(`which ${client.binary}`);
|
|
1375
2789
|
}
|
|
1376
2790
|
])
|
|
1377
2791
|
];
|
|
@@ -1395,7 +2809,9 @@ Usage:
|
|
|
1395
2809
|
maesttro <client> Same, with a specific client — see \`maesttro list\`
|
|
1396
2810
|
maesttro --strict [client]
|
|
1397
2811
|
Same, in strict mode: no GitHub token reaches the runner, so \`gh\` is off
|
|
1398
|
-
maesttro resume <id>
|
|
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
|
|
1399
2815
|
(\`--strict\` or \`--no-strict\` after the id changes it)
|
|
1400
2816
|
maesttro sessions List this repo's sessions
|
|
1401
2817
|
maesttro sessions remove <id>
|
|
@@ -1406,74 +2822,35 @@ Usage:
|
|
|
1406
2822
|
maesttro push [branch] [--force]
|
|
1407
2823
|
Push a branch a session created, after showing what goes out
|
|
1408
2824
|
maesttro list Show available clients
|
|
1409
|
-
maesttro setup One-time: build image,
|
|
1410
|
-
maesttro login
|
|
1411
|
-
|
|
1412
|
-
maesttro
|
|
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
|
|
1413
2832
|
maesttro configure Apply maesttro's client configs (also runs inside the runner)
|
|
1414
2833
|
maesttro update Update the package and rebuild the runner image with refreshed client configs
|
|
1415
|
-
maesttro doctor Health check — Docker, image,
|
|
2834
|
+
maesttro doctor Health check — Docker, image, GitHub and Claude logins, old volume gone, broker, isolation, clients
|
|
1416
2835
|
maesttro help Show this message
|
|
1417
2836
|
`;
|
|
1418
2837
|
|
|
1419
2838
|
// src/launch.ts
|
|
1420
|
-
import { execSync as execSync3, spawn, spawnSync as
|
|
1421
|
-
import { existsSync as
|
|
1422
|
-
import { constants, homedir as
|
|
1423
|
-
import { join as
|
|
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";
|
|
1424
2843
|
|
|
1425
2844
|
// src/backup.ts
|
|
1426
2845
|
import { createHash } from "node:crypto";
|
|
1427
|
-
import { mkdirSync as
|
|
1428
|
-
import { basename, join as
|
|
1429
|
-
|
|
1430
|
-
// src/maesttro-home.ts
|
|
1431
|
-
import {
|
|
1432
|
-
appendFileSync as appendFileSync2,
|
|
1433
|
-
mkdirSync as mkdirSync6,
|
|
1434
|
-
readFileSync as readFileSync5,
|
|
1435
|
-
renameSync as renameSync2,
|
|
1436
|
-
writeFileSync as writeFileSync5
|
|
1437
|
-
} from "node:fs";
|
|
1438
|
-
import { homedir as homedir3 } from "node:os";
|
|
1439
|
-
import { join as join6 } from "node:path";
|
|
1440
|
-
function maesttroHome() {
|
|
1441
|
-
return join6(homedir3(), ".config", "maesttro");
|
|
1442
|
-
}
|
|
1443
|
-
function configPath() {
|
|
1444
|
-
return join6(maesttroHome(), "config.json");
|
|
1445
|
-
}
|
|
1446
|
-
function readMaesttroConfig() {
|
|
1447
|
-
try {
|
|
1448
|
-
return JSON.parse(readFileSync5(configPath(), "utf8"));
|
|
1449
|
-
} catch {
|
|
1450
|
-
return {};
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
function writeMaesttroConfig(config) {
|
|
1454
|
-
mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
|
|
1455
|
-
const temporary = `${configPath()}.tmp`;
|
|
1456
|
-
writeFileSync5(temporary, `${JSON.stringify(config, null, 2)}
|
|
1457
|
-
`);
|
|
1458
|
-
renameSync2(temporary, configPath());
|
|
1459
|
-
}
|
|
1460
|
-
function fileSessionLog(path = join6(maesttroHome(), "maesttro.log")) {
|
|
1461
|
-
return (event, detail) => {
|
|
1462
|
-
try {
|
|
1463
|
-
mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
|
|
1464
|
-
appendFileSync2(path, `${new Date().toISOString()} ${event}${detail ? ` ${detail}` : ""}
|
|
1465
|
-
`);
|
|
1466
|
-
} catch {}
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// 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";
|
|
1471
2848
|
var BACKUPS_KEPT = 5;
|
|
1472
2849
|
function shortHash(value) {
|
|
1473
2850
|
return createHash("sha256").update(value).digest("hex").slice(0, 8);
|
|
1474
2851
|
}
|
|
1475
2852
|
function backupDir(repoRoot, home = maesttroHome()) {
|
|
1476
|
-
return
|
|
2853
|
+
return join12(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
|
|
1477
2854
|
}
|
|
1478
2855
|
function refsFingerprint(refs) {
|
|
1479
2856
|
const sorted = Object.keys(refs).sort().map((ref) => `${ref} ${refs[ref]}`);
|
|
@@ -1491,119 +2868,41 @@ function backupRepo({
|
|
|
1491
2868
|
}) {
|
|
1492
2869
|
if (Object.keys(refs).length === 0)
|
|
1493
2870
|
return;
|
|
1494
|
-
|
|
2871
|
+
mkdirSync11(dir, { recursive: true, mode: 448 });
|
|
1495
2872
|
const fingerprint = refsFingerprint(refs);
|
|
1496
|
-
const existing =
|
|
2873
|
+
const existing = readdirSync4(dir).filter((name) => name.endsWith(".bundle")).sort();
|
|
1497
2874
|
const latest = existing.at(-1);
|
|
1498
2875
|
if (latest?.endsWith(`-${fingerprint}.bundle`)) {
|
|
1499
|
-
return { path:
|
|
2876
|
+
return { path: join12(dir, latest), reused: true };
|
|
1500
2877
|
}
|
|
1501
2878
|
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
1502
|
-
const path =
|
|
2879
|
+
const path = join12(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
|
|
1503
2880
|
if (!git(run, repoRoot, ["bundle", "create", "--quiet", path, "--all"]).ok) {
|
|
1504
2881
|
return;
|
|
1505
2882
|
}
|
|
1506
2883
|
for (const stale of [...existing, basename(path)].sort().slice(0, -keep)) {
|
|
1507
|
-
|
|
2884
|
+
rmSync7(join12(dir, stale), { force: true });
|
|
1508
2885
|
}
|
|
1509
2886
|
return { path, reused: false };
|
|
1510
2887
|
}
|
|
1511
2888
|
|
|
1512
|
-
// src/refresher.ts
|
|
1513
|
-
var REFRESH_LEAD_MS = 10 * 60 * 1000;
|
|
1514
|
-
var WALL_CLOCK_TICK_MS = 60 * 1000;
|
|
1515
|
-
var FIRST_BACKOFF_MS = 5 * 1000;
|
|
1516
|
-
var MAX_BACKOFF_MS = 5 * 60 * 1000;
|
|
1517
|
-
var nodeTimers = {
|
|
1518
|
-
setTimeout: (callback, ms) => setTimeout(callback, ms).unref(),
|
|
1519
|
-
clearTimeout: (handle) => clearTimeout(handle),
|
|
1520
|
-
setInterval: (callback, ms) => setInterval(callback, ms).unref(),
|
|
1521
|
-
clearInterval: (handle) => clearInterval(handle)
|
|
1522
|
-
};
|
|
1523
|
-
function startRefresher({
|
|
1524
|
-
initial,
|
|
1525
|
-
refresh,
|
|
1526
|
-
onRefreshed,
|
|
1527
|
-
onFailed,
|
|
1528
|
-
now = Date.now,
|
|
1529
|
-
timers = nodeTimers
|
|
1530
|
-
}) {
|
|
1531
|
-
let current = initial;
|
|
1532
|
-
let nextAttemptAt = 0;
|
|
1533
|
-
let backoffMs = FIRST_BACKOFF_MS;
|
|
1534
|
-
let inFlight;
|
|
1535
|
-
let stopped = false;
|
|
1536
|
-
let pending;
|
|
1537
|
-
function refreshDueAt(token) {
|
|
1538
|
-
const expiresAt = Date.parse(token.expires_at);
|
|
1539
|
-
const halfRemaining = (expiresAt - now()) / 2;
|
|
1540
|
-
return expiresAt - Math.min(REFRESH_LEAD_MS, Math.max(halfRemaining, 0));
|
|
1541
|
-
}
|
|
1542
|
-
nextAttemptAt = refreshDueAt(initial);
|
|
1543
|
-
function scheduleAt(at) {
|
|
1544
|
-
timers.clearTimeout(pending);
|
|
1545
|
-
pending = timers.setTimeout(check, Math.max(at - now(), 0));
|
|
1546
|
-
}
|
|
1547
|
-
async function attempt() {
|
|
1548
|
-
try {
|
|
1549
|
-
const token = await refresh();
|
|
1550
|
-
if (stopped)
|
|
1551
|
-
return;
|
|
1552
|
-
current = token;
|
|
1553
|
-
backoffMs = FIRST_BACKOFF_MS;
|
|
1554
|
-
nextAttemptAt = refreshDueAt(token);
|
|
1555
|
-
onRefreshed(token);
|
|
1556
|
-
} catch (error) {
|
|
1557
|
-
if (stopped)
|
|
1558
|
-
return;
|
|
1559
|
-
nextAttemptAt = now() + backoffMs;
|
|
1560
|
-
onFailed(error, backoffMs);
|
|
1561
|
-
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
1562
|
-
}
|
|
1563
|
-
scheduleAt(nextAttemptAt);
|
|
1564
|
-
}
|
|
1565
|
-
function check() {
|
|
1566
|
-
if (stopped)
|
|
1567
|
-
return Promise.resolve();
|
|
1568
|
-
if (inFlight)
|
|
1569
|
-
return inFlight;
|
|
1570
|
-
if (now() < nextAttemptAt)
|
|
1571
|
-
return Promise.resolve();
|
|
1572
|
-
inFlight = attempt().finally(() => {
|
|
1573
|
-
inFlight = undefined;
|
|
1574
|
-
});
|
|
1575
|
-
return inFlight;
|
|
1576
|
-
}
|
|
1577
|
-
const tick = timers.setInterval(check, WALL_CLOCK_TICK_MS);
|
|
1578
|
-
scheduleAt(nextAttemptAt);
|
|
1579
|
-
return {
|
|
1580
|
-
current: () => current,
|
|
1581
|
-
check,
|
|
1582
|
-
stop: () => {
|
|
1583
|
-
stopped = true;
|
|
1584
|
-
timers.clearTimeout(pending);
|
|
1585
|
-
timers.clearInterval(tick);
|
|
1586
|
-
}
|
|
1587
|
-
};
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
2889
|
// src/session-dir.ts
|
|
1591
2890
|
import {
|
|
1592
|
-
chmodSync as
|
|
1593
|
-
mkdirSync as
|
|
1594
|
-
mkdtempSync as
|
|
1595
|
-
renameSync as
|
|
1596
|
-
rmSync as
|
|
1597
|
-
writeFileSync as
|
|
2891
|
+
chmodSync as chmodSync5,
|
|
2892
|
+
mkdirSync as mkdirSync12,
|
|
2893
|
+
mkdtempSync as mkdtempSync3,
|
|
2894
|
+
renameSync as renameSync7,
|
|
2895
|
+
rmSync as rmSync8,
|
|
2896
|
+
writeFileSync as writeFileSync10
|
|
1598
2897
|
} from "node:fs";
|
|
1599
|
-
import { tmpdir as
|
|
1600
|
-
import { join as
|
|
2898
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
2899
|
+
import { join as join13 } from "node:path";
|
|
1601
2900
|
var CONTAINER_GH_CONFIG_DIR = "/home/agent/.config/gh-maesttro";
|
|
1602
2901
|
function writeAtomically(path, content) {
|
|
1603
2902
|
const temporary = `${path}.tmp`;
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
2903
|
+
writeFileSync10(temporary, content, { mode: 420 });
|
|
2904
|
+
chmodSync5(temporary, 420);
|
|
2905
|
+
renameSync7(temporary, path);
|
|
1607
2906
|
}
|
|
1608
2907
|
function hostsYaml(token, user) {
|
|
1609
2908
|
return [
|
|
@@ -1615,20 +2914,20 @@ function hostsYaml(token, user) {
|
|
|
1615
2914
|
].join(`
|
|
1616
2915
|
`);
|
|
1617
2916
|
}
|
|
1618
|
-
function createSessionDir(parent =
|
|
1619
|
-
const root =
|
|
1620
|
-
|
|
1621
|
-
const ghDir =
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
writeAtomically(
|
|
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"
|
|
1625
2924
|
`);
|
|
1626
2925
|
return {
|
|
1627
2926
|
ghDir,
|
|
1628
|
-
writeToken: (token, user) => writeAtomically(
|
|
1629
|
-
writeStatus: (status) => writeAtomically(
|
|
2927
|
+
writeToken: (token, user) => writeAtomically(join13(ghDir, "hosts.yml"), hostsYaml(token, user)),
|
|
2928
|
+
writeStatus: (status) => writeAtomically(join13(ghDir, "status"), `${status}
|
|
1630
2929
|
`),
|
|
1631
|
-
remove: () =>
|
|
2930
|
+
remove: () => rmSync8(root, { recursive: true, force: true })
|
|
1632
2931
|
};
|
|
1633
2932
|
}
|
|
1634
2933
|
|
|
@@ -1646,7 +2945,7 @@ function refusalNotice(code, repo) {
|
|
|
1646
2945
|
return `${UNAVAILABLE}: your GitHub account cannot see ${repo} — ${WITHOUT}`;
|
|
1647
2946
|
case "user_token_invalid":
|
|
1648
2947
|
case "not_logged_in":
|
|
1649
|
-
return `${UNAVAILABLE}: GitHub login expired, run \`maesttro login\` — ${WITHOUT}`;
|
|
2948
|
+
return `${UNAVAILABLE}: GitHub login expired, run \`maesttro login github\` — ${WITHOUT}`;
|
|
1650
2949
|
default:
|
|
1651
2950
|
return `${UNAVAILABLE} (${code}) — ${WITHOUT}`;
|
|
1652
2951
|
}
|
|
@@ -1733,6 +3032,7 @@ async function openGitHubSession({
|
|
|
1733
3032
|
log("session.token_minted", `${repo} expires ${initial.expires_at}`);
|
|
1734
3033
|
const refresher = startRefresher({
|
|
1735
3034
|
initial,
|
|
3035
|
+
expiresAtOf: (token) => Date.parse(token.expires_at),
|
|
1736
3036
|
refresh: () => mint({ interactive: false }),
|
|
1737
3037
|
onRefreshed: (token) => {
|
|
1738
3038
|
publish(token);
|
|
@@ -1779,14 +3079,14 @@ function ask(question) {
|
|
|
1779
3079
|
// src/session-clone.ts
|
|
1780
3080
|
import {
|
|
1781
3081
|
copyFileSync,
|
|
1782
|
-
existsSync as
|
|
1783
|
-
lstatSync,
|
|
1784
|
-
mkdirSync as
|
|
1785
|
-
readdirSync as
|
|
1786
|
-
readFileSync as
|
|
1787
|
-
writeFileSync as
|
|
3082
|
+
existsSync as existsSync4,
|
|
3083
|
+
lstatSync as lstatSync4,
|
|
3084
|
+
mkdirSync as mkdirSync13,
|
|
3085
|
+
readdirSync as readdirSync5,
|
|
3086
|
+
readFileSync as readFileSync11,
|
|
3087
|
+
writeFileSync as writeFileSync11
|
|
1788
3088
|
} from "node:fs";
|
|
1789
|
-
import { join as
|
|
3089
|
+
import { join as join14 } from "node:path";
|
|
1790
3090
|
|
|
1791
3091
|
// src/sanitize.ts
|
|
1792
3092
|
function stripControl(text) {
|
|
@@ -1873,27 +3173,27 @@ function createSessionClone(run, root, clonePath, baseSha) {
|
|
|
1873
3173
|
}
|
|
1874
3174
|
function realDir(path) {
|
|
1875
3175
|
try {
|
|
1876
|
-
return
|
|
3176
|
+
return lstatSync4(path).isDirectory();
|
|
1877
3177
|
} catch {
|
|
1878
3178
|
return false;
|
|
1879
3179
|
}
|
|
1880
3180
|
}
|
|
1881
3181
|
function dirents(dir) {
|
|
1882
3182
|
try {
|
|
1883
|
-
return
|
|
3183
|
+
return readdirSync5(dir, { withFileTypes: true });
|
|
1884
3184
|
} catch {
|
|
1885
3185
|
return [];
|
|
1886
3186
|
}
|
|
1887
3187
|
}
|
|
1888
3188
|
function cloneGitDir(clonePath) {
|
|
1889
|
-
const gitDir =
|
|
3189
|
+
const gitDir = join14(clonePath, ".git");
|
|
1890
3190
|
return realDir(gitDir) ? gitDir : undefined;
|
|
1891
3191
|
}
|
|
1892
3192
|
function readSha(path) {
|
|
1893
3193
|
try {
|
|
1894
|
-
if (!
|
|
3194
|
+
if (!lstatSync4(path).isFile())
|
|
1895
3195
|
return;
|
|
1896
|
-
const text =
|
|
3196
|
+
const text = readFileSync11(path, "utf8").trim();
|
|
1897
3197
|
return SHA.test(text) ? text : undefined;
|
|
1898
3198
|
} catch {
|
|
1899
3199
|
return;
|
|
@@ -1903,7 +3203,7 @@ function packedRefs(gitDir) {
|
|
|
1903
3203
|
const refs = new Map;
|
|
1904
3204
|
let text;
|
|
1905
3205
|
try {
|
|
1906
|
-
text =
|
|
3206
|
+
text = readFileSync11(join14(gitDir, "packed-refs"), "utf8");
|
|
1907
3207
|
} catch {
|
|
1908
3208
|
return refs;
|
|
1909
3209
|
}
|
|
@@ -1928,12 +3228,12 @@ function cloneBranches(clonePath) {
|
|
|
1928
3228
|
continue;
|
|
1929
3229
|
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1930
3230
|
if (entry.isDirectory())
|
|
1931
|
-
walk(
|
|
3231
|
+
walk(join14(dir, entry.name), name);
|
|
1932
3232
|
else if (entry.isFile())
|
|
1933
3233
|
names.add(name);
|
|
1934
3234
|
}
|
|
1935
3235
|
};
|
|
1936
|
-
walk(
|
|
3236
|
+
walk(join14(gitDir, "refs", "heads"), "");
|
|
1937
3237
|
for (const ref of packedRefs(gitDir).keys()) {
|
|
1938
3238
|
if (ref.startsWith(HEADS_PREFIX))
|
|
1939
3239
|
names.add(ref.slice(HEADS_PREFIX.length));
|
|
@@ -1946,7 +3246,7 @@ function branchTip(clonePath, branch) {
|
|
|
1946
3246
|
const gitDir = cloneGitDir(clonePath);
|
|
1947
3247
|
if (!gitDir)
|
|
1948
3248
|
return;
|
|
1949
|
-
return readSha(
|
|
3249
|
+
return readSha(join14(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
|
|
1950
3250
|
}
|
|
1951
3251
|
function cloneHead(clonePath) {
|
|
1952
3252
|
const gitDir = cloneGitDir(clonePath);
|
|
@@ -1954,9 +3254,9 @@ function cloneHead(clonePath) {
|
|
|
1954
3254
|
return;
|
|
1955
3255
|
let text;
|
|
1956
3256
|
try {
|
|
1957
|
-
if (!
|
|
3257
|
+
if (!lstatSync4(join14(gitDir, "HEAD")).isFile())
|
|
1958
3258
|
return;
|
|
1959
|
-
text =
|
|
3259
|
+
text = readFileSync11(join14(gitDir, "HEAD"), "utf8").trim();
|
|
1960
3260
|
} catch {
|
|
1961
3261
|
return;
|
|
1962
3262
|
}
|
|
@@ -1977,29 +3277,29 @@ function copyObjectFiles(fromDir, intoDir) {
|
|
|
1977
3277
|
for (const entry of dirents(fromDir)) {
|
|
1978
3278
|
if (!entry.isFile())
|
|
1979
3279
|
continue;
|
|
1980
|
-
const dest =
|
|
1981
|
-
if (
|
|
3280
|
+
const dest = join14(intoDir, entry.name);
|
|
3281
|
+
if (existsSync4(dest))
|
|
1982
3282
|
continue;
|
|
1983
3283
|
if (!created) {
|
|
1984
|
-
|
|
3284
|
+
mkdirSync13(intoDir, { recursive: true });
|
|
1985
3285
|
created = true;
|
|
1986
3286
|
}
|
|
1987
|
-
copyFileSync(
|
|
3287
|
+
copyFileSync(join14(fromDir, entry.name), dest);
|
|
1988
3288
|
}
|
|
1989
3289
|
}
|
|
1990
3290
|
function pinAlternates(clonePath, containerObjects) {
|
|
1991
3291
|
const gitDir = cloneGitDir(clonePath);
|
|
1992
3292
|
if (!gitDir)
|
|
1993
3293
|
return;
|
|
1994
|
-
const objects =
|
|
1995
|
-
const info =
|
|
3294
|
+
const objects = join14(gitDir, "objects");
|
|
3295
|
+
const info = join14(objects, "info");
|
|
1996
3296
|
if (!realDir(objects) || !realDir(info)) {
|
|
1997
3297
|
throw new LaunchError(`Unsafe objects tree in the session clone: ${clonePath}`);
|
|
1998
3298
|
}
|
|
1999
|
-
const alternates =
|
|
3299
|
+
const alternates = join14(info, "alternates");
|
|
2000
3300
|
let stat;
|
|
2001
3301
|
try {
|
|
2002
|
-
stat =
|
|
3302
|
+
stat = lstatSync4(alternates);
|
|
2003
3303
|
} catch {
|
|
2004
3304
|
return;
|
|
2005
3305
|
}
|
|
@@ -2008,73 +3308,29 @@ function pinAlternates(clonePath, containerObjects) {
|
|
|
2008
3308
|
}
|
|
2009
3309
|
const desired = `${containerObjects}
|
|
2010
3310
|
`;
|
|
2011
|
-
if (
|
|
2012
|
-
|
|
3311
|
+
if (readFileSync11(alternates, "utf8") !== desired) {
|
|
3312
|
+
writeFileSync11(alternates, desired);
|
|
2013
3313
|
}
|
|
2014
3314
|
}
|
|
2015
3315
|
function importObjects(run, root, clonePath, tip) {
|
|
2016
3316
|
const gitDir = cloneGitDir(clonePath);
|
|
2017
3317
|
if (!gitDir)
|
|
2018
3318
|
return false;
|
|
2019
|
-
const objects =
|
|
3319
|
+
const objects = join14(gitDir, "objects");
|
|
2020
3320
|
if (!realDir(objects))
|
|
2021
3321
|
return false;
|
|
2022
|
-
const into =
|
|
3322
|
+
const into = join14(root, ".git", "objects");
|
|
2023
3323
|
for (const entry of dirents(objects)) {
|
|
2024
3324
|
if (entry.name === "info" || entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2025
3325
|
continue;
|
|
2026
3326
|
}
|
|
2027
3327
|
if (!OBJECT_SUBDIR.test(entry.name) && entry.name !== "pack")
|
|
2028
3328
|
continue;
|
|
2029
|
-
copyObjectFiles(
|
|
3329
|
+
copyObjectFiles(join14(objects, entry.name), join14(into, entry.name));
|
|
2030
3330
|
}
|
|
2031
3331
|
return git(run, root, ["cat-file", "-e", `${tip}^{commit}`]).ok;
|
|
2032
3332
|
}
|
|
2033
3333
|
|
|
2034
|
-
// src/sessions.ts
|
|
2035
|
-
import { randomBytes } from "node:crypto";
|
|
2036
|
-
import {
|
|
2037
|
-
mkdirSync as mkdirSync10,
|
|
2038
|
-
readdirSync as readdirSync4,
|
|
2039
|
-
readFileSync as readFileSync7,
|
|
2040
|
-
renameSync as renameSync4,
|
|
2041
|
-
rmSync as rmSync5,
|
|
2042
|
-
writeFileSync as writeFileSync8
|
|
2043
|
-
} from "node:fs";
|
|
2044
|
-
import { join as join10 } from "node:path";
|
|
2045
|
-
function newSessionId() {
|
|
2046
|
-
return `maesttro-${randomBytes(4).toString("hex")}`;
|
|
2047
|
-
}
|
|
2048
|
-
function sessionsDir(home = maesttroHome()) {
|
|
2049
|
-
return join10(home, "sessions");
|
|
2050
|
-
}
|
|
2051
|
-
function saveSession(record, dir = sessionsDir()) {
|
|
2052
|
-
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
2053
|
-
const path = join10(dir, `${record.id}.json`);
|
|
2054
|
-
writeFileSync8(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
|
|
2055
|
-
`);
|
|
2056
|
-
renameSync4(`${path}.tmp`, path);
|
|
2057
|
-
}
|
|
2058
|
-
function readSession(id, dir = sessionsDir()) {
|
|
2059
|
-
try {
|
|
2060
|
-
return JSON.parse(readFileSync7(join10(dir, `${id}.json`), "utf8"));
|
|
2061
|
-
} catch {
|
|
2062
|
-
return;
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
function deleteSession(id, dir = sessionsDir()) {
|
|
2066
|
-
rmSync5(join10(dir, `${id}.json`), { force: true });
|
|
2067
|
-
}
|
|
2068
|
-
function sessionsForRepo(repoRoot, dir = sessionsDir()) {
|
|
2069
|
-
let names;
|
|
2070
|
-
try {
|
|
2071
|
-
names = readdirSync4(dir).filter((name) => name.endsWith(".json"));
|
|
2072
|
-
} catch {
|
|
2073
|
-
return [];
|
|
2074
|
-
}
|
|
2075
|
-
return names.map((name) => readSession(name.slice(0, -".json".length), dir)).filter((record) => record?.repoRoot === repoRoot).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
3334
|
// src/workspace-git.ts
|
|
2079
3335
|
var GITHUB_REMOTE = /^(?:https:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/;
|
|
2080
3336
|
function githubRepoFromRemote(remote) {
|
|
@@ -2102,9 +3358,18 @@ function workspaceAuthor(run, workspace) {
|
|
|
2102
3358
|
}
|
|
2103
3359
|
|
|
2104
3360
|
// src/launch.ts
|
|
2105
|
-
var CONTAINER_REPO =
|
|
3361
|
+
var CONTAINER_REPO = CONTAINER_REPO_PATH;
|
|
2106
3362
|
var CONTAINER_PACKAGE = "/opt/maesttro";
|
|
2107
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
|
+
}
|
|
2108
3373
|
function resolveCommand(args) {
|
|
2109
3374
|
const [first, ...rest] = args;
|
|
2110
3375
|
if (first === undefined)
|
|
@@ -2123,16 +3388,19 @@ function sessionMounts({
|
|
|
2123
3388
|
hostHome,
|
|
2124
3389
|
stagedPaths,
|
|
2125
3390
|
clientConfigs,
|
|
2126
|
-
sharedSkills
|
|
3391
|
+
sharedSkills,
|
|
3392
|
+
home,
|
|
3393
|
+
claudeDir
|
|
2127
3394
|
}) {
|
|
2128
|
-
const objects =
|
|
3395
|
+
const objects = join15(repoRoot, ".git", "objects");
|
|
2129
3396
|
const mounts = [
|
|
2130
|
-
{ host:
|
|
3397
|
+
{ host: home, container: "/home/agent" },
|
|
3398
|
+
{ host: claudeDir, container: "/home/agent/.claude" },
|
|
2131
3399
|
{ host: objects, container: CONTAINER_GIT_OBJECTS, readOnly: true },
|
|
2132
3400
|
{ host: sessionDir, container: CONTAINER_REPO },
|
|
2133
3401
|
{ host: packageRoot, container: CONTAINER_PACKAGE, readOnly: true },
|
|
2134
3402
|
...stagedPaths.map((path) => ({
|
|
2135
|
-
host:
|
|
3403
|
+
host: join15(hostHome, path),
|
|
2136
3404
|
container: `${HOST_STAGING_DIR}/${path}`,
|
|
2137
3405
|
readOnly: true
|
|
2138
3406
|
})),
|
|
@@ -2158,9 +3426,9 @@ function dockerRunArgs({
|
|
|
2158
3426
|
extra = [],
|
|
2159
3427
|
name
|
|
2160
3428
|
}) {
|
|
2161
|
-
const volumes = mounts.flatMap(({ host, container, readOnly
|
|
3429
|
+
const volumes = mounts.flatMap(({ host, container, readOnly }) => [
|
|
2162
3430
|
"--mount",
|
|
2163
|
-
`type
|
|
3431
|
+
`type=bind,source=${host},target=${container}${readOnly ? ",readonly" : ""}`
|
|
2164
3432
|
]);
|
|
2165
3433
|
return [
|
|
2166
3434
|
"run",
|
|
@@ -2189,17 +3457,54 @@ Default: ${DEFAULT_CLIENT}. Run \`maesttro <client>\` to pick one, or \`maesttro
|
|
|
2189
3457
|
function ensureReady() {
|
|
2190
3458
|
try {
|
|
2191
3459
|
execSync3(`docker image inspect ${IMAGE}`, { stdio: "ignore" });
|
|
2192
|
-
execSync3(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
|
|
2193
3460
|
} catch {
|
|
2194
3461
|
throw new LaunchError("Runner isn't set up yet. Run `maesttro setup` first.");
|
|
2195
3462
|
}
|
|
2196
3463
|
}
|
|
2197
3464
|
function clearStaleContainer(id) {
|
|
2198
|
-
|
|
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
|
+
}
|
|
2199
3504
|
}
|
|
2200
3505
|
function sharedSkillsDir() {
|
|
2201
|
-
const dir = process.env.AGENT_SHARED_SKILLS ??
|
|
2202
|
-
return
|
|
3506
|
+
const dir = process.env.AGENT_SHARED_SKILLS ?? join15(homedir3(), "agent-shared", "skills");
|
|
3507
|
+
return existsSync5(dir) ? dir : undefined;
|
|
2203
3508
|
}
|
|
2204
3509
|
async function resolveAuthor(workspace) {
|
|
2205
3510
|
const fromRepo = workspaceAuthor(runCommand, workspace);
|
|
@@ -2228,7 +3533,7 @@ function startSession(run, root) {
|
|
|
2228
3533
|
if (!baseSha)
|
|
2229
3534
|
throw new LaunchError(`Can't resolve ${base}.`);
|
|
2230
3535
|
const id = newSessionId();
|
|
2231
|
-
const sessionDir =
|
|
3536
|
+
const sessionDir = join15(root, WORKTREES_DIR, id);
|
|
2232
3537
|
createSessionClone(run, root, sessionDir, baseSha);
|
|
2233
3538
|
return {
|
|
2234
3539
|
id,
|
|
@@ -2245,7 +3550,7 @@ function resumeSession(root, id) {
|
|
|
2245
3550
|
if (!session || session.repoRoot !== root) {
|
|
2246
3551
|
throw new LaunchError(`No session ${id} in this repo. See \`maesttro sessions\`.`);
|
|
2247
3552
|
}
|
|
2248
|
-
if (!
|
|
3553
|
+
if (!existsSync5(session.worktree)) {
|
|
2249
3554
|
throw new LaunchError(`The clone for ${id} is gone: ${session.worktree}`);
|
|
2250
3555
|
}
|
|
2251
3556
|
return session;
|
|
@@ -2268,13 +3573,13 @@ function preserveBranches(run, session) {
|
|
|
2268
3573
|
return preserved;
|
|
2269
3574
|
}
|
|
2270
3575
|
function finishSession(run, session) {
|
|
2271
|
-
if (!
|
|
3576
|
+
if (!existsSync5(session.worktree)) {
|
|
2272
3577
|
deleteSession(session.id);
|
|
2273
3578
|
return;
|
|
2274
3579
|
}
|
|
2275
3580
|
const preserved = preserveBranches(run, session);
|
|
2276
3581
|
if (!cloneHasWork(session.worktree, session.baseSha)) {
|
|
2277
|
-
|
|
3582
|
+
rmSync9(session.worktree, { recursive: true, force: true });
|
|
2278
3583
|
deleteSession(session.id);
|
|
2279
3584
|
return;
|
|
2280
3585
|
}
|
|
@@ -2303,9 +3608,45 @@ function runnerGitHubMode({
|
|
|
2303
3608
|
function exitCodeOf(code, signal) {
|
|
2304
3609
|
if (code !== null)
|
|
2305
3610
|
return code;
|
|
2306
|
-
return signal ? 128 + (
|
|
3611
|
+
return signal ? 128 + (constants2.signals[signal] ?? 0) : 1;
|
|
2307
3612
|
}
|
|
2308
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
|
+
}
|
|
2309
3650
|
async function launch({
|
|
2310
3651
|
args,
|
|
2311
3652
|
resume,
|
|
@@ -2315,9 +3656,10 @@ async function launch({
|
|
|
2315
3656
|
ensureReady();
|
|
2316
3657
|
requireGit(run);
|
|
2317
3658
|
const root = repoRoot(run, process.cwd());
|
|
2318
|
-
|
|
3659
|
+
migrateFromVolumeOnce({
|
|
2319
3660
|
config: readMaesttroConfig(),
|
|
2320
3661
|
saveConfig: writeMaesttroConfig,
|
|
3662
|
+
store: claudeStore(),
|
|
2321
3663
|
announce: (message) => console.log(message)
|
|
2322
3664
|
});
|
|
2323
3665
|
const started = resume ? resumeSession(root, resume) : startSession(run, root);
|
|
@@ -2328,13 +3670,55 @@ async function launch({
|
|
|
2328
3670
|
sessionId: started.id
|
|
2329
3671
|
});
|
|
2330
3672
|
const strict = isStrict(requestedStrict, started);
|
|
3673
|
+
const client = sessionClientArgs({ args, session: started });
|
|
2331
3674
|
const saved = {
|
|
2332
3675
|
...started,
|
|
2333
3676
|
backup: started.backup ?? backup?.path,
|
|
2334
|
-
strict
|
|
3677
|
+
strict,
|
|
3678
|
+
client: client.client ?? started.client
|
|
2335
3679
|
};
|
|
2336
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
|
+
});
|
|
2337
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
|
+
});
|
|
2338
3722
|
const repo = workspaceRepo(run, root);
|
|
2339
3723
|
if (repo && !resume) {
|
|
2340
3724
|
git(run, saved.worktree, [
|
|
@@ -2344,7 +3728,7 @@ async function launch({
|
|
|
2344
3728
|
`https://github.com/${repo}.git`
|
|
2345
3729
|
]);
|
|
2346
3730
|
}
|
|
2347
|
-
|
|
3731
|
+
github = await openGitHubSession({
|
|
2348
3732
|
repo,
|
|
2349
3733
|
author: await resolveAuthor(root),
|
|
2350
3734
|
strict,
|
|
@@ -2354,11 +3738,11 @@ async function launch({
|
|
|
2354
3738
|
store: selectCredentialStore(),
|
|
2355
3739
|
announce: (message) => console.log(message)
|
|
2356
3740
|
},
|
|
2357
|
-
log:
|
|
3741
|
+
log: sessionLog
|
|
2358
3742
|
});
|
|
2359
3743
|
if (github.notice)
|
|
2360
3744
|
console.log(github.notice);
|
|
2361
|
-
const hostHome =
|
|
3745
|
+
const hostHome = homedir3();
|
|
2362
3746
|
const url = maesttroUrl();
|
|
2363
3747
|
const orchestratorWindow = await fetchOrchestratorWindow({ url });
|
|
2364
3748
|
if (orchestratorWindow === undefined) {
|
|
@@ -2372,15 +3756,17 @@ async function launch({
|
|
|
2372
3756
|
if (clientConfigs.removed.length > 0) {
|
|
2373
3757
|
console.log(`Kept out of the runner as secrets: ${clientConfigs.removed.join(", ")}`);
|
|
2374
3758
|
}
|
|
2375
|
-
|
|
3759
|
+
clientConfigDir = writeClientConfigDir(clientConfigs.files);
|
|
2376
3760
|
const { mounts, workdir } = sessionMounts({
|
|
2377
3761
|
repoRoot: root,
|
|
2378
3762
|
sessionDir: saved.worktree,
|
|
2379
3763
|
packageRoot: PACKAGE_ROOT,
|
|
2380
3764
|
hostHome,
|
|
2381
|
-
stagedPaths: STAGED_PATHS.filter((path) =>
|
|
3765
|
+
stagedPaths: STAGED_PATHS.filter((path) => existsSync5(join15(hostHome, path))),
|
|
2382
3766
|
clientConfigs: clientConfigDir.dir,
|
|
2383
|
-
sharedSkills: sharedSkillsDir()
|
|
3767
|
+
sharedSkills: sharedSkillsDir(),
|
|
3768
|
+
home,
|
|
3769
|
+
claudeDir
|
|
2384
3770
|
});
|
|
2385
3771
|
const sessionEnv = {
|
|
2386
3772
|
...github.env,
|
|
@@ -2399,13 +3785,15 @@ async function launch({
|
|
|
2399
3785
|
].concat(repo ? ["MAESTTRO_REPO"] : []).flatMap((name) => ["-e", name]);
|
|
2400
3786
|
clearStaleContainer(saved.id);
|
|
2401
3787
|
pinAlternates(saved.worktree, CONTAINER_GIT_OBJECTS);
|
|
2402
|
-
const child =
|
|
2403
|
-
command: containerCommand(resolveCommand(args)),
|
|
3788
|
+
const child = spawn2("docker", dockerRunArgs({
|
|
3789
|
+
command: containerCommand(resolveCommand(client.args)),
|
|
2404
3790
|
mounts,
|
|
2405
3791
|
workdir,
|
|
2406
3792
|
name: saved.id,
|
|
2407
3793
|
extra: [...github.dockerArgs, ...envFlags]
|
|
2408
3794
|
}), { stdio: "inherit", env: { ...process.env, ...sessionEnv } });
|
|
3795
|
+
interrupts.dispose();
|
|
3796
|
+
claudeNotices.quiet();
|
|
2409
3797
|
const forward = (signal) => child.kill(signal);
|
|
2410
3798
|
for (const signal of FORWARDED_SIGNALS)
|
|
2411
3799
|
process.on(signal, forward);
|
|
@@ -2416,13 +3804,18 @@ async function launch({
|
|
|
2416
3804
|
for (const signal of FORWARDED_SIGNALS)
|
|
2417
3805
|
process.off(signal, forward);
|
|
2418
3806
|
await github.close();
|
|
3807
|
+
if (claudeRefresher?.busy())
|
|
3808
|
+
console.log(`
|
|
3809
|
+
${FINISHING_BACKGROUND_REFRESH}`);
|
|
3810
|
+
await claudeRefresher?.stop();
|
|
3811
|
+
claudeNotices.replay();
|
|
2419
3812
|
clientConfigDir.remove();
|
|
2420
|
-
|
|
3813
|
+
closeSession({ run, session: saved });
|
|
2421
3814
|
process.exit(exitCode);
|
|
2422
3815
|
}
|
|
2423
3816
|
|
|
2424
3817
|
// src/push.ts
|
|
2425
|
-
import { spawnSync as
|
|
3818
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
2426
3819
|
|
|
2427
3820
|
// ../node_modules/.bun/@clack+core@1.5.1/node_modules/@clack/core/dist/index.mjs
|
|
2428
3821
|
import { stdout, stdin } from "node:process";
|
|
@@ -3614,7 +5007,7 @@ async function push({ args }) {
|
|
|
3614
5007
|
return;
|
|
3615
5008
|
}
|
|
3616
5009
|
}
|
|
3617
|
-
const pushed =
|
|
5010
|
+
const pushed = spawnSync4("git", ["-C", root, "push", ...gitFlags, "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
|
|
3618
5011
|
if (pushed.status === 0) {
|
|
3619
5012
|
const done = `Pushed ${branch}. Base for comparison was ${baseSha.slice(0, 8)}.`;
|
|
3620
5013
|
if (tty && !yes)
|
|
@@ -3626,7 +5019,7 @@ async function push({ args }) {
|
|
|
3626
5019
|
}
|
|
3627
5020
|
|
|
3628
5021
|
// src/sessions-command.ts
|
|
3629
|
-
import { existsSync as
|
|
5022
|
+
import { existsSync as existsSync6, rmSync as rmSync10 } from "node:fs";
|
|
3630
5023
|
function parseRemoveArgs(args) {
|
|
3631
5024
|
let all = false;
|
|
3632
5025
|
let force = false;
|
|
@@ -3642,7 +5035,7 @@ function parseRemoveArgs(args) {
|
|
|
3642
5035
|
return { all, force, id };
|
|
3643
5036
|
}
|
|
3644
5037
|
async function removeSession(run, session, force, ask, interactive) {
|
|
3645
|
-
if (
|
|
5038
|
+
if (existsSync6(session.worktree)) {
|
|
3646
5039
|
const preserved = preserveBranches(run, session);
|
|
3647
5040
|
const head = cloneHead(session.worktree);
|
|
3648
5041
|
const unbranched = preserved.length === 0 && head?.detached !== undefined && head.detached !== session.baseSha;
|
|
@@ -3660,7 +5053,7 @@ async function removeSession(run, session, force, ask, interactive) {
|
|
|
3660
5053
|
return "kept";
|
|
3661
5054
|
}
|
|
3662
5055
|
}
|
|
3663
|
-
|
|
5056
|
+
rmSync10(session.worktree, { recursive: true, force: true });
|
|
3664
5057
|
const kept = preserved.length > 0 ? ` Its branches are kept as refs/maesttro/${session.id}/*.` : "";
|
|
3665
5058
|
console.log(`Removed session ${session.id}.${kept}`);
|
|
3666
5059
|
} else {
|
|
@@ -3717,7 +5110,7 @@ async function sessionsCommand({
|
|
|
3717
5110
|
return;
|
|
3718
5111
|
}
|
|
3719
5112
|
for (const session of sessions) {
|
|
3720
|
-
const branches =
|
|
5113
|
+
const branches = existsSync6(session.worktree) ? cloneBranches(session.worktree) : undefined;
|
|
3721
5114
|
const where = branches === undefined ? "clone missing" : branches.length > 0 ? `branches: ${branches.join(", ")}` : "no branches yet";
|
|
3722
5115
|
const mode = session.strict ? " strict" : "";
|
|
3723
5116
|
console.log(` ${session.id} ${session.createdAt.slice(0, 16)} ${where}${mode}`);
|
|
@@ -3745,23 +5138,31 @@ async function setup({ args }) {
|
|
|
3745
5138
|
console.log("-> Building runner image (this pulls node:22-slim + installs CLIs, first run only)...");
|
|
3746
5139
|
run(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`);
|
|
3747
5140
|
console.log(`
|
|
3748
|
-
->
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
console.log(` ${
|
|
3754
|
-
|
|
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
|
+
});
|
|
3755
5148
|
if (args[0]) {
|
|
3756
|
-
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`.");
|
|
3757
5150
|
}
|
|
3758
5151
|
console.log(`
|
|
3759
5152
|
-> Logging in to GitHub...`);
|
|
3760
5153
|
await ensureLogin();
|
|
3761
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(`
|
|
3762
5163
|
-> Verifying isolation...`);
|
|
3763
5164
|
console.log(" personal SSH key should NOT be visible:");
|
|
3764
|
-
run(`docker run --rm ${hostIdentityFlags()}
|
|
5165
|
+
run(`docker run --rm ${hostIdentityFlags()} ${IMAGE} "cat ~/.ssh/id_rsa 2>&1 || true"`);
|
|
3765
5166
|
console.log("\nSetup complete. Run `maesttro` from any project directory to start a session,");
|
|
3766
5167
|
console.log("or `maesttro list` to see available clients.");
|
|
3767
5168
|
}
|
|
@@ -3843,15 +5244,15 @@ function removeStaleRunnerImages({
|
|
|
3843
5244
|
|
|
3844
5245
|
// src/version-check.ts
|
|
3845
5246
|
import { execFile } from "node:child_process";
|
|
3846
|
-
import { existsSync as
|
|
3847
|
-
import { join as
|
|
5247
|
+
import { existsSync as existsSync7, readFileSync as readFileSync12 } from "node:fs";
|
|
5248
|
+
import { join as join16 } from "node:path";
|
|
3848
5249
|
var LOOKUP_TIMEOUT_MS = 3000;
|
|
3849
5250
|
function installedManifest() {
|
|
3850
|
-
return JSON.parse(
|
|
5251
|
+
return JSON.parse(readFileSync12(join16(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
3851
5252
|
}
|
|
3852
5253
|
function canOfferUpdate({
|
|
3853
5254
|
interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
3854
|
-
inRunner =
|
|
5255
|
+
inRunner = existsSync7(RUNNER_MARKER)
|
|
3855
5256
|
} = {}) {
|
|
3856
5257
|
return interactive && !inRunner;
|
|
3857
5258
|
}
|
|
@@ -3945,6 +5346,13 @@ var MODE_FLAGS = {
|
|
|
3945
5346
|
"--strict": true,
|
|
3946
5347
|
"--no-strict": false
|
|
3947
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
|
+
}
|
|
3948
5356
|
function takeModeFlags(args) {
|
|
3949
5357
|
let strict;
|
|
3950
5358
|
let index = 0;
|
|
@@ -3996,13 +5404,13 @@ async function main(argv, commands = defaultCommands) {
|
|
|
3996
5404
|
await commands.doctor();
|
|
3997
5405
|
return;
|
|
3998
5406
|
case "login":
|
|
3999
|
-
await commands.login();
|
|
5407
|
+
await commands.login({ args: rest });
|
|
4000
5408
|
return;
|
|
4001
5409
|
case "logout":
|
|
4002
|
-
await commands.logout();
|
|
5410
|
+
await commands.logout({ args: rest });
|
|
4003
5411
|
return;
|
|
4004
5412
|
case "status":
|
|
4005
|
-
await commands.status();
|
|
5413
|
+
await commands.status({ args: rest });
|
|
4006
5414
|
return;
|
|
4007
5415
|
case "push":
|
|
4008
5416
|
await commands.push({ args: rest });
|
|
@@ -4012,11 +5420,11 @@ async function main(argv, commands = defaultCommands) {
|
|
|
4012
5420
|
return;
|
|
4013
5421
|
case "resume": {
|
|
4014
5422
|
const beforeId = takeModeFlags(rest);
|
|
4015
|
-
const [id, ...afterId] = beforeId.rest;
|
|
5423
|
+
const [id, ...afterId] = clientFirst(beforeId.rest);
|
|
4016
5424
|
const afterIdFlags = takeModeFlags(afterId);
|
|
4017
5425
|
const strict = afterIdFlags.strict ?? beforeId.strict ?? leading.strict;
|
|
4018
5426
|
if (!id) {
|
|
4019
|
-
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...]");
|
|
4020
5428
|
return;
|
|
4021
5429
|
}
|
|
4022
5430
|
await commands.launch({
|