@webappwiz/arbor 0.0.12 → 0.0.14

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/README.md CHANGED
@@ -80,7 +80,7 @@ linear.
80
80
  back would leave its index and files on the old commit.
81
81
  5. Discards the task (worktree, branch and record) exactly as `rm`
82
82
  would. The work is on trunk, so the tree has nothing left to hold, and
83
- `arbor ls` stays a list of live work rather than a graveyard of landed
83
+ `arbor list` stays a list of live work rather than a graveyard of landed
84
84
  tasks. The agent's own directory goes with it, so the success message
85
85
  prints the main tree to `cd` back to.
86
86
 
@@ -105,7 +105,7 @@ Removal leaves a tombstone in `.git/arbor/removed/` so a second `rm` can say
105
105
  and drops the oldest as new ones arrive, so a long-forgotten task reports
106
106
  `not_found` again.
107
107
 
108
- ### `arbor ls [--json]`
108
+ ### `arbor list [--json]`
109
109
 
110
110
  Every task: name, status, lease (`held`/`stale`/`none`), commits ahead of
111
111
  trunk, age. A corrupt record shows as `unknown` instead of taking
@@ -113,7 +113,7 @@ down the listing; a record whose worktree vanished shows as `orphaned`.
113
113
 
114
114
  ### `arbor show <task> [--json]`
115
115
 
116
- One task in full: the row `ls` would print for it, plus the `ARBOR.md`
116
+ One task in full: the row `list` would print for it, plus the `ARBOR.md`
117
117
  its agent keeps at the worktree root and the reason behind an `escalated`
118
118
  status.
119
119
 
@@ -130,7 +130,7 @@ ARBOR.md
130
130
  ...
131
131
  ```
132
132
 
133
- `ls` says a task exists; this says what it is doing. Like `path`, it takes no
133
+ `list` says a task exists; this says what it is doing. Like `path`, it takes no
134
134
  lease, so reading another agent's tree cannot knock it off its own work the way
135
135
  `claim` would. A task with no `ARBOR.md` is called out rather than passed over
136
136
  in silence: it is the one thing that makes the work resumable.
@@ -171,14 +171,14 @@ WHEN ACTION TASK RESULT
171
171
  1h merge alpha ok
172
172
  ```
173
173
 
174
- `ls` is what still exists; this is what happened. Entries outlive their tasks:
174
+ `list` is what still exists; this is what happened. Entries outlive their tasks:
175
175
  a successful `merge` and a `rm` both take the record with them, so this is
176
176
  the only thing that remembers a task landed at all. The last 200 are kept
177
177
  (`logCapacity`) in `.git/arbor/log.jsonl`.
178
178
 
179
179
  ### `arbor dev [--port 4269]`
180
180
 
181
- `ls`, `show` and `log` in a browser, on `http://localhost:4269`, reloading
181
+ `list`, `show` and `log` in a browser, on `http://localhost:4269`, reloading
182
182
  themselves as tasks change. Read-only, and takes no lease.
183
183
 
184
184
  ### `arbor path [task]`
package/arbor.d.ts CHANGED
@@ -6,9 +6,9 @@ import { type Repository } from "./repository.js";
6
6
  /** Everything `arbor` is run with, before the repository middleware adds to it. */
7
7
  export interface ArborDeps extends Deps {
8
8
  fs: Fs;
9
- /** Only `dev` listens, but the runtime is picked once, where arbor starts. */
9
+ /** HTTP server `dev` listens on to serve its preview. */
10
10
  http: HttpServer;
11
- /** Likewise: only `dev` serves the page, and it is built before it ships. */
11
+ /** The built page `dev` serves as its preview. */
12
12
  assets: Assets;
13
13
  }
14
14
  export declare const arbor: import("webappwiz/cmd").Cli<ArborDeps, ArborDeps & Repository>;
package/dev/assets.d.ts CHANGED
@@ -7,11 +7,5 @@ export interface Assets {
7
7
  /** The stylesheet, with Tailwind already compiled. */
8
8
  styles: string;
9
9
  }
10
- /**
11
- * The page this package ships, built by `build.ts` and imported here as text so
12
- * it travels inside the bundle a release publishes. Serving it costs a string
13
- * lookup, and nothing is read off disk: a published package has no `dev/`
14
- * directory to read from, and the CLI's own dependencies stay clear of React
15
- * and Tailwind because neither is needed once the page is built.
16
- */
10
+ /** The page this package ships. Serve each field's contents as-is. */
17
11
  export declare const assets: Assets;
