@join-pantheon/cli 0.32.1 → 0.33.0-rc.2

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/ledger.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, existsSync, openSync, readSync, writeSync, fsyncSync, closeSync, fstatSync, copyFileSync, truncateSync, chmodSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { load } from "./config.js";
4
- import { fsyncDir, renameRetrying, writeFileAtomic } from "./store.js";
4
+ import { fsyncDir, renameRetrying, syncFd, writeFileAtomic } from "./store.js";
5
5
  export class SpaceFull extends Error {
6
6
  bytes;
7
7
  cap;
@@ -107,7 +107,7 @@ export class Ledger {
107
107
  const fd = openSync(this.file, "a", 0o600);
108
108
  try {
109
109
  writeSync(fd, line);
110
- fsyncSync(fd);
110
+ syncFd(fd);
111
111
  }
112
112
  finally {
113
113
  closeSync(fd);
package/dist/machine.js CHANGED
@@ -3,7 +3,8 @@ import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, re
3
3
  import { homedir, hostname, networkInterfaces, platform } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { CONFIG_DIR } from "./config.js";
6
- import { borrowedHome, disableService, existingUnitPath, plistPath, serviceInstalled, serviceLogFile, unitPath, unitPaths, uninstallService } from "./service.js";
6
+ import { borrowedHome, disableService, existingUnitPath, plistPath, serviceInstalled, serviceLogFile, stopWindowsHost, unitPath, unitPaths, uninstallService } from "./service.js";
7
+ import { LEGACY_TASK_NAME, hostAlive, hostChildRunning, hostFiles, isHostKind, readHostState, recordedTaskName } from "./win-host.js";
7
8
  import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir } from "./install.js";
8
9
  const sh = (cmd) => {
9
10
  try {
@@ -100,7 +101,33 @@ function fromSystemd(kind) {
100
101
  ...(s.lastExit ? { lastExit: s.lastExit } : {}),
101
102
  };
102
103
  }
104
+ export function elapsed(ms) {
105
+ const s = Math.max(0, Math.floor(ms / 1000));
106
+ const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
107
+ const two = (n) => String(n).padStart(2, "0");
108
+ return `${d ? `${d}-` : ""}${d || h ? `${two(h)}:` : ""}${two(m)}:${two(sec)}`;
109
+ }
110
+ export function hostProc(kind, o = {}) {
111
+ if ((o.platform ?? platform()) !== "win32" || !isHostKind(kind))
112
+ return {};
113
+ const home = o.home ?? CONFIG_DIR;
114
+ if (!existsSync(hostFiles(home).definition(kind)))
115
+ return {};
116
+ const now = o.now ?? Date.now();
117
+ const st = readHostState(home);
118
+ const up = hostAlive(st, now, o.alive);
119
+ const run = hostChildRunning(kind, home, now, o.alive);
120
+ const r = up ? st.children[kind] : undefined;
121
+ return {
122
+ managedBy: "host", label: `task ${recordedTaskName(home) ?? LEGACY_TASK_NAME}`, logPath: hostFiles(home).childLog(kind),
123
+ ...(run ? { running: true, pid: run.pid, ...(run.since ? { since: elapsed(now - run.since) } : {}) } : { running: false }),
124
+ ...(r?.lastExit && r.lastExit !== "0" ? { lastExit: r.lastExit } : {}),
125
+ };
126
+ }
103
127
  const fromManager = (kind) => {
128
+ const h = hostProc(kind);
129
+ if (h.managedBy)
130
+ return h;
104
131
  const l = fromLaunchd(kind);
105
132
  return l.managedBy ? l : fromSystemd(kind);
106
133
  };
@@ -438,6 +465,7 @@ const defaultStopIo = (port = 8787) => ({
438
465
  borrowed: () => borrowedHome(),
439
466
  loadedFrom: (kind) => managerDefinitionPath(kind),
440
467
  kill: killPid,
468
+ ...(platform() === "win32" ? { stopHost: () => stopWindowsHost() } : {}),
441
469
  });
442
470
  export const borrowedHomeLine = (kind) => `${kind === "services" ? "services" : `${kind}:`} left alone — HOME (${homedir()}) is not your own home (a test, a sandbox or another account's folder), so the service manager's jobs here are the real user's, not this home's. Nothing was stopped; run this from your own login to stop your services`;
443
471
  export const isPantheonCommand = (cmd) => /pantheon/i.test(cmd) || /\bcli\.js\s+(relay|start)\b/.test(cmd);
@@ -500,7 +528,9 @@ export function stopProcResult(kind, io = defaultStopIo()) {
500
528
  else {
501
529
  const how = platform() === "darwin"
502
530
  ? `launchctl disable gui/${process.getuid?.() ?? 0}/dev.pantheon.${kind}`
503
- : `systemctl --user disable --now pantheon-${kind}`;
531
+ : platform() === "win32"
532
+ ? `pantheon service uninstall ${kind}`
533
+ : `systemctl --user disable --now pantheon-${kind}`;
504
534
  lines.push(`${kind}: could not disable the service (${e.message.split("\n")[0]}) — it may start again at login; try: ${how}`);
505
535
  }
506
536
  }
@@ -511,7 +541,7 @@ export function stopProcResult(kind, io = defaultStopIo()) {
511
541
  const stillUp = !!after.running && !k?.killed;
512
542
  if (disabled) {
513
543
  lines.unshift(`${kind}: ${stillUp ? "disabled, but a process is still running (see below)" : p.running || k?.killed ? "stopped, and will not start again at login" : "was not running; disabled, and will not start again at login"}`);
514
- lines.push(` start it again with: pantheon service install ${kind}`);
544
+ lines.push(` start it again with: pantheon service ${p.managedBy === "host" ? "start" : "install"} ${kind}`);
515
545
  }
516
546
  return { lines, stopped: (p.running && disabled && !stillUp) || !!k?.killed };
517
547
  }
@@ -576,6 +606,11 @@ export function stopMachine(what, opts, io = defaultStopIo(opts.port), hosts = h
576
606
  lines.push(`worker: ${k}`);
577
607
  }
578
608
  }
609
+ if (what === "all" && io.stopHost) {
610
+ const h = io.stopHost();
611
+ if (h.length)
612
+ lines.push(...h.map((l) => `host: ${l.trim().replace(/^the Pantheon host /, "")}`), " it starts again at your next logon (services stay paused), or now: pantheon service start worker");
613
+ }
579
614
  if (!relayStopped)
580
615
  return { lines };
581
616
  const teams = opts.teams ?? (opts.teamRelay ? [{ name: "", relay: opts.teamRelay }] : []);
@@ -670,6 +705,142 @@ export function removeHomeLeftovers(home, keep, opts = {}) {
670
705
  }
671
706
  return { removed, left };
672
707
  }
708
+ export const PANTHEON_HOME_ENTRIES = [
709
+ { name: "config.json", what: "your key and team tokens", identity: true },
710
+ { name: "config.json.bak", what: "the previous config.json", identity: true },
711
+ { name: "keys.json", what: "the teammates' keys this machine pinned", identity: true },
712
+ { name: "nonces.json", what: "the signed requests this machine has seen", identity: true },
713
+ { name: "continued.json", what: "what this machine already handled", identity: true },
714
+ { name: "cursors.json", what: "where each agent session last read", identity: true },
715
+ { name: "worker-cursor.json", what: "where the worker last read", identity: true },
716
+ { name: "profile.json", what: "your profile", identity: true },
717
+ { name: "worker-policy.json", what: "the worker's wake policy", identity: true },
718
+ { name: "spend.json", what: "the spend ledger", identity: true },
719
+ { name: "personas", what: "the worker's personas", dir: true, identity: true },
720
+ { name: "worker-state.json", what: "the worker's last state" },
721
+ { name: "worker-run.json", what: "the worker's record of a run in flight" },
722
+ { name: "run-slot.json", what: "the worker's run slot" },
723
+ { name: "worker.pid", what: "the worker's pid file" },
724
+ { name: /^notifier-state(-.+)?\.json$/, sample: "notifier-state-team%2Fone.json", what: "the alerts' state" },
725
+ { name: "update-check.json", what: "the update check's cache" },
726
+ { name: "uri-key", what: "the key pantheon:// links are signed with" },
727
+ { name: "board.json", what: "the board's state" },
728
+ { name: "hook-intent.json", what: "the hook intent" },
729
+ { name: "hook-guard.json", what: "the hook guard" },
730
+ { name: /^(worker|relay|notifier|menubar)\.log(\.1)?$/, sample: "worker.log", what: "service logs an older version wrote here" },
731
+ { name: /^\.write-check\..+$/, sample: ".write-check.1234.5678", what: "a leftover write probe" },
732
+ { name: "bin", what: "the menu bar's and alerts' files", dir: true },
733
+ { name: "logs", what: "the service logs", dir: true },
734
+ { name: "host", what: "the Windows host's queue, state and markers", dir: true },
735
+ { name: "mcp", what: "the agent windows' server state", dir: true },
736
+ { name: "run-homes", what: "the woken runs' homes", dir: true },
737
+ { name: "worktrees", what: "the worker's worktrees", dir: true },
738
+ { name: "no-hooks", what: "the empty hooks folder woken runs' git uses", dir: true },
739
+ { name: "lfs-hooks", what: "the LFS-only hooks folder woken runs' git uses", dir: true },
740
+ { name: "runtime", what: "Pantheon's own Node", dir: true, kept: "managed" },
741
+ { name: "data", what: "the relay's ledger", dir: true, kept: "it is your team's history and may be its only copy; delete it yourself once no relay runs from it" },
742
+ { name: /^(\.DS_Store|Thumbs\.db|desktop\.ini)$/i, sample: ".DS_Store", what: "the file browser's folder cache" },
743
+ ];
744
+ const LOCK_OR_TMP = /^(.+?)(\.lock|(\.[0-9a-f]+)+\.tmp)$/i;
745
+ export function homeEntryFor(name) {
746
+ const direct = (n) => PANTHEON_HOME_ENTRIES.find((e) => typeof e.name === "string" ? e.name === n : e.name.test(n)) ?? null;
747
+ const e = direct(name);
748
+ if (e)
749
+ return e;
750
+ const m = LOCK_OR_TMP.exec(name);
751
+ const base = m ? direct(m[1]) : null;
752
+ return base && !base.dir ? base : null;
753
+ }
754
+ const pause = (ms) => { try {
755
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
756
+ }
757
+ catch { } };
758
+ const realSweepFs = {
759
+ list: (d) => readdirSync(d),
760
+ exists: (p) => existsSync(p),
761
+ remove: (p) => rmSync(p, { recursive: true, force: true }),
762
+ removeDir: (d) => rmdirSync(d),
763
+ };
764
+ export function sweepPantheonHome(home, o = {}) {
765
+ const fs = o.fs ?? realSweepFs;
766
+ const tries = o.tries ?? 5, waitMs = o.waitMs ?? 200;
767
+ const os = o.os ?? platform();
768
+ const removed = [], left = [];
769
+ if (!fs.exists(home))
770
+ return { removed, left, gone: true };
771
+ const spare = new Set((o.spare ?? []).map((p) => resolve(p)));
772
+ const attempt = (p, rm) => {
773
+ let why = "";
774
+ for (let i = 0; i < tries; i++) {
775
+ try {
776
+ rm(p);
777
+ }
778
+ catch (e) {
779
+ why = String(e.code ?? e.message.split("\n")[0]);
780
+ }
781
+ if (!fs.exists(p))
782
+ return null;
783
+ if (i < tries - 1)
784
+ pause(waitMs * (i + 1));
785
+ }
786
+ return why || "still there";
787
+ };
788
+ const lockedHint = os === "win32" ? "; a Pantheon process (an agent window, the menu bar) may still have it open — close it, then delete it" : "; delete it yourself";
789
+ const gone = [], foreign = [], keptForIdentity = [];
790
+ let names = [];
791
+ try {
792
+ names = fs.list(home);
793
+ }
794
+ catch (e) {
795
+ return { removed, left: [`${home} — could not be read (${e.message.split("\n")[0]}), so nothing in it was removed`], gone: false };
796
+ }
797
+ for (const name of names.sort()) {
798
+ const p = join(home, name);
799
+ const e = homeEntryFor(name);
800
+ if (!e) {
801
+ foreign.push(name);
802
+ continue;
803
+ }
804
+ if (spare.has(resolve(p)))
805
+ continue;
806
+ if (e.kept === "managed")
807
+ continue;
808
+ if (e.kept) {
809
+ left.push(`${e.what}, ${p} — kept: ${e.kept}`);
810
+ continue;
811
+ }
812
+ if (o.keepIdentity && e.identity) {
813
+ keptForIdentity.push(name);
814
+ continue;
815
+ }
816
+ const why = attempt(p, fs.remove);
817
+ if (why === null)
818
+ gone.push(name);
819
+ else
820
+ left.push(`${e.what}, ${p} — could not remove (${why})${lockedHint}`);
821
+ }
822
+ if (gone.length)
823
+ removed.push(`Pantheon's own files in ${home}: ${gone.join(", ")}`);
824
+ if (keptForIdentity.length)
825
+ left.push(`what goes with your identity in ${home}: ${keptForIdentity.join(", ")} (--keep-identity)`);
826
+ let rest = [];
827
+ try {
828
+ rest = fs.list(home);
829
+ }
830
+ catch { }
831
+ if (!rest.length) {
832
+ const why = attempt(home, fs.removeDir);
833
+ if (why === null) {
834
+ removed.push(`${home} itself, now empty`);
835
+ return { removed, left, gone: true };
836
+ }
837
+ left.push(`${home} — empty, but could not remove it (${why})${lockedHint}`);
838
+ return { removed, left, gone: false };
839
+ }
840
+ if (foreign.length)
841
+ left.push(`${home}, for what is not Pantheon's: ${foreign.join(", ")} — Pantheon never writes ${foreign.length === 1 ? "it" : "these"}, so ${foreign.length === 1 ? "it was" : "they were"} not touched`);
842
+ return { removed, left, gone: false };
843
+ }
673
844
  export const accountStillExists = (email) => `your account${email ? ` ${email}` : ""} still exists — this machine is signed out. Sign back in with: pantheon login${email ? ` --email ${email}` : ""} (pantheon signup would fail: the email is taken). To delete the account itself: pantheon account delete, before leaving`;
674
845
  const LOG_KINDS = ["worker", "relay", "notifier", "menubar"];
675
846
  export function uninstallMachine(opts, io) {
package/dist/managed.js CHANGED
@@ -23,6 +23,7 @@ export function managedLayout(execPath = process.execPath, os = platform()) {
23
23
  root, nodeDir, node: p, npm: win32.join(nodeDir, "npm.cmd"), npmCli: win32.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"), binDir,
24
24
  launcher: win32.join(binDir, "pantheon.cmd"), pkgDir,
25
25
  entry: win32.join(pkgDir, "bin", "pantheon.js"), cli: win32.join(pkgDir, "dist", "cli.js"),
26
+ prevDir: win32.join(runtime, "cli.prev"),
26
27
  };
27
28
  }
28
29
  const p = posix.normalize(execPath);
@@ -42,8 +43,33 @@ export function managedLayout(execPath = process.execPath, os = platform()) {
42
43
  root, nodeDir, node: p, npm: posix.join(bin, "npm"), npmCli: posix.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"), binDir,
43
44
  launcher: posix.join(binDir, "pantheon"), pkgDir,
44
45
  entry: posix.join(pkgDir, "bin", "pantheon.js"), cli: posix.join(pkgDir, "dist", "cli.js"),
46
+ prevDir: posix.join(runtime, "cli.prev"),
45
47
  };
46
48
  }
