@join-pantheon/cli 0.28.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts.js +13 -2
- package/dist/agents.js +377 -9
- package/dist/cli.js +378 -192
- package/dist/client.js +73 -12
- package/dist/config.js +15 -0
- package/dist/doctor.js +13 -10
- package/dist/grants.js +16 -1
- package/dist/hooks-install.js +4 -2
- package/dist/host-env.js +51 -6
- package/dist/install.js +216 -16
- package/dist/kiro-files.js +23 -6
- package/dist/ledger.js +8 -2
- package/dist/machine.js +286 -60
- package/dist/mcp-supervisor.js +5 -0
- package/dist/mcp.js +33 -6
- package/dist/network.js +258 -57
- package/dist/pending.js +14 -7
- package/dist/quickstart.js +186 -22
- package/dist/refusal.js +29 -0
- package/dist/relay.js +738 -447
- package/dist/roster-file.js +19 -3
- package/dist/run-failure.js +18 -7
- package/dist/service.js +192 -40
- package/dist/setup-guide.js +21 -4
- package/dist/store.js +9 -3
- package/dist/which.js +66 -4
- package/dist/workdir.js +7 -1
- package/dist/worker.js +522 -101
- package/helper/macos/menubar.swift +11 -3
- package/package.json +1 -1
package/dist/roster-file.js
CHANGED
|
@@ -83,10 +83,11 @@ export class Allowlist {
|
|
|
83
83
|
get hasToken() {
|
|
84
84
|
return this.tokenHash !== null;
|
|
85
85
|
}
|
|
86
|
-
mintInvite(by, mayWake, ttlMinutes, token) {
|
|
86
|
+
mintInvite(by, mayWake, ttlMinutes, token, spaceToken) {
|
|
87
87
|
const inv = {
|
|
88
88
|
token, created_by: by, created_at: Date.now(),
|
|
89
89
|
expires_at: Date.now() + ttlMinutes * 60_000, may_wake: mayWake,
|
|
90
|
+
...(spaceToken ? { space_token: spaceToken } : {}),
|
|
90
91
|
};
|
|
91
92
|
this.invites.set(token, inv);
|
|
92
93
|
this.flush();
|
|
@@ -101,6 +102,9 @@ export class Allowlist {
|
|
|
101
102
|
if (Date.now() > inv.expires_at)
|
|
102
103
|
return { ok: false, why: "that invite has expired — ask for a fresh one" };
|
|
103
104
|
const existing = this.entries.get(member);
|
|
105
|
+
if (existing?.revoked_at && inv.created_at <= existing.revoked_at) {
|
|
106
|
+
return { ok: false, why: `"${member}" was removed from this team${existing.revoked_by ? ` by ${existing.revoked_by}` : ""}, so an invite from before that cannot bring them back — ask the founder for a new one` };
|
|
107
|
+
}
|
|
104
108
|
if (existing && existing.publicKey !== publicKey) {
|
|
105
109
|
return {
|
|
106
110
|
ok: false,
|
|
@@ -191,7 +195,7 @@ export class Allowlist {
|
|
|
191
195
|
}
|
|
192
196
|
if (cur.publicKey !== publicKey)
|
|
193
197
|
return "key-mismatch";
|
|
194
|
-
return cur.admitted ? "ok" : "pending";
|
|
198
|
+
return cur.admitted ? "ok" : cur.revoked_at ? "revoked" : "pending";
|
|
195
199
|
}
|
|
196
200
|
admit(member, by, mayWake) {
|
|
197
201
|
const e = this.entries.get(member);
|
|
@@ -200,6 +204,8 @@ export class Allowlist {
|
|
|
200
204
|
e.admitted = true;
|
|
201
205
|
e.admitted_by = by;
|
|
202
206
|
e.may_wake = mayWake;
|
|
207
|
+
delete e.revoked_at;
|
|
208
|
+
delete e.revoked_by;
|
|
203
209
|
this.flush();
|
|
204
210
|
return e;
|
|
205
211
|
}
|
|
@@ -207,18 +213,28 @@ export class Allowlist {
|
|
|
207
213
|
const e = this.entries.get(member);
|
|
208
214
|
return e && e.admitted ? e.publicKey : null;
|
|
209
215
|
}
|
|
216
|
+
cancelInvitesBy(member) {
|
|
217
|
+
for (const [t, i] of this.invites)
|
|
218
|
+
if (i.created_by === member && !i.used_by)
|
|
219
|
+
this.invites.delete(t);
|
|
220
|
+
}
|
|
210
221
|
forget(member) {
|
|
211
222
|
const gone = this.entries.delete(member);
|
|
223
|
+
this.cancelInvitesBy(member);
|
|
212
224
|
if (gone)
|
|
213
225
|
this.flush();
|
|
214
226
|
return gone;
|
|
215
227
|
}
|
|
216
|
-
revoke(member) {
|
|
228
|
+
revoke(member, by) {
|
|
217
229
|
const e = this.entries.get(member);
|
|
218
230
|
if (!e)
|
|
219
231
|
return false;
|
|
220
232
|
e.admitted = false;
|
|
221
233
|
e.may_wake = false;
|
|
234
|
+
e.revoked_at = Date.now();
|
|
235
|
+
if (by)
|
|
236
|
+
e.revoked_by = by;
|
|
237
|
+
this.cancelInvitesBy(member);
|
|
222
238
|
this.flush();
|
|
223
239
|
return true;
|
|
224
240
|
}
|
package/dist/run-failure.js
CHANGED
|
@@ -74,6 +74,8 @@ export function diagnoseRun(f, probe = gitProbe) {
|
|
|
74
74
|
fix: `It can still answer and escalate. To let it deliver work, point the worker at a checkout: pantheon service install worker from inside the repo.`,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
if (!f.allowBash && !(f.needsCommands || BLOCKED_RE.test(f.transcript ?? f.detail) || BLOCKED_RE.test(f.detail)))
|
|
78
|
+
return silent(f);
|
|
77
79
|
if (!f.allowBash) {
|
|
78
80
|
return {
|
|
79
81
|
cause: "no_commands",
|
|
@@ -102,12 +104,21 @@ export function diagnoseRun(f, probe = gitProbe) {
|
|
|
102
104
|
lines: [`git status --short # ${list(dirty)}`, "git add -A", `git commit -m "…"`, `git push${on ? ` -u origin ${on}` : ""}`],
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
return silent(f);
|
|
108
|
+
}
|
|
109
|
+
const silent = (f) => ({
|
|
110
|
+
cause: "silent",
|
|
111
|
+
says: "The run finished cleanly but sent nothing back, so the request is still open.",
|
|
112
|
+
fix: `Read what it did at ${f.runLog}. If it was right to stay silent, answer the teammate yourself; if not, the transcript says where it stopped.`,
|
|
113
|
+
dir: f.worktree,
|
|
114
|
+
lines: [`cat ${f.runLog}`],
|
|
115
|
+
});
|
|
116
|
+
export const BLOCKED_RE = /(?:permission|approval)[^\n]{0,40}\b(?:denied|required|needed|to use (?:the )?(?:bash|shell))|\bnot (?:allowed|permitted) to (?:run|execute|use (?:the )?(?:bash|shell))|\b(?:bash|shell|command|tool)\b[^\n]{0,30}\b(?:was |is )?(?:blocked|denied|not allowed|rejected)\b|read-only sandbox|sandbox[^\n]{0,20}read-only/i;
|
|
117
|
+
export function workNeedsCommands(kind, trigger, triggerAct) {
|
|
118
|
+
if (triggerAct === "DELIVERED")
|
|
119
|
+
return true;
|
|
120
|
+
if (kind === "REVISE" && /\bDELIVERED\b/.test(trigger))
|
|
121
|
+
return true;
|
|
122
|
+
return /\bDELIVER(?:ED)?\b|\b(?:commit|push)\b(?! ?(?:message|hook))|\bpull request\b|\bopen a PR\b|\bimplement\b/i.test(trigger);
|
|
112
123
|
}
|
|
113
124
|
//# sourceMappingURL=run-failure.js.map
|
package/dist/service.js
CHANGED
|
@@ -1,12 +1,47 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync } from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { homedir, platform } from "node:os";
|
|
3
|
+
import { homedir, platform, userInfo } from "node:os";
|
|
4
4
|
import { which } from "./which.js";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { launcher } from "./install.js";
|
|
8
|
+
import { CONFIG_DIR } from "./config.js";
|
|
8
9
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const LABEL = (kind) => `dev.pantheon.${kind}`;
|
|
11
|
+
export function borrowedHome() {
|
|
12
|
+
if (process.env.PANTHEON_SERVICES_ANY_HOME === "1")
|
|
13
|
+
return false;
|
|
14
|
+
try {
|
|
15
|
+
return resolve(homedir()) !== resolve(userInfo().homedir);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const READ_ONLY = (c, a) => c === "launchctl" ? ["print", "print-disabled", "list"].includes(a[0] ?? "")
|
|
22
|
+
: c === "systemctl" ? a.some((x) => ["is-enabled", "is-active", "status", "show", "show-environment"].includes(x))
|
|
23
|
+
: true;
|
|
24
|
+
function guardedExecFile(c, a) {
|
|
25
|
+
if ((c === "launchctl" || c === "systemctl") && !READ_ONLY(c, a) && borrowedHome()) {
|
|
26
|
+
throw new Error(`refused: ${c} ${a[0] ?? ""} would change this user's real services, but HOME (${homedir()}) is not the user's home — a test or a sandbox. Set PANTHEON_SERVICES_ANY_HOME=1 to allow it.`);
|
|
27
|
+
}
|
|
28
|
+
return execFileSync(c, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) ?? "";
|
|
29
|
+
}
|
|
30
|
+
export const realSys = () => ({
|
|
31
|
+
platform: platform(),
|
|
32
|
+
exec: guardedExecFile,
|
|
33
|
+
uid: process.getuid?.() ?? 501,
|
|
34
|
+
user: (() => { try {
|
|
35
|
+
return userInfo().username;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return process.env.USER ?? "";
|
|
39
|
+
} })(),
|
|
40
|
+
});
|
|
41
|
+
export function serviceLogDir() {
|
|
42
|
+
return join(CONFIG_DIR, "logs");
|
|
43
|
+
}
|
|
44
|
+
export const serviceLogFile = (kind) => join(serviceLogDir(), `${kind}.log`);
|
|
10
45
|
const SERVICE_BASE_PATH = [
|
|
11
46
|
"/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin",
|
|
12
47
|
];
|
|
@@ -22,7 +57,7 @@ export function servicesUnavailable(plat = platform()) {
|
|
|
22
57
|
export function plistPath(kind = "worker") {
|
|
23
58
|
return join(homedir(), "Library", "LaunchAgents", `${LABEL(kind)}.plist`);
|
|
24
59
|
}
|
|
25
|
-
function unitPath(kind) {
|
|
60
|
+
export function unitPath(kind) {
|
|
26
61
|
return join(homedir(), ".config", "systemd", "user", `pantheon-${kind}.service`);
|
|
27
62
|
}
|
|
28
63
|
function xml(s) {
|
|
@@ -71,7 +106,11 @@ ${argv.map((a) => ` <string>${xml(a)}</string>`).join("\n")}
|
|
|
71
106
|
</plist>
|
|
72
107
|
`;
|
|
73
108
|
}
|
|
74
|
-
export function
|
|
109
|
+
export function systemdQuote(arg) {
|
|
110
|
+
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%").replace(/\$/g, "$$$$")}"`;
|
|
111
|
+
}
|
|
112
|
+
const systemdValue = (v) => v.replace(/%/g, "%%");
|
|
113
|
+
export function systemdUnit(kind, cwd, args, path = servicePath()) {
|
|
75
114
|
const description = kind === "relay"
|
|
76
115
|
? "pantheon relay — the coordination plane for a space"
|
|
77
116
|
: kind === "worker"
|
|
@@ -83,8 +122,9 @@ After=network-online.target
|
|
|
83
122
|
|
|
84
123
|
[Service]
|
|
85
124
|
Type=simple
|
|
86
|
-
WorkingDirectory=${cwd}
|
|
87
|
-
ExecStart=${argvFor(kind, args).join(" ")}
|
|
125
|
+
WorkingDirectory=${systemdValue(cwd)}
|
|
126
|
+
ExecStart=${argvFor(kind, args).map(systemdQuote).join(" ")}
|
|
127
|
+
Environment=${systemdQuote(`PATH=${path}`)}
|
|
88
128
|
Restart=always
|
|
89
129
|
RestartSec=30
|
|
90
130
|
|
|
@@ -92,29 +132,43 @@ RestartSec=30
|
|
|
92
132
|
WantedBy=default.target
|
|
93
133
|
`;
|
|
94
134
|
}
|
|
95
|
-
export function
|
|
96
|
-
|
|
135
|
+
export function lingerEnabled(sys = realSys()) {
|
|
136
|
+
try {
|
|
137
|
+
const out = sys.exec("loginctl", ["show-user", sys.user || String(sys.uid), "--property=Linger"]);
|
|
138
|
+
const m = /Linger=(yes|no)/.exec(out);
|
|
139
|
+
return m ? m[1] === "yes" : null;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function installService(kind, cwd, args, sys = realSys()) {
|
|
146
|
+
const noServices = servicesUnavailable(sys.platform);
|
|
97
147
|
if (noServices)
|
|
98
148
|
throw new Error(noServices.replace(/^not available — /, ""));
|
|
99
149
|
const out = [];
|
|
100
150
|
const abs = resolve(cwd);
|
|
101
|
-
const logDir =
|
|
102
|
-
|
|
103
|
-
|
|
151
|
+
const logDir = serviceLogDir();
|
|
152
|
+
if (sys.platform === "darwin") {
|
|
153
|
+
mkdirSync(logDir, { recursive: true });
|
|
104
154
|
const p = plistPath(kind);
|
|
105
155
|
mkdirSync(join(homedir(), "Library", "LaunchAgents"), { recursive: true });
|
|
106
156
|
writeFileSync(p, servicePlist(kind, abs, logDir, args));
|
|
107
157
|
out.push(` wrote ${p}`);
|
|
108
158
|
try {
|
|
109
|
-
|
|
159
|
+
sys.exec("launchctl", ["enable", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
try {
|
|
163
|
+
sys.exec("launchctl", ["unload", p]);
|
|
110
164
|
}
|
|
111
165
|
catch { }
|
|
112
166
|
try {
|
|
113
|
-
|
|
167
|
+
sys.exec("launchctl", ["load", "-w", p]);
|
|
114
168
|
out.push(` loaded — the ${kind} now starts at login and restarts if it dies`);
|
|
115
169
|
}
|
|
116
170
|
catch (e) {
|
|
117
|
-
out.push(` could not load it: ${e.message}`);
|
|
171
|
+
out.push(` could not load it: ${e.message.split("\n")[0]}`);
|
|
118
172
|
out.push(` load it yourself with: launchctl load -w ${p}`);
|
|
119
173
|
}
|
|
120
174
|
out.push(` logs: ${join(logDir, `${kind}.log`)}`);
|
|
@@ -122,15 +176,15 @@ export function installService(kind, cwd, args) {
|
|
|
122
176
|
}
|
|
123
177
|
else {
|
|
124
178
|
if (kind === "menubar" || kind === "notifier") {
|
|
125
|
-
throw new Error(`${kind === "menubar" ? "the menu bar" : "desktop alerts"} run on macOS only, and this machine reports ${platform
|
|
179
|
+
throw new Error(`${kind === "menubar" ? "the menu bar" : "desktop alerts"} run on macOS only, and this machine reports ${sys.platform}`);
|
|
126
180
|
}
|
|
127
181
|
const unit = unitPath(kind);
|
|
128
182
|
mkdirSync(join(homedir(), ".config", "systemd", "user"), { recursive: true });
|
|
129
183
|
writeFileSync(unit, systemdUnit(kind, abs, args));
|
|
130
184
|
out.push(` wrote ${unit}`);
|
|
131
185
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
186
|
+
sys.exec("systemctl", ["--user", "daemon-reload"]);
|
|
187
|
+
sys.exec("systemctl", ["--user", "enable", "--now", `pantheon-${kind}`]);
|
|
134
188
|
out.push(` enabled — the ${kind} now starts at login and restarts if it dies`);
|
|
135
189
|
out.push(` logs: journalctl --user -u pantheon-${kind} -f`);
|
|
136
190
|
out.push(` stop: pantheon service uninstall ${kind}`);
|
|
@@ -138,12 +192,15 @@ export function installService(kind, cwd, args) {
|
|
|
138
192
|
catch (e) {
|
|
139
193
|
throw new Error(`the ${kind} unit is written at ${unit} but could not be enabled — systemd --user is not running here (${e.message.split("\n")[0]}). When it is, run: systemctl --user enable --now pantheon-${kind} (WSL: set [boot] systemd=true in /etc/wsl.conf and restart WSL)`);
|
|
140
194
|
}
|
|
195
|
+
if (lingerEnabled(sys) === false) {
|
|
196
|
+
out.push(` ! the ${kind} stops when you log out (closing an SSH session counts). To keep it running: loginctl enable-linger ${sys.user || "$USER"}`);
|
|
197
|
+
}
|
|
141
198
|
}
|
|
142
199
|
return out;
|
|
143
200
|
}
|
|
144
|
-
export function uninstallService(kind) {
|
|
201
|
+
export function uninstallService(kind, sys = realSys()) {
|
|
145
202
|
const out = [];
|
|
146
|
-
if (platform
|
|
203
|
+
if (sys.platform === "win32") {
|
|
147
204
|
const stray = unitPath(kind);
|
|
148
205
|
if (existsSync(stray)) {
|
|
149
206
|
unlinkSync(stray);
|
|
@@ -151,17 +208,22 @@ export function uninstallService(kind) {
|
|
|
151
208
|
}
|
|
152
209
|
return [` no ${kind} service installed`];
|
|
153
210
|
}
|
|
154
|
-
if (platform
|
|
211
|
+
if (sys.platform === "darwin") {
|
|
155
212
|
const p = plistPath(kind);
|
|
156
213
|
if (!existsSync(p))
|
|
157
214
|
return [` no ${kind} service installed`];
|
|
215
|
+
let stopped = false;
|
|
158
216
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
217
|
+
sys.exec("launchctl", ["bootout", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
218
|
+
stopped = true;
|
|
161
219
|
}
|
|
162
|
-
catch {
|
|
163
|
-
|
|
220
|
+
catch { }
|
|
221
|
+
try {
|
|
222
|
+
sys.exec("launchctl", ["unload", "-w", p]);
|
|
223
|
+
stopped = true;
|
|
164
224
|
}
|
|
225
|
+
catch { }
|
|
226
|
+
out.push(stopped ? " stopped" : " was not loaded");
|
|
165
227
|
unlinkSync(p);
|
|
166
228
|
out.push(` removed ${p}`);
|
|
167
229
|
}
|
|
@@ -170,44 +232,134 @@ export function uninstallService(kind) {
|
|
|
170
232
|
if (!existsSync(unit))
|
|
171
233
|
return [` no ${kind} service installed`];
|
|
172
234
|
try {
|
|
173
|
-
|
|
235
|
+
sys.exec("systemctl", ["--user", "disable", "--now", `pantheon-${kind}`]);
|
|
174
236
|
out.push(" disabled and stopped");
|
|
175
237
|
}
|
|
176
238
|
catch {
|
|
177
239
|
out.push(` could not reach systemd --user — if it runs elsewhere: systemctl --user disable --now pantheon-${kind}`);
|
|
178
240
|
}
|
|
179
241
|
unlinkSync(unit);
|
|
242
|
+
try {
|
|
243
|
+
sys.exec("systemctl", ["--user", "daemon-reload"]);
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
180
246
|
out.push(` removed ${unit}`);
|
|
181
247
|
}
|
|
182
248
|
return out;
|
|
183
249
|
}
|
|
184
|
-
export function
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
250
|
+
export function disableService(kind, sys = realSys()) {
|
|
251
|
+
if (sys.platform === "darwin") {
|
|
252
|
+
try {
|
|
253
|
+
sys.exec("launchctl", ["bootout", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
254
|
+
}
|
|
255
|
+
catch { }
|
|
256
|
+
sys.exec("launchctl", ["disable", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
sys.exec("systemctl", ["--user", "disable", "--now", `pantheon-${kind}`]);
|
|
260
|
+
}
|
|
261
|
+
const realExec = guardedExecFile;
|
|
262
|
+
function outOf(exec, c, a) {
|
|
188
263
|
try {
|
|
189
|
-
|
|
190
|
-
exec("launchctl", ["kickstart", "-k", `gui/${uid}/${LABEL(kind)}`]);
|
|
191
|
-
else
|
|
192
|
-
exec("systemctl", ["--user", "restart", `pantheon-${kind}`]);
|
|
193
|
-
return [` ${kind}: restarted on the new code`];
|
|
264
|
+
return { out: String(exec(c, a) ?? ""), ok: true };
|
|
194
265
|
}
|
|
195
266
|
catch (e) {
|
|
196
|
-
|
|
197
|
-
|
|
267
|
+
return { out: String(e.stdout ?? ""), ok: false };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export function serviceState(kind, exec = realExec, plat = platform(), uid = process.getuid?.() ?? 501) {
|
|
271
|
+
if (plat === "darwin") {
|
|
272
|
+
const dis = outOf(exec, "launchctl", ["print-disabled", `gui/${uid}`]).out;
|
|
273
|
+
const disabled = new RegExp(`"${LABEL(kind).replace(/\./g, "\\.")}"\\s*=>\\s*(disabled|true)`).test(dis);
|
|
274
|
+
const pr = outOf(exec, "launchctl", ["print", `gui/${uid}/${LABEL(kind)}`]);
|
|
275
|
+
return { disabled, loaded: pr.ok, running: pr.ok && /^\s*state = running/m.test(pr.out) };
|
|
276
|
+
}
|
|
277
|
+
const en = outOf(exec, "systemctl", ["--user", "is-enabled", `pantheon-${kind}`]).out.trim();
|
|
278
|
+
const act = outOf(exec, "systemctl", ["--user", "is-active", `pantheon-${kind}`]).out.trim();
|
|
279
|
+
return { disabled: /^(disabled|masked)/.test(en), loaded: true, running: act === "active" || act === "activating" };
|
|
280
|
+
}
|
|
281
|
+
export function restartService(kind, exec = realExec, opts = {}) {
|
|
282
|
+
const plat = opts.platform ?? platform();
|
|
283
|
+
if (!serviceInstalled(kind, plat))
|
|
284
|
+
return [` ${kind}: not installed here`];
|
|
285
|
+
const uid = process.getuid?.() ?? 501;
|
|
286
|
+
const st = serviceState(kind, exec, plat, uid);
|
|
287
|
+
if (st.disabled)
|
|
288
|
+
return [` ${kind}: left stopped (you stopped it) — start it again: pantheon service install ${kind}`];
|
|
289
|
+
try {
|
|
290
|
+
if (plat === "darwin") {
|
|
291
|
+
if (st.loaded && !st.running)
|
|
292
|
+
return [` ${kind}: not running — left as it is (launchd starts it again on its own schedule)`];
|
|
293
|
+
if (!st.loaded) {
|
|
198
294
|
exec("launchctl", ["bootstrap", `gui/${uid}`, plistPath(kind)]);
|
|
199
295
|
return [` ${kind}: was not loaded in launchd — loaded and started on the new code`];
|
|
200
296
|
}
|
|
201
|
-
|
|
297
|
+
if (opts.reload) {
|
|
298
|
+
exec("launchctl", ["bootout", `gui/${uid}/${LABEL(kind)}`]);
|
|
299
|
+
exec("launchctl", ["bootstrap", `gui/${uid}`, plistPath(kind)]);
|
|
300
|
+
return [` ${kind}: reloaded with its new definition and restarted on the new code`];
|
|
301
|
+
}
|
|
302
|
+
exec("launchctl", ["kickstart", "-k", `gui/${uid}/${LABEL(kind)}`]);
|
|
303
|
+
return [` ${kind}: restarted on the new code`];
|
|
202
304
|
}
|
|
305
|
+
if (!st.running)
|
|
306
|
+
return [` ${kind}: not running — left as it is (it starts at login)`];
|
|
307
|
+
if (opts.reload)
|
|
308
|
+
exec("systemctl", ["--user", "daemon-reload"]);
|
|
309
|
+
exec("systemctl", ["--user", "restart", `pantheon-${kind}`]);
|
|
310
|
+
return [` ${kind}: restarted on the new code`];
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
203
313
|
return [` ${kind}: could not restart (${e.message.split("\n")[0]}) — pantheon service restart ${kind}`];
|
|
204
314
|
}
|
|
205
315
|
}
|
|
206
|
-
export function
|
|
316
|
+
export function refreshServiceDefinition(kind, plat = platform()) {
|
|
317
|
+
try {
|
|
318
|
+
if (plat === "darwin") {
|
|
319
|
+
const p = plistPath(kind);
|
|
320
|
+
if (!existsSync(p))
|
|
321
|
+
return { changed: false };
|
|
322
|
+
const xml = readFileSync(p, "utf8");
|
|
323
|
+
const log = /<key>StandardOutPath<\/key>\s*<string>([^<]*)<\/string>/.exec(xml)?.[1] ?? "";
|
|
324
|
+
const logDir = serviceLogDir();
|
|
325
|
+
if (unxml(log).startsWith(logDir + "/"))
|
|
326
|
+
return { changed: false };
|
|
327
|
+
const argv = [...(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(xml)?.[1] ?? "").matchAll(/<string>([^<]*)<\/string>/g)].map((m) => unxml(m[1]));
|
|
328
|
+
const at = argv.indexOf(kind);
|
|
329
|
+
const cwd = unxml(/<key>WorkingDirectory<\/key>\s*<string>([^<]*)<\/string>/.exec(xml)?.[1] ?? homedir());
|
|
330
|
+
if (at < 0)
|
|
331
|
+
return { changed: false };
|
|
332
|
+
mkdirSync(logDir, { recursive: true });
|
|
333
|
+
writeFileSync(p, servicePlist(kind, cwd, logDir, argv.slice(at + 1)));
|
|
334
|
+
return { changed: true, line: ` ${kind}: its log now goes to ${join(logDir, `${kind}.log`)}${log ? ` (was ${unxml(log)})` : ""}` };
|
|
335
|
+
}
|
|
336
|
+
if (plat === "win32")
|
|
337
|
+
return { changed: false };
|
|
338
|
+
const u = unitPath(kind);
|
|
339
|
+
if (!existsSync(u))
|
|
340
|
+
return { changed: false };
|
|
341
|
+
const unit = readFileSync(u, "utf8");
|
|
342
|
+
if (/^Environment="PATH=/m.test(unit))
|
|
343
|
+
return { changed: false };
|
|
344
|
+
const exec = /^ExecStart=(.*)$/m.exec(unit)?.[1] ?? "";
|
|
345
|
+
const words = exec.startsWith('"') ? [...exec.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1].replace(/\\(.)/g, "$1").replace(/%%/g, "%").replace(/\$\$/g, "$")) : exec.split(/\s+/);
|
|
346
|
+
const at = words.indexOf(kind);
|
|
347
|
+
const cwd = (/^WorkingDirectory=(.*)$/m.exec(unit)?.[1] ?? homedir()).replace(/%%/g, "%");
|
|
348
|
+
if (at < 0)
|
|
349
|
+
return { changed: false };
|
|
350
|
+
writeFileSync(u, systemdUnit(kind, cwd, words.slice(at + 1)));
|
|
351
|
+
return { changed: true, line: ` ${kind}: its unit now quotes its paths and carries your PATH` };
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
return { changed: false, line: ` ${kind}: could not refresh its service definition (${e.message.split("\n")[0]})` };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const unxml = (s) => s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
358
|
+
export function serviceInstalled(kind, plat = platform()) {
|
|
207
359
|
try {
|
|
208
|
-
if (
|
|
360
|
+
if (plat === "win32")
|
|
209
361
|
return false;
|
|
210
|
-
return existsSync(
|
|
362
|
+
return existsSync(plat === "darwin" ? plistPath(kind) : unitPath(kind));
|
|
211
363
|
}
|
|
212
364
|
catch {
|
|
213
365
|
return false;
|
package/dist/setup-guide.js
CHANGED
|
@@ -27,7 +27,7 @@ export const ALERT_OPTIONS = [
|
|
|
27
27
|
{ id: "decisions", label: "Only decisions that need me (recommended) — never for ordinary agent traffic." },
|
|
28
28
|
{ id: "deliveries", label: "Decisions, and when a teammate delivers work to me." },
|
|
29
29
|
{ id: "worker", label: "Decisions, deliveries, and when my own worker finishes a run." },
|
|
30
|
-
{ id: "none", label: "None — I will look at pantheon status and the menu bar." },
|
|
30
|
+
{ id: "none", label: "None — I will look at pantheon status (and the menu bar, on a Mac)." },
|
|
31
31
|
];
|
|
32
32
|
export function guideDefaults(profile, cfg) {
|
|
33
33
|
const n = profile.notifications ?? { decisions: true, deliveries: false, worker_done: false };
|
|
@@ -42,6 +42,9 @@ export function guideDefaults(profile, cfg) {
|
|
|
42
42
|
persona: false,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
export function optOutsFrom(argv) {
|
|
46
|
+
return { worker: argv.includes("--no-worker"), notifications: argv.includes("--no-notifications") };
|
|
47
|
+
}
|
|
45
48
|
export function detectBaseBranch(project) {
|
|
46
49
|
const git = (...a) => { try {
|
|
47
50
|
return execFileSync("git", a, { cwd: project, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -154,6 +157,9 @@ export function parseSetupFlags(argv) {
|
|
|
154
157
|
return { preset, errors };
|
|
155
158
|
}
|
|
156
159
|
export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
160
|
+
const optOut = ctx.optOut ?? {};
|
|
161
|
+
const os = deps.platform ?? process.platform;
|
|
162
|
+
const mac = os === "darwin";
|
|
157
163
|
const install = deps.installService ?? realInstall;
|
|
158
164
|
const uninstall = deps.uninstallService ?? realUninstall;
|
|
159
165
|
const installed = deps.serviceInstalled ?? realInstalled;
|
|
@@ -283,7 +289,10 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
283
289
|
catch (e) {
|
|
284
290
|
warnings.push(`agent wiring not re-applied (${e.message}) — run pantheon install --user`);
|
|
285
291
|
}
|
|
286
|
-
if (a.wake
|
|
292
|
+
if (optOut.worker && a.wake !== "no") {
|
|
293
|
+
applied.push(`worker: not installed (--no-worker) — ${a.wake === "anyone" ? "teammates may hand you work once it is on" : "only people you approve may hand you work once it is on"}: pantheon service install worker`);
|
|
294
|
+
}
|
|
295
|
+
else if (a.wake === "no") {
|
|
287
296
|
if (installed("worker")) {
|
|
288
297
|
try {
|
|
289
298
|
uninstall("worker");
|
|
@@ -313,6 +322,8 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
313
322
|
else {
|
|
314
323
|
applied.push("worker: run this again from your project's repo to switch it on there");
|
|
315
324
|
}
|
|
325
|
+
const waitIn = mac ? "pantheon status, approvals, and the menu bar" : "pantheon status and pantheon approvals";
|
|
326
|
+
const chosenAlerts = ALERT_OPTIONS.find((o) => o.id === a.notifications).label.split(" — ")[0].replace(/ \(recommended\)/, "");
|
|
316
327
|
if (a.notifications === "none") {
|
|
317
328
|
if (installed("notifier")) {
|
|
318
329
|
try {
|
|
@@ -320,10 +331,16 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
320
331
|
}
|
|
321
332
|
catch { }
|
|
322
333
|
}
|
|
323
|
-
applied.push(
|
|
334
|
+
applied.push(`alerts off — decisions still wait in ${waitIn}`);
|
|
335
|
+
}
|
|
336
|
+
else if (optOut.notifications) {
|
|
337
|
+
applied.push(`alerts: not installed (--no-notifications) — decisions wait in ${waitIn}. Your choice (${chosenAlerts.toLowerCase()}) is saved for when you turn them on${mac ? ": pantheon service install notifier" : ""}`);
|
|
338
|
+
}
|
|
339
|
+
else if (!mac) {
|
|
340
|
+
applied.push(`alerts: none on this machine — desktop alerts are macOS-only. Decisions wait in ${waitIn} (your choice, ${chosenAlerts.toLowerCase()}, is saved)`);
|
|
324
341
|
}
|
|
325
342
|
else {
|
|
326
|
-
if (!installed("notifier")
|
|
343
|
+
if (!installed("notifier")) {
|
|
327
344
|
if (existsSync(hudBinaryPath(deps.home))) {
|
|
328
345
|
try {
|
|
329
346
|
install("notifier", deps.home ?? homedir(), []);
|
package/dist/store.js
CHANGED
|
@@ -20,8 +20,7 @@ export class StateStore {
|
|
|
20
20
|
for (const g of snap.goals ?? [])
|
|
21
21
|
goals.set(g.id, g);
|
|
22
22
|
for (const o of snap.obligations ?? []) {
|
|
23
|
-
|
|
24
|
-
obligations.set(o.id, rest);
|
|
23
|
+
obligations.set(o.id, o.claimed_by ? { ...o, claimed_by: { ...o.claimed_by, at: Date.now() } } : o);
|
|
25
24
|
}
|
|
26
25
|
for (const p of snap.profiles ?? [])
|
|
27
26
|
profiles.set(p.member, p);
|
|
@@ -41,9 +40,16 @@ export class StateStore {
|
|
|
41
40
|
if (!this.dirty)
|
|
42
41
|
return;
|
|
43
42
|
this.dirty = false;
|
|
44
|
-
|
|
43
|
+
try {
|
|
44
|
+
this.flush(goals, obligations, profiles);
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
this.dirty = true;
|
|
48
|
+
this.onError?.(e);
|
|
49
|
+
}
|
|
45
50
|
}, 250);
|
|
46
51
|
}
|
|
52
|
+
onError;
|
|
47
53
|
flush(goals, obligations, profiles) {
|
|
48
54
|
const snap = {
|
|
49
55
|
v: 2,
|
package/dist/which.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, existsSync, statSync } from "node:fs";
|
|
3
|
-
import { posix, win32 } from "node:path";
|
|
2
|
+
import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join, posix, resolve, 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";
|
|
@@ -83,10 +83,72 @@ export function npmSpawn(args, os = platform(), resolveNpm = () => which("npm",
|
|
|
83
83
|
const s = agentSpawn(resolveNpm() ?? "npm.cmd", args, os);
|
|
84
84
|
return { ...s, options: { ...s.options, ...env } };
|
|
85
85
|
}
|
|
86
|
-
const
|
|
87
|
-
export
|
|
86
|
+
const CMD_UNSAFE = /["%!^&|<>\r\n\0]/;
|
|
87
|
+
export const cmdSafe = (a) => !CMD_UNSAFE.test(a);
|
|
88
|
+
const cmdQuote = (a) => {
|
|
89
|
+
if (!cmdSafe(a))
|
|
90
|
+
throw new CmdLineUnsafe(a);
|
|
91
|
+
return /[\s&|<>^()]/.test(a) || a === "" ? `"${a.replace(/(\\+)$/, "$1$1")}"` : a;
|
|
92
|
+
};
|
|
93
|
+
export class CmdLineUnsafe extends Error {
|
|
94
|
+
arg;
|
|
95
|
+
constructor(arg, binary, why) {
|
|
96
|
+
super(`${binary ? `${binary} ${why ?? "is a Windows command script Pantheon could not read"}, and ` : ""}this text cannot be passed through cmd.exe safely (it contains a quote, a %, a !, a cmd.exe operator or a line break) — it was not run`);
|
|
97
|
+
this.arg = arg;
|
|
98
|
+
this.name = "CmdLineUnsafe";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function inspectCmdShim(shim, read = (f) => { try {
|
|
102
|
+
return readFileSync(f, "utf8");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
} }, exists = existsSync, depth = 0) {
|
|
107
|
+
const text = read(shim);
|
|
108
|
+
if (text === null)
|
|
109
|
+
return { why: "is a Windows command script Pantheon could not read" };
|
|
110
|
+
if (text.length > 64 * 1024)
|
|
111
|
+
return { why: "was read but not recognised as a package manager's shim (it is too large to be one)" };
|
|
112
|
+
const dir = dirname(shim);
|
|
113
|
+
const re = /"%(?:~dp0|dp0%)\\?([^"%\r\n]+)"[ \t]*%\*[ \t]*\)?[ \t]*\r?$/gm;
|
|
114
|
+
let rel = null;
|
|
115
|
+
for (let m = re.exec(text); m; m = re.exec(text))
|
|
116
|
+
rel = m[1];
|
|
117
|
+
if (!rel)
|
|
118
|
+
return { why: "was read but not recognised as a package manager's shim" };
|
|
119
|
+
const parts = rel.split(/[\\/]+/).filter(Boolean);
|
|
120
|
+
if (!parts.length)
|
|
121
|
+
return { why: "was read but not recognised as a package manager's shim" };
|
|
122
|
+
const target = resolve(dir, ...parts);
|
|
123
|
+
if (!exists(target))
|
|
124
|
+
return { why: `was read, but what it runs is not there (${target})` };
|
|
125
|
+
if (/\.exe$/i.test(target))
|
|
126
|
+
return { command: target, args: [] };
|
|
127
|
+
if (/\.(?:cmd|bat)$/i.test(target)) {
|
|
128
|
+
if (depth >= 4)
|
|
129
|
+
return { why: "was read, but it runs shims that run shims too deep to follow" };
|
|
130
|
+
return inspectCmdShim(target, read, exists, depth + 1);
|
|
131
|
+
}
|
|
132
|
+
const js = /\.(?:c|m)?js$/i.test(target);
|
|
133
|
+
if (!js && (/\.[A-Za-z0-9]+$/.test(target) || !/\bnode(?:\.exe)?\b/i.test(text))) {
|
|
134
|
+
return { why: `was read, but it runs ${target}, which is not something Pantheon can start without cmd.exe` };
|
|
135
|
+
}
|
|
136
|
+
const localNode = join(dir, "node.exe");
|
|
137
|
+
return { command: exists(localNode) ? localNode : process.execPath, args: [target] };
|
|
138
|
+
}
|
|
139
|
+
export function resolveCmdShim(shim, read, exists) {
|
|
140
|
+
const r = inspectCmdShim(shim, read, exists);
|
|
141
|
+
return "command" in r ? r : null;
|
|
142
|
+
}
|
|
143
|
+
export function agentSpawn(binary, args, os = platform(), shim = (f) => inspectCmdShim(f)) {
|
|
88
144
|
if (!isWindows(os) || !/\.(cmd|bat)$/i.test(binary))
|
|
89
145
|
return { command: binary, args: [...args], options: {} };
|
|
146
|
+
if (!args.every(cmdSafe) || !cmdSafe(binary)) {
|
|
147
|
+
const direct = shim(binary);
|
|
148
|
+
if (!direct || !("command" in direct))
|
|
149
|
+
throw new CmdLineUnsafe(args.find((a) => !cmdSafe(a)) ?? binary, binary, direct?.why);
|
|
150
|
+
return { command: direct.command, args: [...direct.args, ...args], options: {} };
|
|
151
|
+
}
|
|
90
152
|
const line = [binary, ...args].map(cmdQuote).join(" ");
|
|
91
153
|
return {
|
|
92
154
|
command: process.env.ComSpec || "cmd.exe",
|
package/dist/workdir.js
CHANGED
|
@@ -9,6 +9,12 @@ export function gitActivity(path) {
|
|
|
9
9
|
}
|
|
10
10
|
return 0;
|
|
11
11
|
}
|
|
12
|
+
export function mentions(text, name) {
|
|
13
|
+
if (!name)
|
|
14
|
+
return false;
|
|
15
|
+
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
|
+
return new RegExp(`(^|[^A-Za-z0-9._-])${esc}(?=$|[\\/]|\\.git(?![A-Za-z0-9_-])|\\.(?![A-Za-z0-9_-])|[^A-Za-z0-9._-])`).test(text);
|
|
17
|
+
}
|
|
12
18
|
export function pickWorkdir(bindings, hints, team, activity = gitActivity, exists = existsSync) {
|
|
13
19
|
const live = bindings.filter((b) => exists(b.path));
|
|
14
20
|
if (!live.length) {
|
|
@@ -17,7 +23,7 @@ export function pickWorkdir(bindings, hints, team, activity = gitActivity, exist
|
|
|
17
23
|
note: `no folder on this machine is bound to "${team}", so this opens in your home folder. To fix it, run pantheon use ${team} --here inside the repo.`,
|
|
18
24
|
};
|
|
19
25
|
}
|
|
20
|
-
const named = live.filter((b) => hints.some((h) => (b.origin ? h
|
|
26
|
+
const named = live.filter((b) => hints.some((h) => (b.origin ? mentions(h, b.origin) : false) || mentions(h, b.path)));
|
|
21
27
|
const pool = named.length ? named : live;
|
|
22
28
|
const chosen = [...pool].sort((x, y) => (activity(y.path) - activity(x.path)) || (y.bound_at - x.bound_at))[0];
|
|
23
29
|
const others = live.filter((b) => b.path !== chosen.path).map((b) => basename(b.path));
|