@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/add.ts DELETED
@@ -1,192 +0,0 @@
1
- import { color, type Logger } from "webappwiz/log";
2
- import { MarkdownWriter } from "webappwiz/md";
3
- import type { Fs } from "webappwiz/system";
4
- import type { Config } from "./config";
5
- import { fail } from "./exit";
6
- import { PLAN_FILE } from "./plan";
7
- import type { Shell } from "./shell";
8
- import type { Todo, Todos } from "./todo";
9
- import type { WorktreeService } from "./worktree-service";
10
-
11
- const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12
-
13
- export interface AddOptions {
14
- /** Branch the task starts from and merges onto. Defaults to the trunk. */
15
- base?: string;
16
- /**
17
- * The todos this task takes up: their words seed the plan's Goal, and
18
- * nobody else can take them while the task lives.
19
- */
20
- todos?: number[];
21
- }
22
-
23
- export async function add(
24
- {
25
- service,
26
- shell,
27
- config,
28
- log,
29
- fs,
30
- todos,
31
- }: {
32
- service: WorktreeService;
33
- shell: Shell;
34
- config: Config;
35
- log: Logger;
36
- fs: Fs;
37
- todos: Todos;
38
- },
39
- task: string,
40
- { base = config.trunk, todos: ids = [] }: AddOptions = {},
41
- ): Promise<void> {
42
- if (!NAME.test(task)) {
43
- fail(
44
- "usage",
45
- `invalid task name '${task}': use lowercase letters, digits and dashes`,
46
- { task },
47
- );
48
- }
49
-
50
- // A worktree gets its own empty submodule directories, so every task would
51
- // have to bootstrap them before anything builds. Refuse the repo instead.
52
- // Git only reads the superproject's top-level file, so there is nowhere else
53
- // to look; a stale one with no live gitlink refuses too, which is the safe
54
- // direction to be wrong in.
55
- if (await fs.exists(`${service.git.root}/.gitmodules`)) {
56
- fail(
57
- "usage",
58
- "this repo has git submodules (.gitmodules), which arbor does not support: worktrees do not share them",
59
- { task },
60
- );
61
- }
62
-
63
- const found = await service.find(task);
64
- if (found.status === "stray") {
65
- fail(
66
- "exists",
67
- `branch ${found.branch} exists without a worktree: run \`arbor remove ${task}\` first`,
68
- { task, branch: found.branch },
69
- );
70
- }
71
- if (!found.gone) {
72
- fail(
73
- "exists",
74
- `task '${task}' already exists: run \`arbor claim ${task}\``,
75
- { task, worktree: found.path },
76
- );
77
- }
78
-
79
- // Looked up before anything is created, so a todo that is gone or taken
80
- // refuses the whole add instead of leaving a tree behind.
81
- const taken: Todo[] = [];
82
- for (const id of new Set(ids)) {
83
- const todo = await todos.find(id);
84
- if (todo.takenBy) {
85
- await todo.take(task); // refuses, naming the task that has it
86
- }
87
- taken.push(todo);
88
- }
89
-
90
- const added = await service.add(task, { base });
91
- if (added.code !== 0) {
92
- // git's own "invalid reference" sends people to the branch, when what is
93
- // wrong is which branch arbor thinks the trunk is.
94
- const missingTrunk =
95
- base === config.trunk && !(await service.git.branchExists(base));
96
- fail(
97
- "usage",
98
- missingTrunk
99
- ? `trunk '${base}' is not a branch in this repo: set \`trunk\` in arbor.config.ts`
100
- : `git worktree add failed: ${added.stderr}`,
101
- { task },
102
- );
103
- }
104
-
105
- const worktree = await (await service.find(task)).take({ base });
106
-
107
- // The plan is scratch for one task and must never land on trunk. Excluding
108
- // it through the shared `.git` covers every worktree at once and leaves the
109
- // repo's own .gitignore, which arbor would have to commit to, alone.
110
- const info = `${await service.git.commonDir()}/info`;
111
- const excluded = await fs.read(`${info}/exclude`).catch(() => "");
112
- if (!excluded.split("\n").includes(PLAN_FILE)) {
113
- await fs.mkdir(info);
114
- await fs.write(
115
- `${info}/exclude`,
116
- `${excluded.trimEnd()}\n${PLAN_FILE}\n`.trimStart(),
117
- );
118
- }
119
-
120
- await fs.write(
121
- `${worktree.path}/${PLAN_FILE}`,
122
- PLAN(task, taken.length === 0 ? null : goal(taken)),
123
- );
124
- for (const todo of taken) {
125
- await todo.take(task);
126
- }
127
-
128
- // A fresh worktree shares no untracked files with the repo: no node_modules,
129
- // no .env. That is what the hook is for.
130
- if (config.postCheckout) {
131
- const { exitCode } = await shell.stream(
132
- config.postCheckout,
133
- worktree.path,
134
- {
135
- env: {
136
- ARBOR_TASK: task,
137
- ARBOR_WORKTREE: worktree.path,
138
- ARBOR_TRUNK: config.trunk,
139
- },
140
- },
141
- );
142
- if (exitCode !== 0) {
143
- // The worktree stays. Rolling back would throw away a tree the agent
144
- // can fix by hand and re-run the hook in.
145
- fail(
146
- "hook_failed",
147
- `postCheckout hook failed (exit ${exitCode}); worktree left in place at ${worktree.path}`,
148
- { task, worktree: worktree.path },
149
- );
150
- }
151
- }
152
-
153
- log.info(
154
- `${color.green("added")} ${task}\n worktree: ${worktree.path}\n branch: ${worktree.branch}\n base: ${base}`,
155
- );
156
- }
157
-
158
- /**
159
- * Todos as a Goal: each one's subject and detail, and each file attached to it
160
- * by path, which stay readable until the task lands and takes the todo with
161
- * it. One todo is the Goal as it stands; several are each named by id.
162
- */
163
- function goal(todos: Todo[]): string {
164
- return todos
165
- .map((todo) => {
166
- const subject =
167
- todos.length === 1 ? todo.subject : `Todo ${todo.id}: ${todo.subject}`;
168
- return [
169
- subject,
170
- todo.text,
171
- todo.files.map((path) => `Attached: \`${path}\``).join("\n"),
172
- ]
173
- .filter(Boolean)
174
- .join("\n\n");
175
- })
176
- .join("\n\n");
177
- }
178
-
179
- /** The plan a fresh task starts with. `## Goal` is the todos it takes up, if
180
- * any; it and `## Files` are otherwise left empty on purpose: `arbor show` nags
181
- * until the agent fills them in. */
182
- function PLAN(task: string, goal: string | null): string {
183
- const writer = new MarkdownWriter().heading(1, task).heading(2, "Goal");
184
- if (goal !== null) {
185
- writer.text(goal);
186
- }
187
- return writer
188
- .heading(2, "Files")
189
- .heading(2, "Next")
190
- .checklist("fill in Goal and list the steps here as `- [ ]` items")
191
- .toString();
192
- }
package/age.ts DELETED
@@ -1,11 +0,0 @@
1
- /** How long ago an ISO timestamp was, at the coarsest unit that still reads. */
2
- export function age(since: string): string {
3
- const minutes = Math.floor((Date.now() - Date.parse(since)) / 60_000);
4
- if (minutes < 60) {
5
- return `${minutes}m`;
6
- }
7
- if (minutes < 60 * 24) {
8
- return `${Math.floor(minutes / 60)}h`;
9
- }
10
- return `${Math.floor(minutes / (60 * 24))}d`;
11
- }
package/arbor.test.ts DELETED
@@ -1,145 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { basename, join } from "node:path";
3
- import { arbor } from "./arbor";
4
- import { Testing } from "./testing";
5
-
6
- /**
7
- * The cli in this process, run with fakes: the same commands `e2e.test.ts`
8
- * spawns, minus the subprocess. What it proves is the wiring, that every
9
- * dependency an action uses arrives through `run`.
10
- */
11
- describe("arbor cli", () => {
12
- it("runs its commands against the dependencies it is given", async () => {
13
- await using env = await Testing.open();
14
- env.ps.cd(env.root);
15
- const deps = {
16
- log: env.log,
17
- fs: env.fs,
18
- ps: env.ps,
19
- assets: env.assets,
20
- };
21
-
22
- await arbor.run(deps, ["add", "alpha"]);
23
- await arbor.run(deps, ["list", "--json"]);
24
-
25
- const rows = JSON.parse(String(env.log.entries.at(-1)?.message));
26
- expect(rows).toMatchObject([{ task: "alpha", status: "working" }]);
27
- expect(await env.fs.exists(join(`${env.root}-arbor`, "alpha"))).toBe(true);
28
- });
29
-
30
- it("escalates a task and edits its todos", async () => {
31
- await using env = await Testing.open();
32
- env.ps.cd(env.root);
33
- const deps = {
34
- log: env.log,
35
- fs: env.fs,
36
- ps: env.ps,
37
- assets: env.assets,
38
- };
39
- await arbor.run(deps, ["add", "alpha"]);
40
- const worktree = await env.service.find("alpha");
41
- await env.fs.write(
42
- join(worktree.path, "ARBOR.md"),
43
- "# alpha\n\n## Blocked\n\n- [ ] 1. Open it.\n",
44
- );
45
- await env.fs.write(join(env.root, "a.png"), "a");
46
- env.ps.cd(worktree.path);
47
- await arbor.run(deps, ["escalate", "needs a person"]);
48
-
49
- env.ps.cd(env.root);
50
- await arbor.run(deps, [
51
- "todo",
52
- "add",
53
- "write docs",
54
- "the CLI first",
55
- "--file",
56
- "a.png",
57
- ]);
58
- const [attached = ""] = (await env.todos.find(1)).files;
59
- await arbor.run(deps, [
60
- "todo",
61
- "update",
62
- "1",
63
- "--subject",
64
- "write the docs",
65
- "--position",
66
- "1",
67
- "--remove-file",
68
- basename(attached),
69
- ]);
70
- expect((await env.todos.find(1)).state).toMatchObject({
71
- subject: "write the docs",
72
- text: "the CLI first",
73
- position: 1,
74
- files: [],
75
- });
76
-
77
- await arbor.run(deps, ["log", "--json"]);
78
- const entries = JSON.parse(String(env.log.entries.at(-1)?.message));
79
- expect(entries.map((entry: { action: string }) => entry.action)).toContain(
80
- "escalate",
81
- );
82
- });
83
-
84
- it("asks for a review from the command line", async () => {
85
- await using env = await Testing.open();
86
- env.ps.cd(env.root);
87
- const deps = {
88
- log: env.log,
89
- fs: env.fs,
90
- ps: env.ps,
91
- assets: env.assets,
92
- };
93
- await arbor.run(deps, ["add", "alpha"]);
94
-
95
- await arbor.run(deps, [
96
- "escalate",
97
- "check the header",
98
- "--task",
99
- "alpha",
100
- "--review",
101
- ]);
102
-
103
- const worktree = await env.service.find("alpha");
104
- expect(worktree.state?.escalations?.at(-1)?.review).toBe("1");
105
- expect(await env.fs.read(join(worktree.path, "ARBOR.md"))).toContain(
106
- "- [ ] 1. Ready to merge?\n check the header\n",
107
- );
108
- });
109
-
110
- it("reports a refusal as a reason, a message and an exit code", async () => {
111
- await using env = await Testing.open();
112
- env.ps.cd(env.root);
113
-
114
- await arbor.run(
115
- {
116
- log: env.log,
117
- fs: env.fs,
118
- ps: env.ps,
119
- assets: env.assets,
120
- },
121
- ["claim", "nope"],
122
- );
123
-
124
- expect(env.out()).toContain('"reason":"not_found"');
125
- expect(env.proc.lastExit()).toBe(8);
126
- });
127
-
128
- it("refuses a port that cannot exist rather than trying to listen", async () => {
129
- await using env = await Testing.open();
130
- env.ps.cd(env.root);
131
-
132
- await arbor.run(
133
- {
134
- log: env.log,
135
- fs: env.fs,
136
- ps: env.ps,
137
- assets: env.assets,
138
- },
139
- ["dev", "--port", "99999"],
140
- );
141
-
142
- expect(env.out()).not.toContain(" reply ");
143
- expect(env.proc.lastExit()).toBe(1);
144
- });
145
- });