@webappwiz/arbor 0.0.30 → 0.0.31

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 (118) hide show
  1. package/add.d.ts +23 -0
  2. package/age.d.ts +2 -0
  3. package/arbor.d.ts +11 -0
  4. package/attachments.d.ts +36 -0
  5. package/claim.d.ts +11 -0
  6. package/config.d.ts +55 -0
  7. package/config.js +7 -0
  8. package/dev/assets.d.ts +11 -0
  9. package/dev.d.ts +48 -0
  10. package/escalate.d.ts +28 -0
  11. package/exit.d.ts +42 -0
  12. package/git.d.ts +77 -0
  13. package/index.d.ts +2 -0
  14. package/index.js +4712 -0
  15. package/journal.d.ts +29 -0
  16. package/list.d.ts +18 -0
  17. package/load-config.d.ts +15 -0
  18. package/log.d.ts +14 -0
  19. package/merge.d.ts +24 -0
  20. package/package.json +15 -32
  21. package/path.d.ts +13 -0
  22. package/plan.d.ts +83 -0
  23. package/remove.d.ts +19 -0
  24. package/repository.d.ts +25 -0
  25. package/retry.d.ts +17 -0
  26. package/shell.d.ts +17 -0
  27. package/show.d.ts +53 -0
  28. package/snapshot.d.ts +35 -0
  29. package/table.d.ts +5 -0
  30. package/todo.d.ts +264 -0
  31. package/wait.d.ts +22 -0
  32. package/worktree-service.d.ts +51 -0
  33. package/worktree.d.ts +95 -0
  34. package/AGENTS.md +0 -7
  35. package/add.test.ts +0 -125
  36. package/add.ts +0 -192
  37. package/age.ts +0 -11
  38. package/arbor.test.ts +0 -145
  39. package/arbor.ts +0 -429
  40. package/assets.d.ts +0 -9
  41. package/attachments.ts +0 -87
  42. package/build.ts +0 -98
  43. package/claim.test.ts +0 -89
  44. package/claim.ts +0 -73
  45. package/components.json +0 -21
  46. package/config.ts +0 -58
  47. package/dev/api.ts +0 -120
  48. package/dev/app.test.tsx +0 -645
  49. package/dev/app.tsx +0 -80
  50. package/dev/assets.ts +0 -16
  51. package/dev/build/main.txt +0 -68
  52. package/dev/build/shell.txt +0 -21
  53. package/dev/build/styles.txt +0 -2263
  54. package/dev/components/ui/badge.tsx +0 -51
  55. package/dev/components/ui/button.tsx +0 -57
  56. package/dev/components/ui/dialog.tsx +0 -154
  57. package/dev/components/ui/empty.tsx +0 -100
  58. package/dev/components/ui/input-group.tsx +0 -155
  59. package/dev/components/ui/input.tsx +0 -19
  60. package/dev/components/ui/progress.tsx +0 -80
  61. package/dev/components/ui/textarea.tsx +0 -17
  62. package/dev/components/ui/toast.tsx +0 -227
  63. package/dev/feed.ts +0 -69
  64. package/dev/files.tsx +0 -191
  65. package/dev/index.html +0 -21
  66. package/dev/lib/utils.ts +0 -7
  67. package/dev/main.tsx +0 -22
  68. package/dev/markdown.test.tsx +0 -133
  69. package/dev/markdown.tsx +0 -124
  70. package/dev/mentions.tsx +0 -319
  71. package/dev/shadcn.css +0 -649
  72. package/dev/styles.css +0 -107
  73. package/dev/tags.tsx +0 -44
  74. package/dev/tasks.tsx +0 -151
  75. package/dev/todos.tsx +0 -759
  76. package/dev.test.ts +0 -439
  77. package/dev.ts +0 -386
  78. package/e2e.test.ts +0 -256
  79. package/escalate.test.ts +0 -76
  80. package/escalate.ts +0 -112
  81. package/exit.test.ts +0 -71
  82. package/exit.ts +0 -74
  83. package/git.ts +0 -294
  84. package/index.ts +0 -9
  85. package/journal.ts +0 -89
  86. package/list.test.ts +0 -82
  87. package/list.ts +0 -137
  88. package/load-config.test.ts +0 -121
  89. package/load-config.ts +0 -62
  90. package/log.test.ts +0 -65
  91. package/log.ts +0 -40
  92. package/merge.test.ts +0 -332
  93. package/merge.ts +0 -283
  94. package/path.test.ts +0 -35
  95. package/path.ts +0 -38
  96. package/plan.test.ts +0 -318
  97. package/plan.ts +0 -302
  98. package/progress.test.ts +0 -54
  99. package/progress.ts +0 -31
  100. package/remove.test.ts +0 -68
  101. package/remove.ts +0 -88
  102. package/repository.ts +0 -83
  103. package/retry.test.ts +0 -43
  104. package/retry.ts +0 -48
  105. package/shell.ts +0 -44
  106. package/show.test.ts +0 -91
  107. package/show.ts +0 -151
  108. package/snapshot.test.ts +0 -33
  109. package/snapshot.ts +0 -84
  110. package/table.ts +0 -20
  111. package/testing.ts +0 -229
  112. package/todo.test.ts +0 -490
  113. package/todo.ts +0 -834
  114. package/wait.test.ts +0 -61
  115. package/wait.ts +0 -77
  116. package/worktree-service.test.ts +0 -158
  117. package/worktree-service.ts +0 -193
  118. 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
- }