maesttro 0.4.3 → 0.4.4

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 CHANGED
@@ -61,8 +61,11 @@ maesttro opencode # same, with opencode
61
61
  maesttro --strict [client] # same, with no GitHub token in the runner (no `gh`)
62
62
  maesttro bash # plain shell inside the runner, for debugging
63
63
  maesttro sessions # list this repo's kept sessions
64
+ maesttro sessions remove <id> # drop one (preserves branches; --force discards unbranched commits)
65
+ maesttro sessions remove --all # drop all of this repo's sessions
64
66
  maesttro resume <id> # continue one, in the mode it started with
65
67
  maesttro push [branch] # push a branch a session created (shows what goes out, asks first)
68
+ maesttro push [branch] --force # same, as a force push
66
69
 
67
70
  maesttro update # bump the package + rebuild image with refreshed configs
68
71
  ```
package/dist/maesttro.js CHANGED
@@ -1265,8 +1265,14 @@ Usage:
1265
1265
  Same, in strict mode: no GitHub token reaches the runner, so \`gh\` is off
1266
1266
  maesttro resume <id> Continue a kept session in its clone, in the mode it started with
1267
1267
  (\`--strict\` or \`--no-strict\` after the id changes it)
1268
- maesttro sessions List this repo's sessions (\`sessions remove <id>\` to drop one)
1269
- maesttro push [branch] Push a branch a session created, after showing what goes out
1268
+ maesttro sessions List this repo's sessions
1269
+ maesttro sessions remove <id>
1270
+ Drop one. Preserves its branches; asks before discarding commits that
1271
+ aren't on a branch (--force skips the ask)
1272
+ maesttro sessions remove --all
1273
+ Drop all of this repo's sessions
1274
+ maesttro push [branch] [--force]
1275
+ Push a branch a session created, after showing what goes out
1270
1276
  maesttro list Show available clients
1271
1277
  maesttro setup One-time: build image, create volume, log in to GitHub
1272
1278
  maesttro login Log in to GitHub (device flow) so sessions get repo access
@@ -2277,6 +2283,32 @@ async function launch({
2277
2283
 
2278
2284
  // src/push.ts
2279
2285
  import { spawnSync as spawnSync3 } from "node:child_process";
2286
+ var CONFIRM_FLAGS = new Set(["--yes", "-y"]);
2287
+ var FORWARDED_GIT_FLAGS = new Set(["--force"]);
2288
+ function parsePushArgs(args) {
2289
+ const positionals = args.filter((arg) => !arg.startsWith("-"));
2290
+ if (positionals.length > 1) {
2291
+ return {
2292
+ ok: false,
2293
+ reason: `Expected at most one branch, got ${positionals.join(", ")}.`
2294
+ };
2295
+ }
2296
+ const flags = args.filter((arg) => arg.startsWith("-"));
2297
+ const unsupported = flags.filter((flag) => !CONFIRM_FLAGS.has(flag) && !FORWARDED_GIT_FLAGS.has(flag));
2298
+ if (unsupported.length > 0) {
2299
+ return {
2300
+ ok: false,
2301
+ reason: `maesttro push doesn't accept ${unsupported.join(", ")}; it takes --yes and --force.`
2302
+ };
2303
+ }
2304
+ const [requested] = positionals;
2305
+ return {
2306
+ ok: true,
2307
+ ...requested === undefined ? {} : { requested },
2308
+ yes: flags.some((flag) => CONFIRM_FLAGS.has(flag)),
2309
+ gitFlags: flags.filter((flag) => FORWARDED_GIT_FLAGS.has(flag))
2310
+ };
2311
+ }
2280
2312
  function pushCandidates(sessions) {
2281
2313
  const candidates = new Map;
2282
2314
  for (const session of sessions) {
@@ -2328,7 +2360,7 @@ async function choose(candidates) {
2328
2360
  const answer = await ask("Branch to push: ");
2329
2361
  return candidates[Number(answer) - 1];
2330
2362
  }
2331
- function describe(run, root, branch, base, tip) {
2363
+ function describe(run, root, branch, base, tip, gitFlags) {
2332
2364
  const commits = git(run, root, [
2333
2365
  "log",
2334
2366
  "--format= %h %s (%an / %cn)",
@@ -2336,7 +2368,7 @@ function describe(run, root, branch, base, tip) {
2336
2368
  ]).out;
2337
2369
  const stat = git(run, root, ["diff", "--stat", `${base}...${tip}`]).out;
2338
2370
  return [
2339
- `Pushing ${branch} to origin (compared with ${base}):`,
2371
+ `Pushing ${branch} to origin${gitFlags.length ? ` with ${gitFlags.join(" ")}` : ""} (compared with ${base}):`,
2340
2372
  stripControl(commits) || " (no new commits)",
2341
2373
  stat ? `
2342
2374
  ${stripControl(stat)}` : ""
@@ -2345,8 +2377,13 @@ ${stripControl(stat)}` : ""
2345
2377
  }
2346
2378
  async function push({ args }) {
2347
2379
  const run = runCommand;
2348
- const yes = args.includes("--yes") || args.includes("-y");
2349
- const requested = args.find((arg) => !arg.startsWith("-"));
2380
+ const parsed = parsePushArgs(args);
2381
+ if (!parsed.ok) {
2382
+ console.error(parsed.reason);
2383
+ process.exitCode = 1;
2384
+ return;
2385
+ }
2386
+ const { requested, yes, gitFlags } = parsed;
2350
2387
  let root;
2351
2388
  try {
2352
2389
  root = repoRoot(run, process.cwd());
@@ -2373,7 +2410,7 @@ async function push({ args }) {
2373
2410
  return;
2374
2411
  }
2375
2412
  git(run, root, ["update-ref", `refs/maesttro/${sessionId}/${branch}`, tip]);
2376
- console.log(describe(run, root, branch, base, tip));
2413
+ console.log(describe(run, root, branch, base, tip, gitFlags));
2377
2414
  if (!yes) {
2378
2415
  if (!process.stdin.isTTY) {
2379
2416
  console.error("Not a terminal; re-run with --yes to push without confirming.");
@@ -2387,7 +2424,7 @@ Push? [y/N] `);
2387
2424
  return;
2388
2425
  }
2389
2426
  }
2390
- const pushed = spawnSync3("git", ["-C", root, "push", "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
2427
+ const pushed = spawnSync3("git", ["-C", root, "push", ...gitFlags, "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
2391
2428
  if (pushed.status === 0) {
2392
2429
  console.log(`Pushed ${branch}. Base for comparison was ${baseSha.slice(0, 8)}.`);
2393
2430
  }
@@ -2396,32 +2433,88 @@ Push? [y/N] `);
2396
2433
 
2397
2434
  // src/sessions-command.ts
2398
2435
  import { existsSync as existsSync5, rmSync as rmSync7 } from "node:fs";
2399
- function sessionsCommand({ args }) {
2436
+ function parseRemoveArgs(args) {
2437
+ let all = false;
2438
+ let force = false;
2439
+ let id;
2440
+ for (const arg of args) {
2441
+ if (arg === "--all")
2442
+ all = true;
2443
+ else if (arg === "--force")
2444
+ force = true;
2445
+ else if (id === undefined)
2446
+ id = arg;
2447
+ }
2448
+ return { all, force, id };
2449
+ }
2450
+ async function removeSession(run, session, force, ask, interactive) {
2451
+ if (existsSync5(session.worktree)) {
2452
+ const preserved = preserveBranches(run, session);
2453
+ const head = cloneHead(session.worktree);
2454
+ const unbranched = preserved.length === 0 && head?.detached !== undefined && head.detached !== session.baseSha;
2455
+ if (unbranched && !force) {
2456
+ let proceed;
2457
+ if (interactive) {
2458
+ const answer = await ask(`Session ${session.id} has commits on a detached HEAD not on any branch. Removing it discards them. Remove anyway? [y/N] `);
2459
+ proceed = /^(y(es)?)?$/i.test(answer);
2460
+ } else {
2461
+ proceed = false;
2462
+ console.error(`Session ${session.id} has commits on a detached HEAD not on any branch. Resume and create a branch, or re-run with --force.`);
2463
+ }
2464
+ if (!proceed) {
2465
+ console.log(`Kept session ${session.id}.`);
2466
+ return "kept";
2467
+ }
2468
+ }
2469
+ rmSync7(session.worktree, { recursive: true, force: true });
2470
+ const kept = preserved.length > 0 ? ` Its branches are kept as refs/maesttro/${session.id}/*.` : "";
2471
+ console.log(`Removed session ${session.id}.${kept}`);
2472
+ } else {
2473
+ console.log(`Removed session ${session.id}.`);
2474
+ }
2475
+ deleteSession(session.id);
2476
+ return "removed";
2477
+ }
2478
+ async function sessionsCommand({
2479
+ args,
2480
+ ask: ask2 = ask,
2481
+ interactive = process.stdin.isTTY === true
2482
+ }) {
2400
2483
  const run = runCommand;
2401
2484
  const root = repoRoot(run, process.cwd());
2402
- const [action, id] = args;
2485
+ const [action, ...rest] = args;
2403
2486
  if (action === "remove") {
2404
- const session = id ? readSession(id) : undefined;
2405
- if (!session || session.repoRoot !== root) {
2406
- console.error(`No session ${id ?? "(missing id)"} in this repo.`);
2407
- process.exitCode = 1;
2408
- return;
2409
- }
2410
- if (existsSync5(session.worktree)) {
2411
- const preserved = preserveBranches(run, session);
2412
- const head = cloneHead(session.worktree);
2413
- if (preserved.length === 0 && head?.detached !== undefined && head.detached !== session.baseSha) {
2414
- console.error(`${session.id} has commits on a detached HEAD not on any branch; resume and create a branch first.`);
2487
+ const { all, force, id } = parseRemoveArgs(rest);
2488
+ if (all) {
2489
+ if (id !== undefined) {
2490
+ console.error("`maesttro sessions remove` takes either `--all` or a session id, not both.");
2415
2491
  process.exitCode = 1;
2416
2492
  return;
2417
2493
  }
2418
- rmSync7(session.worktree, { recursive: true, force: true });
2419
- const kept = preserved.length > 0 ? ` Its branches are kept as refs/maesttro/${session.id}/*.` : "";
2420
- console.log(`Removed session ${session.id}.${kept}`);
2421
- } else {
2422
- console.log(`Removed session ${session.id}.`);
2494
+ const sessions = sessionsForRepo(root);
2495
+ if (sessions.length === 0) {
2496
+ console.log("No maesttro sessions in this repo.");
2497
+ return;
2498
+ }
2499
+ for (const session of sessions) {
2500
+ await removeSession(run, session, force, ask2, interactive);
2501
+ }
2502
+ return;
2423
2503
  }
2424
- deleteSession(session.id);
2504
+ if (id === undefined) {
2505
+ console.error("Usage: maesttro sessions remove <id> | maesttro sessions remove --all");
2506
+ process.exitCode = 1;
2507
+ return;
2508
+ }
2509
+ const session = readSession(id);
2510
+ if (!session || session.repoRoot !== root) {
2511
+ console.error(`No session ${id} in this repo.`);
2512
+ process.exitCode = 1;
2513
+ return;
2514
+ }
2515
+ const result = await removeSession(run, session, force, ask2, interactive);
2516
+ if (result === "kept")
2517
+ process.exitCode = 1;
2425
2518
  return;
2426
2519
  }
2427
2520
  const sessions = sessionsForRepo(root);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maesttro",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Isolated Docker runner for local coding agents (opencode, claude, ...) under a dedicated bot GitHub identity, with curated configs baked in per client.",
5
5
  "type": "module",
6
6
  "bin": {