@join-pantheon/cli 0.29.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/cli.js CHANGED
@@ -794,6 +794,7 @@ async function main() {
794
794
  }
795
795
  case "relay": {
796
796
  process.on("unhandledRejection", (e) => { console.error(`! unhandled rejection: ${e?.stack ?? String(e)}`); });
797
+ process.on("uncaughtException", (e) => { console.error(`! uncaught exception (kept serving): ${e?.stack ?? String(e)}`); });
797
798
  const relay = new Relay({
798
799
  port: Number(flag("port", process.env.PORT ?? "8787")),
799
800
  host: flag("host", "0.0.0.0"),
package/dist/machine.js CHANGED
@@ -148,15 +148,22 @@ export function logFileFor(kind, proc) {
148
148
  export function dataSummary(dir) {
149
149
  if (!dir || !existsSync(dir))
150
150
  return "no ledger yet";
151
- const files = sh(`ls -1 "${dir}"/*.jsonl 2>/dev/null`);
152
- if (!files)
151
+ let files = [];
152
+ try {
153
+ files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join(dir, f));
154
+ }
155
+ catch { }
156
+ if (!files.length)
153
157
  return `${dir} (empty)`;
154
158
  let acts = 0;
155
- for (const f of files.split("\n")) {
156
- const n = sh(`wc -l < "${f}"`);
157
- acts += Number(n ?? 0);
159
+ for (const f of files) {
160
+ try {
161
+ const t = readFileSync(f, "utf8");
162
+ acts += t.split("\n").filter((l) => l.trim()).length;
163
+ }
164
+ catch { }
158
165
  }
159
- const spaces = files.split("\n").length;
166
+ const spaces = files.length;
160
167
  return `${acts} acts across ${spaces} space${spaces > 1 ? "s" : ""}`;
161
168
  }
162
169
  const defaultStopIo = (port = 8787) => ({
@@ -102,7 +102,12 @@ export function wireUp(opts, deps = {}) {
102
102
  }
103
103
  else if (opts.unattended) {
104
104
  const project = opts.unattended;
105
- const isRepo = sh(`git -C ${JSON.stringify(project)} rev-parse --is-inside-work-tree 2>/dev/null`) === "true";
105
+ const isRepo = (() => { try {
106
+ return execFileSync("git", ["-C", project, "rev-parse", "--is-inside-work-tree"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim() === "true";
107
+ }
108
+ catch {
109
+ return false;
110
+ } })();
106
111
  if (!isRepo && how === "explicit") {
107
112
  tools.push({ tool: "worker", status: `not installed — ${project} is not a git repo` });
108
113
  warnings.push(`--unattended needs a git repo to work in; pass --project <a clone>. Nothing was installed.`);
package/dist/relay.js CHANGED
@@ -47,8 +47,14 @@ function cookieVal(req, name) {
47
47
  return "";
48
48
  for (const part of raw.split(";")) {
49
49
  const eq = part.indexOf("=");
50
- if (eq > -1 && part.slice(0, eq).trim() === name)
51
- return decodeURIComponent(part.slice(eq + 1).trim());
50
+ if (eq > -1 && part.slice(0, eq).trim() === name) {
51
+ try {
52
+ return decodeURIComponent(part.slice(eq + 1).trim());
53
+ }
54
+ catch {
55
+ return "";
56
+ }
57
+ }
52
58
  }
53
59
  return "";
54
60
  }
@@ -162,7 +168,30 @@ export class Relay {
162
168
  spaces = new Map();
163
169
  dataDir;
164
170
  log;
165
- http = createServer((req, res) => this.onHttp(req, res));
171
+ http = createServer((req, res) => {
172
+ const fail = (e) => {
173
+ const bad = e instanceof TypeError || e instanceof URIError;
174
+ if (!bad)
175
+ this.log(`! http ${req.method} ${String(req.url ?? "").slice(0, 120)} failed: ${e?.message ?? e}`);
176
+ try {
177
+ if (!res.headersSent)
178
+ this.json(res, bad ? 400 : 500, { error: bad ? "bad request" : "the relay could not handle that request" });
179
+ else
180
+ res.end();
181
+ }
182
+ catch { }
183
+ };
184
+ try {
185
+ if (!URL.canParse(req.url ?? "/", "http://x"))
186
+ return fail(new TypeError("unparseable request target"));
187
+ const r = this.onHttp(req, res);
188
+ if (r && typeof r.catch === "function")
189
+ r.catch(fail);
190
+ }
191
+ catch (e) {
192
+ fail(e);
193
+ }
194
+ });
166
195
  wss = new WebSocketServer({ noServer: true, maxPayload: 256 * 1024 });
167
196
  heartbeat;
168
197
  compactedAt = new Map();
@@ -2179,7 +2208,7 @@ pantheon signup --email you@work.com --username you</code></pre>
2179
2208
  catch { }
2180
2209
  }
2181
2210
  });
2182
- ws.on("close", (code) => {
2211
+ const closeWork = (code) => {
2183
2212
  if (dbSession) {
2184
2213
  this.accounts?.endSession(dbSession);
2185
2214
  dbSession = null;
@@ -2211,7 +2240,13 @@ pantheon signup --email you@work.com --username you</code></pre>
2211
2240
  this.pushObligations(space);
2212
2241
  this.broadcast(space, { t: "roster", roster: this.roster(space) });
2213
2242
  this.log(`[${space.name}] - ${me.id}/${sessionId}`);
2214
- });
2243
+ };
2244
+ ws.on("close", (code) => { try {
2245
+ closeWork(code);
2246
+ }
2247
+ catch (e) {
2248
+ this.log(`! cleanup after a disconnect failed: ${e.message}`);
2249
+ } });
2215
2250
  ws.on("error", () => { });
2216
2251
  }
2217
2252
  sweep() {
@@ -2229,11 +2264,21 @@ pantheon signup --email you@work.com --username you</code></pre>
2229
2264
  }
2230
2265
  const now = Date.now();
2231
2266
  for (const sp of this.spaces.values()) {
2232
- if (now - (this.compactedAt.get(sp.name) ?? 0) > LIMITS.ledgerCompactEveryMs)
2233
- this.compactSpace(sp);
2234
- this.lapseClaims(sp, now);
2267
+ try {
2268
+ if (now - (this.compactedAt.get(sp.name) ?? 0) > LIMITS.ledgerCompactEveryMs)
2269
+ this.compactSpace(sp);
2270
+ this.lapseClaims(sp, now);
2271
+ }
2272
+ catch (e) {
2273
+ this.log(`! [${sp.name}] housekeeping failed: ${e.message}`);
2274
+ }
2275
+ }
2276
+ try {
2277
+ this.evictSpaces();
2278
+ }
2279
+ catch (e) {
2280
+ this.log(`! evicting idle spaces failed: ${e.message}`);
2235
2281
  }
2236
- this.evictSpaces();
2237
2282
  }
2238
2283
  lapseClaims(sp, now) {
2239
2284
  const live = new Set();
package/dist/service.js CHANGED
@@ -8,9 +8,28 @@ import { launcher } from "./install.js";
8
8
  import { CONFIG_DIR } from "./config.js";
9
9
  const HERE = dirname(fileURLToPath(import.meta.url));
10
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
+ }
11
30
  export const realSys = () => ({
12
31
  platform: platform(),
13
- exec: (c, a) => execFileSync(c, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) ?? "",
32
+ exec: guardedExecFile,
14
33
  uid: process.getuid?.() ?? 501,
15
34
  user: (() => { try {
16
35
  return userInfo().username;
@@ -239,7 +258,7 @@ export function disableService(kind, sys = realSys()) {
239
258
  }
240
259
  sys.exec("systemctl", ["--user", "disable", "--now", `pantheon-${kind}`]);
241
260
  }
242
- const realExec = (c, a) => execFileSync(c, a, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
261
+ const realExec = guardedExecFile;
243
262
  function outOf(exec, c, a) {
244
263
  try {
245
264
  return { out: String(exec(c, a) ?? ""), ok: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
5
5
  "type": "module",
6
6
  "bin": {