@join-pantheon/cli 0.31.2 → 0.32.0-rc.1

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.
@@ -0,0 +1,204 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { VERSION } from "./version.js";
5
+ export const INSTALLER_DIR = fileURLToPath(new URL("../installer/", import.meta.url));
6
+ export const VERSION_PLACEHOLDER = "__PANTHEON_VERSION__";
7
+ export function renderInstaller(text, version) {
8
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
9
+ return null;
10
+ if (!text.includes(VERSION_PLACEHOLDER))
11
+ return null;
12
+ return text.split(VERSION_PLACEHOLDER).join(version);
13
+ }
14
+ const cache = new Map();
15
+ export function installerScript(name, dir = INSTALLER_DIR, version = VERSION) {
16
+ const key = `${dir}\0${name}\0${version}`;
17
+ if (cache.has(key))
18
+ return cache.get(key);
19
+ let out = null;
20
+ try {
21
+ out = renderInstaller(readFileSync(join(dir, name), "utf8"), version);
22
+ }
23
+ catch {
24
+ out = null;
25
+ }
26
+ cache.set(key, out);
27
+ return out;
28
+ }
29
+ export const NPM_PACKAGE = "@join-pantheon/cli";
30
+ export const NPM_PACKUMENT_URL = "https://registry.npmjs.org/@join-pantheon%2fcli";
31
+ export const INSTALLER_MIN_CLI = "0.32.0";
32
+ function parseVer(v) {
33
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v);
34
+ return m ? { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: m[4] ?? null } : null;
35
+ }
36
+ export function compareVersions(a, b) {
37
+ const x = parseVer(a), y = parseVer(b);
38
+ if (!x || !y)
39
+ return a < b ? -1 : a > b ? 1 : 0;
40
+ for (const k of ["major", "minor", "patch"])
41
+ if (x[k] !== y[k])
42
+ return x[k] - y[k];
43
+ if (x.pre === y.pre)
44
+ return 0;
45
+ if (x.pre === null)
46
+ return 1;
47
+ if (y.pre === null)
48
+ return -1;
49
+ const p = x.pre.split("."), q = y.pre.split(".");
50
+ for (let i = 0; i < Math.max(p.length, q.length); i++) {
51
+ if (p[i] === undefined)
52
+ return -1;
53
+ if (q[i] === undefined)
54
+ return 1;
55
+ const pn = /^\d+$/.test(p[i]), qn = /^\d+$/.test(q[i]);
56
+ if (pn && qn && Number(p[i]) !== Number(q[i]))
57
+ return Number(p[i]) - Number(q[i]);
58
+ if (pn !== qn)
59
+ return pn ? -1 : 1;
60
+ if (p[i] !== q[i])
61
+ return p[i] < q[i] ? -1 : 1;
62
+ }
63
+ return 0;
64
+ }
65
+ export function pickFallback(target, versions, floor = INSTALLER_MIN_CLI) {
66
+ const t = parseVer(target);
67
+ if (!t)
68
+ return null;
69
+ let best = null;
70
+ for (const [v, meta] of Object.entries(versions)) {
71
+ const p = parseVer(v);
72
+ if (!p || p.pre !== null)
73
+ continue;
74
+ if (meta && typeof meta === "object" && meta.deprecated)
75
+ continue;
76
+ if (compareVersions(v, target) >= 0 || compareVersions(v, floor) < 0)
77
+ continue;
78
+ if (p.major !== t.major || p.minor < t.minor - 1)
79
+ continue;
80
+ if (!best || compareVersions(v, best) > 0)
81
+ best = v;
82
+ }
83
+ return best;
84
+ }
85
+ export class InstallerGate {
86
+ version;
87
+ confirmed = false;
88
+ fallback = null;
89
+ answered = false;
90
+ inflight = null;
91
+ timer = null;
92
+ closed = false;
93
+ said = "";
94
+ fetchImpl;
95
+ url;
96
+ timeoutMs;
97
+ recheckMs;
98
+ mode;
99
+ log;
100
+ constructor(opts = {}) {
101
+ this.version = opts.version ?? VERSION;
102
+ this.fetchImpl = opts.fetch ?? ((url, init) => fetch(url, init));
103
+ this.url = opts.url ?? NPM_PACKUMENT_URL;
104
+ this.timeoutMs = opts.timeoutMs ?? 8_000;
105
+ this.recheckMs = opts.recheckMs ?? 3 * 60_000;
106
+ this.mode = opts.mode ?? ((process.env.PANTHEON_INSTALLER_GATE ?? "").trim().toLowerCase() === "off" ? "off" : "on");
107
+ this.log = opts.log ?? (() => { });
108
+ }
109
+ get passThrough() { return this.mode === "off" || !parseVer(this.version); }
110
+ start() { void this.check(); }
111
+ check() {
112
+ if (this.confirmed || this.closed || this.passThrough)
113
+ return Promise.resolve();
114
+ this.inflight ??= this.ask().finally(() => {
115
+ this.inflight = null;
116
+ this.answered = true;
117
+ this.schedule();
118
+ });
119
+ return this.inflight;
120
+ }
121
+ async choose() {
122
+ const base = parseVer(this.version);
123
+ if (base && compareVersions(`${base.major}.${base.minor}.${base.patch}`, INSTALLER_MIN_CLI) < 0) {
124
+ return { unavailable: `this relay runs Pantheon ${this.version}, older than the one-line installer needs (${INSTALLER_MIN_CLI}). Install with npm instead: npm install -g @join-pantheon/cli` };
125
+ }
126
+ if (this.passThrough)
127
+ return { version: this.version, fallback: false };
128
+ if (!this.confirmed && !this.answered)
129
+ await this.check();
130
+ if (this.confirmed)
131
+ return { version: this.version, fallback: false };
132
+ if (this.fallback)
133
+ return { version: this.fallback, fallback: true };
134
+ return { unavailable: `the installer for Pantheon ${this.version} is not available yet - try again in a few minutes.` };
135
+ }
136
+ close() {
137
+ this.closed = true;
138
+ if (this.timer)
139
+ clearTimeout(this.timer);
140
+ this.timer = null;
141
+ }
142
+ schedule() {
143
+ if (this.confirmed || this.closed || this.timer)
144
+ return;
145
+ this.timer = setTimeout(() => { this.timer = null; void this.check(); }, this.recheckMs);
146
+ this.timer.unref?.();
147
+ }
148
+ note(line) {
149
+ if (line === this.said)
150
+ return;
151
+ this.said = line;
152
+ try {
153
+ this.log(line);
154
+ }
155
+ catch { }
156
+ }
157
+ async ask() {
158
+ const again = `asking again every ${Math.round(this.recheckMs / 1000)}s`;
159
+ const abort = new AbortController();
160
+ const timer = setTimeout(() => abort.abort(new Error(`no answer in ${this.timeoutMs}ms`)), this.timeoutMs);
161
+ try {
162
+ const res = await this.fetchImpl(this.url, {
163
+ headers: { accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" },
164
+ signal: abort.signal,
165
+ });
166
+ if (res.status === 404) {
167
+ try {
168
+ await res.body?.cancel();
169
+ }
170
+ catch { }
171
+ this.fallback = null;
172
+ this.note(`installer: ${NPM_PACKAGE} is not on npm at all; /install.sh and /install.ps1 say it is not available and fail (${again})`);
173
+ return;
174
+ }
175
+ if (!res.ok) {
176
+ try {
177
+ await res.body?.cancel();
178
+ }
179
+ catch { }
180
+ throw new Error(`the registry answered ${res.status}`);
181
+ }
182
+ const doc = await res.json();
183
+ const versions = doc && typeof doc === "object" && doc.versions && typeof doc.versions === "object" ? doc.versions : null;
184
+ if (!versions)
185
+ throw new Error("the registry's answer lists no versions");
186
+ if (Object.prototype.hasOwnProperty.call(versions, this.version)) {
187
+ this.confirmed = true;
188
+ this.note(`installer: ${NPM_PACKAGE}@${this.version} is on npm; /install.sh and /install.ps1 install it`);
189
+ return;
190
+ }
191
+ this.fallback = pickFallback(this.version, versions);
192
+ this.note(this.fallback
193
+ ? `installer: ${this.version} is not on npm yet; /install.sh and /install.ps1 install ${this.fallback} until it is (${again})`
194
+ : `installer: ${this.version} is not on npm yet and no compatible earlier release is; /install.sh and /install.ps1 say it is not available and fail (${again})`);
195
+ }
196
+ catch (e) {
197
+ this.note(`installer: could not ask npm whether ${this.version} is published (${e?.message ?? String(e)}); ${again}`);
198
+ }
199
+ finally {
200
+ clearTimeout(timer);
201
+ }
202
+ }
203
+ }
204
+ //# sourceMappingURL=installer.js.map
package/dist/machine.js CHANGED
@@ -629,7 +629,8 @@ export function removePantheonWorktrees(repos, roots, git = (repo, args) => exec
629
629
  }
630
630
  return { removed, left };
631
631
  }
