@join-pantheon/cli 0.28.0 → 0.29.0
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 +377 -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 +273 -54
- 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 +180 -21
- package/dist/refusal.js +29 -0
- package/dist/relay.js +686 -440
- package/dist/roster-file.js +19 -3
- package/dist/run-failure.js +18 -7
- package/dist/service.js +173 -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,28 @@
|
|
|
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 const realSys = () => ({
|
|
12
|
+
platform: platform(),
|
|
13
|
+
exec: (c, a) => execFileSync(c, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) ?? "",
|
|
14
|
+
uid: process.getuid?.() ?? 501,
|
|
15
|
+
user: (() => { try {
|
|
16
|
+
return userInfo().username;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return process.env.USER ?? "";
|
|
20
|
+
} })(),
|
|
21
|
+
});
|
|
22
|
+
export function serviceLogDir() {
|
|
23
|
+
return join(CONFIG_DIR, "logs");
|
|
24
|
+
}
|
|
25
|
+
export const serviceLogFile = (kind) => join(serviceLogDir(), `${kind}.log`);
|
|
10
26
|
const SERVICE_BASE_PATH = [
|
|
11
27
|
"/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin",
|
|
12
28
|
];
|
|
@@ -22,7 +38,7 @@ export function servicesUnavailable(plat = platform()) {
|
|
|
22
38
|
export function plistPath(kind = "worker") {
|
|
23
39
|
return join(homedir(), "Library", "LaunchAgents", `${LABEL(kind)}.plist`);
|
|
24
40
|
}
|
|
25
|
-
function unitPath(kind) {
|
|
41
|
+
export function unitPath(kind) {
|
|
26
42
|
return join(homedir(), ".config", "systemd", "user", `pantheon-${kind}.service`);
|
|
27
43
|
}
|
|
28
44
|
function xml(s) {
|
|
@@ -71,7 +87,11 @@ ${argv.map((a) => ` <string>${xml(a)}</string>`).join("\n")}
|
|
|
71
87
|
</plist>
|
|
72
88
|
`;
|
|
73
89
|
}
|
|
74
|
-
export function
|
|
90
|
+
export function systemdQuote(arg) {
|
|
91
|
+
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/%/g, "%%").replace(/\$/g, "$$$$")}"`;
|
|
92
|
+
}
|
|
93
|
+
const systemdValue = (v) => v.replace(/%/g, "%%");
|
|
94
|
+
export function systemdUnit(kind, cwd, args, path = servicePath()) {
|
|
75
95
|
const description = kind === "relay"
|
|
76
96
|
? "pantheon relay — the coordination plane for a space"
|
|
77
97
|
: kind === "worker"
|
|
@@ -83,8 +103,9 @@ After=network-online.target
|
|
|
83
103
|
|
|
84
104
|
[Service]
|
|
85
105
|
Type=simple
|
|
86
|
-
WorkingDirectory=${cwd}
|
|
87
|
-
ExecStart=${argvFor(kind, args).join(" ")}
|
|
106
|
+
WorkingDirectory=${systemdValue(cwd)}
|
|
107
|
+
ExecStart=${argvFor(kind, args).map(systemdQuote).join(" ")}
|
|
108
|
+
Environment=${systemdQuote(`PATH=${path}`)}
|
|
88
109
|
Restart=always
|
|
89
110
|
RestartSec=30
|
|
90
111
|
|
|
@@ -92,29 +113,43 @@ RestartSec=30
|
|
|
92
113
|
WantedBy=default.target
|
|
93
114
|
`;
|
|
94
115
|
}
|
|
95
|
-
export function
|
|
96
|
-
|
|
116
|
+
export function lingerEnabled(sys = realSys()) {
|
|
117
|
+
try {
|
|
118
|
+
const out = sys.exec("loginctl", ["show-user", sys.user || String(sys.uid), "--property=Linger"]);
|
|
119
|
+
const m = /Linger=(yes|no)/.exec(out);
|
|
120
|
+
return m ? m[1] === "yes" : null;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export function installService(kind, cwd, args, sys = realSys()) {
|
|
127
|
+
const noServices = servicesUnavailable(sys.platform);
|
|
97
128
|
if (noServices)
|
|
98
129
|
throw new Error(noServices.replace(/^not available — /, ""));
|
|
99
130
|
const out = [];
|
|
100
131
|
const abs = resolve(cwd);
|
|
101
|
-
const logDir =
|
|
102
|
-
|
|
103
|
-
|
|
132
|
+
const logDir = serviceLogDir();
|
|
133
|
+
if (sys.platform === "darwin") {
|
|
134
|
+
mkdirSync(logDir, { recursive: true });
|
|
104
135
|
const p = plistPath(kind);
|
|
105
136
|
mkdirSync(join(homedir(), "Library", "LaunchAgents"), { recursive: true });
|
|
106
137
|
writeFileSync(p, servicePlist(kind, abs, logDir, args));
|
|
107
138
|
out.push(` wrote ${p}`);
|
|
108
139
|
try {
|
|
109
|
-
|
|
140
|
+
sys.exec("launchctl", ["enable", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
try {
|
|
144
|
+
sys.exec("launchctl", ["unload", p]);
|
|
110
145
|
}
|
|
111
146
|
catch { }
|
|
112
147
|
try {
|
|
113
|
-
|
|
148
|
+
sys.exec("launchctl", ["load", "-w", p]);
|
|
114
149
|
out.push(` loaded — the ${kind} now starts at login and restarts if it dies`);
|
|
115
150
|
}
|
|
116
151
|
catch (e) {
|
|
117
|
-
out.push(` could not load it: ${e.message}`);
|
|
152
|
+
out.push(` could not load it: ${e.message.split("\n")[0]}`);
|
|
118
153
|
out.push(` load it yourself with: launchctl load -w ${p}`);
|
|
119
154
|
}
|
|
120
155
|
out.push(` logs: ${join(logDir, `${kind}.log`)}`);
|
|
@@ -122,15 +157,15 @@ export function installService(kind, cwd, args) {
|
|
|
122
157
|
}
|
|
123
158
|
else {
|
|
124
159
|
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
|
|
160
|
+
throw new Error(`${kind === "menubar" ? "the menu bar" : "desktop alerts"} run on macOS only, and this machine reports ${sys.platform}`);
|
|
126
161
|
}
|
|
127
162
|
const unit = unitPath(kind);
|
|
128
163
|
mkdirSync(join(homedir(), ".config", "systemd", "user"), { recursive: true });
|
|
129
164
|
writeFileSync(unit, systemdUnit(kind, abs, args));
|
|
130
165
|
out.push(` wrote ${unit}`);
|
|
131
166
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
167
|
+
sys.exec("systemctl", ["--user", "daemon-reload"]);
|
|
168
|
+
sys.exec("systemctl", ["--user", "enable", "--now", `pantheon-${kind}`]);
|
|
134
169
|
out.push(` enabled — the ${kind} now starts at login and restarts if it dies`);
|
|
135
170
|
out.push(` logs: journalctl --user -u pantheon-${kind} -f`);
|
|
136
171
|
out.push(` stop: pantheon service uninstall ${kind}`);
|
|
@@ -138,12 +173,15 @@ export function installService(kind, cwd, args) {
|
|
|
138
173
|
catch (e) {
|
|
139
174
|
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
175
|
}
|
|
176
|
+
if (lingerEnabled(sys) === false) {
|
|
177
|
+
out.push(` ! the ${kind} stops when you log out (closing an SSH session counts). To keep it running: loginctl enable-linger ${sys.user || "$USER"}`);
|
|
178
|
+
}
|
|
141
179
|
}
|
|
142
180
|
return out;
|
|
143
181
|
}
|
|
144
|
-
export function uninstallService(kind) {
|
|
182
|
+
export function uninstallService(kind, sys = realSys()) {
|
|
145
183
|
const out = [];
|
|
146
|
-
if (platform
|
|
184
|
+
if (sys.platform === "win32") {
|
|
147
185
|
const stray = unitPath(kind);
|
|
148
186
|
if (existsSync(stray)) {
|
|
149
187
|
unlinkSync(stray);
|
|
@@ -151,17 +189,22 @@ export function uninstallService(kind) {
|
|
|
151
189
|
}
|
|
152
190
|
return [` no ${kind} service installed`];
|
|
153
191
|
}
|
|
154
|
-
if (platform
|
|
192
|
+
if (sys.platform === "darwin") {
|
|
155
193
|
const p = plistPath(kind);
|
|
156
194
|
if (!existsSync(p))
|
|
157
195
|
return [` no ${kind} service installed`];
|
|
196
|
+
let stopped = false;
|
|
158
197
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
198
|
+
sys.exec("launchctl", ["bootout", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
199
|
+
stopped = true;
|
|
161
200
|
}
|
|
162
|
-
catch {
|
|
163
|
-
|
|
201
|
+
catch { }
|
|
202
|
+
try {
|
|
203
|
+
sys.exec("launchctl", ["unload", "-w", p]);
|
|
204
|
+
stopped = true;
|
|
164
205
|
}
|
|
206
|
+
catch { }
|
|
207
|
+
out.push(stopped ? " stopped" : " was not loaded");
|
|
165
208
|
unlinkSync(p);
|
|
166
209
|
out.push(` removed ${p}`);
|
|
167
210
|
}
|
|
@@ -170,44 +213,134 @@ export function uninstallService(kind) {
|
|
|
170
213
|
if (!existsSync(unit))
|
|
171
214
|
return [` no ${kind} service installed`];
|
|
172
215
|
try {
|
|
173
|
-
|
|
216
|
+
sys.exec("systemctl", ["--user", "disable", "--now", `pantheon-${kind}`]);
|
|
174
217
|
out.push(" disabled and stopped");
|
|
175
218
|
}
|
|
176
219
|
catch {
|
|
177
220
|
out.push(` could not reach systemd --user — if it runs elsewhere: systemctl --user disable --now pantheon-${kind}`);
|
|
178
221
|
}
|
|
179
222
|
unlinkSync(unit);
|
|
223
|
+
try {
|
|
224
|
+
sys.exec("systemctl", ["--user", "daemon-reload"]);
|
|
225
|
+
}
|
|
226
|
+
catch { }
|
|
180
227
|
out.push(` removed ${unit}`);
|
|
181
228
|
}
|
|
182
229
|
return out;
|
|
183
230
|
}
|
|
184
|
-
export function
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
231
|
+
export function disableService(kind, sys = realSys()) {
|
|
232
|
+
if (sys.platform === "darwin") {
|
|
233
|
+
try {
|
|
234
|
+
sys.exec("launchctl", ["bootout", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
sys.exec("launchctl", ["disable", `gui/${sys.uid}/${LABEL(kind)}`]);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
sys.exec("systemctl", ["--user", "disable", "--now", `pantheon-${kind}`]);
|
|
241
|
+
}
|
|
242
|
+
const realExec = (c, a) => execFileSync(c, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
243
|
+
function outOf(exec, c, a) {
|
|
188
244
|
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`];
|
|
245
|
+
return { out: String(exec(c, a) ?? ""), ok: true };
|
|
194
246
|
}
|
|
195
247
|
catch (e) {
|
|
196
|
-
|
|
197
|
-
|
|
248
|
+
return { out: String(e.stdout ?? ""), ok: false };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export function serviceState(kind, exec = realExec, plat = platform(), uid = process.getuid?.() ?? 501) {
|
|
252
|
+
if (plat === "darwin") {
|
|
253
|
+
const dis = outOf(exec, "launchctl", ["print-disabled", `gui/${uid}`]).out;
|
|
254
|
+
const disabled = new RegExp(`"${LABEL(kind).replace(/\./g, "\\.")}"\\s*=>\\s*(disabled|true)`).test(dis);
|
|
255
|
+
const pr = outOf(exec, "launchctl", ["print", `gui/${uid}/${LABEL(kind)}`]);
|
|
256
|
+
return { disabled, loaded: pr.ok, running: pr.ok && /^\s*state = running/m.test(pr.out) };
|
|
257
|
+
}
|
|
258
|
+
const en = outOf(exec, "systemctl", ["--user", "is-enabled", `pantheon-${kind}`]).out.trim();
|
|
259
|
+
const act = outOf(exec, "systemctl", ["--user", "is-active", `pantheon-${kind}`]).out.trim();
|
|
260
|
+
return { disabled: /^(disabled|masked)/.test(en), loaded: true, running: act === "active" || act === "activating" };
|
|
261
|
+
}
|
|
262
|
+
export function restartService(kind, exec = realExec, opts = {}) {
|
|
263
|
+
const plat = opts.platform ?? platform();
|
|
264
|
+
if (!serviceInstalled(kind, plat))
|
|
265
|
+
return [` ${kind}: not installed here`];
|
|
266
|
+
const uid = process.getuid?.() ?? 501;
|
|
267
|
+
const st = serviceState(kind, exec, plat, uid);
|
|
268
|
+
if (st.disabled)
|
|
269
|
+
return [` ${kind}: left stopped (you stopped it) — start it again: pantheon service install ${kind}`];
|
|
270
|
+
try {
|
|
271
|
+
if (plat === "darwin") {
|
|
272
|
+
if (st.loaded && !st.running)
|
|
273
|
+
return [` ${kind}: not running — left as it is (launchd starts it again on its own schedule)`];
|
|
274
|
+
if (!st.loaded) {
|
|
198
275
|
exec("launchctl", ["bootstrap", `gui/${uid}`, plistPath(kind)]);
|
|
199
276
|
return [` ${kind}: was not loaded in launchd — loaded and started on the new code`];
|
|
200
277
|
}
|
|
201
|
-
|
|
278
|
+
if (opts.reload) {
|
|
279
|
+
exec("launchctl", ["bootout", `gui/${uid}/${LABEL(kind)}`]);
|
|
280
|
+
exec("launchctl", ["bootstrap", `gui/${uid}`, plistPath(kind)]);
|
|
281
|
+
return [` ${kind}: reloaded with its new definition and restarted on the new code`];
|
|
282
|
+
}
|
|
283
|
+
exec("launchctl", ["kickstart", "-k", `gui/${uid}/${LABEL(kind)}`]);
|
|
284
|
+
return [` ${kind}: restarted on the new code`];
|
|
202
285
|
}
|
|
286
|
+
if (!st.running)
|
|
287
|
+
return [` ${kind}: not running — left as it is (it starts at login)`];
|
|
288
|
+
if (opts.reload)
|
|
289
|
+
exec("systemctl", ["--user", "daemon-reload"]);
|
|
290
|
+
exec("systemctl", ["--user", "restart", `pantheon-${kind}`]);
|
|
291
|
+
return [` ${kind}: restarted on the new code`];
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
203
294
|
return [` ${kind}: could not restart (${e.message.split("\n")[0]}) — pantheon service restart ${kind}`];
|
|
204
295
|
}
|
|
205
296
|
}
|
|
206
|
-
export function
|
|
297
|
+
export function refreshServiceDefinition(kind, plat = platform()) {
|
|
298
|
+
try {
|
|
299
|
+
if (plat === "darwin") {
|
|
300
|
+
const p = plistPath(kind);
|
|
301
|
+
if (!existsSync(p))
|
|
302
|
+
return { changed: false };
|
|
303
|
+
const xml = readFileSync(p, "utf8");
|
|
304
|
+
const log = /<key>StandardOutPath<\/key>\s*<string>([^<]*)<\/string>/.exec(xml)?.[1] ?? "";
|
|
305
|
+
const logDir = serviceLogDir();
|
|
306
|
+
if (unxml(log).startsWith(logDir + "/"))
|
|
307
|
+
return { changed: false };
|
|
308
|
+
const argv = [...(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(xml)?.[1] ?? "").matchAll(/<string>([^<]*)<\/string>/g)].map((m) => unxml(m[1]));
|
|
309
|
+
const at = argv.indexOf(kind);
|
|
310
|
+
const cwd = unxml(/<key>WorkingDirectory<\/key>\s*<string>([^<]*)<\/string>/.exec(xml)?.[1] ?? homedir());
|
|
311
|
+
if (at < 0)
|
|
312
|
+
return { changed: false };
|
|
313
|
+
mkdirSync(logDir, { recursive: true });
|
|
314
|
+
writeFileSync(p, servicePlist(kind, cwd, logDir, argv.slice(at + 1)));
|
|
315
|
+
return { changed: true, line: ` ${kind}: its log now goes to ${join(logDir, `${kind}.log`)}${log ? ` (was ${unxml(log)})` : ""}` };
|
|
316
|
+
}
|
|
317
|
+
if (plat === "win32")
|
|
318
|
+
return { changed: false };
|
|
319
|
+
const u = unitPath(kind);
|
|
320
|
+
if (!existsSync(u))
|
|
321
|
+
return { changed: false };
|
|
322
|
+
const unit = readFileSync(u, "utf8");
|
|
323
|
+
if (/^Environment="PATH=/m.test(unit))
|
|
324
|
+
return { changed: false };
|
|
325
|
+
const exec = /^ExecStart=(.*)$/m.exec(unit)?.[1] ?? "";
|
|
326
|
+
const words = exec.startsWith('"') ? [...exec.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1].replace(/\\(.)/g, "$1").replace(/%%/g, "%").replace(/\$\$/g, "$")) : exec.split(/\s+/);
|
|
327
|
+
const at = words.indexOf(kind);
|
|
328
|
+
const cwd = (/^WorkingDirectory=(.*)$/m.exec(unit)?.[1] ?? homedir()).replace(/%%/g, "%");
|
|
329
|
+
if (at < 0)
|
|
330
|
+
return { changed: false };
|
|
331
|
+
writeFileSync(u, systemdUnit(kind, cwd, words.slice(at + 1)));
|
|
332
|
+
return { changed: true, line: ` ${kind}: its unit now quotes its paths and carries your PATH` };
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
return { changed: false, line: ` ${kind}: could not refresh its service definition (${e.message.split("\n")[0]})` };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const unxml = (s) => s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
339
|
+
export function serviceInstalled(kind, plat = platform()) {
|
|
207
340
|
try {
|
|
208
|
-
if (
|
|
341
|
+
if (plat === "win32")
|
|
209
342
|
return false;
|
|
210
|
-
return existsSync(
|
|
343
|
+
return existsSync(plat === "darwin" ? plistPath(kind) : unitPath(kind));
|
|
211
344
|
}
|
|
212
345
|
catch {
|
|
213
346
|
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));
|