@maltty/cli 1.0.0-rc.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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/bin/maltty.js +3 -0
  4. package/dist/_format-CeQhQTl5.js +23 -0
  5. package/dist/_format-CeQhQTl5.js.map +1 -0
  6. package/dist/attemptAsync-BdXzRSIT.js +42 -0
  7. package/dist/attemptAsync-BdXzRSIT.js.map +1 -0
  8. package/dist/chunk-DsIazq42.js +26 -0
  9. package/dist/commands/add/command.d.ts +7 -0
  10. package/dist/commands/add/command.d.ts.map +1 -0
  11. package/dist/commands/add/command.js +123 -0
  12. package/dist/commands/add/command.js.map +1 -0
  13. package/dist/commands/add/config.d.ts +7 -0
  14. package/dist/commands/add/config.d.ts.map +1 -0
  15. package/dist/commands/add/config.js +57 -0
  16. package/dist/commands/add/config.js.map +1 -0
  17. package/dist/commands/add/index.d.ts +7 -0
  18. package/dist/commands/add/index.d.ts.map +1 -0
  19. package/dist/commands/add/index.js +7 -0
  20. package/dist/commands/add/index.js.map +1 -0
  21. package/dist/commands/add/middleware.d.ts +7 -0
  22. package/dist/commands/add/middleware.d.ts.map +1 -0
  23. package/dist/commands/add/middleware.js +88 -0
  24. package/dist/commands/add/middleware.js.map +1 -0
  25. package/dist/commands/build.d.ts +15 -0
  26. package/dist/commands/build.d.ts.map +1 -0
  27. package/dist/commands/build.js +246 -0
  28. package/dist/commands/build.js.map +1 -0
  29. package/dist/commands/commands.d.ts +14 -0
  30. package/dist/commands/commands.d.ts.map +1 -0
  31. package/dist/commands/commands.js +189 -0
  32. package/dist/commands/commands.js.map +1 -0
  33. package/dist/commands/dev.d.ts +13 -0
  34. package/dist/commands/dev.d.ts.map +1 -0
  35. package/dist/commands/dev.js +58 -0
  36. package/dist/commands/dev.js.map +1 -0
  37. package/dist/commands/doctor.d.ts +13 -0
  38. package/dist/commands/doctor.d.ts.map +1 -0
  39. package/dist/commands/doctor.js +658 -0
  40. package/dist/commands/doctor.js.map +1 -0
  41. package/dist/commands/init.d.ts +7 -0
  42. package/dist/commands/init.d.ts.map +1 -0
  43. package/dist/commands/init.js +305 -0
  44. package/dist/commands/init.js.map +1 -0
  45. package/dist/commands/run.d.ts +19 -0
  46. package/dist/commands/run.d.ts.map +1 -0
  47. package/dist/commands/run.js +478 -0
  48. package/dist/commands/run.js.map +1 -0
  49. package/dist/commands/stories.d.ts +11 -0
  50. package/dist/commands/stories.d.ts.map +1 -0
  51. package/dist/commands/stories.js +22 -0
  52. package/dist/commands/stories.js.map +1 -0
  53. package/dist/config-helpers-pv_2Spux.js +15 -0
  54. package/dist/config-helpers-pv_2Spux.js.map +1 -0
  55. package/dist/defu-BBwssjW3.js +192 -0
  56. package/dist/defu-BBwssjW3.js.map +1 -0
  57. package/dist/detect-AOq1ehTE.js +61 -0
  58. package/dist/detect-AOq1ehTE.js.map +1 -0
  59. package/dist/dist-DW_gRP7_.js +1119 -0
  60. package/dist/dist-DW_gRP7_.js.map +1 -0
  61. package/dist/dist-Xa07jNxS.js +15617 -0
  62. package/dist/dist-Xa07jNxS.js.map +1 -0
  63. package/dist/index.d.ts +1 -0
  64. package/dist/index.js +46 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/jiti-CxWlVmZ_.js +5458 -0
  67. package/dist/jiti-CxWlVmZ_.js.map +1 -0
  68. package/dist/json-D7WbuVK1.js +43 -0
  69. package/dist/json-D7WbuVK1.js.map +1 -0
  70. package/dist/json5-z-PGDMyy.js +777 -0
  71. package/dist/json5-z-PGDMyy.js.map +1 -0
  72. package/dist/jsonc-DqZkfC3S.js +401 -0
  73. package/dist/jsonc-DqZkfC3S.js.map +1 -0
  74. package/dist/jsonc-KADMdSjU.js +2 -0
  75. package/dist/lib/templates/command/command.ts.liquid +12 -0
  76. package/dist/lib/templates/config/config.ts.liquid +16 -0
  77. package/dist/lib/templates/middleware/middleware.ts.liquid +9 -0
  78. package/dist/lib/templates/project/gitignore.liquid +3 -0
  79. package/dist/lib/templates/project/maltty.config.ts.liquid +6 -0
  80. package/dist/lib/templates/project/package.json.liquid +30 -0
  81. package/dist/lib/templates/project/src/commands/hello.ts.liquid +12 -0
  82. package/dist/lib/templates/project/src/config.ts.liquid +16 -0
  83. package/dist/lib/templates/project/src/index.ts.liquid +13 -0
  84. package/dist/lib/templates/project/tsconfig.json.liquid +19 -0
  85. package/dist/lib/templates/project/vitest.config.ts.liquid +8 -0
  86. package/dist/main-C_8x2aB3.js +274 -0
  87. package/dist/main-C_8x2aB3.js.map +1 -0
  88. package/dist/manifest-BIKVDJML.js +112 -0
  89. package/dist/manifest-BIKVDJML.js.map +1 -0
  90. package/dist/multipart-parser-MNkxNO83.js +174 -0
  91. package/dist/multipart-parser-MNkxNO83.js.map +1 -0
  92. package/dist/node-C91xGGqn.js +644 -0
  93. package/dist/node-C91xGGqn.js.map +1 -0
  94. package/dist/node-Drmc03fL.js +3973 -0
  95. package/dist/node-Drmc03fL.js.map +1 -0
  96. package/dist/ohash-BPoV4zBH.js +185 -0
  97. package/dist/ohash-BPoV4zBH.js.map +1 -0
  98. package/dist/pnpm-workspace.yaml +26 -0
  99. package/dist/toml-LuHsHQtV.js +646 -0
  100. package/dist/toml-LuHsHQtV.js.map +1 -0
  101. package/dist/utils-DlIJNGLQ.js +1905 -0
  102. package/dist/utils-DlIJNGLQ.js.map +1 -0
  103. package/dist/validate-C_SXBoLq.js +18 -0
  104. package/dist/validate-C_SXBoLq.js.map +1 -0
  105. package/dist/write-CSwG4Stj.js +152 -0
  106. package/dist/write-CSwG4Stj.js.map +1 -0
  107. package/dist/yaml-BS9RDuUr.js +1067 -0
  108. package/dist/yaml-BS9RDuUr.js.map +1 -0
  109. package/package.json +65 -0
