@akanjs/devkit 3.2.0-rc.0 → 3.2.0-rc.10

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 (110) hide show
  1. package/CHANGELOG.md +238 -11
  2. package/agentAssets.ts +1 -1
  3. package/akanApp/akanApp.host.test.ts +127 -0
  4. package/akanApp/akanApp.host.ts +50 -57
  5. package/akanApp/devHostPolicy.test.ts +11 -11
  6. package/akanApp/devHostPolicy.ts +2 -4
  7. package/akanContext.ts +2 -5
  8. package/akanMcpContract.ts +21 -10
  9. package/akanMcpToolError.ts +16 -0
  10. package/apiReport/apiDeclarationPrinter.ts +120 -0
  11. package/apiReport/apiReport.test.ts +497 -0
  12. package/apiReport/apiReport.ts +232 -0
  13. package/apiReport/apiReportDiff.ts +126 -0
  14. package/apiReport/apiReportPolicy.ts +65 -0
  15. package/apiReport/apiTargets.ts +311 -0
  16. package/apiReport/changesetBump.ts +19 -0
  17. package/applicationBuildReporter.test.ts +21 -0
  18. package/applicationBuildReporter.ts +8 -0
  19. package/applicationBuildRunner.test.ts +67 -0
  20. package/applicationBuildRunner.ts +6 -1
  21. package/artifact/implicitRootLayout.test.ts +4 -0
  22. package/artifact/implicitRootLayout.ts +13 -1
  23. package/artifact/storeBootstrap.ts +3 -2
  24. package/bareErrorScanner.ts +126 -0
  25. package/biome.base.json +2 -1
  26. package/codeAgent/skills/akan-debug/SKILL.md +7 -3
  27. package/codeAgent/skills/akan-err/SKILL.md +7 -0
  28. package/codeAgent/skills/akan-mcp/SKILL.md +7 -1
  29. package/codeAgent/skills/akan-scaffold/SKILL.md +3 -2
  30. package/codeAgent/skills/akan-transport/SKILL.md +2 -0
  31. package/codeAgent/skills/akan-validate/SKILL.md +1 -0
  32. package/codeAgent/tools/McpClient.ts +28 -6
  33. package/codeAgent/tools/McpToolPack.ts +1 -1
  34. package/codeAgent/tools/akanCodePlugins.test.ts +13 -1
  35. package/commandDecorators/argMeta.ts +4 -0
  36. package/commandDecorators/cliError.ts +72 -0
  37. package/commandDecorators/command.ts +186 -96
  38. package/commandDecorators/commandDecorators.test.ts +240 -17
  39. package/commandDecorators/exitCodeError.ts +6 -7
  40. package/commandDecorators/index.ts +1 -0
  41. package/commandDecorators/targetMeta.ts +3 -0
  42. package/conventionSet.test.ts +5 -2
  43. package/createTunnel.test.ts +12 -2
  44. package/csrE2e/csrE2eHarness.fixture.ts +1 -1
  45. package/errorCodeReference.test.ts +282 -0
  46. package/errorCodeReference.ts +71 -0
  47. package/errorCodeScan.ts +551 -0
  48. package/frontendBuild/allRoutesBuilder.test.ts +16 -4
  49. package/frontendBuild/allRoutesBuilder.ts +24 -6
  50. package/frontendBuild/buildRouteClient.test.ts +43 -0
  51. package/frontendBuild/bundleDefine.ts +2 -0
  52. package/frontendBuild/clientBuildTypes.ts +5 -2
  53. package/frontendBuild/clientEntryDiscovery.test.ts +80 -0
  54. package/frontendBuild/clientEntryDiscovery.ts +53 -9
  55. package/frontendBuild/csrArtifactBuilder.test.ts +48 -0
  56. package/frontendBuild/csrArtifactBuilder.ts +9 -3
  57. package/frontendBuild/csrDevBundler.ts +12 -2
  58. package/frontendBuild/csrDevModuleCompiler.test.ts +97 -0
  59. package/frontendBuild/csrDevModuleCompiler.ts +37 -8
  60. package/frontendBuild/csrDevPatcher.test.ts +89 -2
  61. package/frontendBuild/csrDevPatcher.ts +47 -7
  62. package/frontendBuild/csrDevTypes.ts +16 -0
  63. package/frontendBuild/cssCompiler.test.ts +63 -1
  64. package/frontendBuild/devBarrelImports.ts +132 -0
  65. package/frontendBuild/devRegistryBundler.ts +19 -4
  66. package/frontendBuild/mcpAppViewBuilder.test.ts +67 -0
  67. package/frontendBuild/nativeCore.test.ts +148 -0
  68. package/frontendBuild/precompressArtifacts.ts +1 -1
  69. package/frontendBuild/ssrBaseArtifactBuilder.test.ts +26 -0
  70. package/frontendBuild/ssrBaseArtifactBuilder.ts +23 -19
  71. package/frontendBuild/ssrDevBundler.ts +13 -2
  72. package/frontendBuild/vendorImportScanner.test.ts +24 -3
  73. package/frontendBuild/vendorImportScanner.ts +25 -10
  74. package/incrementalBuilder/incrementalBuilder.host.test.ts +30 -1
  75. package/incrementalBuilder/incrementalBuilder.host.ts +10 -22
  76. package/lint/__fixtures__/no-import-common-err/bad.tsx +13 -0
  77. package/lint/__fixtures__/no-import-common-err/good.tsx +12 -0
  78. package/lint/no-import-common-err.grit +31 -0
  79. package/lint/no-init-fetch-in-client.grit +1 -1
  80. package/lint/no-web-only-api-outside-webkit.grit +3 -3
  81. package/maintainabilityBaseline.test.ts +49 -0
  82. package/maintainabilityBaseline.ts +73 -8
  83. package/maintainabilityContract.test.ts +302 -1
  84. package/maintainabilityContract.ts +87 -22
  85. package/maintainabilityExceptionScanner.test.ts +14 -0
  86. package/maintainabilityExceptionScanner.ts +5 -1
  87. package/maintainabilityLimits.ts +12 -0
  88. package/maintainabilityReport.ts +20 -3
  89. package/moduleFieldNames.test.ts +56 -0
  90. package/moduleFieldNames.ts +25 -0
  91. package/package.json +3 -3
  92. package/preview/PreviewChrome.ts +3 -3
  93. package/preview/PreviewView.ts +48 -9
  94. package/preview/previewView.test.ts +154 -3
  95. package/routeJsBudget.test.ts +47 -11
  96. package/routeJsBudget.ts +88 -25
  97. package/transforms/barrelAnalyzer.ts +46 -1
  98. package/transforms/barrelImportsPlugin.ts +61 -45
  99. package/transforms/transforms.test.ts +67 -1
  100. package/typecheck/projectTypecheck.ts +5 -1
  101. package/typecheck/typecheck.proc.ts +3 -2
  102. package/typecheck/typecheckFailure.ts +6 -1
  103. package/workflow/artifacts.ts +25 -7
  104. package/workflow/fieldType.ts +92 -0
  105. package/workflow/index.ts +2 -0
  106. package/workflow/plainName.ts +28 -0
  107. package/workflow/plan.ts +67 -22
  108. package/workflow/source.ts +0 -5
  109. package/workspaceGraph.test.ts +200 -3
  110. package/workspaceGraph.ts +46 -8
