@nanopm/cli 0.1.5 → 0.1.6

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 +245 -124
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3856,10 +3856,10 @@ function parsePrintResult(stdout) {
3856
3856
  }
3857
3857
 
3858
3858
  // ../daemon/src/daemon.ts
3859
- import { mkdir as mkdir5, readdir as readdir2, readFile as readFile7, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3859
+ import { mkdir as mkdir5, readdir as readdir2, readFile as readFile7, rm as rm4, writeFile as writeFile4 } from "fs/promises";
3860
3860
  import { openSync, closeSync, unlinkSync, writeFileSync, readFileSync as readFileSyncNode, mkdirSync } from "fs";
3861
3861
  import { hostname as hostname2 } from "os";
3862
- import { join as join10 } from "path";
3862
+ import { join as join11 } from "path";
3863
3863
  import { setTimeout as sleep } from "timers/promises";
3864
3864
 
3865
3865
  // ../daemon/src/awake.ts
@@ -3878,6 +3878,71 @@ function stayAwake(platform = process.platform) {
3878
3878
  }
3879
3879
  }
3880
3880
 
3881
+ // ../daemon/src/update.ts
3882
+ import { execFile as execFile2 } from "child_process";
3883
+ import { existsSync, realpathSync } from "fs";
3884
+ import { rm as rm2, writeFile as writeFile3 } from "fs/promises";
3885
+ import { homedir as homedir2 } from "os";
3886
+ import { join as join4, sep } from "path";
3887
+ import { promisify as promisify2 } from "util";
3888
+ var PACKAGE = "@nanopm/cli";
3889
+ var LATEST_URL = "https://registry.npmjs.org/@nanopm%2fcli/latest";
3890
+ var RELEASE = /^\d+\.\d+\.\d+$/;
3891
+ var execFileAsync2 = promisify2(execFile2);
3892
+ var root = (home = homedir2()) => join4(home, ".nanopm");
3893
+ var appDir = (home) => join4(root(home), "app");
3894
+ var offFlag = (home) => join4(root(home), "no-auto-update");
3895
+ function installKind(bin = process.argv[1], home = homedir2()) {
3896
+ if (!bin) return "other";
3897
+ let real = bin;
3898
+ try {
3899
+ real = realpathSync(bin);
3900
+ } catch {
3901
+ }
3902
+ return real.startsWith(appDir(home) + sep) ? "installer" : "other";
3903
+ }
3904
+ function newer(latest, current) {
3905
+ if (!latest || !RELEASE.test(latest) || !RELEASE.test(current)) return false;
3906
+ const a = latest.split(".").map(Number);
3907
+ const b = current.split(".").map(Number);
3908
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
3909
+ return false;
3910
+ }
3911
+ async function latestVersion(fetchImpl = fetch) {
3912
+ try {
3913
+ const res = await fetchImpl(LATEST_URL, { signal: AbortSignal.timeout(5e3) });
3914
+ if (!res.ok) return null;
3915
+ const { version } = await res.json();
3916
+ return typeof version === "string" && RELEASE.test(version) ? version : null;
3917
+ } catch {
3918
+ return null;
3919
+ }
3920
+ }
3921
+ async function installVersion(version, home = homedir2()) {
3922
+ if (!RELEASE.test(version)) return { ok: false, why: `not a version: ${version}` };
3923
+ const runtime = join4(root(home), "runtime", "bin");
3924
+ try {
3925
+ await execFileAsync2(join4(runtime, "npm"), ["install", "-g", "--prefix", appDir(home), `${PACKAGE}@${version}`, "--no-fund", "--no-audit", "--loglevel=error"], {
3926
+ timeout: 5 * 6e4,
3927
+ env: { ...process.env, PATH: `${runtime}:${process.env.PATH ?? ""}`, NPM_CONFIG_UPDATE_NOTIFIER: "false" }
3928
+ });
3929
+ return { ok: true };
3930
+ } catch (err) {
3931
+ const failed = err;
3932
+ return { ok: false, why: (failed.stderr?.trim() || failed.message || String(err)).split("\n").slice(-3).join(" ") };
3933
+ }
3934
+ }
3935
+ function autoUpdateOff(home = homedir2()) {
3936
+ return existsSync(offFlag(home));
3937
+ }
3938
+ async function setAutoUpdate(on, home = homedir2()) {
3939
+ if (on) await rm2(offFlag(home), { force: true });
3940
+ else await writeFile3(offFlag(home), "Automatic updates are off: `nanopm update --auto on` turns them back on.\n");
3941
+ }
3942
+ function shouldUpdate(s) {
3943
+ return s.kind === "installer" && !s.off && !s.quickBusy && newer(s.latest, s.current);
3944
+ }
3945
+
3881
3946
  // ../daemon/src/mcp.ts
