@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 +35 -12
- package/dist/host-env.js +4 -3
- package/dist/install.js +33 -4
- package/dist/notify.js +15 -7
- package/dist/quickstart.js +5 -1
- package/dist/service.js +19 -0
- package/dist/update-check.js +3 -1
- package/dist/upgrade.js +8 -3
- package/dist/which.js +23 -1
- package/package.json +2 -2
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 {
|
|
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
|
|
23
|
+
const git = (args, cwd) => {
|
|
23
24
|
try {
|
|
24
|
-
return execFileSync("
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
144
|
-
if (!
|
|
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 =
|
|
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);
|
package/dist/quickstart.js
CHANGED
|
@@ -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 {
|
package/dist/update-check.js
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
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: () =>
|
|
72
|
+
npmUpgrade: () => npm(["install", "-g", `${PKG_NAME}@latest`, "--no-audit", "--no-fund"], { stdio: ["ignore", "pipe", "pipe"] }),
|
|
68
73
|
npmLatest: () => {
|
|
69
74
|
try {
|
|
70
|
-
return
|
|
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.
|
|
4
|
-
"description": "Let your coding agent collaborate directly with your teammates' agents
|
|
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"
|