@@ -7,9 +7,17 @@ import type {
7
7
  } from "@inquirer/prompts";
8
8
  import { Logger } from "akanjs/common";
9
9
  import chalk from "chalk";
10
- import { type Command, program } from "commander";
10
+ import { type Command, type CommanderError, program } from "commander";
11
11
  import { AppSelectionMemory } from "../appSelectionMemory";
12
- import { AppExecutor, Executor, LibExecutor, ModuleExecutor, PkgExecutor, WorkspaceExecutor } from "../executors";
12
+ import {
13
+ AppExecutor,
14
+ Executor,
15
+ LibExecutor,
16
+ ModuleExecutor,
17
+ PkgExecutor,
18
+ type SysExecutor,
19
+ WorkspaceExecutor,
20
+ } from "../executors";
13
21
  // Never the root barrel: it drags ink, ssh2 and the cloud stack into every command process.
14
22
  import { FileSys, getDirname } from "../fileSys";
15
23
  import type { PackageJson } from "../types";
@@ -22,41 +30,68 @@ import {
22
30
  type InternalArgMeta,
23
31
  } from "./argMeta";
24
32
  import { camelToKebabCase } from "./camelToKebabCase";
33
+ import { CliError, type CliErrorCode } from "./cliError";
25
34
  import { CommandContainer } from "./dependencyBuilder";
