@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 +65 -17
- package/package.json +1 -1
- package/skills/build/SKILL.md +3 -1
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
|
|
11226
|
-
else await
|
|
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
|
|
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"
|
|
11460
|
-
const
|
|
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}:
|
|
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.
|
|
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
|
|
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,
|
|
16051
|
-
const { place } = await loadBuildHistory(client, [
|
|
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
package/skills/build/SKILL.md
CHANGED
|
@@ -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.
|