632
- export function removeHomeLeftovers(home, keep) {
632
+ const MANAGED_LAUNCHER = "pantheon";
633
+ export function removeHomeLeftovers(home, keep, opts = {}) {
633
634
  const removed = [], left = [];
634
635
  const items = [
635
636
  ["worker-policy.json", "the worker's wake policy (who may wake it, and self-upgrade) — a rejoin starts from no grants"],
@@ -646,6 +647,19 @@ export function removeHomeLeftovers(home, keep) {
646
647
  left.push(`${what}, ${f} (--keep-identity)`);
647
648
  continue;
648
649
  }
650
+ if (name === "bin" && existsSync(join(f, MANAGED_LAUNCHER))) {
651
+ try {
652
+ const others = readdirSync(f).filter((e) => e !== MANAGED_LAUNCHER);
653
+ for (const e of others)
654
+ rmSync(join(f, e), { recursive: true, force: true });
655
+ if (others.length)
656
+ removed.push(`${what} (${f}${opts.uninstalling ? "" : "; the pantheon command there stays"})`);
657
+ }
658
+ catch (e) {
659
+ left.push(`${what}, ${f} — could not remove (${e.message.split("\n")[0]})`);
660
+ }
661
+ continue;
662
+ }
649
663
  try {
650
664
  rmSync(f, { recursive: true, force: true });
651
665
  removed.push(`${what} (${f})`);
@@ -0,0 +1,238 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { platform } from "node:os";
4
+ import { posix, win32 } from "node:path";
5
+ const PKG = ["@join-pantheon", "cli"];
6
+ export function managedLayout(execPath = process.execPath, os = platform()) {
7
+ if (!execPath)
8
+ return null;
9
+ if (os === "win32") {
10
+ const p = win32.normalize(execPath);
11
+ const nodeDir = win32.dirname(p);
12
+ const runtime = win32.dirname(nodeDir);
13
+ const root = win32.dirname(runtime);
14
+ if (win32.basename(p).toLowerCase() !== "node.exe")
15
+ return null;
16
+ if (win32.basename(nodeDir).toLowerCase() !== "node" || win32.basename(runtime).toLowerCase() !== "runtime")
17
+ return null;
18
+ if (win32.basename(root).toLowerCase() !== "pantheon")
19
+ return null;
20
+ const binDir = win32.join(root, "bin");
21
+ const pkgDir = win32.join(nodeDir, "node_modules", ...PKG);
22
+ return {
23
+ root, nodeDir, node: p, npm: win32.join(nodeDir, "npm.cmd"), npmCli: win32.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"), binDir,
24
+ launcher: win32.join(binDir, "pantheon.cmd"), pkgDir,
25
+ entry: win32.join(pkgDir, "bin", "pantheon.js"), cli: win32.join(pkgDir, "dist", "cli.js"),
26
+ };
27
+ }
28
+ const p = posix.normalize(execPath);
29
+ const bin = posix.dirname(p);
30
+ const nodeDir = posix.dirname(bin);
31
+ const runtime = posix.dirname(nodeDir);
32
+ const root = posix.dirname(runtime);
33
+ if (posix.basename(p) !== "node" || posix.basename(bin) !== "bin")
34
+ return null;
35
+ if (posix.basename(nodeDir) !== "node" || posix.basename(runtime) !== "runtime")
36
+ return null;
37
+ if (posix.basename(root) !== ".pantheon")
38
+ return null;
39
+ const binDir = posix.join(root, "bin");
40
+ const pkgDir = posix.join(nodeDir, "lib", "node_modules", ...PKG);
41
+ return {
42
+ root, nodeDir, node: p, npm: posix.join(bin, "npm"), npmCli: posix.join(nodeDir, "lib", "node_modules", "npm", "bin", "npm-cli.js"), binDir,
43
+ launcher: posix.join(binDir, "pantheon"), pkgDir,
44
+ entry: posix.join(pkgDir, "bin", "pantheon.js"), cli: posix.join(pkgDir, "dist", "cli.js"),
45
+ };
46
+ }
47
+ export function childNodeFlags(execArgv = process.execArgv, layout = managedLayout()) {
48
+ return execArgv.includes("--use-system-ca") || layout ? ["--use-system-ca"] : [];
49
+ }
50
+ export function managedLauncher(layout = managedLayout(), exists = existsSync) {
51
+ return layout && exists(layout.launcher) ? layout.launcher : null;
52
+ }
53
+ export function runtimeVerdict(f) {
54
+ const shadowFix = (who) => `${who} comes before the managed install on PATH, so a terminal runs that one. Remove it (\`npm uninstall -g @join-pantheon/cli\` with the npm that installed it), or put ${f.layout?.binDir ?? (f.expected ? f.expected.replace(/[\\/][^\\/]+$/, "") : "the managed bin folder")} first on PATH — the installer's line in your shell profile does that in every new terminal`;
55
+ const other = f.firstOnPath.find((p) => !!p && !f.ours(p)) ?? null;
56
+ if (f.layout) {
57
+ const where = `Pantheon's own Node v${f.nodeVersion.replace(/^v/, "")} at ${f.layout.nodeDir}`;
58
+ if (other)
59
+ return { level: "warn", detail: `${where} — but an older pantheon, ${other}, comes first on PATH`, fix: shadowFix(other), share: "managed runtime, shadowed on PATH" };
60
+ if (!f.firstOnPath.some((p) => p && f.ours(p))) {
61
+ return { level: "warn", detail: `${where} — but ${f.layout.binDir} is not on PATH here yet`,
62
+ fix: "open a new terminal (the installer added it to your shell profile), or run the installer again", share: "managed runtime, not on PATH yet" };
63
+ }
64
+ return { level: "pass", detail: `${where} (the managed install; ${f.layout.launcher} comes first on PATH)`, share: `managed runtime, Node v${f.nodeVersion.replace(/^v/, "")}` };
65
+ }
66
+ if (!f.expected || !f.expectedExists)
67
+ return null;
68
+ return {
69
+ level: "warn",
70
+ detail: `the managed install is at ${f.expected}, but this pantheon runs on another Node (v${f.nodeVersion.replace(/^v/, "")})${other ? ` — ${other} comes first on PATH` : ""}`,
71
+ fix: shadowFix(other ?? "another pantheon"),
72
+ share: "managed install present, another pantheon ran",
73
+ };
74
+ }
75
+ export function expectedLauncher(home, os = platform(), env = process.env) {
76
+ if (os === "win32") {
77
+ const local = env.LOCALAPPDATA || (home ? win32.join(home, "AppData", "Local") : "");
78
+ return local ? win32.join(local, "Pantheon", "bin", "pantheon.cmd") : null;
79
+ }
80
+ return home ? posix.join(home, ".pantheon", "bin", "pantheon") : null;
81
+ }
82
+ export function runtimeNodeVersion(layout = managedLayout(), run = (node) => execFileSync(node, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 })) {
83
+ if (layout) {
84
+ try {
85
+ const v = run(layout.node).trim().replace(/^v/, "");
86
+ if (/^\d+\.\d+\.\d+$/.test(v))
87
+ return v;
88
+ }
89
+ catch { }
90
+ }
91
+ return process.versions.node;
92
+ }
93
+ const cmp3 = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
94
+ export function nodeSatisfies(version, range) {
95
+ const vm = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
96
+ if (!vm)
97
+ return null;
98
+ const v = [Number(vm[1]), Number(vm[2]), Number(vm[3])];
99
+ const bounds = (c) => {
100
+ const m = /^(>=|<=|>|<|=|\^|~)?v?(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?$/.exec(c);
101
+ if (!m)
102
+ return null;
103
+ const op = m[1] ?? "";
104
+ const parts = [m[2], m[3], m[4]].map((p) => (p === undefined || /^[xX*]$/.test(p) ? null : Number(p)));
105
+ const firstOpen = parts.indexOf(null);
106
+ const given = firstOpen < 0 ? 3 : firstOpen;
107
+ if (parts.slice(given).some((p) => p !== null))
108
+ return null;
109
+ const lo = [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
110
+ const next = (n) => n === 0 ? null : n === 1 ? [lo[0] + 1, 0, 0] : n === 2 ? [lo[0], lo[1] + 1, 0] : [lo[0], lo[1], lo[2] + 1];
111
+ if (given === 0)
112
+ return op === ">" || op === "<" ? [null, [0, 0, 0]] : [null, null];
113
+ switch (op) {
114
+ case "":
115
+ case "=": return [lo, next(given)];
116
+ case ">=": return [lo, null];
117
+ case ">": return [next(given), null];
118
+ case "<": return [null, lo];
119
+ case "<=": return [null, next(given)];
120
+ case "~": return [lo, next(given >= 2 ? 2 : given)];
121
+ case "^": return [lo, next(lo[0] !== 0 || given === 1 ? 1 : lo[1] !== 0 || given === 2 ? 2 : 3)];
122
+ default: return null;
123
+ }
124
+ };
125
+ let readAll = true;
126
+ for (const alt of range.split("||").map((s) => s.trim())) {
127
+ if (alt === "" || alt === "*" || /^[xX]$/.test(alt))
128
+ return true;
129
+ const hy = /^(\S+)\s+-\s+(\S+)$/.exec(alt);
130
+ const comps = hy ? [`>=${hy[1]}`, `<=${hy[2]}`] : alt.replace(/(>=|<=|>|<|=|\^|~)\s+/g, "$1").split(/\s+/);
131
+ let ok = true;
132
+ for (const c of comps) {
133
+ const b = bounds(c);
134
+ if (!b) {
135
+ readAll = false;
136
+ ok = false;
137
+ break;
138
+ }
139
+ if ((b[0] && cmp3(v, b[0]) < 0) || (b[1] && cmp3(v, b[1]) >= 0))
140
+ ok = false;
141
+ }
142
+ if (ok)
143
+ return true;
144
+ }
145
+ return readAll ? false : null;
146
+ }
147
+ const HOSTED_INSTALLER_BASE = "https://relay.joinpantheon.network";
148
+ export function installerBase(relay) {
149
+ if (!relay)
150
+ return HOSTED_INSTALLER_BASE;
151
+ try {
152
+ const u = new URL(relay.trim().replace(/^ws(s?):/i, "http$1:"));
153
+ if (u.protocol === "http:")
154
+ return null;
155
+ if (u.protocol !== "https:")
156
+ return HOSTED_INSTALLER_BASE;
157
+ return u.origin;
158
+ }
159
+ catch {
160
+ return HOSTED_INSTALLER_BASE;
161
+ }
162
+ }
163
+ const TRAVELS = /^[A-Za-z0-9._~:/?#@!&()*+,;=%[\]-]+$/;
164
+ export function quotableInvite(invite) {
165
+ const hash = invite.indexOf("#");
166
+ const head = hash < 0 ? invite : invite.slice(0, hash);
167
+ const out = head.replace(/'/g, "%27") + (hash < 0 ? "" : invite.slice(hash));
168
+ return TRAVELS.test(out) ? out : null;
169
+ }
170
+ export function installerLine(relay, shell, args = []) {
171
+ const base = installerBase(relay);
172
+ if (!base || args.some((a) => !TRAVELS.test(a)))
173
+ return null;
174
+ if (shell === "sh") {
175
+ const q = (a) => (/^[A-Za-z0-9._/:=@%+-]+$/.test(a) ? a : `'${a}'`);
176
+ return `curl -fsSL ${base}/install.sh | sh${args.length ? ` -s -- ${args.map(q).join(" ")}` : ""}`;
177
+ }
178
+ if (shell === "ps" && !args.length)
179
+ return `irm ${base}/install.ps1 | iex`;
180
+ const set = args.length ? `Set-Item Env:PANTHEON_ARGS '${args.join(" ")}'; ` : "";
181
+ return `powershell -NoProfile -ExecutionPolicy Bypass -Command "${set}irm ${base}/install.ps1 | iex"`;
182
+ }
183
+ export function installerLineHere(relay, os = platform()) {
184
+ return installerLine(relay, os === "win32" ? "ps" : "sh") ?? installerLine(HOSTED_INSTALLER_BASE, os === "win32" ? "ps" : "sh");
185
+ }
186
+ export function nodeTooOldSentence(target, range, node, relay, os = platform()) {
187
+ return `${target ? `Pantheon ${target}` : "Pantheon's newest release"} needs Node ${range}, and Pantheon's own Node here is v${node.replace(/^v/, "")}, so nothing was installed — run the installer again, which brings the Node it needs and keeps your teams and settings: ${installerLineHere(relay, os)}`;
188
+ }
189
+ export function runtimeNodeFit(layout, node, newest, relay, os = platform()) {
190
+ if (!layout || !newest)
191
+ return null;
192
+ const fits = nodeSatisfies(node, newest.range);
193
+ if (fits === null)
194
+ return null;
195
+ const v = node.replace(/^v/, "");
196
+ if (fits)
197
+ return { level: "pass", detail: `Node v${v} runs the newest Pantheon (${newest.version} needs Node ${newest.range})`, share: "runtime Node fits the newest release" };
198
+ return {
199
+ level: "warn",
200
+ detail: `the newest Pantheon (${newest.version}) needs Node ${newest.range}; Pantheon's own Node here is v${v}, so \`pantheon upgrade\` will not install it`,
201
+ fix: `run the installer again — it brings the Node the release needs and keeps your teams and settings: ${installerLineHere(relay, os)}`,
202
+ share: "runtime Node too old for the newest release",
203
+ };
204
+ }
205
+ export function joinInstructions(invite, relay, installers = true) {
206
+ const link = quotableInvite(invite);
207
+ const steps = [];
208
+ const sh = link && installers ? installerLine(relay, "sh", ["join", link]) : null;
209
+ const win = link && installers ? installerLine(relay, "windows", ["join", link]) : null;
210
+ if (sh)
211
+ steps.push({ label: "Mac or Linux (Terminal):", lines: [sh] });
212
+ if (win)
213
+ steps.push({ label: "Windows (Command Prompt or PowerShell):", lines: [win] });
214
+ steps.push({
215
+ label: steps.length ? "Already have Node.js 22.13 or newer? This works too:" : "With Node.js 22.13 or newer (nodejs.org):",
216
+ lines: ["npm i -g @join-pantheon/cli", link ? `pantheon join '${link}'` : `pantheon join ${invite}`],
217
+ });
218
+ return {
219
+ intro: "You're invited to my Pantheon team. Paste the line for your computer into a terminal:",
220
+ steps,
221
+ outro: sh || win
222
+ ? "It installs Pantheon (with its own Node, no admin rights), asks for your email, sends you a 6-digit code, and sets up the coding agents on your computer."
223
+ : "`pantheon join` asks for your email, sends you a 6-digit code, and sets up the coding agents on your computer.",
224
+ };
225
+ }
226
+ export async function relayServesInstaller(relay, timeoutMs = 4000) {
227
+ const base = installerBase(relay);
228
+ if (!base)
229
+ return false;
230
+ try {
231
+ const r = await fetch(`${base}/install.sh`, { method: "HEAD", signal: AbortSignal.timeout(timeoutMs) });
232
+ return r.status === 200 && r.headers.get("x-pantheon-installs") !== null;
233
+ }
234
+ catch {
235
+ return false;
236
+ }
237
+ }
238
+ //# sourceMappingURL=managed.js.map
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { dirname, join, resolve } from "node:path";
5
+ import { childNodeFlags } from "./managed.js";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { CONFIG_DIR } from "./config.js";
7
8
  import { VERSION } from "./version.js";
@@ -98,7 +99,7 @@ export async function superviseMcp() {
98
99
  return null;
99
100
  let proc;
100
101
  try {
101
- proc = spawn(process.execPath, [entry, "mcp", "--child"], {
102
+ proc = spawn(process.execPath, [...childNodeFlags(), entry, "mcp", "--child"], {
102
103
  stdio: ["pipe", "pipe", "pipe"],
103
104
  env: { ...process.env, PANTHEON_MCP_SUPERVISED: String(process.pid) },
104
105
  });
package/dist/measure.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { resolve, dirname, join } from "node:path";
4
+ import { childNodeFlags } from "./managed.js";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { claudeGrantedEffective, cursorGrantedEffective, claudeAllow, cursorAllow, isServerWide } from "./grants.js";
6
7
  import { codexApprovalState, copilotMcpConfigFile, kiroMcpConfigFile } from "./install.js";
@@ -29,7 +30,7 @@ export function measureMcp(opts = {}) {
29
30
  done(m);
30
31
  };
31
32
  const t0 = Date.now();
32
- const proc = spawn(process.execPath, [entry, "mcp", "--child"], {
33
+ const proc = spawn(process.execPath, [...childNodeFlags(), entry, "mcp", "--child"], {
33
34
  cwd: opts.cwd, stdio: ["pipe", "pipe", "pipe"], env: opts.env ?? process.env,
34
35
  });
35
36
  const timer = setTimeout(() => finish({ error: `no answer to initialize within ${timeoutMs}ms` }), timeoutMs);
package/dist/notifier.js CHANGED
@@ -1,6 +1,6 @@
1
- import { execFileSync, spawn } from "node:child_process";
2
- import { helperStale, recordHelperBuild } from "./helpers-build.js";
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1
+ import { spawn } from "node:child_process";
2
+ import { helperStale, installHelper } from "./helpers-build.js";
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -58,7 +58,8 @@ function oneLine(value, max = 220) {
58
58
  const HERE = dirname(fileURLToPath(import.meta.url));
59
59
  export const hudBinaryPath = (home = CONFIG_DIR) => join(home, "bin", "pantheon-hud");
60
60
  export const hudSourcePath = () => resolve(HERE, "..", "helper", "macos", "main.swift");
61
- export function ensureNativeNotifier(home = CONFIG_DIR) {
61
+ export const HUD_PREBUILT = "pantheon-hud";
62
+ export function ensureNativeNotifier(home = CONFIG_DIR, deps = {}) {
62
63
  if (platform() !== "darwin")
63
64
  throw new Error("the actionable Pantheon notifier is currently macOS-only");
64
65
  const source = hudSourcePath();
@@ -66,21 +67,10 @@ export function ensureNativeNotifier(home = CONFIG_DIR) {
66
67
  throw new Error(`Pantheon notification helper source is missing: ${source}`);
67
68
  const binary = hudBinaryPath(home);
68
69
  mkdirSync(dirname(binary), { recursive: true });
69
- requireDeveloperTools("notifications", "notifier");
70
- execFileSync("/usr/bin/swiftc", ["-O", source, "-o", binary], { stdio: "pipe" });
71
- chmodSync(binary, 0o755);
72
- recordHelperBuild(binary, source);
73
- return binary;
70
+ return installHelper({ name: HUD_PREBUILT, source, binary, what: "notifications", service: "notifier" }, deps).binary;
74
71
  }
75
72
  export function notifierStale(home = CONFIG_DIR) { return helperStale(hudBinaryPath(home), hudSourcePath()); }
76
- export function requireDeveloperTools(what, service) {
77
- try {
78
- execFileSync("/usr/bin/xcode-select", ["-p"], { stdio: "pipe" });
79
- }
80
- catch {
81
- throw new Error(`building ${what} needs the Xcode Command Line Tools — run \`xcode-select --install\`, then \`pantheon service install ${service}\``);
82
- }
83
- }
73
+ export { requireDeveloperTools } from "./helpers-build.js";
84
74
  export function installedTools() {
85
75
  const seen = new Set();
86
76
  const out = [];
package/dist/notify.js CHANGED
@@ -7,6 +7,7 @@ import { homedir, platform } from "node:os";
7
7
  import { dirname, join, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { whichPantheon } from "./service.js";
10
+ import { managedLayout } from "./managed.js";
10
11
  import { detectForHooks, agentById } from "./agents.js";
11
12
  import { codexHookState, cursorHookState, copilotHookState, kiroHookState, installCodexHooks, installCursorHooks, installCopilotHooks, installKiroHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, removeKiroHooks, } from "./hooks-install.js";
12
13
  export const HOOK_MARKER = "pantheon hook";
@@ -14,7 +15,12 @@ export const HOOK_RE = /(?:pantheon(?:\.cmd|\.exe|\.bat)?["']?\s+hook\s|cli\.js[
14
15
  export const isOurHookCommand = (cmd) => !!cmd && (cmd.includes(HOOK_MARKER) || HOOK_RE.test(cmd));
15
16
  export const hookBin = (bin) => (/\s/.test(bin) ? `"${bin}"` : bin);
16
17
  const HERE = dirname(fileURLToPath(import.meta.url));
17
- export function hookLauncher(os = platform(), found = whichPantheon(), entry = resolve(HERE, "cli.js"), entryExists = existsSync) {
18
+ export function hookLauncher(os = platform(), found = whichPantheon(), entry = resolve(HERE, "cli.js"), entryExists = existsSync, managed = managedLayout(process.execPath, os)) {
19
+ if (os === "win32" && managed && entryExists(managed.cli)) {
20
+ const node = managed.node.replace(/\\/g, "/");
21
+ const cmdWord = /^[A-Za-z0-9_./:~+-]+$/.test(node) ? node : `"${node}"`;
22
+ return `${cmdWord} --use-system-ca ${windowsHookArg(managed.cli.replace(/\\/g, "/"))}`;
23
+ }
18
24
  if (os !== "win32") {
19
25
  if (!found)
20
26
  return null;