maesttro 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -21
- package/dist/maesttro.js +612 -196
- package/docker/Dockerfile +5 -6
- package/docker/configs/opencode/config.json +1 -1
- package/docker/runner-marker +1 -0
- package/package.json +2 -2
- package/docker/configs/claude/settings.json +0 -3
- package/docker/configs/codex/config.json +0 -3
- package/docker/sandbox-marker +0 -1
package/dist/maesttro.js
CHANGED
|
@@ -351,15 +351,342 @@ async function status() {
|
|
|
351
351
|
|
|
352
352
|
// src/configure.ts
|
|
353
353
|
import {
|
|
354
|
-
cpSync,
|
|
355
|
-
existsSync,
|
|
356
|
-
mkdirSync as
|
|
354
|
+
cpSync as cpSync2,
|
|
355
|
+
existsSync as existsSync2,
|
|
356
|
+
mkdirSync as mkdirSync4,
|
|
357
357
|
readdirSync,
|
|
358
|
-
readFileSync as
|
|
359
|
-
writeFileSync as
|
|
358
|
+
readFileSync as readFileSync4,
|
|
359
|
+
writeFileSync as writeFileSync4
|
|
360
360
|
} from "node:fs";
|
|
361
361
|
import { homedir as homedir2 } from "node:os";
|
|
362
|
-
import { dirname as
|
|
362
|
+
import { dirname as dirname5, join as join4 } from "node:path";
|
|
363
|
+
|
|
364
|
+
// src/client-configs.ts
|
|
365
|
+
import {
|
|
366
|
+
chmodSync as chmodSync2,
|
|
367
|
+
cpSync,
|
|
368
|
+
existsSync,
|
|
369
|
+
mkdirSync as mkdirSync3,
|
|
370
|
+
mkdtempSync,
|
|
371
|
+
readFileSync as readFileSync3,
|
|
372
|
+
rmSync as rmSync2,
|
|
373
|
+
writeFileSync as writeFileSync3
|
|
374
|
+
} from "node:fs";
|
|
375
|
+
import { tmpdir } from "node:os";
|
|
376
|
+
import { dirname as dirname3, join as join2 } from "node:path";
|
|
377
|
+
|
|
378
|
+
// src/json-file.ts
|
|
379
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
380
|
+
import { dirname as dirname2 } from "node:path";
|
|
381
|
+
function isObject(value) {
|
|
382
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
383
|
+
}
|
|
384
|
+
function mergeJson(existing, curated) {
|
|
385
|
+
const merged = { ...existing };
|
|
386
|
+
for (const [key, value] of Object.entries(curated)) {
|
|
387
|
+
const current = merged[key];
|
|
388
|
+
merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
|
|
389
|
+
}
|
|
390
|
+
return merged;
|
|
391
|
+
}
|
|
392
|
+
function readJson(path) {
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
395
|
+
return isObject(parsed) ? parsed : {};
|
|
396
|
+
} catch {
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function writeJson(path, value) {
|
|
401
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
402
|
+
writeFileSync2(path, `${JSON.stringify(value, null, 2)}
|
|
403
|
+
`);
|
|
404
|
+
}
|
|
405
|
+
function skipInsignificant(text, from) {
|
|
406
|
+
let index = from;
|
|
407
|
+
while (index < text.length) {
|
|
408
|
+
if (/\s/.test(text[index] ?? "")) {
|
|
409
|
+
index++;
|
|
410
|
+
} else if (text.startsWith("//", index)) {
|
|
411
|
+
while (index < text.length && text[index] !== `
|
|
412
|
+
`)
|
|
413
|
+
index++;
|
|
414
|
+
} else if (text.startsWith("/*", index)) {
|
|
415
|
+
const end = text.indexOf("*/", index + 2);
|
|
416
|
+
index = end === -1 ? text.length : end + 2;
|
|
417
|
+
} else {
|
|
418
|
+
return index;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return index;
|
|
422
|
+
}
|
|
423
|
+
function stripJsonc(text) {
|
|
424
|
+
let output = "";
|
|
425
|
+
let index = 0;
|
|
426
|
+
while (index < text.length) {
|
|
427
|
+
const char = text[index];
|
|
428
|
+
if (char === '"') {
|
|
429
|
+
const start = index;
|
|
430
|
+
index++;
|
|
431
|
+
while (index < text.length && text[index] !== '"') {
|
|
432
|
+
index += text[index] === "\\" ? 2 : 1;
|
|
433
|
+
}
|
|
434
|
+
index++;
|
|
435
|
+
output += text.slice(start, index);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const significant = skipInsignificant(text, index);
|
|
439
|
+
if (significant > index) {
|
|
440
|
+
output += " ";
|
|
441
|
+
index = significant;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const trailingComma = char === "," && /[}\]]/.test(text[skipInsignificant(text, index + 1)] ?? "");
|
|
445
|
+
if (!trailingComma)
|
|
446
|
+
output += char;
|
|
447
|
+
index++;
|
|
448
|
+
}
|
|
449
|
+
return output;
|
|
450
|
+
}
|
|
451
|
+
function parseJsonc(text) {
|
|
452
|
+
const parsed = JSON.parse(stripJsonc(text));
|
|
453
|
+
if (!isObject(parsed))
|
|
454
|
+
throw new Error("expected a JSON object");
|
|
455
|
+
return parsed;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/maesttro-endpoint.ts
|
|
459
|
+
var DEFAULT_MAESTTRO_URL = "http://100.64.0.2";
|
|
460
|
+
var ORCHESTRATOR_MODEL = "maesttro";
|
|
461
|
+
function maesttroUrl(env = process.env) {
|
|
462
|
+
return (env.MAESTTRO_URL?.trim() || DEFAULT_MAESTTRO_URL).replace(/\/+$/, "");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/provider-configs.ts
|
|
466
|
+
var PROVIDER_ID = "maesttro";
|
|
467
|
+
var MAESTTRO_API_KEY = "maesttro";
|
|
468
|
+
var DGX_SPARK_PROVIDER_ID = "dgx-spark";
|
|
469
|
+
var DGX_SPARK_URL = "http://100.64.0.1:8001";
|
|
470
|
+
var INJECTED_OPENCODE_PROVIDERS = [PROVIDER_ID, DGX_SPARK_PROVIDER_ID];
|
|
471
|
+
function without(object, keys) {
|
|
472
|
+
if (!isObject(object))
|
|
473
|
+
return {};
|
|
474
|
+
return Object.fromEntries(Object.entries(object).filter(([key]) => !keys.includes(key)));
|
|
475
|
+
}
|
|
476
|
+
function stringList(value) {
|
|
477
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : undefined;
|
|
478
|
+
}
|
|
479
|
+
function opencodeHostBase(host) {
|
|
480
|
+
return isObject(host.provider) ? { ...host, provider: without(host.provider, INJECTED_OPENCODE_PROVIDERS) } : host;
|
|
481
|
+
}
|
|
482
|
+
var IMAGE_INPUT = {
|
|
483
|
+
attachment: true,
|
|
484
|
+
modalities: { input: ["text", "image"] }
|
|
485
|
+
};
|
|
486
|
+
function opencodeProviders(url) {
|
|
487
|
+
return {
|
|
488
|
+
[PROVIDER_ID]: {
|
|
489
|
+
npm: "@ai-sdk/openai-compatible",
|
|
490
|
+
name: "Maesttro",
|
|
491
|
+
options: { baseURL: `${url}/v1`, apiKey: MAESTTRO_API_KEY },
|
|
492
|
+
models: {
|
|
493
|
+
[ORCHESTRATOR_MODEL]: { name: "Orchestrate", ...IMAGE_INPUT }
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
[DGX_SPARK_PROVIDER_ID]: {
|
|
497
|
+
npm: "@ai-sdk/openai-compatible",
|
|
498
|
+
name: "DGX-Spark",
|
|
499
|
+
options: { baseURL: `${DGX_SPARK_URL}/v1`, apiKey: "" },
|
|
500
|
+
models: {
|
|
501
|
+
"maesttro/lorem": {
|
|
502
|
+
name: "Qwen3.8 27B",
|
|
503
|
+
...IMAGE_INPUT,
|
|
504
|
+
limit: { context: 131072, output: 32768 }
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function opencodeConfig(base, url) {
|
|
511
|
+
const merged = mergeJson({ $schema: "https://opencode.ai/config.json", ...base }, {
|
|
512
|
+
model: `${PROVIDER_ID}/${ORCHESTRATOR_MODEL}`,
|
|
513
|
+
compaction: { auto: true, keep: { tokens: 24000 }, buffer: 32000 }
|
|
514
|
+
});
|
|
515
|
+
const enabled = stringList(base.enabled_providers);
|
|
516
|
+
const disabled = stringList(base.disabled_providers);
|
|
517
|
+
return {
|
|
518
|
+
...merged,
|
|
519
|
+
...enabled ? {
|
|
520
|
+
enabled_providers: [
|
|
521
|
+
...enabled.filter((id) => !INJECTED_OPENCODE_PROVIDERS.includes(id)),
|
|
522
|
+
...INJECTED_OPENCODE_PROVIDERS
|
|
523
|
+
]
|
|
524
|
+
} : {},
|
|
525
|
+
...disabled ? {
|
|
526
|
+
disabled_providers: disabled.filter((id) => !INJECTED_OPENCODE_PROVIDERS.includes(id))
|
|
527
|
+
} : {},
|
|
528
|
+
provider: {
|
|
529
|
+
...isObject(base.provider) ? base.provider : {},
|
|
530
|
+
...opencodeProviders(url)
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/secrets.ts
|
|
536
|
+
var SECRET_KEY = /api[_-]?key|token|secret|passw(or)?d|credential|authorization|bearer|private[_-]?key|cookie/i;
|
|
537
|
+
var ENV_NAME_KEY = /(^|_)env(_var|_key|_vars)?$/i;
|
|
538
|
+
var SECRET_MAPS = new Set(["env", "environment", "headers", "http_headers"]);
|
|
539
|
+
var CREDENTIAL_SHAPES = [
|
|
540
|
+
/(^|[^A-Za-z0-9])sk[-_][A-Za-z0-9_-]{16,}/,
|
|
541
|
+
/(^|[^A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{20,}/,
|
|
542
|
+
/github_pat_[A-Za-z0-9_]{20,}/,
|
|
543
|
+
/glpat-[A-Za-z0-9_-]{16,}/,
|
|
544
|
+
/xox[abposr]-[A-Za-z0-9-]{10,}/,
|
|
545
|
+
/(^|[^A-Za-z0-9])(AKIA|ASIA)[0-9A-Z]{16}/,
|
|
546
|
+
/AIza[0-9A-Za-z_-]{30,}/,
|
|
547
|
+
/\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/i,
|
|
548
|
+
/:\/\/[^/\s:@]+:[^/\s@]+@/,
|
|
549
|
+
/[?&](api[_-]?key|key|token|access_token|secret)=/i,
|
|
550
|
+
/hooks\.slack\.com\/services\//
|
|
551
|
+
];
|
|
552
|
+
var SECRET_FLAG = /^--?[\w-]*(token|api[_-]?key|secret|passw(or)?d)[\w-]*$/i;
|
|
553
|
+
var SECRET_FLAG_WITH_VALUE = /^--?[\w-]*(token|api[_-]?key|secret|passw(or)?d)[\w-]*=/i;
|
|
554
|
+
var REFERENCE = /^\{(env|file):[^}]+\}$|^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/;
|
|
555
|
+
var HARMLESS_MAP_VALUE = /^(\d+(\.\d+)?|true|false|yes|no|on|off)$/i;
|
|
556
|
+
function isSecretString({
|
|
557
|
+
key,
|
|
558
|
+
value,
|
|
559
|
+
inSecretMap
|
|
560
|
+
}) {
|
|
561
|
+
if (value.trim() === "" || REFERENCE.test(value.trim()))
|
|
562
|
+
return false;
|
|
563
|
+
if (CREDENTIAL_SHAPES.some((shape) => shape.test(value)))
|
|
564
|
+
return true;
|
|
565
|
+
if (inSecretMap)
|
|
566
|
+
return !HARMLESS_MAP_VALUE.test(value.trim());
|
|
567
|
+
return SECRET_KEY.test(key) && !ENV_NAME_KEY.test(key);
|
|
568
|
+
}
|
|
569
|
+
function redactValue(value, key, path, inSecretMap, removed) {
|
|
570
|
+
if (typeof value === "string") {
|
|
571
|
+
if (isSecretString({ key, value, inSecretMap })) {
|
|
572
|
+
removed.push(path);
|
|
573
|
+
return { keep: false, value };
|
|
574
|
+
}
|
|
575
|
+
return { keep: true, value };
|
|
576
|
+
}
|
|
577
|
+
if (Array.isArray(value)) {
|
|
578
|
+
const items = value.flatMap((item, index) => {
|
|
579
|
+
const previous = value[index - 1];
|
|
580
|
+
const flagged = typeof item === "string" && (typeof previous === "string" && SECRET_FLAG.test(previous) || SECRET_FLAG_WITH_VALUE.test(item));
|
|
581
|
+
if (flagged) {
|
|
582
|
+
removed.push(`${path}[${index}]`);
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
const result = redactValue(item, key, `${path}[${index}]`, inSecretMap, removed);
|
|
586
|
+
return result.keep ? [result.value] : [];
|
|
587
|
+
});
|
|
588
|
+
return { keep: true, value: items };
|
|
589
|
+
}
|
|
590
|
+
if (isObject(value)) {
|
|
591
|
+
return {
|
|
592
|
+
keep: true,
|
|
593
|
+
value: redactObject(value, path, inSecretMap || SECRET_MAPS.has(key), removed)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return { keep: true, value };
|
|
597
|
+
}
|
|
598
|
+
function redactObject(object, path, inSecretMap, removed) {
|
|
599
|
+
const kept = {};
|
|
600
|
+
for (const [key, value] of Object.entries(object)) {
|
|
601
|
+
const result = redactValue(value, key, path ? `${path}.${key}` : key, inSecretMap, removed);
|
|
602
|
+
const emptied = isObject(value) && Object.keys(value).length > 0 && isObject(result.value) && Object.keys(result.value).length === 0;
|
|
603
|
+
if (result.keep && !emptied)
|
|
604
|
+
kept[key] = result.value;
|
|
605
|
+
}
|
|
606
|
+
return kept;
|
|
607
|
+
}
|
|
608
|
+
function redactSecrets(config) {
|
|
609
|
+
const removed = [];
|
|
610
|
+
return { value: redactObject(config, "", false, removed), removed };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/client-configs.ts
|
|
614
|
+
var CONTAINER_CLIENT_CONFIGS = "/opt/maesttro-clients";
|
|
615
|
+
var OPENCODE_HOST_CONFIGS = [
|
|
616
|
+
".config/opencode/opencode.json",
|
|
617
|
+
".config/opencode/opencode.jsonc"
|
|
618
|
+
];
|
|
619
|
+
var OPENCODE_CONFIG = ".config/opencode/opencode.json";
|
|
620
|
+
var OPENCODE_TUI_STATE = ".local/state/opencode/kv.json";
|
|
621
|
+
var CURATED_OPENCODE_TUI_STATE = {
|
|
622
|
+
thinking_mode: "show",
|
|
623
|
+
thinking_visibility: true
|
|
624
|
+
};
|
|
625
|
+
function readHostFile({
|
|
626
|
+
home,
|
|
627
|
+
file,
|
|
628
|
+
parse,
|
|
629
|
+
warn
|
|
630
|
+
}) {
|
|
631
|
+
const path = join2(home, file);
|
|
632
|
+
if (!existsSync(path))
|
|
633
|
+
return {};
|
|
634
|
+
try {
|
|
635
|
+
return parse(readFileSync3(path, "utf8"));
|
|
636
|
+
} catch {
|
|
637
|
+
warn(`maesttro: ignoring your ~/${file}, it doesn't parse`);
|
|
638
|
+
return {};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function readHostClientConfigs({
|
|
642
|
+
home,
|
|
643
|
+
warn = (message) => console.error(message)
|
|
644
|
+
}) {
|
|
645
|
+
return {
|
|
646
|
+
opencode: OPENCODE_HOST_CONFIGS.reduce((config, file) => mergeJson(config, readHostFile({ home, file, parse: parseJsonc, warn })), {})
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function json(value) {
|
|
650
|
+
return `${JSON.stringify(value, null, 2)}
|
|
651
|
+
`;
|
|
652
|
+
}
|
|
653
|
+
function buildClientConfigs({
|
|
654
|
+
host,
|
|
655
|
+
url
|
|
656
|
+
}) {
|
|
657
|
+
const opencode = redactSecrets(opencodeHostBase(host.opencode));
|
|
658
|
+
return {
|
|
659
|
+
files: { [OPENCODE_CONFIG]: json(opencodeConfig(opencode.value, url)) },
|
|
660
|
+
removed: opencode.removed.map((path) => `opencode ${path}`)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function writeClientConfigDir(files, parent = tmpdir()) {
|
|
664
|
+
const root = mkdtempSync(join2(parent, "maesttro-clients-"));
|
|
665
|
+
chmodSync2(root, 448);
|
|
666
|
+
const dir = join2(root, "home");
|
|
667
|
+
for (const [file, content] of Object.entries(files)) {
|
|
668
|
+
const path = join2(dir, file);
|
|
669
|
+
mkdirSync3(dirname3(path), { recursive: true, mode: 493 });
|
|
670
|
+
writeFileSync3(path, content);
|
|
671
|
+
chmodSync2(path, 420);
|
|
672
|
+
}
|
|
673
|
+
chmodSync2(dir, 493);
|
|
674
|
+
return { dir, remove: () => rmSync2(root, { recursive: true, force: true }) };
|
|
675
|
+
}
|
|
676
|
+
function installClientConfigs({
|
|
677
|
+
home,
|
|
678
|
+
source = CONTAINER_CLIENT_CONFIGS
|
|
679
|
+
}) {
|
|
680
|
+
if (!existsSync(source))
|
|
681
|
+
return false;
|
|
682
|
+
cpSync(source, home, { recursive: true, force: true });
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
function injectOpencodeTuiState({ home }) {
|
|
686
|
+
const path = join2(home, OPENCODE_TUI_STATE);
|
|
687
|
+
writeJson(path, mergeJson(readJson(path), CURATED_OPENCODE_TUI_STATE));
|
|
688
|
+
return path;
|
|
689
|
+
}
|
|
363
690
|
|
|
364
691
|
// src/clients.ts
|
|
365
692
|
var CLIENTS = {
|
|
@@ -374,15 +701,9 @@ var CLIENTS = {
|
|
|
374
701
|
npmPackage: "opencode-ai",
|
|
375
702
|
binary: "opencode",
|
|
376
703
|
configDir: "/home/agent/.config/opencode"
|
|
377
|
-
},
|
|
378
|
-
codex: {
|
|
379
|
-
label: "OpenAI Codex CLI",
|
|
380
|
-
npmPackage: "@openai/codex",
|
|
381
|
-
binary: "codex",
|
|
382
|
-
configDir: "/home/agent/.codex"
|
|
383
704
|
}
|
|
384
705
|
};
|
|
385
|
-
var DEFAULT_CLIENT = "
|
|
706
|
+
var DEFAULT_CLIENT = "opencode";
|
|
386
707
|
function isClientKey(value) {
|
|
387
708
|
return Object.hasOwn(CLIENTS, value);
|
|
388
709
|
}
|
|
@@ -396,109 +717,108 @@ var STAGED_DIRECTORIES = [
|
|
|
396
717
|
];
|
|
397
718
|
var INSTRUCTION_FILES = [
|
|
398
719
|
".claude/CLAUDE.md",
|
|
399
|
-
".config/opencode/AGENTS.md"
|
|
400
|
-
".codex/AGENTS.md"
|
|
720
|
+
".config/opencode/AGENTS.md"
|
|
401
721
|
];
|
|
402
722
|
var STAGED_PATHS = [...INSTRUCTION_FILES, ...STAGED_DIRECTORIES];
|
|
403
723
|
|
|
404
724
|
// src/paths.ts
|
|
405
|
-
import { dirname as
|
|
725
|
+
import { dirname as dirname4, join as join3, resolve } from "node:path";
|
|
406
726
|
import { fileURLToPath } from "node:url";
|
|
407
|
-
var PACKAGE_ROOT = resolve(
|
|
408
|
-
var DOCKERFILE_DIR =
|
|
727
|
+
var PACKAGE_ROOT = resolve(dirname4(fileURLToPath(import.meta.url)), "..");
|
|
728
|
+
var DOCKERFILE_DIR = join3(PACKAGE_ROOT, "docker");
|
|
409
729
|
var IMAGE = "maesttro-runner:latest";
|
|
410
730
|
var VOLUME = process.env.MAESTTRO_VOLUME?.trim() || "maesttro-home";
|
|
411
731
|
|
|
412
|
-
// src/
|
|
413
|
-
function
|
|
732
|
+
// src/runner-context.ts
|
|
733
|
+
function githubMode(value) {
|
|
734
|
+
return value === "on" || value === "strict" ? value : "off";
|
|
735
|
+
}
|
|
736
|
+
function githubRules(context) {
|
|
737
|
+
switch (context.github) {
|
|
738
|
+
case "on":
|
|
739
|
+
return [
|
|
740
|
+
`- \`gh\` works for ${context.repo ?? "this repo"} only: issues, pull requests, comments. Its token is short-lived and refreshed for you.`,
|
|
741
|
+
"- Merging pull requests and deleting remote branches are not possible from here. A person merges."
|
|
742
|
+
];
|
|
743
|
+
case "strict":
|
|
744
|
+
return [
|
|
745
|
+
"- This session runs in strict mode: GitHub is disabled on purpose and the runner holds no GitHub credentials. `gh` does not work, and `git fetch` fails for private repositories.",
|
|
746
|
+
"- Do not look for credentials or ask the user for a token. Local git work still works.",
|
|
747
|
+
"- When a task needs GitHub (an issue, a pull request, a comment, a review, a label), write out what should be posted and hand it to the user."
|
|
748
|
+
];
|
|
749
|
+
case "off":
|
|
750
|
+
return [
|
|
751
|
+
"- GitHub access is unavailable in this session, so `gh` has no token. Local git work still works."
|
|
752
|
+
];
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function afterPushRule(github) {
|
|
756
|
+
return github === "on" ? "- After the user confirms it is pushed, `git fetch` and continue. Open the pull request with `gh pr create --head <branch>` if asked to." : "- After the user confirms it is pushed, continue on the same branch. The user opens the pull request.";
|
|
757
|
+
}
|
|
758
|
+
function runnerContextFromEnv(env) {
|
|
414
759
|
return {
|
|
415
760
|
sessionId: env.MAESTTRO_SESSION_ID,
|
|
416
761
|
base: env.MAESTTRO_BASE,
|
|
417
762
|
repo: env.MAESTTRO_REPO,
|
|
418
|
-
|
|
763
|
+
github: githubMode(env.MAESTTRO_GITHUB)
|
|
419
764
|
};
|
|
420
765
|
}
|
|
421
|
-
function
|
|
766
|
+
function runnerInstructions(context) {
|
|
422
767
|
const base = context.base ?? "the default branch";
|
|
423
|
-
const github = context.githubAccess ? [
|
|
424
|
-
`- \`gh\` works for ${context.repo ?? "this repo"} only: issues, pull requests, comments. Its token is short-lived and refreshed for you.`,
|
|
425
|
-
"- Merging pull requests and deleting remote branches are not possible from here. A person merges."
|
|
426
|
-
] : [
|
|
427
|
-
"- GitHub access is unavailable in this session, so `gh` has no token. Local git work still works."
|
|
428
|
-
];
|
|
429
768
|
return [
|
|
430
|
-
"# Maesttro
|
|
769
|
+
"# Maesttro runner",
|
|
431
770
|
"",
|
|
432
|
-
"You are running inside Maesttro's isolated
|
|
771
|
+
"You are running inside Maesttro's isolated runner container. These rules describe what the environment allows, so plan around them.",
|
|
433
772
|
"",
|
|
434
773
|
"## Your workspace",
|
|
435
774
|
"",
|
|
436
775
|
`- The current directory is an isolated clone of the repository${context.sessionId ? ` for this session (\`${context.sessionId}\`)` : ""}, detached at \`${base}\`. It is the only checkout you can see or change; your main checkout is not here.`,
|
|
437
776
|
"- Before your first commit, create a branch for the task: `git switch -c <short-descriptive-name>`. Keep one branch per task.",
|
|
438
777
|
"- Work in this directory. Anything outside it is not persisted.",
|
|
439
|
-
"- Commit on a branch you create here. The user pushes it from their machine with `maesttro push`; the
|
|
778
|
+
"- 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.",
|
|
440
779
|
"",
|
|
441
780
|
"## Pushing",
|
|
442
781
|
"",
|
|
443
|
-
"- `git push` does not work here: the
|
|
782
|
+
"- `git push` does not work here: the runner has read-only access to the remote. This is deliberate.",
|
|
444
783
|
'- When a branch is ready, commit everything, then tell the user: "Ready to push: <branch>. Run `maesttro push <branch>` from the repository root."',
|
|
445
|
-
|
|
784
|
+
afterPushRule(context.github),
|
|
446
785
|
"",
|
|
447
786
|
"## GitHub",
|
|
448
787
|
"",
|
|
449
|
-
...
|
|
788
|
+
...githubRules(context),
|
|
450
789
|
""
|
|
451
790
|
].join(`
|
|
452
791
|
`);
|
|
453
792
|
}
|
|
454
793
|
|
|
455
794
|
// src/configure.ts
|
|
456
|
-
var
|
|
457
|
-
function isObject(value) {
|
|
458
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
459
|
-
}
|
|
795
|
+
var RUNNER_MARKER = "/etc/maesttro/runner";
|
|
460
796
|
function withoutComments(value) {
|
|
461
797
|
return Object.fromEntries(Object.entries(value).filter(([key]) => !key.startsWith("_comment")).map(([key, inner]) => [
|
|
462
798
|
key,
|
|
463
799
|
isObject(inner) ? withoutComments(inner) : inner
|
|
464
800
|
]));
|
|
465
801
|
}
|
|
466
|
-
function mergeJson(existing, curated) {
|
|
467
|
-
const merged = { ...existing };
|
|
468
|
-
for (const [key, value] of Object.entries(curated)) {
|
|
469
|
-
const current = merged[key];
|
|
470
|
-
merged[key] = isObject(current) && isObject(value) ? mergeJson(current, value) : value;
|
|
471
|
-
}
|
|
472
|
-
return merged;
|
|
473
|
-
}
|
|
474
|
-
function readJson(path) {
|
|
475
|
-
try {
|
|
476
|
-
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
477
|
-
return isObject(parsed) ? parsed : {};
|
|
478
|
-
} catch {
|
|
479
|
-
return {};
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
802
|
function applyProviderConfigs({
|
|
483
803
|
home,
|
|
484
|
-
configsDir =
|
|
804
|
+
configsDir = join4(DOCKERFILE_DIR, "configs")
|
|
485
805
|
}) {
|
|
486
806
|
const written = [];
|
|
487
807
|
for (const [key, client] of Object.entries(CLIENTS)) {
|
|
488
|
-
const source =
|
|
489
|
-
if (!
|
|
808
|
+
const source = join4(configsDir, key);
|
|
809
|
+
if (!existsSync2(source))
|
|
490
810
|
continue;
|
|
491
|
-
const targetDir =
|
|
811
|
+
const targetDir = join4(home, client.configDir.replace(/^\/home\/agent\/?/, ""));
|
|
492
812
|
for (const name of readdirSync(source)) {
|
|
493
813
|
if (!name.endsWith(".json"))
|
|
494
814
|
continue;
|
|
495
|
-
const curated = withoutComments(readJson(
|
|
815
|
+
const curated = withoutComments(readJson(join4(source, name)));
|
|
496
816
|
if (Object.keys(curated).length === 0)
|
|
497
817
|
continue;
|
|
498
|
-
const target =
|
|
499
|
-
|
|
818
|
+
const target = join4(targetDir, name);
|
|
819
|
+
mkdirSync4(targetDir, { recursive: true });
|
|
500
820
|
const merged = mergeJson(readJson(target), curated);
|
|
501
|
-
|
|
821
|
+
writeFileSync4(target, `${JSON.stringify(merged, null, 2)}
|
|
502
822
|
`);
|
|
503
823
|
written.push(target);
|
|
504
824
|
}
|
|
@@ -511,33 +831,41 @@ function stageHostInstructions({
|
|
|
511
831
|
context
|
|
512
832
|
}) {
|
|
513
833
|
for (const directory of STAGED_DIRECTORIES) {
|
|
514
|
-
const source =
|
|
515
|
-
if (!
|
|
834
|
+
const source = join4(staging, directory);
|
|
835
|
+
if (!existsSync2(source))
|
|
516
836
|
continue;
|
|
517
837
|
try {
|
|
518
|
-
|
|
838
|
+
cpSync2(source, join4(home, directory), { recursive: true, force: true });
|
|
519
839
|
} catch {}
|
|
520
840
|
}
|
|
521
|
-
const instructions =
|
|
841
|
+
const instructions = runnerInstructions(context);
|
|
522
842
|
for (const file of INSTRUCTION_FILES) {
|
|
523
|
-
const source =
|
|
524
|
-
const hostCopy =
|
|
525
|
-
const target =
|
|
526
|
-
|
|
527
|
-
|
|
843
|
+
const source = join4(staging, file);
|
|
844
|
+
const hostCopy = existsSync2(source) ? readFileSync4(source, "utf8") : "";
|
|
845
|
+
const target = join4(home, file);
|
|
846
|
+
mkdirSync4(dirname5(target), { recursive: true });
|
|
847
|
+
writeFileSync4(target, hostCopy ? `${hostCopy.trimEnd()}
|
|
528
848
|
|
|
529
849
|
${instructions}` : instructions);
|
|
530
850
|
}
|
|
531
851
|
}
|
|
532
852
|
function configure({
|
|
533
853
|
home = homedir2(),
|
|
534
|
-
|
|
535
|
-
env = process.env
|
|
854
|
+
inRunner = existsSync2(RUNNER_MARKER),
|
|
855
|
+
env = process.env,
|
|
856
|
+
staging = HOST_STAGING_DIR,
|
|
857
|
+
clientConfigs
|
|
536
858
|
} = {}) {
|
|
537
859
|
applyProviderConfigs({ home });
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
}
|
|
860
|
+
if (!inRunner)
|
|
861
|
+
return;
|
|
862
|
+
installClientConfigs({ home, source: clientConfigs });
|
|
863
|
+
injectOpencodeTuiState({ home });
|
|
864
|
+
stageHostInstructions({
|
|
865
|
+
home,
|
|
866
|
+
staging,
|
|
867
|
+
context: runnerContextFromEnv(env)
|
|
868
|
+
});
|
|
541
869
|
}
|
|
542
870
|
|
|
543
871
|
// src/doctor.ts
|
|
@@ -646,7 +974,7 @@ function purgeLegacyToken() {
|
|
|
646
974
|
function hasLegacyToken() {
|
|
647
975
|
return inVolume(DETECT_LEGACY_TOKEN).includes("FOUND");
|
|
648
976
|
}
|
|
649
|
-
var REVOKE_NOTICE = "Removed a long-lived GitHub token that an older maesttro stored in the
|
|
977
|
+
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";
|
|
650
978
|
function purgeLegacyTokenOnce({
|
|
651
979
|
config,
|
|
652
980
|
saveConfig,
|
|
@@ -661,8 +989,8 @@ function purgeLegacyTokenOnce({
|
|
|
661
989
|
}
|
|
662
990
|
|
|
663
991
|
// src/repo.ts
|
|
664
|
-
import { appendFileSync, mkdirSync as
|
|
665
|
-
import { join as
|
|
992
|
+
import { appendFileSync, mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
993
|
+
import { join as join5 } from "node:path";
|
|
666
994
|
var MINIMUM_GIT = [2, 48];
|
|
667
995
|
var WORKTREES_DIR = ".worktrees";
|
|
668
996
|
|
|
@@ -705,7 +1033,7 @@ function repoRoot(run, cwd) {
|
|
|
705
1033
|
"--git-common-dir"
|
|
706
1034
|
]).out;
|
|
707
1035
|
const root = realpathSync(top.out);
|
|
708
|
-
if (realpathSync(commonDir) !==
|
|
1036
|
+
if (realpathSync(commonDir) !== join5(root, ".git")) {
|
|
709
1037
|
throw new LaunchError("This is a git worktree. Run maesttro from the main checkout; each session gets its own isolated clone.");
|
|
710
1038
|
}
|
|
711
1039
|
if (realpathSync(cwd) !== root) {
|
|
@@ -732,12 +1060,12 @@ function defaultBranch(run, root) {
|
|
|
732
1060
|
throw new LaunchError("Can't find the default branch of origin. Run `git remote set-head origin --auto`.");
|
|
733
1061
|
}
|
|
734
1062
|
function ensureWorktreesDir(run, root) {
|
|
735
|
-
|
|
1063
|
+
mkdirSync5(join5(root, WORKTREES_DIR), { recursive: true });
|
|
736
1064
|
if (git(run, root, ["check-ignore", "-q", `${WORKTREES_DIR}/`]).ok)
|
|
737
1065
|
return;
|
|
738
|
-
const info =
|
|
739
|
-
|
|
740
|
-
appendFileSync(
|
|
1066
|
+
const info = join5(root, ".git", "info");
|
|
1067
|
+
mkdirSync5(info, { recursive: true });
|
|
1068
|
+
appendFileSync(join5(info, "exclude"), `
|
|
741
1069
|
${WORKTREES_DIR}/
|
|
742
1070
|
`);
|
|
743
1071
|
}
|
|
@@ -779,7 +1107,7 @@ async function check(label, probe) {
|
|
|
779
1107
|
return false;
|
|
780
1108
|
}
|
|
781
1109
|
}
|
|
782
|
-
function
|
|
1110
|
+
function inRunner(command, { withVolume = true } = {}) {
|
|
783
1111
|
const volume = withVolume ? `-v ${VOLUME}:/home/agent ` : "";
|
|
784
1112
|
return execSync2(`docker run --rm ${hostIdentityFlags()} ${volume}${IMAGE} "${command}"`).toString();
|
|
785
1113
|
}
|
|
@@ -802,7 +1130,7 @@ async function doctor() {
|
|
|
802
1130
|
}
|
|
803
1131
|
],
|
|
804
1132
|
[
|
|
805
|
-
"
|
|
1133
|
+
"Runner image built",
|
|
806
1134
|
() => `built ${execSync2(`docker image inspect ${IMAGE} --format "{{.Created}}"`).toString().trim()}`
|
|
807
1135
|
],
|
|
808
1136
|
[
|
|
@@ -845,9 +1173,9 @@ async function doctor() {
|
|
|
845
1173
|
}
|
|
846
1174
|
],
|
|
847
1175
|
[
|
|
848
|
-
"Personal SSH key NOT visible inside
|
|
1176
|
+
"Personal SSH key NOT visible inside runner (isolation intact)",
|
|
849
1177
|
() => {
|
|
850
|
-
const out =
|
|
1178
|
+
const out = inRunner("cat ~/.ssh/id_rsa 2>&1 || echo MISSING");
|
|
851
1179
|
if (!out.includes("MISSING") && !out.includes("No such file")) {
|
|
852
1180
|
throw new Error("isolation broken");
|
|
853
1181
|
}
|
|
@@ -856,7 +1184,7 @@ async function doctor() {
|
|
|
856
1184
|
...Object.values(CLIENTS).map((client) => [
|
|
857
1185
|
`Client available: ${client.label}`,
|
|
858
1186
|
() => {
|
|
859
|
-
|
|
1187
|
+
inRunner(`which ${client.binary}`, { withVolume: false });
|
|
860
1188
|
}
|
|
861
1189
|
])
|
|
862
1190
|
];
|
|
@@ -873,12 +1201,15 @@ async function doctor() {
|
|
|
873
1201
|
}
|
|
874
1202
|
|
|
875
1203
|
// src/help.ts
|
|
876
|
-
var HELP = `maesttro — isolated
|
|
1204
|
+
var HELP = `maesttro — isolated runner for local coding agents
|
|
877
1205
|
|
|
878
1206
|
Usage:
|
|
879
|
-
maesttro Start a session from the repo root: new clone, default client (
|
|
1207
|
+
maesttro Start a session from the repo root: new clone, default client (opencode)
|
|
880
1208
|
maesttro <client> Same, with a specific client — see \`maesttro list\`
|
|
881
|
-
maesttro
|
|
1209
|
+
maesttro --strict [client]
|
|
1210
|
+
Same, in strict mode: no GitHub token reaches the runner, so \`gh\` is off
|
|
1211
|
+
maesttro resume <id> Continue a kept session in its clone, in the mode it started with
|
|
1212
|
+
(\`--strict\` or \`--no-strict\` after the id changes it)
|
|
882
1213
|
maesttro sessions List this repo's sessions (\`sessions remove <id>\` to drop one)
|
|
883
1214
|
maesttro push [branch] Push a branch a session created, after showing what goes out
|
|
884
1215
|
maesttro list Show available clients
|
|
@@ -886,57 +1217,57 @@ Usage:
|
|
|
886
1217
|
maesttro login Log in to GitHub (device flow) so sessions get repo access
|
|
887
1218
|
maesttro logout Remove the stored GitHub credentials
|
|
888
1219
|
maesttro status Show GitHub login status
|
|
889
|
-
maesttro configure Apply maesttro's client configs (also runs inside the
|
|
890
|
-
maesttro update Update the package and rebuild the image with refreshed client configs
|
|
1220
|
+
maesttro configure Apply maesttro's client configs (also runs inside the runner)
|
|
1221
|
+
maesttro update Update the package and rebuild the runner image with refreshed client configs
|
|
891
1222
|
maesttro doctor Health check — Docker, image, volume, GitHub login, broker, isolation, clients
|
|
892
1223
|
maesttro help Show this message
|
|
893
1224
|
`;
|
|
894
1225
|
|
|
895
1226
|
// src/launch.ts
|
|
896
1227
|
import { execSync as execSync3, spawn } from "node:child_process";
|
|
897
|
-
import { existsSync as
|
|
1228
|
+
import { existsSync as existsSync4, rmSync as rmSync6 } from "node:fs";
|
|
898
1229
|
import { constants, homedir as homedir4 } from "node:os";
|
|
899
|
-
import { join as
|
|
1230
|
+
import { join as join11 } from "node:path";
|
|
900
1231
|
|
|
901
1232
|
// src/backup.ts
|
|
902
1233
|
import { createHash } from "node:crypto";
|
|
903
|
-
import { mkdirSync as
|
|
904
|
-
import { basename, join as
|
|
1234
|
+
import { mkdirSync as mkdirSync7, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
|
|
1235
|
+
import { basename, join as join7 } from "node:path";
|
|
905
1236
|
|
|
906
1237
|
// src/maesttro-home.ts
|
|
907
1238
|
import {
|
|
908
1239
|
appendFileSync as appendFileSync2,
|
|
909
|
-
mkdirSync as
|
|
910
|
-
readFileSync as
|
|
1240
|
+
mkdirSync as mkdirSync6,
|
|
1241
|
+
readFileSync as readFileSync5,
|
|
911
1242
|
renameSync as renameSync2,
|
|
912
|
-
writeFileSync as
|
|
1243
|
+
writeFileSync as writeFileSync5
|
|
913
1244
|
} from "node:fs";
|
|
914
1245
|
import { homedir as homedir3 } from "node:os";
|
|
915
|
-
import { join as
|
|
1246
|
+
import { join as join6 } from "node:path";
|
|
916
1247
|
function maesttroHome() {
|
|
917
|
-
return
|
|
1248
|
+
return join6(homedir3(), ".config", "maesttro");
|
|
918
1249
|
}
|
|
919
1250
|
function configPath() {
|
|
920
|
-
return
|
|
1251
|
+
return join6(maesttroHome(), "config.json");
|
|
921
1252
|
}
|
|
922
1253
|
function readMaesttroConfig() {
|
|
923
1254
|
try {
|
|
924
|
-
return JSON.parse(
|
|
1255
|
+
return JSON.parse(readFileSync5(configPath(), "utf8"));
|
|
925
1256
|
} catch {
|
|
926
1257
|
return {};
|
|
927
1258
|
}
|
|
928
1259
|
}
|
|
929
1260
|
function writeMaesttroConfig(config) {
|
|
930
|
-
|
|
1261
|
+
mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
|
|
931
1262
|
const temporary = `${configPath()}.tmp`;
|
|
932
|
-
|
|
1263
|
+
writeFileSync5(temporary, `${JSON.stringify(config, null, 2)}
|
|
933
1264
|
`);
|
|
934
1265
|
renameSync2(temporary, configPath());
|
|
935
1266
|
}
|
|
936
|
-
function fileSessionLog(path =
|
|
1267
|
+
function fileSessionLog(path = join6(maesttroHome(), "maesttro.log")) {
|
|
937
1268
|
return (event, detail) => {
|
|
938
1269
|
try {
|
|
939
|
-
|
|
1270
|
+
mkdirSync6(maesttroHome(), { recursive: true, mode: 448 });
|
|
940
1271
|
appendFileSync2(path, `${new Date().toISOString()} ${event}${detail ? ` ${detail}` : ""}
|
|
941
1272
|
`);
|
|
942
1273
|
} catch {}
|
|
@@ -949,7 +1280,7 @@ function shortHash(value) {
|
|
|
949
1280
|
return createHash("sha256").update(value).digest("hex").slice(0, 8);
|
|
950
1281
|
}
|
|
951
1282
|
function backupDir(repoRoot, home = maesttroHome()) {
|
|
952
|
-
return
|
|
1283
|
+
return join7(home, "backups", `${basename(repoRoot)}-${shortHash(repoRoot)}`);
|
|
953
1284
|
}
|
|
954
1285
|
function refsFingerprint(refs) {
|
|
955
1286
|
const sorted = Object.keys(refs).sort().map((ref) => `${ref} ${refs[ref]}`);
|
|
@@ -967,20 +1298,20 @@ function backupRepo({
|
|
|
967
1298
|
}) {
|
|
968
1299
|
if (Object.keys(refs).length === 0)
|
|
969
1300
|
return;
|
|
970
|
-
|
|
1301
|
+
mkdirSync7(dir, { recursive: true, mode: 448 });
|
|
971
1302
|
const fingerprint = refsFingerprint(refs);
|
|
972
1303
|
const existing = readdirSync2(dir).filter((name) => name.endsWith(".bundle")).sort();
|
|
973
1304
|
const latest = existing.at(-1);
|
|
974
1305
|
if (latest?.endsWith(`-${fingerprint}.bundle`)) {
|
|
975
|
-
return { path:
|
|
1306
|
+
return { path: join7(dir, latest), reused: true };
|
|
976
1307
|
}
|
|
977
1308
|
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
978
|
-
const path =
|
|
1309
|
+
const path = join7(dir, `${stamp}-${sessionId}-${fingerprint}.bundle`);
|
|
979
1310
|
if (!git(run, repoRoot, ["bundle", "create", "--quiet", path, "--all"]).ok) {
|
|
980
1311
|
return;
|
|
981
1312
|
}
|
|
982
1313
|
for (const stale of [...existing, basename(path)].sort().slice(0, -keep)) {
|
|
983
|
-
|
|
1314
|
+
rmSync3(join7(dir, stale), { force: true });
|
|
984
1315
|
}
|
|
985
1316
|
return { path, reused: false };
|
|
986
1317
|
}
|
|
@@ -1065,20 +1396,20 @@ function startRefresher({
|
|
|
1065
1396
|
|
|
1066
1397
|
// src/session-dir.ts
|
|
1067
1398
|
import {
|
|
1068
|
-
chmodSync as
|
|
1069
|
-
mkdirSync as
|
|
1070
|
-
mkdtempSync,
|
|
1399
|
+
chmodSync as chmodSync3,
|
|
1400
|
+
mkdirSync as mkdirSync8,
|
|
1401
|
+
mkdtempSync as mkdtempSync2,
|
|
1071
1402
|
renameSync as renameSync3,
|
|
1072
|
-
rmSync as
|
|
1073
|
-
writeFileSync as
|
|
1403
|
+
rmSync as rmSync4,
|
|
1404
|
+
writeFileSync as writeFileSync6
|
|
1074
1405
|
} from "node:fs";
|
|
1075
|
-
import { tmpdir } from "node:os";
|
|
1076
|
-
import { join as
|
|
1406
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
1407
|
+
import { join as join8 } from "node:path";
|
|
1077
1408
|
var CONTAINER_GH_CONFIG_DIR = "/home/agent/.config/gh-maesttro";
|
|
1078
1409
|
function writeAtomically(path, content) {
|
|
1079
1410
|
const temporary = `${path}.tmp`;
|
|
1080
|
-
|
|
1081
|
-
|
|
1411
|
+
writeFileSync6(temporary, content, { mode: 420 });
|
|
1412
|
+
chmodSync3(temporary, 420);
|
|
1082
1413
|
renameSync3(temporary, path);
|
|
1083
1414
|
}
|
|
1084
1415
|
function hostsYaml(token, user) {
|
|
@@ -1091,26 +1422,27 @@ function hostsYaml(token, user) {
|
|
|
1091
1422
|
].join(`
|
|
1092
1423
|
`);
|
|
1093
1424
|
}
|
|
1094
|
-
function createSessionDir(parent =
|
|
1095
|
-
const root =
|
|
1096
|
-
|
|
1097
|
-
const ghDir =
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
writeAtomically(
|
|
1425
|
+
function createSessionDir(parent = tmpdir2()) {
|
|
1426
|
+
const root = mkdtempSync2(join8(parent, "maesttro-"));
|
|
1427
|
+
chmodSync3(root, 448);
|
|
1428
|
+
const ghDir = join8(root, "gh");
|
|
1429
|
+
mkdirSync8(ghDir);
|
|
1430
|
+
chmodSync3(ghDir, 493);
|
|
1431
|
+
writeAtomically(join8(ghDir, "config.yml"), `version: "1"
|
|
1101
1432
|
`);
|
|
1102
1433
|
return {
|
|
1103
1434
|
ghDir,
|
|
1104
|
-
writeToken: (token, user) => writeAtomically(
|
|
1105
|
-
writeStatus: (status) => writeAtomically(
|
|
1435
|
+
writeToken: (token, user) => writeAtomically(join8(ghDir, "hosts.yml"), hostsYaml(token, user)),
|
|
1436
|
+
writeStatus: (status) => writeAtomically(join8(ghDir, "status"), `${status}
|
|
1106
1437
|
`),
|
|
1107
|
-
remove: () =>
|
|
1438
|
+
remove: () => rmSync4(root, { recursive: true, force: true })
|
|
1108
1439
|
};
|
|
1109
1440
|
}
|
|
1110
1441
|
|
|
1111
1442
|
// src/github-session.ts
|
|
1112
1443
|
var UNAVAILABLE = "GitHub access unavailable";
|
|
1113
1444
|
var WITHOUT = "running without gh";
|
|
1445
|
+
var STRICT_NOTICE = "Strict mode: no GitHub token in the runner, so `gh` is off. Local git works; push with `maesttro push`.";
|
|
1114
1446
|
function refusalNotice(code, repo) {
|
|
1115
1447
|
switch (code) {
|
|
1116
1448
|
case "broker_unreachable":
|
|
@@ -1153,6 +1485,7 @@ function errorCode(error) {
|
|
|
1153
1485
|
function withoutGitHub(author, notice) {
|
|
1154
1486
|
const env = {
|
|
1155
1487
|
GH_CONFIG_DIR: CONTAINER_GH_CONFIG_DIR,
|
|
1488
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1156
1489
|
...identityEnv(author, undefined)
|
|
1157
1490
|
};
|
|
1158
1491
|
return {
|
|
@@ -1165,12 +1498,15 @@ function withoutGitHub(author, notice) {
|
|
|
1165
1498
|
async function openGitHubSession({
|
|
1166
1499
|
repo,
|
|
1167
1500
|
author,
|
|
1501
|
+
strict = false,
|
|
1168
1502
|
broker,
|
|
1169
1503
|
login,
|
|
1170
1504
|
log,
|
|
1171
1505
|
fetch: fetchImpl = fetch,
|
|
1172
1506
|
createDir = createSessionDir
|
|
1173
1507
|
}) {
|
|
1508
|
+
if (strict)
|
|
1509
|
+
return withoutGitHub(author, STRICT_NOTICE);
|
|
1174
1510
|
if (!repo)
|
|
1175
1511
|
return withoutGitHub(author);
|
|
1176
1512
|
const mint = async ({ interactive }) => {
|
|
@@ -1247,13 +1583,13 @@ function ask(question) {
|
|
|
1247
1583
|
// src/session-clone.ts
|
|
1248
1584
|
import {
|
|
1249
1585
|
copyFileSync,
|
|
1250
|
-
existsSync as
|
|
1586
|
+
existsSync as existsSync3,
|
|
1251
1587
|
lstatSync,
|
|
1252
|
-
mkdirSync as
|
|
1588
|
+
mkdirSync as mkdirSync9,
|
|
1253
1589
|
readdirSync as readdirSync3,
|
|
1254
|
-
readFileSync as
|
|
1590
|
+
readFileSync as readFileSync6
|
|
1255
1591
|
} from "node:fs";
|
|
1256
|
-
import { join as
|
|
1592
|
+
import { join as join9 } from "node:path";
|
|
1257
1593
|
|
|
1258
1594
|
// src/sanitize.ts
|
|
1259
1595
|
function stripControl(text) {
|
|
@@ -1345,14 +1681,14 @@ function dirents(dir) {
|
|
|
1345
1681
|
}
|
|
1346
1682
|
}
|
|
1347
1683
|
function cloneGitDir(clonePath) {
|
|
1348
|
-
const gitDir =
|
|
1684
|
+
const gitDir = join9(clonePath, ".git");
|
|
1349
1685
|
return realDir(gitDir) ? gitDir : undefined;
|
|
1350
1686
|
}
|
|
1351
1687
|
function readSha(path) {
|
|
1352
1688
|
try {
|
|
1353
1689
|
if (!lstatSync(path).isFile())
|
|
1354
1690
|
return;
|
|
1355
|
-
const text =
|
|
1691
|
+
const text = readFileSync6(path, "utf8").trim();
|
|
1356
1692
|
return SHA.test(text) ? text : undefined;
|
|
1357
1693
|
} catch {
|
|
1358
1694
|
return;
|
|
@@ -1362,7 +1698,7 @@ function packedRefs(gitDir) {
|
|
|
1362
1698
|
const refs = new Map;
|
|
1363
1699
|
let text;
|
|
1364
1700
|
try {
|
|
1365
|
-
text =
|
|
1701
|
+
text = readFileSync6(join9(gitDir, "packed-refs"), "utf8");
|
|
1366
1702
|
} catch {
|
|
1367
1703
|
return refs;
|
|
1368
1704
|
}
|
|
@@ -1387,12 +1723,12 @@ function cloneBranches(clonePath) {
|
|
|
1387
1723
|
continue;
|
|
1388
1724
|
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1389
1725
|
if (entry.isDirectory())
|
|
1390
|
-
walk(
|
|
1726
|
+
walk(join9(dir, entry.name), name);
|
|
1391
1727
|
else if (entry.isFile())
|
|
1392
1728
|
names.add(name);
|
|
1393
1729
|
}
|
|
1394
1730
|
};
|
|
1395
|
-
walk(
|
|
1731
|
+
walk(join9(gitDir, "refs", "heads"), "");
|
|
1396
1732
|
for (const ref of packedRefs(gitDir).keys()) {
|
|
1397
1733
|
if (ref.startsWith(HEADS_PREFIX))
|
|
1398
1734
|
names.add(ref.slice(HEADS_PREFIX.length));
|
|
@@ -1405,7 +1741,7 @@ function branchTip(clonePath, branch) {
|
|
|
1405
1741
|
const gitDir = cloneGitDir(clonePath);
|
|
1406
1742
|
if (!gitDir)
|
|
1407
1743
|
return;
|
|
1408
|
-
return readSha(
|
|
1744
|
+
return readSha(join9(gitDir, "refs", "heads", branch)) ?? packedRefs(gitDir).get(`${HEADS_PREFIX}${branch}`);
|
|
1409
1745
|
}
|
|
1410
1746
|
function cloneHead(clonePath) {
|
|
1411
1747
|
const gitDir = cloneGitDir(clonePath);
|
|
@@ -1413,9 +1749,9 @@ function cloneHead(clonePath) {
|
|
|
1413
1749
|
return;
|
|
1414
1750
|
let text;
|
|
1415
1751
|
try {
|
|
1416
|
-
if (!lstatSync(
|
|
1752
|
+
if (!lstatSync(join9(gitDir, "HEAD")).isFile())
|
|
1417
1753
|
return;
|
|
1418
|
-
text =
|
|
1754
|
+
text = readFileSync6(join9(gitDir, "HEAD"), "utf8").trim();
|
|
1419
1755
|
} catch {
|
|
1420
1756
|
return;
|
|
1421
1757
|
}
|
|
@@ -1436,31 +1772,31 @@ function copyObjectFiles(fromDir, intoDir) {
|
|
|
1436
1772
|
for (const entry of dirents(fromDir)) {
|
|
1437
1773
|
if (!entry.isFile())
|
|
1438
1774
|
continue;
|
|
1439
|
-
const dest =
|
|
1440
|
-
if (
|
|
1775
|
+
const dest = join9(intoDir, entry.name);
|
|
1776
|
+
if (existsSync3(dest))
|
|
1441
1777
|
continue;
|
|
1442
1778
|
if (!created) {
|
|
1443
|
-
|
|
1779
|
+
mkdirSync9(intoDir, { recursive: true });
|
|
1444
1780
|
created = true;
|
|
1445
1781
|
}
|
|
1446
|
-
copyFileSync(
|
|
1782
|
+
copyFileSync(join9(fromDir, entry.name), dest);
|
|
1447
1783
|
}
|
|
1448
1784
|
}
|
|
1449
1785
|
function importObjects(run, root, clonePath, tip) {
|
|
1450
1786
|
const gitDir = cloneGitDir(clonePath);
|
|
1451
1787
|
if (!gitDir)
|
|
1452
1788
|
return false;
|
|
1453
|
-
const objects =
|
|
1789
|
+
const objects = join9(gitDir, "objects");
|
|
1454
1790
|
if (!realDir(objects))
|
|
1455
1791
|
return false;
|
|
1456
|
-
const into =
|
|
1792
|
+
const into = join9(root, ".git", "objects");
|
|
1457
1793
|
for (const entry of dirents(objects)) {
|
|
1458
1794
|
if (entry.name === "info" || entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
1459
1795
|
continue;
|
|
1460
1796
|
}
|
|
1461
1797
|
if (!OBJECT_SUBDIR.test(entry.name) && entry.name !== "pack")
|
|
1462
1798
|
continue;
|
|
1463
|
-
copyObjectFiles(
|
|
1799
|
+
copyObjectFiles(join9(objects, entry.name), join9(into, entry.name));
|
|
1464
1800
|
}
|
|
1465
1801
|
return git(run, root, ["cat-file", "-e", `${tip}^{commit}`]).ok;
|
|
1466
1802
|
}
|
|
@@ -1468,36 +1804,36 @@ function importObjects(run, root, clonePath, tip) {
|
|
|
1468
1804
|
// src/sessions.ts
|
|
1469
1805
|
import { randomBytes } from "node:crypto";
|
|
1470
1806
|
import {
|
|
1471
|
-
mkdirSync as
|
|
1807
|
+
mkdirSync as mkdirSync10,
|
|
1472
1808
|
readdirSync as readdirSync4,
|
|
1473
|
-
readFileSync as
|
|
1809
|
+
readFileSync as readFileSync7,
|
|
1474
1810
|
renameSync as renameSync4,
|
|
1475
|
-
rmSync as
|
|
1476
|
-
writeFileSync as
|
|
1811
|
+
rmSync as rmSync5,
|
|
1812
|
+
writeFileSync as writeFileSync7
|
|
1477
1813
|
} from "node:fs";
|
|
1478
|
-
import { join as
|
|
1814
|
+
import { join as join10 } from "node:path";
|
|
1479
1815
|
function newSessionId() {
|
|
1480
1816
|
return `maesttro-${randomBytes(4).toString("hex")}`;
|
|
1481
1817
|
}
|
|
1482
1818
|
function sessionsDir(home = maesttroHome()) {
|
|
1483
|
-
return
|
|
1819
|
+
return join10(home, "sessions");
|
|
1484
1820
|
}
|
|
1485
1821
|
function saveSession(record, dir = sessionsDir()) {
|
|
1486
|
-
|
|
1487
|
-
const path =
|
|
1488
|
-
|
|
1822
|
+
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
1823
|
+
const path = join10(dir, `${record.id}.json`);
|
|
1824
|
+
writeFileSync7(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
|
|
1489
1825
|
`);
|
|
1490
1826
|
renameSync4(`${path}.tmp`, path);
|
|
1491
1827
|
}
|
|
1492
1828
|
function readSession(id, dir = sessionsDir()) {
|
|
1493
1829
|
try {
|
|
1494
|
-
return JSON.parse(
|
|
1830
|
+
return JSON.parse(readFileSync7(join10(dir, `${id}.json`), "utf8"));
|
|
1495
1831
|
} catch {
|
|
1496
1832
|
return;
|
|
1497
1833
|
}
|
|
1498
1834
|
}
|
|
1499
1835
|
function deleteSession(id, dir = sessionsDir()) {
|
|
1500
|
-
|
|
1836
|
+
rmSync5(join10(dir, `${id}.json`), { force: true });
|
|
1501
1837
|
}
|
|
1502
1838
|
function sessionsForRepo(repoRoot, dir = sessionsDir()) {
|
|
1503
1839
|
let names;
|
|
@@ -1555,19 +1891,25 @@ function sessionMounts({
|
|
|
1555
1891
|
packageRoot,
|
|
1556
1892
|
hostHome,
|
|
1557
1893
|
stagedPaths,
|
|
1894
|
+
clientConfigs,
|
|
1558
1895
|
sharedSkills
|
|
1559
1896
|
}) {
|
|
1560
|
-
const objects =
|
|
1897
|
+
const objects = join11(repoRoot, ".git", "objects");
|
|
1561
1898
|
const mounts = [
|
|
1562
1899
|
{ host: VOLUME, container: "/home/agent" },
|
|
1563
1900
|
{ host: objects, container: objects, readOnly: true },
|
|
1564
1901
|
{ host: sessionDir, container: CONTAINER_REPO },
|
|
1565
1902
|
{ host: packageRoot, container: CONTAINER_PACKAGE, readOnly: true },
|
|
1566
1903
|
...stagedPaths.map((path) => ({
|
|
1567
|
-
host:
|
|
1904
|
+
host: join11(hostHome, path),
|
|
1568
1905
|
container: `${HOST_STAGING_DIR}/${path}`,
|
|
1569
1906
|
readOnly: true
|
|
1570
|
-
}))
|
|
1907
|
+
})),
|
|
1908
|
+
{
|
|
1909
|
+
host: clientConfigs,
|
|
1910
|
+
container: CONTAINER_CLIENT_CONFIGS,
|
|
1911
|
+
readOnly: true
|
|
1912
|
+
}
|
|
1571
1913
|
];
|
|
1572
1914
|
if (sharedSkills) {
|
|
1573
1915
|
mounts.push({
|
|
@@ -1616,12 +1958,12 @@ function ensureReady() {
|
|
|
1616
1958
|
execSync3(`docker image inspect ${IMAGE}`, { stdio: "ignore" });
|
|
1617
1959
|
execSync3(`docker volume inspect ${VOLUME}`, { stdio: "ignore" });
|
|
1618
1960
|
} catch {
|
|
1619
|
-
throw new LaunchError("
|
|
1961
|
+
throw new LaunchError("Runner isn't set up yet. Run `maesttro setup` first.");
|
|
1620
1962
|
}
|
|
1621
1963
|
}
|
|
1622
1964
|
function sharedSkillsDir() {
|
|
1623
|
-
const dir = process.env.AGENT_SHARED_SKILLS ??
|
|
1624
|
-
return
|
|
1965
|
+
const dir = process.env.AGENT_SHARED_SKILLS ?? join11(homedir4(), "agent-shared", "skills");
|
|
1966
|
+
return existsSync4(dir) ? dir : undefined;
|
|
1625
1967
|
}
|
|
1626
1968
|
async function resolveAuthor(workspace) {
|
|
1627
1969
|
const fromRepo = workspaceAuthor(runCommand, workspace);
|
|
@@ -1632,7 +1974,7 @@ async function resolveAuthor(workspace) {
|
|
|
1632
1974
|
return config.author;
|
|
1633
1975
|
if (!process.stdin.isTTY)
|
|
1634
1976
|
return;
|
|
1635
|
-
console.log("No git identity found for this repo. Commits in the
|
|
1977
|
+
console.log("No git identity found for this repo. Commits in the runner need an author (stored in ~/.config/maesttro/config.json).");
|
|
1636
1978
|
const name = await ask("Author name: ");
|
|
1637
1979
|
const email = await ask("Author email: ");
|
|
1638
1980
|
if (!name || !email)
|
|
@@ -1650,7 +1992,7 @@ function startSession(run, root) {
|
|
|
1650
1992
|
if (!baseSha)
|
|
1651
1993
|
throw new LaunchError(`Can't resolve ${base}.`);
|
|
1652
1994
|
const id = newSessionId();
|
|
1653
|
-
const sessionDir =
|
|
1995
|
+
const sessionDir = join11(root, WORKTREES_DIR, id);
|
|
1654
1996
|
createSessionClone(run, root, sessionDir, baseSha);
|
|
1655
1997
|
return {
|
|
1656
1998
|
id,
|
|
@@ -1667,7 +2009,7 @@ function resumeSession(root, id) {
|
|
|
1667
2009
|
if (!session || session.repoRoot !== root) {
|
|
1668
2010
|
throw new LaunchError(`No session ${id} in this repo. See \`maesttro sessions\`.`);
|
|
1669
2011
|
}
|
|
1670
|
-
if (!
|
|
2012
|
+
if (!existsSync4(session.worktree)) {
|
|
1671
2013
|
throw new LaunchError(`The clone for ${id} is gone: ${session.worktree}`);
|
|
1672
2014
|
}
|
|
1673
2015
|
return session;
|
|
@@ -1690,13 +2032,13 @@ function preserveBranches(run, session) {
|
|
|
1690
2032
|
return preserved;
|
|
1691
2033
|
}
|
|
1692
2034
|
function finishSession(run, session) {
|
|
1693
|
-
if (!
|
|
2035
|
+
if (!existsSync4(session.worktree)) {
|
|
1694
2036
|
deleteSession(session.id);
|
|
1695
2037
|
return;
|
|
1696
2038
|
}
|
|
1697
2039
|
const preserved = preserveBranches(run, session);
|
|
1698
2040
|
if (!cloneHasWork(session.worktree, session.baseSha)) {
|
|
1699
|
-
|
|
2041
|
+
rmSync6(session.worktree, { recursive: true, force: true });
|
|
1700
2042
|
deleteSession(session.id);
|
|
1701
2043
|
return;
|
|
1702
2044
|
}
|
|
@@ -1710,6 +2052,18 @@ Session ${session.id} kept. Continue with \`maesttro resume ${session.id}\`.`);
|
|
|
1710
2052
|
console.log("You have commits on a detached HEAD. Resume and `git switch -c <name>` to keep them.");
|
|
1711
2053
|
}
|
|
1712
2054
|
}
|
|
2055
|
+
function isStrict(requested, session) {
|
|
2056
|
+
return requested ?? session.strict ?? false;
|
|
2057
|
+
}
|
|
2058
|
+
function runnerGitHubMode({
|
|
2059
|
+
strict,
|
|
2060
|
+
repo,
|
|
2061
|
+
notice
|
|
2062
|
+
}) {
|
|
2063
|
+
if (strict)
|
|
2064
|
+
return "strict";
|
|
2065
|
+
return notice || !repo ? "off" : "on";
|
|
2066
|
+
}
|
|
1713
2067
|
function exitCodeOf(code, signal) {
|
|
1714
2068
|
if (code !== null)
|
|
1715
2069
|
return code;
|
|
@@ -1718,7 +2072,8 @@ function exitCodeOf(code, signal) {
|
|
|
1718
2072
|
var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
1719
2073
|
async function launch({
|
|
1720
2074
|
args,
|
|
1721
|
-
resume
|
|
2075
|
+
resume,
|
|
2076
|
+
strict: requestedStrict
|
|
1722
2077
|
}) {
|
|
1723
2078
|
const run = runCommand;
|
|
1724
2079
|
ensureReady();
|
|
@@ -1736,9 +2091,14 @@ async function launch({
|
|
|
1736
2091
|
refs: snapshotRefs(run, root),
|
|
1737
2092
|
sessionId: started.id
|
|
1738
2093
|
});
|
|
1739
|
-
const
|
|
2094
|
+
const strict = isStrict(requestedStrict, started);
|
|
2095
|
+
const saved = {
|
|
2096
|
+
...started,
|
|
2097
|
+
backup: started.backup ?? backup?.path,
|
|
2098
|
+
strict
|
|
2099
|
+
};
|
|
1740
2100
|
saveSession(saved);
|
|
1741
|
-
console.log(`Session ${saved.id}: clone .worktrees/${saved.id} at ${saved.base}${backup ? `, backup ${backup.reused ? "reused" : "written"}` : ""}.`);
|
|
2101
|
+
console.log(`Session ${saved.id}${strict ? " (strict)" : ""}: clone .worktrees/${saved.id} at ${saved.base}${backup ? `, backup ${backup.reused ? "reused" : "written"}` : ""}.`);
|
|
1742
2102
|
const repo = workspaceRepo(run, root);
|
|
1743
2103
|
if (repo && !resume) {
|
|
1744
2104
|
git(run, saved.worktree, [
|
|
@@ -1751,6 +2111,7 @@ async function launch({
|
|
|
1751
2111
|
const github = await openGitHubSession({
|
|
1752
2112
|
repo,
|
|
1753
2113
|
author: await resolveAuthor(root),
|
|
2114
|
+
strict,
|
|
1754
2115
|
broker: brokerUrl(),
|
|
1755
2116
|
login: {
|
|
1756
2117
|
clientId: githubAppClientId(),
|
|
@@ -1762,19 +2123,28 @@ async function launch({
|
|
|
1762
2123
|
if (github.notice)
|
|
1763
2124
|
console.log(github.notice);
|
|
1764
2125
|
const hostHome = homedir4();
|
|
2126
|
+
const clientConfigs = buildClientConfigs({
|
|
2127
|
+
host: readHostClientConfigs({ home: hostHome }),
|
|
2128
|
+
url: maesttroUrl()
|
|
2129
|
+
});
|
|
2130
|
+
if (clientConfigs.removed.length > 0) {
|
|
2131
|
+
console.log(`Kept out of the runner as secrets: ${clientConfigs.removed.join(", ")}`);
|
|
2132
|
+
}
|
|
2133
|
+
const clientConfigDir = writeClientConfigDir(clientConfigs.files);
|
|
1765
2134
|
const { mounts, workdir } = sessionMounts({
|
|
1766
2135
|
repoRoot: root,
|
|
1767
2136
|
sessionDir: saved.worktree,
|
|
1768
2137
|
packageRoot: PACKAGE_ROOT,
|
|
1769
2138
|
hostHome,
|
|
1770
|
-
stagedPaths: STAGED_PATHS.filter((path) =>
|
|
2139
|
+
stagedPaths: STAGED_PATHS.filter((path) => existsSync4(join11(hostHome, path))),
|
|
2140
|
+
clientConfigs: clientConfigDir.dir,
|
|
1771
2141
|
sharedSkills: sharedSkillsDir()
|
|
1772
2142
|
});
|
|
1773
2143
|
const sessionEnv = {
|
|
1774
2144
|
...github.env,
|
|
1775
2145
|
MAESTTRO_SESSION_ID: saved.id,
|
|
1776
2146
|
MAESTTRO_BASE: saved.base,
|
|
1777
|
-
MAESTTRO_GITHUB:
|
|
2147
|
+
MAESTTRO_GITHUB: runnerGitHubMode({ strict, repo, notice: github.notice }),
|
|
1778
2148
|
...hostIdentityEnv(),
|
|
1779
2149
|
...repo ? { MAESTTRO_REPO: repo } : {}
|
|
1780
2150
|
};
|
|
@@ -1801,6 +2171,7 @@ async function launch({
|
|
|
1801
2171
|
for (const signal of FORWARDED_SIGNALS)
|
|
1802
2172
|
process.off(signal, forward);
|
|
1803
2173
|
await github.close();
|
|
2174
|
+
clientConfigDir.remove();
|
|
1804
2175
|
finishSession(run, saved);
|
|
1805
2176
|
process.exit(exitCode);
|
|
1806
2177
|
}
|
|
@@ -1925,7 +2296,7 @@ Push? [y/N] `);
|
|
|
1925
2296
|
}
|
|
1926
2297
|
|
|
1927
2298
|
// src/sessions-command.ts
|
|
1928
|
-
import { existsSync as
|
|
2299
|
+
import { existsSync as existsSync5, rmSync as rmSync7 } from "node:fs";
|
|
1929
2300
|
function sessionsCommand({ args }) {
|
|
1930
2301
|
const run = runCommand;
|
|
1931
2302
|
const root = repoRoot(run, process.cwd());
|
|
@@ -1937,7 +2308,7 @@ function sessionsCommand({ args }) {
|
|
|
1937
2308
|
process.exitCode = 1;
|
|
1938
2309
|
return;
|
|
1939
2310
|
}
|
|
1940
|
-
if (
|
|
2311
|
+
if (existsSync5(session.worktree)) {
|
|
1941
2312
|
const preserved = preserveBranches(run, session);
|
|
1942
2313
|
const head = cloneHead(session.worktree);
|
|
1943
2314
|
if (preserved.length === 0 && head?.detached !== undefined && head.detached !== session.baseSha) {
|
|
@@ -1945,7 +2316,7 @@ function sessionsCommand({ args }) {
|
|
|
1945
2316
|
process.exitCode = 1;
|
|
1946
2317
|
return;
|
|
1947
2318
|
}
|
|
1948
|
-
|
|
2319
|
+
rmSync7(session.worktree, { recursive: true, force: true });
|
|
1949
2320
|
const kept = preserved.length > 0 ? ` Its branches are kept as refs/maesttro/${session.id}/*.` : "";
|
|
1950
2321
|
console.log(`Removed session ${session.id}.${kept}`);
|
|
1951
2322
|
} else {
|
|
@@ -1960,9 +2331,10 @@ function sessionsCommand({ args }) {
|
|
|
1960
2331
|
return;
|
|
1961
2332
|
}
|
|
1962
2333
|
for (const session of sessions) {
|
|
1963
|
-
const branches =
|
|
2334
|
+
const branches = existsSync5(session.worktree) ? cloneBranches(session.worktree) : undefined;
|
|
1964
2335
|
const where = branches === undefined ? "clone missing" : branches.length > 0 ? `branches: ${branches.join(", ")}` : "no branches yet";
|
|
1965
|
-
|
|
2336
|
+
const mode = session.strict ? " strict" : "";
|
|
2337
|
+
console.log(` ${session.id} ${session.createdAt.slice(0, 16)} ${where}${mode}`);
|
|
1966
2338
|
}
|
|
1967
2339
|
console.log("\n`maesttro resume <id>` continues one; `maesttro sessions remove <id>` removes its clone.");
|
|
1968
2340
|
}
|
|
@@ -1984,7 +2356,7 @@ async function setup({ args }) {
|
|
|
1984
2356
|
console.log(`== maesttro setup ==
|
|
1985
2357
|
`);
|
|
1986
2358
|
ensureDocker();
|
|
1987
|
-
console.log("-> Building
|
|
2359
|
+
console.log("-> Building runner image (this pulls node:22-slim + installs CLIs, first run only)...");
|
|
1988
2360
|
run(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`);
|
|
1989
2361
|
console.log(`
|
|
1990
2362
|
-> Ensuring persistent volume exists...`);
|
|
@@ -2010,10 +2382,10 @@ async function setup({ args }) {
|
|
|
2010
2382
|
|
|
2011
2383
|
// src/update.ts
|
|
2012
2384
|
import { execSync as execSync5 } from "node:child_process";
|
|
2013
|
-
import { readFileSync as
|
|
2014
|
-
import { join as
|
|
2385
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
2386
|
+
import { join as join12 } from "node:path";
|
|
2015
2387
|
function installedPackageName() {
|
|
2016
|
-
const manifest = JSON.parse(
|
|
2388
|
+
const manifest = JSON.parse(readFileSync8(join12(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
2017
2389
|
return manifest.name;
|
|
2018
2390
|
}
|
|
2019
2391
|
function update() {
|
|
@@ -2023,11 +2395,11 @@ function update() {
|
|
|
2023
2395
|
console.log(`-> Updating ${packageName} package...`);
|
|
2024
2396
|
execSync5(`npm i -g ${packageName}@latest`, { stdio: "inherit" });
|
|
2025
2397
|
console.log(`
|
|
2026
|
-
-> Rebuilding
|
|
2398
|
+
-> Rebuilding runner image with refreshed client configs...`);
|
|
2027
2399
|
execSync5(`docker build -t ${IMAGE} "${DOCKERFILE_DIR}"`, { stdio: "inherit" });
|
|
2028
2400
|
console.log(`
|
|
2029
2401
|
Update complete. Bot identity and your project files are untouched —`);
|
|
2030
|
-
console.log("only the image (client versions + curated configs) was refreshed.");
|
|
2402
|
+
console.log("only the runner image (client versions + curated configs) was refreshed.");
|
|
2031
2403
|
}
|
|
2032
2404
|
|
|
2033
2405
|
// src/main.ts
|
|
@@ -2044,11 +2416,48 @@ var defaultCommands = {
|
|
|
2044
2416
|
status,
|
|
2045
2417
|
print: (text) => console.log(text)
|
|
2046
2418
|
};
|
|
2419
|
+
var MODE_FLAGS = {
|
|
2420
|
+
"--strict": true,
|
|
2421
|
+
"--no-strict": false
|
|
2422
|
+
};
|
|
2423
|
+
function takeModeFlags(args) {
|
|
2424
|
+
let strict;
|
|
2425
|
+
let index = 0;
|
|
2426
|
+
while (index < args.length && Object.hasOwn(MODE_FLAGS, args[index] ?? "")) {
|
|
2427
|
+
strict = MODE_FLAGS[args[index] ?? ""];
|
|
2428
|
+
index++;
|
|
2429
|
+
}
|
|
2430
|
+
return {
|
|
2431
|
+
...strict === undefined ? {} : { strict },
|
|
2432
|
+
rest: args.slice(index)
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
var NOT_LAUNCHING = new Set([
|
|
2436
|
+
"setup",
|
|
2437
|
+
"update",
|
|
2438
|
+
"doctor",
|
|
2439
|
+
"login",
|
|
2440
|
+
"logout",
|
|
2441
|
+
"status",
|
|
2442
|
+
"push",
|
|
2443
|
+
"sessions",
|
|
2444
|
+
"configure",
|
|
2445
|
+
"list",
|
|
2446
|
+
"help",
|
|
2447
|
+
"--help",
|
|
2448
|
+
"-h"
|
|
2449
|
+
]);
|
|
2047
2450
|
async function main(argv, commands = defaultCommands) {
|
|
2048
|
-
const
|
|
2451
|
+
const leading = takeModeFlags(argv);
|
|
2452
|
+
const [command, ...rest] = leading.rest;
|
|
2453
|
+
const mode = leading.strict === undefined ? {} : { strict: leading.strict };
|
|
2454
|
+
if (leading.strict !== undefined && command !== undefined && NOT_LAUNCHING.has(command)) {
|
|
2455
|
+
commands.print(`--strict and --no-strict apply to launching a session, not to \`${command}\`.`);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2049
2458
|
switch (command) {
|
|
2050
2459
|
case undefined:
|
|
2051
|
-
await commands.launch({ args: [] });
|
|
2460
|
+
await commands.launch({ args: [], ...mode });
|
|
2052
2461
|
return;
|
|
2053
2462
|
case "setup":
|
|
2054
2463
|
await commands.setup({ args: rest });
|
|
@@ -2075,12 +2484,19 @@ async function main(argv, commands = defaultCommands) {
|
|
|
2075
2484
|
await commands.sessions({ args: rest });
|
|
2076
2485
|
return;
|
|
2077
2486
|
case "resume": {
|
|
2078
|
-
const
|
|
2487
|
+
const beforeId = takeModeFlags(rest);
|
|
2488
|
+
const [id, ...afterId] = beforeId.rest;
|
|
2489
|
+
const afterIdFlags = takeModeFlags(afterId);
|
|
2490
|
+
const strict = afterIdFlags.strict ?? beforeId.strict ?? leading.strict;
|
|
2079
2491
|
if (!id) {
|
|
2080
|
-
commands.print("Usage: maesttro resume <session-id> [client] [args...]");
|
|
2492
|
+
commands.print("Usage: maesttro resume <session-id> [--strict|--no-strict] [client] [args...]");
|
|
2081
2493
|
return;
|
|
2082
2494
|
}
|
|
2083
|
-
await commands.launch({
|
|
2495
|
+
await commands.launch({
|
|
2496
|
+
args: afterIdFlags.rest,
|
|
2497
|
+
resume: id,
|
|
2498
|
+
...strict === undefined ? {} : { strict }
|
|
2499
|
+
});
|
|
2084
2500
|
return;
|
|
2085
2501
|
}
|
|
2086
2502
|
case "configure":
|
|
@@ -2095,7 +2511,7 @@ async function main(argv, commands = defaultCommands) {
|
|
|
2095
2511
|
commands.print(HELP);
|
|
2096
2512
|
return;
|
|
2097
2513
|
default:
|
|
2098
|
-
await commands.launch({ args:
|
|
2514
|
+
await commands.launch({ args: leading.rest, ...mode });
|
|
2099
2515
|
}
|
|
2100
2516
|
}
|
|
2101
2517
|
|