@join-pantheon/cli 0.26.1 → 0.26.3

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/doctor.js CHANGED
@@ -5,7 +5,8 @@ import { grantsSeen, measureMcp, toolsReport, HOST_STARTUP_ALLOWANCE_MS, STARTUP
5
5
  import { FEEDBACK_ADDRESS } from "./feedback.js";
6
6
  import { loadProfile, profileInitialized } from "./profile.js";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
- import { join } from "node:path";
8
+ import { homedir, platform } from "node:os";
9
+ import { join, resolve } from "node:path";
9
10
  import { PantheonClient } from "./client.js";
10
11
  import { load, resolveFor, loadAll, CONFIG_PATH } from "./config.js";
11
12
  import { fingerprint, signChallenge, verifyChallenge } from "./identity.js";
@@ -16,17 +17,28 @@ import { workerProc, menubarProc, notifierProc } from "./machine.js";
16
17
  import { chooseAgent, describeChoice } from "./agents.js";
17
18
  import { onPathNamed } from "./which.js";
18
19
  import { VERSION } from "./version.js";
19
- import { serviceInstalled } from "./service.js";
20
- import { hostEnv } from "./host-env.js";
20
+ import { serviceInstalled, servicesUnavailable } from "./service.js";
21
+ import { hostEnv, platformName } from "./host-env.js";
21
22
  const HOSTED_RELAY = "relay.joinpantheon.network";
22
- const sh = (cmd, cwd) => {
23
+ const git = (args, cwd) => {
23
24
  try {
24
- return execFileSync("sh", ["-lc", cmd], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
25
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
25
26
  }
26
27
  catch {
27
28
  return null;
28
29
  }
29
30
  };
31
+ function claudeLocalOverride(projectDir) {
32
+ try {
33
+ const doc = JSON.parse(readFileSync(join(homedir(), ".claude.json"), "utf8"));
34
+ const norm = (x) => resolve(x).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
35
+ const want = norm(projectDir);
36
+ return Object.entries(doc.projects ?? {}).some(([k, v]) => norm(k) === want && !!v?.mcpServers?.["pantheon"]);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
30
42
  function isPrivate(host) {
31
43
  const h = host.replace(/:\d+$/, "");
32
44
  if (/^(localhost|127\.0\.0\.1|\[::1\]|::1)$/.test(h))
@@ -56,10 +68,10 @@ export async function doctor(projectDir) {
56
68
  : add("fail", "node", `v${process.versions.node} is too old`, "pantheon needs Node 22 or newer (the relay uses node:sqlite) — install it and re-run", `v${process.versions.node}, too old`);
57
69
  const host = hostEnv();
58
70
  if (host.ephemeral) {
59
- add("warn", "machine", `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"} — ${host.evidence.join("; ")}`, `this is not the computer you work on, which is why it reports ${host.platform} to someone who may be sitting at a Mac. The team and your account are fine here, but the menu bar, desktop alerts and the worker belong on your own machine — run the setup from a terminal there.`, `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"}`);
71
+ add("warn", "machine", `${platformName(host.platform)} in ${host.kind === "container" ? "a container" : "a cloud workspace"} — ${host.evidence.join("; ")}`, `this is not the computer you work on, which is why it reports ${platformName(host.platform)} to someone who may be sitting at a Mac. The team and your account are fine here, but the menu bar, desktop alerts and the worker belong on your own machine — run the setup from a terminal there.`, `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"}`);
60
72
  }
61
73
  else {
62
- add("pass", "machine", `${host.platform}${host.desktop ? " — menu bar and desktop alerts can run here" : " — no menu bar or desktop alerts (macOS only); use phone or chat delivery"}`, undefined, `${host.platform}${host.desktop ? ", desktop" : ""}`);
74
+ add("pass", "machine", `${platformName(host.platform)}${host.desktop ? " — menu bar and desktop alerts can run here" : " — no menu bar or desktop alerts (macOS only); use phone or chat delivery"}`, undefined, `${host.platform}${host.desktop ? ", desktop" : ""}`);
63
75
  }
64
76
  let cfg = null;
65
77
  try {
@@ -114,7 +126,15 @@ export async function doctor(projectDir) {
114
126
  add("fail", "mcp entry", ".mcp.json has no pantheon server", "run `pantheon install`", ".mcp.json has no pantheon server");
115
127
  }
116
128
  else if (entry.command === "pantheon") {
117
- add("pass", "mcp entry", "portable (`pantheon mcp`) — safe to commit, works on your teammate's machine", undefined, "portable");
129
+ if (platform() !== "win32") {
130
+ add("pass", "mcp entry", "portable (`pantheon mcp`) — safe to commit, works on your teammate's machine", undefined, "portable");
131
+ }
132
+ else if (claudeLocalOverride(projectDir)) {
133
+ add("pass", "mcp entry", "portable (`pantheon mcp`) for your teammates; Claude Code on this Windows machine starts it through a local override for this folder", undefined, "portable, with a local override");
134
+ }
135
+ else {
136
+ add("warn", "mcp entry", "portable (`pantheon mcp`) — right for your teammates, but a Windows MCP host cannot start `pantheon` itself (it is pantheon.cmd, which needs a shell), so Claude Code shows pantheon as failed in this folder", "run `pantheon install` in this folder — it adds a local override for Claude Code here and leaves the committed file alone", "portable, no local override on Windows");
137
+ }
118
138
  }
119
139
  else {
120
140
  add("fail", "mcp entry", `hard-codes this machine: ${entry.command}`, "make sure `pantheon` is on your PATH, then run `pantheon install` again — it writes the portable `pantheon mcp` entry. Committed as-is it silently fails on the other machine: the MCP server never starts and no error is shown.", "hard-codes a path on this machine");
@@ -123,16 +143,16 @@ export async function doctor(projectDir) {
123
143
  onPath()
124
144
  ? add("pass", "PATH", "`pantheon` resolves by name", undefined, "pantheon resolves by name")
125
145
  : add("warn", "PATH", "`pantheon` is not on PATH", "install it globally (`npm install -g @join-pantheon/cli`) and make sure your npm global bin dir (`npm bin -g`) is on PATH", "pantheon is not on PATH");
126
- const inRepo = sh("git rev-parse --is-inside-work-tree", projectDir) === "true";
146
+ const inRepo = git(["rev-parse", "--is-inside-work-tree"], projectDir) === "true";
127
147
  if (!inRepo) {
128
148
  add("warn", "git", `${projectDir} is not a git repo`, "DELIVERED requires a pushed branch + commit, so the work needs to live in a repo you both clone", "not a git repo");
129
149
  }
130
150
  else {
131
- const remote = sh("git remote get-url origin", projectDir);
151
+ const remote = git(["remote", "get-url", "origin"], projectDir);
132
152
  remote
133
153
  ? add("pass", "git", `repo with origin ${remote}`, undefined, "repo with an origin remote")
134
154
  : add("fail", "git", "repo has no `origin` remote", "DELIVERED names a commit your teammate fetches. Without a shared remote they cannot verify anything you deliver.", "repo without an origin remote");
135
- const branch = sh("git rev-parse --abbrev-ref HEAD", projectDir);
155
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], projectDir);
136
156
  if (branch)
137
157
  add("pass", "git branch", branch);
138
158
  }
@@ -255,7 +275,10 @@ export async function doctor(projectDir) {
255
275
  add("pass", "wake rights", `${who} — worker is running`, undefined, `${wakers.length} may wake; worker running`);
256
276
  }
257
277
  else {
258
- add("warn", "wake rights", `${who}, but no worker is running here, so nothing can be woken`, "start one with `pantheon service install worker --project <a git repo>`; until then a peer's act waits for you to type", `${wakers.length} may wake; worker not running`);
278
+ const noSvc = servicesUnavailable(host.platform);
279
+ add("warn", "wake rights", `${who}, but no worker is running here, so nothing can be woken`, noSvc
280
+ ? "Pantheon does not run in the background on Windows yet, so a peer's act waits until your agent is open"
281
+ : "start one with `pantheon service install worker --project <a git repo>`; until then a peer's act waits for you to type", `${wakers.length} may wake; worker not running`);
259
282
  }
260
283
  }
261
284
  if (host.desktop) {
package/dist/host-env.js CHANGED
@@ -40,9 +40,10 @@ export function hostEnv(env = process.env, plat = process.platform, exists = exi
40
40
  desktop: false,
41
41
  };
42
42
  }
43
+ export const platformName = (p) => p === "win32" ? "Windows" : p === "darwin" ? "macOS" : p === "linux" ? "Linux" : p;
43
44
  export function ephemeralWarning(host) {
44
45
  return `this looks like ${host.kind === "container" ? "a container" : "a cloud workspace"}, not the computer you work on `
45
- + `(${host.evidence.join("; ")}). That is why this machine reports ${host.platform}: your editor's agent is running `
46
+ + `(${host.evidence.join("; ")}). That is why this machine reports ${platformName(host.platform)}: your editor's agent is running `
46
47
  + `somewhere else, so the menu bar and desktop alerts cannot be installed, and anything installed here disappears with `
47
48
  + `the session. Your account and team are fine — run the same setup from a terminal on your own computer to finish it.`;
48
49
  }
@@ -50,8 +51,8 @@ export function desktopUnavailable(host, what) {
50
51
  if (host.desktop)
51
52
  return null;
52
53
  if (host.ephemeral) {
53
- return `not available — ${what} ${what === "desktop alerts" ? "need" : "needs"} your own computer, and this is ${host.kind === "container" ? "a container" : "a cloud workspace"} reporting ${host.platform}`;
54
+ return `not available — ${what} ${what === "desktop alerts" ? "need" : "needs"} your own computer, and this is ${host.kind === "container" ? "a container" : "a cloud workspace"} reporting ${platformName(host.platform)}`;
54
55
  }
55
- return `not available — ${what} ${host.platform === "darwin" ? "could not be built here" : `${what === "desktop alerts" ? "are" : "is"} macOS-only, and this machine is ${host.platform}`}`;
56
+ return `not available — ${what} ${host.platform === "darwin" ? "could not be built here" : `${what === "desktop alerts" ? "are" : "is"} macOS-only, and this machine is ${platformName(host.platform)}`}`;
56
57
  }
57
58
  //# sourceMappingURL=host-env.js.map
package/dist/install.js CHANGED
@@ -8,6 +8,7 @@ import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
8
8
  import { load } from "./config.js";
9
9
  import { loadProfile } from "./profile.js";
10
10
  import { agentSpawn, onPathNamed, which } from "./which.js";
11
+ import { FEEDBACK_ADDRESS } from "./feedback.js";
11
12
  const HERE = dirname(fileURLToPath(import.meta.url));
12
13
  export function onPath() {
13
14
  return onPathNamed("pantheon");
@@ -114,10 +115,11 @@ function upsertBrief(file, cfg) {
114
115
  writeFileSync(file, cur.trimEnd() + "\n\n" + block + "\n");
115
116
  return "updated";
116
117
  }
117
- export function install(projectDir, cfg, force) {
118
+ export function install(projectDir, cfg, force, os = platform(), claudeLocal = claudeLocalScope) {
118
119
  const out = [];
119
- const l = launcher(force);
120
- const entry = { command: l.command, args: l.args };
120
+ const l = launcher(force, os);
121
+ const win = os === "win32";
122
+ const entry = win ? { command: "pantheon", args: ["mcp"] } : { command: l.command, args: l.args };
121
123
  out.push(` .mcp.json ${mergeMcpJson(join(projectDir, ".mcp.json"), entry)} (claude code, conductor, copilot cli)`);
122
124
  out.push(` .cursor/mcp.json ${mergeMcpJson(join(projectDir, ".cursor", "mcp.json"), entry)} (cursor)`);
123
125
  const vscodeDir = join(projectDir, ".vscode");
@@ -137,7 +139,15 @@ export function install(projectDir, cfg, force) {
137
139
  for (const line of codexTomlBlock(userEntry()).split("\n"))
138
140
  out.push(` ${line}`);
139
141
  out.push("");
140
- if (l.portable) {
142
+ if (win) {
143
+ const local = claudeLocal(userEntry(os), projectDir);
144
+ out.push(" \x1b[36mPortable entry\x1b[0m — `pantheon mcp` is what your teammates' machines start, so this config is safe to commit.");
145
+ out.push(" Windows cannot start it from an MCP host (pantheon is pantheon.cmd here, and hosts use no shell), so this");
146
+ out.push(" machine gets its own way in, outside the repo:");
147
+ out.push(` claude code ${local.status}`);
148
+ out.push(` cursor your user-level entry from \`pantheon join\`; if Pantheon does not appear in Cursor for this folder, say so: ${FEEDBACK_ADDRESS}`);
149
+ }
150
+ else if (l.portable) {
141
151
  out.push(" \x1b[36mPortable entry\x1b[0m — `pantheon` resolves on PATH, so this config is safe to commit");
142
152
  out.push(" and will work in every Conductor workspace and on your teammate's machine.");
143
153
  }
@@ -198,6 +208,25 @@ function claudeUserScope(entry) {
198
208
  return { tool, where: "user scope", status: `failed: ${e.message.split("\n")[0]}` };
199
209
  }
200
210
  }
211
+ function claudeLocalScope(entry, projectDir) {
212
+ const tool = "claude code";
213
+ const claudeBin = which("claude");
214
+ if (!claudeBin)
215
+ return { tool, where: "-", status: "skipped (claude not on PATH) — once Claude Code is installed, run `pantheon install` here again" };
216
+ try {
217
+ const remove = agentSpawn(claudeBin, ["mcp", "remove", "pantheon", "-s", "local"]);
218
+ const add = agentSpawn(claudeBin, ["mcp", "add", "pantheon", "-s", "local", "--", entry.command, ...entry.args]);
219
+ try {
220
+ execFileSync(remove.command, remove.args, { stdio: "ignore", cwd: projectDir, ...remove.options });
221
+ }
222
+ catch { }
223
+ execFileSync(add.command, add.args, { stdio: "ignore", cwd: projectDir, ...add.options });
224
+ return { tool, where: "local scope, this folder", status: "local override written — Claude Code starts pantheon here with this machine's node, and nothing in the repo changed" };
225
+ }
226
+ catch (e) {
227
+ return { tool, where: "local scope", status: `failed: ${e.message.split("\n")[0]}` };
228
+ }
229
+ }
201
230
  function cursorUserScope(entry) {
202
231
  const file = join(homedir(), ".cursor", "mcp.json");
203
232
  try {
package/dist/notify.js CHANGED
@@ -1,13 +1,22 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
2
+ import { homedir, platform } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { whichPantheon } from "./service.js";
5
6
  import { detectForHooks, agentById } from "./agents.js";
6
7
  import { codexHookState, cursorHookState, copilotHookState, installCodexHooks, installCursorHooks, installCopilotHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, } from "./hooks-install.js";
7
8
  export const HOOK_MARKER = "pantheon hook";
8
- export const HOOK_RE = /pantheon(?:\.cmd|\.exe|\.bat)?"?\s+hook\s/i;
9
+ export const HOOK_RE = /(?:pantheon(?:\.cmd|\.exe|\.bat)?"?\s+hook\s|cli\.js"?\s+hook\s+(?:stop|prompt|end|codex-stop|copilot-stop|cursor-stop)\b)/i;
9
10
  export const isOurHookCommand = (cmd) => !!cmd && (cmd.includes(HOOK_MARKER) || HOOK_RE.test(cmd));
10
11
  export const hookBin = (bin) => (/\s/.test(bin) ? `"${bin}"` : bin);
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+ export function hookLauncher(os = platform(), found = whichPantheon(), entry = resolve(HERE, "cli.js"), entryExists = existsSync) {
14
+ if (os !== "win32")
15
+ return found ? hookBin(found) : null;
16
+ if (entryExists(entry))
17
+ return `node "${entry.replace(/\\/g, "/")}"`;
18
+ return found ? hookBin(found) : null;
19
+ }
11
20
  export function settingsPath(scope, projectDir = process.cwd()) {
12
21
  return scope === "user"
13
22
  ? join(homedir(), ".claude", "settings.json")
@@ -140,10 +149,9 @@ export function repairHook(home) {
140
149
  function installClaudeHook(scope, projectDir = process.cwd()) {
141
150
  const file = settingsPath(scope, projectDir);
142
151
  const doc = read(file);
143
- const found = whichPantheon();
144
- if (!found)
152
+ const bin = hookLauncher();
153
+ if (!bin)
145
154
  throw new Error("pantheon binary not found on PATH");
146
- const bin = hookBin(found);
147
155
  const events = {
148
156
  Stop: `${bin} hook stop`,
149
157
  UserPromptSubmit: `${bin} hook prompt`,
@@ -168,7 +176,7 @@ function installClaudeHook(scope, projectDir = process.cwd()) {
168
176
  ];
169
177
  }
170
178
  export function installHooks(scope, projectDir = process.cwd(), only) {
171
- const bin = whichPantheon();
179
+ const bin = hookLauncher();
172
180
  if (!bin)
173
181
  throw new Error("pantheon binary not found on PATH");
174
182
  const available = detectForHooks(projectDir);
@@ -9,7 +9,7 @@ import { newKeypair } from "./identity.js";
9
9
  import { installForUser } from "./install.js";
10
10
  import { writeIntent, installHooks } from "./notify.js";
11
11
  import { detectAvailable, agentById } from "./agents.js";
12
- import { installService } from "./service.js";
12
+ import { installService, servicesUnavailable } from "./service.js";
13
13
  import { ensureNativeNotifier } from "./notifier.js";
14
14
  import { ensureTrayBinary, writeTrayCommands } from "./tray.js";
15
15
  import { desktopUnavailable, ephemeralWarning, hostEnv } from "./host-env.js";
@@ -30,6 +30,7 @@ export function wireUp(opts, deps = {}) {
30
30
  const ensureNotifier = deps.ensureNotifier ?? ensureNativeNotifier;
31
31
  const ensureTray = deps.ensureTray ?? ensureTrayBinary;
32
32
  const host = deps.host ?? hostEnv();
33
+ const noServices = servicesUnavailable(host.platform);
33
34
  const tools = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
34
35
  const granted = [];
35
36
  const warnings = [];
@@ -92,6 +93,9 @@ export function wireUp(opts, deps = {}) {
92
93
  if (how === "off") {
93
94
  tools.push({ tool: "worker", status: "off (--no-worker) — this machine cannot answer the team while you are away" });
94
95
  }
96
+ else if (opts.unattended && noServices) {
97
+ tools.push({ tool: "worker", status: noServices });
98
+ }
95
99
  else if (opts.unattended) {
96
100
  const project = opts.unattended;
97
101
  const isRepo = sh(`git -C ${JSON.stringify(project)} rev-parse --is-inside-work-tree 2>/dev/null`) === "true";
package/dist/service.js CHANGED
@@ -13,6 +13,12 @@ const SERVICE_BASE_PATH = [
13
13
  export function servicePath(envPath = process.env.PATH ?? "") {
14
14
  return [...new Set([...envPath.split(":"), ...SERVICE_BASE_PATH].filter(Boolean))].join(":");
15
15
  }
16
+ export function servicesUnavailable(plat = platform()) {
17
+ if (plat !== "win32")
18
+ return null;
19
+ return "not available — Pantheon does not run in the background on Windows yet. "
20
+ + "Your agent still answers peers whenever it is open; nothing runs here while you are away.";
21
+ }
16
22
  export function plistPath(kind = "worker") {
17
23
  return join(homedir(), "Library", "LaunchAgents", `${LABEL(kind)}.plist`);
18
24
  }
@@ -87,6 +93,9 @@ WantedBy=default.target
87
93
  `;
88
94
  }
89
95
  export function installService(kind, cwd, args) {
96
+ const noServices = servicesUnavailable();
97
+ if (noServices)
98
+ throw new Error(noServices.replace(/^not available — /, ""));
90
99
  const out = [];
91
100
  const abs = resolve(cwd);
92
101
  const logDir = join(abs, ".pantheon");
@@ -134,6 +143,14 @@ export function installService(kind, cwd, args) {
134
143
  }
135
144
  export function uninstallService(kind) {
136
145
  const out = [];
146
+ if (platform() === "win32") {
147
+ const stray = unitPath(kind);
148
+ if (existsSync(stray)) {
149
+ unlinkSync(stray);
150
+ return [` removed ${stray}`];
151
+ }
152
+ return [` no ${kind} service installed`];
153
+ }
137
154
  if (platform() === "darwin") {
138
155
  const p = plistPath(kind);
139
156
  if (!existsSync(p))
@@ -188,6 +205,8 @@ export function restartService(kind, exec = (c, a) => execFileSync(c, a, { stdio
188
205
  }
189
206
  export function serviceInstalled(kind) {
190
207
  try {
208
+ if (platform() === "win32")
209
+ return false;
191
210
  return existsSync(platform() === "darwin" ? plistPath(kind) : unitPath(kind));
192
211
  }
193
212
  catch {
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { CONFIG_DIR } from "./config.js";
5
5
  import { repoRoot, PKG_NAME } from "./upgrade.js";
6
6
  import { VERSION } from "./version.js";
7
+ import { npmSpawn } from "./which.js";
7
8
  export function semverGt(x, y) {
8
9
  const p = (s) => s.split(".").map((n) => Number(n) || 0);
9
10
  const [a, b] = [p(x), p(y)];
@@ -80,7 +81,8 @@ export function defaultDeps(home) {
80
81
  },
81
82
  npmLatest: () => {
82
83
  try {
83
- return execFileSync("npm", ["view", PKG_NAME, "version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim() || null;
84
+ const s = npmSpawn(["view", PKG_NAME, "version"]);
85
+ return execFileSync(s.command, s.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000, ...s.options }).trim() || null;
84
86
  }
85
87
  catch {
86
88
  return null;
package/dist/upgrade.js CHANGED
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { installForUser } from "./install.js";
6
+ import { npmSpawn } from "./which.js";
6
7
  import { relayProc, workerProc, notifierProc, menubarProc } from "./machine.js";
7
8
  export function repoRoot() {
8
9
  return join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -28,6 +29,10 @@ export function rewireVia(root, fallback, run = (file, args) => execFileSync(pro
28
29
  }
29
30
  export function defaultDeps() {
30
31
  const git = (root, ...args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
32
+ const npm = (args, opts) => {
33
+ const s = npmSpawn(args);
34
+ return execFileSync(s.command, s.args, { encoding: "utf8", ...opts, ...s.options });
35
+ };
31
36
  return {
32
37
  root: repoRoot(),
33
38
  isGit: (root) => existsSync(join(root, ".git")),
@@ -37,7 +42,7 @@ export function defaultDeps() {
37
42
  fetch: (root) => { git(root, "fetch", "--quiet"); },
38
43
  behind: (root) => Number(git(root, "rev-list", "--count", "HEAD..@{u}")) || 0,
39
44
  pull: (root) => git(root, "pull", "--ff-only"),
40
- build: (root) => execFileSync("npm", ["install", "--no-audit", "--no-fund"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }),
45
+ build: (root) => npm(["install", "--no-audit", "--no-fund"], { cwd: root, stdio: ["ignore", "pipe", "pipe"] }),
41
46
  version: pkgVersion,
42
47
  rewire: () => rewireVia(repoRoot(), () => installForUser().map((t) => ({ tool: t.tool, status: t.status }))),
43
48
  running: () => {
@@ -64,10 +69,10 @@ export function defaultDeps() {
64
69
  catch { }
65
70
  return up;
66
71
  },
67
- npmUpgrade: () => execFileSync("npm", ["install", "-g", `${PKG_NAME}@latest`, "--no-audit", "--no-fund"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }),
72
+ npmUpgrade: () => npm(["install", "-g", `${PKG_NAME}@latest`, "--no-audit", "--no-fund"], { stdio: ["ignore", "pipe", "pipe"] }),
68
73
  npmLatest: () => {
69
74
  try {
70
- return execFileSync("npm", ["view", `${PKG_NAME}`, "version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim() || null;
75
+ return npm(["view", `${PKG_NAME}`, "version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim() || null;
71
76
  }
72
77
  catch {
73
78
  return null;
package/dist/which.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { accessSync, constants, existsSync, statSync } from "node:fs";
3
- import { win32 } from "node:path";
3
+ import { posix, win32 } from "node:path";
4
4
  import { platform } from "node:os";
5
5
  export const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.PS1";
6
6
  const isWindows = (os = platform()) => os === "win32";
@@ -43,6 +43,23 @@ export function whichWindows(name, env = process.env, exists = runnable) {
43
43
  export function which(name, os = platform(), env = process.env) {
44
44
  if (isWindows(os))
45
45
  return whichWindows(name, env);
46
+ return loginShellWhich(name) ?? posixOnPath(name, env);
47
+ }
48
+ function posixOnPath(name, env) {
49
+ if (!name)
50
+ return null;
51
+ if (name.includes("/"))
52
+ return runnable(name) ? name : null;
53
+ for (const dir of (env.PATH ?? "").split(":")) {
54
+ if (!dir)
55
+ continue;
56
+ const full = posix.join(dir, name);
57
+ if (runnable(full))
58
+ return full;
59
+ }
60
+ return null;
61
+ }
62
+ function loginShellWhich(name) {
46
63
  try {
47
64
  const out = execFileSync("sh", ["-lc", `command -v ${name}`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
48
65
  if (!out)
@@ -59,6 +76,11 @@ export function spawnShape(binary, os = platform()) {
59
76
  return { command: binary, shell: false };
60
77
  return { command: binary, shell: /\.(cmd|bat)$/i.test(binary) };
61
78
  }
79
+ export function npmSpawn(args, os = platform(), resolveNpm = () => which("npm", os)) {
80
+ if (!isWindows(os))
81
+ return { command: "npm", args: [...args], options: {} };
82
+ return agentSpawn(resolveNpm() ?? "npm.cmd", args, os);
83
+ }
62
84
  const cmdQuote = (a) => (/[\s"&|<>^()]/.test(a) || a === "" ? `"${a.replace(/"/g, '\\"')}"` : a);
63
85
  export function agentSpawn(binary, args, os = platform()) {
64
86
  if (!isWindows(os) || !/\.(cmd|bat)$/i.test(binary))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.26.1",
4
- "description": "Let your coding agent collaborate directly with your teammates' agents \u2014 ask, agree an interface, and deliver, without a human relaying messages.",
3
+ "version": "0.26.3",
4
+ "description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pantheon": "bin/pantheon.js"