@webappwiz/arbor 0.0.30 → 0.0.32

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.
Files changed (120) hide show
  1. package/README.md +2 -2
  2. package/add.d.ts +23 -0
  3. package/age.d.ts +2 -0
  4. package/arbor.d.ts +11 -0
  5. package/attachments.d.ts +36 -0
  6. package/claim.d.ts +11 -0
  7. package/config.d.ts +55 -0
  8. package/config.js +7 -0
  9. package/dev/assets.d.ts +11 -0
  10. package/dev.d.ts +48 -0
  11. package/escalate.d.ts +28 -0
  12. package/exit.d.ts +42 -0
  13. package/git.d.ts +82 -0
  14. package/index.d.ts +2 -0
  15. package/index.js +4763 -0
  16. package/journal.d.ts +29 -0
  17. package/list.d.ts +18 -0
  18. package/load-config.d.ts +15 -0
  19. package/log.d.ts +14 -0
  20. package/merge.d.ts +24 -0
  21. package/package.json +15 -32
  22. package/path.d.ts +13 -0
  23. package/plan.d.ts +83 -0
  24. package/remove.d.ts +19 -0
  25. package/repository.d.ts +25 -0
  26. package/retry.d.ts +17 -0
  27. package/shell.d.ts +17 -0
  28. package/show.d.ts +53 -0
  29. package/snapshot.d.ts +36 -0
  30. package/table.d.ts +5 -0
  31. package/todo.d.ts +202 -0
  32. package/todos.d.ts +75 -0
  33. package/wait.d.ts +22 -0
  34. package/worktree-service.d.ts +51 -0
  35. package/worktree.d.ts +95 -0
  36. package/AGENTS.md +0 -7
  37. package/add.test.ts +0 -125
  38. package/add.ts +0 -192
  39. package/age.ts +0 -11
  40. package/arbor.test.ts +0 -145
  41. package/arbor.ts +0 -429
  42. package/assets.d.ts +0 -9
  43. package/attachments.ts +0 -87
  44. package/build.ts +0 -98
  45. package/claim.test.ts +0 -89
  46. package/claim.ts +0 -73
  47. package/components.json +0 -21
  48. package/config.ts +0 -58
  49. package/dev/api.ts +0 -120
  50. package/dev/app.test.tsx +0 -645
  51. package/dev/app.tsx +0 -80
  52. package/dev/assets.ts +0 -16
  53. package/dev/build/main.txt +0 -68
  54. package/dev/build/shell.txt +0 -21
  55. package/dev/build/styles.txt +0 -2263
  56. package/dev/components/ui/badge.tsx +0 -51
  57. package/dev/components/ui/button.tsx +0 -57
  58. package/dev/components/ui/dialog.tsx +0 -154
  59. package/dev/components/ui/empty.tsx +0 -100
  60. package/dev/components/ui/input-group.tsx +0 -155
  61. package/dev/components/ui/input.tsx +0 -19
  62. package/dev/components/ui/progress.tsx +0 -80
  63. package/dev/components/ui/textarea.tsx +0 -17
  64. package/dev/components/ui/toast.tsx +0 -227
  65. package/dev/feed.ts +0 -69
  66. package/dev/files.tsx +0 -191
  67. package/dev/index.html +0 -21
  68. package/dev/lib/utils.ts +0 -7
  69. package/dev/main.tsx +0 -22
  70. package/dev/markdown.test.tsx +0 -133
  71. package/dev/markdown.tsx +0 -124
  72. package/dev/mentions.tsx +0 -319
  73. package/dev/shadcn.css +0 -649
  74. package/dev/styles.css +0 -107
  75. package/dev/tags.tsx +0 -44
  76. package/dev/tasks.tsx +0 -151
  77. package/dev/todos.tsx +0 -759
  78. package/dev.test.ts +0 -439
  79. package/dev.ts +0 -386
  80. package/e2e.test.ts +0 -256
  81. package/escalate.test.ts +0 -76
  82. package/escalate.ts +0 -112
  83. package/exit.test.ts +0 -71
  84. package/exit.ts +0 -74
  85. package/git.ts +0 -294
  86. package/index.ts +0 -9
  87. package/journal.ts +0 -89
  88. package/list.test.ts +0 -82
  89. package/list.ts +0 -137
  90. package/load-config.test.ts +0 -121
  91. package/load-config.ts +0 -62
  92. package/log.test.ts +0 -65
  93. package/log.ts +0 -40
  94. package/merge.test.ts +0 -332
  95. package/merge.ts +0 -283
  96. package/path.test.ts +0 -35
  97. package/path.ts +0 -38
  98. package/plan.test.ts +0 -318
  99. package/plan.ts +0 -302
  100. package/progress.test.ts +0 -54
  101. package/progress.ts +0 -31
  102. package/remove.test.ts +0 -68
  103. package/remove.ts +0 -88
  104. package/repository.ts +0 -83
  105. package/retry.test.ts +0 -43
  106. package/retry.ts +0 -48
  107. package/shell.ts +0 -44
  108. package/show.test.ts +0 -91
  109. package/show.ts +0 -151
  110. package/snapshot.test.ts +0 -33
  111. package/snapshot.ts +0 -84
  112. package/table.ts +0 -20
  113. package/testing.ts +0 -229
  114. package/todo.test.ts +0 -490
  115. package/todo.ts +0 -834
  116. package/wait.test.ts +0 -61
  117. package/wait.ts +0 -77
  118. package/worktree-service.test.ts +0 -158
  119. package/worktree-service.ts +0 -193
  120. package/worktree.ts +0 -239
