@akanjs/devkit 3.2.0-rc.1 → 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.
- package/CHANGELOG.md +233 -11
- package/agentAssets.ts +1 -1
- package/akanApp/akanApp.host.test.ts +127 -0
- package/akanApp/akanApp.host.ts +50 -57
- package/akanApp/devHostPolicy.test.ts +11 -11
- package/akanApp/devHostPolicy.ts +2 -4
- package/akanContext.ts +2 -5
- package/akanMcpContract.ts +21 -10
- package/akanMcpToolError.ts +16 -0
- package/apiReport/apiDeclarationPrinter.ts +120 -0
- package/apiReport/apiReport.test.ts +497 -0
- package/apiReport/apiReport.ts +232 -0
- package/apiReport/apiReportDiff.ts +126 -0
- package/apiReport/apiReportPolicy.ts +65 -0
- package/apiReport/apiTargets.ts +311 -0
- package/apiReport/changesetBump.ts +19 -0
- package/applicationBuildReporter.test.ts +21 -0
- package/applicationBuildReporter.ts +8 -0
- package/applicationBuildRunner.test.ts +67 -0
- package/applicationBuildRunner.ts +6 -1
- package/artifact/implicitRootLayout.test.ts +4 -0
- package/artifact/implicitRootLayout.ts +13 -1
- package/artifact/storeBootstrap.ts +3 -2
- package/bareErrorScanner.ts +126 -0
- package/biome.base.json +2 -1
- package/codeAgent/skills/akan-debug/SKILL.md +7 -3
- package/codeAgent/skills/akan-err/SKILL.md +7 -0
- package/codeAgent/skills/akan-mcp/SKILL.md +7 -1
- package/codeAgent/skills/akan-scaffold/SKILL.md +3 -2
- package/codeAgent/skills/akan-transport/SKILL.md +2 -0
- package/codeAgent/skills/akan-validate/SKILL.md +1 -0
- package/codeAgent/tools/McpClient.ts +28 -6
- package/codeAgent/tools/McpToolPack.ts +1 -1
- package/codeAgent/tools/akanCodePlugins.test.ts +13 -1
- package/commandDecorators/argMeta.ts +4 -0
- package/commandDecorators/cliError.ts +72 -0
- package/commandDecorators/command.ts +186 -96
- package/commandDecorators/commandDecorators.test.ts +240 -17
- package/commandDecorators/exitCodeError.ts +6 -7
- package/commandDecorators/index.ts +1 -0
- package/commandDecorators/targetMeta.ts +3 -0
- package/conventionSet.test.ts +5 -2
- package/csrE2e/csrE2eHarness.fixture.ts +1 -1
- package/errorCodeReference.test.ts +282 -0
- package/errorCodeReference.ts +71 -0
- package/errorCodeScan.ts +551 -0
- package/frontendBuild/allRoutesBuilder.test.ts +16 -4
- package/frontendBuild/allRoutesBuilder.ts +24 -6
- package/frontendBuild/buildRouteClient.test.ts +43 -0
- package/frontendBuild/bundleDefine.ts +2 -0
- package/frontendBuild/clientBuildTypes.ts +5 -2
- package/frontendBuild/clientEntryDiscovery.test.ts +80 -0
- package/frontendBuild/clientEntryDiscovery.ts +53 -9
- package/frontendBuild/csrArtifactBuilder.test.ts +48 -0
- package/frontendBuild/csrArtifactBuilder.ts +9 -3
- package/frontendBuild/csrDevBundler.ts +12 -2
- package/frontendBuild/csrDevModuleCompiler.test.ts +97 -0
- package/frontendBuild/csrDevModuleCompiler.ts +37 -8
- package/frontendBuild/csrDevPatcher.test.ts +89 -2
- package/frontendBuild/csrDevPatcher.ts +47 -7
- package/frontendBuild/csrDevTypes.ts +16 -0
- package/frontendBuild/cssCompiler.test.ts +63 -1
- package/frontendBuild/devBarrelImports.ts +132 -0
- package/frontendBuild/devRegistryBundler.ts +19 -4
- package/frontendBuild/mcpAppViewBuilder.test.ts +67 -0
- package/frontendBuild/nativeCore.test.ts +148 -0
- package/frontendBuild/precompressArtifacts.ts +1 -1
- package/frontendBuild/ssrBaseArtifactBuilder.test.ts +26 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +23 -19
- package/frontendBuild/ssrDevBundler.ts +13 -2
- package/frontendBuild/vendorImportScanner.test.ts +24 -3
- package/frontendBuild/vendorImportScanner.ts +25 -10
- package/incrementalBuilder/incrementalBuilder.host.test.ts +30 -1
- package/incrementalBuilder/incrementalBuilder.host.ts +10 -22
- package/lint/__fixtures__/no-import-common-err/bad.tsx +13 -0
- package/lint/__fixtures__/no-import-common-err/good.tsx +12 -0
- package/lint/no-import-common-err.grit +31 -0
- package/lint/no-init-fetch-in-client.grit +1 -1
- package/lint/no-web-only-api-outside-webkit.grit +3 -3
- package/maintainabilityBaseline.test.ts +49 -0
- package/maintainabilityBaseline.ts +73 -8
- package/maintainabilityContract.test.ts +302 -1
- package/maintainabilityContract.ts +87 -22
- package/maintainabilityExceptionScanner.test.ts +14 -0
- package/maintainabilityExceptionScanner.ts +5 -1
- package/maintainabilityLimits.ts +12 -0
- package/maintainabilityReport.ts +20 -3
- package/moduleFieldNames.test.ts +56 -0
- package/moduleFieldNames.ts +25 -0
- package/package.json +3 -3
- package/preview/PreviewView.ts +32 -11
- package/preview/previewView.test.ts +76 -9
- package/routeJsBudget.test.ts +47 -11
- package/routeJsBudget.ts +88 -25
- package/transforms/barrelAnalyzer.ts +46 -1
- package/transforms/barrelImportsPlugin.ts +61 -45
- package/transforms/transforms.test.ts +67 -1
- package/typecheck/projectTypecheck.ts +5 -1
- package/typecheck/typecheck.proc.ts +3 -2
- package/typecheck/typecheckFailure.ts +6 -1
- package/workflow/artifacts.ts +25 -7
- package/workflow/fieldType.ts +92 -0
- package/workflow/index.ts +2 -0
- package/workflow/plainName.ts +28 -0
- package/workflow/plan.ts +67 -22
- package/workflow/source.ts +0 -5
- package/workspaceGraph.test.ts +200 -3
- 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 {
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
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
|
-
|
|
394
|
-
|
|
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
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
programCommand.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
531
|
-
|
|
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
|
};
|