@shaferllc/keel 0.68.0 → 0.77.0

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 (143) hide show
  1. package/AGENTS.md +2 -0
  2. package/README.md +15 -5
  3. package/dist/api/api.config.stub +9 -0
  4. package/dist/api/config.d.ts +13 -0
  5. package/dist/api/config.js +14 -0
  6. package/dist/api/index.d.ts +16 -0
  7. package/dist/api/index.js +13 -0
  8. package/dist/api/provider.d.ts +10 -0
  9. package/dist/api/provider.js +17 -0
  10. package/dist/api/query.d.ts +35 -0
  11. package/dist/api/query.js +42 -0
  12. package/dist/api/resource.d.ts +93 -0
  13. package/dist/api/resource.js +185 -0
  14. package/dist/core/application.js +6 -0
  15. package/dist/core/cache.d.ts +1 -2
  16. package/dist/core/cache.js +9 -2
  17. package/dist/core/cli/index.d.ts +28 -0
  18. package/dist/core/cli/index.js +427 -0
  19. package/dist/core/cli/stubs.d.ts +14 -0
  20. package/dist/core/cli/stubs.js +105 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/health.d.ts +2 -2
  32. package/dist/core/health.js +2 -2
  33. package/dist/core/http/kernel.d.ts +2 -0
  34. package/dist/core/http/kernel.js +48 -0
  35. package/dist/core/http/router.d.ts +5 -5
  36. package/dist/core/http/router.js +5 -5
  37. package/dist/core/i18n.d.ts +162 -0
  38. package/dist/core/i18n.js +472 -0
  39. package/dist/core/index.d.ts +25 -4
  40. package/dist/core/index.js +13 -3
  41. package/dist/core/instrumentation.d.ts +113 -0
  42. package/dist/core/instrumentation.js +52 -0
  43. package/dist/core/logger.d.ts +7 -0
  44. package/dist/core/logger.js +28 -1
  45. package/dist/core/notification.js +10 -1
  46. package/dist/core/package.d.ts +117 -0
  47. package/dist/core/package.js +169 -0
  48. package/dist/core/pages.d.ts +108 -0
  49. package/dist/core/pages.js +199 -0
  50. package/dist/core/queue.js +26 -5
  51. package/dist/core/repl.d.ts +33 -0
  52. package/dist/core/repl.js +88 -0
  53. package/dist/core/scheduler.js +6 -0
  54. package/dist/core/social.d.ts +4 -4
  55. package/dist/core/social.js +4 -4
  56. package/dist/core/storage.js +15 -3
  57. package/dist/core/telemetry.d.ts +208 -0
  58. package/dist/core/telemetry.js +383 -0
  59. package/dist/core/template.d.ts +2 -3
  60. package/dist/core/template.js +2 -3
  61. package/dist/core/testing.d.ts +171 -1
  62. package/dist/core/testing.js +505 -2
  63. package/dist/db/d1.js +13 -0
  64. package/dist/db/pg.d.ts +13 -0
  65. package/dist/db/pg.js +46 -4
  66. package/dist/openapi/config.d.ts +28 -0
  67. package/dist/openapi/config.js +25 -0
  68. package/dist/openapi/doc.d.ts +40 -0
  69. package/dist/openapi/doc.js +20 -0
  70. package/dist/openapi/export.d.ts +8 -0
  71. package/dist/openapi/export.js +19 -0
  72. package/dist/openapi/gate.d.ts +15 -0
  73. package/dist/openapi/gate.js +27 -0
  74. package/dist/openapi/index.d.ts +19 -0
  75. package/dist/openapi/index.js +15 -0
  76. package/dist/openapi/openapi.config.stub +29 -0
  77. package/dist/openapi/provider.d.ts +18 -0
  78. package/dist/openapi/provider.js +35 -0
  79. package/dist/openapi/routes.d.ts +9 -0
  80. package/dist/openapi/routes.js +23 -0
  81. package/dist/openapi/spec.d.ts +23 -0
  82. package/dist/openapi/spec.js +132 -0
  83. package/dist/openapi/ui.d.ts +8 -0
  84. package/dist/openapi/ui.js +31 -0
  85. package/dist/openapi/zod.d.ts +12 -0
  86. package/dist/openapi/zod.js +46 -0
  87. package/dist/watch/config.d.ts +33 -0
  88. package/dist/watch/config.js +38 -0
  89. package/dist/watch/entry.d.ts +53 -0
  90. package/dist/watch/entry.js +105 -0
  91. package/dist/watch/gate.d.ts +20 -0
  92. package/dist/watch/gate.js +32 -0
  93. package/dist/watch/index.d.ts +21 -0
  94. package/dist/watch/index.js +17 -0
  95. package/dist/watch/migration.d.ts +7 -0
  96. package/dist/watch/migration.js +20 -0
  97. package/dist/watch/provider.d.ts +22 -0
  98. package/dist/watch/provider.js +58 -0
  99. package/dist/watch/prune.d.ts +11 -0
  100. package/dist/watch/prune.js +22 -0
  101. package/dist/watch/recorder.d.ts +24 -0
  102. package/dist/watch/recorder.js +39 -0
  103. package/dist/watch/routes.d.ts +13 -0
  104. package/dist/watch/routes.js +55 -0
  105. package/dist/watch/store.d.ts +54 -0
  106. package/dist/watch/store.js +158 -0
  107. package/dist/watch/ui/dist/watch.css +1 -0
  108. package/dist/watch/ui/dist/watch.js +555 -0
  109. package/dist/watch/ui-shell.d.ts +12 -0
  110. package/dist/watch/ui-shell.js +24 -0
  111. package/dist/watch/watch.config.stub +47 -0
  112. package/dist/watch/watchers.d.ts +12 -0
  113. package/dist/watch/watchers.js +156 -0
  114. package/docs/ai-manifest.json +737 -3
  115. package/docs/api-resources.md +119 -0
  116. package/docs/configuration.md +74 -0
  117. package/docs/console.md +217 -13
  118. package/docs/database.md +101 -0
  119. package/docs/examples/api-resources.ts +86 -0
  120. package/docs/examples/configuration.ts +40 -0
  121. package/docs/examples/console.ts +134 -0
  122. package/docs/examples/database.ts +90 -0
  123. package/docs/examples/i18n.ts +117 -0
  124. package/docs/examples/logger.ts +74 -0
  125. package/docs/examples/mail.ts +103 -0
  126. package/docs/examples/pages.ts +82 -0
  127. package/docs/examples/telemetry.ts +127 -0
  128. package/docs/examples/testing.ts +150 -0
  129. package/docs/health.md +4 -4
  130. package/docs/i18n.md +302 -0
  131. package/docs/logger.md +156 -10
  132. package/docs/mail.md +73 -0
  133. package/docs/openapi.md +111 -0
  134. package/docs/packages.md +118 -0
  135. package/docs/pages.md +217 -0
  136. package/docs/storage.md +5 -3
  137. package/docs/telemetry.md +263 -0
  138. package/docs/templates.md +3 -4
  139. package/docs/testing.md +252 -0
  140. package/docs/watch.md +118 -0
  141. package/llms-full.txt +2389 -241
  142. package/llms.txt +13 -1
  143. package/package.json +32 -6
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Interactive prompts for console commands — ask, confirm, choice, and friends.
3
+ *
4
+ * const name = await prompt.ask("What is your name?");
5
+ * const ok = await prompt.confirm("Delete everything?");
6
+ * const driver = await prompt.choice("Pick a driver", ["sqlite", "postgres"]);
7
+ *
8
+ * `node:readline` is imported dynamically, so the core still loads on the edge.
9
+ *
10
+ * ## Prompts are testable, which is the whole point
11
+ *
12
+ * A command that asks questions is normally a command you can't test. So a prompt
13
+ * can be **trapped**: you script the answer up front, and the prompt never
14
+ * touches the terminal.
15
+ *
16
+ * const prompt = createPrompt({ trap: true });
17
+ * prompt.trap("What is your name?").replyWith("Ada");
18
+ * prompt.trap("Delete everything?").reject();
19
+ *
20
+ * An untrapped prompt in trap mode **throws** rather than hanging forever waiting
21
+ * on stdin that a test will never provide.
22
+ */
23
+ const valueOf = (choice) => (typeof choice === "string" ? choice : choice.value);
24
+ const labelOf = (choice) => typeof choice === "string" ? choice : (choice.label ?? choice.value);
25
+ export function createPrompt(options = {}) {
26
+ const traps = new Map();
27
+ const trapping = options.trap ?? false;
28
+ /** Read one line from the terminal. */
29
+ async function readLine(question, mask = false) {
30
+ const readline = await import("node:readline");
31
+ const rl = readline.createInterface({
32
+ input: process.stdin,
33
+ output: process.stdout,
34
+ terminal: true,
35
+ });
36
+ // For a secure prompt, swallow the echo so the password isn't left on screen.
37
+ if (mask) {
38
+ const output = rl;
39
+ const original = output.output.write.bind(output.output);
40
+ output.output.write = (chunk) => {
41
+ if (chunk.includes(question))
42
+ original(chunk);
43
+ };
44
+ }
45
+ return new Promise((resolve) => {
46
+ rl.question(question, (answer) => {
47
+ rl.close();
48
+ if (mask)
49
+ process.stdout.write("\n");
50
+ resolve(answer);
51
+ });
52
+ });
53
+ }
54
+ /**
55
+ * Resolve a question from its trap, running whatever assertions were attached.
56
+ * Returns `undefined` when there's no trap — the caller decides what that means.
57
+ */
58
+ function fromTrap(question, validate) {
59
+ const state = traps.get(question);
60
+ if (!state)
61
+ return undefined;
62
+ for (const { value, message } of state.expectFail) {
63
+ const result = validate ? validate(value) : true;
64
+ if (result === true) {
65
+ throw new Error(`Expected the prompt "${question}" to reject ${JSON.stringify(value)}, but it passed.`);
66
+ }
67
+ if (message !== undefined && result !== message) {
68
+ throw new Error(`Expected the prompt "${question}" to reject ${JSON.stringify(value)} with "${message}", got "${result}".`);
69
+ }
70
+ }
71
+ for (const value of state.expectPass) {
72
+ const result = validate ? validate(value) : true;
73
+ if (result !== true) {
74
+ throw new Error(`Expected the prompt "${question}" to accept ${JSON.stringify(value)}, but it said "${result}".`);
75
+ }
76
+ }
77
+ state.answered = true;
78
+ return state.answer;
79
+ }
80
+ /** Nothing scripted this question, and there's no terminal to ask. */
81
+ function untrapped(question) {
82
+ throw new Error(`The prompt "${question}" was not trapped. Script it with prompt.trap(${JSON.stringify(question)}).replyWith(…) — ` +
83
+ `otherwise the command would block forever waiting for input a test can't give it.`);
84
+ }
85
+ function check(question, value, options) {
86
+ const problem = options?.validate?.(value);
87
+ if (problem !== undefined && problem !== true)
88
+ throw new Error(`${question}: ${problem}`);
89
+ return options?.result ? options.result(value) : value;
90
+ }
91
+ const prompt = {
92
+ async ask(question, options) {
93
+ if (trapping) {
94
+ const answer = fromTrap(question, options?.validate);
95
+ if (answer === undefined)
96
+ untrapped(question);
97
+ return check(question, answer, options);
98
+ }
99
+ const hint = options?.default ? ` (${options.default})` : "";
100
+ const raw = (await readLine(`${question}${hint} `)).trim();
101
+ const value = raw || options?.default || "";
102
+ const problem = options?.validate?.(value);
103
+ // Ask again rather than dying — a typo shouldn't cost them the whole command.
104
+ if (problem !== undefined && problem !== true) {
105
+ console.error(problem);
106
+ return prompt.ask(question, options);
107
+ }
108
+ return options?.result ? options.result(value) : value;
109
+ },
110
+ async secure(question, options) {
111
+ if (trapping) {
112
+ const answer = fromTrap(question, options?.validate);
113
+ if (answer === undefined)
114
+ untrapped(question);
115
+ return check(question, answer, options);
116
+ }
117
+ const value = (await readLine(`${question} `, true)).trim();
118
+ return check(question, value, options);
119
+ },
120
+ async confirm(question, options) {
121
+ return prompt.toggle(question, ["y", "n"], options);
122
+ },
123
+ async toggle(question, labels, options) {
124
+ if (trapping) {
125
+ const answer = fromTrap(question, options?.validate);
126
+ if (answer === undefined)
127
+ untrapped(question);
128
+ return check(question, answer, options);
129
+ }
130
+ const fallback = options?.default ?? false;
131
+ const hint = fallback ? `[${labels[0].toUpperCase()}/${labels[1]}]` : `[${labels[0]}/${labels[1].toUpperCase()}]`;
132
+ const raw = (await readLine(`${question} ${hint} `)).trim().toLowerCase();
133
+ const value = raw === "" ? fallback : raw === labels[0].toLowerCase() || raw === "yes" || raw === "true";
134
+ return check(question, value, options);
135
+ },
136
+ async choice(question, choices, options) {
137
+ if (trapping) {
138
+ const index = fromTrap(question);
139
+ if (index === undefined)
140
+ untrapped(question);
141
+ const choice = choices[index];
142
+ if (!choice)
143
+ throw new Error(`The prompt "${question}" has no option at index ${index}.`);
144
+ return check(question, valueOf(choice), options);
145
+ }
146
+ console.log(question);
147
+ choices.forEach((choice, i) => console.log(` ${i + 1}) ${labelOf(choice)}`));
148
+ const raw = (await readLine("> ")).trim();
149
+ const index = Number(raw) - 1;
150
+ const choice = choices[index];
151
+ if (!choice) {
152
+ console.error(`Pick a number between 1 and ${choices.length}.`);
153
+ return prompt.choice(question, choices, options);
154
+ }
155
+ return check(question, valueOf(choice), options);
156
+ },
157
+ async multiple(question, choices, options) {
158
+ if (trapping) {
159
+ const indexes = fromTrap(question);
160
+ if (indexes === undefined)
161
+ untrapped(question);
162
+ const values = indexes.map((i) => {
163
+ const choice = choices[i];
164
+ if (!choice)
165
+ throw new Error(`The prompt "${question}" has no option at index ${i}.`);
166
+ return valueOf(choice);
167
+ });
168
+ return check(question, values, options);
169
+ }
170
+ console.log(`${question} (comma-separated numbers)`);
171
+ choices.forEach((choice, i) => console.log(` ${i + 1}) ${labelOf(choice)}`));
172
+ const raw = (await readLine("> ")).trim();
173
+ const values = raw
174
+ .split(",")
175
+ .map((part) => choices[Number(part.trim()) - 1])
176
+ .filter((choice) => choice !== undefined)
177
+ .map(valueOf);
178
+ return check(question, values, options);
179
+ },
180
+ async autocomplete(question, choices, options) {
181
+ if (trapping) {
182
+ const answer = fromTrap(question);
183
+ if (answer === undefined)
184
+ untrapped(question);
185
+ return check(question, answer, options);
186
+ }
187
+ // Without a raw-mode TTY there's no live filtering to do, so it degrades to
188
+ // a plain choice list rather than pretending.
189
+ return prompt.choice(question, choices, options);
190
+ },
191
+ trap(question) {
192
+ const state = {
193
+ question,
194
+ answered: false,
195
+ expectFail: [],
196
+ expectPass: [],
197
+ };
198
+ traps.set(question, state);
199
+ const api = {
200
+ replyWith(value) {
201
+ state.answer = value;
202
+ return api;
203
+ },
204
+ accept() {
205
+ state.answer = true;
206
+ return api;
207
+ },
208
+ reject() {
209
+ state.answer = false;
210
+ return api;
211
+ },
212
+ chooseOption(index) {
213
+ state.answer = index;
214
+ return api;
215
+ },
216
+ chooseOptions(indexes) {
217
+ state.answer = indexes;
218
+ return api;
219
+ },
220
+ assertFails(value, message) {
221
+ state.expectFail.push({ value, message });
222
+ return api;
223
+ },
224
+ assertPasses(value) {
225
+ state.expectPass.push(value);
226
+ return api;
227
+ },
228
+ };
229
+ return api;
230
+ },
231
+ assertAllTrapsUsed() {
232
+ const unused = [...traps.values()].filter((t) => !t.answered).map((t) => t.question);
233
+ if (unused.length) {
234
+ throw new Error(`These prompts were trapped but never asked: ${unused.map((q) => `"${q}"`).join(", ")}.`);
235
+ }
236
+ },
237
+ };
238
+ return prompt;
239
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Terminal UI for console commands — colors, a logger, tables, boxes, and a task
3
+ * runner. No dependency: ANSI codes are a dozen escape sequences, not a package.
4
+ *
5
+ * ui.success("Migrated 3 tables");
6
+ * ui.table(["Name", "Rows"]).row(["users", "42"]).render();
7
+ *
8
+ * Everything goes through the same `write`, which is what makes the whole thing
9
+ * testable: `createUi({ raw: true })` strips the colors and buffers the output
10
+ * instead of printing it, so a test can assert on exactly what a command said.
11
+ */
12
+ declare const CODES: {
13
+ readonly reset: 0;
14
+ readonly bold: 1;
15
+ readonly dim: 2;
16
+ readonly italic: 3;
17
+ readonly underline: 4;
18
+ readonly red: 31;
19
+ readonly green: 32;
20
+ readonly yellow: 33;
21
+ readonly blue: 34;
22
+ readonly magenta: 35;
23
+ readonly cyan: 36;
24
+ readonly white: 37;
25
+ readonly gray: 90;
26
+ readonly bgRed: 41;
27
+ readonly bgGreen: 42;
28
+ readonly bgYellow: 43;
29
+ };
30
+ export type ColorName = keyof typeof CODES;
31
+ /** Paint text, or don't — the same call site works either way. */
32
+ export interface Colors {
33
+ (name: ColorName, text: string): string;
34
+ readonly enabled: boolean;
35
+ }
36
+ /** Strip ANSI escapes — used by the raw mode, and handy in assertions. */
37
+ export declare function stripAnsi(text: string): string;
38
+ export interface Table {
39
+ head(...columns: string[]): Table;
40
+ row(cells: string[]): Table;
41
+ rows(rows: string[][]): Table;
42
+ /** Render it and write it out. */
43
+ render(): void;
44
+ /** Render it to a string instead of writing it. */
45
+ toString(): string;
46
+ }
47
+ /** What a single task can report while it runs. */
48
+ export interface TaskHandle {
49
+ update(message: string): void;
50
+ }
51
+ export interface Tasks {
52
+ add(title: string, run: (task: TaskHandle) => Promise<string | void> | string | void): Tasks;
53
+ /** Run them in order. Resolves to false if any task failed. */
54
+ run(): Promise<boolean>;
55
+ }
56
+ export interface UiOptions {
57
+ /**
58
+ * Buffer output instead of printing it, and drop the colors. What tests use —
59
+ * `ui.logs` then holds every line.
60
+ */
61
+ raw?: boolean;
62
+ /** Force colors on or off. Defaults to on unless raw, or NO_COLOR is set. */
63
+ colors?: boolean;
64
+ }
65
+ export interface Ui {
66
+ readonly colors: Colors;
67
+ /** In raw mode, every line written. Empty otherwise. */
68
+ readonly logs: string[];
69
+ /** In raw mode, every line written to stderr. */
70
+ readonly errors: string[];
71
+ /** Write a line verbatim. */
72
+ write(line: string): void;
73
+ debug(message: string): void;
74
+ info(message: string): void;
75
+ success(message: string): void;
76
+ warning(message: string): void;
77
+ /** Goes to stderr. */
78
+ error(message: string): void;
79
+ /** Goes to stderr. */
80
+ fatal(message: string): void;
81
+ /**
82
+ * A one-line status for something you did: `CREATE app/Models/User.ts`.
83
+ * The verb is padded so a run of them lines up.
84
+ */
85
+ action(verb: string, target: string, status?: "done" | "skipped" | "failed"): void;
86
+ table(head?: string[]): Table;
87
+ /** A boxed message — for the thing you want them to actually read. */
88
+ sticker(lines: string[], title?: string): void;
89
+ /** Numbered next-steps. */
90
+ instructions(lines: string[], title?: string): void;
91
+ tasks(): Tasks;
92
+ /** Clear the captured buffers (raw mode). */
93
+ clear(): void;
94
+ }
95
+ export declare function createUi(options?: UiOptions): Ui;
96
+ export {};
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Terminal UI for console commands — colors, a logger, tables, boxes, and a task
3
+ * runner. No dependency: ANSI codes are a dozen escape sequences, not a package.
4
+ *
5
+ * ui.success("Migrated 3 tables");
6
+ * ui.table(["Name", "Rows"]).row(["users", "42"]).render();
7
+ *
8
+ * Everything goes through the same `write`, which is what makes the whole thing
9
+ * testable: `createUi({ raw: true })` strips the colors and buffers the output
10
+ * instead of printing it, so a test can assert on exactly what a command said.
11
+ */
12
+ /* -------------------------------- colors ---------------------------------- */
13
+ const CODES = {
14
+ reset: 0,
15
+ bold: 1,
16
+ dim: 2,
17
+ italic: 3,
18
+ underline: 4,
19
+ red: 31,
20
+ green: 32,
21
+ yellow: 33,
22
+ blue: 34,
23
+ magenta: 35,
24
+ cyan: 36,
25
+ white: 37,
26
+ gray: 90,
27
+ bgRed: 41,
28
+ bgGreen: 42,
29
+ bgYellow: 43,
30
+ };
31
+ function makeColors(enabled) {
32
+ const fn = ((name, text) => enabled ? `\u001b[${CODES[name]}m${text}\u001b[0m` : text);
33
+ fn.enabled = enabled;
34
+ return fn;
35
+ }
36
+ /** Strip ANSI escapes — used by the raw mode, and handy in assertions. */
37
+ export function stripAnsi(text) {
38
+ // eslint-disable-next-line no-control-regex
39
+ return text.replace(/\u001b\[\d+m/g, "");
40
+ }
41
+ const SYMBOLS = {
42
+ info: "›",
43
+ success: "✔",
44
+ warning: "⚠",
45
+ error: "✖",
46
+ debug: "·",
47
+ };
48
+ export function createUi(options = {}) {
49
+ const raw = options.raw ?? false;
50
+ const useColors = options.colors ?? (!raw && !process.env.NO_COLOR);
51
+ const colors = makeColors(useColors);
52
+ const logs = [];
53
+ const errors = [];
54
+ const write = (line) => {
55
+ if (raw)
56
+ logs.push(stripAnsi(line));
57
+ else
58
+ console.log(line);
59
+ };
60
+ const writeError = (line) => {
61
+ if (raw)
62
+ errors.push(stripAnsi(line));
63
+ else
64
+ console.error(line);
65
+ };
66
+ const label = (symbol, color, message) => `${colors(color, symbol)} ${message}`;
67
+ const ui = {
68
+ colors,
69
+ logs,
70
+ errors,
71
+ write,
72
+ debug: (message) => write(label(SYMBOLS.debug, "gray", colors("gray", message))),
73
+ info: (message) => write(label(SYMBOLS.info, "blue", message)),
74
+ success: (message) => write(label(SYMBOLS.success, "green", message)),
75
+ warning: (message) => write(label(SYMBOLS.warning, "yellow", message)),
76
+ error: (message) => writeError(label(SYMBOLS.error, "red", message)),
77
+ fatal: (message) => writeError(label(SYMBOLS.error, "red", colors("bold", message))),
78
+ action(verb, target, status = "done") {
79
+ const color = status === "failed" ? "red" : status === "skipped" ? "yellow" : "green";
80
+ const line = `${colors(color, verb.toUpperCase().padEnd(8))}${target}`;
81
+ if (status === "failed")
82
+ writeError(line);
83
+ else
84
+ write(line);
85
+ },
86
+ table(head) {
87
+ const headers = head ? [...head] : [];
88
+ const body = [];
89
+ const table = {
90
+ head(...columns) {
91
+ headers.length = 0;
92
+ headers.push(...columns);
93
+ return table;
94
+ },
95
+ row(cells) {
96
+ body.push(cells);
97
+ return table;
98
+ },
99
+ rows(list) {
100
+ body.push(...list);
101
+ return table;
102
+ },
103
+ toString() {
104
+ const all = headers.length ? [headers, ...body] : body;
105
+ if (!all.length)
106
+ return "";
107
+ const columns = Math.max(...all.map((r) => r.length));
108
+ const widths = Array.from({ length: columns }, (_, i) => Math.max(...all.map((row) => stripAnsi(row[i] ?? "").length)));
109
+ const line = (cells) => cells
110
+ .map((cell, i) => cell + " ".repeat(Math.max(0, widths[i] - stripAnsi(cell).length)))
111
+ .join(" ")
112
+ .trimEnd();
113
+ const out = [];
114
+ if (headers.length) {
115
+ out.push(line(headers.map((h) => colors("bold", h))));
116
+ out.push(widths.map((w) => "─".repeat(w)).join(" "));
117
+ }
118
+ for (const row of body)
119
+ out.push(line(row));
120
+ return out.join("\n");
121
+ },
122
+ render() {
123
+ const text = table.toString();
124
+ if (text)
125
+ write(text);
126
+ },
127
+ };
128
+ return table;
129
+ },
130
+ sticker(lines, title) {
131
+ const content = title ? [colors("bold", title), "", ...lines] : [...lines];
132
+ const width = Math.max(...content.map((l) => stripAnsi(l).length));
133
+ write(`┌─${"─".repeat(width)}─┐`);
134
+ for (const line of content) {
135
+ const pad = " ".repeat(width - stripAnsi(line).length);
136
+ write(`│ ${line}${pad} │`);
137
+ }
138
+ write(`└─${"─".repeat(width)}─┘`);
139
+ },
140
+ instructions(lines, title) {
141
+ if (title)
142
+ write(colors("bold", title));
143
+ lines.forEach((line, i) => write(` ${colors("gray", `${i + 1}.`)} ${line}`));
144
+ },
145
+ tasks() {
146
+ const queue = [];
147
+ const runner = {
148
+ add(title, run) {
149
+ queue.push({ title, run });
150
+ return runner;
151
+ },
152
+ async run() {
153
+ let ok = true;
154
+ for (const { title, run } of queue) {
155
+ let note = "";
156
+ const handle = {
157
+ update: (message) => {
158
+ note = message;
159
+ },
160
+ };
161
+ const started = Date.now();
162
+ try {
163
+ const result = await run(handle);
164
+ const detail = result || note;
165
+ const ms = Date.now() - started;
166
+ ui.write(`${colors("green", SYMBOLS.success)} ${title}${detail ? ` ${colors("gray", `— ${detail}`)}` : ""} ${colors("gray", `(${ms}ms)`)}`);
167
+ }
168
+ catch (error) {
169
+ ok = false;
170
+ // Stop at the first failure: the tasks after it almost certainly
171
+ // depend on it, and a cascade of red tells you nothing new.
172
+ ui.error(`${title} — ${error instanceof Error ? error.message : String(error)}`);
173
+ break;
174
+ }
175
+ }
176
+ return ok;
177
+ },
178
+ };
179
+ return runner;
180
+ },
181
+ clear() {
182
+ logs.length = 0;
183
+ errors.length = 0;
184
+ },
185
+ };
186
+ return ui;
187
+ }