@zerotal/core 1.0.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 (201) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -0
  4. package/package.json +72 -0
  5. package/src/application/Application.ts +1671 -0
  6. package/src/application/BootDoctor.ts +108 -0
  7. package/src/application/DevErrorPage.ts +567 -0
  8. package/src/application/ExceptionHandler.ts +183 -0
  9. package/src/application/currentApp.ts +73 -0
  10. package/src/assets/assets.ts +79 -0
  11. package/src/assets/index.ts +16 -0
  12. package/src/auth/AuthenticatedUser.ts +18 -0
  13. package/src/build/PackageLinter.ts +146 -0
  14. package/src/build/PackageScaffold.ts +127 -0
  15. package/src/build/codemod.ts +64 -0
  16. package/src/build/index.ts +12 -0
  17. package/src/command/Command.ts +254 -0
  18. package/src/command/CommandRunner.ts +593 -0
  19. package/src/command/OutputWriter.ts +61 -0
  20. package/src/command/builtin/CompileCommand.ts +46 -0
  21. package/src/command/builtin/CssBuildCommand.ts +71 -0
  22. package/src/command/builtin/KeyGenerateCommand.ts +58 -0
  23. package/src/command/builtin/LintPackagesCommand.ts +72 -0
  24. package/src/command/builtin/MakeCommandCommand.ts +85 -0
  25. package/src/command/builtin/MakeControllerCommand.ts +95 -0
  26. package/src/command/builtin/MakeEventCommand.ts +85 -0
  27. package/src/command/builtin/MakeJobCommand.ts +53 -0
  28. package/src/command/builtin/MakeListenerCommand.ts +35 -0
  29. package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
  30. package/src/command/builtin/MakeNotificationCommand.ts +48 -0
  31. package/src/command/builtin/MakeObserverCommand.ts +78 -0
  32. package/src/command/builtin/MakePackageCommand.ts +45 -0
  33. package/src/command/builtin/MakePolicyCommand.ts +66 -0
  34. package/src/command/builtin/MakeProviderCommand.ts +75 -0
  35. package/src/command/builtin/MakeRequestCommand.ts +47 -0
  36. package/src/command/builtin/MakeResourceCommand.ts +61 -0
  37. package/src/command/builtin/MakeTestCommand.ts +120 -0
  38. package/src/command/builtin/ReloadCommand.ts +52 -0
  39. package/src/command/builtin/ReplCommand.ts +174 -0
  40. package/src/command/builtin/RouteListCommand.ts +188 -0
  41. package/src/command/builtin/ServeCommand.ts +321 -0
  42. package/src/command/builtin/StartCommand.ts +3 -0
  43. package/src/command/builtin/StatusCommand.ts +71 -0
  44. package/src/command/builtin/TestCommand.ts +172 -0
  45. package/src/command/builtin/WorkerCommand.ts +27 -0
  46. package/src/command/builtin/index.ts +53 -0
  47. package/src/command/scaffold/worker.ts.txt +12 -0
  48. package/src/command/scaffold/zerotal.ts.txt +26 -0
  49. package/src/command/startZerotal.ts +55 -0
  50. package/src/config/AppConfig.ts +253 -0
  51. package/src/config/ConfigLoader.ts +117 -0
  52. package/src/config/ConfigManager.ts +169 -0
  53. package/src/config/index.ts +46 -0
  54. package/src/config/registry.ts +59 -0
  55. package/src/config/validation.ts +117 -0
  56. package/src/container/Container.ts +606 -0
  57. package/src/container/ContextualBindingBuilder.ts +57 -0
  58. package/src/container/ScopedResolver.ts +117 -0
  59. package/src/container/index.ts +32 -0
  60. package/src/container/inject.ts +55 -0
  61. package/src/container/types.ts +71 -0
  62. package/src/context/RequestContext.ts +91 -0
  63. package/src/contracts/auth.ts +24 -0
  64. package/src/contracts/index.ts +23 -0
  65. package/src/contracts/session.ts +70 -0
  66. package/src/contracts/transaction.ts +26 -0
  67. package/src/conventions/ConventionLoader.ts +128 -0
  68. package/src/conventions/builtinConcerns.ts +131 -0
  69. package/src/crypt/Crypt.ts +141 -0
  70. package/src/crypt/URLSigner.ts +96 -0
  71. package/src/datetime/Carbon.ts +1396 -0
  72. package/src/datetime/CarbonInterval.ts +421 -0
  73. package/src/datetime/clock.ts +28 -0
  74. package/src/datetime/index.ts +23 -0
  75. package/src/datetime/temporal-shim.ts +1 -0
  76. package/src/dev/BuildOutput.ts +131 -0
  77. package/src/dev/CssPlugins.ts +184 -0
  78. package/src/dev/DevBuildHook.ts +74 -0
  79. package/src/dev/DevOrchestrator.ts +213 -0
  80. package/src/dev/DevReloadMiddleware.ts +101 -0
  81. package/src/dev/DevReloadServer.ts +85 -0
  82. package/src/dev/DevWsServer.ts +45 -0
  83. package/src/dev/index.ts +19 -0
  84. package/src/dev/reloadClient.ts +39 -0
  85. package/src/env/Def.ts +232 -0
  86. package/src/env/EnvSchema.ts +105 -0
  87. package/src/env/index.ts +34 -0
  88. package/src/env/t.ts +128 -0
  89. package/src/errors/ConfigError.ts +12 -0
  90. package/src/errors/ContainerErrors.ts +143 -0
  91. package/src/errors/HttpError.ts +127 -0
  92. package/src/errors/ValidationError.ts +19 -0
  93. package/src/errors/ZerotalError.ts +25 -0
  94. package/src/errors/index.ts +46 -0
  95. package/src/events/CallQueuedListener.ts +66 -0
  96. package/src/events/Emitter.ts +280 -0
  97. package/src/events/EventFake.ts +160 -0
  98. package/src/events/FrameworkEvents.ts +252 -0
  99. package/src/facade/Facade.ts +101 -0
  100. package/src/facade/facades/App.ts +155 -0
  101. package/src/facade/facades/Artisan.ts +63 -0
  102. package/src/facade/facades/Config.ts +21 -0
  103. package/src/facade/facades/Events.ts +19 -0
  104. package/src/facade/facades/index.ts +28 -0
  105. package/src/global.d.ts +9 -0
  106. package/src/hash/Hash.ts +60 -0
  107. package/src/health/Health.ts +221 -0
  108. package/src/health/index.ts +27 -0
  109. package/src/helpers/Collection.ts +435 -0
  110. package/src/helpers/config.ts +59 -0
  111. package/src/helpers/fluent.ts +52 -0
  112. package/src/helpers/html.ts +11 -0
  113. package/src/helpers/index.ts +266 -0
  114. package/src/helpers/make.ts +35 -0
  115. package/src/helpers/markdown.ts +73 -0
  116. package/src/helpers/pageElements.ts +27 -0
  117. package/src/helpers/request.ts +62 -0
  118. package/src/helpers/response.ts +411 -0
  119. package/src/helpers/str.ts +208 -0
  120. package/src/http/Http.ts +298 -0
  121. package/src/http/HttpClient.ts +289 -0
  122. package/src/http/Resource.ts +171 -0
  123. package/src/http/UploadedFile.ts +204 -0
  124. package/src/http/Uri.ts +490 -0
  125. package/src/http/index.ts +46 -0
  126. package/src/http/negotiate.ts +213 -0
  127. package/src/http/originGuard.ts +76 -0
  128. package/src/http/sniffContentType.ts +105 -0
  129. package/src/http/url.ts +204 -0
  130. package/src/http/withHeaders.ts +24 -0
  131. package/src/index.ts +250 -0
  132. package/src/lock/LockManager.ts +228 -0
  133. package/src/lock/config.ts +49 -0
  134. package/src/lock/drivers/LockDriver.ts +32 -0
  135. package/src/lock/drivers/MemoryLockDriver.ts +52 -0
  136. package/src/lock/drivers/RedisLockDriver.ts +58 -0
  137. package/src/lock/drivers/SqliteLockDriver.ts +85 -0
  138. package/src/lock/errors.ts +20 -0
  139. package/src/lock/facades/Lock.ts +114 -0
  140. package/src/lock/index.ts +53 -0
  141. package/src/logger/Log.ts +35 -0
  142. package/src/logger/LogManager.ts +430 -0
  143. package/src/logger/LoggerMiddleware.ts +125 -0
  144. package/src/logger/channels/ConsoleChannel.ts +139 -0
  145. package/src/logger/channels/DailyChannel.ts +74 -0
  146. package/src/logger/channels/NullChannel.ts +17 -0
  147. package/src/logger/channels/SingleChannel.ts +34 -0
  148. package/src/logger/channels/StackChannel.ts +29 -0
  149. package/src/logger/config.ts +90 -0
  150. package/src/logger/format.ts +96 -0
  151. package/src/logger/frameworkLog.ts +93 -0
  152. package/src/logger/index.ts +68 -0
  153. package/src/logger/renderTable.ts +111 -0
  154. package/src/logger/types.ts +212 -0
  155. package/src/macros/config.macro.ts +50 -0
  156. package/src/metrics/HttpMetrics.ts +114 -0
  157. package/src/metrics/index.ts +18 -0
  158. package/src/middleware/BaseMiddleware.ts +72 -0
  159. package/src/middleware/CorsMiddleware.ts +152 -0
  160. package/src/middleware/RateLimiter.ts +255 -0
  161. package/src/middleware/SecureHeadersMiddleware.ts +127 -0
  162. package/src/middleware/ThrottleMiddleware.ts +252 -0
  163. package/src/middleware/WebhookMiddleware.ts +204 -0
  164. package/src/pipeline/ContextRegistry.ts +42 -0
  165. package/src/pipeline/HttpContext.ts +865 -0
  166. package/src/pipeline/Pipeline.ts +150 -0
  167. package/src/pipeline/currentPage.ts +46 -0
  168. package/src/pipeline/types.ts +80 -0
  169. package/src/provider/LockProvider.ts +64 -0
  170. package/src/provider/LogProvider.ts +137 -0
  171. package/src/provider/ServiceProvider.ts +84 -0
  172. package/src/provider/StorageProvider.ts +45 -0
  173. package/src/router/FileRouter.ts +526 -0
  174. package/src/router/Route.ts +76 -0
  175. package/src/router/RouteHandler.ts +335 -0
  176. package/src/router/Router.ts +1247 -0
  177. package/src/router/domain.ts +65 -0
  178. package/src/security/index.ts +22 -0
  179. package/src/storage/FakeDisk.ts +233 -0
  180. package/src/storage/StorageFilesMiddleware.ts +150 -0
  181. package/src/storage/StorageManager.ts +173 -0
  182. package/src/storage/config.ts +47 -0
  183. package/src/storage/drivers/LocalDriver.ts +138 -0
  184. package/src/storage/drivers/S3Driver.ts +169 -0
  185. package/src/storage/errors.ts +135 -0
  186. package/src/storage/facades/Storage.ts +3 -0
  187. package/src/storage/global.d.ts +7 -0
  188. package/src/storage/index.ts +22 -0
  189. package/src/storage/root.ts +59 -0
  190. package/src/storage/types.ts +104 -0
  191. package/src/support/appKey.ts +38 -0
  192. package/src/support/cookie.ts +72 -0
  193. package/src/support/crypto.ts +52 -0
  194. package/src/support/deepMerge.ts +117 -0
  195. package/src/support/env.ts +71 -0
  196. package/src/support/network.ts +79 -0
  197. package/src/support/port.ts +197 -0
  198. package/src/support/str.ts +122 -0
  199. package/src/view/FileRouteResolver.ts +59 -0
  200. package/src/view/index.ts +144 -0
  201. package/src/view/jsx-runtime.ts +233 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Codemods — small, idempotent source transforms used by `make:*` generators to