package/dev.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type AsyncResource } from "webappwiz/disposable";
1
2
  import type { HttpServer } from "webappwiz/http";
2
3
  import type { Logger } from "webappwiz/log";
3
4
  import { type Fs, type PortProvider } from "webappwiz/system";
@@ -8,30 +9,19 @@ import type { WorktreeService } from "./worktree-service.js";
8
9
  export declare const DEFAULT_PORT = 4269;
9
10
  /** How far above the port asked for `dev` will look before giving up. */
10
11
  export declare const PORT_SPAN = 20;
11
- /**
12
- * Where `dev` will listen, given the port asked for. A flag is outside input,
13
- * so a port that cannot exist is a refusal with an exit code rather than the
14
- * assertion `OpenPortProvider` would raise for a bug in here.
15
- */
16
- export declare function devPorts(from: number): PortProvider;
17
12
  /** What `dev` lets a caller choose. */
18
13
  export interface DevOptions {
19
14
  /** Where to listen; the port `--port` asked for, and the span above it. */
20
15
  ports?: PortProvider;
21
16
  }
22
17
  /** A running server, and the one thing a caller ever wants to do with it. */
23
- export interface DevServer {
18
+ export interface DevServer extends AsyncResource {
24
19
  port: number;
25
- stop(): Promise<void>;
26
20
  }
27
21
  /**
28
- * Serves what `ls`, `show` and `log` print, as one page that refetches when the
22
+ * Serves what `list`, `show` and `log` print, as one page that refetches when the
29
23
  * repo changes. Read-only on purpose: driving arbor is what the CLI is for, and
30
24
  * a button that took a lease would fight the agent holding it.
31
- *
32
- * The page itself is a React app under `dev/`, built before publishing and
33
- * carried in the bundle, so nothing here builds markup and nothing reads it
34
- * off disk.
35
25
  */
36
26
  export declare function dev({ service, fs, journal, log, http, assets, }: {
37
27
  service: WorktreeService;
@@ -41,3 +31,9 @@ export declare function dev({ service, fs, journal, log, http, assets, }: {
41
31
  http: HttpServer;
42
32
  assets: Assets;
43
33
  }, { ports }?: DevOptions): Promise<DevServer>;
34
+ /**
35
+ * Where `dev` will listen, given the port asked for. A flag is outside input,
36
+ * so a port that cannot exist is a refusal with an exit code rather than the
37
+ * assertion `OpenPortProvider` would raise for a bug in here.
38
+ */
39
+ export declare function devPorts(from: number): PortProvider;
package/escalate.d.ts CHANGED
@@ -2,6 +2,10 @@ import { type Logger } from "webappwiz/log";
2
2
  import type { Lock } from "webappwiz/system";
3
3
  import type { Git } from "./git.js";
4
4
  import type { WorktreeService } from "./worktree-service.js";
5
+ export interface EscalateOptions {
6
+ /** Task to escalate. Defaults to the one `cwd` is a worktree for. */
7
+ task?: string;
8
+ }
5
9
  /**
6
10
  * The way out that is not "resolve the conflict badly to finish the task".
7
11
  * When both sides restructured the same logic there is no correct merge, only
@@ -12,4 +16,4 @@ export declare function escalate({ service, git, lock, log, }: {
12
16
  git: Git;
13
17
  lock: Lock;
14
18
  log: Logger;
15
- }, reason: string, cwd: string, task?: string): Promise<void>;
19
+ }, reason: string, cwd: string, { task }?: EscalateOptions): Promise<void>;
package/index.js CHANGED
@@ -62,7 +62,6 @@ function exits() {
62
62
  // add.ts
63
63
  var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
64
64
  var PLAN_FILE = "ARBOR.md";
65
- var PLAN = (task) => new MarkdownWriter().heading(1, task).heading(2, "Goal").heading(2, "Files").heading(2, "Next").checklist("fill in Goal and list the steps here as `- [ ]` items").toString();
66
65
  async function add({
67
66
  service,
68
67
  shell,
@@ -100,8 +99,7 @@ ${PLAN_FILE}
100
99
  }
101
100
  await fs.write(`${worktree.path}/${PLAN_FILE}`, PLAN(task));
102
101
  if (config.postCheckout) {
103
- const { exitCode } = await shell.stream(config.postCheckout, {
104
- cwd: worktree.path,
102
+ const { exitCode } = await shell.stream(config.postCheckout, worktree.path, {
105
103
  env: {
106
104
  ARBOR_TASK: task,
107
105
  ARBOR_WORKTREE: worktree.path,
@@ -117,6 +115,9 @@ ${PLAN_FILE}
117
115
  branch: ${worktree.branch}
118
116
  base: ${base}`);
119
117
  }
118
+ function PLAN(task) {
119
+ return new MarkdownWriter().heading(1, task).heading(2, "Goal").heading(2, "Files").heading(2, "Next").checklist("fill in Goal and list the steps here as `- [ ]` items").toString();
120
+ }
120
121
 
121
122
  // claim.ts
122
123
  import { color as color2 } from "webappwiz/log";
@@ -161,6 +162,7 @@ ${changes.map((change) => ` ${change}`).join(`
161
162
  }
162
163
 
163
164
  // dev.ts
165
+ import { AsyncDisposer } from "webappwiz/disposable";
164
166
  import {
165
167
  MAX_PORT,
166
168
  OpenPortProvider
@@ -271,41 +273,47 @@ function checkPlan(text, { task, escalated = false }) {
271
273
 
272
274
  // show.ts
273
275
  var FILE = "ARBOR.md";
274
- async function show({ service, fs, log: log2 }, task, { json = false } = {}) {
276
+ async function show({ service, fs, log: log2 }, task, opts = {}) {
275
277
  const worktree = await service.find(task);
276
278
  if (worktree.gone) {
277
- fail("not_found", `no task '${task}': run \`arbor ls\` to see what there is`, { task });
279
+ fail("not_found", `no task '${task}': run \`arbor list\` to see what there is`, { task });
278
280
  }
279
- const details = await taskDetails(worktree, { fs });
280
- if (json) {
281
+ const details = await new TaskDetails({ fs }).get(worktree);
282
+ if (opts.json ?? false) {
281
283
  log2.info(JSON.stringify(details, null, "\t"));
282
284
  return;
283
285
  }
284
286
  log2.info(report(details));
285
287
  }
286
- async function taskDetails(worktree, opts = {}) {
287
- const files = opts.fs ?? new NodeFs;
288
- const { state } = worktree;
289
- const stat = worktree.hasBranch ? await worktree.diffStat() : null;
290
- const plan = worktree.exists ? await files.read(`${worktree.path}/${FILE}`).catch(() => null) : null;
291
- return {
292
- task: worktree.task,
293
- status: worktree.status,
294
- branch: worktree.branch,
295
- base: worktree.base,
296
- worktree: worktree.path,
297
- lease: worktree.leaseStatus,
298
- ahead: worktree.hasBranch ? await worktree.commitsAhead() : null,
299
- added: stat?.added ?? null,
300
- removed: stat?.removed ?? null,
301
- age: state ? age(state.createdAt) : null,
302
- escalation: state?.escalations?.at(-1)?.reason ?? null,
303
- plan,
304
- planProblems: plan === null ? [] : checkPlan(plan, {
288
+
289
+ class TaskDetails {
290
+ fs;
291
+ constructor(opts = {}) {
292
+ this.fs = opts.fs ?? new NodeFs;
293
+ }
294
+ async get(worktree) {
295
+ const { state } = worktree;
296
+ const stat = worktree.hasBranch ? await worktree.diffStat() : null;
297
+ const plan = worktree.exists ? await this.fs.read(`${worktree.path}/${FILE}`).catch(() => null) : null;
298
+ return {
305
299
  task: worktree.task,
306
- escalated: worktree.status === "escalated"
307
- })
308
- };
300
+ status: worktree.status,
301
+ branch: worktree.branch,
302
+ base: worktree.base,
303
+ worktree: worktree.path,
304
+ lease: worktree.leaseStatus,
305
+ ahead: worktree.hasBranch ? await worktree.commitsAhead() : null,
306
+ added: stat?.added ?? null,
307
+ removed: stat?.removed ?? null,
308
+ age: state ? age(state.createdAt) : null,
309
+ escalation: state?.escalations?.at(-1)?.reason ?? null,
310
+ plan,
311
+ planProblems: plan === null ? [] : checkPlan(plan, {
312
+ task: worktree.task,
313
+ escalated: worktree.status === "escalated"
314
+ })
315
+ };
316
+ }
309
317
  }
310
318
  function report(details) {
311
319
  const lines = [
@@ -340,10 +348,10 @@ function diff(details) {
340
348
 
341
349
  // snapshot.ts
342
350
  async function snapshot(service, journal, opts = {}) {
343
- const fs = opts.fs ?? new NodeFs2;
351
+ const details = new TaskDetails({ fs: opts.fs ?? new NodeFs2 });
344
352
  const tasks = [];
345
353
  for (const worktree of await service.list()) {
346
- tasks.push(await taskDetails(worktree, { fs }));
354
+ tasks.push(await details.get(worktree));
347
355
  }
348
356
  return { tasks, entries: await journal.tail(DEFAULT_COUNT) };
349
357
  }
@@ -368,14 +376,6 @@ function fingerprint({ tasks, entries }) {
368
376
  // dev.ts
369
377
  var DEFAULT_PORT = 4269;
370
378
  var PORT_SPAN = 20;
371
- function devPorts(from) {
372
- if (!Number.isInteger(from) || from < 0 || from > MAX_PORT) {
373
- fail("usage", `invalid port '${from}': use a whole number 0 to ${MAX_PORT}`, {
374
- port: from
375
- });
376
- }
377
- return OpenPortProvider.span({ from, span: PORT_SPAN });
378
- }
379
379
  var POLL_MS = 2000;
380
380
  async function dev({
381
381
  service,
@@ -448,14 +448,22 @@ async function dev({
448
448
  }
449
449
  }, { port: await ports.get(), idleTimeout: Duration.zero() });
450
450
  log2.info(`arbor dev on http://localhost:${listening.port}`);
451
+ const disposer = new AsyncDisposer;
452
+ disposer.defer(() => listening.stop());
453
+ disposer.defer(async () => clearInterval(poll));
451
454
  return {
452
455
  port: listening.port,
453
- stop: async () => {
454
- clearInterval(poll);
455
- await listening.stop();
456
- }
456
+ disposeAsync: disposer.disposeAsync
457
457
  };
458
458
  }
459
+ function devPorts(from) {
460
+ if (!Number.isInteger(from) || from < 0 || from > MAX_PORT) {
461
+ fail("usage", `invalid port '${from}': use a whole number 0 to ${MAX_PORT}`, {
462
+ port: from
463
+ });
464
+ }
465
+ return OpenPortProvider.span({ from, span: PORT_SPAN });
466
+ }
459
467
 
460
468
  // escalate.ts
461
469
  import { color as color6 } from "webappwiz/log";
@@ -464,7 +472,7 @@ async function escalate({
464
472
  git,
465
473
  lock,
466
474
  log: log2
467
- }, reason, cwd, task) {
475
+ }, reason, cwd, { task } = {}) {
468
476
  const branch = await git.currentBranch(cwd).catch(() => "");
469
477
  const name = task || service.taskFor(branch);
470
478
  if (!name) {
@@ -496,9 +504,9 @@ async function escalate({
496
504
  `));
497
505
  }
498
506
 
499
- // ls.ts
507
+ // list.ts
500
508
  import { color as color7 } from "webappwiz/log";
501
- async function ls({ service, log: log2 }, { json = false } = {}) {
509
+ async function list({ service, log: log2 }, { json = false } = {}) {
502
510
  const rows = [];
503
511
  for (const worktree of await service.list()) {
504
512
  rows.push(await row2(worktree));
@@ -605,8 +613,7 @@ async function merge({
605
613
  `), { task, paths });
606
614
  }
607
615
  const gate = [config.postRewrite, config.preMerge].filter(Boolean).join(" && ");
608
- const gated = gate ? await shell.run(gate, {
609
- cwd: worktree.path,
616
+ const gated = gate ? await shell.run(gate, worktree.path, {
610
617
  env: {
611
618
  ARBOR_TASK: task,
612
619
  ARBOR_WORKTREE: worktree.path
@@ -657,8 +664,7 @@ ${gated.stderr}`)
657
664
  Run \`arbor rm ${task}\` to clean up.`, { task });
658
665
  }
659
666
  if (config.postMerge) {
660
- const ran = await shell.run(config.postMerge, {
661
- cwd: landing,
667
+ const ran = await shell.run(config.postMerge, landing, {
662
668
  env: { ARBOR_TASK: task }
663
669
  });
664
670
  if (ran.exitCode !== 0) {
@@ -695,7 +701,7 @@ async function path({ service, log: log2 }, task) {
695
701
  }
696
702
  const worktree = await service.find(task);
697
703
  if (worktree.gone) {
698
- fail("not_found", `no task '${task}': run \`arbor ls\` to see what there is`, { task });
704
+ fail("not_found", `no task '${task}': run \`arbor list\` to see what there is`, { task });
699
705
  }
700
706
  if (!worktree.exists) {
701
707
  fail("orphaned", `'${task}' has no worktree at ${worktree.path}`, {
@@ -932,11 +938,11 @@ class Shell {
932
938
  constructor(opts = {}) {
933
939
  this.ps = opts.ps ?? new NodePs2;
934
940
  }
935
- run(command, opts) {
936
- return this.ps.spawnCapture(["sh", "-c", command], opts);
941
+ run(command, cwd, opts = {}) {
942
+ return this.ps.spawnCapture(["sh", "-c", command], { ...opts, cwd });
937
943
  }
938
- stream(command, opts) {
939
- return this.ps.spawn(["sh", "-c", command], opts);
944
+ stream(command, cwd, opts = {}) {
945
+ return this.ps.spawn(["sh", "-c", command], { ...opts, cwd });
940
946
  }
941
947
  }
942
948
 
@@ -1309,7 +1315,7 @@ async function wait({ service, log: log2 }, task, { timeout = DEFAULT_TIMEOUT, p
1309
1315
  for (;; ) {
1310
1316
  const worktree = await service.find(task);
1311
1317
  if (worktree.status === "absent") {
1312
- fail("not_found", `no task '${task}': run \`arbor ls\` to see what there is`, { task });
1318
+ fail("not_found", `no task '${task}': run \`arbor list\` to see what there is`, { task });
1313
1319
  }
1314
1320
  if (!RUNNING.includes(worktree.status)) {
1315
1321
  log2.info(report2(worktree));
@@ -1336,11 +1342,6 @@ function report2(worktree) {
1336
1342
  }
1337
1343
 
1338
1344
  // arbor.ts
1339
- var here = async ({
1340
- service,
1341
- git,
1342
- ps
1343
- }) => service.taskFor(await git.currentBranch(ps.cwd()).catch(() => ""));
1344
1345
  var arbor = cli("arbor").use(exits()).use(repository());
1345
1346
  arbor.command("add").description("start a new task: create branch task/<task>, a worktree at ../<repo>-arbor/<task> and a state record").arg("task", t.string(), { description: "task name (lowercase-with-dashes)" }).option("base", t.string(), {
1346
1347
  default: "",
@@ -1352,8 +1353,8 @@ arbor.command("rm").description("discard a task: worktree, branch and state file
1352
1353
  default: false,
1353
1354
  description: "discard even when another agent holds the lease"
1354
1355
  }).action((opts, ctx) => ctx.journal.record("rm", opts.task, () => rm(ctx, opts.task, { force: opts.force })));
1355
- arbor.command("ls").description("list every task: name, status, lease (held: an agent is on it now; stale: gone quiet, normal for a task mid-edit; none), commits ahead of trunk, age").option("json", t.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => ls(ctx, { json: opts.json }));
1356
- arbor.command("show").description("read one task without touching it: everything `ls` lists for it, plus the ARBOR.md its agent left at the worktree root; takes no lease, so it cannot knock that agent off its own tree").arg("task", t.string(), { description: "task name" }).option("json", t.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
1356
+ arbor.command("list").description("list every task: name, status, lease (held: an agent is on it now; stale: gone quiet, normal for a task mid-edit; none), commits ahead of trunk, age").option("json", t.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => list(ctx, { json: opts.json }));
1357
+ arbor.command("show").description("read one task without touching it: everything `list` shows for it, plus the ARBOR.md its agent left at the worktree root; takes no lease, so it cannot knock that agent off its own tree").arg("task", t.string(), { description: "task name" }).option("json", t.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => show(ctx, opts.task, { json: opts.json }));
1357
1358
  arbor.command("wait").description("block until a task stops moving: escalated, or gone (merged or removed), or broken; takes no lease, and gives up with `timeout` rather than waiting forever").arg("task", t.string(), { description: "task name" }).option("timeout-secs", t.number(), {
1358
1359
  default: DEFAULT_TIMEOUT.secs,
1359
1360
  description: "how long to wait before giving up"
@@ -1364,7 +1365,7 @@ arbor.command("log").description("show what has been done here recently: one lin
1364
1365
  default: DEFAULT_COUNT,
1365
1366
  description: "how many entries to show"
1366
1367
  }).option("json", t.boolean(), { default: false, description: "emit JSON" }).action((opts, ctx) => log(ctx, { count: opts.count, json: opts.json }));
1367
- arbor.command("dev").description("serve `ls`, `show` and `log` as a web page; read-only").option("port", t.number(), {
1368
+ arbor.command("dev").description("serve `list`, `show` and `log` as a web page; read-only").option("port", t.number(), {
1368
1369
  default: DEFAULT_PORT,
1369
1370
  description: "port to listen on, or the next open one above it"
1370
1371
  }).action((opts, ctx) => dev(ctx, { ports: devPorts(opts.port) }));
@@ -1375,8 +1376,17 @@ arbor.command("path").description("print a task's worktree path, or the main tre
1375
1376
  arbor.command("escalate").description("hand this task to a human and stop: records the reason, drops the lease and leaves the worktree exactly as it is; use instead of resolving a genuine conflict badly just to finish").arg("reason", t.string(), { description: "why this needs a human" }).option("task", t.string(), {
1376
1377
  default: "",
1377
1378
  description: "task name, when run outside its worktree"
1378
- }).action(async (opts, ctx) => ctx.journal.record("escalate", opts.task || await here(ctx), () => escalate(ctx, opts.reason, ctx.ps.cwd(), opts.task || undefined)));
1379
+ }).action(async (opts, ctx) => ctx.journal.record("escalate", opts.task || await here(ctx), () => escalate(ctx, opts.reason, ctx.ps.cwd(), {
1380
+ task: opts.task || undefined
1381
+ })));
1379
1382
  arbor.command("retry").description("give an escalated task another mergeRetryCount merge attempts and put it back to working; the way out of `budget_exhausted` that is not rm and redo, and only from escalated, so a human has seen the tree first").arg("task", t.string(), { description: "task name" }).action((opts, ctx) => ctx.journal.record("retry", opts.task, () => retry(ctx, opts.task)));
1383
+ async function here({
1384
+ service,
1385
+ git,
1386
+ ps
1387
+ }) {
1388
+ return service.taskFor(await git.currentBranch(ps.cwd()).catch(() => ""));
1389
+ }
1380
1390
 
1381
1391
  // dev/build/main.txt
1382
1392
  var main_default = `var __create = Object.create;
@@ -18914,7 +18924,7 @@ class Feed {
18914
18924
  stream.onerror = () => this.set({ offline: true });
18915
18925
  this.load();
18916
18926
  }
18917
- stop() {
18927
+ dispose() {
18918
18928
  this.stream?.close();
18919
18929
  this.stream = null;
18920
18930
  }
@@ -19131,7 +19141,7 @@ function App() {
19131
19141
  const [feed] = import_react6.useState(() => new Feed);
19132
19142
  useDisposerEffect((disposer) => {
19133
19143
  feed.start();
19134
- disposer.defer(() => feed.stop());
19144
+ disposer.use(feed);
19135
19145
  }, [feed]);
19136
19146
  const { snapshot, offline } = useReactive(feed, (feed2) => ({ snapshot: feed2.snapshot, offline: feed2.offline }), ["changed"]);
19137
19147
  const [tab, setTab] = import_react6.useState("tasks");
@@ -1,10 +1,10 @@
1
1
  import { type Logger } from "webappwiz/log";
2
2
  import type { WorktreeService } from "./worktree-service.js";
3
- export interface LsOptions {
3
+ export interface ListOptions {
4
4
  /** Print the rows as JSON instead of a table. */
5
5
  json?: boolean;
6
6
  }
7
- export declare function ls({ service, log }: {
7
+ export declare function list({ service, log }: {
8
8
  service: WorktreeService;
9
9
  log: Logger;
10
- }, { json }?: LsOptions): Promise<void>;
10
+ }, { json }?: ListOptions): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webappwiz/arbor",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Runs several AI coding agents on one repository at once, each in its own git worktree",
5
5
  "license": "MIT",
6
6
  "author": "Jared Johnson",
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "webappwiz": "^0.0.12"
20
+ "webappwiz": "^0.0.14"
21
21
  },
22
22
  "main": "./index.js",
23
23
  "types": "./index.d.ts",
package/shell.d.ts CHANGED
@@ -1,8 +1,4 @@
1
1
  import { type Ps, type SpawnCaptureResult, type SpawnOptions, type SpawnResult } from "webappwiz/system";
2
- /** Spawn options for a shell command. `cwd` is the worktree it runs against. */
3
- export type ShellRunOptions = SpawnOptions & {
4
- cwd: string;
5
- };
6
2
  /** What a `Shell` spawns through; the real process by default. */
7
3
  export interface ShellOptions {
8
4
  ps?: Ps;
@@ -15,7 +11,7 @@ export declare class Shell {
15
11
  private readonly ps;
16
12
  constructor(opts?: ShellOptions);
17
13
  /** Captures output, for commands whose failure has to be reported back. */
18
- run(command: string, opts: ShellRunOptions): Promise<SpawnCaptureResult>;
14
+ run(command: string, cwd: string, opts?: SpawnOptions): Promise<SpawnCaptureResult>;
19
15
  /** Inherits stdio, for commands the agent should watch as they run. */
20
- stream(command: string, opts: ShellRunOptions): Promise<SpawnResult>;
16
+ stream(command: string, cwd: string, opts?: SpawnOptions): Promise<SpawnResult>;
21
17
  }
package/show.d.ts CHANGED
@@ -23,7 +23,7 @@ export interface ShowOptions {
23
23
  json?: boolean;
24
24
  }
25
25
  /**
26
- * One task in full: what `ls` shows for it, plus the `ARBOR.md` its agent
26
+ * One task in full: what `list` shows for it, plus the `ARBOR.md` its agent
27
27
  * left at the worktree root. Reading a tree this way takes no lease, so it
28
28
  * cannot knock the agent driving it off its own work.
29
29
  */
@@ -31,13 +31,18 @@ export declare function show({ service, fs, log }: {
31
31
  service: WorktreeService;
32
32
  fs: Fs;
33
33
  log: Logger;
34
- }, task: string, { json }?: ShowOptions): Promise<void>;
34
+ }, task: string, opts?: ShowOptions): Promise<void>;
35
+ /** Configures how {@link TaskDetails} gathers a task's info. */
36
+ export interface TaskDetailsOptions {
37
+ /** What the worktree is read through; the real filesystem by default. */
38
+ fs?: Fs;
39
+ }
35
40
  /**
36
41
  * Everything there is to say about one task. Split out so `dev` can render the
37
42
  * same fields this prints, rather than assembling its own and drifting.
38
43
  */
