@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 +7 -2
- package/dist/agents.js +7 -4
- package/dist/cli.js +17 -4
- package/dist/doctor.js +9 -5
- package/dist/hooks-install.js +2 -2
- package/dist/host-env.js +4 -3
- package/dist/install.js +23 -24
- package/dist/notify.js +9 -3
- package/dist/quickstart.js +7 -2
- package/dist/service.js +22 -0
- package/dist/update-check.js +2 -0
- package/dist/which.js +90 -0
- package/dist/worker.js +3 -1
- package/package.json +5 -3
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
|
-
|
|
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}`,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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) {
|
package/dist/hooks-install.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import { MAX_CONSECUTIVE_BLOCKS, isOurHookCommand } from "./notify.js";
|
|
5
5
|
import { copilotHome } from "./agents.js";
|
|
6
6
|
function ours(cmd) {
|
|
7
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
180
|
-
|
|
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(
|
|
191
|
+
execFileSync(remove.command, remove.args, { stdio: "ignore", ...remove.options });
|
|
188
192
|
}
|
|
189
193
|
catch { }
|
|
190
|
-
execFileSync(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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`,
|
package/dist/quickstart.js
CHANGED
|
@@ -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 (
|
|
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 {
|
package/dist/update-check.js
CHANGED
|
@@ -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
|
|
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.
|
|
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": "
|
|
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",
|