@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/journal.ts DELETED
@@ -1,89 +0,0 @@
1
- import { type Fs, NodeFs } from "webappwiz/system";
2
- import { Exit } from "./exit";
3
-
4
- /** One thing that was done to a task, and how it went. */
5
- export interface Entry {
6
- at: string;
7
- action: string;
8
- /** The task it was about, or null when the command names none. */
9
- task: string | null;
10
- /** The refusal reason, or null when the command succeeded. */
11
- reason: string | null;
12
- }
13
-
14
- /**
15
- * What has been done in this repo, oldest first. Entries outlive their tasks:
16
- * `merge` and `remove` take the record with them, so this is the only thing that
17
- * remembers a task existed at all.
18
- */
19
- /** What a `Journal` is stored through; the real filesystem by default. */
20
- export interface JournalOptions {
21
- fs?: Fs;
22
- }
23
-
24
- export class Journal {
25
- private readonly fs: Fs;
26
-
27
- constructor(
28
- private readonly path: string,
29
- private readonly capacity: number,
30
- opts: JournalOptions = {},
31
- ) {
32
- this.fs = opts.fs ?? new NodeFs();
33
- }
34
-
35
- async record<T>(
36
- action: string,
37
- task: string | null,
38
- run: () => Promise<T>,
39
- ): Promise<T> {
40
- try {
41
- const out = await run();
42
- await this.append(action, task, null);
43
- return out;
44
- } catch (entry) {
45
- await this.append(
46
- action,
47
- task,
48
- entry instanceof Exit ? entry.reason : "error",
49
- );
50
- throw entry;
51
- }
52
- }
53
-
54
- async tail(count: number): Promise<Entry[]> {
55
- // slice(-0) is slice(0), which would hand back everything.
56
- return count <= 0 ? [] : (await this.entries()).slice(-count);
57
- }
58
-
59
- private async append(
60
- action: string,
61
- task: string | null,
62
- reason: string | null,
63
- ): Promise<void> {
64
- // Rewriting the whole file is fine at a few hundred lines, and two agents
65
- // finishing at the same instant lose an entry rather than corrupt one.
66
- // Switch to an append + occasional compaction if `Fs` ever grows an append.
67
- const entry: Entry = { at: new Date().toISOString(), action, task, reason };
68
- const kept = [...(await this.entries()), entry].slice(-this.capacity);
69
- await this.fs.write(
70
- this.path,
71
- kept.map((entry) => `${JSON.stringify(entry)}\n`).join(""),
72
- );
73
- }
74
-
75
- private async entries(): Promise<Entry[]> {
76
- const raw = await this.fs.read(this.path).catch(() => "");
77
- return raw
78
- .split("\n")
79
- .filter(Boolean)
80
- .flatMap((line) => {
81
- try {
82
- // A torn line loses one entry, not the whole log.
83
- return [JSON.parse(line) as Entry];
84
- } catch {
85
- return [];
86
- }
87
- });
88
- }
89
- }
package/list.test.ts DELETED
@@ -1,82 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { color } from "webappwiz/log";
3
- import { add } from "./add";
4
- import { list } from "./list";
5
- import { Testing } from "./testing";
6
-
7
- describe("list", () => {
8
- let deps: Testing;
9
-
10
- beforeEach(async () => {
11
- deps = await Testing.open();
12
- });
13
-
14
- afterEach(() => deps.disposeAsync());
15
-
16
- it("lists tasks, survives a corrupt record, and flags orphans", async () => {
17
- await add(deps, "alpha");
18
- await add(deps, "beta");
19
- await deps.fs.write(deps.service.recordPath("broken"), "{not json");
20
- const beta = (await deps.service.find("beta")).path;
21
- await deps.fs.rm(beta, { recursive: true, force: true });
22
-
23
- await list(deps);
24
-
25
- expect(deps.out()).toContain("alpha");
26
- expect(deps.out()).toContain("unknown"); // the corrupt row, not a crash
27
- expect(deps.out()).toContain("orphaned");
28
- expect(color.strip(deps.out())).toContain("+0 -0");
29
-
30
- deps.log.clear();
31
- await list(deps, { json: true });
32
- const rows = JSON.parse(deps.out());
33
- expect(rows.map((fixture: { task: string }) => fixture.task)).toEqual([
34
- "alpha",
35
- "beta",
36
- "broken",
37
- ]);
38
- });
39
-
40
- it("names each task's changed and planned files with --files", async () => {
41
- await add(deps, "alpha");
42
- const worktree = (await deps.service.find("alpha")).path;
43
- await deps.commit(worktree, "committed.txt", "a\n", "add committed");
44
- await deps.fs.write(`${worktree}/untracked.txt`, "b\n");
45
- await deps.fs.write(
46
- `${worktree}/ARBOR.md`,
47
- "# alpha\n\n## Files\n\n- `committed.txt`\n- planned.txt\n\nprose, not a path\n",
48
- );
49
-
50
- deps.log.clear();
51
- await list(deps, { files: true });
52
-
53
- const out = color.strip(deps.out());
54
- expect(out).toContain("changed committed.txt");
55
- expect(out).toContain("changed untracked.txt");
56
- expect(out).toContain("planned planned.txt");
57
- expect(out).not.toContain("planned committed.txt");
58
- expect(out).not.toContain("ARBOR.md"); // excluded, so never a change
59
-
60
- deps.log.clear();
61
- await list(deps, { json: true, files: true });
62
- const [row] = JSON.parse(deps.out());
63
- expect(row.changed).toEqual(["committed.txt", "untracked.txt"]);
64
- expect(row.planned).toEqual(["committed.txt", "planned.txt"]);
65
- });
66
-
67
- it("leaves files out unless asked", async () => {
68
- await add(deps, "alpha");
69
- deps.log.clear();
70
-
71
- await list(deps, { json: true });
72
-
73
- const [row] = JSON.parse(deps.out());
74
- expect(row).not.toHaveProperty("changed");
75
- });
76
-
77
- it("says so plainly when there is nothing to list", async () => {
78
- await list(deps);
79
-
80
- expect(deps.out()).toContain("no tasks");
81
- });
82
- });
package/list.ts DELETED
@@ -1,137 +0,0 @@
1
- import { color, type Logger } from "webappwiz/log";
2
- import type { Fs } from "webappwiz/system";
3
- import { age } from "./age";
4
- import { PLAN_FILE, plannedFiles } from "./plan";
5
- import { table } from "./table";
6
- import type { Worktree } from "./worktree";
7
- import type { WorktreeService } from "./worktree-service";
8
-
9
- interface Row {
10
- task: string;
11
- status: string;
12
- lease: "held" | "stale" | "none";
13
- ahead: number | null;
14
- added: number | null;
15
- removed: number | null;
16
- age: string;
17
- worktree: string | null;
18
- /** Paths the task has touched, committed or not; only with `files`. */
19
- changed?: string[] | null;
20
- /** Paths its `ARBOR.md` plans to touch; only with `files`. */
21
- planned?: string[];
22
- }
23
-
24
- export interface ListOptions {
25
- /** Print the rows as JSON instead of a table. */
26
- json?: boolean;
27
- /**
28
- * Add each task's changed and planned files, which is what an agent checks
29
- * its own plan against before starting. Off by default: it costs git calls
30
- * and a file read per task.
31
- */
32
- files?: boolean;
33
- }
34
-
35
- export async function list(
36
- { service, log, fs }: { service: WorktreeService; log: Logger; fs: Fs },
37
- { json = false, files = false }: ListOptions = {},
38
- ): Promise<void> {
39
- const rows: Row[] = [];
40
- for (const worktree of await service.list()) {
41
- const base = await row(worktree);
42
- rows.push(files ? { ...base, ...(await paths(worktree, fs)) } : base);
43
- }
44
-
45
- if (json) {
46
- log.info(JSON.stringify(rows, null, "\t"));
47
- return;
48
- }
49
- if (rows.length === 0) {
50
- log.info("no tasks: run `arbor add <task>` to start one");
51
- return;
52
- }
53
- log.info(listing(rows));
54
- }
55
-
56
- async function row(worktree: Worktree): Promise<Row> {
57
- const { state } = worktree;
58
- const diff = state ? await worktree.diffStat() : null;
59
- return {
60
- task: worktree.task,
61
- // One unreadable record shows as `unknown` instead of taking down the
62
- // whole listing.
63
- status: worktree.status,
64
- lease: worktree.leaseStatus,
65
- ahead: state ? await worktree.commitsAhead() : null,
66
- added: diff?.added ?? null,
67
- removed: diff?.removed ?? null,
68
- age: state ? age(state.createdAt) : "?",
69
- worktree: state ? worktree.path : null,
70
- };
71
- }
72
-
73
- async function paths(
74
- worktree: Worktree,
75
- fs: Fs,
76
- ): Promise<Pick<Row, "changed" | "planned">> {
77
- const plan = worktree.exists
78
- ? await fs.read(`${worktree.path}/${PLAN_FILE}`).catch(() => null)
79
- : null;
80
- return {
81
- changed: worktree.hasBranch ? await worktree.changedFiles() : null,
82
- planned: plan === null ? [] : plannedFiles(plan),
83
- };
84
- }
85
-
86
- function diff(row: Row): string {
87
- if (row.added === null || row.removed === null) {
88
- return "?";
89
- }
90
- return `${color.green(`+${row.added}`)} ${color.red(`-${row.removed}`)}`;
91
- }
92
-
93
- function listing(rows: Row[]): string {
94
- const cells = rows.map((row) => [
95
- row.task,
96
- row.status,
97
- row.lease,
98
- row.ahead === null ? "?" : String(row.ahead),
99
- diff(row),
100
- row.age,
101
- ]);
102
- const out = [
103
- table(["TASK", "STATUS", "LEASE", "AHEAD", "DIFF", "AGE"], cells),
104
- ];
105
- for (const row of rows) {
106
- if (row.changed !== undefined) {
107
- out.push("", ...files(row));
108
- }
109
- }
110
- const orphaned = rows.filter((row) => row.status === "orphaned");
111
- if (orphaned.length > 0) {
112
- out.push(
113
- "",
114
- color.yellow(
115
- `${orphaned.length} orphaned record(s): run \`arbor remove ${orphaned[0]?.task}\``,
116
- ),
117
- );
118
- }
119
- return out.join("\n");
120
- }
121
-
122
- /**
123
- * One task's paths, each once: `changed` when the task has touched it,
124
- * `planned` when only its `ARBOR.md` names it so far.
125
- */
126
- function files(row: Row): string[] {
127
- const changed = row.changed ?? [];
128
- const planned = (row.planned ?? []).filter((path) => !changed.includes(path));
129
- const lines = [
130
- ...changed.map((path) => ` changed ${path}`),
131
- ...planned.map((path) => ` ${color.dim("planned")} ${path}`),
132
- ];
133
- return [
134
- color.bold(row.task),
135
- ...(lines.length > 0 ? lines : [color.dim(" no files yet")]),
136
- ];
137
- }
@@ -1,121 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { NodeFs } from "webappwiz/system";
6
- import { FakePs } from "webappwiz/system/testing";
7
- import { Git } from "./git";
8
- import { loadConfig } from "./load-config";
9
- import { Testing } from "./testing";
10
-
11
- describe("loadConfig", () => {
12
- const fs = new NodeFs();
13
- let root: string;
14
- let git: Git;
15
-
16
- beforeEach(async () => {
17
- root = await mkdtemp(join(tmpdir(), "arbor-config-"));
18
- const ps = new FakePs();
19
- // Stands in for a repo with no `origin/HEAD` to read.
20
- ps.simulate(() => Promise.resolve(1));
21
- git = new Git(root, { ps });
22
- });
23
-
24
- afterEach(async () => {
25
- await rm(root, { recursive: true, force: true });
26
- });
27
-
28
- it("defaults the worktree root to a sibling of the repo, named after it", async () => {
29
- const config = await loadConfig(root, { fs, git });
30
-
31
- expect(config.worktreeRoot).toBe(`${root}-arbor`);
32
- });
33
-
34
- it("leaves every hook unset, so a repo runs only what it asks for", async () => {
35
- const config = await loadConfig(root, { fs, git });
36
-
37
- expect(config.postCheckout).toBeNull();
38
- expect(config.postRewrite).toBeNull();
39
- expect(config.preMerge).toBeNull();
40
- expect(config.postMerge).toBeNull();
41
- });
42
-
43
- it("overrides only the defaults arbor.config.ts names", async () => {
44
- await fs.write(
45
- join(root, "arbor.config.ts"),
46
- `export default { trunk: "trunk", mergeRetryCount: 7 };\n`,
47
- );
48
-
49
- const config = await loadConfig(root, { fs, git });
50
-
51
- expect(config.trunk).toBe("trunk");
52
- expect(config.mergeRetryCount).toBe(7);
53
- expect(config.leaseStalenessMs).toBe(90_000);
54
- });
55
-
56
- it("falls back to main when the repo has no origin/HEAD", async () => {
57
- const config = await loadConfig(root, { fs, git });
58
-
59
- expect(config.trunk).toBe("main");
60
- });
61
-
62
- it("reports the config path and the remedy when the file will not import", async () => {
63
- await fs.write(
64
- join(root, "arbor.config.ts"),
65
- `import "./nowhere-at-all";\nexport default {};\n`,
66
- );
67
-
68
- const failure = await loadConfig(root, { fs, git }).catch(
69
- (entry: Error) => entry,
70
- );
71
-
72
- expect(failure).toBeInstanceOf(Error);
73
- expect((failure as Error).message).toContain("arbor.config.ts");
74
- expect((failure as Error).message).toContain("bun install");
75
- });
76
-
77
- it("takes the trunk from what origin/HEAD points at", async () => {
78
- await using fixture = await Testing.open();
79
- await originHead(fixture, "master");
80
-
81
- const config = await loadConfig(fixture.root, {
82
- fs,
83
- git: new Git(fixture.root, { ps: fixture.ps }),
84
- });
85
-
86
- expect(config.trunk).toBe("master");
87
- });
88
-
89
- it("prefers the trunk arbor.config.ts names over what it detects", async () => {
90
- await using fixture = await Testing.open();
91
- await originHead(fixture, "master");
92
- await fixture.fs.write(
93
- join(fixture.root, "arbor.config.ts"),
94
- `export default { trunk: "release" };\n`,
95
- );
96
-
97
- const config = await loadConfig(fixture.root, {
98
- fs,
99
- git: new Git(fixture.root, { ps: fixture.ps }),
100
- });
101
-
102
- expect(config.trunk).toBe("release");
103
- });
104
- });
105
-
106
- /** Fabricates the ref a clone would fetch, without a remote to fetch from. */
107
- async function originHead(fixture: Testing, branch: string): Promise<void> {
108
- await fixture.gitCli(
109
- fixture.root,
110
- "remote",
111
- "add",
112
- "origin",
113
- "https://example.invalid/repo.git",
114
- );
115
- await fixture.gitCli(
116
- fixture.root,
117
- "symbolic-ref",
118
- "refs/remotes/origin/HEAD",
119
- `refs/remotes/origin/${branch}`,
120
- );
121
- }
package/load-config.ts DELETED
@@ -1,62 +0,0 @@
1
- import { basename, resolve } from "node:path";
2
- import { type Fs, NodeFs } from "webappwiz/system";
3
- import type { Config } from "./config";
4
- import { Git } from "./git";
5
-
6
- export interface LoadConfigOptions {
7
- /** What `arbor.config.ts` is looked for through; the real one by default. */
8
- fs?: Fs;
9
- /** What the trunk is detected through; the repo's own git by default. */
10
- git?: Git;
11
- }
12
-
13
- /**
14
- * The settings arbor runs a repo with: what `arbor.config.ts` names, then the
15
- * branch `origin/HEAD` points at for the trunk, then arbor's own defaults.
16
- * Throws when the config file is there but cannot be imported.
17
- */
18
- export async function loadConfig(
19
- root: string,
20
- opts: LoadConfigOptions = {},
21
- ): Promise<Config> {
22
- const found = await file(opts.fs ?? new NodeFs(), root);
23
- // Only a repo that has not said which branch is its trunk pays for the
24
- // detecting spawn.
25
- const trunk =
26
- found.trunk ??
27
- (await (opts.git ?? new Git(root)).defaultBranch()) ??
28
- "main";
29
- return { ...defaults(root, trunk), ...found };
30
- }
31
-
32
- function defaults(root: string, trunk: string): Config {
33
- return {
34
- trunk,
35
- worktreeRoot: resolve(root, "..", `${basename(root)}-arbor`),
36
- postCheckout: null,
37
- postRewrite: null,
38
- preMerge: null,
39
- postMerge: null,
40
- leaseStalenessMs: 90_000,
41
- mergeRetryCount: 2,
42
- removedCapacity: 50,
43
- logCapacity: 1000,
44
- todoStalenessMs: 30 * 24 * 60 * 60 * 1000,
45
- };
46
- }
47
-
48
- async function file(fs: Fs, root: string): Promise<Partial<Config>> {
49
- const path = `${root}/arbor.config.ts`;
50
- if (!(await fs.exists(path))) {
51
- return {};
52
- }
53
- // Falling back to the defaults would be worse than failing: a config arbor
54
- // silently ignored is a `preMerge` gate that silently stopped running.
55
- const mod = (await import(path).catch((cause: unknown) => {
56
- throw new Error(
57
- `could not load ${path}: ${cause}. Its imports may be stale: try \`bun install\` in ${root}.`,
58
- { cause },
59
- );
60
- })) as { default?: Partial<Config> };
61
- return mod.default ?? {};
62
- }
package/log.test.ts DELETED
@@ -1,65 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { join } from "node:path";
3
- import { Exit } from "./exit";
4
- import { Journal } from "./journal";
5
- import { log } from "./log";
6
- import { Testing } from "./testing";
7
-
8
- describe("log", () => {
9
- let deps: Testing;
10
-
11
- beforeEach(async () => {
12
- deps = await Testing.open();
13
- });
14
-
15
- afterEach(() => deps.disposeAsync());
16
-
17
- it("records how each command ended, refusals included", async () => {
18
- await deps.journal.record("create", "alpha", async () => {});
19
- await expect(
20
- deps.journal.record("merge", "alpha", () =>
21
- Promise.reject(new Exit("tests_failed", "nope")),
22
- ),
23
- ).toBail("tests_failed");
24
-
25
- await log(deps);
26
-
27
- const out = deps.out();
28
- expect(out).toContain("create");
29
- expect(out).toContain("alpha");
30
- expect(out).toContain("ok");
31
- expect(out).toContain("tests_failed");
32
-
33
- // Newest last, and --count trims from the front.
34
- deps.log.clear();
35
- await log(deps, { count: 1, json: true });
36
- expect(JSON.parse(deps.out())).toMatchObject([
37
- { action: "merge", task: "alpha", reason: "tests_failed" },
38
- ]);
39
- });
40
-
41
- it("keeps the log capped and asking for none returns none", async () => {
42
- // Room for two, so a third entry has to push the first off.
43
- const journal = new Journal(join(deps.arborDir, "log.jsonl"), 2, {
44
- fs: deps.fs,
45
- });
46
- await journal.record("create", "a", async () => {});
47
- await journal.record("create", "b", async () => {});
48
- await journal.record("create", "c", async () => {});
49
-
50
- expect((await journal.tail(10)).map((entry) => entry.task)).toEqual([
51
- "b",
52
- "c",
53
- ]);
54
- expect(await journal.tail(0)).toEqual([]);
55
-
56
- await log({ journal, log: deps.log }, { json: true });
57
- expect(JSON.parse(deps.out())).toHaveLength(2);
58
- });
59
-
60
- it("says so plainly when nothing has happened", async () => {
61
- await log(deps);
62
-
63
- expect(deps.out()).toContain("nothing recorded yet");
64
- });
65
- });
package/log.ts DELETED
@@ -1,40 +0,0 @@
1
- import { color, type Logger } from "webappwiz/log";
2
- import { age } from "./age";
3
- import type { Entry, Journal } from "./journal";
4
- import { table } from "./table";
5
-
6
- /** Enough to cover a session's worth of work without scrolling. */
7
- export const DEFAULT_COUNT = 20;
8
-
9
- export interface LogOptions {
10
- /** How many of the most recent entries to show. */
11
- count?: number;
12
- /** Print the entries as JSON instead of a table. */
13
- json?: boolean;
14
- }
15
-
16
- export async function log(
17
- { journal, log }: { journal: Journal; log: Logger },
18
- { count = DEFAULT_COUNT, json = false }: LogOptions = {},
19
- ): Promise<void> {
20
- const entries = await journal.tail(count);
21
-
22
- if (json) {
23
- log.info(JSON.stringify(entries, null, "\t"));
24
- return;
25
- }
26
- if (entries.length === 0) {
27
- log.info("nothing recorded yet");
28
- return;
29
- }
30
- log.info(table(["WHEN", "ACTION", "TASK", "RESULT"], entries.map(row)));
31
- }
32
-
33
- function row(entry: Entry): string[] {
34
- return [
35
- age(entry.at),
36
- entry.action,
37
- entry.task ?? "-",
38
- entry.reason === null ? color.green("ok") : color.red(entry.reason),
39
- ];
40
- }