39
- export interface TaskDetailsOptions {
40
- /** What the worktree is read through; the real filesystem by default. */
41
- fs?: Fs;
44
+ export declare class TaskDetails {
45
+ private readonly fs;
46
+ constructor(opts?: TaskDetailsOptions);
47
+ get(worktree: Worktree): Promise<Details>;
42
48
  }
43
- export declare function taskDetails(worktree: Worktree, opts?: TaskDetailsOptions): Promise<Details>;
package/snapshot.d.ts CHANGED
@@ -2,13 +2,13 @@ import { type Fs } from "webappwiz/system";
2
2
  import type { Entry, Journal } from "./journal.js";
3
3
  import { type Details } from "./show.js";
4
4
  import type { WorktreeService } from "./worktree-service.js";
5
- /** Everything one page shows: `ls` and `show` for each task, plus `log`. */
5
+ /** Everything one page shows: `list` and `show` for each task, plus `log`. */
6
6
  export interface Snapshot {
7
7
  tasks: Details[];
8
8
  entries: Entry[];
9
9
  }
10
10
  /**
11
- * Reads the whole repo the way `ls`, `show` and `log` do. Takes no lease, so
11
+ * Reads the whole repo the way `list`, `show` and `log` do. Takes no lease, so
12
12
  * serving a page cannot knock an agent off the tree it is driving.
13
13
  */
14
14
  export interface SnapshotOptions {