@join-pantheon/cli 0.26.0 → 0.26.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/bin/pantheon.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { existsSync } from "node:fs";
11
11
  import { dirname, resolve } from "node:path";
12
- import { fileURLToPath } from "node:url";
12
+ import { fileURLToPath, pathToFileURL } from "node:url";
13
13
 
14
14
  const here = dirname(fileURLToPath(import.meta.url));
15
15
  const root = resolve(here, "..");
@@ -25,4 +25,9 @@ if (!existsSync(entry)) {
25
25
  process.exit(1);
26
26
  }
27
27
 
28
- await import(entry);
28
+ // A path is not a URL, and on Windows that is fatal rather than pedantic: an
29
+ // absolute POSIX path happens to work as an import specifier, while `D:\...`
30
+ // is read as the scheme "d:" and Node refuses it — so `pantheon --version`,
31
+ // the first command anyone runs, died with ERR_UNSUPPORTED_ESM_URL_SCHEME on
32
+ // every Windows machine. A real Windows runner is what said so.
33
+ await import(pathToFileURL(entry).href);
package/dist/agents.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
3
+ import { homedir, platform } from "node:os";
4
4
  import { join, dirname, isAbsolute } from "node:path";
5
5
  import { launcher } from "./install.js";
6
+ import { agentSpawn, which } from "./which.js";
6
7
  import { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
7
8
  export function launchArgs(agent, prompt, name) {
8
9
  if (agent === "claude-code")
@@ -38,7 +39,7 @@ const sh = (cmd) => {
38
39
  };
39
40
  export function cliOnPath(name) {
40
41
  if (!binaryCache.has(`path:${name}`))
41
- binaryCache.set(`path:${name}`, sh(`command -v ${name}`));
42
+ binaryCache.set(`path:${name}`, which(name));
42
43
  return binaryCache.get(`path:${name}`) ?? null;
43
44
  }
44
45
  export function agentById(id) {
@@ -87,7 +88,8 @@ const binaryCache = new Map();
87
88
  export function resolveBinary(backend) {
88
89
  for (const b of backend.binaries) {
89
90
  if (!binaryCache.has(b)) {
90
- let found = sh(`command -v ${b}`) ? b : null;
91
+ const hit = which(b);
92
+ let found = hit ? (platform() === "win32" ? hit : b) : null;
91
93
  if (found && b === "agent") {
92
94
  const v = `${sh("agent --version 2>/dev/null | head -1") ?? ""} ${sh("agent --help 2>/dev/null | head -3") ?? ""}`;
93
95
  if (!/cursor/i.test(v))
@@ -119,7 +121,8 @@ export function copilotSignedIn(run = runStatus, env = process.env, gh = () => !
119
121
  }
120
122
  const runStatus = (binary, args) => {
121
123
  try {
122
- const out = execFileSync(binary, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 8_000, env: { ...process.env, NO_COLOR: "1" } });
124
+ const sp = agentSpawn(binary, args);
125
+ const out = execFileSync(sp.command, sp.args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 8_000, env: { ...process.env, NO_COLOR: "1" }, ...sp.options });
123
126
  return { status: 0, out };
124
127
  }
125
128
  catch (e) {
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { basename, dirname, join, resolve } from "node:path";
12
12
  import { PantheonClient } from "./client.js";
13
+ import { agentSpawn } from "./which.js";
13
14
  import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
14
15
  import { fmtLimit } from "./plans.js";
15
16
  import { validEmail, validUsername } from "./accounts.js";
@@ -2318,7 +2319,10 @@ async function main() {
2318
2319
  const prompt = o ? dp(o.id, dt(o.body)) : `Read Pantheon act ${a.id}${goalId ? ` and the thread of goal ${goalId}` : ""}. Explain in plain words what happened and whether anything is owed by me, then give me numbered next steps. Do not act until I choose.`;
2319
2320
  console.error(D(`opening ${agent.id}…`));
2320
2321
  const { spawnSync } = await import("node:child_process");
2321
- spawnSync(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)), { stdio: "inherit" });
2322
+ {
2323
+ const sp = agentSpawn(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)));
2324
+ spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2325
+ }
2322
2326
  return;
2323
2327
  }
2324
2328
  if (choice === "2") {
@@ -2382,7 +2386,10 @@ async function main() {
2382
2386
  const bin = cliOnPath("grok") ?? die("grok is not on PATH here");
2383
2387
  console.error(D("opening grok…"));
2384
2388
  const { spawnSync } = await import("node:child_process");
2385
- spawnSync(bin, [hud.prompt], { stdio: "inherit" });
2389
+ {
2390
+ const sp = agentSpawn(bin, [hud.prompt]);
2391
+ spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2392
+ }
2386
2393
  return;
2387
2394
  }
2388
2395
  if (hud.prompt) {
@@ -2391,7 +2398,10 @@ async function main() {
2391
2398
  die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
2392
2399
  console.error(D(`opening ${agent.id}…`));
2393
2400
  const { spawnSync } = await import("node:child_process");
2394
- spawnSync(agent.binary, launchArgs(agent.id, hud.prompt, sessionName(hud.prompt)), { stdio: "inherit" });
2401
+ {
2402
+ const sp = agentSpawn(agent.binary, launchArgs(agent.id, hud.prompt, sessionName(hud.prompt)));
2403
+ spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2404
+ }
2395
2405
  return;
2396
2406
  }
2397
2407
  if (hud.decision)
@@ -2414,7 +2424,10 @@ async function main() {
2414
2424
  die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
2415
2425
  console.error(D(`opening ${agent.id} with the question…`));
2416
2426
  const { spawnSync } = await import("node:child_process");
2417
- spawnSync(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)), { stdio: "inherit" });
2427
+ {
2428
+ const sp = agentSpawn(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)));
2429
+ spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2430
+ }
2418
2431
  return;
2419
2432
  }
2420
2433
  case "approvals": {
package/dist/doctor.js CHANGED
@@ -14,9 +14,10 @@ import { hookState, readIntent, agentHookStates } from "./notify.js";
14
14
  import { loadPersonas } from "./persona.js";
15
15
  import { workerProc, menubarProc, notifierProc } from "./machine.js";
16
16
  import { chooseAgent, describeChoice } from "./agents.js";
17
+ import { onPathNamed } from "./which.js";
17
18
  import { VERSION } from "./version.js";
18
- import { serviceInstalled } from "./service.js";
19
- import { hostEnv } from "./host-env.js";
19
+ import { serviceInstalled, servicesUnavailable } from "./service.js";
20
+ import { hostEnv, platformName } from "./host-env.js";
20
21
  const HOSTED_RELAY = "relay.joinpantheon.network";
21
22
  const sh = (cmd, cwd) => {
22
23
  try {
@@ -55,7 +56,7 @@ export async function doctor(projectDir) {
55
56
  : 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`);
56
57
  const host = hostEnv();
57
58
  if (host.ephemeral) {
58
- 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"}`);
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 ${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"}`);
59
60
  }
60
61
  else {
61
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" : ""}`);
@@ -135,7 +136,7 @@ export async function doctor(projectDir) {
135
136
  if (branch)
136
137
  add("pass", "git branch", branch);
137
138
  }
138
- const agents = ["claude", "codex", "cursor-agent", "copilot"].filter((a) => sh(`command -v ${a}`));
139
+ const agents = ["claude", "codex", "cursor-agent", "copilot"].filter((a) => onPathNamed(a));
139
140
  agents.length
140
141
  ? add("pass", "agent cli", agents.join(", "), undefined, agents.join(", "))
141
142
  : add("warn", "agent cli", "found none of claude / codex / cursor-agent / copilot on PATH", "fine if you only drive agents through a GUI (Conductor, Cursor, VS Code), but `pantheon worker` needs a CLI to spawn", "none of claude / codex / cursor-agent / copilot on PATH");
@@ -254,7 +255,10 @@ export async function doctor(projectDir) {
254
255
  add("pass", "wake rights", `${who} — worker is running`, undefined, `${wakers.length} may wake; worker running`);
255
256
  }
256
257
  else {
257
- 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`);
258
+ const noSvc = servicesUnavailable(host.platform);
259
+ add("warn", "wake rights", `${who}, but no worker is running here, so nothing can be woken`, noSvc
260
+ ? "Pantheon does not run in the background on Windows yet, so a peer's act waits until your agent is open"
261
+ : "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`);
258
262
  }
259
263
  }
260
264
  if (host.desktop) {
@@ -1,10 +1,10 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
- import { HOOK_MARKER, MAX_CONSECUTIVE_BLOCKS } from "./notify.js";
4
+ import { MAX_CONSECUTIVE_BLOCKS, isOurHookCommand } from "./notify.js";
5
5
  import { copilotHome } from "./agents.js";
6
6
  function ours(cmd) {
7
- return !!cmd?.includes(HOOK_MARKER);
7
+ return isOurHookCommand(cmd);
8
8
  }
9
9
  function readJson(file) {
10
10
  if (!existsSync(file))
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
@@ -7,18 +7,15 @@ import { grantToolPermissions, claudeSettingsFile, cursorCliConfigFile } from ".
7
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
+ import { agentSpawn, onPathNamed, which } from "./which.js";
10
11
  const HERE = dirname(fileURLToPath(import.meta.url));
11
12
  export function onPath() {
12
- try {
13
- execFileSync("sh", ["-lc", "command -v pantheon"], { stdio: ["ignore", "ignore", "ignore"] });
14
- return true;
15
- }
16
- catch {
17
- return false;
18
- }
13
+ return onPathNamed("pantheon");
19
14
  }
20
- export function launcher(force) {
15
+ export function launcher(force, os = platform()) {
21
16
  const portable = force === "portable" || (force !== "local" && onPath());
17
+ if (os === "win32")
18
+ return { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"], portable: false };
22
19
  return portable
23
20
  ? { command: "pantheon", args: ["mcp"], portable: true }
24
21
  : { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"], portable: false };
@@ -159,9 +156,17 @@ export function install(projectDir, cfg, force) {
159
156
  function localEntry() {
160
157
  return { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"] };
161
158
  }
162
- function userEntry() {
159
+ function userEntry(os = platform()) {
160
+ if (os === "win32") {
161
+ const node = process.execPath;
162
+ const entry = resolve(HERE, "cli.js");
163
+ if (existsSync(entry))
164
+ return { command: node, args: [entry, "mcp"] };
165
+ const abs = which("pantheon");
166
+ return abs ? { command: abs, args: ["mcp"] } : { command: "pantheon", args: ["mcp"] };
167
+ }
163
168
  try {
164
- const abs = execFileSync("sh", ["-lc", "command -v pantheon"], { encoding: "utf8" }).trim();
169
+ const abs = which("pantheon") ?? "";
165
170
  if (abs && !isVersionManagedPath(abs))
166
171
  return { command: abs, args: ["mcp"] };
167
172
  if (abs)
@@ -176,18 +181,17 @@ function isVersionManagedPath(p) {
176
181
  }
177
182
  function claudeUserScope(entry) {
178
183
  const tool = "claude code";
179
- try {
180
- execFileSync("sh", ["-lc", "command -v claude"], { stdio: "ignore" });
181
- }
182
- catch {
184
+ const claudeBin = which("claude");
185
+ if (!claudeBin)
183
186
  return { tool, where: "-", status: "skipped (claude not on PATH)" };
184
- }
185
187
  try {
188
+ const remove = agentSpawn(claudeBin, ["mcp", "remove", "pantheon", "-s", "user"]);
189
+ const add = agentSpawn(claudeBin, ["mcp", "add", "pantheon", "-s", "user", "--", entry.command, ...entry.args]);
186
190
  try {
187
- execFileSync("claude", ["mcp", "remove", "pantheon", "-s", "user"], { stdio: "ignore" });
191
+ execFileSync(remove.command, remove.args, { stdio: "ignore", ...remove.options });
188
192
  }
189
193
  catch { }
190
- execFileSync("claude", ["mcp", "add", "pantheon", "-s", "user", "--", entry.command, ...entry.args], { stdio: "ignore" });
194
+ execFileSync(add.command, add.args, { stdio: "ignore", ...add.options });
191
195
  return { tool, where: "user scope", status: "ready in every project and every Conductor window" };
192
196
  }
193
197
  catch (e) {
@@ -207,13 +211,8 @@ function copilotUserScope(entry) {
207
211
  const tool = "copilot";
208
212
  const file = copilotMcpConfigFile();
209
213
  let present = existsSync(dirname(file));
210
- if (!present) {
211
- try {
212
- execFileSync("sh", ["-lc", "command -v copilot"], { stdio: "ignore" });
213
- present = true;
214
- }
215
- catch { }
216
- }
214
+ if (!present && onPathNamed("copilot"))
215
+ present = true;
217
216
  if (!present)
218
217
  return { tool, where: "-", status: "skipped (copilot not on PATH)" };
219
218
  try {
package/dist/notify.js CHANGED
@@ -5,6 +5,9 @@ import { whichPantheon } from "./service.js";
5
5
  import { detectForHooks, agentById } from "./agents.js";
6
6
  import { codexHookState, cursorHookState, copilotHookState, installCodexHooks, installCursorHooks, installCopilotHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, } from "./hooks-install.js";
7
7
  export const HOOK_MARKER = "pantheon hook";
8
+ export const HOOK_RE = /pantheon(?:\.cmd|\.exe|\.bat)?"?\s+hook\s/i;
9
+ export const isOurHookCommand = (cmd) => !!cmd && (cmd.includes(HOOK_MARKER) || HOOK_RE.test(cmd));
10
+ export const hookBin = (bin) => (/\s/.test(bin) ? `"${bin}"` : bin);
8
11
  export function settingsPath(scope, projectDir = process.cwd()) {
9
12
  return scope === "user"
10
13
  ? join(homedir(), ".claude", "settings.json")
@@ -25,7 +28,7 @@ function write(file, doc) {
25
28
  writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
26
29
  }
27
30
  function ours(g) {
28
- return g.hooks?.some((h) => h.command?.includes(HOOK_MARKER)) ?? false;
31
+ return g.hooks?.some((h) => isOurHookCommand(h.command)) ?? false;
29
32
  }
30
33
  export function hookState(projectDir = process.cwd(), only) {
31
34
  for (const scope of (only ? [only] : ["project", "user"])) {
@@ -46,7 +49,7 @@ export function hookState(projectDir = process.cwd(), only) {
46
49
  if (!hit)
47
50
  continue;
48
51
  events.push(event);
49
- command ??= hit.hooks.find((h) => h.command.includes(HOOK_MARKER)).command;
52
+ command ??= hit.hooks.find((h) => isOurHookCommand(h.command)).command;
50
53
  }
51
54
  if (events.length)
52
55
  return { installed: true, scope, file, command, events };
@@ -137,7 +140,10 @@ export function repairHook(home) {
137
140
  function installClaudeHook(scope, projectDir = process.cwd()) {
138
141
  const file = settingsPath(scope, projectDir);
139
142
  const doc = read(file);
140
- const bin = whichPantheon();
143
+ const found = whichPantheon();
144
+ if (!found)
145
+ throw new Error("pantheon binary not found on PATH");
146
+ const bin = hookBin(found);
141
147
  const events = {
142
148
  Stop: `${bin} hook stop`,
143
149
  UserPromptSubmit: `${bin} hook prompt`,
@@ -9,13 +9,14 @@ 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";
16
16
  import { existsSync } from "node:fs";
17
17
  import { grant as grantCap, loadPolicy, policyPath, savePolicy } from "./worker-policy.js";
18
18
  import { Relay } from "./relay.js";
19
+ import { which } from "./which.js";
19
20
  const sh = (cmd) => {
20
21
  try {
21
22
  return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
@@ -29,6 +30,7 @@ export function wireUp(opts, deps = {}) {
29
30
  const ensureNotifier = deps.ensureNotifier ?? ensureNativeNotifier;
30
31
  const ensureTray = deps.ensureTray ?? ensureTrayBinary;
31
32
  const host = deps.host ?? hostEnv();
33
+ const noServices = servicesUnavailable(host.platform);
32
34
  const tools = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
33
35
  const granted = [];
34
36
  const warnings = [];
@@ -91,6 +93,9 @@ export function wireUp(opts, deps = {}) {
91
93
  if (how === "off") {
92
94
  tools.push({ tool: "worker", status: "off (--no-worker) — this machine cannot answer the team while you are away" });
93
95
  }
96
+ else if (opts.unattended && noServices) {
97
+ tools.push({ tool: "worker", status: noServices });
98
+ }
94
99
  else if (opts.unattended) {
95
100
  const project = opts.unattended;
96
101
  const isRepo = sh(`git -C ${JSON.stringify(project)} rev-parse --is-inside-work-tree 2>/dev/null`) === "true";
@@ -126,7 +131,7 @@ export function findReach(port) {
126
131
  if (ts && /^\d+\.\d+\.\d+\.\d+$/.test(ts)) {
127
132
  return { url: `ws://${ts}:${port}`, how: `tailscale (${ts})`, shareable: true };
128
133
  }
129
- if (sh("command -v cloudflared")) {
134
+ if (which("cloudflared")) {
130
135
  return {
131
136
  url: `ws://127.0.0.1:${port}`, how: "localhost", shareable: false,
132
137
  hint: `cloudflared is installed. To let a teammate reach you:\n` +
package/dist/service.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { homedir, platform } from "node:os";
4
+ import { which } from "./which.js";
4
5
  import { dirname, join, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { launcher } from "./install.js";
@@ -12,6 +13,12 @@ const SERVICE_BASE_PATH = [
12
13
  export function servicePath(envPath = process.env.PATH ?? "") {
13
14
  return [...new Set([...envPath.split(":"), ...SERVICE_BASE_PATH].filter(Boolean))].join(":");
14
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
+ }
15
22
  export function plistPath(kind = "worker") {
16
23
  return join(homedir(), "Library", "LaunchAgents", `${LABEL(kind)}.plist`);
17
24
  }
@@ -29,6 +36,8 @@ function argvFor(kind, args) {
29
36
  return abs ? [abs, kind, ...args] : [process.execPath, resolve(HERE, "cli.js"), kind, ...args];
30
37
  }
31
38
  export function whichPantheon() {
39
+ if (platform() === "win32")
40
+ return which("pantheon");
32
41
  try {
33
42
  const p = execFileSync("/usr/bin/which", ["pantheon"], { encoding: "utf8" }).trim();
34
43
  return p.startsWith("/") && existsSync(p) ? p : null;
@@ -84,6 +93,9 @@ WantedBy=default.target
84
93
  `;
85
94
  }
86
95
  export function installService(kind, cwd, args) {
96
+ const noServices = servicesUnavailable();
97
+ if (noServices)
98
+ throw new Error(noServices.replace(/^not available — /, ""));
87
99
  const out = [];
88
100
  const abs = resolve(cwd);
89
101
  const logDir = join(abs, ".pantheon");
@@ -131,6 +143,14 @@ export function installService(kind, cwd, args) {
131
143
  }
132
144
  export function uninstallService(kind) {
133
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
+ }
134
154
  if (platform() === "darwin") {
135
155
  const p = plistPath(kind);
136
156
  if (!existsSync(p))
@@ -185,6 +205,8 @@ export function restartService(kind, exec = (c, a) => execFileSync(c, a, { stdio
185
205
  }
186
206
  export function serviceInstalled(kind) {
187
207
  try {
208
+ if (platform() === "win32")
209
+ return false;
188
210
  return existsSync(platform() === "darwin" ? plistPath(kind) : unitPath(kind));
189
211
  }
190
212
  catch {
@@ -43,6 +43,8 @@ export function updateStatus(deps) {
43
43
  export function versionLine(version, behind, latest) {
44
44
  if (behind <= 0)
45
45
  return `pantheon ${version}`;
46
+ if (latest && !semverGt(latest, version))
47
+ return `pantheon ${version}`;
46
48
  return latest
47
49
  ? `pantheon ${version} · ${latest} available — run \`pantheon upgrade\``
48
50
  : `pantheon ${version} · ${behind} update${behind === 1 ? "" : "s"} behind — run \`pantheon upgrade\``;
package/dist/which.js ADDED
@@ -0,0 +1,90 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
3
+ import { posix, win32 } from "node:path";
4
+ import { platform } from "node:os";
5
+ export const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.PS1";
6
+ const isWindows = (os = platform()) => os === "win32";
7
+ function runnable(file) {
8
+ try {
9
+ if (!existsSync(file) || !statSync(file).isFile())
10
+ return false;
11
+ if (isWindows())
12
+ return true;
13
+ accessSync(file, constants.X_OK);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export function whichWindows(name, env = process.env, exists = runnable) {
21
+ if (!name)
22
+ return null;
23
+ const exts = (env.PATHEXT || DEFAULT_PATHEXT).split(";").map((e) => e.trim()).filter(Boolean);
24
+ const ext = win32.extname(name);
25
+ const hasExt = !!ext && exts.some((e) => e.toLowerCase() === ext.toLowerCase());
26
+ const candidates = (base) => hasExt ? [base] : exts.flatMap((e) => (e === e.toLowerCase() ? [base + e] : [base + e.toLowerCase(), base + e]));
27
+ if (win32.isAbsolute(name)) {
28
+ for (const c of candidates(name))
29
+ if (exists(c))
30
+ return c;
31
+ return null;
32
+ }
33
+ for (const dir of (env.PATH || env.Path || "").split(";")) {
34
+ const d = dir.replace(/^"|"$/g, "").trim();
35
+ if (!d)
36
+ continue;
37
+ for (const c of candidates(win32.join(d, name)))
38
+ if (exists(c))
39
+ return c;
40
+ }
41
+ return null;
42
+ }
43
+ export function which(name, os = platform(), env = process.env) {
44
+ if (isWindows(os))
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) {
63
+ try {
64
+ const out = execFileSync("sh", ["-lc", `command -v ${name}`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
65
+ if (!out)
66
+ return null;
67
+ return out.startsWith("/") && !runnable(out) ? null : out;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ export const onPathNamed = (name, os = platform(), env = process.env) => which(name, os, env) !== null;
74
+ export function spawnShape(binary, os = platform()) {
75
+ if (!isWindows(os))
76
+ return { command: binary, shell: false };
77
+ return { command: binary, shell: /\.(cmd|bat)$/i.test(binary) };
78
+ }
79
+ const cmdQuote = (a) => (/[\s"&|<>^()]/.test(a) || a === "" ? `"${a.replace(/"/g, '\\"')}"` : a);
80
+ export function agentSpawn(binary, args, os = platform()) {
81
+ if (!isWindows(os) || !/\.(cmd|bat)$/i.test(binary))
82
+ return { command: binary, args: [...args], options: {} };
83
+ const line = [binary, ...args].map(cmdQuote).join(" ");
84
+ return {
85
+ command: process.env.ComSpec || "cmd.exe",
86
+ args: ["/d", "/s", "/c", `"${line}"`],
87
+ options: { windowsVerbatimArguments: true },
88
+ };
89
+ }
90
+ //# sourceMappingURL=which.js.map
package/dist/worker.js CHANGED
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { PantheonClient } from "./client.js";
7
7
  import { headlessSpawn, materializeSpawnFiles, chooseAgent, describeChoice } from "./agents.js";
8
+ import { agentSpawn } from "./which.js";
8
9
  import { VERSION } from "./version.js";
9
10
  import { applyAnswer, loadPolicy, mayAsk, needsSelfUpgrade, recordAsk, savePolicy, useGrant, } from "./worker-policy.js";
10
11
  import { agentHookStates, repairHook } from "./notify.js";
@@ -616,7 +617,8 @@ export class Worker {
616
617
  return { obligation: o, branch, worktree, ok: false, detail };
617
618
  }
618
619
  const detail = await new Promise((res) => {
619
- const child = procSpawn(command, args, { cwd: worktree, stdio: ["ignore", "pipe", "pipe"], env });
620
+ const sp = agentSpawn(command, args);
621
+ const child = procSpawn(sp.command, sp.args, { cwd: worktree, stdio: ["ignore", "pipe", "pipe"], env, ...sp.options });
620
622
  let out = "";
621
623
  child.stdout.on("data", (b) => { out += b.toString(); });
622
624
  child.stderr.on("data", (b) => { out += b.toString(); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
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": {
@@ -10,13 +10,15 @@
10
10
  "node": ">=22"
11
11
  },
12
12
  "scripts": {
13
- "build": "rm -rf dist && tsc -p .",
13
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p .",
14
14
  "dev": "tsc -p . --watch",
15
15
  "relay": "node dist/cli.js relay",
16
16
  "test": "PANTHEON_HOME=\"$(mktemp -d)\" HOME=\"$(mktemp -d)\" node --test --test-force-exit dist/test/*.test.js",
17
17
  "prepare": "npm run build",
18
18
  "prepublishOnly": "npm run build && npm test",
19
- "start": "node dist/cli.js relay"
19
+ "start": "node dist/cli.js relay",
20
+ "test:ci": "node scripts/test.mjs",
21
+ "smoke": "node scripts/smoke.mjs"
20
22
  },
21
23
  "dependencies": {
22
24
  "@modelcontextprotocol/sdk": "^1.0.0",