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