agent-yes 1.197.0 → 1.198.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,399 @@
1
+ import "./logger-CDIsZ-Pp.js";
2
+ import "./globalPidIndex-CoNr7tS8.js";
3
+ import "./messageLog-CxrKJj77.js";
4
+ import "./configShared-aKTg-sa5.js";
5
+ import { S as listRecords } from "./subcommands-BeUGFe4x.js";
6
+ import "./e2e-jb0Hp43q.js";
7
+ import "./webrtcLink-B7REGtK2.js";
8
+ import "./remotes-Cim0dBU7.js";
9
+ import { i as getProvisionRoot } from "./workspaceConfig-CjaRvTjf.js";
10
+ import { existsSync } from "fs";
11
+ import { lstat, opendir } from "fs/promises";
12
+ import path from "path";
13
+
14
+ //#region ts/ws.ts
15
+ /**
16
+ * `ay ws` — workspace management over the codehost/provision standard layout
17
+ * (`<wsRoot>/<owner>/<repo>/tree/<branch>`, independent clones except forked
18
+ * linked worktrees).
19
+ *
20
+ * v1 surface (read/list/new/fork only — deletion/gc/doctor are deliberately
21
+ * deferred until workspace lifecycle data exists to design them around):
22
+ *
23
+ * ay ws ls [--status] [--json]
24
+ * ay ws status [<spec-or-path>] [--path|--spec] [--json]
25
+ * ay ws new <source> [--create]
26
+ * ay ws fork <branch> [--from <path>] [--wip]
27
+ *
28
+ * `--json` schema (stable, versioned via `schema`):
29
+ * ls: { schema:"ay-ws/v1", wsRoot, workspaces: WsEntry[] }
30
+ * status: { schema:"ay-ws/v1", workspace: WsEntry }
31
+ * WsEntry = { owner, repo, branch, path, agents: { live: number },
32
+ * git?: GitStatus | null, gitError?: string }
33
+ *
34
+ * codehost/provision is a peer package (bun-linked in dev). Like serve.ts's
35
+ * fork path, it is imported lazily so `ay ls` etc. never pay for it and a
36
+ * missing install produces a clear actionable error instead of a module crash.
37
+ */
38
+ async function loadProvision() {
39
+ try {
40
+ return await import("codehost/provision");
41
+ } catch (e) {
42
+ throw new Error(`'ay ws' needs the 'codehost' package (codehost/provision) — install it (npm i -g codehost) or 'bun link' it for local dev: ${e.message}`);
43
+ }
44
+ }
45
+ const WS_JSON_SCHEMA = "ay-ws/v1";
46
+ const STATUS_CONCURRENCY = 8;
47
+ const MAX_BRANCH_DEPTH = 8;
48
+ /**
49
+ * Path containment that matches how the OS actually compares paths: segment
50
+ * boundaries via path.relative (never a raw startsWith), case-insensitive on
51
+ * Windows (and macOS's default case-insensitive FS is fine with this too —
52
+ * false positives there require two dirs differing only by case).
53
+ */
54
+ function isPathInside(parent, child) {
55
+ let p = path.resolve(parent);
56
+ let c = path.resolve(child);
57
+ if (process.platform === "win32") {
58
+ p = p.toLowerCase();
59
+ c = c.toLowerCase();
60
+ }
61
+ const rel = path.relative(p, c);
62
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
63
+ }
64
+ /** True when `dir` is a git checkout root (`.git` file = linked worktree, dir = clone). */
65
+ function isCheckoutRoot(dir) {
66
+ return existsSync(path.join(dir, ".git"));
67
+ }
68
+ async function subdirs(dir) {
69
+ const out = [];
70
+ let d;
71
+ try {
72
+ d = await opendir(dir);
73
+ } catch {
74
+ return out;
75
+ }
76
+ for await (const ent of d) {
77
+ if (ent.name.startsWith(".")) continue;
78
+ if (ent.isDirectory()) out.push(ent.name);
79
+ else if (ent.isSymbolicLink()) continue;
80
+ }
81
+ return out.sort();
82
+ }
83
+ /**
84
+ * Walk `<wsRoot>/<owner>/<repo>/tree/**` collecting checkout roots. The branch
85
+ * may contain `/`, so below `tree/` we descend until a `.git` marker is found
86
+ * (a checkout root is never nested inside another checkout in this layout),
87
+ * bounded by MAX_BRANCH_DEPTH.
88
+ */
89
+ async function walkWorkspaces(wsRoot) {
90
+ const found = [];
91
+ async function walkBranches(dir, owner, repo, segs) {
92
+ if (segs.length > MAX_BRANCH_DEPTH) return;
93
+ for (const name of await subdirs(dir)) {
94
+ const p = path.join(dir, name);
95
+ const branchSegs = [...segs, name];
96
+ if (isCheckoutRoot(p)) found.push({
97
+ owner,
98
+ repo,
99
+ branch: branchSegs.join("/"),
100
+ path: p
101
+ });
102
+ else await walkBranches(p, owner, repo, branchSegs);
103
+ }
104
+ }
105
+ for (const owner of await subdirs(wsRoot)) for (const repo of await subdirs(path.join(wsRoot, owner))) {
106
+ const tree = path.join(wsRoot, owner, repo, "tree");
107
+ try {
108
+ if (!(await lstat(tree)).isDirectory()) continue;
109
+ } catch {
110
+ continue;
111
+ }
112
+ await walkBranches(tree, owner, repo, []);
113
+ }
114
+ return found;
115
+ }
116
+ /** Live (non-exited) agents whose cwd sits inside `wsPath`. */
117
+ function liveAgentsIn(records, wsPath) {
118
+ return records.filter((r) => r.cwd && isPathInside(wsPath, r.cwd));
119
+ }
120
+ async function mapBounded(items, limit, fn) {
121
+ const out = new Array(items.length);
122
+ let next = 0;
123
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
124
+ while (next < items.length) {
125
+ const i = next++;
126
+ out[i] = await fn(items[i]);
127
+ }
128
+ });
129
+ await Promise.all(workers);
130
+ return out;
131
+ }
132
+ /**
133
+ * Resolve a status/inspect operand deterministically:
134
+ * 1. `--path` forces path, `--spec` forces source-spec parsing
135
+ * 2. an operand that exists on disk is a path
136
+ * 3. otherwise it must parse as a source (owner/repo[@branch|/tree/branch] or URL)
137
+ * Prints the normalized resolution so the user always sees what was addressed.
138
+ */
139
+ async function resolveOperand(prov, operand, mode, wsRoot) {
140
+ const asPath = () => {
141
+ const dir = path.resolve(operand);
142
+ if (!existsSync(dir)) throw new Error(`path does not exist: ${dir}`);
143
+ if (!isCheckoutRoot(dir)) throw new Error(`not a git checkout root (no .git): ${dir}`);
144
+ return {
145
+ dir,
146
+ spec: null
147
+ };
148
+ };
149
+ const asSpec = () => {
150
+ const spec = prov.parseSource(operand);
151
+ if (!spec) throw new Error(`cannot parse "${operand}" as a source — expected <owner>/<repo>, <owner>/<repo>@<branch>, <owner>/<repo>/tree/<branch>, or a github URL` + (existsSync(operand) ? "" : " (and it is not an existing path)"));
152
+ const dir = prov.folderFor(spec, wsRoot);
153
+ if (!existsSync(dir)) throw new Error(`workspace not provisioned: ${dir} (ay ws new ${operand})`);
154
+ return {
155
+ dir,
156
+ spec
157
+ };
158
+ };
159
+ if (mode === "path") return asPath();
160
+ if (mode === "spec") return asSpec();
161
+ if (existsSync(path.resolve(operand))) return asPath();
162
+ return asSpec();
163
+ }
164
+ /**
165
+ * Default source for `ws fork`: the calling agent's registered cwd when run
166
+ * inside an agent (AGENT_YES_PID is the wrapper pid of the enclosing agent),
167
+ * else the current directory.
168
+ */
169
+ async function defaultForkFrom() {
170
+ const envPid = Number(process.env.AGENT_YES_PID);
171
+ if (envPid > 0) {
172
+ const recs = await listRecords(void 0, {
173
+ all: true,
174
+ active: false,
175
+ json: false,
176
+ latest: false,
177
+ cwdScope: null
178
+ });
179
+ const self = recs.find((r) => r.wrapper_pid === envPid) ?? recs.find((r) => r.pid === envPid);
180
+ if (self?.cwd) return self.cwd;
181
+ }
182
+ return process.cwd();
183
+ }
184
+ function parseFlags(args, known) {
185
+ const flags = {};
186
+ const positional = [];
187
+ for (let i = 0; i < args.length; i++) {
188
+ const a = args[i];
189
+ if (!a.startsWith("--")) {
190
+ positional.push(a);
191
+ continue;
192
+ }
193
+ const eq = a.indexOf("=");
194
+ const name = eq === -1 ? a.slice(2) : a.slice(2, eq);
195
+ const kind = known[name];
196
+ if (!kind) throw new Error(`unknown flag --${name}`);
197
+ if (kind === "bool") {
198
+ if (eq !== -1) throw new Error(`--${name} takes no value`);
199
+ flags[name] = true;
200
+ } else {
201
+ const v = eq !== -1 ? a.slice(eq + 1) : args[++i];
202
+ if (v === void 0) throw new Error(`--${name} requires a value`);
203
+ flags[name] = v;
204
+ }
205
+ }
206
+ return {
207
+ flags,
208
+ positional
209
+ };
210
+ }
211
+ async function cmdWsLs(args) {
212
+ const { flags, positional } = parseFlags(args, {
213
+ status: "bool",
214
+ json: "bool"
215
+ });
216
+ if (positional.length > 0) throw new Error(`ws ls takes no positional args`);
217
+ const prov = await loadProvision();
218
+ const wsRoot = prov.resolveWsRoot(getProvisionRoot());
219
+ const [bare, records] = await Promise.all([walkWorkspaces(wsRoot), listRecords(void 0, {
220
+ all: false,
221
+ active: false,
222
+ json: false,
223
+ latest: false,
224
+ cwdScope: null
225
+ })]);
226
+ let entries = bare.map((w) => ({
227
+ ...w,
228
+ agents: { live: liveAgentsIn(records, w.path).length }
229
+ }));
230
+ if (flags.status) entries = await mapBounded(entries, STATUS_CONCURRENCY, async (e) => {
231
+ try {
232
+ return {
233
+ ...e,
234
+ git: await prov.readStatus(e.path)
235
+ };
236
+ } catch (err) {
237
+ return {
238
+ ...e,
239
+ git: null,
240
+ gitError: err.message.slice(0, 200)
241
+ };
242
+ }
243
+ });
244
+ if (flags.json) {
245
+ process.stdout.write(JSON.stringify({
246
+ schema: WS_JSON_SCHEMA,
247
+ wsRoot,
248
+ workspaces: entries
249
+ }, null, 2) + "\n");
250
+ return 0;
251
+ }
252
+ if (entries.length === 0) {
253
+ process.stderr.write(`no workspaces under ${wsRoot} (layout <owner>/<repo>/tree/<branch>)\n`);
254
+ return 0;
255
+ }
256
+ const specOf = (e) => `${e.owner}/${e.repo}@${e.branch}`;
257
+ const specW = Math.max(9, ...entries.map((e) => specOf(e).length));
258
+ const agentsW = 6;
259
+ const header = flags.status ? `${"WORKSPACE".padEnd(specW)} ${"AGENTS".padEnd(agentsW)} GIT\n` : `${"WORKSPACE".padEnd(specW)} ${"AGENTS".padEnd(agentsW)} PATH\n`;
260
+ process.stdout.write(header);
261
+ for (const e of entries) {
262
+ const agents = e.agents.live > 0 ? String(e.agents.live) : "-";
263
+ const tail = flags.status ? gitSummary(e) : e.path;
264
+ process.stdout.write(`${specOf(e).padEnd(specW)} ${agents.padEnd(agentsW)} ${tail}\n`);
265
+ }
266
+ if (entries.some((e) => e.agents.live > 0)) process.stderr.write(`\n ay ls --cwd <path> # the agents inside a workspace\n`);
267
+ return 0;
268
+ }
269
+ function gitSummary(e) {
270
+ if (e.gitError) return `error: ${e.gitError}`;
271
+ const g = e.git;
272
+ if (!g) return "?";
273
+ const parts = [];
274
+ if (g.dirty) parts.push("dirty");
275
+ if (g.ahead > 0) parts.push(`ahead ${g.ahead}`);
276
+ if (g.behind > 0) parts.push(`behind ${g.behind}`);
277
+ if (!g.hasUpstream) parts.push("no-upstream");
278
+ return parts.length ? parts.join(", ") : "clean";
279
+ }
280
+ async function cmdWsStatus(args) {
281
+ const { flags, positional } = parseFlags(args, {
282
+ json: "bool",
283
+ path: "bool",
284
+ spec: "bool"
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");
288
+ 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
+ const git = await prov.readStatus(dir);
292
+ const layoutSpec = spec ?? (() => {
293
+ const root = prov.resolveWsRoot(wsRoot);
294
+ if (!isPathInside(root, dir)) return null;
295
+ const segs = path.relative(root, dir).split(path.sep);
296
+ return segs.length >= 4 && segs[2] === "tree" ? {
297
+ owner: segs[0],
298
+ repo: segs[1],
299
+ branch: segs.slice(3).join("/")
300
+ } : null;
301
+ })();
302
+ const entry = {
303
+ owner: layoutSpec?.owner ?? "",
304
+ repo: layoutSpec?.repo ?? "",
305
+ branch: layoutSpec?.branch ?? git.branch,
306
+ path: dir,
307
+ agents: { live: liveAgentsIn(await listRecords(void 0, {
308
+ all: false,
309
+ active: false,
310
+ json: false,
311
+ latest: false,
312
+ cwdScope: null
313
+ }), dir).length },
314
+ git
315
+ };
316
+ if (flags.json) {
317
+ process.stdout.write(JSON.stringify({
318
+ schema: WS_JSON_SCHEMA,
319
+ workspace: entry
320
+ }, null, 2) + "\n");
321
+ return 0;
322
+ }
323
+ process.stdout.write(`${dir}\n` + (layoutSpec ? ` spec: ${layoutSpec.owner}/${layoutSpec.repo}@${layoutSpec.branch}\n` : "") + ` branch: ${git.branch} @ ${git.head}\n state: ${gitSummary(entry)}\n agents: ${entry.agents.live} live\n`);
324
+ if (entry.agents.live > 0) process.stderr.write(`\n ay ls --cwd ${dir}\n`);
325
+ return 0;
326
+ }
327
+ async function cmdWsNew(args) {
328
+ const { flags, positional } = parseFlags(args, { create: "bool" });
329
+ const source = positional[0];
330
+ if (!source || positional.length > 1) throw new Error("usage: ay ws new <owner>/<repo>[@branch] [--create]");
331
+ const prov = await loadProvision();
332
+ const wsRoot = getProvisionRoot();
333
+ const spec = prov.parseSource(source);
334
+ if (!spec) throw new Error(`cannot parse "${source}" as a source (owner/repo[@branch] or URL)`);
335
+ process.stderr.write(`provisioning ${spec.owner}/${spec.repo}@${spec.branch} …\n`);
336
+ let res = await prov.provision(spec, { wsRoot });
337
+ if (!res.ok && res.reason === "branch-not-found" && flags.create) {
338
+ process.stderr.write(`branch not found on remote — creating it locally (--create)\n`);
339
+ res = await prov.createBranch(spec, { wsRoot });
340
+ }
341
+ if (!res.ok) {
342
+ process.stderr.write(`provision failed (${res.reason ?? "error"}): ${res.error ?? "unknown"}\n` + (res.reason === "branch-not-found" && !flags.create ? ` (branch missing on the remote — re-run with --create to branch off the default)\n` : ""));
343
+ return 1;
344
+ }
345
+ process.stdout.write(`${res.action} ${res.folder}\n`);
346
+ return 0;
347
+ }
348
+ async function cmdWsFork(args) {
349
+ const { flags, positional } = parseFlags(args, {
350
+ from: "value",
351
+ wip: "bool"
352
+ });
353
+ const branch = positional[0];
354
+ if (!branch || positional.length > 1) throw new Error("usage: ay ws fork <new-branch> [--from <path>] [--wip]");
355
+ const prov = await loadProvision();
356
+ const fromCwd = path.resolve(typeof flags.from === "string" ? flags.from : await defaultForkFrom());
357
+ process.stderr.write(`forking ${fromCwd} → branch ${branch}${flags.wip ? " (with WIP)" : ""} …\n`);
358
+ const res = await prov.forkWorktree({
359
+ fromCwd,
360
+ branch,
361
+ wsRoot: getProvisionRoot(),
362
+ wip: !!flags.wip
363
+ });
364
+ if (!res.ok) {
365
+ process.stderr.write(`fork failed: ${res.error ?? "unknown"}\n`);
366
+ return 1;
367
+ }
368
+ process.stdout.write(`${res.action} ${res.folder}\n`);
369
+ process.stderr.write(`\n ay claude --cwd ${res.folder} -- "<prompt>" # work there\n`);
370
+ return 0;
371
+ }
372
+ function wsHelp() {
373
+ process.stdout.write("ay ws - workspaces under <wsRoot>/<owner>/<repo>/tree/<branch>\n\n ay ws ls [--status] [--json] list workspaces (+git state, +live agent count)\n ay ws status [<spec|path>] [--json] one workspace's git state (default: cwd)\n ay ws new <owner>/<repo>[@branch] clone/refresh a workspace (--create: new branch)\n ay ws fork <branch> [--from <path>] sibling worktree off HEAD (--wip: carry changes)\n\n targets: owner/repo, owner/repo@branch, owner/repo/tree/branch, github URL, or a path\n wsRoot: CODEHOST_WS_ROOT > config provisionRoot > ~/ws\n");
374
+ return 0;
375
+ }
376
+ /** `ay ws <sub> …` dispatcher (called from runSubcommand). */
377
+ async function cmdWs(args) {
378
+ const sub = args[0];
379
+ const rest = args.slice(1);
380
+ switch (sub) {
381
+ case "ls":
382
+ case "list": return cmdWsLs(rest);
383
+ case "status": return cmdWsStatus(rest);
384
+ case "new": return cmdWsNew(rest);
385
+ case "fork": return cmdWsFork(rest);
386
+ case void 0:
387
+ case "help":
388
+ case "--help":
389
+ case "-h": return wsHelp();
390
+ default:
391
+ process.stderr.write(`ay ws: unknown subcommand "${sub}"\n\n`);
392
+ wsHelp();
393
+ return 1;
394
+ }
395
+ }
396
+
397
+ //#endregion
398
+ export { cmdWs };
399
+ //# sourceMappingURL=ws-XM0AQ7Qk.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.197.0",
3
+ "version": "1.198.1",
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",
package/ts/serve.ts CHANGED
@@ -763,8 +763,12 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
763
763
  removed++;
764
764
  process.stdout.write(`removed '${DAEMON_NAME}' from ${m.id}\n`);
765
765
  if (m.id === "pm2") {
766
- // Drop it from the persisted pm2 list too, so `pm2 resurrect` won't revive it.
767
- await spawnExit([m.bin, "save"]);
766
+ // Drop it from the persisted pm2 list too, so `pm2 resurrect` won't
767
+ // revive it. --force is required: with the process list now empty, a
768
+ // plain `pm2 save` REFUSES to overwrite the dump ("Nothing to save!
769
+ // Please use --force") and the deleted daemon stays in dump.pm2 —
770
+ // resurrect/startup would then boot it alongside the new manager's.
771
+ await spawnExit([m.bin, "save", "--force"]);
768
772
  // Remove the Windows login auto-start entry we added at install time.
769
773
  if (process.platform === "win32")
770
774
  await spawnExit(["reg", "delete", WIN_RUN_KEY, "/v", WIN_RUN_VALUE, "/f"]);
package/ts/subcommands.ts CHANGED
@@ -368,7 +368,7 @@ const SUBCOMMANDS = new Set([
368
368
  // alias like `cy` (= claude-yes = "agent-yes claude") must NOT treat these as
369
369
  // subcommands — `cy setup …` should run claude with that text, not manage the
370
370
  // host. Kept separate from SUBCOMMANDS so a runner alias falls straight through.
371
- const MANAGER_SUBCOMMANDS = new Set(["setup"]);
371
+ const MANAGER_SUBCOMMANDS = new Set(["setup", "ws"]);
372
372
 
373
373
  const IDLE_THRESHOLD_MS = 60 * 1000;
374
374
 
@@ -475,6 +475,10 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
475
475
  const { cmdSetup } = await import("./setup.ts");
476
476
  return cmdSetup(rest);
477
477
  }
478
+ case "ws": {
479
+ const { cmdWs } = await import("./ws.ts");
480
+ return cmdWs(rest);
481
+ }
478
482
  case "schedule": {
479
483
  const { cmdSchedule } = await import("./schedule.ts");
480
484
  return cmdSchedule(rest);
@@ -572,6 +576,11 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
572
576
  const setupLine = managerCommands
573
577
  ? ` ay setup guided setup: pick a workspace, share to agent-yes.com\n`
574
578
  : ``;
579
+ // `ws` is manager-only for the same reason as `setup`.
580
+ const wsLines = managerCommands
581
+ ? ` ay ws ls [--status] list <owner>/<repo>/tree/<branch> workspaces\n` +
582
+ ` ay ws new <owner>/<repo>[@branch] clone/refresh a workspace (ay ws help for more)\n`
583
+ : ``;
575
584
  // Only agents carry AGENT_YES_PID — a human shell never sets it — so this
576
585
  // section is skipped entirely (no async work at all) for interactive use.
577
586
  const self = process.env.AGENT_YES_PID ? await resolveSender() : null;
@@ -599,6 +608,7 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
599
608
  ` ay result <keyword> [--wait] pull an agent's structured result envelope\n` +
600
609
  ` ay result set '<json>' (inside an agent) deposit your result envelope\n` +
601
610
  ` ay reap kill process groups leaked by dead agents\n` +
611
+ wsLines +
602
612
  `\n` +
603
613
  `Remote:\n` +
604
614
  setupLine +