3
+ * wire newly-created code into existing bootstrap files (so you don't hand-edit
4
+ * `bootstrap/providers.ts` after every `make:provider`).
5
+ */
6
+
7
+ /** Insert an import statement after the last existing import (deduped). */
8
+ export function addImport(source: string, importStatement: string): string {
9
+ if (source.includes(importStatement)) return source;
10
+ const lines = source.split('\n');
11
+ let lastImportIndex = -1;
12
+ for (let index = 0; index < lines.length; index++) {
13
+ if (/^\s*import\s/.test(lines[index]!)) lastImportIndex = index;
14
+ }
15
+ lines.splice(lastImportIndex + 1, 0, importStatement);
16
+ return lines.join('\n');
17
+ }
18
+
19
+ /**
20
+ * Append an identifier to a `export default [ ... ]` array literal, preserving
21
+ * indentation and the trailing-comma style. No-op if already present.
22
+ */
23
+ export function addToDefaultArrayExport(source: string, identifier: string): string {
24
+ const start = source.indexOf('export default [');
25
+ if (start === -1) return source;
26
+ const close = source.indexOf('];', start);
27
+ if (close === -1) return source;
28
+ const body = source.slice(start, close);
29
+ if (new RegExp(`\\b${identifier}\\b`).test(body)) return source; // Already listed.
30
+ // Infer indentation from an existing item, else two spaces.
31
+ const indentMatch = body.match(/\n(\s+)\S/);
32
+ const indent = indentMatch ? indentMatch[1] : ' ';
33
+ return source.slice(0, close) + `${indent}${identifier},\n` + source.slice(close);
34
+ }
35
+
36
+ export type RegisterResult = 'added' | 'exists' | 'missing';
37
+
38
+ export interface RegisterProviderOptions {
39
+ className: string;
40
+ /** Import specifier, e.g. '../app/providers/FooProvider.ts' or '@zerotal/foo'. */
41
+ importPath: string;
42
+ /** Bootstrap file. Default: 'bootstrap/providers.ts'. */
43
+ bootstrapPath?: string;
44
+ }
45
+
46
+ /**
47
+ * Register a provider in the app's bootstrap providers file: adds the import and
48
+ * appends it to the default-exported array. Idempotent.
49
+ *
50
+ * @returns 'added' | 'exists' (already registered) | 'missing' (no bootstrap file)
51
+ */
52
+ export async function registerProvider(options: RegisterProviderOptions): Promise<RegisterResult> {
53
+ const path = options.bootstrapPath ?? 'bootstrap/providers.ts';
54
+ const file = Bun.file(path);
55
+ if (!(await file.exists())) return 'missing';
56
+
57
+ let source = await file.text();
58
+ if (new RegExp(`\\b${options.className}\\b`).test(source)) return 'exists';
59
+
60
+ source = addImport(source, `import { ${options.className} } from "${options.importPath}";`);
61
+ source = addToDefaultArrayExport(source, options.className);
62
+ await Bun.write(path, source);
63
+ return 'added';
64
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Source codemods — the `@zerotal/core/build` subpath.
3
+ *
4
+ * These are the transforms `make:provider` uses to register a provider in
5
+ * `bootstrap/providers.ts`. They live behind a subpath rather than the kernel
6
+ * barrel because a generator is build-time tooling: an application never calls
7
+ * them at runtime, and the barrel is deliberately frozen to the lean kernel set.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ export { addImport, addToDefaultArrayExport, registerProvider } from "./codemod.ts";
12
+ export type { RegisterProviderOptions, RegisterResult } from "./codemod.ts";
@@ -0,0 +1,254 @@
1
+ /**
2
+ * The base class every console command extends, providing argument/flag metadata,
3
+ * coloured terminal output helpers, and interactive prompts. Subclasses implement
4
+ * `run()` to do the work.
5
+ */
6
+ import { TerminalWriter } from "./OutputWriter.ts";
7
+ import type { OutputWriter } from "./OutputWriter.ts";
8
+
9
+ /** Definition of a positional argument a command accepts. */
10
+ export type ArgDef = {
11
+ name: string;
12
+ required?: boolean;
13
+ default?: string;
14
+ };
15
+
16
+ /** Definition of a named flag a command accepts. */
17
+ export type FlagDef = {
18
+ name: string;
19
+ short?: string;
20
+ type: "string" | "boolean" | "number";
21
+ description?: string;
22
+ default?: unknown;
23
+ };
24
+
25
+ /**
26
+ * Base class for console commands; subclasses declare their name/args/flags via
27
+ * static properties and implement `run()`.
28
+ *
29
+ * A command's identity and CLI surface are described with the static fields
30
+ * ({@link Command.commandName | commandName}, {@link Command.description | description},
31
+ * {@link Command.args | args}, {@link Command.flags | flags}, and
32
+ * {@link Command.needsApp | needsApp}); the work happens in `run()`, where parsed
33
+ * {@link Command.args | this.args} and {@link Command.flags | this.flags} are
34
+ * available along with output helpers (`info`, `error`, `line`, `table`, …) and
35
+ * interactive prompts (`ask`, `confirm`, `choice`, `secret`). Register the class
36
+ * with {@link CommandRunner.register} to expose it as `bun zt <commandName>`.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { Command } from "@zerotal/core";
41
+ * import type { ArgDef, FlagDef } from "@zerotal/core";
42
+ *
43
+ * export class GreetCommand extends Command {
44
+ * static commandName = "greet";
45
+ * static description = "Print a greeting";
46
+ * static needsApp = false;
47
+ * static args: ArgDef[] = [{ name: "name", required: true }];
48
+ * static flags: FlagDef[] = [
49
+ * { name: "loud", short: "l", type: "boolean", default: false },
50
+ * ];
51
+ *
52
+ * async run(): Promise<void> {
53
+ * const greeting = `Hello, ${this.args["name"]}!`;
54
+ * this.info(this.flags["loud"] ? greeting.toUpperCase() : greeting);
55
+ * }
56
+ * }
57
+ * ```
58
+ * Invoke it once registered:
59
+ * ```bash
60
+ * bun zt greet Ada --loud
61
+ * ```
62
+ */
63
+ export abstract class Command {
64
+ static commandName: string;
65
+ static description: string;
66
+ static needsApp: boolean;
67
+ static args: ArgDef[] = [];
68
+ static flags: FlagDef[] = [];
69
+
70
+ /** Output destination. Replaced with BufferWriter by Artisan.call(). */
71
+ _writer: OutputWriter = new TerminalWriter();
72
+
73
+ /** Parsed positional arguments — set by CommandRunner before run(). */
74
+ args: Record<string, string> = {};
75
+ /** Parsed flags — set by CommandRunner before run(). */
76
+ flags: Record<string, string | boolean | number> = {};
77
+ /** Application instance — set by CommandRunner when needsApp is true. */
78
+ app: unknown = undefined;
79
+
80
+ abstract run(): Promise<void>;
81
+
82
+ info(msg: string): void {
83
+ this._writer.writeLine(`\x1b[32m${msg}\x1b[0m`);
84
+ }
85
+ error(msg: string): void {
86
+ this._writer.writeError(`\x1b[31m${msg}\x1b[0m`);
87
+ }
88
+ warn(msg: string): void {
89
+ this._writer.writeLine(`\x1b[33m${msg}\x1b[0m`);
90
+ }
91
+ line(msg: string): void {
92
+ this._writer.writeLine(`\x1b[36m${msg}\x1b[0m`);
93
+ }
94
+ dim(msg: string): void {
95
+ this._writer.writeLine(`\x1b[2m${msg}\x1b[0m`);
96
+ }
97
+ write(msg: string): void {
98
+ this._writer.write(msg);
99
+ }
100
+ newLine(): void {
101
+ this._writer.writeLine("");
102
+ }
103
+ section(title: string): void {
104
+ this._writer.writeLine(`\n\x1b[1m${title}\x1b[0m`);
105
+ }
106
+ table(rows: [string, string][], indent = 2): void {
107
+ const columnWidth = Math.max(...rows.map(([key]) => key.length)) + 4;
108
+ for (const [key, value] of rows) {
109
+ this._writer.writeLine(
110
+ " ".repeat(indent) + key.padEnd(columnWidth) + `\x1b[2m${value}\x1b[0m`,
111
+ );
112
+ }
113
+ }
114
+
115
+ // ── Interactive prompts ───────────────────────────────────────────────
116
+ // These read from stdin. They only work when the process has a real TTY
117
+ // (i.e. interactive console mode). Do not call them in tests — mock
118
+ // _readLine() instead.
119
+
120
+ /**
121
+ * Prompt the user for text input and wait for Enter.
122
+ *
123
+ * @example
124
+ * const name = await this.ask('What is your name?');
125
+ * const env = await this.ask('Environment?', 'production');
126
+ */
127
+ async ask(question: string, defaultValue?: string): Promise<string> {
128
+ const hint = defaultValue ? ` [${defaultValue}]` : "";
129
+ this._writer.write(`\x1b[36m${question}${hint}: \x1b[0m`);
130
+ const answer = (await this._readLine()).trim();
131
+ return answer || defaultValue || "";
132
+ }
133
+
134
+ /**
135
+ * Prompt the user for a yes/no confirmation.
136
+ * Returns true for y/yes, false otherwise.
137
+ *
138
+ * @example
139
+ * const ok = await this.confirm('Run migrations?');
140
+ * const ok = await this.confirm('Overwrite file?', true);
141
+ */
142
+ async confirm(question: string, defaultValue = false): Promise<boolean> {
143
+ const hint = defaultValue ? "[Y/n]" : "[y/N]";
144
+ this._writer.write(`\x1b[33m${question} ${hint}: \x1b[0m`);
145
+ const answer = (await this._readLine()).trim().toLowerCase();
146
+ if (!answer) return defaultValue;
147
+ return answer === "y" || answer === "yes";
148
+ }
149
+
150
+ /**
151
+ * Prompt the user to select one option from a numbered list.
152
+ * Returns the selected string. Defaults to the first option on invalid input.
153
+ *
154
+ * @example
155
+ * const env = await this.choice('Environment:', ['local', 'staging', 'production']);
156
+ */
157
+ async choice(question: string, options: string[]): Promise<string> {
158
+ this._writer.writeLine(`\x1b[36m${question}\x1b[0m`);
159
+ options.forEach((option, index) => {
160
+ this._writer.writeLine(` \x1b[2m[${index + 1}]\x1b[0m ${option}`);
161
+ });
162
+ this._writer.write("Enter number: ");
163
+ const answer = (await this._readLine()).trim();
164
+ const index = parseInt(answer, 10) - 1;
165
+ const first = options[0] ?? "";
166
+ if (isNaN(index) || index < 0 || index >= options.length) return first;
167
+ return options[index] ?? first;
168
+ }
169
+
170
+ async secret(question: string): Promise<string> {
171
+ this._writer.write(`\x1b[36m${question} \x1b[0m`);
172
+ const tty = process.stdin as unknown as {
173
+ isTTY?: boolean;
174
+ setRawMode?: (mode: boolean) => void;
175
+ };
176
+
177
+ // Windows does not support raw-mode hidden input — fall back to a visible prompt.
178
+ // On Unix TTYs, enable raw mode so characters are not echoed.
179
+ const canHide =
180
+ !!tty.isTTY && typeof tty.setRawMode === "function" && process.platform !== "win32";
181
+
182
+ if (!canHide) {
183
+ const answer = (await this._readLine()).trim();
184
+ this._writer.writeLine("");
185
+ return answer;
186
+ }
187
+
188
+ try {
189
+ tty.setRawMode!(true);
190
+ const characters: string[] = [];
191
+ const stdinIterator = this._stdinIter();
192
+ const decoder = new TextDecoder();
193
+ outer: while (true) {
194
+ const { value, done } = await stdinIterator.next();
195
+ if (done) break;
196
+ for (const character of decoder.decode(value)) {
197
+ if (character === "\r" || character === "\n") break outer;
198
+ if (character === "\x7f" || character === "\b") {
199
+ characters.pop();
200
+ continue;
201
+ }
202
+ if (character >= " ") characters.push(character);
203
+ }
204
+ }
205
+ return characters.join("");
206
+ } finally {
207
+ tty.setRawMode!(false);
208
+ this._writer.writeLine("");
209
+ }
210
+ }
211
+
212
+ /** Read one line from stdin. Override in tests to avoid blocking. */
213
+ async _readLine(): Promise<string> {
214
+ // Return any line already buffered from a previous read.
215
+ const bufferedNewlineIndex = this._lineBuf.indexOf("\n");
216
+ if (bufferedNewlineIndex !== -1) {
217
+ const line = this._lineBuf.slice(0, bufferedNewlineIndex);
218
+ this._lineBuf = this._lineBuf.slice(bufferedNewlineIndex + 1);
219
+ return line.replace(/\r$/, "");
220
+ }
221
+
222
+ const stdinIterator = this._stdinIter();
223
+ const decoder = new TextDecoder();
224
+
225
+ while (true) {
226
+ const { value, done } = await stdinIterator.next();
227
+ if (done) break;
228
+ this._lineBuf += decoder.decode(value);
229
+ const newlineIndex = this._lineBuf.indexOf("\n");
230
+ if (newlineIndex !== -1) {
231
+ const line = this._lineBuf.slice(0, newlineIndex);
232
+ this._lineBuf = this._lineBuf.slice(newlineIndex + 1);
233
+ return line.replace(/\r$/, "");
234
+ }
235
+ }
236
+
237
+ const remaining = this._lineBuf;
238
+ this._lineBuf = "";
239
+ return remaining.replace(/\r$/, "");
240
+ }
241
+
242
+ // Shared stdin async iterator — one per Command instance so consecutive prompts
243
+ // don't each open a competing stream on the same stdin fd.
244
+ private _lineBuf = "";
245
+ private _stdinIterator: AsyncIterator<Uint8Array> | undefined;
246
+ private _stdinIter(): AsyncIterator<Uint8Array> {
247
+ if (!this._stdinIterator) {
248
+ this._stdinIterator = (Bun.stdin.stream() as unknown as AsyncIterable<Uint8Array>)[
249
+ Symbol.asyncIterator
250
+ ]();
251
+ }
252
+ return this._stdinIterator;
253
+ }
254
+ }