agent-yes 1.198.1 → 1.200.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/SUPPORTED_CLIS-D8PoWxsA.js +9 -0
- package/dist/{SUPPORTED_CLIS-C7QmgASg.js → SUPPORTED_CLIS-Dt-oEBTl.js} +2 -2
- package/dist/{agentShare-DrJKcWS3.js → agentShare-D-9ppbuL.js} +2 -2
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-DHQRGGia.js → notifyDaemon-CQZ_wPhN.js} +2 -2
- package/dist/{rustBinary-CpCcTU4t.js → rustBinary-C5SrmIUM.js} +2 -2
- package/dist/{schedule-D9xMFKNu.js → schedule-lPUt6BkE.js} +4 -4
- package/dist/{serve-XJnu31FE.js → serve-DlUJ7Hg9.js} +53 -11
- package/dist/{setup-CpXyQ-_L.js → setup-Dalhb8_0.js} +2 -2
- package/dist/{subcommands-BeUGFe4x.js → subcommands-DvXyUjWT.js} +7 -7
- package/dist/{subcommands-DMIEF50I.js → subcommands-g1kLml5I.js} +1 -1
- package/dist/{ts-nIDrsHdX.js → ts-Oyau4Eap.js} +2 -2
- package/dist/{versionChecker-BG3XuJbO.js → versionChecker-CeLrr-kQ.js} +2 -2
- package/dist/{ws-XM0AQ7Qk.js → ws-DZDjlG3o.js} +45 -23
- package/lab/ui/index.html +293 -26
- package/package.json +1 -1
- package/ts/serve.ts +52 -1
- package/ts/ws.spec.ts +72 -1
- package/ts/ws.ts +39 -16
- package/dist/SUPPORTED_CLIS-B2VMxH56.js +0 -9
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./ts-Oyau4Eap.js";
|
|
2
|
+
import "./logger-CDIsZ-Pp.js";
|
|
3
|
+
import "./versionChecker-CeLrr-kQ.js";
|
|
4
|
+
import "./pidStore-BIvsBQ8X.js";
|
|
5
|
+
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
|
+
import "./messageLog-CxrKJj77.js";
|
|
7
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dt-oEBTl.js";
|
|
8
|
+
|
|
9
|
+
export { SUPPORTED_CLIS };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-Oyau4Eap.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-
|
|
8
|
+
//# sourceMappingURL=SUPPORTED_CLIS-Dt-oEBTl.js.map
|
|
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
|
|
|
2
2
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
3
|
import "./messageLog-CxrKJj77.js";
|
|
4
4
|
import "./configShared-aKTg-sa5.js";
|
|
5
|
-
import { M as resolveOne } from "./subcommands-
|
|
5
|
+
import { M as resolveOne } from "./subcommands-DvXyUjWT.js";
|
|
6
6
|
import "./e2e-jb0Hp43q.js";
|
|
7
7
|
import "./webrtcLink-B7REGtK2.js";
|
|
8
8
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -229,4 +229,4 @@ function transformEvent(rawEvent, agentId, forwarded) {
|
|
|
229
229
|
|
|
230
230
|
//#endregion
|
|
231
231
|
export { createScopedShare, listShares, revokeAllShares, revokeShare };
|
|
232
|
-
//# sourceMappingURL=agentShare-
|
|
232
|
+
//# sourceMappingURL=agentShare-D-9ppbuL.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
|
|
3
3
|
import { n as logger } from "./logger-CDIsZ-Pp.js";
|
|
4
|
-
import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-
|
|
5
|
-
import { n as getRustBinary } from "./rustBinary-
|
|
4
|
+
import { i as versionString, n as displayVersion, t as checkAndAutoUpdate } from "./versionChecker-CeLrr-kQ.js";
|
|
5
|
+
import { n as getRustBinary } from "./rustBinary-C5SrmIUM.js";
|
|
6
6
|
import { argv } from "process";
|
|
7
7
|
import { spawn } from "child_process";
|
|
8
8
|
import ms from "ms";
|
|
@@ -285,7 +285,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
285
285
|
const rawArg = process.argv[2];
|
|
286
286
|
const managerCommands = !invokedCliName(process.argv);
|
|
287
287
|
const isHelpFlag = rawArg === "-h" || rawArg === "--help";
|
|
288
|
-
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-
|
|
288
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-g1kLml5I.js");
|
|
289
289
|
if (isHelpFlag && process.argv.length === 3) {
|
|
290
290
|
await cmdHelp(managerCommands);
|
|
291
291
|
process.exit(0);
|
|
@@ -327,7 +327,7 @@ if (config.useRust) {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
if (rustBinary) {
|
|
330
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
330
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-D8PoWxsA.js");
|
|
331
331
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
332
332
|
if (config.verbose) {
|
|
333
333
|
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-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-Oyau4Eap.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-CeLrr-kQ.js";
|
|
4
4
|
import "./pidStore-BIvsBQ8X.js";
|
|
5
5
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
6
|
import "./messageLog-CxrKJj77.js";
|
|
@@ -2,7 +2,7 @@ import { n as logger } from "./logger-CDIsZ-Pp.js";
|
|
|
2
2
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
3
|
import "./messageLog-CxrKJj77.js";
|
|
4
4
|
import "./configShared-aKTg-sa5.js";
|
|
5
|
-
import { $ as notifyDir, G as gcInboxes, J as liveWatchers, K as hostId, Q as daemonLockOwnerPath, S as listRecords, W as appendEvent, X as shouldStealLock, Y as readInbox, Z as daemonLockDir, _ as isPidAlive, c as deriveLiveState, k as renderLogTailLines, q as listInboxParents } from "./subcommands-
|
|
5
|
+
import { $ as notifyDir, G as gcInboxes, J as liveWatchers, K as hostId, Q as daemonLockOwnerPath, S as listRecords, W as appendEvent, X as shouldStealLock, Y as readInbox, Z as daemonLockDir, _ as isPidAlive, c as deriveLiveState, k as renderLogTailLines, q as listInboxParents } from "./subcommands-DvXyUjWT.js";
|
|
6
6
|
import "./e2e-jb0Hp43q.js";
|
|
7
7
|
import "./webrtcLink-B7REGtK2.js";
|
|
8
8
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -589,4 +589,4 @@ async function ensureDaemon() {
|
|
|
589
589
|
|
|
590
590
|
//#endregion
|
|
591
591
|
export { daemonStatus, ensureDaemon, requestDaemonStop, runDaemon };
|
|
592
|
-
//# sourceMappingURL=notifyDaemon-
|
|
592
|
+
//# sourceMappingURL=notifyDaemon-CQZ_wPhN.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
1
|
+
import { r as getInstalledPackage } from "./versionChecker-CeLrr-kQ.js";
|
|
2
2
|
import { execFileSync } from "child_process";
|
|
3
3
|
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
4
4
|
import { chmod, copyFile } from "fs/promises";
|
|
@@ -225,4 +225,4 @@ async function getRustBinary(options = {}) {
|
|
|
225
225
|
|
|
226
226
|
//#endregion
|
|
227
227
|
export { getRustBinary as n, findSpawnHiddenLauncher as t };
|
|
228
|
-
//# sourceMappingURL=rustBinary-
|
|
228
|
+
//# sourceMappingURL=rustBinary-C5SrmIUM.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-Oyau4Eap.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-CeLrr-kQ.js";
|
|
4
4
|
import "./pidStore-BIvsBQ8X.js";
|
|
5
5
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
6
|
import "./messageLog-CxrKJj77.js";
|
|
7
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
7
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dt-oEBTl.js";
|
|
8
8
|
import { d as resolveSpawnCwd } from "./workspaceConfig-CjaRvTjf.js";
|
|
9
9
|
import { n as liveEnv, t as ensureNodeRuntime } from "./nodeRuntime-CwNJuwH5.js";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
@@ -151,4 +151,4 @@ async function cmdSchedule(rest) {
|
|
|
151
151
|
|
|
152
152
|
//#endregion
|
|
153
153
|
export { cmdSchedule };
|
|
154
|
-
//# sourceMappingURL=schedule-
|
|
154
|
+
//# sourceMappingURL=schedule-lPUt6BkE.js.map
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-Oyau4Eap.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
4
|
-
import { t as findSpawnHiddenLauncher } from "./rustBinary-
|
|
3
|
+
import { r as getInstalledPackage } from "./versionChecker-CeLrr-kQ.js";
|
|
4
|
+
import { t as findSpawnHiddenLauncher } from "./rustBinary-C5SrmIUM.js";
|
|
5
5
|
import "./pidStore-BIvsBQ8X.js";
|
|
6
6
|
import { a as updateGlobalPidStatus } from "./globalPidIndex-CoNr7tS8.js";
|
|
7
7
|
import { r as recordInbox } from "./messageLog-CxrKJj77.js";
|
|
8
8
|
import { t as pgidForWrapper } from "./reaper-BUHCyxdF.js";
|
|
9
9
|
import "./configShared-aKTg-sa5.js";
|
|
10
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
11
|
-
import { A as renderRawLog, D as recentMessageEdges, E as readPtysize, L as snapshotStatus, M as resolveOne, O as recentReadEdges, S as listRecords, T as readNotes, U as writeToIpc, b as isUserTyping, et as TYPING_BADGE, f as extractNeedsInput, k as renderLogTailLines, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, u as extractBadges } from "./subcommands-
|
|
10
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dt-oEBTl.js";
|
|
11
|
+
import { A as renderRawLog, D as recentMessageEdges, E as readPtysize, L as snapshotStatus, M as resolveOne, O as recentReadEdges, S as listRecords, T as readNotes, U as writeToIpc, b as isUserTyping, et as TYPING_BADGE, f as extractNeedsInput, k as renderLogTailLines, l as deriveLiveStatus, o as controlCodeFromName, p as extractTaskCounts, u as extractBadges } from "./subcommands-DvXyUjWT.js";
|
|
12
12
|
import "./e2e-jb0Hp43q.js";
|
|
13
13
|
import "./webrtcLink-B7REGtK2.js";
|
|
14
14
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -22,7 +22,7 @@ import path from "path";
|
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
import { randomBytes, timingSafeEqual } from "crypto";
|
|
25
|
-
import { renameSync, watch, writeFileSync } from "node:fs";
|
|
25
|
+
import { existsSync, renameSync, watch, writeFileSync } from "node:fs";
|
|
26
26
|
|
|
27
27
|
//#region ts/terminalReply.ts
|
|
28
28
|
/**
|
|
@@ -1441,6 +1441,48 @@ Options:
|
|
|
1441
1441
|
provision: hasProvisionHook()
|
|
1442
1442
|
}
|
|
1443
1443
|
});
|
|
1444
|
+
if (req.method === "GET" && p === "/api/ws") {
|
|
1445
|
+
const ws = await import("./ws-DZDjlG3o.js");
|
|
1446
|
+
try {
|
|
1447
|
+
await ws.loadProvision();
|
|
1448
|
+
} catch (e) {
|
|
1449
|
+
return new Response(e.message, { status: 501 });
|
|
1450
|
+
}
|
|
1451
|
+
try {
|
|
1452
|
+
const { wsRoot, workspaces } = await ws.collectWorkspaces();
|
|
1453
|
+
return Response.json({
|
|
1454
|
+
schema: ws.WS_JSON_SCHEMA,
|
|
1455
|
+
wsRoot,
|
|
1456
|
+
workspaces
|
|
1457
|
+
});
|
|
1458
|
+
} catch (e) {
|
|
1459
|
+
return new Response(`workspace walk failed: ${e.message}`, { status: 500 });
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (req.method === "GET" && p === "/api/ws/status") {
|
|
1463
|
+
const dirRaw = url.searchParams.get("path");
|
|
1464
|
+
if (!dirRaw) return new Response("missing ?path=<workspace dir>", { status: 400 });
|
|
1465
|
+
const ws = await import("./ws-DZDjlG3o.js");
|
|
1466
|
+
let prov;
|
|
1467
|
+
try {
|
|
1468
|
+
prov = await ws.loadProvision();
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
return new Response(e.message, { status: 501 });
|
|
1471
|
+
}
|
|
1472
|
+
const wsRoot = prov.resolveWsRoot(getProvisionRoot());
|
|
1473
|
+
const dir = path.resolve(dirRaw);
|
|
1474
|
+
if (!ws.isPathInside(wsRoot, dir)) return new Response(`path is outside the workspace root ${wsRoot}`, { status: 400 });
|
|
1475
|
+
if (!existsSync(path.join(dir, ".git"))) return new Response(`not a workspace checkout (no .git): ${dir}`, { status: 404 });
|
|
1476
|
+
try {
|
|
1477
|
+
const workspace = await ws.workspaceStatus(dir);
|
|
1478
|
+
return Response.json({
|
|
1479
|
+
schema: ws.WS_JSON_SCHEMA,
|
|
1480
|
+
workspace
|
|
1481
|
+
});
|
|
1482
|
+
} catch (e) {
|
|
1483
|
+
return new Response(`git status failed: ${e.message}`, { status: 502 });
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1444
1486
|
if (req.method === "GET" && p === "/api/notes") {
|
|
1445
1487
|
const notes = await readNotes();
|
|
1446
1488
|
return Response.json(Object.fromEntries(notes));
|
|
@@ -1976,7 +2018,7 @@ Options:
|
|
|
1976
2018
|
const perm = body.perm ?? "r";
|
|
1977
2019
|
if (perm !== "r" && perm !== "rw") return new Response(`invalid perm ${perm} (want r or rw)`, { status: 400 });
|
|
1978
2020
|
try {
|
|
1979
|
-
const { createScopedShare } = await import("./agentShare-
|
|
2021
|
+
const { createScopedShare } = await import("./agentShare-D-9ppbuL.js");
|
|
1980
2022
|
const share = await createScopedShare({
|
|
1981
2023
|
agent: body.agent,
|
|
1982
2024
|
perm,
|
|
@@ -1991,12 +2033,12 @@ Options:
|
|
|
1991
2033
|
}
|
|
1992
2034
|
}
|
|
1993
2035
|
if (req.method === "GET" && p === "/api/shares") {
|
|
1994
|
-
const { listShares } = await import("./agentShare-
|
|
2036
|
+
const { listShares } = await import("./agentShare-D-9ppbuL.js");
|
|
1995
2037
|
return Response.json(listShares());
|
|
1996
2038
|
}
|
|
1997
2039
|
const revokeM = /^\/api\/share\/([^/]+)$/.exec(p);
|
|
1998
2040
|
if (req.method === "DELETE" && revokeM) {
|
|
1999
|
-
const { revokeShare } = await import("./agentShare-
|
|
2041
|
+
const { revokeShare } = await import("./agentShare-D-9ppbuL.js");
|
|
2000
2042
|
const ok = revokeShare(decodeURIComponent(revokeM[1]));
|
|
2001
2043
|
return new Response(ok ? "revoked" : "no such share", { status: ok ? 200 : 404 });
|
|
2002
2044
|
}
|
|
@@ -2178,7 +2220,7 @@ Options:
|
|
|
2178
2220
|
const shutdown = (resolve) => {
|
|
2179
2221
|
if (heartbeat) clearInterval(heartbeat);
|
|
2180
2222
|
closeShare?.();
|
|
2181
|
-
import("./agentShare-
|
|
2223
|
+
import("./agentShare-D-9ppbuL.js").then((m) => m.revokeAllShares()).catch(() => {});
|
|
2182
2224
|
server?.stop();
|
|
2183
2225
|
resolve();
|
|
2184
2226
|
};
|
|
@@ -2191,4 +2233,4 @@ Options:
|
|
|
2191
2233
|
|
|
2192
2234
|
//#endregion
|
|
2193
2235
|
export { cmdServe };
|
|
2194
|
-
//# sourceMappingURL=serve-
|
|
2236
|
+
//# sourceMappingURL=serve-DlUJ7Hg9.js.map
|
|
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
|
|
|
32
32
|
if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
|
|
33
33
|
if (noShare) return 0;
|
|
34
34
|
process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
|
|
35
|
-
const { cmdServe } = await import("./serve-
|
|
35
|
+
const { cmdServe } = await import("./serve-DlUJ7Hg9.js");
|
|
36
36
|
return cmdServe([
|
|
37
37
|
"install",
|
|
38
38
|
"--share",
|
|
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
|
|
|
42
42
|
|
|
43
43
|
//#endregion
|
|
44
44
|
export { cmdSetup };
|
|
45
|
-
//# sourceMappingURL=setup-
|
|
45
|
+
//# sourceMappingURL=setup-Dalhb8_0.js.map
|
|
@@ -1184,19 +1184,19 @@ async function runSubcommand(argv) {
|
|
|
1184
1184
|
case "restart": return await cmdRestart(rest);
|
|
1185
1185
|
case "note": return await cmdNote(rest);
|
|
1186
1186
|
case "serve": {
|
|
1187
|
-
const { cmdServe } = await import("./serve-
|
|
1187
|
+
const { cmdServe } = await import("./serve-DlUJ7Hg9.js");
|
|
1188
1188
|
return cmdServe(rest);
|
|
1189
1189
|
}
|
|
1190
1190
|
case "setup": {
|
|
1191
|
-
const { cmdSetup } = await import("./setup-
|
|
1191
|
+
const { cmdSetup } = await import("./setup-Dalhb8_0.js");
|
|
1192
1192
|
return cmdSetup(rest);
|
|
1193
1193
|
}
|
|
1194
1194
|
case "ws": {
|
|
1195
|
-
const { cmdWs } = await import("./ws-
|
|
1195
|
+
const { cmdWs } = await import("./ws-DZDjlG3o.js");
|
|
1196
1196
|
return cmdWs(rest);
|
|
1197
1197
|
}
|
|
1198
1198
|
case "schedule": {
|
|
1199
|
-
const { cmdSchedule } = await import("./schedule-
|
|
1199
|
+
const { cmdSchedule } = await import("./schedule-lPUt6BkE.js");
|
|
1200
1200
|
return cmdSchedule(rest);
|
|
1201
1201
|
}
|
|
1202
1202
|
case "remote": {
|
|
@@ -4216,7 +4216,7 @@ async function cmdNotify(rest) {
|
|
|
4216
4216
|
}
|
|
4217
4217
|
const ensure = async () => {
|
|
4218
4218
|
if (!argv["ensure-daemon"]) return;
|
|
4219
|
-
const { ensureDaemon } = await import("./notifyDaemon-
|
|
4219
|
+
const { ensureDaemon } = await import("./notifyDaemon-CQZ_wPhN.js");
|
|
4220
4220
|
await ensureDaemon().catch(() => null);
|
|
4221
4221
|
};
|
|
4222
4222
|
await heartbeatWatcher(parent, selfStartedAt);
|
|
@@ -4290,7 +4290,7 @@ async function cmdNotifyCursor(args) {
|
|
|
4290
4290
|
}
|
|
4291
4291
|
async function cmdNotifyd(rest) {
|
|
4292
4292
|
const sub = rest[0] ?? "status";
|
|
4293
|
-
const daemon = await import("./notifyDaemon-
|
|
4293
|
+
const daemon = await import("./notifyDaemon-CQZ_wPhN.js");
|
|
4294
4294
|
switch (sub) {
|
|
4295
4295
|
case "run": return daemon.runDaemon();
|
|
4296
4296
|
case "once": return daemon.runDaemon({ once: true });
|
|
@@ -4317,4 +4317,4 @@ async function cmdNotifyd(rest) {
|
|
|
4317
4317
|
|
|
4318
4318
|
//#endregion
|
|
4319
4319
|
export { notifyDir as $, renderRawLog as A, submitAndConfirm as B, matchKeyword as C, recentMessageEdges as D, readPtysize as E, restartHintLines as F, gcInboxes as G, writeKeysPaced as H, runSubcommand as I, liveWatchers as J, hostId as K, snapshotStatus as L, resolveOne as M, resolveReadWindow as N, recentReadEdges as O, resolveResumeArgs as P, daemonLockOwnerPath as Q, stdinActivityPath as R, listRecords as S, readNotes as T, writeToIpc as U, waitForLogQuiet as V, appendEvent as W, shouldStealLock as X, readInbox as Y, daemonLockDir as Z, isPidAlive as _, cmdHelp as a, isUserTyping as b, deriveLiveState as c, extractMenu as d, TYPING_BADGE as et, extractNeedsInput as f, isExitRequest as g, isAgentStuck as h, backoffWhileTyping as i, renderRawLogLines as j, renderLogTailLines as k, deriveLiveStatus as l, finalizedLines as m, READ_PAGE_DEFAULT as n, controlCodeFromName as o, extractTaskCounts as p, listInboxParents as q, TYPING_WINDOW_MS as r, cursorAbs as s, GRACEFUL_EXIT_COMMANDS as t, extractBadges as u, isSlashCommand as v, menuSelectKeys as w, lastStdinAt as x, isSubcommand as y, stopTipForCli as z };
|
|
4320
|
-
//# sourceMappingURL=subcommands-
|
|
4320
|
+
//# sourceMappingURL=subcommands-DvXyUjWT.js.map
|
|
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
|
|
|
2
2
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
3
|
import "./messageLog-CxrKJj77.js";
|
|
4
4
|
import "./configShared-aKTg-sa5.js";
|
|
5
|
-
import { A as renderRawLog, B as submitAndConfirm, C as matchKeyword, D as recentMessageEdges, E as readPtysize, F as restartHintLines, H as writeKeysPaced, I as runSubcommand, L as snapshotStatus, M as resolveOne, N as resolveReadWindow, O as recentReadEdges, P as resolveResumeArgs, R as stdinActivityPath, S as listRecords, T as readNotes, U as writeToIpc, V as waitForLogQuiet, _ as isPidAlive, a as cmdHelp, b as isUserTyping, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as renderRawLogLines, k as renderLogTailLines, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as menuSelectKeys, x as lastStdinAt, y as isSubcommand, z as stopTipForCli } from "./subcommands-
|
|
5
|
+
import { A as renderRawLog, B as submitAndConfirm, C as matchKeyword, D as recentMessageEdges, E as readPtysize, F as restartHintLines, H as writeKeysPaced, I as runSubcommand, L as snapshotStatus, M as resolveOne, N as resolveReadWindow, O as recentReadEdges, P as resolveResumeArgs, R as stdinActivityPath, S as listRecords, T as readNotes, U as writeToIpc, V as waitForLogQuiet, _ as isPidAlive, a as cmdHelp, b as isUserTyping, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as renderRawLogLines, k as renderLogTailLines, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as menuSelectKeys, x as lastStdinAt, y as isSubcommand, z as stopTipForCli } from "./subcommands-DvXyUjWT.js";
|
|
6
6
|
import "./e2e-jb0Hp43q.js";
|
|
7
7
|
import "./webrtcLink-B7REGtK2.js";
|
|
8
8
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-CeLrr-kQ.js";
|
|
3
3
|
import { t as agentYesHome } from "./agentYesHome-CtHb5b71.js";
|
|
4
4
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CNMl13dC.js";
|
|
5
5
|
import { t as PidStore } from "./pidStore-BIvsBQ8X.js";
|
|
@@ -1901,4 +1901,4 @@ function sleep(ms) {
|
|
|
1901
1901
|
|
|
1902
1902
|
//#endregion
|
|
1903
1903
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1904
|
-
//# sourceMappingURL=ts-
|
|
1904
|
+
//# sourceMappingURL=ts-Oyau4Eap.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.
|
|
10
|
+
var version = "1.200.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-CeLrr-kQ.js.map
|
|
@@ -2,7 +2,7 @@ import "./logger-CDIsZ-Pp.js";
|
|
|
2
2
|
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
3
|
import "./messageLog-CxrKJj77.js";
|
|
4
4
|
import "./configShared-aKTg-sa5.js";
|
|
5
|
-
import { S as listRecords } from "./subcommands-
|
|
5
|
+
import { S as listRecords } from "./subcommands-DvXyUjWT.js";
|
|
6
6
|
import "./e2e-jb0Hp43q.js";
|
|
7
7
|
import "./webrtcLink-B7REGtK2.js";
|
|
8
8
|
import "./remotes-Cim0dBU7.js";
|
|
@@ -208,12 +208,12 @@ function parseFlags(args, known) {
|
|
|
208
208
|
positional
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Data API shared by `ay ws ls` and serve's `GET /api/ws`: every workspace
|
|
213
|
+
* under the standard layout with its live-agent count, optionally joined with
|
|
214
|
+
* git status (bounded concurrency — readStatus can block on git's timeout).
|
|
215
|
+
*/
|
|
216
|
+
async function collectWorkspaces(opts) {
|
|
217
217
|
const prov = await loadProvision();
|
|
218
218
|
const wsRoot = prov.resolveWsRoot(getProvisionRoot());
|
|
219
219
|
const [bare, records] = await Promise.all([walkWorkspaces(wsRoot), listRecords(void 0, {
|
|
@@ -227,7 +227,7 @@ async function cmdWsLs(args) {
|
|
|
227
227
|
...w,
|
|
228
228
|
agents: { live: liveAgentsIn(records, w.path).length }
|
|
229
229
|
}));
|
|
230
|
-
if (
|
|
230
|
+
if (opts?.status) entries = await mapBounded(entries, STATUS_CONCURRENCY, async (e) => {
|
|
231
231
|
try {
|
|
232
232
|
return {
|
|
233
233
|
...e,
|
|
@@ -241,6 +241,18 @@ async function cmdWsLs(args) {
|
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
243
|
});
|
|
244
|
+
return {
|
|
245
|
+
wsRoot,
|
|
246
|
+
workspaces: entries
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function cmdWsLs(args) {
|
|
250
|
+
const { flags, positional } = parseFlags(args, {
|
|
251
|
+
status: "bool",
|
|
252
|
+
json: "bool"
|
|
253
|
+
});
|
|
254
|
+
if (positional.length > 0) throw new Error(`ws ls takes no positional args`);
|
|
255
|
+
const { wsRoot, workspaces: entries } = await collectWorkspaces({ status: !!flags.status });
|
|
244
256
|
if (flags.json) {
|
|
245
257
|
process.stdout.write(JSON.stringify({
|
|
246
258
|
schema: WS_JSON_SCHEMA,
|
|
@@ -277,20 +289,16 @@ function gitSummary(e) {
|
|
|
277
289
|
if (!g.hasUpstream) parts.push("no-upstream");
|
|
278
290
|
return parts.length ? parts.join(", ") : "clean";
|
|
279
291
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (flags.path && flags.spec) throw new Error("--path and --spec are mutually exclusive");
|
|
287
|
-
if (positional.length > 1) throw new Error("ws status takes at most one target");
|
|
292
|
+
/**
|
|
293
|
+
* One workspace's WsEntry by checkout dir — git status + live-agent count, the
|
|
294
|
+
* owner/repo/branch back-derived from the layout when `spec` isn't provided.
|
|
295
|
+
* Shared by `ay ws status` and serve's `GET /api/ws/status`.
|
|
296
|
+
*/
|
|
297
|
+
async function workspaceStatus(dir, spec) {
|
|
288
298
|
const prov = await loadProvision();
|
|
289
|
-
const wsRoot = getProvisionRoot();
|
|
290
|
-
const { dir, spec } = await resolveOperand(prov, positional[0] ?? ".", flags.path ? "path" : flags.spec ? "spec" : "auto", wsRoot);
|
|
291
299
|
const git = await prov.readStatus(dir);
|
|
292
300
|
const layoutSpec = spec ?? (() => {
|
|
293
|
-
const root = prov.resolveWsRoot(
|
|
301
|
+
const root = prov.resolveWsRoot(getProvisionRoot());
|
|
294
302
|
if (!isPathInside(root, dir)) return null;
|
|
295
303
|
const segs = path.relative(root, dir).split(path.sep);
|
|
296
304
|
return segs.length >= 4 && segs[2] === "tree" ? {
|
|
@@ -299,7 +307,7 @@ async function cmdWsStatus(args) {
|
|
|
299
307
|
branch: segs.slice(3).join("/")
|
|
300
308
|
} : null;
|
|
301
309
|
})();
|
|
302
|
-
|
|
310
|
+
return {
|
|
303
311
|
owner: layoutSpec?.owner ?? "",
|
|
304
312
|
repo: layoutSpec?.repo ?? "",
|
|
305
313
|
branch: layoutSpec?.branch ?? git.branch,
|
|
@@ -313,6 +321,19 @@ async function cmdWsStatus(args) {
|
|
|
313
321
|
}), dir).length },
|
|
314
322
|
git
|
|
315
323
|
};
|
|
324
|
+
}
|
|
325
|
+
async function cmdWsStatus(args) {
|
|
326
|
+
const { flags, positional } = parseFlags(args, {
|
|
327
|
+
json: "bool",
|
|
328
|
+
path: "bool",
|
|
329
|
+
spec: "bool"
|
|
330
|
+
});
|
|
331
|
+
if (flags.path && flags.spec) throw new Error("--path and --spec are mutually exclusive");
|
|
332
|
+
if (positional.length > 1) throw new Error("ws status takes at most one target");
|
|
333
|
+
const prov = await loadProvision();
|
|
334
|
+
const wsRoot = getProvisionRoot();
|
|
335
|
+
const { dir, spec } = await resolveOperand(prov, positional[0] ?? ".", flags.path ? "path" : flags.spec ? "spec" : "auto", wsRoot);
|
|
336
|
+
const entry = await workspaceStatus(dir, spec);
|
|
316
337
|
if (flags.json) {
|
|
317
338
|
process.stdout.write(JSON.stringify({
|
|
318
339
|
schema: WS_JSON_SCHEMA,
|
|
@@ -320,7 +341,8 @@ async function cmdWsStatus(args) {
|
|
|
320
341
|
}, null, 2) + "\n");
|
|
321
342
|
return 0;
|
|
322
343
|
}
|
|
323
|
-
|
|
344
|
+
const git = entry.git;
|
|
345
|
+
process.stdout.write(`${dir}\n` + (entry.owner ? ` spec: ${entry.owner}/${entry.repo}@${entry.branch}\n` : "") + ` branch: ${git.branch} @ ${git.head}\n state: ${gitSummary(entry)}\n agents: ${entry.agents.live} live\n`);
|
|
324
346
|
if (entry.agents.live > 0) process.stderr.write(`\n ay ls --cwd ${dir}\n`);
|
|
325
347
|
return 0;
|
|
326
348
|
}
|
|
@@ -395,5 +417,5 @@ async function cmdWs(args) {
|
|
|
395
417
|
}
|
|
396
418
|
|
|
397
419
|
//#endregion
|
|
398
|
-
export { cmdWs };
|
|
399
|
-
//# sourceMappingURL=ws-
|
|
420
|
+
export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, workspaceStatus };
|
|
421
|
+
//# sourceMappingURL=ws-DZDjlG3o.js.map
|
package/lab/ui/index.html
CHANGED
|
@@ -1184,6 +1184,15 @@
|
|
|
1184
1184
|
color: var(--peer);
|
|
1185
1185
|
flex: none;
|
|
1186
1186
|
}
|
|
1187
|
+
.pinmark {
|
|
1188
|
+
font-size: 10px;
|
|
1189
|
+
line-height: 1;
|
|
1190
|
+
flex: none;
|
|
1191
|
+
}
|
|
1192
|
+
/* Pinned rows: a subtle accent rail on the left edge (both list layouts). */
|
|
1193
|
+
.row.pinned {
|
|
1194
|
+
box-shadow: inset 2px 0 0 var(--accent);
|
|
1195
|
+
}
|
|
1187
1196
|
.r1 {
|
|
1188
1197
|
display: flex;
|
|
1189
1198
|
align-items: center;
|
|
@@ -2077,7 +2086,7 @@
|
|
|
2077
2086
|
<div class="omnibox">
|
|
2078
2087
|
<input
|
|
2079
2088
|
id="omni-input"
|
|
2080
|
-
placeholder="Search agents by title, then output…"
|
|
2089
|
+
placeholder="Search agents by title, then output… · "/" commands · /ws workspaces"
|
|
2081
2090
|
spellcheck="false"
|
|
2082
2091
|
autocapitalize="off"
|
|
2083
2092
|
autocomplete="off"
|
|
@@ -4094,10 +4103,9 @@
|
|
|
4094
4103
|
term && term.clearSelection && term.clearSelection();
|
|
4095
4104
|
} catch {}
|
|
4096
4105
|
}
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
const selText = term && term.hasSelection && term.hasSelection() ? term.getSelection() : "";
|
|
4106
|
+
// Build an empty context-menu shell plus `addItem`/`addSep` helpers, shared
|
|
4107
|
+
// by the terminal menu and the left-panel agent menu.
|
|
4108
|
+
function ctxMenuShell() {
|
|
4101
4109
|
const menu = document.createElement("div");
|
|
4102
4110
|
menu.className = "ctxmenu";
|
|
4103
4111
|
menu.setAttribute("role", "menu");
|
|
@@ -4127,6 +4135,28 @@
|
|
|
4127
4135
|
s.className = "ctxmenu-sep";
|
|
4128
4136
|
menu.appendChild(s);
|
|
4129
4137
|
};
|
|
4138
|
+
return { menu, addItem, addSep };
|
|
4139
|
+
}
|
|
4140
|
+
// Append a built menu at the pointer, clamp into the viewport, and wire the
|
|
4141
|
+
// shared dismiss listeners (outside-click, Escape, blur, resize).
|
|
4142
|
+
function openCtxMenu(menu, ev) {
|
|
4143
|
+
document.body.appendChild(menu);
|
|
4144
|
+
ctxMenuEl = menu;
|
|
4145
|
+
const r = menu.getBoundingClientRect();
|
|
4146
|
+
const x = Math.max(4, Math.min(ev.clientX, window.innerWidth - r.width - 4));
|
|
4147
|
+
const y = Math.max(4, Math.min(ev.clientY, window.innerHeight - r.height - 4));
|
|
4148
|
+
menu.style.left = x + "px";
|
|
4149
|
+
menu.style.top = y + "px";
|
|
4150
|
+
document.addEventListener("pointerdown", onCtxOutside, true);
|
|
4151
|
+
document.addEventListener("keydown", onCtxKey, true);
|
|
4152
|
+
window.addEventListener("blur", hideTermMenu);
|
|
4153
|
+
window.addEventListener("resize", hideTermMenu);
|
|
4154
|
+
}
|
|
4155
|
+
function showTermMenu(ev, term) {
|
|
4156
|
+
ev.preventDefault();
|
|
4157
|
+
hideTermMenu();
|
|
4158
|
+
const selText = term && term.hasSelection && term.hasSelection() ? term.getSelection() : "";
|
|
4159
|
+
const { menu, addItem, addSep } = ctxMenuShell();
|
|
4130
4160
|
|
|
4131
4161
|
// Copy: disabled with nothing selected (mirrors the old behavior, minus
|
|
4132
4162
|
// the auto-copy — the user now chooses it explicitly).
|
|
@@ -4158,18 +4188,20 @@
|
|
|
4158
4188
|
addSep();
|
|
4159
4189
|
addItem("Edit quick commands…", {}, editQuickCmds);
|
|
4160
4190
|
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4191
|
+
openCtxMenu(menu, ev);
|
|
4192
|
+
}
|
|
4193
|
+
// Right-click menu for a left-panel agent row: pin/unpin to the top of the
|
|
4194
|
+
// list (a pin also surfaces the agent in /r/'s screen-fixed palette).
|
|
4195
|
+
function showAgentMenu(ev, key) {
|
|
4196
|
+
ev.preventDefault();
|
|
4197
|
+
hideTermMenu();
|
|
4198
|
+
const e = entries.find((x) => x._key === key);
|
|
4199
|
+
if (!e) return;
|
|
4200
|
+
const { menu, addItem } = ctxMenuShell();
|
|
4201
|
+
const name = cliLabel(e) || ident(e) || "pid " + e.pid;
|
|
4202
|
+
if (isPinned(key)) addItem("Unpin “" + name + "”", { hint: "📌" }, () => togglePin(key));
|
|
4203
|
+
else addItem("Pin “" + name + "” to top", { hint: "📌" }, () => togglePin(key));
|
|
4204
|
+
openCtxMenu(menu, ev);
|
|
4173
4205
|
}
|
|
4174
4206
|
// Lightweight editor: overlay + textarea of `Label | template` lines. Save
|
|
4175
4207
|
// writes localStorage; Reset restores the built-in defaults.
|
|
@@ -4575,14 +4607,55 @@
|
|
|
4575
4607
|
let sortMode = localStorage.getItem("ay.sortMode") || "state";
|
|
4576
4608
|
if (!SORT_MODES.includes(sortMode)) sortMode = "state";
|
|
4577
4609
|
|
|
4610
|
+
// Pinned agents: a set of agent _key values that float to the top of the
|
|
4611
|
+
// list, shared (same localStorage key) with /r/, where they also surface in
|
|
4612
|
+
// the screen-fixed palette. Kept in memory and re-synced on cross-tab writes.
|
|
4613
|
+
const AY_PINNED_KEY = "ay.pinned";
|
|
4614
|
+
function loadPinned() {
|
|
4615
|
+
try {
|
|
4616
|
+
const a = JSON.parse(localStorage.getItem(AY_PINNED_KEY) || "[]");
|
|
4617
|
+
return new Set(Array.isArray(a) ? a : []);
|
|
4618
|
+
} catch {
|
|
4619
|
+
return new Set();
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
let pinned = loadPinned();
|
|
4623
|
+
function isPinned(key) {
|
|
4624
|
+
return pinned.has(key);
|
|
4625
|
+
}
|
|
4626
|
+
function togglePin(key) {
|
|
4627
|
+
if (!key) return;
|
|
4628
|
+
if (pinned.has(key)) pinned.delete(key);
|
|
4629
|
+
else pinned.add(key);
|
|
4630
|
+
try {
|
|
4631
|
+
localStorage.setItem(AY_PINNED_KEY, JSON.stringify([...pinned]));
|
|
4632
|
+
} catch {}
|
|
4633
|
+
renderList();
|
|
4634
|
+
}
|
|
4635
|
+
// Cross-tab / cross-view sync: /r/ (or another /w/ tab) writing ay.pinned
|
|
4636
|
+
// fires a storage event here; reload the set and re-render.
|
|
4637
|
+
window.addEventListener("storage", (ev) => {
|
|
4638
|
+
if (ev.key === AY_PINNED_KEY) {
|
|
4639
|
+
pinned = loadPinned();
|
|
4640
|
+
renderList();
|
|
4641
|
+
}
|
|
4642
|
+
});
|
|
4643
|
+
|
|
4578
4644
|
// The filtered + sorted entry list, in the SAME order the left panel renders
|
|
4579
4645
|
// and the keyboard nav steps. Sorting runs before layeredRows builds the
|
|
4580
4646
|
// room/peer/agent tree, so it reorders roots/siblings without breaking nesting.
|
|
4647
|
+
// Pinned entries then float to the front of their sorted group (a pinned root
|
|
4648
|
+
// rises to the top; a pinned subagent rises among its siblings).
|
|
4581
4649
|
function orderedEntries(toks) {
|
|
4582
|
-
|
|
4650
|
+
const sorted = sortEntries(
|
|
4583
4651
|
entries.filter((e) => matches(e, toks)),
|
|
4584
4652
|
sortMode,
|
|
4585
4653
|
);
|
|
4654
|
+
if (!pinned.size) return sorted;
|
|
4655
|
+
const hi = [],
|
|
4656
|
+
lo = [];
|
|
4657
|
+
for (const e of sorted) (pinned.has(e._key) ? hi : lo).push(e);
|
|
4658
|
+
return hi.concat(lo);
|
|
4586
4659
|
}
|
|
4587
4660
|
|
|
4588
4661
|
// A small git chip (±changed ⑂pins ⊙sub-dirty ↑ahead ↓behind) for a row,
|
|
@@ -4672,9 +4745,16 @@
|
|
|
4672
4745
|
(peers ? " peerfocus" : "") +
|
|
4673
4746
|
(flash ? " stdinflash" : "") +
|
|
4674
4747
|
(sent ? " sendflash" : "") +
|
|
4675
|
-
(recv ? " recvflash" : "")
|
|
4748
|
+
(recv ? " recvflash" : "") +
|
|
4749
|
+
(pinned.has(e._key) ? " pinned" : "")
|
|
4676
4750
|
);
|
|
4677
4751
|
}
|
|
4752
|
+
// A small 📌 marker on a pinned row (right-click to unpin).
|
|
4753
|
+
function pinMarkHtml(e) {
|
|
4754
|
+
return pinned.has(e._key)
|
|
4755
|
+
? `<span class="pinmark" title="pinned — right-click to unpin">📌</span>`
|
|
4756
|
+
: "";
|
|
4757
|
+
}
|
|
4678
4758
|
// A yellow chip showing how many peers are watching this agent ("" if none).
|
|
4679
4759
|
function peerChipHtml(e) {
|
|
4680
4760
|
const n = focusByKey.get(e._key) || 0;
|
|
@@ -4726,6 +4806,7 @@
|
|
|
4726
4806
|
const cli = cliLabel(e);
|
|
4727
4807
|
return `<div class="row crow ${e._key === sel ? "sel" : ""}${rowFlags(e)}" data-key="${esc(e._key)}">
|
|
4728
4808
|
${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}
|
|
4809
|
+
${pinMarkHtml(e)}
|
|
4729
4810
|
<span class="dot ${esc(e.status)}"></span>
|
|
4730
4811
|
${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
|
|
4731
4812
|
${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
|
|
@@ -4756,7 +4837,7 @@
|
|
|
4756
4837
|
)
|
|
4757
4838
|
.join("");
|
|
4758
4839
|
return `<div class="row ${e._key === sel ? "sel" : ""}${rowFlags(e)}" data-key="${esc(e._key)}">
|
|
4759
|
-
<div class="r1">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}<span class="dot ${esc(e.status)}"></span>
|
|
4840
|
+
<div class="r1">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}${pinMarkHtml(e)}<span class="dot ${esc(e.status)}"></span>
|
|
4760
4841
|
<span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
|
|
4761
4842
|
<span class="badge">pid ${e.pid}</span>
|
|
4762
4843
|
${foldChipHtml(e)}
|
|
@@ -5179,6 +5260,11 @@
|
|
|
5179
5260
|
const row = ev.target.closest(".row");
|
|
5180
5261
|
if (row) select(row.dataset.key);
|
|
5181
5262
|
});
|
|
5263
|
+
// Right-click an agent row → pin/unpin menu.
|
|
5264
|
+
$("list").addEventListener("contextmenu", (ev) => {
|
|
5265
|
+
const row = ev.target.closest(".row");
|
|
5266
|
+
if (row && row.dataset.key) showAgentMenu(ev, row.dataset.key);
|
|
5267
|
+
});
|
|
5182
5268
|
// Mobile back: return to the list pane. The tail keeps streaming in the
|
|
5183
5269
|
// background (selection unchanged), so reopening the agent is instant.
|
|
5184
5270
|
const goBack = () => document.querySelector(".app").classList.remove("show-detail");
|
|
@@ -5434,16 +5520,19 @@
|
|
|
5434
5520
|
// the omnibox falls through to its normal search/spawn behaviour.
|
|
5435
5521
|
function omniCommandRows(q) {
|
|
5436
5522
|
const target = omniAnchor();
|
|
5437
|
-
if (!target) return [];
|
|
5438
5523
|
// Match on the FIRST token so a command can carry args after it
|
|
5439
5524
|
// (e.g. "/fork fix the auth bug" → cmd "fork", topic "fix the auth bug").
|
|
5440
5525
|
const parts = q.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
5441
5526
|
const cmdTerm = (parts[0] || "").toLowerCase(); // command token after "/"
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5527
|
+
// Agent-targeted commands need an open agent to act on; /ws (below)
|
|
5528
|
+
// doesn't, so the target check gates only these rows, not the builder.
|
|
5529
|
+
const rows = !target
|
|
5530
|
+
? []
|
|
5531
|
+
: OMNI_COMMANDS.filter((c) => c.cmd.slice(1).startsWith(cmdTerm)).map((c) => ({
|
|
5532
|
+
kind: "cmd",
|
|
5533
|
+
cmd: c,
|
|
5534
|
+
target,
|
|
5535
|
+
}));
|
|
5447
5536
|
// "/fork [topic]" — a slash alias for the fork spawn row so fork is reachable
|
|
5448
5537
|
// straight from the command palette (⌘K → "/fork"), not only via the row that
|
|
5449
5538
|
// auto-appends to a search. It's a spawn (kind:"fork"), not a kind:"cmd" that
|
|
@@ -5463,9 +5552,124 @@
|
|
|
5463
5552
|
src: src._src,
|
|
5464
5553
|
});
|
|
5465
5554
|
}
|
|
5555
|
+
// "/ws" discoverability — typing "/" (or "/w") lists the workspace
|
|
5556
|
+
// browser next to the agent commands; a full "/ws…" query is intercepted
|
|
5557
|
+
// in runOmni before this builder runs. Needs no anchor: workspaces exist
|
|
5558
|
+
// independently of any open agent.
|
|
5559
|
+
if ("ws".startsWith(cmdTerm)) rows.push({ kind: "wshint" });
|
|
5466
5560
|
return rows;
|
|
5467
5561
|
}
|
|
5468
5562
|
|
|
5563
|
+
// ---- "/ws" — workspace browser (⌘K → /ws) ------------------------------
|
|
5564
|
+
// Lists every workspace under each connected host's codehost layout
|
|
5565
|
+
// (<wsRoot>/<owner>/<repo>/tree/<branch>) via GET /api/ws — including ones
|
|
5566
|
+
// with NO live agents, which the agent list can never show. Enter spawns a
|
|
5567
|
+
// new agent in the highlighted workspace; a "/ws owner/repo[@branch]" query
|
|
5568
|
+
// that parses as a source adds a provision row (POST /api/spawn {from},
|
|
5569
|
+
// gated server-side by the provision hook/allowlist). Fetched at most once
|
|
5570
|
+
// per TTL from every writable host; hosts without the endpoint (older
|
|
5571
|
+
// daemon) or without codehost (501) just drop out of the merge.
|
|
5572
|
+
const OMNI_WS_TTL = 30_000;
|
|
5573
|
+
let omniWsCache = null; // { at, items: [{ ws, srcId, host }] }
|
|
5574
|
+
let omniWsFetchSeq = 0;
|
|
5575
|
+
let omniWsFetching = false;
|
|
5576
|
+
function omniWsFetch() {
|
|
5577
|
+
const seq = ++omniWsFetchSeq;
|
|
5578
|
+
omniWsFetching = true;
|
|
5579
|
+
const srcs = [...sources.values()].filter((s) => s.tx?.fetchJSON && s.live && !s.readonly);
|
|
5580
|
+
Promise.all(
|
|
5581
|
+
srcs.map(async (s) => {
|
|
5582
|
+
try {
|
|
5583
|
+
const r = await s.tx.fetchJSON("/api/ws");
|
|
5584
|
+
const host = s.deviceLabel || (s.id === LOCAL ? "local" : s.room || s.id);
|
|
5585
|
+
return (r?.workspaces || []).map((ws) => ({ ws, srcId: s.id, host }));
|
|
5586
|
+
} catch {
|
|
5587
|
+
return []; // 404 (older daemon) / 501 (no codehost) / transport error
|
|
5588
|
+
}
|
|
5589
|
+
}),
|
|
5590
|
+
).then((all) => {
|
|
5591
|
+
if (seq !== omniWsFetchSeq) return; // superseded by a newer fetch
|
|
5592
|
+
omniWsFetching = false;
|
|
5593
|
+
omniWsCache = { at: Date.now(), items: all.flat() };
|
|
5594
|
+
// Repaint only if the user is still looking at a /ws view.
|
|
5595
|
+
if (omniOpen() && /^\/ws(\s|$)/i.test($("omni-input").value.trim())) runOmni();
|
|
5596
|
+
});
|
|
5597
|
+
}
|
|
5598
|
+
const wsSpecOf = (w) => `${w.owner}/${w.repo}@${w.branch}`;
|
|
5599
|
+
// JS twin of ts/ws.ts gitSummary — keep the vocabulary identical.
|
|
5600
|
+
function wsGitSummary(ws) {
|
|
5601
|
+
if (ws.gitError) return "error: " + ws.gitError;
|
|
5602
|
+
const g = ws.git;
|
|
5603
|
+
if (!g) return "";
|
|
5604
|
+
const parts = [];
|
|
5605
|
+
if (g.dirty) parts.push("dirty");
|
|
5606
|
+
if (g.ahead > 0) parts.push("ahead " + g.ahead);
|
|
5607
|
+
if (g.behind > 0) parts.push("behind " + g.behind);
|
|
5608
|
+
if (!g.hasUpstream) parts.push("no-upstream");
|
|
5609
|
+
return parts.length ? parts.join(", ") : "clean";
|
|
5610
|
+
}
|
|
5611
|
+
function omniWsRows(q) {
|
|
5612
|
+
const arg = q.replace(/^\/ws\s*/i, "").trim();
|
|
5613
|
+
if (!omniWsCache) {
|
|
5614
|
+
if (!omniWsFetching) omniWsFetch();
|
|
5615
|
+
return [{ kind: "wsinfo", text: "loading workspaces…" }];
|
|
5616
|
+
}
|
|
5617
|
+
// Stale cache: refresh behind the (still rendered) cached view.
|
|
5618
|
+
if (Date.now() - omniWsCache.at > OMNI_WS_TTL && !omniWsFetching) omniWsFetch();
|
|
5619
|
+
const toks = arg.toLowerCase().split(/\s+/).filter(Boolean);
|
|
5620
|
+
const rows = omniWsCache.items
|
|
5621
|
+
.filter((it) =>
|
|
5622
|
+
toks.every((t) =>
|
|
5623
|
+
(wsSpecOf(it.ws) + " " + it.ws.path + " " + it.host).toLowerCase().includes(t),
|
|
5624
|
+
),
|
|
5625
|
+
)
|
|
5626
|
+
.sort(
|
|
5627
|
+
(a, b) =>
|
|
5628
|
+
(b.ws.agents?.live || 0) - (a.ws.agents?.live || 0) ||
|
|
5629
|
+
wsSpecOf(a.ws).localeCompare(wsSpecOf(b.ws)),
|
|
5630
|
+
)
|
|
5631
|
+
.slice(0, 30)
|
|
5632
|
+
.map((it) => ({ kind: "ws", ws: it.ws, srcId: it.srcId, host: it.host }));
|
|
5633
|
+
// A single owner/repo[@branch] token that parses as a source gets a
|
|
5634
|
+
// provision row even when similar workspaces exist (the user may want a
|
|
5635
|
+
// branch that isn't checked out yet).
|
|
5636
|
+
if (/^[\w.-]+\/[\w.-]+(@[\w./-]+)?$/.test(arg)) rows.push({ kind: "wsnew", spec: arg });
|
|
5637
|
+
if (!rows.length)
|
|
5638
|
+
rows.push({
|
|
5639
|
+
kind: "wsinfo",
|
|
5640
|
+
text: omniWsCache.items.length
|
|
5641
|
+
? "no workspaces match — /ws owner/repo@branch provisions a new one"
|
|
5642
|
+
: "no workspaces on any connected host (layout <wsRoot>/<owner>/<repo>/tree/<branch>)",
|
|
5643
|
+
});
|
|
5644
|
+
return rows;
|
|
5645
|
+
}
|
|
5646
|
+
// Lazy git status for the HIGHLIGHTED workspace only — a full-status join
|
|
5647
|
+
// is O(workspaces × git status) and measured in seconds, so state loads
|
|
5648
|
+
// per-row on selection, debounced like the terminal preview. Results land
|
|
5649
|
+
// on the CACHED ws object, so they survive re-filtering keystrokes.
|
|
5650
|
+
let omniWsStatusTimer = null;
|
|
5651
|
+
function omniWsStatusPeek() {
|
|
5652
|
+
const row = omniRows[omniIdx];
|
|
5653
|
+
if (!row || row.kind !== "ws" || row.ws.git !== undefined || row.ws._st) return;
|
|
5654
|
+
if (omniWsStatusTimer) clearTimeout(omniWsStatusTimer);
|
|
5655
|
+
omniWsStatusTimer = setTimeout(async () => {
|
|
5656
|
+
if (!omniOpen()) return;
|
|
5657
|
+
row.ws._st = 1;
|
|
5658
|
+
const s = sources.get(row.srcId);
|
|
5659
|
+
if (!s?.tx?.fetchJSON) return;
|
|
5660
|
+
try {
|
|
5661
|
+
const r = await s.tx.fetchJSON(
|
|
5662
|
+
"/api/ws/status?path=" + encodeURIComponent(row.ws.path),
|
|
5663
|
+
);
|
|
5664
|
+
if (r?.workspace) Object.assign(row.ws, r.workspace);
|
|
5665
|
+
else row.ws.git = null;
|
|
5666
|
+
} catch {
|
|
5667
|
+
row.ws.git = null; // no status chip — the row stays useful without it
|
|
5668
|
+
}
|
|
5669
|
+
if (omniOpen() && omniRows.includes(row)) renderOmni();
|
|
5670
|
+
}, 150);
|
|
5671
|
+
}
|
|
5672
|
+
|
|
5469
5673
|
// Which agent the highlighted candidate should preview in the background: an
|
|
5470
5674
|
// agent row → that agent; the spawn row / an empty list → the launch-time
|
|
5471
5675
|
// agent (so the backdrop shows the spawn target's cwd).
|
|
@@ -5552,6 +5756,39 @@
|
|
|
5552
5756
|
<div class="omni-sub">⏎ to choose a working dir · prompt: ${esc(q || "(empty)")}</div>
|
|
5553
5757
|
</div></div>`;
|
|
5554
5758
|
}
|
|
5759
|
+
if (r.kind === "wshint") {
|
|
5760
|
+
return `<div class="omni-row omni-spawn${sc}" data-i="${i}">
|
|
5761
|
+
<span class="dot active"></span>
|
|
5762
|
+
<div class="omni-main">
|
|
5763
|
+
<div class="omni-title">▤ Workspaces <span style="opacity:.55">/ws</span></div>
|
|
5764
|
+
<div class="omni-sub">browse every workspace on the fleet — spawn into idle checkouts · ⏎ to open</div>
|
|
5765
|
+
</div></div>`;
|
|
5766
|
+
}
|
|
5767
|
+
if (r.kind === "wsinfo") {
|
|
5768
|
+
return `<div class="omni-row${sc}" data-i="${i}">
|
|
5769
|
+
<div class="omni-main"><div class="omni-sub">${esc(r.text)}</div></div></div>`;
|
|
5770
|
+
}
|
|
5771
|
+
if (r.kind === "ws") {
|
|
5772
|
+
const live = r.ws.agents?.live || 0;
|
|
5773
|
+
const st = wsGitSummary(r.ws);
|
|
5774
|
+
const multi = sources.size > 1;
|
|
5775
|
+
return `<div class="omni-row omni-spawn${sc}" data-i="${i}">
|
|
5776
|
+
<span class="dot ${live ? "active" : "exited"}"></span>
|
|
5777
|
+
<div class="omni-main">
|
|
5778
|
+
<div class="omni-title">▤ ${esc(wsSpecOf(r.ws))}${
|
|
5779
|
+
live ? ` <span style="opacity:.7">· ${live} agent${live > 1 ? "s" : ""}</span>` : ""
|
|
5780
|
+
}${multi ? ` <span style="opacity:.55">@ ${esc(r.host)}</span>` : ""}</div>
|
|
5781
|
+
<div class="omni-sub">${esc(r.ws.path)}${st ? " · <b>" + esc(st) + "</b>" : ""} · ⏎ spawn agent here</div>
|
|
5782
|
+
</div></div>`;
|
|
5783
|
+
}
|
|
5784
|
+
if (r.kind === "wsnew") {
|
|
5785
|
+
return `<div class="omni-row omni-spawn${sc}" data-i="${i}">
|
|
5786
|
+
<span class="dot active"></span>
|
|
5787
|
+
<div class="omni-main">
|
|
5788
|
+
<div class="omni-title">⊕ Provision workspace <b>${esc(r.spec)}</b> & spawn ${esc(anchor?.cli || "claude")}</div>
|
|
5789
|
+
<div class="omni-sub">clone/refresh via codehost — gated by the host's provision hook/allowlist</div>
|
|
5790
|
+
</div></div>`;
|
|
5791
|
+
}
|
|
5555
5792
|
if (r.kind === "cmd") {
|
|
5556
5793
|
const t = r.target;
|
|
5557
5794
|
const dz = r.cmd.danger ? " danger" : "";
|
|
@@ -5576,6 +5813,7 @@
|
|
|
5576
5813
|
$("omni-results").innerHTML = html || `<div class="omni-empty">no matches</div>`;
|
|
5577
5814
|
const selrow = $("omni-results").querySelector(".omni-row.sel");
|
|
5578
5815
|
if (selrow) selrow.scrollIntoView({ block: "nearest" });
|
|
5816
|
+
omniWsStatusPeek(); // lazy git state for a highlighted /ws row (no-op otherwise)
|
|
5579
5817
|
}
|
|
5580
5818
|
|
|
5581
5819
|
function runOmni() {
|
|
@@ -5583,6 +5821,16 @@
|
|
|
5583
5821
|
// A "/…" query is a slash command against the open agent, not a search —
|
|
5584
5822
|
// show the matching command rows and skip the agent/spawn/tail machinery.
|
|
5585
5823
|
if (q.startsWith("/")) {
|
|
5824
|
+
// "/ws …" opens the workspace browser — its rows come from /api/ws,
|
|
5825
|
+
// not the agent list, and it must never fall through to agent search
|
|
5826
|
+
// (the query is a workspace filter, not a spawn prompt).
|
|
5827
|
+
if (/^\/ws(\s|$)/i.test(q)) {
|
|
5828
|
+
if (omniTailTimer) clearTimeout(omniTailTimer);
|
|
5829
|
+
omniRows = omniWsRows(q);
|
|
5830
|
+
omniIdx = 0;
|
|
5831
|
+
renderOmni();
|
|
5832
|
+
return;
|
|
5833
|
+
}
|
|
5586
5834
|
const cmds = omniCommandRows(q);
|
|
5587
5835
|
if (cmds.length) {
|
|
5588
5836
|
if (omniTailTimer) clearTimeout(omniTailTimer);
|
|
@@ -5691,6 +5939,25 @@
|
|
|
5691
5939
|
await row.cmd.run(row.target);
|
|
5692
5940
|
return;
|
|
5693
5941
|
}
|
|
5942
|
+
// /ws rows — handled BEFORE the ⌘⏎ spawn-here fallthrough: in the
|
|
5943
|
+
// workspace browser the typed text is a filter, not a prompt, so
|
|
5944
|
+
// "spawn here with this prompt" is never the right reading of it.
|
|
5945
|
+
if (row && row.kind === "wshint") {
|
|
5946
|
+
$("omni-input").value = "/ws ";
|
|
5947
|
+
runOmni();
|
|
5948
|
+
return; // stays open — the browser replaces the command list
|
|
5949
|
+
}
|
|
5950
|
+
if (row && row.kind === "wsinfo") return; // informational, not activatable
|
|
5951
|
+
if (row && row.kind === "ws") {
|
|
5952
|
+
closeOmni(false);
|
|
5953
|
+
await spawnAndSelect({ cli: anchor?.cli || "claude", cwd: row.ws.path }, row.srcId);
|
|
5954
|
+
return;
|
|
5955
|
+
}
|
|
5956
|
+
if (row && row.kind === "wsnew") {
|
|
5957
|
+
closeOmni(false);
|
|
5958
|
+
await spawnAndSelect({ cli: anchor?.cli || "claude", from: row.spec }, anchor?._src);
|
|
5959
|
+
return;
|
|
5960
|
+
}
|
|
5694
5961
|
// ⌘⏎ always = spawn-here (the fast default), regardless of the selected row.
|
|
5695
5962
|
if (forceSpawn || (row && row.kind === "spawn")) {
|
|
5696
5963
|
closeOmni(false); // committing — keep the bg as-is, spawnAndSelect takes over
|
package/package.json
CHANGED
package/ts/serve.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
|
|
2
|
-
import { renameSync, watch, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, renameSync, watch, writeFileSync } from "node:fs";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
@@ -2026,6 +2026,57 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2026
2026
|
});
|
|
2027
2027
|
}
|
|
2028
2028
|
|
|
2029
|
+
// GET /api/ws — every workspace under the codehost layout
|
|
2030
|
+
// (<wsRoot>/<owner>/<repo>/tree/<branch>) with live-agent counts. This is
|
|
2031
|
+
// what the console's ⌘K "/ws" browser renders: unlike /api/ls it also shows
|
|
2032
|
+
// workspaces with NO agents in them, so you can spawn into an idle checkout.
|
|
2033
|
+
// Read-only and cheap (a directory walk, no git); per-workspace git state is
|
|
2034
|
+
// fetched lazily via /api/ws/status below. 501 mirrors the fork path when
|
|
2035
|
+
// codehost/provision isn't installed on this host.
|
|
2036
|
+
if (req.method === "GET" && p === "/api/ws") {
|
|
2037
|
+
const ws = await import("./ws.ts");
|
|
2038
|
+
try {
|
|
2039
|
+
await ws.loadProvision();
|
|
2040
|
+
} catch (e) {
|
|
2041
|
+
return new Response((e as Error).message, { status: 501 });
|
|
2042
|
+
}
|
|
2043
|
+
try {
|
|
2044
|
+
const { wsRoot, workspaces } = await ws.collectWorkspaces();
|
|
2045
|
+
return Response.json({ schema: ws.WS_JSON_SCHEMA, wsRoot, workspaces });
|
|
2046
|
+
} catch (e) {
|
|
2047
|
+
return new Response(`workspace walk failed: ${(e as Error).message}`, { status: 500 });
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// GET /api/ws/status?path=<dir> — one workspace's git state (dirty / ahead /
|
|
2052
|
+
// behind / no-upstream) + live-agent count. The path must sit inside the
|
|
2053
|
+
// workspace root: this endpoint runs `git status` on the directory, and a
|
|
2054
|
+
// console client with the share token is not necessarily a host admin, so it
|
|
2055
|
+
// must not be able to probe arbitrary host paths.
|
|
2056
|
+
if (req.method === "GET" && p === "/api/ws/status") {
|
|
2057
|
+
const dirRaw = url.searchParams.get("path");
|
|
2058
|
+
if (!dirRaw) return new Response("missing ?path=<workspace dir>", { status: 400 });
|
|
2059
|
+
const ws = await import("./ws.ts");
|
|
2060
|
+
let prov: Awaited<ReturnType<typeof ws.loadProvision>>;
|
|
2061
|
+
try {
|
|
2062
|
+
prov = await ws.loadProvision();
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
return new Response((e as Error).message, { status: 501 });
|
|
2065
|
+
}
|
|
2066
|
+
const wsRoot = prov.resolveWsRoot(getProvisionRoot());
|
|
2067
|
+
const dir = path.resolve(dirRaw);
|
|
2068
|
+
if (!ws.isPathInside(wsRoot, dir))
|
|
2069
|
+
return new Response(`path is outside the workspace root ${wsRoot}`, { status: 400 });
|
|
2070
|
+
if (!existsSync(path.join(dir, ".git")))
|
|
2071
|
+
return new Response(`not a workspace checkout (no .git): ${dir}`, { status: 404 });
|
|
2072
|
+
try {
|
|
2073
|
+
const workspace = await ws.workspaceStatus(dir);
|
|
2074
|
+
return Response.json({ schema: ws.WS_JSON_SCHEMA, workspace });
|
|
2075
|
+
} catch (e) {
|
|
2076
|
+
return new Response(`git status failed: ${(e as Error).message}`, { status: 502 });
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2029
2080
|
// GET /api/notes
|
|
2030
2081
|
if (req.method === "GET" && p === "/api/notes") {
|
|
2031
2082
|
const notes = await readNotes();
|
package/ts/ws.spec.ts
CHANGED
|
@@ -2,7 +2,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import { mkdtempSync, mkdirSync, realpathSync, writeFileSync, symlinkSync, rmSync } from "fs";
|
|
3
3
|
import os from "os";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
cmdWs,
|
|
7
|
+
collectWorkspaces,
|
|
8
|
+
isPathInside,
|
|
9
|
+
resolveOperand,
|
|
10
|
+
walkWorkspaces,
|
|
11
|
+
workspaceStatus,
|
|
12
|
+
WS_JSON_SCHEMA,
|
|
13
|
+
} from "./ws.ts";
|
|
6
14
|
|
|
7
15
|
// Mutable fake for codehost/provision — each test overrides what it needs.
|
|
8
16
|
// ws.ts imports the module lazily; vitest's registry mock intercepts that too.
|
|
@@ -433,3 +441,66 @@ describe("cmdWs (mocked provision)", () => {
|
|
|
433
441
|
}
|
|
434
442
|
});
|
|
435
443
|
});
|
|
444
|
+
|
|
445
|
+
// Data API consumed by serve's GET /api/ws and /api/ws/status (the console's
|
|
446
|
+
// ⌘K /ws browser) — same mocked provision, no stdout involved.
|
|
447
|
+
describe("collectWorkspaces / workspaceStatus (mocked provision)", () => {
|
|
448
|
+
let root: string;
|
|
449
|
+
let homeBackup: string | undefined;
|
|
450
|
+
|
|
451
|
+
beforeEach(() => {
|
|
452
|
+
root = realpathSync(mkdtempSync(path.join(os.tmpdir(), "ay-ws-api-")));
|
|
453
|
+
prov.wsRoot = root;
|
|
454
|
+
prov.readStatus
|
|
455
|
+
.mockReset()
|
|
456
|
+
.mockResolvedValue({ branch: "main", head: "abc123", ahead: 1, behind: 0, dirty: true, hasUpstream: true });
|
|
457
|
+
homeBackup = process.env.AGENT_YES_HOME;
|
|
458
|
+
process.env.AGENT_YES_HOME = path.join(root, ".ay-home");
|
|
459
|
+
});
|
|
460
|
+
afterEach(() => {
|
|
461
|
+
if (homeBackup === undefined) delete process.env.AGENT_YES_HOME;
|
|
462
|
+
else process.env.AGENT_YES_HOME = homeBackup;
|
|
463
|
+
rmSync(root, { recursive: true, force: true });
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const mkCheckout = (rel: string) => {
|
|
467
|
+
const dir = path.join(root, rel);
|
|
468
|
+
mkdirSync(path.join(dir, ".git"), { recursive: true });
|
|
469
|
+
return dir;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
it("collectWorkspaces: entries without git by default, joined when status:true", async () => {
|
|
473
|
+
const dir = mkCheckout("o/r/tree/main");
|
|
474
|
+
const bare = await collectWorkspaces();
|
|
475
|
+
expect(bare.wsRoot).toBe(root);
|
|
476
|
+
expect(bare.workspaces).toEqual([
|
|
477
|
+
{ owner: "o", repo: "r", branch: "main", path: dir, agents: { live: 0 } },
|
|
478
|
+
]);
|
|
479
|
+
expect(prov.readStatus).not.toHaveBeenCalled();
|
|
480
|
+
|
|
481
|
+
const withStatus = await collectWorkspaces({ status: true });
|
|
482
|
+
expect(withStatus.workspaces[0]!.git).toMatchObject({ dirty: true, ahead: 1 });
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("workspaceStatus: back-derives the layout spec (slash branch) and counts agents", async () => {
|
|
486
|
+
const dir = mkCheckout("o/r/tree/feat/x");
|
|
487
|
+
const entry = await workspaceStatus(dir);
|
|
488
|
+
expect(entry).toMatchObject({
|
|
489
|
+
owner: "o",
|
|
490
|
+
repo: "r",
|
|
491
|
+
branch: "feat/x",
|
|
492
|
+
path: dir,
|
|
493
|
+
agents: { live: 0 },
|
|
494
|
+
});
|
|
495
|
+
expect(entry.git).toMatchObject({ dirty: true });
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("workspaceStatus: a dir outside the layout keeps git's branch, no spec", async () => {
|
|
499
|
+
const dir = path.join(root, "elsewhere");
|
|
500
|
+
mkdirSync(path.join(dir, ".git"), { recursive: true });
|
|
501
|
+
prov.wsRoot = path.join(root, "not-here");
|
|
502
|
+
const entry = await workspaceStatus(dir);
|
|
503
|
+
expect(entry.owner).toBe("");
|
|
504
|
+
expect(entry.branch).toBe("main"); // from readStatus, not the layout
|
|
505
|
+
});
|
|
506
|
+
});
|
package/ts/ws.ts
CHANGED
|
@@ -69,7 +69,7 @@ interface Provision {
|
|
|
69
69
|
}): Promise<ProvisionResult>;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
async function loadProvision(): Promise<Provision> {
|
|
72
|
+
export async function loadProvision(): Promise<Provision> {
|
|
73
73
|
try {
|
|
74
74
|
return (await import("codehost/provision")) as unknown as Provision;
|
|
75
75
|
} catch (e) {
|
|
@@ -298,9 +298,14 @@ function parseFlags(
|
|
|
298
298
|
// subcommands
|
|
299
299
|
// ---------------------------------------------------------------------------
|
|
300
300
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Data API shared by `ay ws ls` and serve's `GET /api/ws`: every workspace
|
|
303
|
+
* under the standard layout with its live-agent count, optionally joined with
|
|
304
|
+
* git status (bounded concurrency — readStatus can block on git's timeout).
|
|
305
|
+
*/
|
|
306
|
+
export async function collectWorkspaces(opts?: {
|
|
307
|
+
status?: boolean;
|
|
308
|
+
}): Promise<{ wsRoot: string; workspaces: WsEntry[] }> {
|
|
304
309
|
const prov = await loadProvision();
|
|
305
310
|
const wsRoot = prov.resolveWsRoot(getProvisionRoot());
|
|
306
311
|
|
|
@@ -314,7 +319,7 @@ async function cmdWsLs(args: string[]): Promise<number> {
|
|
|
314
319
|
agents: { live: liveAgentsIn(records, w.path).length },
|
|
315
320
|
}));
|
|
316
321
|
|
|
317
|
-
if (
|
|
322
|
+
if (opts?.status) {
|
|
318
323
|
entries = await mapBounded(entries, STATUS_CONCURRENCY, async (e) => {
|
|
319
324
|
try {
|
|
320
325
|
return { ...e, git: await prov.readStatus(e.path) };
|
|
@@ -323,6 +328,13 @@ async function cmdWsLs(args: string[]): Promise<number> {
|
|
|
323
328
|
}
|
|
324
329
|
});
|
|
325
330
|
}
|
|
331
|
+
return { wsRoot, workspaces: entries };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function cmdWsLs(args: string[]): Promise<number> {
|
|
335
|
+
const { flags, positional } = parseFlags(args, { status: "bool", json: "bool" });
|
|
336
|
+
if (positional.length > 0) throw new Error(`ws ls takes no positional args`);
|
|
337
|
+
const { wsRoot, workspaces: entries } = await collectWorkspaces({ status: !!flags.status });
|
|
326
338
|
|
|
327
339
|
if (flags.json) {
|
|
328
340
|
process.stdout.write(
|
|
@@ -366,22 +378,20 @@ function gitSummary(e: WsEntry): string {
|
|
|
366
378
|
return parts.length ? parts.join(", ") : "clean";
|
|
367
379
|
}
|
|
368
380
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
381
|
+
/**
|
|
382
|
+
* One workspace's WsEntry by checkout dir — git status + live-agent count, the
|
|
383
|
+
* owner/repo/branch back-derived from the layout when `spec` isn't provided.
|
|
384
|
+
* Shared by `ay ws status` and serve's `GET /api/ws/status`.
|
|
385
|
+
*/
|
|
386
|
+
export async function workspaceStatus(dir: string, spec?: RepoSpec | null): Promise<WsEntry> {
|
|
373
387
|
const prov = await loadProvision();
|
|
374
|
-
const wsRoot = getProvisionRoot();
|
|
375
|
-
const operand = positional[0] ?? ".";
|
|
376
|
-
const mode = flags.path ? "path" : flags.spec ? "spec" : "auto";
|
|
377
|
-
const { dir, spec } = await resolveOperand(prov, operand, mode, wsRoot);
|
|
378
388
|
const git = await prov.readStatus(dir);
|
|
379
389
|
|
|
380
390
|
// Fill owner/repo/branch from the layout when addressed by path, best-effort.
|
|
381
391
|
const layoutSpec =
|
|
382
392
|
spec ??
|
|
383
393
|
(() => {
|
|
384
|
-
const root = prov.resolveWsRoot(
|
|
394
|
+
const root = prov.resolveWsRoot(getProvisionRoot());
|
|
385
395
|
if (!isPathInside(root, dir)) return null;
|
|
386
396
|
const segs = path.relative(root, dir).split(path.sep);
|
|
387
397
|
return segs.length >= 4 && segs[2] === "tree"
|
|
@@ -389,7 +399,7 @@ async function cmdWsStatus(args: string[]): Promise<number> {
|
|
|
389
399
|
: null;
|
|
390
400
|
})();
|
|
391
401
|
|
|
392
|
-
|
|
402
|
+
return {
|
|
393
403
|
owner: layoutSpec?.owner ?? "",
|
|
394
404
|
repo: layoutSpec?.repo ?? "",
|
|
395
405
|
branch: layoutSpec?.branch ?? git.branch,
|
|
@@ -408,14 +418,27 @@ async function cmdWsStatus(args: string[]): Promise<number> {
|
|
|
408
418
|
},
|
|
409
419
|
git,
|
|
410
420
|
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function cmdWsStatus(args: string[]): Promise<number> {
|
|
424
|
+
const { flags, positional } = parseFlags(args, { json: "bool", path: "bool", spec: "bool" });
|
|
425
|
+
if (flags.path && flags.spec) throw new Error("--path and --spec are mutually exclusive");
|
|
426
|
+
if (positional.length > 1) throw new Error("ws status takes at most one target");
|
|
427
|
+
const prov = await loadProvision();
|
|
428
|
+
const wsRoot = getProvisionRoot();
|
|
429
|
+
const operand = positional[0] ?? ".";
|
|
430
|
+
const mode = flags.path ? "path" : flags.spec ? "spec" : "auto";
|
|
431
|
+
const { dir, spec } = await resolveOperand(prov, operand, mode, wsRoot);
|
|
432
|
+
const entry = await workspaceStatus(dir, spec);
|
|
411
433
|
|
|
412
434
|
if (flags.json) {
|
|
413
435
|
process.stdout.write(JSON.stringify({ schema: WS_JSON_SCHEMA, workspace: entry }, null, 2) + "\n");
|
|
414
436
|
return 0;
|
|
415
437
|
}
|
|
438
|
+
const git = entry.git!;
|
|
416
439
|
process.stdout.write(
|
|
417
440
|
`${dir}\n` +
|
|
418
|
-
(
|
|
441
|
+
(entry.owner ? ` spec: ${entry.owner}/${entry.repo}@${entry.branch}\n` : "") +
|
|
419
442
|
` branch: ${git.branch} @ ${git.head}\n` +
|
|
420
443
|
` state: ${gitSummary(entry)}\n` +
|
|
421
444
|
` agents: ${entry.agents.live} live\n`,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import "./ts-nIDrsHdX.js";
|
|
2
|
-
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-BG3XuJbO.js";
|
|
4
|
-
import "./pidStore-BIvsBQ8X.js";
|
|
5
|
-
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
|
-
import "./messageLog-CxrKJj77.js";
|
|
7
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C7QmgASg.js";
|
|
8
|
-
|
|
9
|
-
export { SUPPORTED_CLIS };
|