@nanopm/cli 0.1.15 → 0.1.17

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/dist/index.js CHANGED
@@ -5420,7 +5420,20 @@ var isDecideForMe = (answer) => /^\s*decide(\s+for\s+me)?\s*\.?\s*$/i.test(answe
5420
5420
  function answerForAgent(answer) {
5421
5421
  return isDecideForMe(answer) ? DECIDE_FOR_ME : answer;
5422
5422
  }
5423
- var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr", "handed"];
5423
+ var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr", "handed", "throw", "thrown"];
5424
+ function originOf(job) {
5425
+ const named = (job.params ?? {}).origin;
5426
+ 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;
5427
+ }
5428
+ function throwRefusal(runs, jobId) {
5429
+ if (runs.some((r) => ["queued", "running", "waiting"].includes(r.status))) return "This build is on its way again: stop it first.";
5430
+ const newest = [...runs].sort((x, y) => y.created_at.localeCompare(x.created_at))[0];
5431
+ return newest?.id === jobId ? null : "This build was continued since: refresh the page.";
5432
+ }
5433
+ async function buildRuns(client, projectId, origin) {
5434
+ 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}`);
5435
+ return data ?? [];
5436
+ }
5424
5437
  var BUILD_CONFLICT = { resolve: "Let Nano resolve it", mine: "I\u2019ll do it in my terminal" };
5425
5438
  var chose = (answer, option) => {
5426
5439
  const plain = (s) => s.trim().toLowerCase().replace(/[’‘]/g, "'");
@@ -5462,6 +5475,11 @@ function buildHistory(events, at) {
5462
5475
  if (last?.kind === "paused") return found({ why: "limit" });
5463
5476
  return found({ why: "lost" });
5464
5477
  }
5478
+ function parkedDue(events, now) {
5479
+ const stop2 = [...events].reverse().find((e) => e.kind === "paused" || e.kind === "question");
5480
+ if (stop2?.kind === "paused") return Date.parse(String(stop2.payload.resets_at ?? "")) <= now;
5481
+ return stop2?.kind === "question" && events.some((e) => e.kind === "answer" && e.seq === stop2.seq);
5482
+ }
5465
5483
  function wasOf(params) {
5466
5484
  return params?.was === "proposed" ? "proposed" : "accepted";
5467
5485
  }
@@ -5570,6 +5588,15 @@ async function addWorktree(clone, dir, branch) {
5570
5588
  async function removeWorktree(clone, dir) {
5571
5589
  await git(clone, ["worktree", "remove", "--force", dir]).catch(() => "");
5572
5590
  }
5591
+ async function deleteBranch(clone, branch) {
5592
+ return git(clone, ["branch", "-D", branch]).then(
5593
+ () => true,
5594
+ () => false
5595
+ );
5596
+ }
5597
+ async function branchExists(clone, branch) {
5598
+ return Boolean(await git(clone, ["branch", "--list", branch]).catch(() => ""));
5599
+ }
5573
5600
  async function commonGitDir(cwd) {
5574
5601
  return git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
5575
5602
  }
@@ -11222,8 +11249,8 @@ async function runBuildJob(input) {
11222
11249
  let said = history.report;
11223
11250
  const stop2 = async (message, opts = { keep: true }) => {
11224
11251
  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);
11252
+ if (!opts.keep) await removeWorktree(clone, dir);
11253
+ else if (!await merging(worktree)) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
11227
11254
  return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
11228
11255
  };
11229
11256
  const park = async (why) => {
@@ -11381,8 +11408,7 @@ function askOf(input) {
11381
11408
  };
11382
11409
  }
11383
11410
  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;
11411
+ const origin = originOf(job);
11386
11412
  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
11413
  return loadBuildHistory(client, [.../* @__PURE__ */ new Set([origin, job.id, ...(runs ?? []).map((r) => r.id)])], { home: homedir4(), jobId: origin });
11388
11414
  }
@@ -11456,17 +11482,37 @@ async function resumePausedBuilds(client, daemonId, now = Date.now()) {
11456
11482
  const { data: parked } = await client.from("jobs").select("id").eq("skill", BUILD_SKILL).eq("status", "waiting").eq("assigned_daemon_id", daemonId);
11457
11483
  const ids = (parked ?? []).map((j) => j.id);
11458
11484
  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
- });
11485
+ 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");
11486
+ const due = ids.filter((id) => parkedDue((stops ?? []).filter((e) => e.job_id === id).map((e) => ({ ...e, payload: e.payload ?? {} })), now));
11466
11487
  if (!due.length) return [];
11467
11488
  const { data } = await client.from("jobs").update({ status: "queued", assigned_daemon_id: null }).in("id", due).eq("status", "waiting").select("id");
11468
11489
  return (data ?? []).map((j) => j.id);
11469
11490
  }
11491
+ async function throwAwayBuilds(client, daemonId, userId) {
11492
+ const { data: throws } = await client.from("job_events").select("job_id").eq("kind", "throw");
11493
+ if (!throws?.length) return [];
11494
+ const { data: marked } = await client.from("job_events").select("job_id").eq("kind", "thrown").in("job_id", throws.map((t) => t.job_id));
11495
+ const open = throws.map((t) => t.job_id).filter((id) => !marked?.some((m) => m.job_id === id));
11496
+ if (!open.length) return [];
11497
+ 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}`);
11498
+ if (!asked?.length) return [];
11499
+ const { data: clones } = await client.from("clones").select("project_id, repo_path").eq("daemon_id", daemonId);
11500
+ const thrown = [];
11501
+ for (const job of asked) {
11502
+ const runs = await buildRuns(client, job.project_id, originOf(job));
11503
+ const first = runs.find((r) => r.id === originOf(job));
11504
+ if (!first || first.assigned_daemon_id !== daemonId || first.requested_by !== userId || throwRefusal(runs, job.id)) continue;
11505
+ const { place } = await loadHistory(client, job);
11506
+ const clone = clones?.find((c) => c.project_id === job.project_id)?.repo_path;
11507
+ if (place && clone && existsSync5(clone)) {
11508
+ if (existsSync5(place.dir)) await removeWorktree(clone, place.dir);
11509
+ if (existsSync5(place.dir) || await branchExists(clone, place.branch) && !await deleteBranch(clone, place.branch)) continue;
11510
+ } else if (place) continue;
11511
+ await appendJobEvent(client, job.id, "thrown", {}, "nanopm");
11512
+ thrown.push(job.id);
11513
+ }
11514
+ return thrown;
11515
+ }
11470
11516
  async function shippedBuilds(client, daemonId, userId) {
11471
11517
  const { data: building } = await client.from("solutions").select("id, ref, project_id").eq("status", "building");
11472
11518
  if (!building?.length) return [];
@@ -11645,7 +11691,8 @@ async function runDaemon(options = {}) {
11645
11691
  if (options.once && Date.now() - startedAt > onceTimeoutMs) break;
11646
11692
  if (!options.once && Date.now() - lastPauseCheck > PAUSE_CHECK_MS) {
11647
11693
  lastPauseCheck = Date.now();
11648
- for (const id of await resumePausedBuilds(auth.client, daemonId).catch(() => [])) log(`build ${id}: the Claude limit reset, carrying on`);
11694
+ for (const id of await resumePausedBuilds(auth.client, daemonId).catch(() => [])) log(`build ${id}: carrying on (its limit reset, or its answer came)`);
11695
+ for (const id of await throwAwayBuilds(auth.client, daemonId, auth.userId).catch(() => [])) log(`build ${id}: thrown away, its copy and its local branch`);
11649
11696
  }
11650
11697
  if (!options.once && Date.now() - lastShipCheck > SHIP_CHECK_MS) {
11651
11698
  lastShipCheck = Date.now();
@@ -14604,7 +14651,7 @@ import { promisify as promisify8 } from "util";
14604
14651
  // package.json
14605
14652
  var package_default = {
14606
14653
  name: "@nanopm/cli",
14607
- version: "0.1.15",
14654
+ version: "0.1.17",
14608
14655
  description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
14609
14656
  type: "module",
14610
14657
  license: "MIT",
@@ -16025,7 +16072,7 @@ async function build(ref, opts) {
16025
16072
  }
16026
16073
  const stopped = opts.continue || opts.throwAway ? await buildOf(auth.client, project.id, ["failed", "cancelled"], solution.id) : null;
16027
16074
  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);
16075
+ if (opts.throwAway) return throwAway(auth.client, stopped, repo.root, out);
16029
16076
  const [daemon, local] = await Promise.all([awakeDaemon(auth.client, auth.userId), localDaemonId()]);
16030
16077
  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
16078
  await rememberClone(auth.client, project.id, repo.root);
@@ -16047,11 +16094,12 @@ async function buildOf(client, projectId, statuses, solutionId) {
16047
16094
  const params = data?.params ?? {};
16048
16095
  return data ? { id: data.id, solutionId: params.solution_id ?? "", was: wasOf(data.params), origin: params.origin ?? data.id } : null;
16049
16096
  }
16050
- async function throwAway(client, jobId, clone, out) {
16051
- const { place } = await loadBuildHistory(client, [jobId], { home: homedir6(), jobId });
16097
+ async function throwAway(client, build2, clone, out) {
16098
+ const { place } = await loadBuildHistory(client, [build2.origin], { home: homedir6(), jobId: build2.origin });
16052
16099
  if (!place) throw new Error("that build left nothing on this machine.");
16053
16100
  if (existsSync7(place.dir)) await execFileAsync9("git", ["-C", clone, "worktree", "remove", "--force", place.dir]);
16054
16101
  await execFileAsync9("git", ["-C", clone, "branch", "-D", place.branch]).catch(() => void 0);
16102
+ await appendJobEvent(client, build2.id, "thrown", {}, "user");
16055
16103
  out(`Thrown away: the copy of the code and the local branch ${place.branch}.`);
16056
16104
  return 0;
16057
16105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanopm/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",
@@ -52,7 +52,9 @@ question per `ask_user`, with the options, and what you looked at in `why`.
52
52
  stops to wait on a background task is a build that stops.
53
53
  - The analytics events the spec names ship with the change, named as it says. No word a user
54
54
  typed goes in an event.
55
- - Do not touch secrets. `.env` files and keys are denied to you, deliberately.
55
+ - Do not touch secrets. `.env` files and keys are denied to you, deliberately. Your sandbox keeps
56
+ you from reading them, so `git status` and `git diff` show them as deleted (`D`) and print
57
+ "Operation not permitted": they are not deleted. Never report them, restore them or stage them.
56
58
  - Do not reformat, rename or tidy anything the spec did not ask for. A diff the founder cannot
57
59
  review in one sitting is a diff they will not merge.
58
60
  - Install a new dependency only if the spec allows it; otherwise ask.