@@ -0,0 +1,1119 @@
1
+ import { i as toError, n as err, r as ok, t as attemptAsync } from "./attemptAsync-BdXzRSIT.js";
2
+ import { a as z, i as M, n as process_exports, r as fs_exports, t as path_exports } from "./node-C91xGGqn.js";
3
+ import { t as readManifest } from "./manifest-BIKVDJML.js";
4
+ import { t as compileTargets } from "./utils-DlIJNGLQ.js";
5
+ import { builtinModules, createRequire } from "node:module";
6
+ import { basename, extname, join, resolve } from "node:path";
7
+ import { readdir } from "node:fs/promises";
8
+ import { build } from "tsdown";
9
+ //#region ../../node_modules/.pnpm/es-toolkit@1.46.1/node_modules/es-toolkit/dist/function/noop.mjs
10
+ function noop() {}
11
+ //#endregion
12
+ //#region ../../node_modules/.pnpm/es-toolkit@1.46.1/node_modules/es-toolkit/dist/predicate/isNil.mjs
13
+ function isNil(x) {
14
+ return x == null;
15
+ }
16
+ //#endregion
17
+ //#region ../bundler/dist/index.js
18
+ /**
19
+ * Generate JavaScript source code for a static autoloader virtual module.
20
+ *
21
+ * The generated module statically imports every discovered command file and
22
+ * exports an `autoload()` function that returns the pre-built CommandMap.
23
+ * Directory commands that merge a parent handler with subcommands are re-tagged
24
+ * via `withTag` because the TAG symbol is non-enumerable and lost on spread.
25
+ *
26
+ * @param params - The scan result and path to the tag utility module.
27
+ * @returns JavaScript source code for the static autoloader.
28
+ */
29
+ function generateStaticAutoloader(params) {
30
+ const importLines = buildImportStatements(collectImports(params.scan), params.tagModulePath);
31
+ const commandsObject = buildCommandsObject(params.scan);
32
+ return [
33
+ ...importLines,
34
+ "",
35
+ `const commands = ${commandsObject}`,
36
+ "",
37
+ "export async function autoload() {",
38
+ " return commands",
39
+ "}",
40
+ ""
41
+ ].join("\n");
42
+ }
43
+ /**
44
+ * Collect all import entries from a scan result.
45
+ *
46
+ * @private
47
+ * @param scan - The scan result to collect imports from.
48
+ * @returns A flat array of all import entries.
49
+ */
50
+ function collectImports(scan) {
51
+ return [...scan.files.map((file) => fileToImport(file, [])), ...scan.dirs.flatMap((dir) => collectDirImports(dir, []))];
52
+ }
53
+ /**
54
+ * Recursively collect import entries from a scanned directory.
55
+ *
56
+ * @private
57
+ * @param dir - The scanned directory.
58
+ * @param parentPath - The path segments leading to this directory.
59
+ * @returns A flat array of import entries for the directory and its children.
60
+ */
61
+ function collectDirImports(dir, parentPath) {
62
+ const currentPath = [...parentPath, dir.name];
63
+ return [
64
+ ...buildIndexImport(dir.index, currentPath),
65
+ ...dir.files.map((file) => fileToImport(file, currentPath)),
66
+ ...dir.dirs.flatMap((sub) => collectDirImports(sub, currentPath))
67
+ ];
68
+ }
69
+ /**
70
+ * Create an import entry for a leaf command file.
71
+ *
72
+ * @private
73
+ * @param file - The scanned file.
74
+ * @param parentPath - The path segments leading to the file's parent directory.
75
+ * @returns An import entry with a generated identifier.
76
+ */
77
+ function fileToImport(file, parentPath) {
78
+ return {
79
+ filePath: file.filePath,
80
+ identifier: toIdentifier([...parentPath, file.name])
81
+ };
82
+ }
83
+ /**
84
+ * Convert a path segment array to a valid JavaScript identifier.
85
+ *
86
+ * Joins segments with underscores and prefixes with `_`.
87
+ * Example: `['deploy', 'preview']` becomes `_deploy_preview`.
88
+ *
89
+ * @private
90
+ * @param segments - The path segments to convert.
91
+ * @returns A valid JavaScript identifier string.
92
+ */
93
+ function toIdentifier(segments) {
94
+ return `_${segments.map((s) => s.replaceAll("-", "$")).join("_")}`;
95
+ }
96
+ /**
97
+ * Build the array of import statement lines.
98
+ *
99
+ * @private
100
+ * @param imports - All collected import entries.
101
+ * @param tagModulePath - Absolute path to the tag utility module.
102
+ * @returns An array of import statement strings.
103
+ */
104
+ function buildImportStatements(imports, tagModulePath) {
105
+ const tagLine = buildTagImportLine(imports, tagModulePath);
106
+ const importLines = imports.map((entry) => `import ${entry.identifier} from '${path_exports.toImportUrl(entry.filePath)}'`);
107
+ return [
108
+ ...tagLine,
109
+ "",
110
+ ...importLines
111
+ ];
112
+ }
113
+ /**
114
+ * Build the JavaScript object literal string for the top-level commands map.
115
+ *
116
+ * @private
117
+ * @param scan - The scan result.
118
+ * @returns A string representation of the commands object literal.
119
+ */
120
+ function buildCommandsObject(scan) {
121
+ return formatObject([...scan.files.map((file) => buildFileEntry(file, [])), ...scan.dirs.map((dir) => buildDirEntry(dir, []))]);
122
+ }
123
+ /**
124
+ * Build an object entry string for a leaf command file.
125
+ *
126
+ * @private
127
+ * @param file - The scanned file.
128
+ * @param parentPath - Path segments to the file's parent.
129
+ * @returns A string like `'status': _status`.
130
+ */
131
+ function buildFileEntry(file, parentPath) {
132
+ const identifier = toIdentifier([...parentPath, file.name]);
133
+ return `'${file.name}': ${identifier}`;
134
+ }
135
+ /**
136
+ * Build an object entry string for a directory command (possibly with subcommands).
137
+ *
138
+ * @private
139
+ * @param dir - The scanned directory.
140
+ * @param parentPath - Path segments to the directory's parent.
141
+ * @returns A string representing the directory command with withTag wrapping.
142
+ */
143
+ function buildDirEntry(dir, parentPath) {
144
+ const currentPath = [...parentPath, dir.name];
145
+ const commandsObj = formatObject([...dir.files.map((file) => buildFileEntry(file, currentPath)), ...dir.dirs.map((sub) => buildDirEntry(sub, currentPath))]);
146
+ if (dir.index) {
147
+ const indexIdentifier = toIdentifier(currentPath);
148
+ return `'${dir.name}': withTag({ ...${indexIdentifier}, commands: ${commandsObj} }, 'Command')`;
149
+ }
150
+ return `'${dir.name}': withTag({ commands: ${commandsObj} }, 'Command')`;
151
+ }
152
+ /**
153
+ * Build the import entry for an index file, if present.
154
+ *
155
+ * @private
156
+ * @param index - The absolute path to the index file, or undefined.
157
+ * @param currentPath - The current path segments for identifier generation.
158
+ * @returns An array with zero or one import entries.
159
+ */
160
+ function buildIndexImport(index, currentPath) {
161
+ if (!index) return [];
162
+ return [{
163
+ filePath: index,
164
+ identifier: toIdentifier(currentPath)
165
+ }];
166
+ }
167
+ /**
168
+ * Build the tag import line if any imports exist.
169
+ *
170
+ * @private
171
+ * @param imports - The collected import entries.
172
+ * @param tagModulePath - The absolute path to the tag module.
173
+ * @returns An array with zero or one import statement strings.
174
+ */
175
+ function buildTagImportLine(imports, tagModulePath) {
176
+ if (imports.length === 0) return [];
177
+ return [`import { withTag } from '${path_exports.toImportUrl(tagModulePath)}'`];
178
+ }
179
+ /**
180
+ * Format an array of key-value strings as a JavaScript object literal.
181
+ *
182
+ * @private
183
+ * @param entries - The key-value pair strings.
184
+ * @returns A formatted object literal string.
185
+ */
186
+ function formatObject(entries) {
187
+ if (entries.length === 0) return "{}";
188
+ return `{\n${entries.map((entry) => ` ${entry},`).join("\n")}\n}`;
189
+ }
190
+ const VALID_EXTENSIONS = new Set([
191
+ ".ts",
192
+ ".js",
193
+ ".mjs",
194
+ ".tsx",
195
+ ".jsx"
196
+ ]);
197
+ const INDEX_NAME = "index";
198
+ /**
199
+ * Scan a commands directory and produce a tree structure for static code generation.
200
+ *
201
+ * Mirrors the runtime autoloader's rules: valid extensions are `.ts`, `.js`, `.mjs`;
202
+ * files and directories starting with `_` or `.` are skipped; `index` files in
203
+ * subdirectories become parent command handlers.
204
+ *
205
+ * @param dir - Absolute path to the commands directory.
206
+ * @returns A tree of scanned files and directories.
207
+ */
208
+ async function scanCommandsDir(dir) {
209
+ const entries = await readdir(dir, { withFileTypes: true });
210
+ const files = entries.filter(isCommandFile).map((entry) => toScannedFile(dir, entry));
211
+ return {
212
+ dirs: await Promise.all(entries.filter(isCommandDir).map((entry) => scanSubDir(join(dir, entry.name)))),
213
+ files
214
+ };
215
+ }
216
+ /**
217
+ * Recursively scan a subdirectory into a ScannedDir.
218
+ *
219
+ * @private
220
+ * @param dir - Absolute path to the subdirectory.
221
+ * @returns A ScannedDir representing the directory and its contents.
222
+ */
223
+ async function scanSubDir(dir) {
224
+ const name = basename(dir);
225
+ const entries = await readdir(dir, { withFileTypes: true });
226
+ const indexEntry = findIndexEntry(entries);
227
+ const files = entries.filter(isCommandFile).map((entry) => toScannedFile(dir, entry));
228
+ return {
229
+ dirs: await Promise.all(entries.filter(isCommandDir).map((entry) => scanSubDir(join(dir, entry.name)))),
230
+ files,
231
+ index: resolveIndexPath(dir, indexEntry),
232
+ name
233
+ };
234
+ }
235
+ /**
236
+ * Convert a directory entry into a ScannedFile.
237
+ *
238
+ * @private
239
+ * @param dir - Parent directory absolute path.
240
+ * @param entry - The directory entry for the file.
241
+ * @returns A ScannedFile with name and absolute file path.
242
+ */
243
+ function toScannedFile(dir, entry) {
244
+ return {
245
+ filePath: join(dir, entry.name),
246
+ name: basename(entry.name, extname(entry.name))
247
+ };
248
+ }
249
+ /**
250
+ * Find the index file entry among a list of directory entries.
251
+ *
252
+ * @private
253
+ * @param entries - The directory entries to search.
254
+ * @returns The Dirent for the index file, or undefined.
255
+ */
256
+ function findIndexEntry(entries) {
257
+ return entries.find((entry) => entry.isFile() && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".d.tsx") && VALID_EXTENSIONS.has(extname(entry.name)) && basename(entry.name, extname(entry.name)) === INDEX_NAME);
258
+ }
259
+ /**
260
+ * Predicate: entry is a valid command file (not index, not hidden/private).
261
+ *
262
+ * @private
263
+ * @param entry - The directory entry to check.
264
+ * @returns True when the entry is a scannable command file.
265
+ */
266
+ function isCommandFile(entry) {
267
+ if (!entry.isFile()) return false;
268
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) return false;
269
+ if (entry.name.endsWith(".d.ts") || entry.name.endsWith(".d.tsx")) return false;
270
+ if (!VALID_EXTENSIONS.has(extname(entry.name))) return false;
271
+ return basename(entry.name, extname(entry.name)) !== INDEX_NAME;
272
+ }
273
+ /**
274
+ * Predicate: entry is a scannable command directory (not hidden/private).
275
+ *
276
+ * @private
277
+ * @param entry - The directory entry to check.
278
+ * @returns True when the entry is a scannable command directory.
279
+ */
280
+ function isCommandDir(entry) {
281
+ if (!entry.isDirectory()) return false;
282
+ return !entry.name.startsWith("_") && !entry.name.startsWith(".");
283
+ }
284
+ /**
285
+ * Resolve the absolute path to an index file entry, or undefined.
286
+ *
287
+ * @private
288
+ * @param dir - The parent directory absolute path.
289
+ * @param entry - The index file Dirent, or undefined.
290
+ * @returns The absolute path to the index file, or undefined.
291
+ */
292
+ function resolveIndexPath(dir, entry) {
293
+ if (!entry) return;
294
+ return join(dir, entry.name);
295
+ }
296
+ const VIRTUAL_MODULE_ID = "virtual:maltty-static-commands";
297
+ const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_MODULE_ID}`;
298
+ const AUTOLOADER_REGION_START = "//#region src/autoload.ts";
299
+ const AUTOLOADER_REGION_END = "//#endregion";
300
+ /**
301
+ * Create a rolldown plugin that replaces the runtime autoloader with a static version.
302
+ *
303
+ * Uses a three-hook approach to break the circular dependency between maltty's
304
+ * dist and user command files (which `import { command } from 'maltty'`):
305
+ *
306
+ * 1. `transform` — detects maltty's pre-bundled dist and replaces the autoloader
307
+ * region with a dynamic `import()` to a virtual module
308
+ * 2. `resolveId` — resolves the virtual module identifier
309
+ * 3. `load` — scans the commands directory and generates a static autoloader
310
+ * module with all command imports pre-resolved
311
+ *
312
+ * The dynamic import ensures command files execute after maltty's code is fully
313
+ * initialized, avoiding `ReferenceError` from accessing `TAG` before its
314
+ * declaration.
315
+ *
316
+ * @param params - The commands directory and tag module path.
317
+ * @returns A rolldown plugin for static autoloading.
318
+ */
319
+ function createAutoloadPlugin(params) {
320
+ return {
321
+ async load(id) {
322
+ if (id !== RESOLVED_VIRTUAL_ID) return null;
323
+ return generateStaticAutoloader({
324
+ scan: await scanCommandsDir(params.commandsDir),
325
+ tagModulePath: params.tagModulePath
326
+ });
327
+ },
328
+ name: "maltty-static-autoloader",
329
+ resolveId(source) {
330
+ if (source === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_ID;
331
+ return null;
332
+ },
333
+ transform(code, _id) {
334
+ const regionStart = code.indexOf(AUTOLOADER_REGION_START);
335
+ if (regionStart === -1) return null;
336
+ const regionEnd = code.indexOf(AUTOLOADER_REGION_END, regionStart);
337
+ if (regionEnd === -1) return null;
338
+ const before = code.slice(0, regionStart);
339
+ const after = code.slice(regionEnd + 12);
340
+ return `${before}${buildStaticRegion()}${after}`;
341
+ }
342
+ };
343
+ }
344
+ /**
345
+ * Build the replacement autoloader region that delegates to the virtual module.
346
+ *
347
+ * @private
348
+ * @returns The replacement region string with dynamic import.
349
+ */
350
+ function buildStaticRegion() {
351
+ return [
352
+ "//#region src/autoload.ts (static)",
353
+ "async function autoload() {",
354
+ ` const mod = await import('${VIRTUAL_MODULE_ID}')`,
355
+ " return mod.autoload()",
356
+ "}",
357
+ "//#endregion"
358
+ ].join("\n");
359
+ }
360
+ /**
361
+ * Shebang line prepended to CLI entry files.
362
+ */
363
+ const SHEBANG = "#!/usr/bin/env node\n";
364
+ /**
365
+ * Default entry point for the CLI source.
366
+ */
367
+ const DEFAULT_ENTRY = "./src/index.ts";
368
+ /**
369
+ * Default directory for CLI commands.
370
+ */
371
+ const DEFAULT_COMMANDS = "./commands";
372
+ /**
373
+ * File extensions produced by maltty builds that are safe to remove during clean.
374
+ */
375
+ const BUILD_ARTIFACT_EXTENSIONS = [
376
+ ".js",
377
+ ".mjs",
378
+ ".js.map",
379
+ ".mjs.map"
380
+ ];
381
+ /**
382
+ * Packages that must always be bundled into the output.
383
+ *
384
+ * The `maltty` framework and its internal `@maltty/*` packages must be inlined
385
+ * so the autoload plugin can intercept and replace the runtime autoloader
386
+ * with a static version for compiled binaries.
387
+ */
388
+ const ALWAYS_BUNDLE = [/^@?maltty/];
389
+ /**
390
+ * Packages that are optional or conditional dependencies of bundled libraries
391
+ * (c12, ink) that must be stubbed during compile-mode builds. These are behind
392
+ * dynamic `import()` calls or runtime guards that never execute in production,
393
+ * but when all deps are inlined, rolldown traces into them statically.
394
+ */
395
+ const STUB_PACKAGES = [
396
+ "chokidar",
397
+ "magicast",
398
+ "giget",
399
+ "react-devtools-core"
400
+ ];
401
+ /**
402
+ * Node.js builtin modules in both bare and `node:` prefixed forms.
403
+ */
404
+ const NODE_BUILTINS = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)];
405
+ const TAG_MODULE_PATH = createRequire(import.meta.url).resolve("@maltty/utils/tag");
406
+ /**
407
+ * Convert a resolved bundler config to a tsdown InlineConfig for production builds.
408
+ *
409
+ * @param params - The resolved config and optional version for compile-time injection.
410
+ * @returns A tsdown InlineConfig ready for `build()`.
411
+ */
412
+ function toTsdownBuildConfig(params) {
413
+ return {
414
+ banner: SHEBANG,
415
+ clean: false,
416
+ config: false,
417
+ cwd: params.config.cwd,
418
+ define: buildDefine({
419
+ define: params.config.build.define,
420
+ version: params.config.version
421
+ }),
422
+ deps: buildDeps(params.config.build.external, params.compile ?? false),
423
+ dts: false,
424
+ entry: { index: params.config.entry },
425
+ format: "esm",
426
+ inputOptions: { resolve: { mainFields: ["module", "main"] } },
427
+ logLevel: "silent",
428
+ minify: params.config.build.minify,
429
+ outDir: params.config.buildOutDir,
430
+ outputOptions: { codeSplitting: false },
431
+ platform: "node",
432
+ plugins: [createAutoloadPlugin({
433
+ commandsDir: params.config.commands,
434
+ tagModulePath: TAG_MODULE_PATH
435
+ }), ...buildPlugins(params.compile ?? false)],
436
+ sourcemap: params.config.build.sourcemap,
437
+ target: params.config.build.target,
438
+ treeshake: true
439
+ };
440
+ }
441
+ /**
442
+ * Convert a resolved bundler config to a tsdown InlineConfig for watch mode.
443
+ *
444
+ * @param params - The resolved config, optional version, and optional success callback.
445
+ * @returns A tsdown InlineConfig with `watch: true`.
446
+ */
447
+ function toTsdownWatchConfig(params) {
448
+ return {
449
+ ...toTsdownBuildConfig({ config: params.config }),
450
+ logLevel: "error",
451
+ onSuccess: params.onSuccess,
452
+ watch: true
453
+ };
454
+ }
455
+ /**
456
+ * Build the `deps` configuration for tsdown.
457
+ *
458
+ * When compiling to a standalone binary, all dependencies must be inlined so
459
+ * `bun build --compile` never encounters unresolvable bare imports. Only
460
+ * Node.js builtins and explicit user externals are kept external.
461
+ *
462
+ * In normal (non-compile) mode, only `@maltty/*` packages are force-bundled
463
+ * and everything else in `node_modules` is left external by tsdown's default.
464
+ *
465
+ * @private
466
+ * @param userExternals - Additional packages the user explicitly marked external.
467
+ * @param compile - Whether the build targets a compiled binary.
468
+ * @returns A tsdown `deps` configuration object.
469
+ */
470
+ function buildDeps(userExternals, compile) {
471
+ return {
472
+ alwaysBundle: resolveAlwaysBundle(compile),
473
+ neverBundle: [...NODE_BUILTINS, ...userExternals]
474
+ };
475
+ }
476
+ /**
477
+ * Resolve the `alwaysBundle` rule list based on whether we're compiling.
478
+ *
479
+ * @private
480
+ * @param compile - Whether the build targets a compiled binary.
481
+ * @returns The matchers tsdown should always bundle.
482
+ */
483
+ function resolveAlwaysBundle(compile) {
484
+ if (compile) return [/./];
485
+ return [...ALWAYS_BUNDLE];
486
+ }
487
+ /**
488
+ * Build the `define` map for tsdown/rolldown.
489
+ *
490
+ * Merges three sources (lowest to highest precedence):
491
+ * 1. `MALTTY_PUBLIC_*` env vars — prefixed with `process.env.` for rolldown replacement
492
+ * 2. `__MALTTY_VERSION__` — injected when a version string is available
493
+ * 3. Explicit `define` from `maltty.config.ts` — user overrides win
494
+ *
495
+ * @private
496
+ * @param params - The version and user-defined constants.
497
+ * @returns A define map for tsdown/rolldown.
498
+ */
499
+ function buildDefine(params) {
500
+ const envVars = resolveBuildVars();
501
+ const envDefines = Object.fromEntries(Object.entries(envVars).map(([key, value]) => [`process.env.${key}`, value]));
502
+ const versionDefine = M(params.version).with(void 0, () => ({})).otherwise((v) => ({ __MALTTY_VERSION__: JSON.stringify(v) }));
503
+ return {
504
+ ...envDefines,
505
+ ...versionDefine,
506
+ ...params.define
507
+ };
508
+ }
509
+ /**
510
+ * Resolve `MALTTY_PUBLIC_*` environment variables into a define map.
511
+ *
512
+ * Scans `process.env` for keys prefixed with `MALTTY_PUBLIC_` and returns
513
+ * clean key-value pairs with JSON-stringified values.
514
+ *
515
+ * @param env - The environment variables to scan (defaults to `process.env`).
516
+ * @returns A define map with clean keys (no `process.env.` prefix).
517
+ */
518
+ function resolveBuildVars(env = process.env) {
519
+ return Object.fromEntries(Object.entries(env).filter(([key]) => key.startsWith("MALTTY_PUBLIC_")).map(([key, value]) => [key, JSON.stringify(value ?? "")]));
520
+ }
521
+ /**
522
+ * Build additional plugins needed when compiling to a standalone binary.
523
+ *
524
+ * When `compile` is true, includes a stub plugin that replaces optional/conditional
525
+ * dependencies with empty modules so rolldown (and subsequently bun compile) never
526
+ * attempts to resolve packages that don't exist at runtime.
527
+ *
528
+ * @private
529
+ * @param compile - Whether the build targets a compiled binary.
530
+ * @returns An array of rolldown plugins (empty when not compiling).
531
+ */
532
+ function buildPlugins(compile) {
533
+ if (compile) return [createStubPlugin(STUB_PACKAGES)];
534
+ return [];
535
+ }
536
+ /**
537
+ * Create a rolldown plugin that replaces specified packages with empty modules.
538
+ *
539
+ * Libraries like c12 and ink have optional/conditional dependencies behind
540
+ * dynamic `import()` calls or runtime guards (e.g. `isDev()`). When all deps
541
+ * are inlined for compile mode, rolldown traces into these statically. Stubbing
542
+ * at the resolve level ensures the real package is never loaded.
543
+ *
544
+ * @private
545
+ * @param packages - Package names to replace with empty modules.
546
+ * @returns A rolldown plugin.
547
+ */
548
+ function createStubPlugin(packages) {
549
+ const stubbed = new Set(packages);
550
+ const STUB_PREFIX = "\0stub:";
551
+ return {
552
+ name: "maltty-stub-packages",
553
+ resolveId(source) {
554
+ if (stubbed.has(source)) return `${STUB_PREFIX}${source}`;
555
+ return null;
556
+ },
557
+ load(id) {
558
+ if (id.startsWith(STUB_PREFIX)) return "export default undefined;";
559
+ return null;
560
+ }
561
+ };
562
+ }
563
+ /**
564
+ * Remove maltty build artifacts from the output directory.
565
+ *
566
+ * Removes files matching known build artifact extensions (`.js`, `.mjs`,
567
+ * `.js.map`, `.mjs.map`). When compile mode is active, also removes the
568
+ * exact binary files that would be produced based on the resolved compile
569
+ * name and targets.
570
+ *
571
+ * @param params - The resolved config and whether compile mode is active.
572
+ * @returns A {@link CleanResult} describing what was removed and what was skipped.
573
+ */
574
+ async function clean(params) {
575
+ const [listError, entries] = await fs_exports.list(params.resolved.buildOutDir);
576
+ if (listError) return {
577
+ foreign: [],
578
+ removed: []
579
+ };
580
+ const binaryNames = M(params).with({ compile: true }, ({ resolved }) => buildBinaryNames(resolved.compile.name, resolved.compile.targets)).otherwise(() => /* @__PURE__ */ new Set());
581
+ const results = await Promise.all(entries.map(async (name) => {
582
+ if (isBuildArtifact(name) || binaryNames.has(name)) {
583
+ const [removeError] = await fs_exports.remove(join(params.resolved.buildOutDir, name));
584
+ if (removeError) return {
585
+ type: "foreign",
586
+ name
587
+ };
588
+ return {
589
+ type: "removed",
590
+ name
591
+ };
592
+ }
593
+ return {
594
+ type: "foreign",
595
+ name
596
+ };
597
+ }));
598
+ return {
599
+ removed: results.filter((r) => r.type === "removed").map((r) => r.name),
600
+ foreign: results.filter((r) => r.type === "foreign").map((r) => r.name)
601
+ };
602
+ }
603
+ /**
604
+ * Check whether a filename matches a known build artifact extension.
605
+ *
606
+ * @private
607
+ * @param filename - The filename to check.
608
+ * @returns `true` when the file ends with a known build artifact extension.
609
+ */
610
+ function isBuildArtifact(filename) {
611
+ return BUILD_ARTIFACT_EXTENSIONS.some((ext) => filename.endsWith(ext));
612
+ }
613
+ /**
614
+ * Build the set of exact binary filenames that compile would produce.
615
+ *
616
+ * Single-target builds produce `{name}`, multi-target builds produce
617
+ * `{name}-{target}`. Windows targets append `.exe` to match the file bun
618
+ * actually creates on disk.
619
+ *
620
+ * @private
621
+ * @param name - The resolved binary base name.
622
+ * @param targets - The resolved compile targets (may be empty → defaults used).
623
+ * @returns A set of filenames to remove.
624
+ */
625
+ function buildBinaryNames(name, targets) {
626
+ const resolvedTargets = M(targets).with([], () => compileTargets.filter((t) => t.default).map((t) => t.target)).otherwise(() => targets);
627
+ const names = resolvedTargets.map((target) => {
628
+ const base = M(resolvedTargets.length).with(1, () => name).otherwise(() => `${name}-${target}`);
629
+ if (target.startsWith("windows-")) return `${base}.exe`;
630
+ return base;
631
+ });
632
+ return new Set(names);
633
+ }
634
+ /**
635
+ * Build a descriptive error message for a failed tsdown operation.
636
+ *
637
+ * When verbose is false only a short header is returned. When verbose is true
638
+ * the underlying error message is appended so callers get actionable detail.
639
+ *
640
+ * @param params - The phase, error, and verbose flag.
641
+ * @returns A formatted error message.
642
+ */
643
+ function formatBuildError(params) {
644
+ const header = `tsdown ${params.phase} failed`;
645
+ const detail = toError(params.error).message.trim();
646
+ if (params.verbose && detail.length > 0) return `${header}\n${detail}`;
647
+ return header;
648
+ }
649
+ /**
650
+ * Known entry file names produced by tsdown for ESM builds, in preference order.
651
+ */
652
+ const ENTRY_CANDIDATES = ["index.mjs", "index.js"];
653
+ /**
654
+ * Resolve the bundled entry file in a build output directory.
655
+ *
656
+ * tsdown may produce `index.mjs` or `index.js` depending on the project's
657
+ * `package.json` `type` field and tsdown configuration. This function checks
658
+ * for both candidates and returns the first one that exists on disk.
659
+ *
660
+ * @param outDir - Absolute path to the build output directory.
661
+ * @returns The absolute path to the entry file, or `undefined` when none is found.
662
+ */
663
+ async function resolveBuildEntry(outDir) {
664
+ const candidates = ENTRY_CANDIDATES.map((name) => join(outDir, name));
665
+ return M((await Promise.all(candidates.map(async (path) => ({
666
+ path,
667
+ found: await fs_exports.exists(path)
668
+ })))).find((r) => r.found)).with(void 0, () => void 0).otherwise((entry) => entry.path);
669
+ }
670
+ /**
671
+ * Run the tsdown build with a resolved config.
672
+ *
673
+ * Cleans artifacts when enabled, maps to a tsdown InlineConfig, and invokes
674
+ * tsdown's build API.
675
+ *
676
+ * @param params - The resolved config and whether compile mode is active.
677
+ * @returns A result tuple with build output on success or an Error on failure.
678
+ */
679
+ async function build$1(params) {
680
+ if (params.resolved.build.clean) await clean({
681
+ resolved: params.resolved,
682
+ compile: params.compile
683
+ });
684
+ const inlineConfig = toTsdownBuildConfig({
685
+ compile: params.compile,
686
+ config: params.resolved
687
+ });
688
+ const [buildError] = await attemptAsync(() => build(inlineConfig));
689
+ if (buildError) return err(new Error(formatBuildError({
690
+ phase: "build",
691
+ error: buildError,
692
+ verbose: params.verbose ?? false
693
+ }), { cause: buildError }));
694
+ const entryFile = await resolveBuildEntry(params.resolved.buildOutDir);
695
+ if (!entryFile) return err(/* @__PURE__ */ new Error(`build produced no entry file in ${params.resolved.buildOutDir}`));
696
+ return ok({
697
+ define: {
698
+ ...resolveBuildVars(),
699
+ ...params.resolved.build.define
700
+ },
701
+ entryFile,
702
+ outDir: params.resolved.buildOutDir,
703
+ version: params.resolved.version
704
+ });
705
+ }
706
+ /**
707
+ * Start a watch-mode build using tsdown.
708
+ *
709
+ * The returned promise resolves only when tsdown's watch terminates (typically on process exit).
710
+ *
711
+ * @param params - The resolved config, optional success callback, and verbose flag.
712
+ * @returns A result tuple with void on success or an Error on failure.
713
+ */
714
+ async function watch(params) {
715
+ const watchConfig = toTsdownWatchConfig({
716
+ config: params.resolved,
717
+ onSuccess: params.onSuccess
718
+ });
719
+ const [watchError] = await attemptAsync(() => build(watchConfig));
720
+ if (watchError) return err(new Error(formatBuildError({
721
+ phase: "watch",
722
+ error: watchError,
723
+ verbose: params.verbose ?? false
724
+ }), { cause: watchError }));
725
+ return ok();
726
+ }
727
+ /**
728
+ * Compile a maltty CLI tool into standalone binaries using `bun build --compile`.
729
+ *
730
+ * Expects the bundled entry to already exist in `outDir` (i.e., `build()` must
731
+ * be run first). For each requested target (or defaults when none configured),
732
+ * spawns `bun build --compile` to produce a self-contained binary.
733
+ *
734
+ * @param params - The resolved config, lifecycle hooks, and verbose flag.
735
+ * @returns A result tuple with compile output on success or an Error on failure.
736
+ */
737
+ async function compile(params) {
738
+ if (!await process_exports.exists("bun")) return err(/* @__PURE__ */ new Error("bun is not installed or not found in PATH. Install it from https://bun.sh to use compile."));
739
+ const bundledEntry = await resolveBuildEntry(params.resolved.buildOutDir);
740
+ if (!bundledEntry) return err(/* @__PURE__ */ new Error(`bundled entry not found in ${params.resolved.buildOutDir} — run build() first`));
741
+ const targets = resolveTargets(params.resolved.compile.targets);
742
+ const isMultiTarget = targets.length > 1;
743
+ const results = await compileTargetsSequentially({
744
+ autoloadDotenv: params.resolved.compile.autoloadDotenv,
745
+ bundledEntry,
746
+ cwd: params.resolved.cwd,
747
+ isMultiTarget,
748
+ lifecycle: params.lifecycle,
749
+ name: params.resolved.compile.name,
750
+ outDir: params.resolved.compileOutDir,
751
+ targets,
752
+ verbose: params.verbose ?? false
753
+ });
754
+ await cleanBunBuildArtifacts(params.resolved.cwd);
755
+ const failedResult = results.find((r) => r[0] !== null);
756
+ if (failedResult) return err(failedResult[0]);
757
+ return ok({ binaries: results.filter((r) => r[1] !== null).map(([, binary]) => binary) });
758
+ }
759
+ /**
760
+ * Look up the human-readable label for a compile target.
761
+ *
762
+ * @param target - The compile target identifier.
763
+ * @returns A descriptive label (e.g., "macOS Apple Silicon").
764
+ */
765
+ function resolveTargetLabel(target) {
766
+ const entry = compileTargets.find((t) => t.target === target);
767
+ if (entry) return entry.label;
768
+ return target;
769
+ }
770
+ /**
771
+ * Compile targets one at a time to avoid overwhelming bun with concurrent processes.
772
+ *
773
+ * @private
774
+ * @param params - The targets and compilation parameters.
775
+ * @returns The accumulated result tuples for each target.
776
+ */
777
+ async function compileTargetsSequentially(params) {
778
+ return params.targets.reduce(async (accPromise, target) => {
779
+ const acc = await accPromise;
780
+ const meta = {
781
+ target,
782
+ label: resolveTargetLabel(target)
783
+ };
784
+ if (params.lifecycle.onStepStart) await params.lifecycle.onStepStart({
785
+ phase: "compile",
786
+ step: "target",
787
+ meta
788
+ });
789
+ const result = await compileSingleTarget({
790
+ autoloadDotenv: params.autoloadDotenv,
791
+ bundledEntry: params.bundledEntry,
792
+ cwd: params.cwd,
793
+ isMultiTarget: params.isMultiTarget,
794
+ name: params.name,
795
+ outDir: params.outDir,
796
+ target,
797
+ verbose: params.verbose
798
+ });
799
+ if (params.lifecycle.onStepFinish) await params.lifecycle.onStepFinish({
800
+ phase: "compile",
801
+ step: "target",
802
+ meta
803
+ });
804
+ return [...acc, result];
805
+ }, Promise.resolve([]));
806
+ }
807
+ /**
808
+ * Compile a single target via `bun build --compile`.
809
+ *
810
+ * @private
811
+ * @param params - Target compilation parameters.
812
+ * @returns A result tuple with the compiled binary info or an error.
813
+ */
814
+ async function compileSingleTarget(params) {
815
+ const binaryName = resolveBinaryName$1(params.name, params.target, params.isMultiTarget);
816
+ const outfile = join(params.outDir, binaryName);
817
+ const [mapError, bunTarget] = mapCompileTarget(params.target);
818
+ if (mapError) return err(mapError);
819
+ const args = [
820
+ "build",
821
+ "--compile",
822
+ ...resolveAutoloadFlags({ autoloadDotenv: params.autoloadDotenv }),
823
+ params.bundledEntry,
824
+ "--outfile",
825
+ outfile,
826
+ "--target",
827
+ bunTarget
828
+ ];
829
+ const [execError] = await process_exports.exec({
830
+ cmd: "bun",
831
+ args,
832
+ cwd: params.cwd
833
+ });
834
+ if (execError) return err(new Error(formatCompileError(params.target, execError, params.verbose), { cause: execError }));
835
+ return ok({
836
+ label: resolveTargetLabel(params.target),
837
+ path: outfile,
838
+ target: params.target
839
+ });
840
+ }
841
+ /**
842
+ * Resolve the list of compile targets, falling back to the default set.
843
+ *
844
+ * @private
845
+ * @param explicit - User-specified targets (may be empty).
846
+ * @returns The targets to compile for.
847
+ */
848
+ function resolveTargets(explicit) {
849
+ if (explicit.length > 0) return explicit;
850
+ return compileTargets.filter((t) => t.default).map((t) => t.target);
851
+ }
852
+ /**
853
+ * Build the output binary name, appending the target suffix for multi-target
854
+ * builds and the `.exe` extension for Windows targets.
855
+ *
856
+ * Windows targets must end with `.exe` because that is the file bun actually
857
+ * produces (bun auto-appends `.exe` for any `bun-windows-*` target). Recording
858
+ * the path without `.exe` would cause filesystem operations on the recorded
859
+ * path to fail and would diverge from what is on disk.
860
+ *
861
+ * @private
862
+ * @param name - Base binary name.
863
+ * @param target - The compile target.
864
+ * @param isMultiTarget - Whether multiple targets are being compiled.
865
+ * @returns The resolved binary file name.
866
+ */
867
+ function resolveBinaryName$1(name, target, isMultiTarget) {
868
+ return appendBinaryExtension(buildBaseBinaryName(name, target, isMultiTarget), target);
869
+ }
870
+ /**
871
+ * Build the base name without the platform extension.
872
+ *
873
+ * @private
874
+ * @param name - Base binary name.
875
+ * @param target - The compile target.
876
+ * @param isMultiTarget - Whether multiple targets are being compiled.
877
+ * @returns The base binary name with optional target suffix.
878
+ */
879
+ function buildBaseBinaryName(name, target, isMultiTarget) {
880
+ if (isMultiTarget) return `${name}-${target}`;
881
+ return name;
882
+ }
883
+ /**
884
+ * Append the `.exe` extension to Windows binary names.
885
+ *
886
+ * @private
887
+ * @param base - The base binary name.
888
+ * @param target - The compile target.
889
+ * @returns The base name plus `.exe` for Windows targets, or the base unchanged.
890
+ */
891
+ function appendBinaryExtension(base, target) {
892
+ if (target.startsWith("windows-")) return `${base}.exe`;
893
+ return base;
894
+ }
895
+ /**
896
+ * Map a `CompileTarget` to Bun's `--target` string.
897
+ *
898
+ * Every supported maltty target must have an explicit mapping. An unrecognized
899
+ * target is a fatal error — it means `compileTargets` was extended without
900
+ * updating this function.
901
+ *
902
+ * @private
903
+ * @param target - The maltty compile target.
904
+ * @returns A result with the Bun target string, or an Error for unknown targets.
905
+ */
906
+ function mapCompileTarget(target) {
907
+ return M(target).with("darwin-arm64", () => ok("bun-darwin-arm64")).with("darwin-x64", () => ok("bun-darwin-x64")).with("linux-arm64", () => ok("bun-linux-arm64")).with("linux-x64", () => ok("bun-linux-x64")).with("linux-x64-musl", () => ok("bun-linux-x64-musl")).with("windows-arm64", () => ok("bun-windows-arm64")).with("windows-x64", () => ok("bun-windows-x64")).otherwise(() => err(/* @__PURE__ */ new Error(`unknown compile target: ${target}`)));
908
+ }
909
+ /**
910
+ * Build a descriptive error message for a failed compile target.
911
+ *
912
+ * @private
913
+ * @param target - The compile target that failed.
914
+ * @param execError - The error returned by execFile.
915
+ * @param verbose - Whether to include stderr output in the message.
916
+ * @returns A formatted error message.
917
+ */
918
+ function formatCompileError(target, execError, verbose) {
919
+ const header = `bun build --compile failed for target ${target}`;
920
+ if (!verbose) return header;
921
+ const { stderr } = execError;
922
+ if (stderr && stderr.trim().length > 0) return `${header}\n${stderr.trim()}`;
923
+ return header;
924
+ }
925
+ /**
926
+ * Build the CLI flags that control Bun's compile-time config autoloading.
927
+ *
928
+ * `bunfig.toml` loading is always disabled — maltty CLIs should never load
929
+ * Bun runtime config. `.env` loading is controlled by the `autoloadDotenv`
930
+ * option (disabled by default).
931
+ *
932
+ * @private
933
+ * @param params - The autoload settings.
934
+ * @returns An array of CLI flag strings.
935
+ */
936
+ function resolveAutoloadFlags(params) {
937
+ return [{
938
+ enabled: false,
939
+ flag: "--no-compile-autoload-bunfig"
940
+ }, {
941
+ enabled: params.autoloadDotenv,
942
+ flag: "--no-compile-autoload-dotenv"
943
+ }].filter((c) => !c.enabled).map((c) => c.flag);
944
+ }
945
+ /**
946
+ * Remove temporary `.bun-build` files that `bun build --compile` leaves behind.
947
+ *
948
+ * @private
949
+ * @param cwd - The working directory to clean.
950
+ */
951
+ async function cleanBunBuildArtifacts(cwd) {
952
+ const [listError, entries] = await fs_exports.list(cwd);
953
+ if (listError) return;
954
+ const artifacts = entries.filter((name) => name.endsWith(".bun-build")).map((name) => join(cwd, name));
955
+ await Promise.allSettled(artifacts.map(fs_exports.remove));
956
+ }
957
+ /**
958
+ * Normalize the `compile` config field from `boolean | CompileOptions | undefined` to `CompileOptions`.
959
+ *
960
+ * - `true` → `{}` (compile with defaults)
961
+ * - `false` / `undefined` → `{}` (no explicit options, caller decides whether to compile)
962
+ * - object → pass through
963
+ *
964
+ * @param value - The raw compile config value.
965
+ * @returns A normalized CompileOptions object.
966
+ */
967
+ function normalizeCompileOptions(value) {
968
+ if (typeof value === "object") return value;
969
+ return {};
970
+ }
971
+ /**
972
+ * Fill defaults and resolve relative paths against `cwd`.
973
+ *
974
+ * This is a pure function — the incoming config is already validated by `@maltty/config`.
975
+ * It only fills missing optional fields with defaults and resolves paths to absolute.
976
+ *
977
+ * @param params - The raw config and working directory.
978
+ * @returns A fully resolved bundler configuration.
979
+ */
980
+ function resolveConfig(params) {
981
+ const { config, cwd } = params;
982
+ const entry = resolve(cwd, config.entry ?? "./src/index.ts");
983
+ const commands = resolve(cwd, config.commands ?? "./commands");
984
+ const buildOpts = config.build ?? {};
985
+ const compileOpts = normalizeCompileOptions(config.compile);
986
+ const buildOutDir = resolve(cwd, buildOpts.out ?? "./dist");
987
+ const compileOutDir = resolve(cwd, compileOpts.out ?? "./dist");
988
+ return {
989
+ build: {
990
+ clean: buildOpts.clean ?? true,
991
+ define: buildOpts.define ?? {},
992
+ external: buildOpts.external ?? [],
993
+ minify: buildOpts.minify ?? true,
994
+ sourcemap: buildOpts.sourcemap ?? true,
995
+ target: buildOpts.target ?? "node18"
996
+ },
997
+ buildOutDir,
998
+ commands,
999
+ compile: {
1000
+ autoloadDotenv: compileOpts.autoloadDotenv ?? false,
1001
+ name: compileOpts.name ?? params.binaryName,
1002
+ targets: compileOpts.targets ?? []
1003
+ },
1004
+ compileOutDir,
1005
+ cwd,
1006
+ entry,
1007
+ include: config.include ?? [],
1008
+ version: params.version
1009
+ };
1010
+ }
1011
+ /**
1012
+ * Create a bundler instance for a maltty CLI project.
1013
+ *
1014
+ * Reads the project manifest once, resolves config, and returns methods
1015
+ * that share the resolved state. Lifecycle hooks fire at phase boundaries.
1016
+ * Per-call overrides replace base hooks for that invocation.
1017
+ *
1018
+ * @param params - The config, working directory, and optional lifecycle hooks.
1019
+ * @returns A bundler with build, watch, and compile methods.
1020
+ */
1021
+ async function createBundler(params) {
1022
+ const [, manifest] = await readManifest(params.cwd);
1023
+ const { version, name: packageName } = manifest ?? {};
1024
+ const binaryName = M(params.config.compile).with({ name: z.string }, (c) => c.name).otherwise(() => resolveBinaryName(packageName));
1025
+ const resolved = resolveConfig({
1026
+ config: params.config,
1027
+ cwd: params.cwd,
1028
+ version,
1029
+ binaryName
1030
+ });
1031
+ const hasCompile = !isNil(params.config.compile);
1032
+ const baseLifecycle = {
1033
+ onFinish: params.onFinish,
1034
+ onStart: params.onStart,
1035
+ onStepFinish: params.onStepFinish,
1036
+ onStepStart: params.onStepStart
1037
+ };
1038
+ return {
1039
+ build: async (overrides = {}) => {
1040
+ const lifecycle = resolveLifecycle(baseLifecycle, overrides);
1041
+ await lifecycle.onStart({ phase: "build" });
1042
+ const result = await build$1({
1043
+ compile: hasCompile,
1044
+ resolved,
1045
+ verbose: overrides.verbose
1046
+ });
1047
+ await lifecycle.onFinish({ phase: "build" });
1048
+ return result;
1049
+ },
1050
+ watch: async (overrides = {}) => {
1051
+ const lifecycle = resolveLifecycle(baseLifecycle, overrides);
1052
+ await lifecycle.onStart({ phase: "watch" });
1053
+ const result = await watch({
1054
+ onSuccess: overrides.onSuccess,
1055
+ resolved,
1056
+ verbose: overrides.verbose
1057
+ });
1058
+ await lifecycle.onFinish({ phase: "watch" });
1059
+ return result;
1060
+ },
1061
+ compile: async (overrides = {}) => {
1062
+ const lifecycle = resolveLifecycle(baseLifecycle, overrides);
1063
+ await lifecycle.onStart({ phase: "compile" });
1064
+ const result = await compile({
1065
+ lifecycle,
1066
+ resolved,
1067
+ verbose: overrides.verbose
1068
+ });
1069
+ await lifecycle.onFinish({ phase: "compile" });
1070
+ return result;
1071
+ }
1072
+ };
1073
+ }
1074
+ /**
1075
+ * Derive the binary name from the package.json name, stripping scope.
1076
+ *
1077
+ * @private
1078
+ * @param packageName - The package name from manifest, or undefined.
1079
+ * @returns The binary name.
1080
+ */
1081
+ function resolveBinaryName(packageName) {
1082
+ if (!packageName) return "cli";
1083
+ return stripScope(packageName);
1084
+ }
1085
+ /**
1086
+ * Strip the npm scope prefix from a package name.
1087
+ *
1088
+ * @private
1089
+ * @param name - The package name (e.g. `@scope/my-cli`).
1090
+ * @returns The unscoped name (e.g. `my-cli`).
1091
+ */
1092
+ function stripScope(name) {
1093
+ const slashIndex = name.indexOf("/");
1094
+ if (name.startsWith("@") && slashIndex > 0) return name.slice(slashIndex + 1);
1095
+ return name;
1096
+ }
1097
+ /**
1098
+ * Merge base lifecycle hooks with per-call overrides.
1099
+ *
1100
+ * Per-call hooks replace base hooks (no chaining). Missing hooks
1101
+ * are filled with no-ops so callers don't need null checks.
1102
+ *
1103
+ * @private
1104
+ * @param base - The base lifecycle hooks from the factory.
1105
+ * @param overrides - Per-call hook overrides.
1106
+ * @returns A lifecycle with all hooks guaranteed to be defined.
1107
+ */
1108
+ function resolveLifecycle(base, overrides = {}) {
1109
+ return {
1110
+ onFinish: overrides.onFinish ?? base.onFinish ?? noop,
1111
+ onStart: overrides.onStart ?? base.onStart ?? noop,
1112
+ onStepFinish: overrides.onStepFinish ?? base.onStepFinish ?? noop,
1113
+ onStepStart: overrides.onStepStart ?? base.onStepStart ?? noop
1114
+ };
1115
+ }
1116
+ //#endregion
1117
+ export { normalizeCompileOptions as i, DEFAULT_ENTRY as n, createBundler as r, DEFAULT_COMMANDS as t };
1118
+
1119
+ //# sourceMappingURL=dist-DW_gRP7_.js.map