@nanopm/cli 0.1.16 → 0.1.18

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.
Files changed (2) hide show
  1. package/dist/index.js +100 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5369,9 +5369,10 @@ function stepsLeft(plan, done) {
5369
5369
  function buildDir(home, jobId) {
5370
5370
  return `${home.replace(/\/+$/, "")}/.cache/nanopm/builds/${jobId}`;
5371
5371
  }
5372
- function buildBranch(ref, title) {
5372
+ function buildBranch(ref, title, jobId) {
5373
5373
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
5374
- return `nano/${ref.toLowerCase()}${slug ? `-${slug}` : ""}`;
5374
+ const own = jobId.replace(/[^a-z0-9]/gi, "").slice(0, 6).toLowerCase();
5375
+ return `nano/${ref.toLowerCase()}${slug ? `-${slug}` : ""}-${own}`;
5375
5376
  }
5376
5377
  var REGISTRIES = {
5377
5378
  "package-lock.json": ["registry.npmjs.org"],
@@ -5420,7 +5421,20 @@ var isDecideForMe = (answer) => /^\s*decide(\s+for\s+me)?\s*\.?\s*$/i.test(answe
5420
5421
  function answerForAgent(answer) {
5421
5422
  return isDecideForMe(answer) ? DECIDE_FOR_ME : answer;
5422
5423
  }
5423
- var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr", "handed"];
5424
+ var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr", "handed", "throw", "thrown", "closed"];
5425
+ function originOf(job) {
5426
+ const named = (job.params ?? {}).origin;
5427
+ return typeof named === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(named) ? named : job.id;
5428
+ }
5429
+ function throwRefusal(runs, jobId) {
5430
+ if (runs.some((r) => ["queued", "running", "waiting"].includes(r.status))) return "This build is on its way again: stop it first.";
5431
+ const newest = [...runs].sort((x, y) => y.created_at.localeCompare(x.created_at))[0];
5432
+ return newest?.id === jobId ? null : "This build was continued since: refresh the page.";
5433
+ }
5434
+ async function buildRuns(client, projectId, origin) {
5435
+ const { data } = await client.from("jobs").select("id, status, created_at, assigned_daemon_id, requested_by").eq("project_id", projectId).eq("skill", BUILD_SKILL).or(`id.eq.${origin},params->>origin.eq.${origin}`);
5436
+ return data ?? [];
5437
+ }
5424
5438
  var BUILD_CONFLICT = { resolve: "Let Nano resolve it", mine: "I\u2019ll do it in my terminal" };
5425
5439
  var chose = (answer, option) => {
5426
5440
  const plain = (s) => s.trim().toLowerCase().replace(/[’‘]/g, "'");
@@ -5462,6 +5476,11 @@ function buildHistory(events, at) {
5462
5476
  if (last?.kind === "paused") return found({ why: "limit" });
5463
5477
  return found({ why: "lost" });
5464
5478
  }
5479
+ function parkedDue(events, now) {
5480
+ const stop2 = [...events].reverse().find((e) => e.kind === "paused" || e.kind === "question");
5481
+ if (stop2?.kind === "paused") return Date.parse(String(stop2.payload.resets_at ?? "")) <= now;
5482
+ return stop2?.kind === "question" && events.some((e) => e.kind === "answer" && e.seq === stop2.seq);
5483
+ }
5465
5484
  function wasOf(params) {
5466
5485
  return params?.was === "proposed" ? "proposed" : "accepted";
5467
5486
  }
@@ -5570,6 +5589,15 @@ async function addWorktree(clone, dir, branch) {
5570
5589
  async function removeWorktree(clone, dir) {
5571
5590
  await git(clone, ["worktree", "remove", "--force", dir]).catch(() => "");
5572
5591
  }
5592
+ async function deleteBranch(clone, branch) {
5593
+ return git(clone, ["branch", "-D", branch]).then(
5594
+ () => true,
5595
+ () => false
5596
+ );
5597
+ }
5598
+ async function branchExists(clone, branch) {
5599
+ return Boolean(await git(clone, ["branch", "--list", branch]).catch(() => ""));
5600
+ }
5573
5601
  async function commonGitDir(cwd) {
5574
5602
  return git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
5575
5603
  }
@@ -5635,11 +5663,12 @@ async function ghReady(cwd) {
5635
5663
  return /ENOENT/.test(errorMessage(err)) ? "the GitHub CLI is not installed on this machine: brew install gh, then gh auth login" : "the GitHub CLI is not signed in on this machine: run gh auth login in your terminal";
5636
5664
  }
5637
5665
  }
5638
- async function pullRequestMerged(cwd, number) {
5666
+ async function pullRequestState(cwd, number) {
5639
5667
  try {
5640
- return await gh(cwd, ["pr", "view", String(number), "--json", "state", "--jq", ".state"]) === "MERGED";
5668
+ const { state, headRefName } = JSON.parse(await gh(cwd, ["pr", "view", String(number), "--json", "state,headRefName"]));
5669
+ return (state === "MERGED" || state === "CLOSED" || state === "OPEN") && typeof headRefName === "string" ? { state, head: headRefName } : null;
5641
5670
  } catch {
5642
- return false;
5671
+ return null;
5643
5672
  }
5644
5673
  }
5645
5674
  async function openPullRequest(cwd, pr) {
@@ -11199,7 +11228,7 @@ async function prepareBuild(client, job, clone, adapter) {
11199
11228
  await startSolution(client, job.project_id, solution.ref, job.id);
11200
11229
  return { ok: true, build: { solution, clone, ...place, worktree: worktree2, network: await network(place.dir), history } };
11201
11230
  }
11202
- const branch = buildBranch(solution.ref, solution.title);
11231
+ const branch = buildBranch(solution.ref, solution.title, job.id);
11203
11232
  await mkdir5(join13(dir, ".."), { recursive: true, mode: 448 });
11204
11233
  const { base, fork, worktree } = await addWorktree(clone, dir, branch);
11205
11234
  made = true;
@@ -11222,8 +11251,8 @@ async function runBuildJob(input) {
11222
11251
  let said = history.report;
11223
11252
  const stop2 = async (message, opts = { keep: true }) => {
11224
11253
  await unstartSolution(client, job.project_id, solution.ref, was).catch(() => void 0);
11225
- if (opts.keep && !await merging(worktree)) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
11226
- else await removeWorktree(clone, dir);
11254
+ if (!opts.keep) await removeWorktree(clone, dir);
11255
+ else if (!await merging(worktree)) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
11227
11256
  return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
11228
11257
  };
11229
11258
  const park = async (why) => {
@@ -11381,8 +11410,7 @@ function askOf(input) {
11381
11410
  };
11382
11411
  }
11383
11412
  async function loadHistory(client, job) {
11384
- const named = (job.params ?? {}).origin;
11385
- const origin = typeof named === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(named) ? named : job.id;
11413
+ const origin = originOf(job);
11386
11414
  const { data: runs } = await client.from("jobs").select("id").eq("project_id", job.project_id).eq("skill", BUILD_SKILL).or(`id.eq.${origin},params->>origin.eq.${origin}`);
11387
11415
  return loadBuildHistory(client, [.../* @__PURE__ */ new Set([origin, job.id, ...(runs ?? []).map((r) => r.id)])], { home: homedir4(), jobId: origin });
11388
11416
  }
@@ -11456,37 +11484,71 @@ async function resumePausedBuilds(client, daemonId, now = Date.now()) {
11456
11484
  const { data: parked } = await client.from("jobs").select("id").eq("skill", BUILD_SKILL).eq("status", "waiting").eq("assigned_daemon_id", daemonId);
11457
11485
  const ids = (parked ?? []).map((j) => j.id);
11458
11486
  if (!ids.length) return [];
11459
- const { data: stops } = await client.from("job_events").select("job_id, kind, payload").in("job_id", ids).in("kind", ["paused", "question"]).order("id", { ascending: false });
11460
- const last = /* @__PURE__ */ new Map();
11461
- for (const e of stops ?? []) if (!last.has(e.job_id)) last.set(e.job_id, e);
11462
- const due = ids.filter((id) => {
11463
- const stop2 = last.get(id);
11464
- return stop2?.kind === "paused" && Date.parse(String(stop2.payload?.resets_at ?? "")) <= now;
11465
- });
11487
+ const { data: stops } = await client.from("job_events").select("job_id, kind, seq, payload").in("job_id", ids).in("kind", ["paused", "question", "answer"]).order("id");
11488
+ const due = ids.filter((id) => parkedDue((stops ?? []).filter((e) => e.job_id === id).map((e) => ({ ...e, payload: e.payload ?? {} })), now));
11466
11489
  if (!due.length) return [];
11467
11490
  const { data } = await client.from("jobs").update({ status: "queued", assigned_daemon_id: null }).in("id", due).eq("status", "waiting").select("id");
11468
11491
  return (data ?? []).map((j) => j.id);
11469
11492
  }
11470
- async function shippedBuilds(client, daemonId, userId) {
11471
- const { data: building } = await client.from("solutions").select("id, ref, project_id").eq("status", "building");
11493
+ async function throwAwayBuilds(client, daemonId, userId) {
11494
+ const { data: throws } = await client.from("job_events").select("job_id").eq("kind", "throw");
11495
+ if (!throws?.length) return [];
11496
+ const { data: marked } = await client.from("job_events").select("job_id").eq("kind", "thrown").in("job_id", throws.map((t) => t.job_id));
11497
+ const open = throws.map((t) => t.job_id).filter((id) => !marked?.some((m) => m.job_id === id));
11498
+ if (!open.length) return [];
11499
+ const { data: asked } = await client.from("jobs").select("id, project_id, params").in("id", open).eq("skill", BUILD_SKILL).eq("requested_by", userId).or(`assigned_daemon_id.eq.${daemonId},target_daemon_id.eq.${daemonId}`);
11500
+ if (!asked?.length) return [];
11501
+ const { data: clones } = await client.from("clones").select("project_id, repo_path").eq("daemon_id", daemonId);
11502
+ const thrown = [];
11503
+ for (const job of asked) {
11504
+ const runs = await buildRuns(client, job.project_id, originOf(job));
11505
+ const first = runs.find((r) => r.id === originOf(job));
11506
+ if (!first || first.assigned_daemon_id !== daemonId || first.requested_by !== userId || throwRefusal(runs, job.id)) continue;
11507
+ const { place } = await loadHistory(client, job);
11508
+ const clone = clones?.find((c) => c.project_id === job.project_id)?.repo_path;
11509
+ if (place && clone && existsSync5(clone)) {
11510
+ if (existsSync5(place.dir)) await removeWorktree(clone, place.dir);
11511
+ if (existsSync5(place.dir) || await branchExists(clone, place.branch) && !await deleteBranch(clone, place.branch)) continue;
11512
+ } else if (place) continue;
11513
+ await appendJobEvent(client, job.id, "thrown", {}, "nanopm");
11514
+ thrown.push(job.id);
11515
+ }
11516
+ return thrown;
11517
+ }
11518
+ async function settledBuilds(client, daemonId, userId) {
11519
+ const { data: building } = await client.from("solutions").select("id, ref, project_id").eq("status", "building").is("superseded_by", null);
11472
11520
  if (!building?.length) return [];
11473
11521
  const [{ data: built }, { data: clones }] = await Promise.all([
11474
- client.from("jobs").select("id, params, pr:result->pr").eq("skill", BUILD_SKILL).eq("status", "succeeded").eq("assigned_daemon_id", daemonId).eq("requested_by", userId).in("params->>solution_id", building.map((s) => s.id)),
11522
+ client.from("jobs").select("id, project_id, params, created_at, pr:result->pr").eq("skill", BUILD_SKILL).eq("status", "succeeded").eq("assigned_daemon_id", daemonId).eq("requested_by", userId).in("params->>solution_id", building.map((s) => s.id)).order("created_at", { ascending: false }),
11475
11523
  client.from("clones").select("project_id, repo_path").eq("daemon_id", daemonId)
11476
11524
  ]);
11525
+ const newest = [...new Map((built ?? []).reverse().map((j) => [j.params?.solution_id, j])).values()];
11526
+ const { data: closed } = newest.length ? await client.from("job_events").select("job_id").in("job_id", newest.map((j) => j.id)).eq("kind", "closed") : { data: [] };
11477
11527
  const cloneOf = new Map((clones ?? []).map((c) => [c.project_id, c.repo_path]));
11478
- const shipped = [];
11479
- for (const j of built ?? []) {
11528
+ const settled2 = [];
11529
+ for (const j of newest.filter((n) => !closed?.some((c) => c.job_id === n.id))) {
11480
11530
  const solution = building.find((s) => s.id === j.params?.solution_id);
11481
11531
  const number = j.pr?.number;
11482
11532
  const clone = solution ? cloneOf.get(solution.project_id) : void 0;
11483
- if (!solution || typeof number !== "number" || number <= 0 || !clone || !existsSync5(clone) || !await pullRequestMerged(clone, number)) continue;
11484
- const { error } = await client.rpc("finish_solution", { p_project_id: solution.project_id, p_ref: solution.ref, p_job_id: j.id });
11485
- if (error) continue;
11486
- await logActivity({ client }, { id: j.id, project_id: solution.project_id }, "shipped", `Pull request #${number} merged: ${solution.ref} shipped`);
11487
- shipped.push(j.id);
11488
- }
11489
- return shipped;
11533
+ if (!solution || typeof number !== "number" || number <= 0 || !clone || !existsSync5(clone)) continue;
11534
+ const [{ place }, pr] = await Promise.all([loadHistory(client, j), pullRequestState(clone, number)]);
11535
+ if (!place || !pr || pr.head !== place.branch) continue;
11536
+ const at = { id: j.id, project_id: solution.project_id };
11537
+ if (pr.state === "MERGED") {
11538
+ const { error } = await client.rpc("finish_solution", { p_project_id: solution.project_id, p_ref: solution.ref, p_job_id: j.id });
11539
+ if (error) continue;
11540
+ await logActivity({ client }, at, "shipped", `Pull request #${number} merged: ${solution.ref} shipped`);
11541
+ settled2.push({ id: j.id, ref: solution.ref, merged: true });
11542
+ } else if (pr.state === "CLOSED") {
11543
+ const { error } = await client.rpc("unstart_solution", { p_project_id: solution.project_id, p_ref: solution.ref, p_to: wasOf(j.params) });
11544
+ if (error) continue;
11545
+ await deleteBranch(clone, place.branch);
11546
+ await appendJobEvent(client, j.id, "closed", { number }, "nanopm");
11547
+ await logActivity({ client }, at, "closed", `Pull request #${number} closed without merging: ${solution.ref} back on the roadmap`);
11548
+ settled2.push({ id: j.id, ref: solution.ref, merged: false });
11549
+ }
11550
+ }
11551
+ return settled2;
11490
11552
  }
11491
11553
  async function tellTheApp(accessToken, jobId, fetchImpl = fetch) {
11492
11554
  try {
@@ -11645,11 +11707,12 @@ async function runDaemon(options = {}) {
11645
11707
  if (options.once && Date.now() - startedAt > onceTimeoutMs) break;
11646
11708
  if (!options.once && Date.now() - lastPauseCheck > PAUSE_CHECK_MS) {
11647
11709
  lastPauseCheck = Date.now();
11648
- for (const id of await resumePausedBuilds(auth.client, daemonId).catch(() => [])) log(`build ${id}: the Claude limit reset, carrying on`);
11710
+ for (const id of await resumePausedBuilds(auth.client, daemonId).catch(() => [])) log(`build ${id}: carrying on (its limit reset, or its answer came)`);
11711
+ for (const id of await throwAwayBuilds(auth.client, daemonId, auth.userId).catch(() => [])) log(`build ${id}: thrown away, its copy and its local branch`);
11649
11712
  }
11650
11713
  if (!options.once && Date.now() - lastShipCheck > SHIP_CHECK_MS) {
11651
11714
  lastShipCheck = Date.now();
11652
- for (const id of await shippedBuilds(auth.client, daemonId, auth.userId).catch(() => [])) log(`build ${id}: its pull request merged, shipped`);
11715
+ for (const b of await settledBuilds(auth.client, daemonId, auth.userId).catch(() => [])) log(`build ${b.id}: its pull request ${b.merged ? `merged, ${b.ref} shipped` : `closed without merging, ${b.ref} back on the roadmap`}`);
11653
11716
  }
11654
11717
  if (!options.once && Date.now() - lastDeployCheck > DEPLOY_CHECK_MS) {
11655
11718
  lastDeployCheck = Date.now();
@@ -14604,7 +14667,7 @@ import { promisify as promisify8 } from "util";
14604
14667
  // package.json
14605
14668
  var package_default = {
14606
14669
  name: "@nanopm/cli",
14607
- version: "0.1.16",
14670
+ version: "0.1.18",
14608
14671
  description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
14609
14672
  type: "module",
14610
14673
  license: "MIT",
@@ -16025,7 +16088,7 @@ async function build(ref, opts) {
16025
16088
  }
16026
16089
  const stopped = opts.continue || opts.throwAway ? await buildOf(auth.client, project.id, ["failed", "cancelled"], solution.id) : null;
16027
16090
  if ((opts.continue || opts.throwAway) && !stopped) throw new Error(`${solution.ref} has no stopped build to ${opts.continue ? "continue" : "throw away"}.`);
16028
- if (opts.throwAway) return throwAway(auth.client, stopped.origin, repo.root, out);
16091
+ if (opts.throwAway) return throwAway(auth.client, stopped, repo.root, out);
16029
16092
  const [daemon, local] = await Promise.all([awakeDaemon(auth.client, auth.userId), localDaemonId()]);
16030
16093
  if (!local || daemon?.id !== local) throw new Error("This machine\u2019s daemon is asleep, so nothing was queued. `nanopm daemon install` starts it; then build again.");
16031
16094
  await rememberClone(auth.client, project.id, repo.root);
@@ -16047,11 +16110,12 @@ async function buildOf(client, projectId, statuses, solutionId) {
16047
16110
  const params = data?.params ?? {};
16048
16111
  return data ? { id: data.id, solutionId: params.solution_id ?? "", was: wasOf(data.params), origin: params.origin ?? data.id } : null;
16049
16112
  }
16050
- async function throwAway(client, jobId, clone, out) {
16051
- const { place } = await loadBuildHistory(client, [jobId], { home: homedir6(), jobId });
16113
+ async function throwAway(client, build2, clone, out) {
16114
+ const { place } = await loadBuildHistory(client, [build2.origin], { home: homedir6(), jobId: build2.origin });
16052
16115
  if (!place) throw new Error("that build left nothing on this machine.");
16053
16116
  if (existsSync7(place.dir)) await execFileAsync9("git", ["-C", clone, "worktree", "remove", "--force", place.dir]);
16054
16117
  await execFileAsync9("git", ["-C", clone, "branch", "-D", place.branch]).catch(() => void 0);
16118
+ await appendJobEvent(client, build2.id, "thrown", {}, "user");
16055
16119
  out(`Thrown away: the copy of the code and the local branch ${place.branch}.`);
16056
16120
  return 0;
16057
16121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanopm/cli",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
5
5
  "type": "module",
6
6
  "license": "MIT",