package/snapshot.ts DELETED
@@ -1,84 +0,0 @@
1
- import { basename } from "node:path";
2
- import type { Fs } from "webappwiz/system";
3
- import { type Details, TaskDetails } from "./show";
4
- import type { TodoState, Todos } from "./todo";
5
- import type { WorktreeService } from "./worktree-service";
6
-
7
- /**
8
- * Everything one page shows: `todo list`, and `list` and `show` for each
9
- * task.
10
- */
11
- export interface Snapshot {
12
- /** The repository's directory name, so a page among many says whose it is. */
13
- repo: string;
14
- /** Where the repository sits, with the home directory as `~`. */
15
- path: string;
16
- /** Past this age a todo is offered for removal rather than recommended. */
17
- todoStalenessMs: number;
18
- todos: TodoState[];
19
- tasks: Details[];
20
- }
21
-
22
- /**
23
- * Reads the whole repo the way the CLI's read commands do. Takes no lease, so
24
- * the page can sit open on a live tree without disturbing the agent in it.
25
- */
26
- export async function snapshot({
27
- service,
28
- todos,
29
- fs,
30
- home,
31
- }: {
32
- service: WorktreeService;
33
- todos: Todos;
34
- fs: Fs;
35
- /** The home directory to show as `~`, when there is one. */
36
- home?: string;
37
- }): Promise<Snapshot> {
38
- const details = new TaskDetails({ fs });
39
- const tasks: Details[] = [];
40
- for (const worktree of await service.list()) {
41
- tasks.push(await details.get(worktree));
42
- }
43
- return {
44
- repo: basename(service.git.root),
45
- path: tilde(service.git.root, home),
46
- todoStalenessMs: service.config.todoStalenessMs,
47
- todos: (await todos.all()).map((todo) => todo.state),
48
- tasks,
49
- };
50
- }
51
-
52
- /**
53
- * What the page is actually showing, minus the fields that move on their own.
54
- * `age` ticks every minute, and hashing it would push to every open page for
55
- * nothing.
56
- */
57
- export function fingerprint({ todos, tasks }: Snapshot): string {
58
- return JSON.stringify([
59
- todos,
60
- tasks.map((task) => [
61
- task.task,
62
- task.status,
63
- task.lease,
64
- task.ahead,
65
- task.added,
66
- task.removed,
67
- task.escalation,
68
- task.review,
69
- task.plan,
70
- task.planProblems,
71
- ]),
72
- ]);
73
- }
74
-
75
- /** `root` with a leading `home` written as `~`, the way a shell prompt shows it. */
76
- function tilde(root: string, home: string | undefined): string {
77
- if (!home) {
78
- return root;
79
- }
80
- if (root === home) {
81
- return "~";
82
- }
83
- return root.startsWith(`${home}/`) ? `~${root.slice(home.length)}` : root;
84
- }
package/table.ts DELETED
@@ -1,20 +0,0 @@
1
- import { color } from "webappwiz/log";
2
-
3
- /**
4
- * Columns padded to their widest cell. Padding goes by visible width, not
5
- * string length: cells carry color codes.
6
- */
7
- export function table(header: string[], rows: string[][]): string {
8
- const width = (cell: string): number => color.strip(cell).length;
9
- const widths = header.map((heading, i) =>
10
- Math.max(heading.length, ...rows.map((row) => width(row[i] ?? ""))),
11
- );
12
- const line = (cells: string[]): string =>
13
- cells
14
- .map((cell, i) =>
15
- cell.padEnd((widths[i] ?? 0) + cell.length - width(cell)),
16
- )
17
- .join(" ")
18
- .trimEnd();
19
- return [color.dim(line(header)), ...rows.map(line)].join("\n");
20
- }
package/testing.ts DELETED
@@ -1,229 +0,0 @@
1
- import { expect } from "bun:test";
2
- import { mkdtemp, realpath, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { CounterIdProvider } from "webappwiz/id";
6
- import { color, MemoryLogger } from "webappwiz/log";
7
- import { FileLock, NodeFs, NodePs } from "webappwiz/system";
8
- import { FakeProcess } from "webappwiz/system/testing";
9
- import type { Config } from "./config";
10
- import { assets } from "./dev/assets";
11
- import { Exit, type Reason } from "./exit";
12
- import { Git } from "./git";
13
- import { Journal } from "./journal";
14
- import { Shell } from "./shell";
15
- import { Todos } from "./todo";
16
- import { WorktreeService } from "./worktree-service";
17
-
18
- /**
19
- * A throwaway git repo with one commit on `main`, holding every dependency an
20
- * arbor command runs against, wired to it. A command takes the whole thing as
21
- * its deps: `await add(deps, "alpha")`.
22
- *
23
- * It coordinates the dependencies and nothing else. What a test is about, the
24
- * tasks, the commits, the config it needs to differ, the test does itself.
25
- */
26
- export class Testing implements AsyncDisposable {
27
- readonly root: string;
28
- readonly arborDir: string;
29
- readonly fs = new NodeFs();
30
- /** Spawns for real; its exits and signals are the fake process's to keep. */
31
- readonly proc = new FakeProcess();
32
- readonly ps: NodePs;
33
- readonly log = new MemoryLogger();
34
- /** Stored file names count up from 0, so a test can name them. */
35
- readonly ids = new CounterIdProvider();
36
- // The real ones, which publish: `dev` is tested by serving a page and
37
- // fetching it, and asserting on what comes back is the point.
38
- readonly assets = assets;
39
- /** Arbitrary but valid settings; a test changes what it cares about. */
40
- readonly config: Config;
41
- readonly git: Git;
42
- readonly service: WorktreeService;
43
- readonly shell: Shell;
44
- readonly journal: Journal;
45
- readonly lockPath: string;
46
- readonly lock: FileLock;
47
- readonly todos: Todos;
48
-
49
- private constructor(private readonly base: string) {
50
- this.root = join(base, "repo");
51
- this.arborDir = join(this.root, ".git", "arbor");
52
- this.ps = new NodePs({ proc: this.proc });
53
- this.config = {
54
- trunk: "main",
55
- worktreeRoot: `${this.root}-arbor`,
56
- postCheckout: null,
57
- postRewrite: null,
58
- // Keeps merge's gate green unless a test asks otherwise.
59
- preMerge: "true",
60
- postMerge: null,
61
- leaseStalenessMs: 90_000,
62
- removedCapacity: 50,
63
- mergeRetryCount: 2,
64
- logCapacity: 200,
65
- todoStalenessMs: 30 * 24 * 60 * 60 * 1000,
66
- };
67
- this.git = new Git(this.root, { ps: this.ps, fs: this.fs });
68
- this.service = new WorktreeService(this.git, this.config, this.arborDir, {
69
- fs: this.fs,
70
- ps: this.ps,
71
- });
72
- this.shell = new Shell({ ps: this.ps });
73
- this.journal = new Journal(
74
- join(this.arborDir, "log.jsonl"),
75
- this.config.logCapacity,
76
- { fs: this.fs },
77
- );
78
- this.lockPath = join(this.arborDir, "merge.lock");
79
- this.lock = new FileLock(this.lockPath, {
80
- fs: this.fs,
81
- ps: this.ps,
82
- log: this.log,
83
- stalenessMs: this.config.leaseStalenessMs,
84
- });
85
- this.todos = new Todos(
86
- join(this.arborDir, "todos"),
87
- new FileLock(join(this.arborDir, "todos.lock"), {
88
- fs: this.fs,
89
- ps: this.ps,
90
- log: this.log,
91
- }),
92
- { fs: this.fs, ids: this.ids },
93
- );
94
- }
95
-
96
- /** A repo of its own, seeded and ready for a command. */
97
- static async open(): Promise<Testing> {
98
- const base = await realpath(await mkdtemp(join(tmpdir(), "arbor-")));
99
- const testing = new Testing(base);
100
- await testing.seed();
101
- return testing;
102
- }
103
-
104
- /** Runs git in `cwd`, named apart from the `git` a command is handed. */
105
- async gitCli(cwd: string, ...args: string[]): Promise<string> {
106
- const { exitCode, stdout, stderr } = await this.ps.spawnCapture([
107
- "git",
108
- "-C",
109
- cwd,
110
- ...args,
111
- ]);
112
- if (exitCode !== 0) {
113
- throw new Error(`git ${args.join(" ")}: ${stderr}`);
114
- }
115
- return stdout.trim();
116
- }
117
-
118
- /** Writes a file in `cwd` and commits it. */
119
- async commit(
120
- cwd: string,
121
- file: string,
122
- content: string,
123
- message: string,
124
- ): Promise<void> {
125
- await this.fs.write(join(cwd, file), content);
126
- await this.gitCli(cwd, "add", file);
127
- await this.gitCli(cwd, "commit", "-m", message);
128
- }
129
-
130
- /** Everything logged so far, one line each, without colour. */
131
- out(): string {
132
- return color.strip(
133
- this.log.entries.map((entry) => String(entry.message)).join("\n"),
134
- );
135
- }
136
-
137
- /** Deletes the repo, its worktrees and the temporary directory holding them. */
138
- disposeAsync(): Promise<void> {
139
- return rm(this.base, { recursive: true, force: true });
140
- }
141
-
142
- // so a concurrent test can own its repo with `await using`, instead of
143
- // afterEach handing every test in the file the same one
144
- [Symbol.asyncDispose](): Promise<void> {
145
- return this.disposeAsync();
146
- }
147
-
148
- private async seed(): Promise<void> {
149
- await this.fs.mkdir(this.root);
150
- await this.gitCli(this.root, "init", "-b", "main");
151
- await this.gitCli(this.root, "config", "user.email", "arbor@example.com");
152
- await this.gitCli(this.root, "config", "user.name", "arbor");
153
- await this.gitCli(this.root, "config", "commit.gpgsign", "false");
154
- await this.commit(this.root, "README.md", "seed\n", "seed");
155
- await this.service.init();
156
- }
157
- }
158
-
159
- /** pid 1 always exists, so it stands in for another agent that is still running. */
160
- export const LIVE_PID = 1;
161
-
162
- /** What a refusal carries beyond its reason, when a test says. */
163
- export interface BailOptions {
164
- /** A fragment of the message the person reading it gets, or every one. */
165
- message?: string | string[];
166
- /** Everything the refusal reports alongside the reason. */
167
- data?: Record<string, unknown>;
168
- }
169
-
170
- declare module "bun:test" {
171
- interface Matchers<T> {
172
- /** Asserts the work refuses: `await expect(add(t, "x")).toBail("usage")`. */
173
- toBail(reason: Reason, expected?: BailOptions): Promise<void>;
174
- }
175
- }
176
-
177
- // Registered here because every test file that asserts a refusal already
178
- // imports `Testing`, and `expect.extend` has to have run before the first one.
179
- expect.extend({
180
- async toBail(work, reason, expected = {}) {
181
- const exit = await refusal(work);
182
- if (exit === null) {
183
- return {
184
- pass: false,
185
- message: () => `expected a ${reason} refusal, but it succeeded`,
186
- };
187
- }
188
- if (exit.reason !== reason) {
189
- return {
190
- pass: false,
191
- message: () =>
192
- `expected a ${reason} refusal, got ${exit.reason}: ${exit.message}`,
193
- };
194
- }
195
- const fragments = [expected.message ?? []].flat();
196
- const missing = fragments.filter((text) => !exit.message.includes(text));
197
- if (missing.length > 0) {
198
- return {
199
- pass: false,
200
- message: () =>
201
- `expected the ${reason} message to contain ${this.utils.printExpected(missing)}, got ${this.utils.printReceived(exit.message)}`,
202
- };
203
- }
204
- if (expected.data !== undefined && !this.equals(exit.data, expected.data)) {
205
- return {
206
- pass: false,
207
- message: () =>
208
- `expected the ${reason} data to be ${this.utils.printExpected(expected.data)}, got ${this.utils.printReceived(exit.data)}`,
209
- };
210
- }
211
- return { pass: true, message: () => `expected no ${reason} refusal` };
212
- },
213
- });
214
-
215
- /**
216
- * The `Exit` some work refused with, or null when it did not refuse. Anything
217
- * else it threw is a real failure and is left to blow up the test as itself.
218
- */
219
- async function refusal(work: unknown): Promise<Exit | null> {
220
- try {
221
- await work;
222
- return null;
223
- } catch (entry) {
224
- if (entry instanceof Exit) {
225
- return entry;
226
- }
227
- throw entry;
228
- }
229
- }