agent-yes 1.196.3 → 1.198.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/default.config.yaml +51 -12
- package/dist/SUPPORTED_CLIS-Bhb4JKPv.js +9 -0
- package/dist/{SUPPORTED_CLIS-D3knhJ6_.js → SUPPORTED_CLIS-BuScRhc9.js} +2 -2
- package/dist/{agent-yes.config-ugPYTPOd.js → agent-yes.config-whqkqq9P.js} +2 -2
- package/dist/{agentShare-M6FE9AsI.js → agentShare-CD1AIPMR.js} +8 -7
- package/dist/cli.js +8 -8
- package/dist/{configShared-0MnIQ652.js → configShared-aKTg-sa5.js} +1 -1
- package/dist/{e2e-BeKjLhmO.js → e2e-jb0Hp43q.js} +1 -1
- package/dist/{expose-B3AGhVhM.js → expose-k9l6ZJDA.js} +1 -1
- package/dist/{forkNested-C1kKsXi5.js → forkNested-DOyDAHnC.js} +1 -1
- package/dist/index.js +3 -2
- package/dist/messageLog-CxrKJj77.js +94 -0
- package/dist/{nodeRuntime-DZRjxpNp.js → nodeRuntime-CwNJuwH5.js} +1 -1
- package/dist/{notifyDaemon-DgxP6GAn.js → notifyDaemon-DYoMC4tA.js} +7 -6
- package/dist/{openBrowser-DQVkxCyj.js → openBrowser-CSj21pZE.js} +1 -1
- package/dist/{reaper-CWF2_ATd.js → reaper-BUHCyxdF.js} +1 -1
- package/dist/{reaper-jfSmog15.js → reaper-CJSu8UVZ.js} +1 -1
- package/dist/{remotes-CW-yxSgW.js → remotes-CXqYR0p8.js} +3 -3
- package/dist/{remotes-oSmwSYaV.js → remotes-Cim0dBU7.js} +3 -3
- package/dist/{rustBinary-C9q17obe.js → rustBinary-DLI26u3O.js} +2 -2
- package/dist/{schedule-SisMmURQ.js → schedule-DAl2mCc9.js} +7 -6
- package/dist/{serve-BOrkFR6H.js → serve-9jP4V1l_.js} +26 -25
- package/dist/{setup-CFbckIlf.js → setup-B55UN6xV.js} +3 -3
- package/dist/{share-29YbzpbU.js → share-BfIU8t_h.js} +2 -2
- package/dist/{share-nvqy64A2.js → share-Ciw1mWVN.js} +2 -2
- package/dist/{spawnGate-CWbdnJ5K.js → spawnGate-BkjeS4tg.js} +1 -1
- package/dist/{spawnGate-DRWa_Fzj.js → spawnGate-CXjEz6Pf.js} +2 -2
- package/dist/{subcommands-CMX-dej0.js → subcommands-D7AmlO_6.js} +24 -105
- package/dist/{subcommands-DSbYPDt5.js → subcommands-lUi22sO5.js} +6 -5
- package/dist/{tray-BKTZ5gIf.js → tray-BSFi35Qg.js} +1 -1
- package/dist/{ts-Bqa5HjY3.js → ts-D9RFyHqn.js} +84 -7
- package/dist/{versionChecker-ZIgviJ62.js → versionChecker-DQQIK4Og.js} +2 -2
- package/dist/{webrtcLink-BG0Xc4-W.js → webrtcLink-B7REGtK2.js} +2 -2
- package/dist/{webrtcRemote-BmuVKcrx.js → webrtcRemote-CvIEfWX8.js} +3 -3
- package/dist/{workspaceConfig-_GtAZtsi.js → workspaceConfig-CjaRvTjf.js} +1 -1
- package/dist/ws-BpHPhcPH.js +399 -0
- package/package.json +1 -1
- package/ts/autoRetry.spec.ts +90 -1
- package/ts/autoRetry.ts +76 -0
- package/ts/index.ts +37 -4
- package/ts/messageLog.ts +3 -2
- package/ts/subcommands.ts +20 -6
- package/ts/ws.spec.ts +435 -0
- package/ts/ws.ts +522 -0
- package/dist/SUPPORTED_CLIS-L3rJRumq.js +0 -8
package/ts/subcommands.ts
CHANGED
|
@@ -211,7 +211,7 @@ export interface MessageEdge {
|
|
|
211
211
|
by: number;
|
|
212
212
|
target: number;
|
|
213
213
|
at: number;
|
|
214
|
-
kind?: "key" | "select";
|
|
214
|
+
kind?: "key" | "select" | "auto-retry";
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/**
|
|
@@ -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 +
|
|
@@ -3279,10 +3289,14 @@ async function cmdMsgs(rest: string[]): Promise<number> {
|
|
|
3279
3289
|
|
|
3280
3290
|
for (const { dir, rec } of shown) {
|
|
3281
3291
|
const when = new Date(rec.at).toLocaleTimeString();
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3292
|
+
// auto-retry nudges are written by the agent's own wrapper (from is null
|
|
3293
|
+
// there too) — label them as agent-yes, not a human send.
|
|
3294
|
+
const fromLabel = rec.from
|
|
3295
|
+
? `${rec.from.cli} #${rec.from.pid}`
|
|
3296
|
+
: rec.kind === "auto-retry"
|
|
3297
|
+
? "agent-yes"
|
|
3298
|
+
: "human";
|
|
3299
|
+
const peer = dir === "out" ? `→ ${rec.to.cli} #${rec.to.pid}` : `← ${fromLabel}`;
|
|
3286
3300
|
const via = rec.remote ? ` (via ${rec.remote})` : "";
|
|
3287
3301
|
const flag = rec.confirmed === false ? " (unconfirmed)" : "";
|
|
3288
3302
|
const tag = rec.kind ? `[${rec.kind}] ` : "";
|
package/ts/ws.spec.ts
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, realpathSync, writeFileSync, symlinkSync, rmSync } from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { cmdWs, isPathInside, resolveOperand, walkWorkspaces, WS_JSON_SCHEMA } from "./ws.ts";
|
|
6
|
+
|
|
7
|
+
// Mutable fake for codehost/provision — each test overrides what it needs.
|
|
8
|
+
// ws.ts imports the module lazily; vitest's registry mock intercepts that too.
|
|
9
|
+
const prov = vi.hoisted(() => ({
|
|
10
|
+
wsRoot: "",
|
|
11
|
+
parseSource: (input: string): { owner: string; repo: string; branch: string } | null => {
|
|
12
|
+
const m = input.match(/^([^/]+)\/([^/@]+)(?:@(.+))?$/);
|
|
13
|
+
return m ? { owner: m[1]!, repo: m[2]!, branch: m[3] ?? "main" } : null;
|
|
14
|
+
},
|
|
15
|
+
readStatus: vi.fn(),
|
|
16
|
+
provision: vi.fn(),
|
|
17
|
+
createBranch: vi.fn(),
|
|
18
|
+
forkWorktree: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("codehost/provision", () => ({
|
|
22
|
+
resolveWsRoot: (w?: string) => w ?? prov.wsRoot,
|
|
23
|
+
folderFor: (spec: { owner: string; repo: string; branch: string }, wsRoot?: string) =>
|
|
24
|
+
path.join(wsRoot ?? prov.wsRoot, spec.owner, spec.repo, "tree", spec.branch),
|
|
25
|
+
parseSource: (input: string) => prov.parseSource(input),
|
|
26
|
+
readStatus: (dir: string) => prov.readStatus(dir),
|
|
27
|
+
provision: (spec: unknown, opts?: unknown) => prov.provision(spec, opts),
|
|
28
|
+
createBranch: (spec: unknown, opts?: unknown) => prov.createBranch(spec, opts),
|
|
29
|
+
forkWorktree: (opts: unknown) => prov.forkWorktree(opts),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe("isPathInside", () => {
|
|
33
|
+
it("contains itself and descendants", () => {
|
|
34
|
+
expect(isPathInside("/a/b", "/a/b")).toBe(true);
|
|
35
|
+
expect(isPathInside("/a/b", "/a/b/c/d")).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("rejects siblings sharing a name prefix (segment boundary, not startsWith)", () => {
|
|
39
|
+
expect(isPathInside("/a/repo", "/a/repo-two/x")).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects parents and unrelated paths", () => {
|
|
43
|
+
expect(isPathInside("/a/b/c", "/a/b")).toBe(false);
|
|
44
|
+
expect(isPathInside("/a/b", "/z")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("resolves relative segments before comparing", () => {
|
|
48
|
+
expect(isPathInside("/a/b", "/a/b/../b/c")).toBe(true);
|
|
49
|
+
expect(isPathInside("/a/b", "/a/b/../c")).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("walkWorkspaces", () => {
|
|
54
|
+
let root: string;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
root = mkdtempSync(path.join(os.tmpdir(), "ay-ws-test-"));
|
|
57
|
+
});
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
rmSync(root, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const mkCheckout = (rel: string, gitAs: "dir" | "file") => {
|
|
63
|
+
const dir = path.join(root, rel);
|
|
64
|
+
mkdirSync(dir, { recursive: true });
|
|
65
|
+
if (gitAs === "dir") mkdirSync(path.join(dir, ".git"));
|
|
66
|
+
else writeFileSync(path.join(dir, ".git"), "gitdir: /elsewhere\n");
|
|
67
|
+
return dir;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
it("finds clones and linked worktrees, including branches containing '/'", async () => {
|
|
71
|
+
const a = mkCheckout("owner/repo/tree/main", "dir");
|
|
72
|
+
const b = mkCheckout("owner/repo/tree/feat/deep/branch", "file");
|
|
73
|
+
const found = await walkWorkspaces(root);
|
|
74
|
+
expect(found).toEqual([
|
|
75
|
+
{ owner: "owner", repo: "repo", branch: "feat/deep/branch", path: b },
|
|
76
|
+
{ owner: "owner", repo: "repo", branch: "main", path: a },
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does not descend into a checkout root looking for nested checkouts", async () => {
|
|
81
|
+
const a = mkCheckout("o/r/tree/main", "dir");
|
|
82
|
+
// a stray nested checkout below an existing root must not double-report
|
|
83
|
+
mkdirSync(path.join(a, "vendor", ".git"), { recursive: true });
|
|
84
|
+
const found = await walkWorkspaces(root);
|
|
85
|
+
expect(found.map((w) => w.branch)).toEqual(["main"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("skips non-layout dirs, dotdirs, and symlinks", async () => {
|
|
89
|
+
mkCheckout("o/r/tree/main", "dir");
|
|
90
|
+
mkdirSync(path.join(root, "o/r/notes"), { recursive: true }); // no tree/ marker
|
|
91
|
+
mkdirSync(path.join(root, ".hidden/x/tree/y"), { recursive: true });
|
|
92
|
+
mkdirSync(path.join(root, "loop/r/tree"), { recursive: true });
|
|
93
|
+
// symlink cycle under tree/ must not hang or be reported
|
|
94
|
+
symlinkSync(path.join(root, "loop"), path.join(root, "loop/r/tree/self"));
|
|
95
|
+
const found = await walkWorkspaces(root);
|
|
96
|
+
expect(found.map((w) => `${w.owner}/${w.repo}@${w.branch}`)).toEqual(["o/r@main"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("bounds the branch-depth walk", async () => {
|
|
100
|
+
const deep = "o/r/tree/" + Array.from({ length: 12 }, (_, i) => `d${i}`).join("/");
|
|
101
|
+
mkCheckout(deep, "dir");
|
|
102
|
+
const found = await walkWorkspaces(root);
|
|
103
|
+
expect(found).toEqual([]); // beyond MAX_BRANCH_DEPTH → ignored, not crashed
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns [] for an empty or missing root", async () => {
|
|
107
|
+
expect(await walkWorkspaces(root)).toEqual([]);
|
|
108
|
+
expect(await walkWorkspaces(path.join(root, "nope"))).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("resolveOperand", () => {
|
|
113
|
+
let root: string;
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
root = mkdtempSync(path.join(os.tmpdir(), "ay-ws-op-"));
|
|
116
|
+
});
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
rmSync(root, { recursive: true, force: true });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const fakeProv = (specDir: string) =>
|
|
122
|
+
({
|
|
123
|
+
parseSource: (input: string) => {
|
|
124
|
+
const m = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
|
|
125
|
+
return m ? { owner: m[1]!, repo: m[2]!, branch: m[3]! } : null;
|
|
126
|
+
},
|
|
127
|
+
folderFor: () => specDir,
|
|
128
|
+
resolveWsRoot: (w?: string) => w ?? root,
|
|
129
|
+
}) as any;
|
|
130
|
+
|
|
131
|
+
const mkCheckout = (rel: string) => {
|
|
132
|
+
const dir = path.join(root, rel);
|
|
133
|
+
mkdirSync(path.join(dir, ".git"), { recursive: true });
|
|
134
|
+
return dir;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
it("an existing local path wins over spec parsing in auto mode", async () => {
|
|
138
|
+
const dir = mkCheckout("o/r/tree/main");
|
|
139
|
+
const res = await resolveOperand(fakeProv("/unused"), dir, "auto", undefined);
|
|
140
|
+
expect(res).toEqual({ dir, spec: null });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("falls back to spec parsing when the operand is not a path", async () => {
|
|
144
|
+
const dir = mkCheckout("o/r/tree/main");
|
|
145
|
+
const res = await resolveOperand(fakeProv(dir), "o/r@main", "auto", undefined);
|
|
146
|
+
expect(res.dir).toBe(dir);
|
|
147
|
+
expect(res.spec).toEqual({ owner: "o", repo: "r", branch: "main" });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("--path mode rejects a directory that is not a checkout root", async () => {
|
|
151
|
+
const dir = path.join(root, "plain");
|
|
152
|
+
mkdirSync(dir);
|
|
153
|
+
await expect(resolveOperand(fakeProv("/unused"), dir, "path", undefined)).rejects.toThrow(
|
|
154
|
+
/not a git checkout root/,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("--spec mode reports an unprovisioned workspace with the fix-it hint", async () => {
|
|
159
|
+
await expect(
|
|
160
|
+
resolveOperand(fakeProv(path.join(root, "missing")), "o/r@main", "spec", undefined),
|
|
161
|
+
).rejects.toThrow(/not provisioned.*ay ws new/s);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("reports a precise parse error for garbage", async () => {
|
|
165
|
+
await expect(resolveOperand(fakeProv("/unused"), "not a spec", "spec", undefined)).rejects.toThrow(
|
|
166
|
+
/cannot parse "not a spec"/,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("json schema tag", () => {
|
|
172
|
+
it("is versioned", () => {
|
|
173
|
+
expect(WS_JSON_SCHEMA).toBe("ay-ws/v1");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("cmdWs (mocked provision)", () => {
|
|
178
|
+
let root: string;
|
|
179
|
+
let out: string[];
|
|
180
|
+
let err: string[];
|
|
181
|
+
let homeBackup: string | undefined;
|
|
182
|
+
|
|
183
|
+
const CLEAN = { branch: "main", head: "abc123", ahead: 0, behind: 0, dirty: false, hasUpstream: true };
|
|
184
|
+
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
// realpath: os.tmpdir() is a symlink on macOS (/var → /private/var), and
|
|
187
|
+
// process.chdir + cwd-derived paths come back resolved.
|
|
188
|
+
root = realpathSync(mkdtempSync(path.join(os.tmpdir(), "ay-ws-cmd-")));
|
|
189
|
+
prov.wsRoot = root;
|
|
190
|
+
prov.readStatus.mockReset().mockResolvedValue(CLEAN);
|
|
191
|
+
prov.provision.mockReset();
|
|
192
|
+
prov.createBranch.mockReset();
|
|
193
|
+
prov.forkWorktree.mockReset();
|
|
194
|
+
out = [];
|
|
195
|
+
err = [];
|
|
196
|
+
vi.spyOn(process.stdout, "write").mockImplementation((s: any) => (out.push(String(s)), true));
|
|
197
|
+
vi.spyOn(process.stderr, "write").mockImplementation((s: any) => (err.push(String(s)), true));
|
|
198
|
+
// Isolate the agent registry so live-agent counts are deterministic (0).
|
|
199
|
+
homeBackup = process.env.AGENT_YES_HOME;
|
|
200
|
+
process.env.AGENT_YES_HOME = path.join(root, ".ay-home");
|
|
201
|
+
});
|
|
202
|
+
afterEach(() => {
|
|
203
|
+
vi.restoreAllMocks();
|
|
204
|
+
if (homeBackup === undefined) delete process.env.AGENT_YES_HOME;
|
|
205
|
+
else process.env.AGENT_YES_HOME = homeBackup;
|
|
206
|
+
rmSync(root, { recursive: true, force: true });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const mkCheckout = (rel: string) => {
|
|
210
|
+
const dir = path.join(root, rel);
|
|
211
|
+
mkdirSync(path.join(dir, ".git"), { recursive: true });
|
|
212
|
+
return dir;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
it("no args / unknown sub → help (0) and error (1)", async () => {
|
|
216
|
+
expect(await cmdWs([])).toBe(0);
|
|
217
|
+
expect(out.join("")).toContain("ay ws ls");
|
|
218
|
+
expect(await cmdWs(["nope"])).toBe(1);
|
|
219
|
+
expect(err.join("")).toContain('unknown subcommand "nope"');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("ls: empty root note, then table with agent hint column", async () => {
|
|
223
|
+
expect(await cmdWs(["ls"])).toBe(0);
|
|
224
|
+
expect(err.join("")).toContain("no workspaces under");
|
|
225
|
+
mkCheckout("o/r/tree/main");
|
|
226
|
+
expect(await cmdWs(["ls"])).toBe(0);
|
|
227
|
+
const table = out.join("");
|
|
228
|
+
expect(table).toContain("WORKSPACE");
|
|
229
|
+
expect(table).toContain("o/r@main");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("ls --json: versioned schema envelope", async () => {
|
|
233
|
+
const dir = mkCheckout("o/r/tree/feat/x");
|
|
234
|
+
expect(await cmdWs(["ls", "--json"])).toBe(0);
|
|
235
|
+
const doc = JSON.parse(out.join(""));
|
|
236
|
+
expect(doc.schema).toBe(WS_JSON_SCHEMA);
|
|
237
|
+
expect(doc.wsRoot).toBe(root);
|
|
238
|
+
expect(doc.workspaces).toEqual([
|
|
239
|
+
{ owner: "o", repo: "r", branch: "feat/x", path: dir, agents: { live: 0 } },
|
|
240
|
+
]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("ls --status: git summaries and the per-entry error fallback", async () => {
|
|
244
|
+
mkCheckout("o/r/tree/clean");
|
|
245
|
+
mkCheckout("o/r/tree/messy");
|
|
246
|
+
mkCheckout("o/r/tree/broken");
|
|
247
|
+
prov.readStatus.mockImplementation(async (dir: string) => {
|
|
248
|
+
if (dir.endsWith("broken")) throw new Error("boom");
|
|
249
|
+
if (dir.endsWith("messy"))
|
|
250
|
+
return { branch: "messy", head: "h", ahead: 2, behind: 1, dirty: true, hasUpstream: false };
|
|
251
|
+
return CLEAN;
|
|
252
|
+
});
|
|
253
|
+
expect(await cmdWs(["ls", "--status"])).toBe(0);
|
|
254
|
+
const table = out.join("");
|
|
255
|
+
expect(table).toContain("clean");
|
|
256
|
+
expect(table).toContain("dirty, ahead 2, behind 1, no-upstream");
|
|
257
|
+
expect(table).toContain("error: boom");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("ls rejects positional args and unknown flags", async () => {
|
|
261
|
+
await expect(cmdWs(["ls", "stray"])).rejects.toThrow(/no positional/);
|
|
262
|
+
await expect(cmdWs(["ls", "--nope"])).rejects.toThrow(/unknown flag --nope/);
|
|
263
|
+
await expect(cmdWs(["ls", "--json=1"])).rejects.toThrow(/takes no value/);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("status: layout spec + state text for a path target, --json shape", async () => {
|
|
267
|
+
const dir = mkCheckout("o/r/tree/feat/y");
|
|
268
|
+
expect(await cmdWs(["status", dir])).toBe(0);
|
|
269
|
+
const text = out.join("");
|
|
270
|
+
expect(text).toContain("spec: o/r@feat/y");
|
|
271
|
+
expect(text).toContain("state: clean");
|
|
272
|
+
out.length = 0;
|
|
273
|
+
expect(await cmdWs(["status", dir, "--json"])).toBe(0);
|
|
274
|
+
const doc = JSON.parse(out.join(""));
|
|
275
|
+
expect(doc.schema).toBe(WS_JSON_SCHEMA);
|
|
276
|
+
expect(doc.workspace.branch).toBe("feat/y");
|
|
277
|
+
expect(doc.workspace.git).toEqual(CLEAN);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("status: a checkout outside the layout omits the spec line", async () => {
|
|
281
|
+
const outside = mkdtempSync(path.join(os.tmpdir(), "ay-ws-out-"));
|
|
282
|
+
try {
|
|
283
|
+
mkdirSync(path.join(outside, ".git"));
|
|
284
|
+
expect(await cmdWs(["status", outside])).toBe(0);
|
|
285
|
+
expect(out.join("")).not.toContain("spec:");
|
|
286
|
+
} finally {
|
|
287
|
+
rmSync(outside, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("status: --path and --spec are mutually exclusive; one target max", async () => {
|
|
292
|
+
await expect(cmdWs(["status", "--path", "--spec"])).rejects.toThrow(/mutually exclusive/);
|
|
293
|
+
await expect(cmdWs(["status", "a", "b"])).rejects.toThrow(/at most one/);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("new: provisions and prints the action + folder", async () => {
|
|
297
|
+
prov.provision.mockResolvedValue({ ok: true, action: "cloned", folder: "/x" });
|
|
298
|
+
expect(await cmdWs(["new", "o/r@dev"])).toBe(0);
|
|
299
|
+
expect(prov.provision).toHaveBeenCalledWith(
|
|
300
|
+
{ owner: "o", repo: "r", branch: "dev" },
|
|
301
|
+
{ wsRoot: undefined },
|
|
302
|
+
);
|
|
303
|
+
expect(out.join("")).toContain("cloned /x");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("new: branch-not-found hints at --create, and --create falls back to createBranch", async () => {
|
|
307
|
+
prov.provision.mockResolvedValue({ ok: false, action: "error", reason: "branch-not-found", error: "nope" });
|
|
308
|
+
expect(await cmdWs(["new", "o/r@dev"])).toBe(1);
|
|
309
|
+
expect(err.join("")).toContain("--create");
|
|
310
|
+
expect(prov.createBranch).not.toHaveBeenCalled();
|
|
311
|
+
|
|
312
|
+
prov.createBranch.mockResolvedValue({ ok: true, action: "created", folder: "/y" });
|
|
313
|
+
expect(await cmdWs(["new", "o/r@dev", "--create"])).toBe(0);
|
|
314
|
+
expect(out.join("")).toContain("created /y");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("new: usage and parse errors", async () => {
|
|
318
|
+
await expect(cmdWs(["new"])).rejects.toThrow(/usage: ay ws new/);
|
|
319
|
+
await expect(cmdWs(["new", "///"])).rejects.toThrow(/cannot parse/);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("fork: --from is resolved and passed through with --wip", async () => {
|
|
323
|
+
prov.forkWorktree.mockResolvedValue({ ok: true, action: "forked", folder: "/f" });
|
|
324
|
+
expect(await cmdWs(["fork", "nb", "--from", root, "--wip"])).toBe(0);
|
|
325
|
+
expect(prov.forkWorktree).toHaveBeenCalledWith({
|
|
326
|
+
fromCwd: path.resolve(root),
|
|
327
|
+
branch: "nb",
|
|
328
|
+
wsRoot: undefined,
|
|
329
|
+
wip: true,
|
|
330
|
+
});
|
|
331
|
+
expect(out.join("")).toContain("forked /f");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("help aliases and the list alias dispatch", async () => {
|
|
335
|
+
for (const h of ["help", "--help", "-h"]) {
|
|
336
|
+
out.length = 0;
|
|
337
|
+
expect(await cmdWs([h])).toBe(0);
|
|
338
|
+
expect(out.join("")).toContain("ay ws ls");
|
|
339
|
+
}
|
|
340
|
+
mkCheckout("o/r/tree/main");
|
|
341
|
+
out.length = 0;
|
|
342
|
+
expect(await cmdWs(["list"])).toBe(0);
|
|
343
|
+
expect(out.join("")).toContain("o/r@main");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("ls/status: live agents from the registry produce counts and the ay-ls hint", async () => {
|
|
347
|
+
const dir = mkCheckout("o/r/tree/main");
|
|
348
|
+
const home = process.env.AGENT_YES_HOME!;
|
|
349
|
+
mkdirSync(home, { recursive: true });
|
|
350
|
+
writeFileSync(
|
|
351
|
+
path.join(home, "pids.jsonl"),
|
|
352
|
+
JSON.stringify({
|
|
353
|
+
pid: process.pid,
|
|
354
|
+
cli: "claude",
|
|
355
|
+
prompt: null,
|
|
356
|
+
cwd: path.join(dir, "sub"),
|
|
357
|
+
log_file: null,
|
|
358
|
+
status: "active",
|
|
359
|
+
exit_code: null,
|
|
360
|
+
exit_reason: null,
|
|
361
|
+
started_at: 1,
|
|
362
|
+
}) + "\n",
|
|
363
|
+
);
|
|
364
|
+
expect(await cmdWs(["ls"])).toBe(0);
|
|
365
|
+
expect(err.join("")).toContain("ay ls --cwd");
|
|
366
|
+
out.length = 0;
|
|
367
|
+
err.length = 0;
|
|
368
|
+
expect(await cmdWs(["status", dir])).toBe(0);
|
|
369
|
+
expect(out.join("")).toContain("agents: 1 live");
|
|
370
|
+
expect(err.join("")).toContain(`ay ls --cwd ${dir}`);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("status: defaults to cwd, and honors --path / --spec modes", async () => {
|
|
374
|
+
const dir = mkCheckout("o/r/tree/main");
|
|
375
|
+
const prevCwd = process.cwd();
|
|
376
|
+
process.chdir(dir);
|
|
377
|
+
try {
|
|
378
|
+
expect(await cmdWs(["status"])).toBe(0);
|
|
379
|
+
expect(out.join("")).toContain("spec: o/r@main");
|
|
380
|
+
} finally {
|
|
381
|
+
process.chdir(prevCwd);
|
|
382
|
+
}
|
|
383
|
+
out.length = 0;
|
|
384
|
+
expect(await cmdWs(["status", dir, "--path"])).toBe(0);
|
|
385
|
+
expect(out.join("")).toContain("branch: main");
|
|
386
|
+
out.length = 0;
|
|
387
|
+
expect(await cmdWs(["status", "o/r@main", "--spec", "--json"])).toBe(0);
|
|
388
|
+
expect(JSON.parse(out.join("")).workspace.path).toBe(dir);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("new: a non-branch failure reports without the --create hint", async () => {
|
|
392
|
+
prov.provision.mockResolvedValue({ ok: false, action: "error", reason: "repo-not-found", error: "gone" });
|
|
393
|
+
expect(await cmdWs(["new", "o/r"])).toBe(1);
|
|
394
|
+
const msg = err.join("");
|
|
395
|
+
expect(msg).toContain("repo-not-found");
|
|
396
|
+
expect(msg).not.toContain("--create");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("fork: --from=<eq-form> and a stale AGENT_YES_PID both resolve", async () => {
|
|
400
|
+
prov.forkWorktree.mockResolvedValue({ ok: true, action: "forked", folder: "/f" });
|
|
401
|
+
expect(await cmdWs(["fork", "nb", `--from=${root}`])).toBe(0);
|
|
402
|
+
expect(prov.forkWorktree).toHaveBeenCalledWith(
|
|
403
|
+
expect.objectContaining({ fromCwd: path.resolve(root) }),
|
|
404
|
+
);
|
|
405
|
+
// An AGENT_YES_PID with no matching registry record falls back to cwd.
|
|
406
|
+
const envBackup = process.env.AGENT_YES_PID;
|
|
407
|
+
process.env.AGENT_YES_PID = "999999999";
|
|
408
|
+
try {
|
|
409
|
+
expect(await cmdWs(["fork", "nb2"])).toBe(0);
|
|
410
|
+
expect(prov.forkWorktree).toHaveBeenLastCalledWith(
|
|
411
|
+
expect.objectContaining({ fromCwd: path.resolve(process.cwd()) }),
|
|
412
|
+
);
|
|
413
|
+
} finally {
|
|
414
|
+
if (envBackup === undefined) delete process.env.AGENT_YES_PID;
|
|
415
|
+
else process.env.AGENT_YES_PID = envBackup;
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("fork: defaults --from to cwd (no agent env), surfaces failure", async () => {
|
|
420
|
+
const envBackup = process.env.AGENT_YES_PID;
|
|
421
|
+
delete process.env.AGENT_YES_PID;
|
|
422
|
+
try {
|
|
423
|
+
prov.forkWorktree.mockResolvedValue({ ok: false, error: "no origin" });
|
|
424
|
+
expect(await cmdWs(["fork", "nb"])).toBe(1);
|
|
425
|
+
expect(prov.forkWorktree).toHaveBeenCalledWith(
|
|
426
|
+
expect.objectContaining({ fromCwd: path.resolve(process.cwd()), wip: false }),
|
|
427
|
+
);
|
|
428
|
+
expect(err.join("")).toContain("fork failed: no origin");
|
|
429
|
+
await expect(cmdWs(["fork"])).rejects.toThrow(/usage: ay ws fork/);
|
|
430
|
+
await expect(cmdWs(["fork", "nb", "--from"])).rejects.toThrow(/requires a value/);
|
|
431
|
+
} finally {
|
|
432
|
+
if (envBackup !== undefined) process.env.AGENT_YES_PID = envBackup;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|