3882
3947
  function memoryMcpCommand(env = process.env) {
3883
3948
  const override = env.NANOPM_MCP_COMMAND;
@@ -4865,7 +4930,7 @@ async function appStoreSearch(term, country, limit, opts = {}) {
4865
4930
 
4866
4931
  // ../connectors/src/stores/play.ts
4867
4932
  var SEARCH2 = "https://play.google.com/store/search";
4868
- var PACKAGE = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+){2,}$/i;
4933
+ var PACKAGE2 = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+){2,}$/i;
4869
4934
  function playSearchUrl(term, country) {
4870
4935
  const url = new URL(SEARCH2);
4871
4936
  url.searchParams.set("q", term);
@@ -4889,7 +4954,7 @@ function blobs(html) {
4889
4954
  function cards(node, found = []) {
4890
4955
  if (!Array.isArray(node)) return found;
4891
4956
  const head = node[0];
4892
- if (Array.isArray(head) && head.length === 2 && typeof head[0] === "string" && PACKAGE.test(head[0]) && head[1] === 7) {
4957
+ if (Array.isArray(head) && head.length === 2 && typeof head[0] === "string" && PACKAGE2.test(head[0]) && head[1] === 7) {
4893
4958
  found.push(node);
4894
4959
  return found;
4895
4960
  }
@@ -5022,13 +5087,13 @@ async function sendDigestFor(client, accessToken, jobId, fetchImpl = fetch) {
5022
5087
  }
5023
5088
 
5024
5089
  // ../daemon/src/deploys.ts
5025
- import { execFile as execFile2 } from "child_process";
5026
- import { promisify as promisify2 } from "util";
5027
- var execFileAsync2 = promisify2(execFile2);
5090
+ import { execFile as execFile3 } from "child_process";
5091
+ import { promisify as promisify3 } from "util";
5092
+ var execFileAsync3 = promisify3(execFile3);
5028
5093
  var SETTLE_HOURS = 3;
5029
5094
  var MIN_COMMITS = 3;
5030
5095
  async function git(cwd, args) {
5031
- const { stdout } = await execFileAsync2("git", ["-C", cwd, ...args], { timeout: 2e4, maxBuffer: 1024 * 1024 });
5096
+ const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], { timeout: 2e4, maxBuffer: 1024 * 1024 });
5032
5097
  return stdout.trim();
5033
5098
  }
5034
5099
  async function defaultBranch(cwd) {
@@ -5124,17 +5189,17 @@ async function reviewWithoutData(client, job) {
5124
5189
  }
5125
5190
 
5126
5191
  // ../daemon/src/skills.ts
5127
- import { existsSync, readdirSync } from "fs";
5192
+ import { existsSync as existsSync2, readdirSync } from "fs";
5128
5193
  import { readFile as readFile2 } from "fs/promises";
5129
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
5194
+ import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
5130
5195
  function findSkillsDir(env = process.env) {
5131
5196
  if (env.NANOPM_SKILLS_DIR) return env.NANOPM_SKILLS_DIR;
5132
5197
  const candidates = [
5133
- join4(process.cwd(), "skills"),
5198
+ join5(process.cwd(), "skills"),
5134
5199
  ...process.argv[1] ? [resolve2(dirname2(process.argv[1]), "..", "skills")] : []
5135
5200
  ];
5136
5201
  for (const dir of candidates) {
5137
- if (existsSync(join4(dir, "onboard", "skill.json"))) return dir;
5202
+ if (existsSync2(join5(dir, "onboard", "skill.json"))) return dir;
5138
5203
  }
5139
5204
  throw new Error(`Cannot find the skills directory (tried ${candidates.join(", ")}). Set NANOPM_SKILLS_DIR.`);
5140
5205
  }
@@ -5144,9 +5209,9 @@ function skillDirs(skillsDir, env = process.env) {
5144
5209
  }
5145
5210
  async function loadSkill(name, skillsDir = findSkillsDir()) {
5146
5211
  if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`Invalid skill name "${name}"`);
5147
- const dir = skillDirs(skillsDir).map((d) => join4(d, name)).find((d) => existsSync(join4(d, "skill.json")));
5212
+ const dir = skillDirs(skillsDir).map((d) => join5(d, name)).find((d) => existsSync2(join5(d, "skill.json")));
5148
5213
  if (!dir) throw new Error(`Unknown skill "${name}"`);
5149
- const raw = JSON.parse(await readFile2(join4(dir, "skill.json"), "utf8"));
5214
+ const raw = JSON.parse(await readFile2(join5(dir, "skill.json"), "utf8"));
5150
5215
  let manifest;
5151
5216
  try {
5152
5217
  manifest = parseSkillManifest(raw);
@@ -5155,7 +5220,7 @@ async function loadSkill(name, skillsDir = findSkillsDir()) {
5155
5220
  }
5156
5221
  const tainted = taintedByTheWeb(manifest);
5157
5222
  if (tainted) throw new Error(`the "${name}" skill cannot be loaded: ${tainted}`);
5158
- const body = await readFile2(join4(dir, "SKILL.md"), "utf8");
5223
+ const body = await readFile2(join5(dir, "SKILL.md"), "utf8");
5159
5224
  return { manifest, body, dir };
5160
5225
  }
5161
5226
  var NOT_WITH_READ_WEB = ["write_memory", "problems", "propose", "review", "write_repo", "run_commands"];
@@ -5190,7 +5255,7 @@ function renderPrompt(template, params) {
5190
5255
  var NEEDS_THE_REQUESTERS_CLONE = ["write_repo", "run_commands"];
5191
5256
  var NEEDS_THE_REQUESTERS_IDENTITY = /* @__PURE__ */ new Set(["want", "start"]);
5192
5257
  async function clonelessSkills(skillsDir = findSkillsDir()) {
5193
- const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync(join4(skillsDir, d.name, "skill.json"))).map((d) => d.name);
5258
+ const names = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync2(join5(skillsDir, d.name, "skill.json"))).map((d) => d.name);
5194
5259
  const out = [];
5195
5260
  for (const name of names) {
5196
5261
  try {
@@ -5402,9 +5467,9 @@ async function runMappingJob({ client, job, adapter, spec, mcpServers, onEvent,
5402
5467
  };
5403
5468
  const { data: rootId, error: rootError } = await client.rpc("mapping_run_root", { p_job_id: job.id });
5404
5469
  if (rootError) throw new Error(rootError.message);
5405
- const { data: root, error: readError } = await client.from("jobs").select("result").eq("id", rootId).single();
5470
+ const { data: root2, error: readError } = await client.from("jobs").select("result").eq("id", rootId).single();
5406
5471
  if (readError) throw new Error(readError.message);
5407
- const saved = root.result;
5472
+ const saved = root2.result;
5408
5473
  if (saved?.mapping_receipt) return { status: "succeeded", result: saved.mapping_audit?.checked.summary ?? `Problem map published: ${saved.mapping_receipt.refs.join(", ")}` };
5409
5474
  const previousUsage = mappingUsage(saved?.mapping_checkpoints ?? []);
5410
5475
  const result = await runProblemMapping({
@@ -5520,7 +5585,7 @@ var UNANSWERED = /structured[ _]output|returned no result/i;
5520
5585
 
5521
5586
  // ../daemon/src/site.ts
5522
5587
  import { readFile as readFile3 } from "fs/promises";
5523
- import { join as join5 } from "path";
5588
+ import { join as join6 } from "path";
5524
5589
  var SITE_PATHS = ["/", "/about", "/pricing"];
5525
5590
  var PAGE_CHARS = 6e3;
5526
5591
  var SITE_TIMEOUT_MS = 2e4;
@@ -5643,7 +5708,7 @@ function siteFromSearch(name, hits) {
5643
5708
  }
5644
5709
  async function read(repoRoot, name) {
5645
5710
  try {
5646
- const text2 = await readFile3(join5(repoRoot, name), "utf8");
5711
+ const text2 = await readFile3(join6(repoRoot, name), "utf8");
5647
5712
  return text2.length > 2e5 ? null : text2;
5648
5713
  } catch {
5649
5714
  return null;
@@ -5698,7 +5763,7 @@ async function fromReadme(repoRoot) {
5698
5763
 
5699
5764
  // ../daemon/src/repo-docs.ts
5700
5765
  import { readFile as readFile4, readdir, stat } from "fs/promises";
5701
- import { join as join6, relative } from "path";
5766
+ import { join as join7, relative } from "path";
5702
5767
  var WRITTEN = /(persona|audience|customer|user-research|research\/|icp|segment)/i;
5703
5768
  var ABOUT_PEOPLE = /(persona|audience|customer|user-research|research\/|interview|feedback|survey|verbatim|voice|icp|segment)/i;
5704
5769
  var DOC = /\.(md|mdx|txt)$/i;
@@ -5716,7 +5781,7 @@ async function readDocs(cwd, opts = {}) {
5716
5781
  return;
5717
5782
  }
5718
5783
  for (const entry of entries) {
5719
- const full = join6(dir, entry.name);
5784
+ const full = join7(dir, entry.name);
5720
5785
  if (SKIP.test(`/${relative(cwd, full)}`)) continue;
5721
5786
  if (entry.isDirectory()) await walk(full, depth + 1);
5722
5787
  else if (DOC.test(entry.name) && (!opts.onlyAboutPeople || ABOUT_PEOPLE.test(relative(cwd, full)))) found.push(full);
@@ -7389,15 +7454,15 @@ async function mapRows(client, projectId, persona) {
7389
7454
  function scopeNodes(rows2, scope) {
7390
7455
  return scope ? descendants(rows2, scope) : [...rows2];
7391
7456
  }
7392
- function descendants(rows2, root) {
7457
+ function descendants(rows2, root2) {
7393
7458
  const byId = new Map(rows2.map((r) => [r.id, r]));
7394
7459
  const current = (id) => {
7395
7460
  let n = id ? byId.get(id) : void 0;
7396
7461
  for (let hops = 0; n && n.status === "replaced" && n.replaced_by && hops < 50; hops += 1) n = byId.get(n.replaced_by);
7397
7462
  return n && n.status !== "replaced" ? n : null;
7398
7463
  };
7399
- const out = [root];
7400
- const ids = /* @__PURE__ */ new Set([root.id]);
7464
+ const out = [root2];
7465
+ const ids = /* @__PURE__ */ new Set([root2.id]);
7401
7466
  for (let grew = true; grew; ) {
7402
7467
  grew = false;
7403
7468
  for (const r of rows2) {
@@ -7706,7 +7771,7 @@ function opportunitiesOutputSchema(which) {
7706
7771
  // ../daemon/src/crew.ts
7707
7772
  import { randomUUID as randomUUID8 } from "crypto";
7708
7773
  import { mkdir as mkdir2 } from "fs/promises";
7709
- import { join as join7 } from "path";
7774
+ import { join as join8 } from "path";
7710
7775
  var READ_ONLY = {
7711
7776
  read_repo: /^(Read|Glob|Grep)$/,
7712
7777
  read_data: /(^|__)(list_connections|describe_source|query)$/
@@ -7721,7 +7786,7 @@ async function attempt6({ adapter, spec, onEvent, skill: skillName, name, schema
7721
7786
  if (skill.manifest.capabilities.some((c) => !holds.includes(c))) throw new Error(`The ${name} may not hold tools.`);
7722
7787
  const refused = grant.find((c) => !READ_ONLY[c] || !skill.manifest.capabilities.includes(c));
7723
7788
  if (refused) throw new Error(`The ${name} may not hold ${refused}.`);
7724
- const runDir = dir ? join7(spec.runDir, dir) : spec.runDir;
7789
+ const runDir = dir ? join8(spec.runDir, dir) : spec.runDir;
7725
7790
  if (dir) await mkdir2(runDir, { recursive: true, mode: 448 });
7726
7791
  let result;
7727
7792
  let usage = null;
@@ -9398,21 +9463,21 @@ var told = (r) => ({
9398
9463
  });
9399
9464
 
9400
9465
  // ../daemon/src/objective-definitions.ts
9401
- import { execFile as execFile4 } from "child_process";
9466
+ import { execFile as execFile5 } from "child_process";
9402
9467
  import { randomUUID as randomUUID11 } from "crypto";
9403
- import { existsSync as existsSync2 } from "fs";
9468
+ import { existsSync as existsSync3 } from "fs";
9404
9469
  import { mkdir as mkdir3, readFile as readFile5 } from "fs/promises";
9405
- import { join as join8 } from "path";
9406
- import { promisify as promisify4 } from "util";
9470
+ import { join as join9 } from "path";
9471
+ import { promisify as promisify5 } from "util";
9407
9472
 
9408
9473
  // ../daemon/src/git.ts
9409
- import { execFile as execFile3 } from "child_process";
9410
- import { promisify as promisify3 } from "util";
9411
- var execFileAsync3 = promisify3(execFile3);
9474
+ import { execFile as execFile4 } from "child_process";
9475
+ import { promisify as promisify4 } from "util";
9476
+ var execFileAsync4 = promisify4(execFile4);
9412
9477
  var MAX_BUFFER = 4 * 1024 * 1024;
9413
9478
  var WIKI_DIR = ".nanopm/wiki/docs";
9414
9479
  async function git2(cwd, args) {
9415
- const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER, timeout: 2e4 });
9480
+ const { stdout } = await execFileAsync4("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER, timeout: 2e4 });
9416
9481
  return stdout.trim();
9417
9482
  }
9418
9483
  async function listTrackedFiles(cwd) {
@@ -9511,7 +9576,7 @@ function renderTrackedFiles(files, max = TRACKED_FILES_MAX, wiki = []) {
9511
9576
  }
9512
9577
 
9513
9578
  // ../daemon/src/objective-definitions.ts
9514
- var execFileAsync4 = promisify4(execFile4);
9579
+ var execFileAsync5 = promisify5(execFile5);
9515
9580
  var DEFINITIONS_TIMEOUT_MS = 9e4;
9516
9581
  var DEFINITIONS_THINKING_TOKENS = 2e3;
9517
9582
  var READ_TOOLS3 = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "StructuredOutput"]);
@@ -9520,7 +9585,7 @@ var COUNTING_LINES_MAX = 80;
9520
9585
  var PER_FILE = 4;
9521
9586
  async function countingLines(cwd) {
9522
9587
  try {
9523
- const { stdout } = await execFileAsync4("git", ["grep", "-n", "-I", "-i", "-E", COUNTING, "--", ".", ":!*lock*", ":!*.min.*", ":!*.map", ":!*.svg", ":!*.snap"], {
9588
+ const { stdout } = await execFileAsync5("git", ["grep", "-n", "-I", "-i", "-E", COUNTING, "--", ".", ":!*lock*", ":!*.min.*", ":!*.map", ":!*.svg", ":!*.snap"], {
9524
9589
  cwd,
9525
9590
  maxBuffer: 16 * 1024 * 1024,
9526
9591
  timeout: 5e3
@@ -9545,7 +9610,7 @@ function pickLines(raw) {
9545
9610
  }
9546
9611
  async function killRecorded(runDir) {
9547
9612
  try {
9548
- const pid = Number((await readFile5(join8(runDir, "harness.pid"), "utf8")).trim());
9613
+ const pid = Number((await readFile5(join9(runDir, "harness.pid"), "utf8")).trim());
9549
9614
  if (pid > 1) process.kill(pid, "SIGKILL");
9550
9615
  } catch {
9551
9616
  }
@@ -9565,7 +9630,7 @@ async function readDefinitions({ adapter, spec, publish: publish5, journal, hasR
9565
9630
  if (!hasRepo) return give({}, "No repository to read: the card offers the common definitions.", "ok");
9566
9631
  try {
9567
9632
  const skill = await loadSkill("define-objective");
9568
- const runDir = join8(spec.runDir, "definitions");
9633
+ const runDir = join9(spec.runDir, "definitions");
9569
9634
  await mkdir3(runDir, { recursive: true, mode: 448 });
9570
9635
  const schema = definitionsOutputSchema();
9571
9636
  const [lines, listed] = await Promise.all([countingLines(spec.cwd), listTrackedFiles(spec.cwd)]);
@@ -9627,7 +9692,7 @@ ${renderTrackedFiles(listed.files, void 0, listed.wiki)}
9627
9692
  if (!result) throw new Error("no answer");
9628
9693
  const parsed = DefinitionsAnswer.safeParse(JSON.parse(result));
9629
9694
  if (!parsed.success) throw new Error("an answer in the wrong shape");
9630
- const checked = checkProposals(parsed.data, (file) => existsSync2(join8(spec.cwd, file)));
9695
+ const checked = checkProposals(parsed.data, (file) => existsSync3(join9(spec.cwd, file)));
9631
9696
  const definitions2 = lever ? checked[lever] ? { [lever]: checked[lever] } : {} : checked;
9632
9697
  const kept = Object.values(definitions2).reduce((n, list) => n + (list?.length ?? 0), 0);
9633
9698
  const read6 = Object.values(definitions2).flat().filter((d) => d?.found).length;
@@ -9900,10 +9965,10 @@ function localDate(timezone, at) {
9900
9965
 
9901
9966
  // ../daemon/src/run-job.ts
9902
9967
  import { randomUUID as randomUUID12 } from "crypto";
9903
- import { existsSync as existsSync3 } from "fs";
9904
- import { mkdir as mkdir4, readFile as readFile6, rm as rm2 } from "fs/promises";
9905
- import { homedir as homedir2, tmpdir } from "os";
9906
- import { join as join9, resolve as resolve4 } from "path";
9968
+ import { existsSync as existsSync4 } from "fs";
9969
+ import { mkdir as mkdir4, readFile as readFile6, rm as rm3 } from "fs/promises";
9970
+ import { homedir as homedir3, tmpdir } from "os";
9971
+ import { join as join10, resolve as resolve4 } from "path";
9907
9972
 
9908
9973
  // ../daemon/src/events.ts
9909
9974
  var JobEventWriter = class {
@@ -9984,13 +10049,13 @@ function lighten(value) {
9984
10049
  }
9985
10050
 
9986
10051
  // ../daemon/src/repo.ts
9987
- import { execFile as execFile5 } from "child_process";
9988
- import { promisify as promisify5 } from "util";
9989
- var execFileAsync5 = promisify5(execFile5);
10052
+ import { execFile as execFile6 } from "child_process";
10053
+ import { promisify as promisify6 } from "util";
10054
+ var execFileAsync6 = promisify6(execFile6);
9990
10055
  var MAX_BUFFER2 = 4 * 1024 * 1024;
9991
10056
  var TIMEOUT_MS = 6e4;
9992
10057
  async function git3(cwd, args, timeoutMs = TIMEOUT_MS) {
9993
- const { stdout } = await execFileAsync5("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: timeoutMs });
10058
+ const { stdout } = await execFileAsync6("git", ["-C", cwd, ...args], { maxBuffer: MAX_BUFFER2, timeout: timeoutMs });
9994
10059
  return stdout.trim();
9995
10060
  }
9996
10061
  var RepoError = class extends Error {
@@ -10061,7 +10126,7 @@ async function pushBranch(cwd, name) {
10061
10126
  const state = await readState(cwd);
10062
10127
  if (!state.hasRemote) return { pushed: false, reason: "this repository has no origin remote" };
10063
10128
  try {
10064
- const { stderr } = await execFileAsync5("git", ["-C", cwd, "push", "--set-upstream", "origin", name], {
10129
+ const { stderr } = await execFileAsync6("git", ["-C", cwd, "push", "--set-upstream", "origin", name], {
10065
10130
  maxBuffer: MAX_BUFFER2,
10066
10131
  timeout: 12e4
10067
10132
  });
@@ -10341,7 +10406,7 @@ async function runJob(job, ctx) {
10341
10406
  return outcome;
10342
10407
  }
10343
10408
  async function execute(job, skill, ctx, events) {
10344
- const runDir = join9(nanopmHome(), "run", job.id);
10409
+ const runDir = join10(nanopmHome(), "run", job.id);
10345
10410
  await mkdir4(runDir, { recursive: true, mode: 448 });
10346
10411
  const resumeSessionId = job.resume_of ? await sessionOf(ctx.client, job.resume_of) : void 0;
10347
10412
  const sessionId = resumeSessionId ?? randomUUID12();
@@ -10386,7 +10451,7 @@ async function execute(job, skill, ctx, events) {
10386
10451
  clearTimeout(timer);
10387
10452
  clearInterval(heartbeat2);
10388
10453
  clearInterval(cancelPoll);
10389
- await rm2(runDir, { recursive: true, force: true });
10454
+ await rm3(runDir, { recursive: true, force: true });
10390
10455
  }
10391
10456
  }
10392
10457
  const quickRun2 = QUICK_RUNS[job.skill];
@@ -10413,8 +10478,8 @@ async function execute(job, skill, ctx, events) {
10413
10478
  clearTimeout(timer);
10414
10479
  clearInterval(heartbeat2);
10415
10480
  clearInterval(cancelPoll);
10416
- await rm2(runDir, { recursive: true, force: true });
10417
- await rm2(cwd2, { recursive: true, force: true });
10481
+ await rm3(runDir, { recursive: true, force: true });
10482
+ await rm3(cwd2, { recursive: true, force: true });
10418
10483
  }
10419
10484
  }
10420
10485
  const capabilities = capCapabilities(skill.manifest.capabilities, autonomy2);
@@ -10622,7 +10687,7 @@ async function execute(job, skill, ctx, events) {
10622
10687
  clearInterval(heartbeat2);
10623
10688
  clearInterval(cancelPoll);
10624
10689
  await definitionsRead;
10625
- await rm2(runDir, { recursive: true, force: true });
10690
+ await rm3(runDir, { recursive: true, force: true });
10626
10691
  }
10627
10692
  if (outcome?.status === "succeeded" && verified && verified.ran && !verified.passed) {
10628
10693
  outcome = {
@@ -10704,7 +10769,7 @@ function harnessWebFor(searchProvider, machineCanSearch) {
10704
10769
  }
10705
10770
  async function machineWebSearch() {
10706
10771
  try {
10707
- const parsed = JSON.parse(await readFile6(join9(nanopmHome(), "daemon.json"), "utf8"));
10772
+ const parsed = JSON.parse(await readFile6(join10(nanopmHome(), "daemon.json"), "utf8"));
10708
10773
  return parsed.webSearch === true;
10709
10774
  } catch {
10710
10775
  return false;
@@ -10746,7 +10811,7 @@ function buildHeader(job, project, cwd, harnessWeb = false, memory = false) {
10746
10811
  return lines.join("\n");
10747
10812
  }
10748
10813
  function scratchDir(jobId) {
10749
- return join9(tmpdir(), `nanopm-run-${jobId}`);
10814
+ return join10(tmpdir(), `nanopm-run-${jobId}`);
10750
10815
  }
10751
10816
  async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
10752
10817
  const refuse2 = (why) => {
@@ -10771,7 +10836,7 @@ async function resolveCwd(ctx, job, params, fallback, mustBeTheClone = false) {
10771
10836
  await mkdir4(fallback, { recursive: true, mode: 448 });
10772
10837
  return fallback;
10773
10838
  }
10774
- if (existsSync3(allowed)) return resolve4(allowed);
10839
+ if (existsSync4(allowed)) return resolve4(allowed);
10775
10840
  if (mustBeTheClone) refuse2(`${allowed} is not on this machine any more`);
10776
10841
  await mkdir4(fallback, { recursive: true, mode: 448 });
10777
10842
  return fallback;
@@ -10782,10 +10847,10 @@ ${body}
10782
10847
  </${label}>`;
10783
10848
  }
10784
10849
  function harnessEnv(timeoutMs, source = process.env) {
10785
- const home = source.HOME ?? homedir2();
10850
+ const home = source.HOME ?? homedir3();
10786
10851
  const env = {
10787
10852
  HOME: home,
10788
- PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join9(home, ".local", "bin")].join(":"),
10853
+ PATH: [source.PATH ?? "/usr/local/bin:/usr/bin:/bin", join10(home, ".local", "bin")].join(":"),
10789
10854
  MCP_TOOL_TIMEOUT: String(timeoutMs)
10790
10855
  };
10791
10856
  for (const key of ["USER", "LANG", "TMPDIR", "SHELL", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
@@ -10865,6 +10930,10 @@ async function runDaemon(options = {}) {
10865
10930
  const startedAt = Date.now();
10866
10931
  let lastDeployCheck = 0;
10867
10932
  const DEPLOY_CHECK_MS = 30 * 6e4;
10933
+ const UPDATE_CHECK_MS = 60 * 6e4;
10934
+ const updatable = !options.once && installKind() === "installer";
10935
+ let lastUpdateCheck = Date.now() - UPDATE_CHECK_MS + 5 * 6e4;
10936
+ let restart = false;
10868
10937
  const cloneless = await clonelessSkills().catch(() => []);
10869
10938
  const staleAfter = process.env.NANOPM_STALE_DAEMON_AFTER ?? "2 minutes";
10870
10939
  let testingConnections = false;
@@ -10924,6 +10993,21 @@ async function runDaemon(options = {}) {
10924
10993
  if (check.queued) log(`deploy on ${check.projectName}: review queued (${check.reason})`);
10925
10994
  }
10926
10995
  }
10996
+ if (updatable && Date.now() - lastUpdateCheck > UPDATE_CHECK_MS) {
10997
+ lastUpdateCheck = Date.now();
10998
+ const current = options.version ?? "dev";
10999
+ const off = autoUpdateOff();
11000
+ const latest = off ? null : await latestVersion();
11001
+ if (shouldUpdate({ kind: "installer", off, quickBusy: quick?.busy() ?? false, latest, current })) {
11002
+ const done = await installVersion(latest);
11003
+ if (done.ok) {
11004
+ log(`updated to ${latest}; restarting on it`);
11005
+ restart = true;
11006
+ break;
11007
+ }
11008
+ log(`update to ${latest} failed: ${done.why}; staying on ${current}`);
11009
+ }
11010
+ }
10927
11011
  await heartbeat(auth.client, daemonId);
10928
11012
  await sleep(pollIntervalMs, void 0, { signal: options.signal }).catch(() => void 0);
10929
11013
  }
@@ -10934,7 +11018,7 @@ async function runDaemon(options = {}) {
10934
11018
  lock.release();
10935
11019
  log(`daemon ${daemonId} offline`);
10936
11020
  }
10937
- return { daemonId, processed };
11021
+ return { daemonId, processed, restart };
10938
11022
  }
10939
11023
  async function chainAfter(auth, job, outcome, log) {
10940
11024
  if (NOT_CHAINED_BY_DAEMON.includes(job.skill)) return;
@@ -10986,7 +11070,7 @@ function quickLane(ctx, daemonId, log, everyMs = QUICK_POLL_MS) {
10986
11070
  });
10987
11071
  }, everyMs);
10988
11072
  timer.unref?.();
10989
- return { stop: () => clearInterval(timer) };
11073
+ return { stop: () => clearInterval(timer), busy: () => busy };
10990
11074
  }
10991
11075
  async function parentOf(auth, id) {
10992
11076
  const { data } = await auth.client.from("jobs").select("skill, params").eq("id", id).maybeSingle();
@@ -11031,7 +11115,7 @@ var heldInThisProcess = false;
11031
11115
  function acquireLock() {
11032
11116
  if (heldInThisProcess) throw new Error("a nanopm daemon is already running in this process");
11033
11117
  mkdirSync(nanopmHome(), { recursive: true, mode: 448 });
11034
- const path = join10(nanopmHome(), "daemon.pid");
11118
+ const path = join11(nanopmHome(), "daemon.pid");
11035
11119
  for (; ; ) {
11036
11120
  try {
11037
11121
  const fd = openSync(path, "wx");
@@ -11118,13 +11202,13 @@ async function reclaimOwnJobs(client, daemonId) {
11118
11202
  async function harnessPids(runDir) {
11119
11203
  const dirs = [runDir];
11120
11204
  try {
11121
- for (const d of await readdir2(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join10(runDir, d.name));
11205
+ for (const d of await readdir2(runDir, { withFileTypes: true })) if (d.isDirectory()) dirs.push(join11(runDir, d.name));
11122
11206
  } catch {
11123
11207
  }
11124
11208
  const pids = [];
11125
11209
  for (const dir of dirs) {
11126
11210
  try {
11127
- const pid = Number((await readFile7(join10(dir, "harness.pid"), "utf8")).trim());
11211
+ const pid = Number((await readFile7(join11(dir, "harness.pid"), "utf8")).trim());
11128
11212
  if (pid > 1) pids.push(pid);
11129
11213
  } catch {
11130
11214
  }
@@ -11132,17 +11216,17 @@ async function harnessPids(runDir) {
11132
11216
  return pids;
11133
11217
  }
11134
11218
  async function killOrphanedHarness(jobId) {
11135
- const runDir = join10(nanopmHome(), "run", jobId);
11219
+ const runDir = join11(nanopmHome(), "run", jobId);
11136
11220
  for (const pid of await harnessPids(runDir)) {
11137
11221
  try {
11138
11222
  process.kill(pid, "SIGTERM");
11139
11223
  } catch {
11140
11224
  }
11141
11225
  }
11142
- await rm3(runDir, { recursive: true, force: true });
11226
+ await rm4(runDir, { recursive: true, force: true });
11143
11227
  }
11144
11228
  async function registerDaemon(auth, version, opts = {}) {
11145
- const path = join10(nanopmHome(), "daemon.json");
11229
+ const path = join11(nanopmHome(), "daemon.json");
11146
11230
  let identity = null;
11147
11231
  try {
11148
11232
  identity = JSON.parse(await readFile7(path, "utf8"));
@@ -11184,7 +11268,7 @@ async function registerDaemon(auth, version, opts = {}) {
11184
11268
  }
11185
11269
  async function rememberDaemon(id, shared = false) {
11186
11270
  await mkdir5(nanopmHome(), { recursive: true, mode: 448 });
11187
- await writeFile3(join10(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
11271
+ await writeFile4(join11(nanopmHome(), "daemon.json"), JSON.stringify({ id, shared }, null, 2) + "\n", { mode: 384 });
11188
11272
  }
11189
11273
  async function heartbeat(client, daemonId) {
11190
11274
  await client.from("daemons").update({ status: "online", last_seen_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", daemonId);
@@ -11389,11 +11473,11 @@ function checkAgainstDecisions(item, prior, sourceDates) {
11389
11473
  }
11390
11474
  if (match.status === "rejected") {
11391
11475
  if (!match.decidedAt) return `"${item.title}" was rejected; it comes back only with evidence written since`;
11392
- const newer = item.sources.some((s) => {
11476
+ const newer2 = item.sources.some((s) => {
11393
11477
  const at = sourceDates(s.ref);
11394
11478
  return at !== void 0 && at > match.decidedAt;
11395
11479
  });
11396
- if (!newer) {
11480
+ if (!newer2) {
11397
11481
  return `"${item.title}" was rejected on ${match.decidedAt.slice(0, 10)}. It comes back only citing something learned since, and saying what changed`;
11398
11482
  }
11399
11483
  }
@@ -11537,14 +11621,14 @@ function refuseFetch(url, allowlist, open = false) {
11537
11621
 
11538
11622
  // ../memory-mcp/src/repo-rules.ts
11539
11623
  import { lstat, unlink } from "fs/promises";
11540
- import { relative as relative3, resolve as resolve5, sep } from "path";
11624
+ import { relative as relative3, resolve as resolve5, sep as sep2 } from "path";
11541
11625
  var REMOVE_MAX = 50;
11542
11626
  function refuseRemove(workspace, requested) {
11543
- const root = resolve5(workspace);
11544
- const target = resolve5(root, requested);
11545
- const rel = relative3(root, target);
11546
- if (!rel || rel.startsWith("..") || rel.startsWith(`..${sep}`)) return "outside the workspace";
11547
- if (rel === ".git" || rel.startsWith(`.git${sep}`)) return "inside .git";
11627
+ const root2 = resolve5(workspace);
11628
+ const target = resolve5(root2, requested);
11629
+ const rel = relative3(root2, target);
11630
+ if (!rel || rel.startsWith("..") || rel.startsWith(`..${sep2}`)) return "outside the workspace";
11631
+ if (rel === ".git" || rel.startsWith(`.git${sep2}`)) return "inside .git";
11548
11632
  if (looksLikeASecret(rel)) return "looks like a secret, which the agent may neither read nor remove";
11549
11633
  return null;
11550
11634
  }
@@ -13163,16 +13247,16 @@ async function startMemoryServer(env = process.env) {
13163
13247
  import { readFile as readFile9 } from "fs/promises";
13164
13248
 
13165
13249
  // src/project.ts
13166
- import { execFile as execFile6 } from "child_process";
13250
+ import { execFile as execFile7 } from "child_process";
13167
13251
  import { basename } from "path";
13168
- import { promisify as promisify6 } from "util";
13252
+ import { promisify as promisify7 } from "util";
13169
13253
 
13170
13254
  // src/local.ts
13171
13255
  import { readFile as readFile8 } from "fs/promises";
13172
- import { join as join11 } from "path";
13256
+ import { join as join12 } from "path";
13173
13257
  async function localDaemonId() {
13174
13258
  try {
13175
- const parsed = JSON.parse(await readFile8(join11(nanopmHome(), "daemon.json"), "utf8"));
13259
+ const parsed = JSON.parse(await readFile8(join12(nanopmHome(), "daemon.json"), "utf8"));
13176
13260
  return parsed.id ?? null;
13177
13261
  } catch {
13178
13262
  return null;
@@ -13183,30 +13267,30 @@ function useColor() {
13183
13267
  }
13184
13268
 
13185
13269
  // src/project.ts
13186
- var execFileAsync6 = promisify6(execFile6);
13270
+ var execFileAsync7 = promisify7(execFile7);
13187
13271
  async function detectRepo(cwd) {
13188
- let root;
13272
+ let root2;
13189
13273
  try {
13190
- root = (await execFileAsync6("git", ["-C", cwd, "rev-parse", "--show-toplevel"])).stdout.trim();
13274
+ root2 = (await execFileAsync7("git", ["-C", cwd, "rev-parse", "--show-toplevel"])).stdout.trim();
13191
13275
  } catch {
13192
13276
  return null;
13193
13277
  }
13194
13278
  let remote = null;
13195
13279
  try {
13196
- remote = canonicalRemote((await execFileAsync6("git", ["-C", root, "remote", "get-url", "origin"])).stdout.trim());
13280
+ remote = canonicalRemote((await execFileAsync7("git", ["-C", root2, "remote", "get-url", "origin"])).stdout.trim());
13197
13281
  } catch {
13198
13282
  remote = null;
13199
13283
  }
13200
- const name = remote ? remote.split("/").pop() : basename(root);
13201
- return { root, remote, name, empty: await repoIsEmpty(root) };
13284
+ const name = remote ? remote.split("/").pop() : basename(root2);
13285
+ return { root: root2, remote, name, empty: await repoIsEmpty(root2) };
13202
13286
  }
13203
- async function repoIsEmpty(root) {
13287
+ async function repoIsEmpty(root2) {
13204
13288
  try {
13205
- await execFileAsync6("git", ["-C", root, "rev-parse", "--verify", "HEAD"]);
13289
+ await execFileAsync7("git", ["-C", root2, "rev-parse", "--verify", "HEAD"]);
13206
13290
  } catch {
13207
13291
  return true;
13208
13292
  }
13209
- const files = (await execFileAsync6("git", ["-C", root, "ls-files"])).stdout.split("\n").filter(Boolean);
13293
+ const files = (await execFileAsync7("git", ["-C", root2, "ls-files"])).stdout.split("\n").filter(Boolean);
13210
13294
  return files.every((f) => {
13211
13295
  const base = basename(f);
13212
13296
  return base.startsWith(".") || /^(readme|licen[cs]e)(\.|$)/i.test(base);
@@ -13875,17 +13959,17 @@ async function askOne(auth, projectId, question, daemonId, opts, out) {
13875
13959
  }
13876
13960
 
13877
13961
  // src/commands/daemon-service.ts
13878
- import { execFile as execFile7 } from "child_process";
13879
- import { existsSync as existsSync4 } from "fs";
13880
- import { mkdir as mkdir6, readFile as readFile11, rm as rm4, writeFile as writeFile4 } from "fs/promises";
13881
- import { homedir as homedir3 } from "os";
13882
- import { join as join12 } from "path";
13883
- import { promisify as promisify7 } from "util";
13962
+ import { execFile as execFile8 } from "child_process";
13963
+ import { existsSync as existsSync5 } from "fs";
13964
+ import { mkdir as mkdir6, readFile as readFile11, rm as rm5, writeFile as writeFile5 } from "fs/promises";
13965
+ import { homedir as homedir4 } from "os";
13966
+ import { join as join13 } from "path";
13967
+ import { promisify as promisify8 } from "util";
13884
13968
 
13885
13969
  // package.json
13886
13970
  var package_default = {
13887
13971
  name: "@nanopm/cli",
13888
- version: "0.1.5",
13972
+ version: "0.1.6",
13889
13973
  description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
13890
13974
  type: "module",
13891
13975
  license: "MIT",
@@ -13934,13 +14018,13 @@ var package_default = {
13934
14018
  var VERSION = package_default.version;
13935
14019
 
13936
14020
  // src/commands/daemon-service.ts
13937
- var execFileAsync7 = promisify7(execFile7);
14021
+ var execFileAsync8 = promisify8(execFile8);
13938
14022
  var LAUNCHD_LABEL = "pm.nano.daemon";
13939
14023
  function plistPath() {
13940
- return join12(homedir3(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
14024
+ return join13(homedir4(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
13941
14025
  }
13942
14026
  function daemonLogPath() {
13943
- return join12(nanopmHome(), "daemon.log");
14027
+ return join13(nanopmHome(), "daemon.log");
13944
14028
  }
13945
14029
  function renderPlist(opts) {
13946
14030
  const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -13999,31 +14083,39 @@ async function installDaemon(out = (l) => console.log(l), opts = {}) {
13999
14083
  out(web.ok ? "Claude's own search answers; searches go through your subscription." : `Claude's search is not available on this machine (${web.message}); searches use DuckDuckGo.`);
14000
14084
  const bin = process.argv[1];
14001
14085
  if (!bin) throw new Error("cannot locate the nanopm binary");
14002
- const home = homedir3();
14003
- const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join12(home, ".local", "bin")].join(":");
14004
- await mkdir6(join12(home, "Library", "LaunchAgents"), { recursive: true });
14086
+ const home = homedir4();
14087
+ const path = [process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", join13(home, ".local", "bin")].join(":");
14088
+ await mkdir6(join13(home, "Library", "LaunchAgents"), { recursive: true });
14005
14089
  await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
14006
14090
  if (opts.shared !== void 0) await rememberShared(opts.shared);
14007
14091
  await found({ ...detection, authenticated: true });
14008
- await writeFile4(plistPath(), renderPlist({ node: process.execPath, bin, home, path, logPath: daemonLogPath(), nanopmHome: nanopmHome() }), { mode: 420 });
14009
- const target = `gui/${process.getuid?.() ?? 501}`;
14010
- await execFileAsync7("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
14011
- await execFileAsync7("launchctl", ["bootstrap", target, plistPath()]);
14092
+ await writeFile5(plistPath(), renderPlist({ node: process.execPath, bin, home, path, logPath: daemonLogPath(), nanopmHome: nanopmHome() }), { mode: 420 });
14093
+ const target = guiTarget();
14094
+ await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
14095
+ await execFileAsync8("launchctl", ["bootstrap", target, plistPath()]);
14012
14096
  out(`Installed ${LAUNCHD_LABEL}; the daemon starts now and at every login. Logs: ${daemonLogPath()}`);
14013
14097
  const shared = opts.shared ?? await localShared();
14014
14098
  out(shared ? "This machine is shared with your projects: it also runs scheduled reviews and partners' jobs." : "This machine runs your jobs only. `nanopm daemon install --share` to offer it to your projects.");
14015
14099
  if (opts.next !== false) out(`Next, in a repo: nanopm onboard \xB7 or continue at ${webUrl()}/new`);
14016
14100
  }
14101
+ function guiTarget() {
14102
+ return `gui/${process.getuid?.() ?? 501}`;
14103
+ }
14104
+ async function restartDaemon() {
14105
+ if (!existsSync5(plistPath())) return false;
14106
+ await execFileAsync8("launchctl", ["kickstart", "-k", `${guiTarget()}/${LAUNCHD_LABEL}`]);
14107
+ return true;
14108
+ }
14017
14109
  async function uninstallDaemon(out = (l) => console.log(l)) {
14018
14110
  requireMac();
14019
- const target = `gui/${process.getuid?.() ?? 501}`;
14020
- await execFileAsync7("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
14021
- await rm4(plistPath(), { force: true });
14111
+ const target = guiTarget();
14112
+ await execFileAsync8("launchctl", ["bootout", target, plistPath()]).catch(() => void 0);
14113
+ await rm5(plistPath(), { force: true });
14022
14114
  out(`Removed ${LAUNCHD_LABEL}.`);
14023
14115
  }
14024
14116
  async function daemonStatus(out = (l) => console.log(l)) {
14025
14117
  const id = await localDaemonId();
14026
- const installed = process.platform === "darwin" && existsSync4(plistPath());
14118
+ const installed = process.platform === "darwin" && existsSync5(plistPath());
14027
14119
  out(`launchd: ${installed ? "installed" : "not installed"}`);
14028
14120
  if (!id) {
14029
14121
  out("daemon: never registered on this machine");
@@ -14053,14 +14145,14 @@ async function daemonLogs(lines = 50) {
14053
14145
  }
14054
14146
  async function localWebSearch() {
14055
14147
  try {
14056
- const parsed = JSON.parse(await readFile11(join12(nanopmHome(), "daemon.json"), "utf8"));
14148
+ const parsed = JSON.parse(await readFile11(join13(nanopmHome(), "daemon.json"), "utf8"));
14057
14149
  return parsed.webSearch === true;
14058
14150
  } catch {
14059
14151
  return false;
14060
14152
  }
14061
14153
  }
14062
14154
  async function rememberWebSearch(webSearch2) {
14063
- const path = join12(nanopmHome(), "daemon.json");
14155
+ const path = join13(nanopmHome(), "daemon.json");
14064
14156
  let parsed = {};
14065
14157
  try {
14066
14158
  parsed = JSON.parse(await readFile11(path, "utf8"));
@@ -14068,18 +14160,18 @@ async function rememberWebSearch(webSearch2) {
14068
14160
  parsed = {};
14069
14161
  }
14070
14162
  await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
14071
- await writeFile4(path, JSON.stringify({ ...parsed, webSearch: webSearch2 }, null, 2) + "\n", { mode: 384 });
14163
+ await writeFile5(path, JSON.stringify({ ...parsed, webSearch: webSearch2 }, null, 2) + "\n", { mode: 384 });
14072
14164
  }
14073
14165
  async function localShared() {
14074
14166
  try {
14075
- const parsed = JSON.parse(await readFile11(join12(nanopmHome(), "daemon.json"), "utf8"));
14167
+ const parsed = JSON.parse(await readFile11(join13(nanopmHome(), "daemon.json"), "utf8"));
14076
14168
  return Boolean(parsed.shared);
14077
14169
  } catch {
14078
14170
  return false;
14079
14171
  }
14080
14172
  }
14081
14173
  async function rememberShared(shared) {
14082
- const path = join12(nanopmHome(), "daemon.json");
14174
+ const path = join13(nanopmHome(), "daemon.json");
14083
14175
  let parsed = {};
14084
14176
  try {
14085
14177
  parsed = JSON.parse(await readFile11(path, "utf8"));
@@ -14087,7 +14179,7 @@ async function rememberShared(shared) {
14087
14179
  parsed = {};
14088
14180
  }
14089
14181
  await mkdir6(nanopmHome(), { recursive: true, mode: 448 });
14090
- await writeFile4(path, JSON.stringify({ ...parsed, shared }, null, 2) + "\n", { mode: 384 });
14182
+ await writeFile5(path, JSON.stringify({ ...parsed, shared }, null, 2) + "\n", { mode: 384 });
14091
14183
  }
14092
14184
 
14093
14185
  // src/format/log.ts
@@ -14215,11 +14307,11 @@ async function correctionNote(client, jobId) {
14215
14307
  import { setTimeout as sleep4 } from "timers/promises";
14216
14308
 
14217
14309
  // src/browser.ts
14218
- import { execFile as execFile8 } from "child_process";
14310
+ import { execFile as execFile9 } from "child_process";
14219
14311
  function openBrowser(url, interactive = Boolean(process.stdout.isTTY)) {
14220
14312
  if (!interactive) return false;
14221
14313
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
14222
- execFile8(cmd, [url], () => void 0);
14314
+ execFile9(cmd, [url], () => void 0);
14223
14315
  return true;
14224
14316
  }
14225
14317
 
@@ -14303,6 +14395,33 @@ async function logout(out = (l) => console.log(l)) {
14303
14395
  out("Logged out. The daemon on this machine will stop taking jobs at its next start.");
14304
14396
  }
14305
14397
 
14398
+ // src/commands/update.ts
14399
+ async function update(opts, out = (l) => console.log(l)) {
14400
+ if (opts.auto !== void 0) {
14401
+ if (opts.auto !== "on" && opts.auto !== "off") throw new Error("--auto takes on or off");
14402
+ await setAutoUpdate(opts.auto === "on");
14403
+ out(opts.auto === "on" ? "Automatic updates are on: NanoPM updates itself when it is idle." : "Automatic updates are off. `nanopm update` still updates when you ask.");
14404
+ return;
14405
+ }
14406
+ if (installKind() !== "installer") {
14407
+ out(`This NanoPM (${VERSION}) was not installed with the install line, so it does not update itself.`);
14408
+ out("From a clone of nanopm2: git pull && pnpm build && nanopm daemon install");
14409
+ out("Installed with npm: npm i -g @nanopm/cli@latest && nanopm daemon install");
14410
+ return;
14411
+ }
14412
+ const latest = await latestVersion();
14413
+ if (!latest) throw new Error("could not reach the npm registry; try again in a moment");
14414
+ if (!newer(latest, VERSION)) {
14415
+ out(`Already up to date (${VERSION}).`);
14416
+ return;
14417
+ }
14418
+ out(`Updating ${VERSION} \u2192 ${latest}\u2026`);
14419
+ const done = await installVersion(latest);
14420
+ if (!done.ok) throw new Error(`${done.why}
14421
+ The install line does the same: curl -fsSL https://nanopm.app/install | sh`);
14422
+ out(await restartDaemon() ? `Updated ${VERSION} \u2192 ${latest}; the daemon restarted on it.` : `Updated ${VERSION} \u2192 ${latest}.`);
14423
+ }
14424
+
14306
14425
  // src/format/roadmap.ts
14307
14426
  var SHAPE = {
14308
14427
  proposed: { mark: "\u25CB", colour: ANSI.amber, label: "proposed" },
@@ -15292,7 +15411,7 @@ async function verifyCommand(command, opts) {
15292
15411
 
15293
15412
  // src/detect-sources.ts
15294
15413
  import { readFile as readFile12 } from "fs/promises";
15295
- import { join as join13 } from "path";
15414
+ import { join as join14 } from "path";
15296
15415
  var ENV_FILES = [".env", ".env.local", ".env.development.local", ".env.development", ".env.example"];
15297
15416
  var POSTGRES_KEYS = [
15298
15417
  "DATABASE_URL",
@@ -15342,7 +15461,7 @@ async function envFiles(repoRoot) {
15342
15461
  const out = [];
15343
15462
  for (const file of ENV_FILES) {
15344
15463
  try {
15345
- out.push({ file, env: parseEnv(await readFile12(join13(repoRoot, file), "utf8")) });
15464
+ out.push({ file, env: parseEnv(await readFile12(join14(repoRoot, file), "utf8")) });
15346
15465
  } catch {
15347
15466
  continue;
15348
15467
  }
@@ -17083,7 +17202,7 @@ async function invite(email, opts) {
17083
17202
  const [, ...rest] = inviteLines({ name: project.name, code, email: email.trim().toLowerCase(), web: webUrl() });
17084
17203
  return [`Invite to ${paint(ANSI.bold, project.name)}: ${paint(ANSI.bold, code)}`, ...rest.map((l) => l ? paint(ANSI.dim, l) : l)].join("\n");
17085
17204
  }
17086
- async function join14(code, opts) {
17205
+ async function join15(code, opts) {
17087
17206
  const normalized2 = code.trim().toUpperCase();
17088
17207
  if (!INVITE_CODE.test(normalized2)) throw new Error(`that does not look like an invite code: ten letters and digits, like ${"K7PQ2XM9RT"}`);
17089
17208
  const repo = await detectRepo(opts.cwd);
@@ -17368,7 +17487,7 @@ async function showSchedule(opts) {
17368
17487
  }
17369
17488
 
17370
17489
  // src/commands/status.ts
17371
- import { existsSync as existsSync5 } from "fs";
17490
+ import { existsSync as existsSync6 } from "fs";
17372
17491
  async function status(opts) {
17373
17492
  const auth = await authenticate();
17374
17493
  const color = opts.color ?? useColor();
@@ -17421,7 +17540,7 @@ async function projectStatus(client, project) {
17421
17540
  openProblems: (problems2.data ?? []).length,
17422
17541
  unprovenProblems: (problems2.data ?? []).filter((p) => p.importance_basis === "judgment").length,
17423
17542
  search: describeSearch(search, await localWebSearch()),
17424
- onThisMachine: Boolean(project.repoPath && existsSync5(project.repoPath)),
17543
+ onThisMachine: Boolean(project.repoPath && existsSync6(project.repoPath)),
17425
17544
  measures: measures({
17426
17545
  solutionStatuses: (roadmap2.data ?? []).map((r) => r.status),
17427
17546
  decisions: decisions2.data ?? [],
@@ -17460,6 +17579,7 @@ function buildProgram() {
17460
17579
  process.once("SIGINT", stop2);
17461
17580
  process.once("SIGTERM", stop2);
17462
17581
  const result = await runDaemon({ once: opts.once, version: VERSION, signal: abort.signal });
17582
+ if (result.restart) process.exit(0);
17463
17583
  if (opts.once && result.processed === 0) {
17464
17584
  console.error("nanopm: no queued job arrived in time");
17465
17585
  process.exitCode = 2;
@@ -17469,6 +17589,7 @@ function buildProgram() {
17469
17589
  daemon.command("uninstall").description("Remove the background daemon").action(async () => uninstallDaemon());
17470
17590
  daemon.command("status").description("Is the daemon online, and what is queued").action(async () => daemonStatus());
17471
17591
  daemon.command("logs").description("The daemon log").option("-n <lines>", "lines", (v) => Number(v), 50).action(async (opts) => console.log(await daemonLogs(opts.n)));
17592
+ program.command("update").description("Update NanoPM to the latest release now; it also updates itself when idle").option("--auto <on|off>", "turn the automatic update on or off").action(async (opts) => update(opts));
17472
17593
  program.command("login").description("Connect this computer to your nano.pm account, and start the daemon on it").option("--no-browser", "print the link instead of opening it").option("--no-daemon", "link only; do not install the background daemon").option("--web <url>", "nano.pm base URL (development)").action(async (opts) => {
17473
17594
  await login({ noBrowser: !opts.browser, noDaemon: !opts.daemon, web: opts.web });
17474
17595
  });
@@ -17779,7 +17900,7 @@ function buildProgram() {
17779
17900
  console.log(await invite(email, { cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
17780
17901
  });
17781
17902
  program.command("join <code>").description("Join a project you were invited to, from inside your clone of its repository").option("--no-color", "plain output").action(async (code, opts) => {
17782
- console.log(await join14(code, { cwd: process.cwd(), color: colorOf(opts) }));
17903
+ console.log(await join15(code, { cwd: process.cwd(), color: colorOf(opts) }));
17783
17904
  });
17784
17905
  program.command("members").description("Who is on this project, and whether their daemon is awake").option("--project <id>", "project id (default: the project of the current repo)").option("--no-color", "plain output").action(async (opts) => {
17785
17906
  console.log(await members({ cwd: process.cwd(), projectId: opts.project, color: colorOf(opts) }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanopm/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",