49
+ const shq = (p) => `'${p.replace(/'/g, `'\\''`)}'`;
50
+ export function launcherRepairLines(m, os = platform()) {
51
+ if (os === "win32") {
52
+ return [
53
+ "rem An upgrade cut off mid-swap leaves the previous version beside the runtime: put it back.",
54
+ 'if not exist "%~dp0..\\runtime\\node\\node_modules\\@join-pantheon\\cli\\" if exist "%~dp0..\\runtime\\cli.prev\\bin\\pantheon.js" move "%~dp0..\\runtime\\cli.prev" "%~dp0..\\runtime\\node\\node_modules\\@join-pantheon\\cli" >nul 2>&1',
55
+ ];
56
+ }
57
+ return [
58
+ "# An upgrade cut off mid-swap leaves the previous version beside the runtime: put it back.",
59
+ `[ -e ${shq(m.pkgDir)} ] || { [ -f ${shq(posix.join(m.prevDir, "bin", "pantheon.js"))} ] && mv ${shq(m.prevDir)} ${shq(m.pkgDir)} 2>/dev/null; }`,
60
+ ];
61
+ }
62
+ export function launcherWithRepair(current, m, os = platform()) {
63
+ if (current.includes("cli.prev"))
64
+ return null;
65
+ const nl = os === "win32" ? "\r\n" : "\n";
66
+ const lines = current.split(nl);
67
+ const at = os === "win32" ? lines.findIndex((l) => l.startsWith('"%~dp0..\\runtime\\node\\node.exe"')) : lines.findIndex((l) => l.startsWith("exec "));
68
+ if (at < 0)
69
+ return null;
70
+ lines.splice(at, 0, ...launcherRepairLines(m, os));
71
+ return lines.join(nl);
72
+ }
47
73
  export function childNodeFlags(execArgv = process.execArgv, layout = managedLayout()) {
48
74
  return execArgv.includes("--use-system-ca") || layout ? ["--use-system-ca"] : [];
49
75
  }
@@ -178,7 +204,7 @@ export function installerLine(relay, shell, args = []) {
178
204
  if (shell === "ps" && !args.length)
179
205
  return `irm ${base}/install.ps1 | iex`;
180
206
  const set = args.length ? `Set-Item Env:PANTHEON_ARGS '${args.join(" ")}'; ` : "";
181
- return `powershell -NoProfile -ExecutionPolicy Bypass -Command "${set}irm ${base}/install.ps1 | iex"`;
207
+ return `powershell -NoProfile -Command "${set}irm ${base}/install.ps1 | iex"`;
182
208
  }
183
209
  export function installerLineHere(relay, os = platform()) {
184
210
  return installerLine(relay, os === "win32" ? "ps" : "sh") ?? installerLine(HOSTED_INSTALLER_BASE, os === "win32" ? "ps" : "sh");