agent-yes 1.108.0 → 1.109.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.
@@ -0,0 +1,8 @@
1
+ import "./ts-BJvj36Z3.js";
2
+ import "./logger-B9h0djqx.js";
3
+ import "./versionChecker-DGSlVqgt.js";
4
+ import "./pidStore-DBjlqzo8.js";
5
+ import "./globalPidIndex-yVd3mbsV.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C0AwpO1R.js";
7
+
8
+ export { SUPPORTED_CLIS };
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-D_sIq4Yv.js";
1
+ import { t as CLIS_CONFIG } from "./ts-BJvj36Z3.js";
2
2
 
3
3
  //#region ts/SUPPORTED_CLIS.ts
4
4
  const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
5
5
 
6
6
  //#endregion
7
7
  export { SUPPORTED_CLIS as t };
8
- //# sourceMappingURL=SUPPORTED_CLIS-BzuJvKuH.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-C0AwpO1R.js.map
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { n as logger } from "./logger-B9h0djqx.js";
3
- import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-osmGP6ly.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-DGSlVqgt.js";
4
4
  import { argv } from "process";
5
5
  import { execFileSync, spawn } from "child_process";
6
6
  import ms from "ms";
@@ -482,7 +482,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
482
482
  {
483
483
  const rawArg = process.argv[2];
484
484
  const isHelpFlag = rawArg === "-h" || rawArg === "--help";
485
- const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-4tanf24s.js");
485
+ const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-B-1ABS7S.js");
486
486
  if (isHelpFlag && process.argv.length === 3) {
487
487
  cmdHelp();
488
488
  process.exit(0);
@@ -515,7 +515,7 @@ if (config.useRust) {
515
515
  }
516
516
  }
517
517
  if (rustBinary) {
518
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-B_gPkhav.js");
518
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-BlZnNglM.js");
519
519
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
520
520
  if (config.verbose) {
521
521
  console.log(`[rust] Using binary: ${rustBinary}`);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-D_sIq4Yv.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-BJvj36Z3.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-osmGP6ly.js";
3
+ import "./versionChecker-DGSlVqgt.js";
4
4
  import "./pidStore-DBjlqzo8.js";
5
5
  import "./globalPidIndex-yVd3mbsV.js";
6
6
 
@@ -1,11 +1,11 @@
1
- import "./ts-D_sIq4Yv.js";
1
+ import "./ts-BJvj36Z3.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-osmGP6ly.js";
3
+ import "./versionChecker-DGSlVqgt.js";
4
4
  import "./pidStore-DBjlqzo8.js";
5
5
  import "./globalPidIndex-yVd3mbsV.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BzuJvKuH.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C0AwpO1R.js";
7
7
  import "./remotes-C3xPRtfg.js";
8
- import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-BLPtg1xN.js";
8
+ import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-CPJDMI84.js";
9
9
  import yargs from "yargs";
10
10
  import { mkdir, open, readFile, writeFile } from "fs/promises";
11
11
  import { homedir } from "os";
@@ -552,4 +552,4 @@ Options:
552
552
 
553
553
  //#endregion
554
554
  export { cmdServe };
555
- //# sourceMappingURL=serve-D4puI0b2.js.map
555
+ //# sourceMappingURL=serve-AjtjceCu.js.map
@@ -1,6 +1,6 @@
1
1
  import "./logger-B9h0djqx.js";
2
2
  import "./globalPidIndex-yVd3mbsV.js";
3
3
  import "./remotes-C3xPRtfg.js";
4
- import { a as finalizedLines, c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, h as stopTipForCli, i as cursorAbs, l as matchKeyword, m as snapshotStatus, n as cmdHelp, o as isPidAlive, p as runSubcommand, r as controlCodeFromName, s as isSubcommand, t as GRACEFUL_EXIT_COMMANDS, u as readNotes } from "./subcommands-BLPtg1xN.js";
4
+ import { a as finalizedLines, c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, h as stopTipForCli, i as cursorAbs, l as matchKeyword, m as snapshotStatus, n as cmdHelp, o as isPidAlive, p as runSubcommand, r as controlCodeFromName, s as isSubcommand, t as GRACEFUL_EXIT_COMMANDS, u as readNotes } from "./subcommands-CPJDMI84.js";
5
5
 
6
6
  export { cmdHelp, isSubcommand, runSubcommand };
@@ -163,7 +163,7 @@ async function runSubcommand(argv) {
163
163
  case "restart": return await cmdRestart(rest);
164
164
  case "note": return await cmdNote(rest);
165
165
  case "serve": {
166
- const { cmdServe } = await import("./serve-D4puI0b2.js");
166
+ const { cmdServe } = await import("./serve-AjtjceCu.js");
167
167
  return cmdServe(rest);
168
168
  }
169
169
  case "setup": {
@@ -1595,4 +1595,4 @@ async function cmdStatus(rest) {
1595
1595
 
1596
1596
  //#endregion
1597
1597
  export { finalizedLines as a, listRecords as c, renderRawLog as d, resolveOne as f, writeToIpc as g, stopTipForCli as h, cursorAbs as i, matchKeyword as l, snapshotStatus as m, cmdHelp as n, isPidAlive as o, runSubcommand as p, controlCodeFromName as r, isSubcommand as s, GRACEFUL_EXIT_COMMANDS as t, readNotes as u };
1598
- //# sourceMappingURL=subcommands-BLPtg1xN.js.map
1598
+ //# sourceMappingURL=subcommands-CPJDMI84.js.map
@@ -1,5 +1,5 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { r as getInstalledPackage } from "./versionChecker-osmGP6ly.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-DGSlVqgt.js";
3
3
  import { n as agentYesHome, t as PidStore } from "./pidStore-DBjlqzo8.js";
4
4
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CJxsoGdb.js";
5
5
  import { i as readGlobalPids } from "./globalPidIndex-yVd3mbsV.js";
@@ -1714,4 +1714,4 @@ function sleep(ms) {
1714
1714
 
1715
1715
  //#endregion
1716
1716
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1717
- //# sourceMappingURL=ts-D_sIq4Yv.js.map
1717
+ //# sourceMappingURL=ts-BJvj36Z3.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "agent-yes";
10
- var version = "1.108.0";
10
+ var version = "1.109.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -221,4 +221,4 @@ async function displayVersion() {
221
221
 
222
222
  //#endregion
223
223
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
224
- //# sourceMappingURL=versionChecker-osmGP6ly.js.map
224
+ //# sourceMappingURL=versionChecker-DGSlVqgt.js.map
@@ -28,6 +28,80 @@ export function ident(e, cap) {
28
28
  return `${c(rb.repo)}/${c(rb.branch)}`;
29
29
  }
30
30
 
31
+ // ---- device-aware identity (multi-room) -----------------------------------
32
+ // When several machines' agents are aggregated into one list, an agent's full
33
+ // identity is user@host:owner/repo/branch. The device (user@host) comes from the
34
+ // codehost peer label on `_host`; the path (owner/repo/branch) from the cwd.
35
+
36
+ // Split a codehost device label into { user, host }. "sno@taka" → both parts;
37
+ // "taka" (no @) → host only; "" / missing → both empty (a local/unknown device).
38
+ export function deviceParts(host) {
39
+ if (!host) return { user: "", host: "" };
40
+ const at = String(host).indexOf("@");
41
+ return at >= 0
42
+ ? { user: String(host).slice(0, at), host: String(host).slice(at + 1) }
43
+ : { user: "", host: String(host) };
44
+ }
45
+
46
+ // The five identity fields, in display order, for one agent.
47
+ export function identFields(e) {
48
+ const d = deviceParts(e._host);
49
+ const rb = repoBranch(e) || { owner: "", repo: "", branch: "" };
50
+ return { user: d.user, host: d.host, owner: rb.owner, repo: rb.repo, branch: rb.branch };
51
+ }
52
+
53
+ const IDENT_ORDER = ["user", "host", "owner", "repo", "branch"];
54
+
55
+ // Precompute, over the whole shown list: which fields are uniform (identical for
56
+ // every agent — so they can be omitted) and whether any device info exists at
57
+ // all (if not, we render the legacy path-only identity, no user@host: prefix).
58
+ export function identContext(entries) {
59
+ const fields = entries.map(identFields);
60
+ const uniform = {};
61
+ for (const f of IDENT_ORDER) uniform[f] = new Set(fields.map((x) => x[f])).size <= 1;
62
+ const anyDevice = fields.some((x) => x.user || x.host);
63
+ return { uniform, anyDevice };
64
+ }
65
+
66
+ // Build an agent's compact identity against a precomputed identContext. Each
67
+ // field is clipped to `cap` chars (compact one-liner) and BLANKED when uniform
68
+ // across the list — but the separators (@ : / /) are kept so the string stays
69
+ // machine-parseable: e.g. all on one device → "@:age/mai", a mixed-device list →
70
+ // "sno@tak:age/mai". A purely local list (no devices anywhere) falls back to the
71
+ // legacy "own/rep/bra" with no device prefix.
72
+ export function compactIdent(e, ctx, cap = 3) {
73
+ const m = identFields(e);
74
+ const clip = (s) => (cap && s.length > cap ? s.slice(0, cap) : s);
75
+ const v = (f) => (ctx.uniform[f] ? "" : clip(m[f]));
76
+ const path = `${v("owner")}/${v("repo")}/${v("branch")}`;
77
+ return ctx.anyDevice ? `${v("user")}@${v("host")}:${path}` : path;
78
+ }
79
+
80
+ // The full, uncapped identity for a hover title — every field shown, device
81
+ // prefix only when this agent actually has device info.
82
+ export function fullIdent(e) {
83
+ const m = identFields(e);
84
+ const path = `${m.owner}/${m.repo}/${m.branch}`;
85
+ return m.user || m.host ? `${m.user}@${m.host}:${path}` : path;
86
+ }
87
+
88
+ // True when a compact identity carries at least one real character (not just
89
+ // separators) — used to decide whether to render the identity span at all.
90
+ export function hasIdent(s) {
91
+ return /[^@:/]/.test(s || "");
92
+ }
93
+
94
+ // Count of distinct devices (user@host) present in the list. >1 means "not
95
+ // alone" → worth showing the device tag in the detailed view.
96
+ export function deviceCount(entries) {
97
+ const set = new Set();
98
+ for (const e of entries) {
99
+ const { user, host } = deviceParts(e._host);
100
+ if (user || host) set.add(user + "@" + host);
101
+ }
102
+ return set.size;
103
+ }
104
+
31
105
  // Derive codehost-style mnemonic tags from a cwd like .../ws/<owner>/<repo>/tree/<wt>.
32
106
  export function tagsFor(e) {
33
107
  const t = [];
@@ -63,6 +137,15 @@ export function matches(e, toks) {
63
137
  if (ci > 0) {
64
138
  const k = tok.slice(0, ci),
65
139
  v = tok.slice(ci + 1);
140
+ // room: / device: filter the aggregation by source and machine.
141
+ if (k === "room")
142
+ return String(e._room || "")
143
+ .toLowerCase()
144
+ .includes(v);
145
+ if (k === "device" || k === "dev")
146
+ return String(e._host || "")
147
+ .toLowerCase()
148
+ .includes(v);
66
149
  return tagsFor(e).some(([tk, tv]) => tk === k && tv.toLowerCase().includes(v));
67
150
  }
68
151
  return hay.toLowerCase().includes(tok);
package/lab/ui/index.html CHANGED
@@ -186,6 +186,19 @@
186
186
  .rooms .ritem.cur {
187
187
  box-shadow: inset 2px 0 0 var(--green);
188
188
  }
189
+ .rooms .rstat {
190
+ font-family: var(--mono);
191
+ font-size: 10.5px;
192
+ flex: none;
193
+ min-width: 18px;
194
+ }
195
+ .rooms .rstat.on {
196
+ color: var(--green);
197
+ }
198
+ .rooms .rstat.off {
199
+ color: var(--muted);
200
+ opacity: 0.6;
201
+ }
189
202
  .rooms .rname {
190
203
  font-family: var(--mono);
191
204
  color: var(--accent);
@@ -787,10 +800,15 @@
787
800
  age,
788
801
  matches,
789
802
  nextIndex,
803
+ identContext,
804
+ compactIdent,
805
+ fullIdent,
806
+ hasIdent,
807
+ deviceCount,
790
808
  } from "./console-logic.js";
791
809
 
792
810
  let entries = [];
793
- let sel = null; // selected keyword (pid as string)
811
+ let sel = null; // selected agent's composite key (room#pid)
794
812
  let es = null; // live-tail subscription closer
795
813
  let term = null; // xterm.js Terminal rendering the raw PTY stream
796
814
  let fit = null;
@@ -1072,20 +1090,14 @@
1072
1090
  : path;
1073
1091
  };
1074
1092
 
1075
- const Conn = {
1076
- rtc: null, // RTCClient when remote (ay share), null when local
1077
- ch: null, // CodehostClient when viewing a codehost room
1093
+ // ---- transports: a uniform { fetchJSON, post, subscribe } over each wire ----
1094
+ // local (same-origin ay serve), an ay-share RTCClient, or a codehost room.
1095
+ // CodehostClient already exposes this shape, so it's used as a tx directly.
1096
+ const localTx = {
1078
1097
  async fetchJSON(path) {
1079
- if (this.ch) return this.ch.fetchJSON(path);
1080
- if (this.rtc) return JSON.parse((await this.rtc.req("GET", path)).text);
1081
1098
  return (await fetch(withTok(path))).json();
1082
1099
  },
1083
1100
  async post(path, bodyObj) {
1084
- if (this.ch) return this.ch.post(path, bodyObj);
1085
- if (this.rtc) {
1086
- const r = await this.rtc.req("POST", path, JSON.stringify(bodyObj));
1087
- return { ok: r.status >= 200 && r.status < 300, text: r.text };
1088
- }
1089
1101
  const r = await fetch(withTok(path), {
1090
1102
  method: "POST",
1091
1103
  headers: { "Content-Type": "application/json" },
@@ -1093,13 +1105,27 @@
1093
1105
  });
1094
1106
  return { ok: r.ok, text: await r.text() };
1095
1107
  },
1096
- // onText gets each parsed SSE data payload (same shape as the local EventSource path).
1097
1108
  subscribe(path, onText, onOpen, onError) {
1098
- if (this.ch) return this.ch.subscribe(path, onText, onOpen, onError);
1099
- if (this.rtc) {
1109
+ const ev = new EventSource(withTok(path));
1110
+ ev.onopen = () => onOpen && onOpen();
1111
+ ev.onmessage = (e) => onText(JSON.parse(e.data));
1112
+ ev.onerror = () => onError && onError();
1113
+ return () => ev.close();
1114
+ },
1115
+ };
1116
+ function rtcTx(rtc) {
1117
+ return {
1118
+ async fetchJSON(path) {
1119
+ return JSON.parse((await rtc.req("GET", path)).text);
1120
+ },
1121
+ async post(path, bodyObj) {
1122
+ const r = await rtc.req("POST", path, JSON.stringify(bodyObj));
1123
+ return { ok: r.status >= 200 && r.status < 300, text: r.text };
1124
+ },
1125
+ subscribe(path, onText, onOpen, onError) {
1100
1126
  onOpen && onOpen();
1101
1127
  let buf = "";
1102
- return this.rtc.subscribe(path, (raw) => {
1128
+ return rtc.subscribe(path, (raw) => {
1103
1129
  buf += raw;
1104
1130
  let i;
1105
1131
  while ((i = buf.indexOf("\n\n")) >= 0) {
@@ -1113,14 +1139,66 @@
1113
1139
  }
1114
1140
  }
1115
1141
  });
1116
- }
1117
- const ev = new EventSource(withTok(path));
1118
- ev.onopen = () => onOpen && onOpen();
1119
- ev.onmessage = (e) => onText(JSON.parse(e.data));
1120
- ev.onerror = () => onError && onError();
1121
- return () => ev.close();
1122
- },
1123
- };
1142
+ },
1143
+ };
1144
+ }
1145
+
1146
+ // ---- fleet: local + every saved room, all connected at once ------------
1147
+ // Each source contributes its agents to one merged list (tagged with the
1148
+ // owning source on `_room`/`_key`); per-agent ops route back via srcFor/txFor.
1149
+ // Live counts in the rooms panel come from each source's serverCount/devices.
1150
+ const LOCAL = "local";
1151
+ const sources = new Map(); // id -> { id, host, kind, tx, client, live, devices, serverCount }
1152
+ const srcFor = (e) => (e && sources.get(e._room)) || sources.get(LOCAL) || null;
1153
+ const txFor = (e) => srcFor(e)?.tx || localTx;
1154
+
1155
+ // The local source is only worth polling when this page is actually backed
1156
+ // by an ay serve: localhost, or served by `ay serve --http` (which leaves a
1157
+ // token), or when there are no rooms to fall back on. On the public origin
1158
+ // with rooms, skip it so we don't hammer a 404 every poll.
1159
+ function ensureLocalSource() {
1160
+ const isLocalhost = ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname);
1161
+ const hasToken = !!localStorage.getItem("ay.localToken");
1162
+ const enabled = isLocalhost || hasToken || Object.keys(loadRooms()).length === 0;
1163
+ if (enabled && !sources.has(LOCAL)) {
1164
+ sources.set(LOCAL, {
1165
+ id: LOCAL,
1166
+ host: "local",
1167
+ kind: "local",
1168
+ tx: localTx,
1169
+ client: null,
1170
+ live: false,
1171
+ tried: true, // no connect phase — polled directly
1172
+ devices: new Set(),
1173
+ serverCount: 0,
1174
+ });
1175
+ } else if (!enabled) {
1176
+ sources.delete(LOCAL);
1177
+ }
1178
+ }
1179
+
1180
+ // Pull one source's agent list, tagging each row with its origin + a
1181
+ // composite key (pids can collide across rooms). Updates the source's live
1182
+ // flag, device set, and server count for the rooms panel.
1183
+ async function listSource(s) {
1184
+ try {
1185
+ const arr = await s.tx.fetchJSON("/api/ls?all=1");
1186
+ s.live = true;
1187
+ s.devices = new Set();
1188
+ const out = (Array.isArray(arr) ? arr : []).map((e) => {
1189
+ const host = e._host || "";
1190
+ if (host) s.devices.add(host);
1191
+ return { ...e, _room: s.id, _key: s.id + "#" + e.pid, _host: host };
1192
+ });
1193
+ s.serverCount =
1194
+ s.kind === "ch" ? s.client?.hosts().length || 0 : s.devices.size || (s.live ? 1 : 0);
1195
+ return out;
1196
+ } catch {
1197
+ s.live = false;
1198
+ s.serverCount = 0;
1199
+ return [];
1200
+ }
1201
+ }
1124
1202
 
1125
1203
  // Compact list: one line per agent (dot + cli + title), persisted per device.
1126
1204
  let compactList = localStorage.getItem("ay.compactList") === "1";
@@ -1130,16 +1208,21 @@
1130
1208
  const shown = entries.filter((e) => matches(e, toks));
1131
1209
  $("count").textContent = `${shown.length} / ${entries.length} agents`;
1132
1210
  $("viewbtn").classList.toggle("on", compactList);
1211
+ // identContext blanks any field uniform across the shown list (so a
1212
+ // single-device fleet shows no device); multiDevice gates the detailed
1213
+ // host tag so it only appears when machines are actually mixed.
1214
+ const ctx = identContext(shown);
1215
+ const multiDevice = deviceCount(shown) > 1;
1133
1216
  if (compactList) {
1134
1217
  $("list").innerHTML =
1135
1218
  shown
1136
1219
  .map((e) => {
1137
1220
  const t = e.title || e.prompt || "";
1138
- const id = ident(e, true);
1221
+ const id = compactIdent(e, ctx);
1139
1222
  const cli = cliLabel(e);
1140
- return `<div class="row crow ${String(e.pid) === sel ? "sel" : ""}" data-pid="${e.pid}">
1223
+ return `<div class="row crow ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
1141
1224
  <span class="dot ${esc(e.status)}"></span>
1142
- ${id ? `<span class="cident" title="${esc(ident(e))}">${esc(id)}</span>` : ""}
1225
+ ${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
1143
1226
  ${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
1144
1227
  <span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
1145
1228
  <span class="age">${age(e)}</span></div>`;
@@ -1150,13 +1233,16 @@
1150
1233
  $("list").innerHTML =
1151
1234
  shown
1152
1235
  .map((e) => {
1236
+ // The host tag only earns its place when several machines are in
1237
+ // play; otherwise it's noise (every row would carry the same one).
1153
1238
  const tags = tagsFor(e)
1239
+ .filter(([k]) => k !== "host" || multiDevice)
1154
1240
  .map(
1155
1241
  ([k, v]) =>
1156
1242
  `<span class="rtag" data-k="${k}"><span style="opacity:.55">${k}:</span>${esc(v)}</span>`,
1157
1243
  )
1158
1244
  .join("");
1159
- return `<div class="row ${String(e.pid) === sel ? "sel" : ""}" data-pid="${e.pid}">
1245
+ return `<div class="row ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
1160
1246
  <div class="r1"><span class="dot ${esc(e.status)}"></span>
1161
1247
  <span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
1162
1248
  <span class="badge">pid ${e.pid}</span>
@@ -1194,40 +1280,67 @@
1194
1280
  } catch {}
1195
1281
  }
1196
1282
 
1283
+ // Match a stored/linked selection token against an entry: either the full
1284
+ // composite key (room#pid) or a bare pid (?pid= deep links, legacy ay.sel).
1285
+ const matchSel = (e, token) => e._key === token || String(e.pid) === String(token);
1286
+
1197
1287
  async function loadList() {
1198
- const remote = Conn.ch || Conn.rtc;
1199
- try {
1200
- entries = await Conn.fetchJSON("/api/ls?all=1");
1201
- setConn(remote ? "● " + (curRoom || "remote") : "● local", "var(--green)");
1202
- } catch (e) {
1203
- setConn(remote ? "● peer down" : "● ay serve down", "var(--red)");
1288
+ const srcs = [...sources.values()];
1289
+ const lists = await Promise.all(srcs.map(listSource));
1290
+ entries = lists.flat();
1291
+ // Badge: total agents + how many rooms are live. Red only when nothing
1292
+ // at all is reachable (no source answered).
1293
+ const roomSrcs = srcs.filter((s) => s.id !== LOCAL);
1294
+ const liveRooms = roomSrcs.filter((s) => s.live).length;
1295
+ const anyLive = srcs.some((s) => s.live);
1296
+ const connecting = roomSrcs.some((s) => !s.tried);
1297
+ const n = entries.length;
1298
+ if (!srcs.length) {
1299
+ setConn("● no fleet", "var(--muted)");
1300
+ } else if (!anyLive) {
1301
+ if (connecting) setConn("● connecting…", "var(--amber)");
1302
+ else setConn(roomSrcs.length ? "● rooms offline" : "● ay serve down", "var(--red)");
1303
+ } else {
1304
+ const roomBit = liveRooms ? ` · ${liveRooms} room${liveRooms === 1 ? "" : "s"}` : "";
1305
+ setConn(`● ${n} agent${n === 1 ? "" : "s"}${roomBit}`, "var(--green)");
1204
1306
  }
1307
+ renderRoomsIfOpen();
1205
1308
  renderList();
1206
- if (autoPid && entries.some((x) => String(x.pid) === String(autoPid))) {
1207
- const pid = autoPid;
1309
+ if (autoPid && entries.some((x) => matchSel(x, autoPid))) {
1310
+ const tok = autoPid;
1208
1311
  autoPid = null;
1312
+ const e = entries.find((x) => matchSel(x, tok));
1209
1313
  // On a phone, a restored selection re-highlights the row but stays on
1210
1314
  // the list — opening it would flip straight into the full-screen
1211
1315
  // terminal (show-detail) and hide the list. An explicit ?pid= link is
1212
1316
  // a deliberate jump, so it still opens. Desktop shows both panes, so
1213
1317
  // there's nothing to scope — always open.
1214
1318
  if (autoPidExplicit || window.innerWidth > 720) {
1215
- select(pid);
1319
+ select(e._key);
1216
1320
  } else {
1217
- sel = pid;
1321
+ sel = e._key;
1218
1322
  renderList();
1219
1323
  }
1220
1324
  }
1221
1325
  }
1222
1326
 
1223
- function select(pid) {
1224
- sel = String(pid);
1327
+ function select(keyOrPid) {
1328
+ const e =
1329
+ entries.find((x) => x._key === keyOrPid) ||
1330
+ entries.find((x) => String(x.pid) === String(keyOrPid));
1331
+ if (!e) {
1332
+ sel = String(keyOrPid);
1333
+ return;
1334
+ }
1335
+ sel = e._key;
1225
1336
  // Remember the selection so a refresh re-opens this agent (see boot/autoPid).
1226
1337
  try {
1227
1338
  localStorage.setItem("ay.sel", sel);
1228
1339
  } catch {}
1229
- const e = entries.find((x) => String(x.pid) === sel);
1230
- if (!e) return;
1340
+ // pid + tx are how we talk to the agent's own host; sel (composite) is
1341
+ // only for UI identity/highlight, since pids can collide across rooms.
1342
+ const pid = e.pid;
1343
+ const tx = txFor(e);
1231
1344
  renderList();
1232
1345
  // Mobile: flip the single-column layout to the terminal ("detail") pane.
1233
1346
  // Done BEFORE term.open below so xterm measures a visible container.
@@ -1267,15 +1380,15 @@
1267
1380
  // feed it, so we just surface the latest title as the header name. Falls
1268
1381
  // back to the cli name when the agent never sets one.
1269
1382
  term.onTitleChange((t) => {
1270
- if (sel === String(e.pid) && t && t.trim()) $("rname").textContent = t.trim();
1383
+ if (sel === e._key && t && t.trim()) $("rname").textContent = t.trim();
1271
1384
  });
1272
1385
  // Adapt: drive the agent's PTY to the browser terminal size (POST
1273
1386
  // /api/resize → winsize + SIGWINCH) so its TUI reflows to match what we
1274
1387
  // render. Suppressed while we're merely adopting the agent's OWN size.
1275
1388
  let adoptingAgentSize = false;
1276
1389
  const pushSize = () => {
1277
- if (term && sel && !adoptingAgentSize)
1278
- Conn.post("/api/resize/" + encodeURIComponent(sel), {
1390
+ if (term && sel === e._key && !adoptingAgentSize)
1391
+ tx.post("/api/resize/" + encodeURIComponent(pid), {
1279
1392
  cols: term.cols,
1280
1393
  rows: term.rows,
1281
1394
  }).catch(() => {});
@@ -1287,17 +1400,18 @@
1287
1400
  // covers the UTF-8 mouse encoding (DECSET 1005). Verified end-to-end:
1288
1401
  // a drag emits \x1b[<0;..M / \x1b[<32;..M / \x1b[<0;..m, wheel \x1b[<64/65..M.
1289
1402
  const fwd = (d) => {
1290
- if (sel) Conn.post("/api/send", { keyword: sel, msg: d, code: "none" }).catch(() => {});
1403
+ if (sel === e._key)
1404
+ tx.post("/api/send", { keyword: pid, msg: d, code: "none" }).catch(() => {});
1291
1405
  };
1292
1406
  term.onData(fwd);
1293
1407
  term.onBinary(fwd);
1294
1408
  // Render the existing buffer at the AGENT's current width first so its
1295
1409
  // wrapping is correct, instead of forcing our viewport width onto stale
1296
1410
  // content. The user adapts to the window by resizing it (fit → push).
1297
- const selPid = sel;
1298
- Conn.fetchJSON("/api/size/" + encodeURIComponent(selPid))
1411
+ const selKey = e._key;
1412
+ tx.fetchJSON("/api/size/" + encodeURIComponent(pid))
1299
1413
  .then((sz) => {
1300
- if (sel !== selPid || !term) return;
1414
+ if (sel !== selKey || !term) return;
1301
1415
  if (sz && sz.cols && sz.rows) {
1302
1416
  adoptingAgentSize = true;
1303
1417
  term.resize(sz.cols, sz.rows);
@@ -1320,8 +1434,8 @@
1320
1434
  // viewer is pinned to the bottom, and cap the buffer so it can't grow forever.
1321
1435
  $("livedot").className = "dot idle";
1322
1436
  $("livetxt").textContent = "connecting…";
1323
- const close = Conn.subscribe(
1324
- "/api/tail/" + encodeURIComponent(sel) + "?raw=1",
1437
+ const close = tx.subscribe(
1438
+ "/api/tail/" + encodeURIComponent(pid) + "?raw=1",
1325
1439
  (raw) => {
1326
1440
  if (term) term.write(raw);
1327
1441
  },
@@ -1339,7 +1453,7 @@
1339
1453
 
1340
1454
  $("list").addEventListener("click", (ev) => {
1341
1455
  const row = ev.target.closest(".row");
1342
- if (row) select(row.dataset.pid);
1456
+ if (row) select(row.dataset.key);
1343
1457
  });
1344
1458
  // Mobile back button: return to the list pane. The tail keeps streaming in the
1345
1459
  // background (selection unchanged), so reopening the agent is instant.
@@ -1370,10 +1484,10 @@
1370
1484
  const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
1371
1485
  const shown = entries.filter((e) => matches(e, toks));
1372
1486
  if (!shown.length) return;
1373
- const cur = shown.findIndex((e) => String(e.pid) === sel);
1487
+ const cur = shown.findIndex((e) => e._key === sel);
1374
1488
  const next = shown[nextIndex(shown.length, cur, dir)];
1375
- select(String(next.pid));
1376
- const row = $("list").querySelector('.row[data-pid="' + next.pid + '"]');
1489
+ select(next._key);
1490
+ const row = $("list").querySelector('.row[data-key="' + CSS.escape(next._key) + '"]');
1377
1491
  if (row) row.scrollIntoView({ block: "nearest" });
1378
1492
  }
1379
1493
  // Alt+ArrowDown / Alt+ArrowUp cycles agents. Capture phase on window so it
@@ -1392,7 +1506,6 @@
1392
1506
 
1393
1507
  // ---- rooms: localStorage cache + a manager you open by clicking the badge ----
1394
1508
  const ROOMS_KEY = "ay.rooms";
1395
- let curRoom = null;
1396
1509
  const loadRooms = () => {
1397
1510
  try {
1398
1511
  return JSON.parse(localStorage.getItem(ROOMS_KEY) || "{}");
@@ -1428,57 +1541,79 @@
1428
1541
  return "ch-" + (h >>> 0).toString(36).slice(0, 4).padStart(4, "0");
1429
1542
  }
1430
1543
 
1431
- function dropConn() {
1432
- if (Conn.rtc) {
1433
- try {
1434
- Conn.rtc.pc?.close();
1435
- } catch {}
1436
- Conn.rtc = null;
1437
- }
1438
- if (Conn.ch) {
1439
- try {
1440
- Conn.ch.close();
1441
- } catch {}
1442
- Conn.ch = null;
1443
- }
1544
+ function removeSource(room) {
1545
+ const s = sources.get(room);
1546
+ if (!s) return;
1547
+ try {
1548
+ s.client?.close?.();
1549
+ s.client?.pc?.close?.();
1550
+ } catch {}
1551
+ sources.delete(room);
1444
1552
  }
1445
1553
 
1446
- async function connectRoom(room, token, host) {
1554
+ // Add a room to the fleet and connect it — WITHOUT dropping the others, so
1555
+ // every saved room streams its agents at once. Idempotent: a room already
1556
+ // in the fleet just refreshes. The connection runs in the background; the
1557
+ // next poll picks up its agents once the tunnel is open.
1558
+ async function addRoomSource(room, token, host) {
1447
1559
  host = host || SIG_DEFAULT;
1448
1560
  saveRoom(room, token, host); // cache so the badge can list & reconnect later
1561
+ if (sources.has(room)) return;
1562
+ const s = {
1563
+ id: room,
1564
+ host,
1565
+ kind: host === CH_HOST ? "ch" : "rtc",
1566
+ tx: null,
1567
+ client: null,
1568
+ live: false,
1569
+ tried: false,
1570
+ devices: new Set(),
1571
+ serverCount: 0,
1572
+ };
1573
+ sources.set(room, s);
1574
+ renderRoomsIfOpen();
1449
1575
  try {
1450
- localStorage.setItem("ay.lastRoom", room); // reconnect here on a bare open
1451
- } catch {}
1452
- curRoom = room;
1453
- dropConn();
1454
- setConn("● connecting " + room + "…", "var(--amber)");
1455
- if (host === CH_HOST) {
1456
- const c = new CodehostClient(token);
1457
- c.onstate = (s) => {
1458
- if (s === "closed") setConn("● room lost", "var(--red)");
1459
- };
1460
- try {
1576
+ if (host === CH_HOST) {
1577
+ const c = new CodehostClient(token);
1578
+ c.onstate = (st) => {
1579
+ if (st === "closed") {
1580
+ s.live = false;
1581
+ renderRoomsIfOpen();
1582
+ }
1583
+ };
1461
1584
  await c.connect();
1462
- Conn.ch = c;
1463
- } catch (e) {
1464
- setConn("● connect failed", "var(--red)");
1585
+ s.client = c;
1586
+ s.tx = c;
1587
+ } else {
1588
+ const c = new RTCClient(host, room, token);
1589
+ c.onstate = (st) => {
1590
+ if (st === "failed" || st === "closed") {
1591
+ s.live = false;
1592
+ renderRoomsIfOpen();
1593
+ }
1594
+ };
1595
+ await c.connect();
1596
+ s.client = c;
1597
+ s.tx = rtcTx(c);
1465
1598
  }
1466
- loadList();
1467
- return;
1468
- }
1469
- const c = new RTCClient(host, room, token);
1470
- c.onstate = (s) => {
1471
- if (s === "failed" || s === "closed") setConn("● peer lost", "var(--red)");
1472
- };
1473
- try {
1474
- await c.connect();
1475
- Conn.rtc = c;
1599
+ s.live = true;
1476
1600
  } catch (e) {
1477
- setConn("● connect failed", "var(--red)");
1601
+ s.live = false;
1478
1602
  }
1603
+ s.tried = true;
1604
+ renderRoomsIfOpen();
1479
1605
  loadList();
1480
1606
  }
1481
1607
 
1608
+ // Connect to every saved room at once (called on boot).
1609
+ function connectAllRooms() {
1610
+ const r = loadRooms();
1611
+ for (const name of Object.keys(r)) addRoomSource(name, r[name].token, r[name].host);
1612
+ }
1613
+
1614
+ // Back-compat alias: a freshly pasted/linked room is added like any other.
1615
+ const connectRoom = (room, token, host) => addRoomSource(room, token, host);
1616
+
1482
1617
  function parseRoomInput(s) {
1483
1618
  s = s.trim();
1484
1619
  const hash = s.indexOf("#");
@@ -1493,13 +1628,32 @@
1493
1628
  return m ? { room: m[1], token: m[2], host: m[3] } : null;
1494
1629
  }
1495
1630
 
1631
+ // Re-render the rooms panel only if it's currently open (so live-count
1632
+ // updates from connect/poll land without forcing it open).
1633
+ function renderRoomsIfOpen() {
1634
+ if ($("rooms").style.display !== "none") renderRooms();
1635
+ }
1636
+
1637
+ // A room's live state: a room is reachable whenever we can join its
1638
+ // signaling server, so "live" really means "how many serving machines are
1639
+ // in it right now". The signaling layer can't tell us the count until we've
1640
+ // connected, so we show the source's last-known serverCount.
1641
+ function roomStatus(n) {
1642
+ const s = sources.get(n);
1643
+ if (!s) return `<span class="rstat off">○</span>`;
1644
+ if (!s.live) return `<span class="rstat off" title="offline">○</span>`;
1645
+ const c = s.serverCount;
1646
+ return `<span class="rstat on" title="${c} live ${c === 1 ? "server" : "servers"}">● ${c}</span>`;
1647
+ }
1648
+
1496
1649
  function renderRooms() {
1497
1650
  const r = loadRooms();
1498
1651
  const names = Object.keys(r).sort((a, b) => r[b].ts - r[a].ts);
1499
1652
  const items = names.length
1500
1653
  ? names
1501
1654
  .map(
1502
- (n) => `<div class="ritem ${n === curRoom ? "cur" : ""}">
1655
+ (n) => `<div class="ritem ${sources.get(n)?.live ? "cur" : ""}">
1656
+ ${roomStatus(n)}
1503
1657
  <span class="rname" data-room="${esc(n)}">${esc(n)}</span>
1504
1658
  <span class="rhost">${esc(r[n].host)}</span>
1505
1659
  <span class="rx" data-del="${esc(n)}" title="forget">✕</span></div>`,
@@ -1529,17 +1683,26 @@
1529
1683
  $("rooms").addEventListener("click", (ev) => {
1530
1684
  const name = ev.target.closest(".rname");
1531
1685
  if (name) {
1686
+ // Rooms are all connected at once now; clicking one filters the list to
1687
+ // it (and re-adds it if it was forgotten/offline), rather than switching.
1532
1688
  const r = loadRooms()[name.dataset.room];
1533
1689
  if (r) {
1534
1690
  connectRoom(name.dataset.room, r.token, r.host);
1691
+ $("q").value = "room:" + name.dataset.room;
1692
+ try {
1693
+ localStorage.setItem("ay.filter", $("q").value);
1694
+ } catch {}
1695
+ renderList();
1535
1696
  $("rooms").style.display = "none";
1536
1697
  }
1537
1698
  return;
1538
1699
  }
1539
1700
  const del = ev.target.closest(".rx");
1540
1701
  if (del) {
1702
+ removeSource(del.dataset.del);
1541
1703
  dropRoom(del.dataset.del);
1542
1704
  renderRooms();
1705
+ loadList();
1543
1706
  return;
1544
1707
  }
1545
1708
  if (ev.target.id === "roomadd") {
@@ -1587,16 +1750,33 @@
1587
1750
  $("launch").style.display = "flex";
1588
1751
  }
1589
1752
 
1590
- // Spawn a new agent on the CURRENT connection (local same-origin, or the
1591
- // connected remote fleet) and select it once it registers. Shared by the
1592
- // launch-URL flow and the "+ New agent" button. Returns false on a spawn
1593
- // error (the alert is already shown), true otherwise.
1594
- async function spawnAndSelect(spec) {
1753
+ // Pick the fleet to spawn on: an explicit roomId, else the selected agent's
1754
+ // source, else local, else any live source.
1755
+ function spawnTarget(roomId) {
1756
+ return (
1757
+ sources.get(roomId) ||
1758
+ srcFor(entries.find((e) => e._key === sel)) ||
1759
+ sources.get(LOCAL) ||
1760
+ [...sources.values()].find((s) => s.live) ||
1761
+ [...sources.values()][0] ||
1762
+ null
1763
+ );
1764
+ }
1765
+
1766
+ // Spawn a new agent on the chosen fleet and select it once it registers.
1767
+ // Shared by the launch-URL flow and the "+ New agent" button. Returns false
1768
+ // on a spawn error (the alert is already shown), true otherwise.
1769
+ async function spawnAndSelect(spec, roomId) {
1595
1770
  await loadList();
1596
- // Match by "newest agent that wasn't here before" — the spawn returns the
1597
- // wrapper pid, but the agent registers under the runtime's own pid.
1598
- const before = new Set(entries.map((e) => e.pid));
1599
- const res = await Conn.post("/api/spawn", {
1771
+ const target = spawnTarget(roomId);
1772
+ if (!target) {
1773
+ alert("no fleet to launch on connect a room or run `ay serve` locally");
1774
+ return false;
1775
+ }
1776
+ // Match by "newest agent that wasn't here before" ON THIS fleet — the
1777
+ // spawn returns the wrapper pid, but the agent registers under its own.
1778
+ const before = new Set(entries.filter((e) => e._room === target.id).map((e) => e.pid));
1779
+ const res = await target.tx.post("/api/spawn", {
1600
1780
  cli: spec.cli || "claude",
1601
1781
  cwd: spec.cwd || undefined,
1602
1782
  prompt: spec.prompt || undefined,
@@ -1608,10 +1788,10 @@
1608
1788
  for (let i = 0; i < 14; i++) {
1609
1789
  await loadList();
1610
1790
  const fresh = entries
1611
- .filter((e) => !before.has(e.pid))
1791
+ .filter((e) => e._room === target.id && !before.has(e.pid))
1612
1792
  .sort((a, b) => (b.started_at || 0) - (a.started_at || 0));
1613
1793
  if (fresh.length) {
1614
- select(fresh[0].pid);
1794
+ select(fresh[0]._key);
1615
1795
  return true;
1616
1796
  }
1617
1797
  await new Promise((r) => setTimeout(r, 800));
@@ -1624,7 +1804,7 @@
1624
1804
  if (!r) return;
1625
1805
  $("launch").style.display = "none";
1626
1806
  await connectRoom(room, r.token, r.host);
1627
- await spawnAndSelect(spec);
1807
+ await spawnAndSelect(spec, room);
1628
1808
  }
1629
1809
 
1630
1810
  $("launch").addEventListener("click", (ev) => {
@@ -1641,9 +1821,11 @@
1641
1821
  // when there is one, prompt optional) → POST /api/spawn on this connection.
1642
1822
  // Always allowed: the console already controls every running agent's stdin.
1643
1823
  function showNew() {
1644
- const here = entries.find((x) => String(x.pid) === sel);
1824
+ const here = entries.find((x) => x._key === sel);
1645
1825
  const cwd = here?.cwd || "";
1646
- const where = Conn.rtc ? curRoom || "remote fleet" : "local";
1826
+ const target = spawnTarget();
1827
+ const where = target ? (target.id === LOCAL ? "local" : target.id) : "local";
1828
+ $("newform").dataset.room = target ? target.id : "";
1647
1829
  $("newform").innerHTML = `<div class="lcard">
1648
1830
  <div class="ltitle">New agent · ${esc(where)}</div>
1649
1831
  <div class="nfield"><label>CLI</label><input id="nf-cli" value="claude" spellcheck="false" autocapitalize="off" /></div>
@@ -1668,7 +1850,7 @@
1668
1850
  };
1669
1851
  go.disabled = true;
1670
1852
  go.textContent = "launching…";
1671
- const ok = await spawnAndSelect(spec);
1853
+ const ok = await spawnAndSelect(spec, $("newform").dataset.room || undefined);
1672
1854
  if (ok) {
1673
1855
  $("newform").style.display = "none";
1674
1856
  } else {
@@ -1694,6 +1876,7 @@
1694
1876
  // boot: a launch URL opens the launcher; otherwise connect from the hash (then
1695
1877
  // eat the token); a bare #room reconnects from the cached token; else local.
1696
1878
  async function boot() {
1879
+ ensureLocalSource();
1697
1880
  const raw = location.hash.replace(/^#/, "");
1698
1881
  if (raw.startsWith("launch=")) {
1699
1882
  let spec = null;
@@ -1702,8 +1885,8 @@
1702
1885
  } catch {}
1703
1886
  history.replaceState(null, document.title, location.pathname + location.search); // eat launch params
1704
1887
  if (spec) showLaunch(spec);
1705
- setConn("● local", "var(--muted)");
1706
1888
  startPolling();
1889
+ connectAllRooms();
1707
1890
  return;
1708
1891
  }
1709
1892
  // #k=<token> — local-mode auth from `ay serve --http`'s printed link.
@@ -1714,8 +1897,9 @@
1714
1897
  } catch {}
1715
1898
  // SECURITY: strip the token from the URL immediately.
1716
1899
  history.replaceState(null, document.title, location.pathname + location.search);
1717
- setConn("● local", "var(--muted)");
1900
+ ensureLocalSource();
1718
1901
  startPolling();
1902
+ connectAllRooms();
1719
1903
  return;
1720
1904
  }
1721
1905
  const h = decodeURIComponent(raw);
@@ -1749,23 +1933,13 @@
1749
1933
  } else if (bare && loadRooms()[bare[1]]) {
1750
1934
  const r = loadRooms()[bare[1]];
1751
1935
  pending = { room: bare[1], token: r.token, host: r.host };
1752
- } else if (!raw) {
1753
- // No hash → reconnect to the last-used room (or the most recent saved
1754
- // one), so opening agent-yes.com brings back your list automatically.
1755
- const rooms = loadRooms();
1756
- const names = Object.keys(rooms);
1757
- if (names.length) {
1758
- const last = localStorage.getItem("ay.lastRoom");
1759
- const pick =
1760
- last && rooms[last] ? last : names.sort((a, b) => rooms[b].ts - rooms[a].ts)[0];
1761
- pending = { room: pick, token: rooms[pick].token, host: rooms[pick].host };
1762
- }
1763
1936
  }
1764
- // Render the UI immediately and refresh on a timer; connect to a room (if
1765
- // any) in the BACKGROUND so a dead/slow cached room never blanks the page.
1766
- if (!pending) setConn("● local", "var(--muted)");
1937
+ // Render the UI immediately and refresh on a timer; connect to every
1938
+ // saved room in the BACKGROUND so a dead/slow room never blanks the page,
1939
+ // and they all stream their agents into one list at once.
1767
1940
  startPolling();
1768
1941
  if (pending) connectRoom(pending.room, pending.token, pending.host);
1942
+ connectAllRooms();
1769
1943
  }
1770
1944
 
1771
1945
  // ---- activity-gated polling + auto-reload on new deploy ----------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.108.0",
3
+ "version": "1.109.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
@@ -57,7 +57,10 @@ describe("workspaceConfig", () => {
57
57
  });
58
58
 
59
59
  it("absolute path → used as-is", () => {
60
- expect(resolveSpawnCwd("/tmp/elsewhere")).toBe("/tmp/elsewhere");
60
+ // path.resolve keeps a POSIX-absolute path on Unix but anchors it to the
61
+ // current drive on Windows (→ C:\tmp\elsewhere); compare against the same
62
+ // resolution the impl uses so the assertion holds on both (cf. "a/b" below).
63
+ expect(resolveSpawnCwd("/tmp/elsewhere")).toBe(path.resolve("/tmp/elsewhere"));
61
64
  });
62
65
 
63
66
  it("tilde path → home-based", () => {
@@ -1,8 +0,0 @@
1
- import "./ts-D_sIq4Yv.js";
2
- import "./logger-B9h0djqx.js";
3
- import "./versionChecker-osmGP6ly.js";
4
- import "./pidStore-DBjlqzo8.js";
5
- import "./globalPidIndex-yVd3mbsV.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BzuJvKuH.js";
7
-
8
- export { SUPPORTED_CLIS };