26
- import { ExitCodeError } from "./exitCodeError";
27
35
  import { formatCommandHelp, formatHelp } from "./helpFormatter";
28
- import { type CommandCls, getTargetCommandNames, getTargetMetas, type TargetFailure } from "./targetMeta";
36
+ import {
37
+ type CommandCls,
38
+ getTargetCommandNames,
39
+ getTargetMetas,
40
+ type TargetFailure,
41
+ type TargetOption,
42
+ } from "./targetMeta";
29
43
  import { WorkspaceVersionCheck } from "./workspaceVersionCheck";
30
44
 
31
45
  const loggedCliErrorObjects = new WeakSet<object>();
46
+ const answeredCliErrors = new WeakSet<object>();
32
47
  const loggedCliErrorMessages = new Set<string>();
33
48
 
34
49
  export const markCliErrorPrinted = (error: unknown) => {
35
- if (typeof error === "object" && error !== null) loggedCliErrorObjects.add(error);
36
- };
37
-
38
- const formatCliError = (error: unknown): string => {
39
- if (error instanceof Error) return error.message || error.name;
40
- if (typeof error === "string") return error.trim() || "Unknown error";
41
- if (error === null || error === undefined) return "Unknown error";
42
- try {
43
- const json = JSON.stringify(error);
44
- if (json) return json;
45
- } catch {
46
- return String(error);
47
- }
48
- return String(error) || "Unknown error";
50
+ if (typeof error !== "object" || error === null) return;
51
+ loggedCliErrorObjects.add(error);
52
+ answeredCliErrors.add(error);
49
53
  };
50
54
 
51
- const printCliError = (error: unknown) => {
55
+ const printCliError = (error: unknown, stream: "log" | "error" = "log") => {
52
56
  if (typeof error === "object" && error !== null) {
53
57
  if (loggedCliErrorObjects.has(error)) return;
54
58
  loggedCliErrorObjects.add(error);
55
59
  }
56
- const message = formatCliError(error);
60
+ const message = CliError.messageOf(error);
57
61
  if (loggedCliErrorMessages.has(message)) return;
58
62
  loggedCliErrorMessages.add(message);
59
- Logger.rawLog(`\n${chalk.red(message)}`);
63
+ Logger.rawLog(`\n${chalk.red(message)}`, undefined, stream);
64
+ };
65
+
66
+ //? The default counts: `akan mcp-call` answers JSON only.
67
+ const asksJson = (argMetas: (ArgMeta | InternalArgMeta)[], opt: Record<string, unknown>) =>
68
+ argMetas.some((argMeta) => {
69
+ if (argMeta.type !== "Option") return false;
70
+ const value = opt[argMeta.name] ?? argMeta.argsOption.default;
71
+ if (argMeta.name === "json") return value === true || value === "true";
72
+ return argMeta.name === "format" && value === "json";
73
+ });
74
+
75
+ //* Waits for stdout to take the line: the CLI exits right after, and an exit drops what a pipe still buffers.
76
+ const writeJsonFailure = async (error: unknown) => {
77
+ if (typeof error === "object" && error !== null && answeredCliErrors.has(error)) return;
78
+ const line = `${JSON.stringify(CliError.failureOf(error))}\n`;
79
+ await new Promise<void>((resolve) => process.stdout.write(line, () => resolve()));
80
+ };
81
+
82
+ const failureReporterOf =
83
+ (json: boolean, onFailure: TargetOption["onFailure"], failure: TargetFailure) => async (error: unknown) => {
84
+ if (onFailure) await onFailure(error, failure);
85
+ else if (json) await writeJsonFailure(error);
86
+ };
87
+
88
+ //? commander refuses a flag, an argument or a command before any action runs, having printed why to stderr.
89
+ //? Thrown, not exited: the failure path awaits onFailure, and an exit drops what a pipe still buffers.
90
+ const usageRefusalOf = (error: CommanderError) => {
91
+ if (error.exitCode === 0) process.exit(0);
92
+ const refused = new CliError("usage", error.message.replace(/^error: /, ""));
93
+ loggedCliErrorObjects.add(refused);
94
+ return refused;
60
95
  };
61
96
 
62
97
  const handleOption = (programCommand: Command, argMeta: ArgMeta) => {
@@ -132,22 +167,34 @@ const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
132
167
  if (!enumChoices || typeof enumChoices === "function") return;
133
168
  const choices = normalizeEnumChoices(enumChoices);
134
169
  if (choices.some((choice) => String(choice.value) === String(value))) return;
135
- throw new Error(
170
+ throw new CliError(
171
+ "usage",
136
172
  `${argLabelOf(argMeta)} argument '${String(value)}' is invalid. Allowed choices are ${choices.map((choice) => choice.name).join(", ")}.`,
137
173
  );
138
174
  };
139
175
 
140
176
  // On a piped stdin a prompt takes the pipe as its answer and echoes it: a piped secret printed as the secret's name.
141
- export const assertPromptable = (
142
- problem: string,
143
- { choices = [], remedy = "pass it on the command line" }: { choices?: readonly unknown[]; remedy?: string } = {},
144
- ) => {
177
+ export const assertPromptable = (problem: string, refusal: Refusal = {}) => {
145
178
  if (process.stdin.isTTY) return;
146
- throw new Error(
147
- `${problem}: stdin is not a terminal to prompt on, so ${remedy}${choices.length ? ` (choices: ${choices.join(", ")})` : ""}.`,
148
- );
179
+ throw refusalOf(problem, "stdin is not a terminal to prompt on", refusal);
149
180
  };
150
181
 
182
+ interface Refusal {
183
+ choices?: readonly unknown[];
184
+ remedy?: string;
185
+ code?: CliErrorCode;
186
+ }
187
+
188
+ const refusalOf = (
189
+ problem: string,
190
+ reason: string,
191
+ { choices = [], remedy = "pass it on the command line", code = "usage" }: Refusal,
192
+ ) =>
193
+ new CliError(
194
+ code,
195
+ `${problem}: ${reason}, so ${remedy}${choices.length ? ` (choices: ${choices.join(", ")})` : ""}.`,
196
+ );
197
+
151
198
  const inputMessageOf = ({ name, argsOption: { desc, example, ask } }: ArgMeta) =>
152
199
  ask ? `${ask}: ` : desc ? `${desc}: ` : `Enter the ${name} value${example ? ` (example: ${example})` : ""}: `;
153
200
 
@@ -220,7 +267,8 @@ const assertCurrentDirectoryIsWorkspaceRoot = async () => {
220
267
  ]);
221
268
  if (hasPackageJson && hasTsConfig && hasMemberDirs.some(Boolean)) return;
222
269
 
223
- throw new Error(
270
+ throw new CliError(
271
+ "usage",
224
272
  [
225
273
  "Akan CLI commands must be run from the workspace root.",
226
274
  `Current directory: ${cwd}`,
@@ -270,7 +318,8 @@ export const getAppsArgumentValue = async (
270
318
  if (requested.length) {
271
319
  const unknown = requested.filter((name) => !appNames.includes(name));
272
320
  if (unknown.length)
273
- throw new Error(
321
+ throw new CliError(
322
+ "unknownTarget",
274
323
  `Unknown app${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Available: ${appNames.join(", ")}, all`,
275
324
  );
276
325
  return [...new Set(requested)].map((name) => AppExecutor.from(workspace, name));
@@ -293,11 +342,29 @@ export const getInternalArgumentValue = async (
293
342
  value: string | undefined,
294
343
  workspace: WorkspaceExecutor,
295
344
  rerun?: RerunOf,
345
+ { destructive = false }: { destructive?: boolean } = {},
296
346
  ) => {
297
347
  if (argMeta.type === "Workspace") return workspace;
298
348
  const sysType = argMeta.type.toLowerCase();
299
349
  const [appNames, libNames, pkgNames] = await workspace.getExecs();
300
350
  const problemOf = (kind: string) => (value ? `Unknown ${kind} '${value}'` : `Missing ${kind}`);
351
+ //? Refused in a terminal too: a select deletes on a stray Enter, its cursor resting on the first choice.
352
+ const assertAskable = (kind: string, choices: readonly unknown[], placeholder: string) => {
353
+ const refusal: Refusal = { choices, code: value ? "unknownTarget" : "usage", ...rerunRemedyOf(rerun, placeholder) };
354
+ if (destructive) throw refusalOf(problemOf(kind), "this command deletes files and never picks its target", refusal);
355
+ assertPromptable(problemOf(kind), refusal);
356
+ };
357
+ const takeOnly = <Target>(kind: string, names: readonly string[], take: (name: string) => Target) => {
358
+ const [only] = names;
359
+ if (value || destructive || process.stdin.isTTY || names.length !== 1 || !only) return null;
360
+ const how = rerun ? `: ${rerun(only)}` : "";
361
+ Logger.rawLog(
362
+ `No ${kind} named, so this runs on '${only}', the only one in this workspace${how}`,
363
+ undefined,
364
+ "error",
365
+ );
366
+ return take(only);
367
+ };
301
368
  if (sysType === "sys" || sysType === "exec") {
302
369
  const pkgChoices = sysType === "exec" ? pkgNames : [];
303
370
  const execOf = (name: string) => {
@@ -308,70 +375,73 @@ export const getInternalArgumentValue = async (
308
375
  };
309
376
  const found = value ? execOf(value) : null;
310
377
  if (found) return found;
311
- if (!value && argMeta.option?.onlyApp && appNames.length === 1 && appNames[0])
378
+ if (!value && !destructive && argMeta.option?.onlyApp && appNames.length === 1 && appNames[0])
312
379
  return AppExecutor.from(workspace, appNames[0]);
313
380
  const [kind, placeholder] =
314
381
  sysType === "exec" ? ["app, lib or pkg", "<app-lib-or-pkg>"] : ["app or lib", "<app-or-lib>"];
315
382
  const choices = [...appNames, ...libNames, ...pkgChoices];
316
- const only = choices.length === 1 ? execOf(choices[0] ?? "") : null;
317
- //? Not for Lib, Pkg or Module, which name what a command acts on: `akan remove-library` would delete the only one.
318
- if (!value && only && !process.stdin.isTTY) {
319
- const how = rerun ? `: ${rerun(only.name)}` : "";
320
- Logger.rawLog(
321
- `No ${kind} named, so this runs on '${only.name}', the only one in this workspace${how}`,
322
- undefined,
323
- "error",
324
- );
325
- return only;
326
- }
327
- assertPromptable(problemOf(kind), { choices, ...rerunRemedyOf(rerun, placeholder) });
383
+ const only = takeOnly(kind, choices, execOf);
384
+ if (only) return only;
385
+ assertAskable(kind, choices, placeholder);
328
386
  const name = await select<string>({
329
387
  message: sysType === "exec" ? `Select the App or Lib or Pkg name` : `Select the App or Lib name`,
330
388
  choices,
331
389
  });
332
390
  const picked = execOf(name);
333
- if (!picked) throw new Error(`Invalid system name: ${name}`);
391
+ if (!picked) throw new CliError("unknownTarget", `Invalid system name: ${name}`);
334
392
  return picked;
335
393
  } else if (sysType === "app") {
336
394
  if (value && appNames.includes(value)) return AppExecutor.from(workspace, value);
337
- if (!value && appNames.length === 1 && appNames[0]) return AppExecutor.from(workspace, appNames[0]);
338
- assertPromptable(problemOf("app"), { choices: appNames, ...rerunRemedyOf(rerun, "<app>") });
395
+ if (!value && !destructive && appNames.length === 1 && appNames[0]) return AppExecutor.from(workspace, appNames[0]);
396
+ assertAskable("app", appNames, "<app>");
339
397
  const appName = await select<string>({ message: `Select the ${sysType} name`, choices: appNames });
340
398
  return AppExecutor.from(workspace, appName);
341
399
  } else if (sysType === "lib") {
342
400
  if (value && libNames.includes(value)) return LibExecutor.from(workspace, value);
343
- assertPromptable(problemOf("lib"), { choices: libNames, ...rerunRemedyOf(rerun, "<lib>") });
401
+ const only = takeOnly("lib", libNames, (name) => LibExecutor.from(workspace, name));
402
+ if (only) return only;
403
+ assertAskable("lib", libNames, "<lib>");
344
404
  const libName = await select<string>({ message: `Select the ${sysType} name`, choices: libNames });
345
405
  return LibExecutor.from(workspace, libName);
346
406
  } else if (sysType === "pkg") {
347
407
  const pkgs = await workspace.getPkgs();
348
408
  if (value && pkgs.includes(value)) return PkgExecutor.from(workspace, value);
349
- assertPromptable(problemOf("pkg"), { choices: pkgs, ...rerunRemedyOf(rerun, "<pkg>") });
409
+ const only = takeOnly("pkg", pkgs, (name) => PkgExecutor.from(workspace, name));
410
+ if (only) return only;
411
+ assertAskable("pkg", pkgs, "<pkg>");
350
412
  const pkgName = await select<string>({ message: `Select the ${sysType} name`, choices: pkgs });
351
413
  return PkgExecutor.from(workspace, pkgName);
352
414
  } else if (sysType === "module") {
353
- if (value) {
354
- const [sysName, moduleName] = value.split(":");
355
- if (!sysName || !moduleName) throw new Error(`Invalid module name: ${value}`);
356
- if (appNames.includes(sysName)) {
357
- const app = AppExecutor.from(workspace, sysName);
358
- const modules = await app.getModules();
359
- if (modules.includes(moduleName)) return ModuleExecutor.from(app, moduleName);
360
- else throw new Error(`Invalid module name: ${moduleName}`);
361
- } else if (libNames.includes(sysName)) {
362
- const lib = LibExecutor.from(workspace, sysName);
363
- const modules = await lib.getModules();
364
- if (modules.includes(moduleName)) return ModuleExecutor.from(lib, moduleName);
365
- } else throw new Error(`Invalid system name: ${sysName}`);
366
- }
367
415
  const systems = [
368
416
  ...appNames.map((name) => AppExecutor.from(workspace, name)),
369
417
  ...libNames.map((name) => LibExecutor.from(workspace, name)),
370
418
  ];
371
- const choices = process.stdin.isTTY
372
- ? []
373
- : (await Promise.all(systems.map(async (sys) => (await sys.getModules()).map((m) => `${sys.name}:${m}`)))).flat();
374
- assertPromptable(problemOf("module"), { choices, ...rerunRemedyOf(rerun, "<sys:module>") });
419
+ const modulesOf = async (sys: SysExecutor) => [
420
+ ...(await sys.getModules()),
421
+ ...(argMeta.option?.services ? await sys.getServiceModules() : []),
422
+ ];
423
+ const listModules = async () =>
424
+ (await Promise.all(systems.map(async (sys) => (await modulesOf(sys)).map((m) => `${sys.name}:${m}`)))).flat();
425
+ if (value) {
426
+ const [sysName, moduleName] = value.split(":");
427
+ if (!sysName || !moduleName) throw new CliError("usage", `Invalid module name: ${value}`);
428
+ const sys = systems.find((each) => each.name === sysName);
429
+ if (!sys) throw new CliError("unknownTarget", `Invalid system name: ${sysName}`);
430
+ if ((await modulesOf(sys)).includes(moduleName)) return ModuleExecutor.from(sys, moduleName);
431
+ throw refusalOf(problemOf("module"), `${sysName} has no such module`, {
432
+ code: "unknownTarget",
433
+ choices: await listModules(),
434
+ ...rerunRemedyOf(rerun, "<sys:module>"),
435
+ });
436
+ }
437
+ const choices = process.stdin.isTTY && !destructive ? [] : await listModules();
438
+ const only = takeOnly("module", choices, (label) => {
439
+ const [sysName, moduleName = ""] = label.split(":");
440
+ const sys = systems.find((each) => each.name === sysName);
441
+ return sys ? ModuleExecutor.from(sys, moduleName) : null;
442
+ });
443
+ if (only) return only;
444
+ assertAskable("module", choices, "<sys:module>");
375
445
  const { type, name } = await select<{ type: "app" | "lib"; name: string }>({
376
446
  message: `select the App or Lib name`,
377
447
  choices: [
@@ -380,7 +450,7 @@ export const getInternalArgumentValue = async (
380
450
  ],
381
451
  });
382
452
  const executor = type === "app" ? AppExecutor.from(workspace, name) : LibExecutor.from(workspace, name);
383
- const modules = await executor.getModules();
453
+ const modules = await modulesOf(executor);
384
454
  const moduleName = await select<string>({
385
455
  message: `Select the module name`,
386
456
  choices: modules.map((name) => ({ name: `${executor.name}:${name}`, value: name })),
@@ -389,13 +459,33 @@ export const getInternalArgumentValue = async (
389
459
  } else throw new Error(`Invalid system type: ${argMeta.type}`);
390
460
  };
391
461
 
462
+ const cliFailureOf = () => {
463
+ let failing = false;
464
+ const failure = {
465
+ exitCode: 1,
466
+ usageExitCode: undefined as number | undefined,
467
+ stream: "log" as "log" | "error",
468
+ report: async (_error: unknown) => {},
469
+ exitCodeOf: (error: unknown) => {
470
+ const { usageExitCode } = failure;
471
+ if (usageExitCode !== undefined && error instanceof CliError && CliError.usageCodes.includes(error.code))
472
+ return usageExitCode;
473
+ return CliError.exitCodeOf(error) ?? failure.exitCode;
474
+ },
475
+ fail: async (error: unknown) => {
476
+ if (failing) return;
477
+ failing = true;
478
+ printCliError(error, failure.stream);
479
+ await failure.report(error).catch((reportError: unknown) => printCliError(reportError, "error"));
480
+ process.exit(failure.exitCodeOf(error));
481
+ },
482
+ };
483
+ return failure;
484
+ };
485
+
392
486
  export const runCommands = async (...commands: CommandCls[]) => {
393
- let failureExitCode = 1;
394
- let reportFailure = async (_error: unknown) => {};
395
- process.on("unhandledRejection", (error) => {
396
- printCliError(error);
397
- process.exit(ExitCodeError.codeOf(error) ?? failureExitCode);
398
- });
487
+ const failure = cliFailureOf();
488
+ process.on("unhandledRejection", (error) => void failure.fail(error));
399
489
  const __dirname = getDirname(import.meta.url);
400
490
  const packageJsonCandidates = [`${path.dirname(Bun.main)}/package.json`, `${__dirname}/../package.json`];
401
491
  let cliPackageJson: PackageJson | null = null;
@@ -429,21 +519,16 @@ export const runCommands = async (...commands: CommandCls[]) => {
429
519
  let programCommand = program.command(commandName, {
430
520
  hidden: targetMeta.targetOption.devOnly,
431
521
  });
432
- const targetFailureExitCode = targetMeta.targetOption.failureExitCode;
433
- const usageExitCode = targetMeta.targetOption.usageExitCode ?? targetFailureExitCode;
522
+ const { failureExitCode: targetFailureExitCode, usageExitCode: targetUsageExitCode } = targetMeta.targetOption;
434
523
  const { onFailure } = targetMeta.targetOption;
435
524
  const [allArgMetas] = getArgMetas(command, targetMeta.key);
436
- //? commander exits 1 on a flag or argument it refuses, before any action runs, having printed why.
437
- //? Thrown, not exited: the failure path below awaits onFailure, and an exit drops what a pipe still buffers.
438
- if (usageExitCode !== undefined || onFailure)
439
- programCommand.exitOverride((error) => {
440
- if (error.exitCode === 0) process.exit(0);
441
- const refused = new ExitCodeError(error.message.replace(/^error: /, ""), usageExitCode ?? error.exitCode);
442
- markCliErrorPrinted(refused);
443
- const given = givenOptionsOf(allArgMetas, programCommand.opts());
444
- reportFailure = async (failure) => await onFailure?.(failure, { usage: true, given });
445
- throw refused;
446
- });
525
+ programCommand.exitOverride((error) => {
526
+ const refused = usageRefusalOf(error);
527
+ failure.usageExitCode = targetUsageExitCode;
528
+ const given = givenOptionsOf(allArgMetas, programCommand.opts());
529
+ failure.report = failureReporterOf(asksJson(allArgMetas, given), onFailure, { usage: true, given });
530
+ throw refused;
531
+ });
447
532
  for (const argMeta of allArgMetas) {
448
533
  if (argMeta.type === "Option") programCommand = handleOption(programCommand, argMeta);
449
534
  else if (argMeta.type === "Argument") programCommand = handleArgument(programCommand, argMeta);
@@ -472,12 +557,17 @@ export const runCommands = async (...commands: CommandCls[]) => {
472
557
  };
473
558
 
474
559
  programCommand.action(async (...args: unknown[]) => {
475
- failureExitCode = targetFailureExitCode ?? 1;
560
+ failure.exitCode = targetFailureExitCode ?? 1;
561
+ failure.usageExitCode = targetUsageExitCode;
476
562
  if (!targetMeta.targetOption.stdio) Logger.rawLog();
477
563
  const cmdArgs = args.slice(0, args.length - 2);
478
564
  const opt = args[args.length - 2] as Record<string, unknown>;
479
- reportFailure = async (failure) =>
480
- await onFailure?.(failure, { usage: false, given: givenOptionsOf(allArgMetas, opt) });
565
+ const json = asksJson(allArgMetas, opt);
566
+ failure.stream = json ? "error" : "log";
567
+ failure.report = failureReporterOf(json, onFailure, {
568
+ usage: false,
569
+ given: givenOptionsOf(allArgMetas, opt),
570
+ });
481
571
  const commandArgs = [] as unknown[];
482
572
  if (targetMeta.targetOption.runsOnWorkspaceRoot) {
483
573
  await assertCurrentDirectoryIsWorkspaceRoot();
@@ -503,6 +593,7 @@ export const runCommands = async (...commands: CommandCls[]) => {
503
593
  cmdArgs[argMeta.idx] as string,
504
594
  workspace,
505
595
  rerun,
596
+ { destructive: targetMeta.targetOption.destructive },
506
597
  );
507
598
  if (commandArgs[argMeta.idx] instanceof AppExecutor)
508
599
  process.env.AKAN_PUBLIC_APP_NAME = (commandArgs[argMeta.idx] as AppExecutor).name;
@@ -520,17 +611,16 @@ export const runCommands = async (...commands: CommandCls[]) => {
520
611
  await targetMeta.handler.call(cmd, ...commandArgs);
521
612
  if (!targetMeta.targetOption.stdio) Logger.rawLog();
522
613
  } catch (e) {
523
- printCliError(e);
614
+ printCliError(e, failure.stream);
524
615
  throw e;
525
616
  }
526
617
  });
527
618
  }
528
619
  }
529
620
  }
530
- // Handled here, not by the handler above: a rejection reaching the entry's top-level await prints Bun's own trace.
531
- await program.parseAsync(process.argv).catch(async (error: unknown) => {
532
- printCliError(error);
533
- await reportFailure(error).catch(printCliError);
534
- process.exit(ExitCodeError.codeOf(error) ?? failureExitCode);
621
+ program.exitOverride((error) => {
622
+ throw usageRefusalOf(error);
535
623
  });
624
+ // Handled here, not by the handler above: a rejection reaching the entry's top-level await prints Bun's own trace.
625
+ await program.parseAsync(process.argv).catch(failure.fail);
536
626
  };