heximon 0.9.0 → 0.10.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.
- package/README.md +54 -2
- package/dist/bin.mjs +2106 -814
- package/dist/index.d.mts +997 -4
- package/dist/index.mjs +2 -2
- package/dist/skills-command-Czsv_c74.mjs +1799 -0
- package/package.json +16 -16
- package/dist/skills-command-W72zUEYD.mjs +0 -940
package/dist/bin.mjs
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { _ as StdoutRedirect, d as RegistryAccessStatus, f as CommandOutput, g as CliEntry, i as createCommand, m as PromptRefusedError, n as ProjectRoot, p as CommandFailure, r as CreateDefaults, t as skillsCommand, u as RegistryAccess, v as CliDiagnosticCode, x as PromptCancelledError, y as CliDiagnostics } from "./skills-command-Czsv_c74.mjs";
|
|
3
3
|
import { createRequire, register } from "node:module";
|
|
4
4
|
import { cancel, confirm, intro, isCancel, log, outro, select, spinner } from "@clack/prompts";
|
|
5
5
|
import process from "node:process";
|
|
6
|
+
import { defineCommand } from "citty";
|
|
7
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { AstNodeType, CompileEnvironment, CompileMode, DeployPlatform, DiagnosticCode, DiagnosticRenderMode, DiagnosticRenderer, DiagnosticSeverity, ImportKind, ReexportIndex, RuntimeConfigFile, RuntimeSingletons, TerminalStyle, UpgradeBaselineFile } from "@heximon/compiler";
|
|
6
9
|
import { spawn } from "node:child_process";
|
|
7
|
-
import { defineCommand, runMain } from "citty";
|
|
8
10
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "pathe";
|
|
9
11
|
import tab from "@bomb.sh/tab/citty";
|
|
10
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { AstNodeType, CompileEnvironment, CompileMode, DeployPlatform, DiagnosticCode, DiagnosticRenderMode, DiagnosticRenderer, DiagnosticSeverity, ImportKind, ReexportIndex, RuntimeConfigFile, RuntimeSingletons, TerminalStyle, UpgradeBaselineFile } from "@heximon/compiler";
|
|
12
12
|
import { ConfigLoader, LibraryEmitter, LibraryFactoryPreflight, LibraryFreshness, ProjectCompiler, RuntimeConfigLoader, SourceModuleResolution, SourceParser, WorkspaceTopologyReader } from "@heximon/compiler/project";
|
|
13
13
|
import { NodeCompileSeam } from "@heximon/node/compiler";
|
|
14
14
|
import { TransactionCensus, TransactionCensusPolicy } from "@heximon/cqrs/compiler";
|
|
15
|
-
import { BoundaryProvenanceAuditor, BoundaryProvenanceStatus, CredentialSelection, DatabaseConfigDetector, DeployDiagnosticSeverity, DeployLedger, DeployManifestStore, DeployRunner, DeployStrategyUnavailableError, DeployTargetResolver, DeployableServiceReader, DotenvCascade, DrizzleKitRunner, EnvFileReader, EnvFileUnreadableError, HostManagedDeployUnsupportedError, ImportScanner, MigrationInspector, MigrationTargetComposer, PackageCopyAuditor, PeerClosureAuditor, PlatformTracingAuditor, PlatformTracingStatus, WorkspaceConsumerLocator } from "@heximon/deploy";
|
|
15
|
+
import { BoundaryProvenanceAuditor, BoundaryProvenanceStatus, CredentialSelection, DatabaseConfigDetector, DeployDiagnosticSeverity, DeployLedger, DeployManifestStore, DeployRunner, DeployStrategyUnavailableError, DeployTargetResolver, DeployableServiceReader, DotenvCascade, DrizzleKitRunner, EnvFileReader, EnvFileUnreadableError, HostManagedDeployUnsupportedError, ImportScanner, MigrationInspector, MigrationTargetComposer, PackageCopyAuditor, PeerClosureAuditor, PlatformTracingAuditor, PlatformTracingStatus, SecretMask, WorkspaceConsumerLocator } from "@heximon/deploy";
|
|
16
16
|
import { RouteCensus, RouteScopeGap } from "@heximon/http/compiler";
|
|
17
17
|
import { pathToFileURL } from "node:url";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
|
+
//#region src/check-json-document.ts
|
|
20
|
+
/** The stable codes of the failures `heximon check` raises itself, beside the compiler's own. */
|
|
21
|
+
const CheckDiagnosticCode = {
|
|
22
|
+
/** A consumed workspace library changed since its last `vp pack`, so the compile read an old manifest. */
|
|
23
|
+
StaleLibrary: "check.stale-library" };
|
|
24
|
+
//#endregion
|
|
19
25
|
//#region src/compile-diagnostics.ts
|
|
20
26
|
/**
|
|
21
27
|
* Renders compile diagnostics to stderr with the same codeframe layout the dev server shows — a box-drawing
|
|
@@ -63,29 +69,19 @@ var CompileModeResolver = class {
|
|
|
63
69
|
}
|
|
64
70
|
};
|
|
65
71
|
/**
|
|
66
|
-
* Resolve one command's compile mode.
|
|
72
|
+
* Resolve one command's compile mode, or throw the refusal.
|
|
67
73
|
*
|
|
68
|
-
* @param commandName - The verb, for the refusal messages (`routes`, `graph`, `check`).
|
|
69
74
|
* @param rootDir - The absolute project root the shape is detected from.
|
|
70
75
|
* @param args - The parsed `--library` / `--app` flags.
|
|
71
76
|
* @param sourceDirectories - The resolved `--src` directories, checked against the library single-root rule.
|
|
72
|
-
* @returns The resolved mode
|
|
73
|
-
*
|
|
77
|
+
* @returns The resolved mode.
|
|
78
|
+
* @throws CommandFailure with {@link CliDiagnosticCode.InvalidOptions} when no mode can honour the flags.
|
|
74
79
|
*/
|
|
75
|
-
static resolve(
|
|
76
|
-
if (args.library === true && args.app === true)
|
|
77
|
-
process.stderr.write(`heximon ${commandName}: --library and --app are mutually exclusive — pass at most one.\n`);
|
|
78
|
-
process.exitCode = 1;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
80
|
+
static resolve(rootDir, args, sourceDirectories) {
|
|
81
|
+
if (args.library === true && args.app === true) throw CommandFailure.of(CliDiagnosticCode.InvalidOptions, "--library and --app are mutually exclusive — pass at most one.");
|
|
81
82
|
const forcedMode = args.library === true ? CompileMode.Library : args.app === true ? CompileMode.App : void 0;
|
|
82
83
|
const mode = forcedMode ?? (LibraryEmitter.isLibraryPackage(rootDir) ? CompileMode.Library : CompileMode.App);
|
|
83
|
-
if (mode === CompileMode.Library && (sourceDirectories?.length ?? 0) > 1)
|
|
84
|
-
process.stderr.write(`heximon ${commandName}: a library compile reads a single source directory, so it cannot honour a multi-directory --src. Pass one directory, or --app to read this project as an application.
|
|
85
|
-
`);
|
|
86
|
-
process.exitCode = 1;
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
84
|
+
if (mode === CompileMode.Library && (sourceDirectories?.length ?? 0) > 1) throw CommandFailure.of(CliDiagnosticCode.InvalidOptions, "a library compile reads a single source directory, so it cannot honour a multi-directory --src. Pass one directory, or --app to read this project as an application.");
|
|
89
85
|
return {
|
|
90
86
|
mode,
|
|
91
87
|
forced: forcedMode !== void 0
|
|
@@ -97,6 +93,65 @@ var CompileModeResolver = class {
|
|
|
97
93
|
}
|
|
98
94
|
};
|
|
99
95
|
//#endregion
|
|
96
|
+
//#region src/config-sidecar.ts
|
|
97
|
+
/**
|
|
98
|
+
* Loads the `heximon.config.ts` sidecar for a command. A sidecar that throws is the same failure in every
|
|
99
|
+
* command: one {@link CliDiagnosticCode.ConfigUnreadable} diagnostic whose `file` is the sidecar.
|
|
100
|
+
*/
|
|
101
|
+
var ConfigSidecar = class ConfigSidecar {
|
|
102
|
+
/** The fix hint of every config failure. */
|
|
103
|
+
static hint = "Fix the error in the config file, then run the command again.";
|
|
104
|
+
/**
|
|
105
|
+
* Load the sidecar.
|
|
106
|
+
*
|
|
107
|
+
* @param rootDir - The absolute project root.
|
|
108
|
+
* @param environment - The `$env` name to resolve; `NODE_ENV` when omitted.
|
|
109
|
+
* @returns The resolved config, or `undefined` when the project has no sidecar.
|
|
110
|
+
* @throws CommandFailure with {@link CliDiagnosticCode.ConfigUnreadable} when the sidecar throws.
|
|
111
|
+
*/
|
|
112
|
+
static async load(rootDir, environment) {
|
|
113
|
+
try {
|
|
114
|
+
return await ConfigLoader.load(rootDir, environment);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new CommandFailure([await ConfigSidecar.diagnosticOf(rootDir, error, DiagnosticSeverity.Error)]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Run work that loads the sidecar inside another library (a deploy target, a Nitro host). When the work
|
|
121
|
+
* throws, the sidecar is loaded again: if it throws too, the shared failure replaces the error.
|
|
122
|
+
*
|
|
123
|
+
* @param rootDir - The absolute project root.
|
|
124
|
+
* @param environment - The `$env` name the work resolves.
|
|
125
|
+
* @param work - The work.
|
|
126
|
+
* @returns What the work returned.
|
|
127
|
+
*/
|
|
128
|
+
static async during(rootDir, environment, work) {
|
|
129
|
+
try {
|
|
130
|
+
return await work();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (!(error instanceof CommandFailure)) await ConfigSidecar.load(rootDir, environment);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The diagnostic of a sidecar that threw.
|
|
138
|
+
*
|
|
139
|
+
* @param rootDir - The absolute project root.
|
|
140
|
+
* @param error - What the load threw.
|
|
141
|
+
* @param severity - `error` when the command stops; `warning` when it goes on without the config.
|
|
142
|
+
* @param consequence - What the command does without the config, added to the message.
|
|
143
|
+
* @returns The diagnostic, with `file` set when the sidecar file is found.
|
|
144
|
+
*/
|
|
145
|
+
static async diagnosticOf(rootDir, error, severity, consequence) {
|
|
146
|
+
const file = await ConfigLoader.resolveFile(rootDir);
|
|
147
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
148
|
+
return CliDiagnostics.of(severity, CliDiagnosticCode.ConfigUnreadable, `heximon.config.ts could not be loaded${consequence === void 0 ? "" : `, ${consequence}`}: ${reason}`, {
|
|
149
|
+
hint: ConfigSidecar.hint,
|
|
150
|
+
...file === void 0 ? {} : { file }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
//#endregion
|
|
100
155
|
//#region src/project-compile-session.ts
|
|
101
156
|
/**
|
|
102
157
|
* The from-disk compile prologue every inspection command shares (`check`, `graph`, `routes`, `workflows`):
|
|
@@ -144,9 +199,10 @@ var ProjectCompileSession = class ProjectCompileSession {
|
|
|
144
199
|
*
|
|
145
200
|
* @param options - The project root, the `$env` name, and the resolved source directories.
|
|
146
201
|
* @returns The session the caller compiles through.
|
|
202
|
+
* @throws CommandFailure with `cli.config-unreadable` when the sidecar throws.
|
|
147
203
|
*/
|
|
148
204
|
static async open(options) {
|
|
149
|
-
const config = await
|
|
205
|
+
const config = await ConfigSidecar.load(options.rootDir, options.environment);
|
|
150
206
|
const strategy = NodeCompileSeam.resolveStrategy(config?.platform);
|
|
151
207
|
return new ProjectCompileSession({
|
|
152
208
|
rootDir: options.rootDir,
|
|
@@ -519,9 +575,10 @@ const checkCommand = defineCommand({
|
|
|
519
575
|
description: "Project root directory.",
|
|
520
576
|
default: "."
|
|
521
577
|
},
|
|
578
|
+
json: CommandOutput.jsonFlag,
|
|
522
579
|
format: {
|
|
523
580
|
type: "string",
|
|
524
|
-
description: "Output format: 'pretty' (default) or 'json'.",
|
|
581
|
+
description: "Output format: 'pretty' (default) or 'json' (the same as --json).",
|
|
525
582
|
default: "pretty"
|
|
526
583
|
},
|
|
527
584
|
strict: {
|
|
@@ -557,120 +614,123 @@ const checkCommand = defineCommand({
|
|
|
557
614
|
}
|
|
558
615
|
},
|
|
559
616
|
async run({ args }) {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
diagnostics: staleLibraries,
|
|
584
|
-
files: [],
|
|
585
|
-
...emitted.prunedVerdicts === void 0 ? {} : { prunedVerdicts: emitted.prunedVerdicts }
|
|
586
|
-
} : {
|
|
587
|
-
diagnostics: emitted.diagnostics,
|
|
588
|
-
files: emitted.files,
|
|
589
|
-
...emitted.prunedVerdicts === void 0 ? {} : { prunedVerdicts: emitted.prunedVerdicts }
|
|
590
|
-
};
|
|
591
|
-
} else {
|
|
592
|
-
const output = session.compile(true, { verifyBaseline });
|
|
593
|
-
const staleLibraries = await session.staleLibraryDiagnostics();
|
|
594
|
-
if (staleLibraries.length > 0) checked = {
|
|
595
|
-
diagnostics: staleLibraries,
|
|
596
|
-
files: []
|
|
597
|
-
};
|
|
598
|
-
else {
|
|
599
|
-
session.compiler.writeTypeVerdicts(output);
|
|
600
|
-
session.compiler.healTsconfigInclude(session.typeVerdictTarget());
|
|
601
|
-
const emitted = args.emit || session.config?.debug === true;
|
|
602
|
-
if (emitted) {
|
|
603
|
-
session.compiler.write(session.outputDirectory, output.files);
|
|
604
|
-
await session.compiler.formatOnDisk(rootDir, session.outputDirectory, output.files);
|
|
605
|
-
}
|
|
606
|
-
checked = {
|
|
607
|
-
diagnostics: output.diagnostics,
|
|
608
|
-
files: output.files,
|
|
609
|
-
...emitted ? { emittedTo: session.outputDirectory } : {}
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
const blockingErrors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error && diagnostic.escalatedFrom === void 0 && !UpgradeBaseline.isBreaking(diagnostic.code)).length;
|
|
614
|
-
const rebaselined = mode === CompileMode.App && args.update && blockingErrors === 0 ? UpgradeBaseline.update(rootDir, checked.files) : void 0;
|
|
615
|
-
if (rebaselined?.changed === true && checked.diagnostics.some((diagnostic) => UpgradeBaseline.isBreaking(diagnostic.code))) session.compiler.writeTypeVerdicts(session.compile(true, { verifyBaseline }));
|
|
616
|
-
const diagnostics = rebaselined !== void 0 ? checked.diagnostics.filter((diagnostic) => !UpgradeBaseline.isBreaking(diagnostic.code)) : checked.diagnostics;
|
|
617
|
-
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).length;
|
|
618
|
-
const warningCount = diagnostics.length - errorCount;
|
|
619
|
-
const failed = errorCount > 0 || args.strict && warningCount > 0;
|
|
620
|
-
if (asJson) {
|
|
621
|
-
const census = TransactionCensusReport.read(checked.files);
|
|
622
|
-
process.stdout.write(`${JSON.stringify({
|
|
623
|
-
ok: !failed,
|
|
624
|
-
mode,
|
|
625
|
-
errors: errorCount,
|
|
626
|
-
warnings: warningCount,
|
|
627
|
-
diagnostics,
|
|
628
|
-
...census === void 0 ? {} : { transactionCensus: census },
|
|
629
|
-
...checked.prunedVerdicts === void 0 ? {} : { prunedVerdicts: checked.prunedVerdicts },
|
|
630
|
-
...checked.emittedTo === void 0 ? {} : { emittedTo: checked.emittedTo },
|
|
631
|
-
...args.update === true ? { upgradeBaseline: rebaselined === void 0 ? {
|
|
632
|
-
written: false,
|
|
633
|
-
blockingErrors
|
|
617
|
+
const commandOutput = CommandOutput.resolve("check", args);
|
|
618
|
+
await commandOutput.run(async () => {
|
|
619
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
620
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
621
|
+
const resolved = CompileModeResolver.resolve(rootDir, args, sourceDirectories);
|
|
622
|
+
const { mode } = resolved;
|
|
623
|
+
const verifyBaseline = args["verify-baseline"] === true;
|
|
624
|
+
if (verifyBaseline && args.update === true) throw CommandFailure.of(CliDiagnosticCode.InvalidOptions, "--verify-baseline reports what the committed baseline has not acknowledged, and --update acknowledges everything — pass one or the other.");
|
|
625
|
+
if (!commandOutput.isJson()) intro(`heximon check — ${basename(rootDir)} · ${CompileModeResolver.stamp(resolved)}`);
|
|
626
|
+
const session = await ProjectCompileSession.open({
|
|
627
|
+
rootDir,
|
|
628
|
+
environment: args.env,
|
|
629
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
630
|
+
});
|
|
631
|
+
let checked;
|
|
632
|
+
if (mode === CompileMode.Library) {
|
|
633
|
+
const emitted = session.compileLibrary({ pruneVerdicts: true });
|
|
634
|
+
const staleLibraries = await session.staleLibraryDiagnostics();
|
|
635
|
+
checked = staleLibraries.length > 0 ? {
|
|
636
|
+
diagnostics: staleLibraries,
|
|
637
|
+
files: [],
|
|
638
|
+
staleLibraries: true,
|
|
639
|
+
...emitted.prunedVerdicts === void 0 ? {} : { prunedVerdicts: emitted.prunedVerdicts }
|
|
634
640
|
} : {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
641
|
+
diagnostics: emitted.diagnostics,
|
|
642
|
+
files: emitted.files,
|
|
643
|
+
...emitted.prunedVerdicts === void 0 ? {} : { prunedVerdicts: emitted.prunedVerdicts }
|
|
644
|
+
};
|
|
645
|
+
} else {
|
|
646
|
+
const output = session.compile(true, { verifyBaseline });
|
|
647
|
+
const staleLibraries = await session.staleLibraryDiagnostics();
|
|
648
|
+
if (staleLibraries.length > 0) checked = {
|
|
649
|
+
diagnostics: staleLibraries,
|
|
650
|
+
files: [],
|
|
651
|
+
staleLibraries: true
|
|
652
|
+
};
|
|
653
|
+
else {
|
|
654
|
+
session.compiler.writeTypeVerdicts(output);
|
|
655
|
+
session.compiler.healTsconfigInclude(session.typeVerdictTarget());
|
|
656
|
+
const emitted = args.emit || session.config?.debug === true;
|
|
657
|
+
if (emitted) {
|
|
658
|
+
session.compiler.write(session.outputDirectory, output.files);
|
|
659
|
+
await session.compiler.formatOnDisk(rootDir, session.outputDirectory, output.files);
|
|
660
|
+
}
|
|
661
|
+
checked = {
|
|
662
|
+
diagnostics: output.diagnostics,
|
|
663
|
+
files: output.files,
|
|
664
|
+
...emitted ? { emittedTo: session.outputDirectory } : {}
|
|
665
|
+
};
|
|
666
|
+
}
|
|
658
667
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
668
|
+
const blockingErrors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error && diagnostic.escalatedFrom === void 0 && !UpgradeBaseline.isBreaking(diagnostic.code)).length;
|
|
669
|
+
const rebaselined = mode === CompileMode.App && args.update && blockingErrors === 0 ? UpgradeBaseline.update(rootDir, checked.files) : void 0;
|
|
670
|
+
if (rebaselined?.changed === true && checked.diagnostics.some((diagnostic) => UpgradeBaseline.isBreaking(diagnostic.code))) session.compiler.writeTypeVerdicts(session.compile(true, { verifyBaseline }));
|
|
671
|
+
const diagnostics = rebaselined !== void 0 ? checked.diagnostics.filter((diagnostic) => !UpgradeBaseline.isBreaking(diagnostic.code)) : checked.diagnostics;
|
|
672
|
+
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).length;
|
|
673
|
+
const warningCount = diagnostics.length - errorCount;
|
|
674
|
+
const failed = errorCount > 0 || args.strict && warningCount > 0;
|
|
675
|
+
if (commandOutput.isJson()) {
|
|
676
|
+
const census = TransactionCensusReport.read(checked.files);
|
|
677
|
+
const document = {
|
|
678
|
+
ok: !failed,
|
|
679
|
+
mode,
|
|
680
|
+
errors: errorCount,
|
|
681
|
+
warnings: warningCount,
|
|
682
|
+
diagnostics: CliDiagnostics.fromCompiler(diagnostics).map((diagnostic) => checked.staleLibraries === true ? {
|
|
683
|
+
...diagnostic,
|
|
684
|
+
code: CheckDiagnosticCode.StaleLibrary
|
|
685
|
+
} : diagnostic),
|
|
686
|
+
...census === void 0 ? {} : { transactionCensus: census },
|
|
687
|
+
...checked.prunedVerdicts === void 0 ? {} : { prunedVerdicts: checked.prunedVerdicts },
|
|
688
|
+
...checked.emittedTo === void 0 ? {} : { emittedTo: checked.emittedTo },
|
|
689
|
+
...args.update === true ? { upgradeBaseline: rebaselined === void 0 ? {
|
|
690
|
+
written: false,
|
|
691
|
+
blockingErrors
|
|
692
|
+
} : {
|
|
693
|
+
written: true,
|
|
694
|
+
changed: rebaselined.changed,
|
|
695
|
+
path: rebaselined.target
|
|
696
|
+
} } : {}
|
|
697
|
+
};
|
|
698
|
+
commandOutput.emit(document);
|
|
665
699
|
} else {
|
|
666
|
-
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
700
|
+
if (checked.emittedTo !== void 0) log.info(`Wrote the generated wiring to ${checked.emittedTo}`);
|
|
701
|
+
if (checked.prunedVerdicts !== void 0) log.info(`Removed the stale DI verdict mirror ${checked.prunedVerdicts} — it was written by an app-mode compile of this package, and a library compile emits none to replace it.`);
|
|
702
|
+
if (mode === CompileMode.Library && (args.emit === true || session.config?.debug === true)) log.warn(`${args.emit === true ? "`--emit`" : "`debug: true`"} writes app wiring only. A library's generated factory is written by \`vp pack\`.`);
|
|
703
|
+
if (mode === CompileMode.Library && verifyBaseline) log.warn(`${UpgradeBaselineFile.fileName} is an app-level artifact — a library's durable surface is verified by each app that composes it, so there is nothing here to verify.`);
|
|
704
|
+
if (mode === CompileMode.Library && args.update === true) log.warn(`${UpgradeBaselineFile.fileName} is an app-level artifact — a library's durable surface is baselined by each app that composes it, so there is nothing here to update.`);
|
|
705
|
+
else if (rebaselined !== void 0) log.success(rebaselined.changed ? `Updated ${UpgradeBaselineFile.fileName} — commit it to acknowledge the durable-surface changes.` : `${UpgradeBaselineFile.fileName} already up to date.`);
|
|
706
|
+
else if (args.update === true && blockingErrors > 0) log.warn(`${UpgradeBaselineFile.fileName} NOT updated — this compile has ${blockingErrors} unrelated error${blockingErrors === 1 ? "" : "s"}, so its durable surface is only partly analyzed and baselining it would acknowledge less than the app really has. Fix those first, then re-run.`);
|
|
707
|
+
else if (args.update === true) log.info("Nothing durable to baseline — no workflows, queue channels or other durable surface found.");
|
|
708
|
+
const census = TransactionCensusReport.read(checked.files);
|
|
709
|
+
const censusSummary = census === void 0 ? void 0 : TransactionCensusReport.summarize(census);
|
|
710
|
+
if (census !== void 0 && censusSummary !== void 0) {
|
|
711
|
+
const sections = [censusSummary];
|
|
712
|
+
const composed = TransactionCensusReport.composedWraps(census);
|
|
713
|
+
const unwrapped = TransactionCensusReport.unwrapped(census);
|
|
714
|
+
if (composed.length > 0) sections.push(`Composed, wrapped on the database the package handed over:\n${composed.map((line) => ` ${line}`).join("\n")}`);
|
|
715
|
+
if (unwrapped.length > 0) sections.push(`Not wrapped:\n${unwrapped.map((line) => ` ${line}`).join("\n")}`);
|
|
716
|
+
log.info(sections.join("\n"));
|
|
717
|
+
}
|
|
718
|
+
if (mode === CompileMode.App && UpgradeBaseline.hasDurableSurface(checked.files) && !existsSync(join(rootDir, UpgradeBaselineFile.fileName))) log.info(`Durable surface found but no committed ${UpgradeBaselineFile.fileName} — run \`heximon check --update\` once and commit it to arm the upgrade-safety gate.`);
|
|
719
|
+
if (diagnostics.length > 0) CompileDiagnostics.renderToStderr(rootDir, diagnostics);
|
|
720
|
+
if (failed) {
|
|
721
|
+
const parts = [`${errorCount} error${errorCount === 1 ? "" : "s"}`];
|
|
722
|
+
if (warningCount > 0) parts.push(`${warningCount} warning${warningCount === 1 ? "" : "s"}`);
|
|
723
|
+
outro(`Check failed — ${parts.join(", ")}.`);
|
|
724
|
+
} else {
|
|
725
|
+
const closing = mode === CompileMode.Library ? "Library compiled." : "Verdicts refreshed.";
|
|
726
|
+
if (warningCount > 0) {
|
|
727
|
+
log.warn(`${warningCount} warning${warningCount === 1 ? "" : "s"} — not failing (pass --strict to fail on warnings too).`);
|
|
728
|
+
outro(closing);
|
|
729
|
+
} else outro(`No issues — ${closing.toLowerCase()}`);
|
|
730
|
+
}
|
|
671
731
|
}
|
|
672
|
-
|
|
673
|
-
|
|
732
|
+
if (failed) process.exitCode = 1;
|
|
733
|
+
});
|
|
674
734
|
}
|
|
675
735
|
});
|
|
676
736
|
//#endregion
|
|
@@ -711,10 +771,13 @@ var CliSpinner = class {
|
|
|
711
771
|
* message rather than hanging on a prompt no one can answer). Optional creds (e.g. a Cloudflare token when
|
|
712
772
|
* `wrangler login` already authenticates) are never prompted, so a logged-in deploy is not interrupted.
|
|
713
773
|
*
|
|
774
|
+
* Under `--json` the prompter refuses every prompt: the resolver then fails once, with one `cli.option-required`
|
|
775
|
+
* diagnostic for each required credential that is not set.
|
|
776
|
+
*
|
|
714
777
|
* This whole ladder is CLI-specific. A non-CLI caller (a dashboard) resolves the same `CredentialSpec`s its own way
|
|
715
778
|
* — from a vault / secret store — and hands `DeployRunner` an equivalent map; it never constructs this resolver.
|
|
716
779
|
*/
|
|
717
|
-
var CredentialResolver = class {
|
|
780
|
+
var CredentialResolver = class CredentialResolver {
|
|
718
781
|
/**
|
|
719
782
|
* Resolve the given credential specs into a value map.
|
|
720
783
|
*
|
|
@@ -722,10 +785,12 @@ var CredentialResolver = class {
|
|
|
722
785
|
* @param options - Interactivity, the prompter, and the environment source.
|
|
723
786
|
* @returns The resolved `envVar → value` map (holds secrets — never log it).
|
|
724
787
|
* @throws PromptCancelledError when the operator cancels an interactive prompt.
|
|
788
|
+
* @throws CommandFailure when the prompter refused one or more prompts (`--json`).
|
|
725
789
|
*/
|
|
726
790
|
static async resolve(specs, options) {
|
|
727
791
|
const env = options.env ?? process.env;
|
|
728
792
|
const values = /* @__PURE__ */ new Map();
|
|
793
|
+
const refused = [];
|
|
729
794
|
for (const spec of specs) {
|
|
730
795
|
const fromEnv = env[spec.envVar];
|
|
731
796
|
if (fromEnv !== void 0 && fromEnv.length > 0) {
|
|
@@ -733,13 +798,47 @@ var CredentialResolver = class {
|
|
|
733
798
|
continue;
|
|
734
799
|
}
|
|
735
800
|
if (spec.required && options.interactive && options.prompter !== void 0) {
|
|
736
|
-
const label =
|
|
737
|
-
|
|
801
|
+
const label = CredentialResolver.labelOf(spec);
|
|
802
|
+
let prompted;
|
|
803
|
+
try {
|
|
804
|
+
prompted = spec.secret ? await options.prompter.secret(label) : await options.prompter.text(label);
|
|
805
|
+
} catch (error) {
|
|
806
|
+
if (!(error instanceof PromptRefusedError)) throw error;
|
|
807
|
+
refused.push(spec);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
738
810
|
if (prompted.length > 0) values.set(spec.envVar, prompted);
|
|
739
811
|
}
|
|
740
812
|
}
|
|
813
|
+
if (refused.length > 0) throw new CommandFailure(refused.map((spec) => CredentialResolver.missing(spec)));
|
|
741
814
|
return values;
|
|
742
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* The prompt label of one credential: its name and its environment variable.
|
|
818
|
+
*
|
|
819
|
+
* @param spec - The credential.
|
|
820
|
+
* @returns The label, for example `Cloudflare API token (CLOUDFLARE_API_TOKEN)`.
|
|
821
|
+
*/
|
|
822
|
+
static labelOf(spec) {
|
|
823
|
+
return `${spec.name} (${spec.envVar})`;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Map a prompt label back to the environment variable that answers it, for a refusing prompter.
|
|
827
|
+
*
|
|
828
|
+
* @param specs - The credentials the run resolves.
|
|
829
|
+
* @returns The map function; an unknown label gives the label itself.
|
|
830
|
+
*/
|
|
831
|
+
static optionFor(specs) {
|
|
832
|
+
const envVarByLabel = new Map(specs.map((spec) => [CredentialResolver.labelOf(spec), spec.envVar]));
|
|
833
|
+
return (message) => envVarByLabel.get(message) ?? message;
|
|
834
|
+
}
|
|
835
|
+
/** The `cli.option-required` diagnostic of one required credential that is not set. */
|
|
836
|
+
static missing(spec) {
|
|
837
|
+
return CliDiagnostics.error(CliDiagnosticCode.OptionRequired, `${spec.name} is not set, and the command never prompts for it under --json.`, {
|
|
838
|
+
hint: `Set ${spec.envVar}.`,
|
|
839
|
+
option: spec.envVar
|
|
840
|
+
});
|
|
841
|
+
}
|
|
743
842
|
};
|
|
744
843
|
//#endregion
|
|
745
844
|
//#region src/deploy-command-failure.ts
|
|
@@ -764,6 +863,48 @@ var DeployCommandFailure = class {
|
|
|
764
863
|
}
|
|
765
864
|
return false;
|
|
766
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* The `--json` diagnostic of an expected halt. A cancelled prompt has none: `--json` never prompts.
|
|
868
|
+
*
|
|
869
|
+
* @param error - The error {@link report} accepted.
|
|
870
|
+
* @param codes - The codes of the command.
|
|
871
|
+
* @returns The error diagnostic, or `undefined` when the error has no code in `codes`.
|
|
872
|
+
*/
|
|
873
|
+
static diagnosticOf(error, codes) {
|
|
874
|
+
const code = error instanceof DeployStrategyUnavailableError ? codes.StrategyUnavailable : error instanceof HostManagedDeployUnsupportedError ? codes.HostManagedUnsupported : error instanceof EnvFileUnreadableError ? codes.EnvFileUnreadable : void 0;
|
|
875
|
+
return code === void 0 || !(error instanceof Error) ? void 0 : CliDiagnostics.error(code, error.message);
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region src/deploy-json-document.ts
|
|
880
|
+
/**
|
|
881
|
+
* The stable codes of the `heximon deploy --json` diagnostics. A flag conflict uses
|
|
882
|
+
* `cli.invalid-options`, and a missing answer (`--yes`, a credential variable) uses `cli.option-required`.
|
|
883
|
+
*/
|
|
884
|
+
const DeployDiagnosticCode = {
|
|
885
|
+
/**
|
|
886
|
+
* A finding of the deploy run (preflight, migrations, build, infrastructure, provisioning, ship), at its own
|
|
887
|
+
* severity. A dry run that goes on past an error finding reports it as a warning, so `ok` stays `true`.
|
|
888
|
+
*/
|
|
889
|
+
Finding: "deploy.finding",
|
|
890
|
+
/** The run stopped before the upload. Work that ran before the stop is not rolled back. */
|
|
891
|
+
Halted: "deploy.halted",
|
|
892
|
+
/** The vendor rejected the upload, so the artifact is not live. */
|
|
893
|
+
ShipFailed: "deploy.ship-failed",
|
|
894
|
+
/** The app shipped, but the `--verify` liveness probe got no 2xx answer. */
|
|
895
|
+
NotLive: "deploy.not-live",
|
|
896
|
+
/** The app shipped, but a migration after the ship failed to apply. */
|
|
897
|
+
MigrationFailed: "deploy.migration-failed",
|
|
898
|
+
/** `--migrate` found no database config to apply. */
|
|
899
|
+
NoMigrationConfig: "deploy.no-migration-config",
|
|
900
|
+
/** `--destroy` on a platform that has no teardown step. */
|
|
901
|
+
DestroyUnsupported: "deploy.destroy-unsupported",
|
|
902
|
+
/** No deploy strategy is installed for the platform. */
|
|
903
|
+
StrategyUnavailable: "deploy.strategy-unavailable",
|
|
904
|
+
/** The `--env-file` file cannot be read. */
|
|
905
|
+
EnvFileUnreadable: "deploy.env-file-unreadable",
|
|
906
|
+
/** A Nitro or Nuxt build uses a preset that the deploy cannot ship. */
|
|
907
|
+
HostManagedUnsupported: "deploy.host-managed-unsupported"
|
|
767
908
|
};
|
|
768
909
|
//#endregion
|
|
769
910
|
//#region src/deploy-command.ts
|
|
@@ -818,6 +959,8 @@ var DeployArgumentGuards = class DeployArgumentGuards {
|
|
|
818
959
|
* failure, which a local test cannot stage.
|
|
819
960
|
*/
|
|
820
961
|
var DeployReport = class DeployReport {
|
|
962
|
+
/** The hint of an error finding that a dry run went on past. */
|
|
963
|
+
static dryRunFindingHint = "The dry run went on, but a deploy without --dry-run stops on this finding.";
|
|
821
964
|
/**
|
|
822
965
|
* Print the whole outcome and decide the command's exit.
|
|
823
966
|
*
|
|
@@ -837,8 +980,7 @@ var DeployReport = class DeployReport {
|
|
|
837
980
|
return false;
|
|
838
981
|
}
|
|
839
982
|
if (outcome.halted || outcome.result === void 0) {
|
|
840
|
-
|
|
841
|
-
cancel(completed.length === 0 ? "Halted before ship — nothing changed." : `Halted before ship — nothing was uploaded, but earlier work already completed and was NOT rolled back: ${completed.join("; ")}.`);
|
|
983
|
+
cancel(DeployReport.haltMessage(outcome));
|
|
842
984
|
return false;
|
|
843
985
|
}
|
|
844
986
|
for (const message of outcome.result.messages) log.message(message);
|
|
@@ -866,6 +1008,112 @@ var DeployReport = class DeployReport {
|
|
|
866
1008
|
static hasFailedMigration(outcome) {
|
|
867
1009
|
return (outcome.migrations ?? []).some((migrated) => migrated.diagnostics.some((diagnostic) => diagnostic.severity === DeployDiagnosticSeverity.Error));
|
|
868
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* The `--json` diagnostics of an outcome: each finding of the runner, each missing credential, and one
|
|
1013
|
+
* closing error when {@link render} fails the run.
|
|
1014
|
+
*
|
|
1015
|
+
* A run that succeeds exits 0, so it has no error diagnostic: a dry run goes on past a finding that would
|
|
1016
|
+
* stop a real deploy, and that finding is a warning here, with a hint that says so.
|
|
1017
|
+
*
|
|
1018
|
+
* @param outcome - What the runner reported.
|
|
1019
|
+
* @param succeeded - What {@link render} returned for the outcome.
|
|
1020
|
+
* @returns The diagnostics, in the order the text report prints them.
|
|
1021
|
+
*/
|
|
1022
|
+
static diagnostics(outcome, succeeded) {
|
|
1023
|
+
const diagnostics = outcome.diagnostics.map((diagnostic) => DeployReport.finding(diagnostic, succeeded));
|
|
1024
|
+
for (const credential of outcome.missingCredentials) diagnostics.push({
|
|
1025
|
+
...CliDiagnostics.error(CliDiagnosticCode.OptionRequired, `${credential.name} is not set.`, {
|
|
1026
|
+
hint: `Set ${credential.envVar}.`,
|
|
1027
|
+
option: credential.envVar
|
|
1028
|
+
}),
|
|
1029
|
+
severity: outcome.halted ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning
|
|
1030
|
+
});
|
|
1031
|
+
for (const diagnostic of outcome.verification?.diagnostics ?? []) diagnostics.push(DeployReport.finding(diagnostic, succeeded));
|
|
1032
|
+
const closing = DeployReport.closingDiagnostic(outcome);
|
|
1033
|
+
if (closing !== void 0) diagnostics.push(closing);
|
|
1034
|
+
return diagnostics;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* The `ship` section of the `--json` document.
|
|
1038
|
+
*
|
|
1039
|
+
* @param outcome - What the runner reported.
|
|
1040
|
+
* @param run - The facts the command recorded around the runner.
|
|
1041
|
+
* @returns The section.
|
|
1042
|
+
*/
|
|
1043
|
+
static ship(outcome, run) {
|
|
1044
|
+
return {
|
|
1045
|
+
halted: outcome.halted,
|
|
1046
|
+
...run,
|
|
1047
|
+
missingCredentials: outcome.missingCredentials.map((credential) => ({
|
|
1048
|
+
name: credential.name,
|
|
1049
|
+
envVar: credential.envVar
|
|
1050
|
+
})),
|
|
1051
|
+
...outcome.targetEnsure === void 0 ? {} : { targetBootstrap: DeployReport.resources(outcome.targetEnsure) },
|
|
1052
|
+
...outcome.provision === void 0 ? {} : { provision: DeployReport.resources(outcome.provision) },
|
|
1053
|
+
migrations: (outcome.migrations ?? []).map((migrated) => ({
|
|
1054
|
+
target: migrated.target,
|
|
1055
|
+
phase: migrated.phase,
|
|
1056
|
+
applied: migrated.applied,
|
|
1057
|
+
failed: migrated.diagnostics.some((diagnostic) => diagnostic.severity === DeployDiagnosticSeverity.Error)
|
|
1058
|
+
})),
|
|
1059
|
+
...outcome.result === void 0 ? {} : { result: {
|
|
1060
|
+
platform: outcome.result.platform,
|
|
1061
|
+
shipped: outcome.result.shipped,
|
|
1062
|
+
failed: outcome.result.failed === true,
|
|
1063
|
+
...outcome.result.url === void 0 ? {} : { url: outcome.result.url },
|
|
1064
|
+
messages: outcome.result.messages
|
|
1065
|
+
} },
|
|
1066
|
+
...outcome.verification === void 0 ? {} : { verification: {
|
|
1067
|
+
url: outcome.verification.url,
|
|
1068
|
+
live: outcome.verification.live
|
|
1069
|
+
} },
|
|
1070
|
+
completedBeforeHalt: outcome.halted ? DeployReport.completedBeforeHalt(outcome) : []
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* The error that closes a failed run, in the same order of checks as {@link render}; `undefined` when
|
|
1075
|
+
* the run succeeded.
|
|
1076
|
+
*/
|
|
1077
|
+
static closingDiagnostic(outcome) {
|
|
1078
|
+
if (outcome.result?.failed === true) return CliDiagnostics.error(DeployDiagnosticCode.ShipFailed, `The ship to ${outcome.result.platform} failed, so the artifact is not live.`);
|
|
1079
|
+
if (outcome.halted || outcome.result === void 0) return CliDiagnostics.error(DeployDiagnosticCode.Halted, DeployReport.haltMessage(outcome));
|
|
1080
|
+
if (outcome.verification !== void 0 && !outcome.verification.live) return CliDiagnostics.error(DeployDiagnosticCode.NotLive, `Deployed to ${outcome.result.platform}, but ${outcome.verification.url} does not respond.`);
|
|
1081
|
+
if (DeployReport.hasFailedMigration(outcome)) return CliDiagnostics.error(DeployDiagnosticCode.MigrationFailed, `Deployed to ${outcome.result.platform}, but a migration failed to apply.`);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* The halt line. Only the UPLOAD is known not to have happened: target bootstrap, provisioning and a
|
|
1085
|
+
* pre-ship migration run earlier and are never rolled back, so the line names them instead of denying them.
|
|
1086
|
+
*/
|
|
1087
|
+
static haltMessage(outcome) {
|
|
1088
|
+
const completed = DeployReport.completedBeforeHalt(outcome);
|
|
1089
|
+
return completed.length === 0 ? "Halted before ship — nothing changed." : `Halted before ship — nothing was uploaded, but earlier work already completed and was NOT rolled back: ${completed.join("; ")}.`;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* One runner finding as a `deploy.finding` diagnostic, at its own severity. In a run that succeeds, an
|
|
1093
|
+
* error finding is a warning, because the run went on past it.
|
|
1094
|
+
*/
|
|
1095
|
+
static finding(diagnostic, succeeded) {
|
|
1096
|
+
if (diagnostic.severity !== DeployDiagnosticSeverity.Error) return {
|
|
1097
|
+
severity: DiagnosticSeverity.Warning,
|
|
1098
|
+
code: DeployDiagnosticCode.Finding,
|
|
1099
|
+
message: diagnostic.message
|
|
1100
|
+
};
|
|
1101
|
+
return {
|
|
1102
|
+
severity: succeeded ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error,
|
|
1103
|
+
code: DeployDiagnosticCode.Finding,
|
|
1104
|
+
message: diagnostic.message,
|
|
1105
|
+
...succeeded ? { hint: DeployReport.dryRunFindingHint } : {}
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
/** Copy a phase's resource outcome into its document shape. */
|
|
1109
|
+
static resources(result) {
|
|
1110
|
+
return {
|
|
1111
|
+
created: result.created,
|
|
1112
|
+
existing: result.existing,
|
|
1113
|
+
failed: result.failed,
|
|
1114
|
+
messages: result.messages
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
869
1117
|
/** The runner's own findings, at the severity each carries. */
|
|
870
1118
|
static logDiagnostics(diagnostics) {
|
|
871
1119
|
for (const diagnostic of diagnostics ?? []) if (diagnostic.severity === DeployDiagnosticSeverity.Error) log.error(diagnostic.message);
|
|
@@ -923,18 +1171,70 @@ var DeployReport = class DeployReport {
|
|
|
923
1171
|
return false;
|
|
924
1172
|
}
|
|
925
1173
|
};
|
|
1174
|
+
/**
|
|
1175
|
+
* Builds the `heximon deploy --json` document. The run fields are set as the run reaches them, so a
|
|
1176
|
+
* failure document still says how far the run got.
|
|
1177
|
+
*/
|
|
1178
|
+
var DeployDocument = class {
|
|
1179
|
+
args;
|
|
1180
|
+
target;
|
|
1181
|
+
credentials;
|
|
1182
|
+
/**
|
|
1183
|
+
* @param args - The parsed deploy flags.
|
|
1184
|
+
*/
|
|
1185
|
+
constructor(args) {
|
|
1186
|
+
this.args = args;
|
|
1187
|
+
}
|
|
1188
|
+
/** Record the resolved target. */
|
|
1189
|
+
resolved(target) {
|
|
1190
|
+
this.target = {
|
|
1191
|
+
platform: target.platformName ?? "node",
|
|
1192
|
+
targetClass: target.targetClass,
|
|
1193
|
+
hostManaged: target.hostManaged === true,
|
|
1194
|
+
...target.artifactDir === void 0 ? {} : { artifactDir: target.artifactDir }
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
/** Record the names of the resolved credential variables. */
|
|
1198
|
+
resolvedCredentials(credentials) {
|
|
1199
|
+
this.credentials = [...credentials.keys()];
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* The document of the run.
|
|
1203
|
+
*
|
|
1204
|
+
* @param ok - Whether the command exits 0.
|
|
1205
|
+
* @param diagnostics - The diagnostics of the run.
|
|
1206
|
+
* @param sections - The `ship` or `destroy` section, once the run has one.
|
|
1207
|
+
* @returns The document.
|
|
1208
|
+
*/
|
|
1209
|
+
document(ok, diagnostics, sections = {}) {
|
|
1210
|
+
return {
|
|
1211
|
+
ok,
|
|
1212
|
+
diagnostics,
|
|
1213
|
+
action: this.args.destroy ? "destroy" : "ship",
|
|
1214
|
+
environment: this.args.env,
|
|
1215
|
+
dryRun: this.args["dry-run"],
|
|
1216
|
+
...this.target === void 0 ? {} : { target: this.target },
|
|
1217
|
+
...this.credentials === void 0 ? {} : { credentials: this.credentials },
|
|
1218
|
+
...sections
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
926
1222
|
/** The `--destroy` teardown and the pre-ship discovery/migration composition the ship path runs. */
|
|
927
|
-
var DeployPhases = class {
|
|
1223
|
+
var DeployPhases = class DeployPhases {
|
|
1224
|
+
/** The halt line of a `--migrate` run that found no database config. */
|
|
1225
|
+
static noMigrationConfig = "--migrate found no database config to apply — expected a `*.config.ts` that `export default`s a Drizzle config (see `heximon migrations status`), or apply migrations manually before deploying.";
|
|
928
1226
|
/**
|
|
929
|
-
* Confirm and run the teardown.
|
|
1227
|
+
* Confirm and run the teardown, and record its document.
|
|
930
1228
|
*
|
|
931
1229
|
* @returns Whether the command should exit non-zero.
|
|
932
1230
|
*/
|
|
933
|
-
static async destroy(args, target, rootDir, credentials,
|
|
1231
|
+
static async destroy(args, target, rootDir, credentials, commandOutput, report) {
|
|
934
1232
|
const platformName = target.platformName ?? "node";
|
|
1233
|
+
const interactive = commandOutput.isInteractive();
|
|
1234
|
+
const question = `Remove the deployed app on ${platformName} (env: ${args.env})? This cannot be undone.`;
|
|
935
1235
|
if (interactive) {
|
|
936
1236
|
const answer = await confirm({
|
|
937
|
-
message:
|
|
1237
|
+
message: question,
|
|
938
1238
|
initialValue: false
|
|
939
1239
|
});
|
|
940
1240
|
if (isCancel(answer)) {
|
|
@@ -946,6 +1246,7 @@ var DeployPhases = class {
|
|
|
946
1246
|
return false;
|
|
947
1247
|
}
|
|
948
1248
|
} else if (!args.yes) {
|
|
1249
|
+
commandOutput.assertCanPrompt("--yes", question);
|
|
949
1250
|
cancel("Refusing to --destroy non-interactively without --yes.");
|
|
950
1251
|
return true;
|
|
951
1252
|
}
|
|
@@ -965,12 +1266,21 @@ var DeployPhases = class {
|
|
|
965
1266
|
throw teardownError;
|
|
966
1267
|
}
|
|
967
1268
|
removing?.stop(args["dry-run"] ? "Dry run — nothing removed." : "Teardown step complete.");
|
|
1269
|
+
const destroy = {
|
|
1270
|
+
platform: teardown.result?.platform ?? teardown.platformName ?? "node",
|
|
1271
|
+
unsupported: teardown.unsupported,
|
|
1272
|
+
destroyed: teardown.result?.destroyed === true,
|
|
1273
|
+
messages: teardown.result?.messages ?? []
|
|
1274
|
+
};
|
|
968
1275
|
if (teardown.unsupported) {
|
|
969
|
-
|
|
1276
|
+
const message = `Platform ${teardown.platformName ?? "node (default)"} has nothing to remove (no destroy step).`;
|
|
1277
|
+
cancel(message);
|
|
1278
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(DeployDiagnosticCode.DestroyUnsupported, message)], { destroy }));
|
|
970
1279
|
return true;
|
|
971
1280
|
}
|
|
972
1281
|
for (const message of teardown.result?.messages ?? []) log.message(message);
|
|
973
1282
|
outro(teardown.result?.destroyed === true ? `Removed from ${teardown.result.platform}.` : `Dry run — nothing removed (${teardown.result?.platform ?? target.platformName ?? "node"}).`);
|
|
1283
|
+
commandOutput.emit(report.document(true, [], { destroy }));
|
|
974
1284
|
return false;
|
|
975
1285
|
}
|
|
976
1286
|
/**
|
|
@@ -1046,7 +1356,7 @@ var DeployPhases = class {
|
|
|
1046
1356
|
};
|
|
1047
1357
|
} else if (composed.notes.length > 0) log.warn("--migrate will apply 0 databases this run — every detected database config is applied by its own mechanism (see the notes above). Nothing is migrated by this deploy.");
|
|
1048
1358
|
else {
|
|
1049
|
-
cancel(
|
|
1359
|
+
cancel(DeployPhases.noMigrationConfig);
|
|
1050
1360
|
return null;
|
|
1051
1361
|
}
|
|
1052
1362
|
if (args.migrations !== void 0) log.info("--migrations <dir> is superseded by the auto-detected database config(s) under --migrate.");
|
|
@@ -1070,6 +1380,7 @@ const deployCommand = defineCommand({
|
|
|
1070
1380
|
description: "Project root directory.",
|
|
1071
1381
|
default: "."
|
|
1072
1382
|
},
|
|
1383
|
+
json: CommandOutput.jsonFlag,
|
|
1073
1384
|
env: {
|
|
1074
1385
|
type: "string",
|
|
1075
1386
|
description: "Deploy environment (production / staging / preview:<id>).",
|
|
@@ -1141,101 +1452,588 @@ const deployCommand = defineCommand({
|
|
|
1141
1452
|
}
|
|
1142
1453
|
},
|
|
1143
1454
|
async run({ args }) {
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
try {
|
|
1156
|
-
const target = await DeployTargetResolver.resolve(rootDir, environment, deployArgs.platform);
|
|
1157
|
-
log.info(`Platform: ${target.platformName ?? "node (default)"} · target class: ${target.targetClass}`);
|
|
1158
|
-
if (target.hostManaged) log.info(`Host-managed build detected (${target.artifactDir}) — shipping it, skipping \`vp build\`.`);
|
|
1159
|
-
const profileMismatch = DeployArgumentGuards.profileMismatch(deployArgs.profile, target.platformName);
|
|
1160
|
-
if (profileMismatch !== void 0) {
|
|
1161
|
-
cancel(profileMismatch);
|
|
1455
|
+
const commandOutput = CommandOutput.resolve("deploy", args);
|
|
1456
|
+
await commandOutput.run(async () => {
|
|
1457
|
+
const deployArgs = args;
|
|
1458
|
+
const rootDir = ProjectRoot.resolve(deployArgs.cwd);
|
|
1459
|
+
const environment = deployArgs.env;
|
|
1460
|
+
const interactive = commandOutput.isInteractive();
|
|
1461
|
+
const report = new DeployDocument(deployArgs);
|
|
1462
|
+
intro(`heximon deploy · ${environment}`);
|
|
1463
|
+
const incoherence = DeployArgumentGuards.incoherence(deployArgs);
|
|
1464
|
+
if (incoherence !== void 0) {
|
|
1465
|
+
cancel(incoherence);
|
|
1162
1466
|
process.exitCode = 1;
|
|
1467
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(CliDiagnosticCode.InvalidOptions, incoherence)]));
|
|
1163
1468
|
return;
|
|
1164
1469
|
}
|
|
1165
|
-
const specs = CredentialSelection.inPlay(target.strategy.requiredCredentials(), { provisioning: deployArgs.provision });
|
|
1166
|
-
const credentials = await CredentialResolver.resolve(specs, {
|
|
1167
|
-
interactive,
|
|
1168
|
-
prompter: interactive ? new ClackPrompter() : void 0
|
|
1169
|
-
});
|
|
1170
|
-
if (credentials.size > 0) log.success(`Resolved ${credentials.size} credential(s).`);
|
|
1171
|
-
if (deployArgs.destroy) {
|
|
1172
|
-
if (await DeployPhases.destroy(deployArgs, target, rootDir, credentials, interactive)) process.exitCode = 1;
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
let secrets;
|
|
1176
|
-
if (deployArgs["env-file"] !== void 0) {
|
|
1177
|
-
secrets = await EnvFileReader.read(resolve(rootDir, deployArgs["env-file"]));
|
|
1178
|
-
log.info(`Loaded ${secrets.size} secret(s) from ${deployArgs["env-file"]}.`);
|
|
1179
|
-
}
|
|
1180
|
-
if (!(deployArgs["skip-build"] || target.hostManaged === true)) {
|
|
1181
|
-
const injectable = DeployLedger.injectableEntries(await DeployLedger.read(rootDir, environment));
|
|
1182
|
-
if (injectable.size > 0) log.info(`Injecting ${injectable.size} deploy-ledger value(s) into the build: ${[...injectable.keys()].join(", ")}.`);
|
|
1183
|
-
}
|
|
1184
|
-
DeployPhases.logHarvest(await DeployPhases.harvest(rootDir), deployArgs.migrate);
|
|
1185
|
-
const migrations = await DeployPhases.composeMigrations(deployArgs, rootDir, secrets);
|
|
1186
|
-
if (migrations === null) {
|
|
1187
|
-
process.exitCode = 1;
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
const progress = CliSpinner.begin(interactive, "Preparing the deploy…");
|
|
1191
|
-
const onProgress = (event) => {
|
|
1192
|
-
if (progress === void 0) log.info(event.message);
|
|
1193
|
-
else progress.message(event.message);
|
|
1194
|
-
};
|
|
1195
|
-
let outcome;
|
|
1196
1470
|
try {
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
...deployArgs.profile === void 0 ? {} : { profile: deployArgs.profile },
|
|
1213
|
-
onProgress,
|
|
1214
|
-
...migrations === void 0 ? {} : { migrations }
|
|
1471
|
+
const target = await ConfigSidecar.during(rootDir, environment, () => DeployTargetResolver.resolve(rootDir, environment, deployArgs.platform));
|
|
1472
|
+
report.resolved(target);
|
|
1473
|
+
log.info(`Platform: ${target.platformName ?? "node (default)"} · target class: ${target.targetClass}`);
|
|
1474
|
+
if (target.hostManaged) log.info(`Host-managed build detected (${target.artifactDir}) — shipping it, skipping \`vp build\`.`);
|
|
1475
|
+
const profileMismatch = DeployArgumentGuards.profileMismatch(deployArgs.profile, target.platformName);
|
|
1476
|
+
if (profileMismatch !== void 0) {
|
|
1477
|
+
cancel(profileMismatch);
|
|
1478
|
+
process.exitCode = 1;
|
|
1479
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(CliDiagnosticCode.InvalidOptions, profileMismatch)]));
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const specs = CredentialSelection.inPlay(target.strategy.requiredCredentials(), { provisioning: deployArgs.provision });
|
|
1483
|
+
const credentials = await CredentialResolver.resolve(specs, {
|
|
1484
|
+
interactive: interactive || commandOutput.isJson() && !deployArgs["dry-run"],
|
|
1485
|
+
prompter: commandOutput.prompter(CredentialResolver.optionFor(specs))
|
|
1215
1486
|
});
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1487
|
+
report.resolvedCredentials(credentials);
|
|
1488
|
+
if (credentials.size > 0) log.success(`Resolved ${credentials.size} credential(s).`);
|
|
1489
|
+
if (deployArgs.destroy) {
|
|
1490
|
+
if (await DeployPhases.destroy(deployArgs, target, rootDir, credentials, commandOutput, report)) process.exitCode = 1;
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
let secrets;
|
|
1494
|
+
if (deployArgs["env-file"] !== void 0) {
|
|
1495
|
+
secrets = await EnvFileReader.read(resolve(rootDir, deployArgs["env-file"]));
|
|
1496
|
+
log.info(`Loaded ${secrets.size} secret(s) from ${deployArgs["env-file"]}.`);
|
|
1497
|
+
}
|
|
1498
|
+
const buildSkipped = deployArgs["skip-build"] || target.hostManaged === true;
|
|
1499
|
+
let ledgerValues = [];
|
|
1500
|
+
if (!buildSkipped) {
|
|
1501
|
+
const injectable = DeployLedger.injectableEntries(await DeployLedger.read(rootDir, environment));
|
|
1502
|
+
ledgerValues = [...injectable.keys()];
|
|
1503
|
+
if (injectable.size > 0) log.info(`Injecting ${injectable.size} deploy-ledger value(s) into the build: ${ledgerValues.join(", ")}.`);
|
|
1504
|
+
}
|
|
1505
|
+
DeployPhases.logHarvest(await DeployPhases.harvest(rootDir), deployArgs.migrate);
|
|
1506
|
+
const migrations = await DeployPhases.composeMigrations(deployArgs, rootDir, secrets);
|
|
1507
|
+
if (migrations === null) {
|
|
1508
|
+
process.exitCode = 1;
|
|
1509
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(DeployDiagnosticCode.NoMigrationConfig, DeployPhases.noMigrationConfig)]));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const progress = CliSpinner.begin(interactive, "Preparing the deploy…");
|
|
1513
|
+
const phases = [];
|
|
1514
|
+
const onProgress = (event) => {
|
|
1515
|
+
phases.push(event.phase);
|
|
1516
|
+
if (progress === void 0) log.info(event.message);
|
|
1517
|
+
else progress.message(event.message);
|
|
1518
|
+
};
|
|
1519
|
+
let outcome;
|
|
1520
|
+
try {
|
|
1521
|
+
outcome = await DeployRunner.run({
|
|
1522
|
+
target,
|
|
1523
|
+
rootDir,
|
|
1524
|
+
environment,
|
|
1525
|
+
credentials,
|
|
1526
|
+
dryRun: deployArgs["dry-run"],
|
|
1527
|
+
skipBuild: buildSkipped,
|
|
1528
|
+
...target.artifactDir === void 0 ? {} : { artifactDir: target.artifactDir },
|
|
1529
|
+
provision: deployArgs.provision,
|
|
1530
|
+
...secrets === void 0 ? {} : { secrets },
|
|
1531
|
+
...deployArgs.create === void 0 ? {} : { createTarget: {
|
|
1532
|
+
name: deployArgs.create,
|
|
1533
|
+
...deployArgs.team === void 0 ? {} : { team: deployArgs.team }
|
|
1534
|
+
} },
|
|
1535
|
+
verify: deployArgs.verify,
|
|
1536
|
+
...deployArgs.profile === void 0 ? {} : { profile: deployArgs.profile },
|
|
1537
|
+
onProgress,
|
|
1538
|
+
...migrations === void 0 ? {} : { migrations }
|
|
1539
|
+
});
|
|
1540
|
+
} catch (runError) {
|
|
1541
|
+
progress?.stop("Deploy failed — see the error below.");
|
|
1542
|
+
throw runError;
|
|
1543
|
+
}
|
|
1544
|
+
progress?.stop(outcome.halted ? "Stopped — details below." : "Deploy steps complete.");
|
|
1545
|
+
const succeeded = DeployReport.render(outcome, interactive);
|
|
1546
|
+
if (!succeeded) process.exitCode = 1;
|
|
1547
|
+
commandOutput.emit(report.document(succeeded, DeployReport.diagnostics(outcome, succeeded), { ship: DeployReport.ship(outcome, {
|
|
1548
|
+
phases,
|
|
1549
|
+
buildSkipped,
|
|
1550
|
+
ledgerValues,
|
|
1551
|
+
migrationTargets: migrations?.apply === true ? migrations.targets.map((migrationTarget) => migrationTarget.name) : []
|
|
1552
|
+
}) }));
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
if (DeployCommandFailure.report(error)) {
|
|
1555
|
+
const diagnostic = DeployCommandFailure.diagnosticOf(error, DeployDiagnosticCode);
|
|
1556
|
+
if (diagnostic !== void 0) commandOutput.emit(report.document(false, [diagnostic]));
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (error instanceof CommandFailure && commandOutput.isJson()) {
|
|
1560
|
+
commandOutput.emit(report.document(false, error.diagnostics));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
throw error;
|
|
1219
1564
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
//#endregion
|
|
1569
|
+
//#region src/doctor-json-document.ts
|
|
1570
|
+
/** Which audit set one `heximon doctor` run ran. */
|
|
1571
|
+
const DoctorMode = {
|
|
1572
|
+
/** The default run: the audits of an app and the libraries it consumes. */
|
|
1573
|
+
App: "app",
|
|
1574
|
+
/** `--self`: the pre-publish check of the current library package. */
|
|
1575
|
+
Self: "self"
|
|
1576
|
+
};
|
|
1577
|
+
/** The verdict on one audited package in a {@link DoctorJsonDocument}. */
|
|
1578
|
+
const DoctorPackageStatus = {
|
|
1579
|
+
/** The packed manifest matches the local source of the package. */
|
|
1580
|
+
Fresh: "fresh",
|
|
1581
|
+
/**
|
|
1582
|
+
* The local source changed since the last pack, some recorded source is gone, or a file the bundle read outside
|
|
1583
|
+
* the source changed or is gone.
|
|
1584
|
+
*/
|
|
1585
|
+
Stale: "stale",
|
|
1586
|
+
/** The packed manifest has no provenance stamp, so nothing was compared. */
|
|
1587
|
+
NoProvenance: "no-provenance",
|
|
1588
|
+
/** The package has no local source (a published package), so nothing was compared. */
|
|
1589
|
+
NoSource: "no-source",
|
|
1590
|
+
/** The package resolves to more than one physical copy. */
|
|
1591
|
+
Fork: "fork"
|
|
1592
|
+
};
|
|
1593
|
+
/** The outcome of the physical-copy census in a {@link DoctorJsonDocument}. */
|
|
1594
|
+
const DoctorCopyCensusStatus = {
|
|
1595
|
+
/** Every probed package resolves to one copy. */
|
|
1596
|
+
SingleCopies: "single-copies",
|
|
1597
|
+
/** At least one probed package resolves to more than one copy. */
|
|
1598
|
+
Forked: "forked",
|
|
1599
|
+
/** The census ran but found no `node_modules` to resolve from, so it could not look. */
|
|
1600
|
+
NothingProbed: "nothing-probed",
|
|
1601
|
+
/** `--self` found no pnpm workspace, so the census did not run. */
|
|
1602
|
+
NotRun: "not-run"
|
|
1603
|
+
};
|
|
1604
|
+
/**
|
|
1605
|
+
* The stable codes of the `heximon doctor` diagnostics. A `heximon.config.ts` that `--self` cannot load is the
|
|
1606
|
+
* shared `cli.config-unreadable`, as a warning: the run goes on without its `expectedOpenPeers`.
|
|
1607
|
+
*/
|
|
1608
|
+
const DoctorDiagnosticCode = {
|
|
1609
|
+
/** `@heximon/*` cannot be read from GitHub Packages. Error. */
|
|
1610
|
+
RegistryAccessFailed: "doctor.registry-access-failed",
|
|
1611
|
+
/** A packed library no longer matches its local source. Error. */
|
|
1612
|
+
StalePackage: "doctor.stale-package",
|
|
1613
|
+
/** A package resolves to more than one physical copy. Error when it fails the run, else warning. */
|
|
1614
|
+
PackageFork: "doctor.package-fork",
|
|
1615
|
+
/** The copy census found nothing to resolve from. Warning. */
|
|
1616
|
+
CopiesNotProbed: "doctor.copies-not-probed",
|
|
1617
|
+
/** The deployed wrangler config does not carry the tracing directives. Warning. */
|
|
1618
|
+
PlatformTracingNotCarried: "doctor.platform-tracing-not-carried",
|
|
1619
|
+
/** A `@heximon/*` peer of the dependency closure is declared nowhere in `package.json`. Warning. */
|
|
1620
|
+
PeerPivotUndeclared: "doctor.peer-pivot-undeclared",
|
|
1621
|
+
/** An `expectedOpenPeers` name matches no undeclared peer. Warning. */
|
|
1622
|
+
StalePeerAcknowledgement: "doctor.stale-peer-acknowledgement"
|
|
1623
|
+
};
|
|
1624
|
+
//#endregion
|
|
1625
|
+
//#region src/doctor-audit.ts
|
|
1626
|
+
/** Runs the `heximon doctor` audits, and owns the verdicts that the exit status and both renderers read. */
|
|
1627
|
+
var DoctorAudit = class DoctorAudit {
|
|
1628
|
+
/**
|
|
1629
|
+
* Run every audit of one mode. Read-only.
|
|
1630
|
+
*
|
|
1631
|
+
* @param rootDir - The absolute project root.
|
|
1632
|
+
* @param self - Run the `--self` pre-publish check instead of the app audits.
|
|
1633
|
+
* @returns The audit result.
|
|
1634
|
+
*/
|
|
1635
|
+
static async run(rootDir, self) {
|
|
1636
|
+
const registry = await new RegistryAccess().probe();
|
|
1637
|
+
return self ? DoctorAudit.runSelf(rootDir, registry) : DoctorAudit.runApp(rootDir, registry);
|
|
1638
|
+
}
|
|
1639
|
+
/** Whether the registry probe failed. */
|
|
1640
|
+
static isRegistryFailed(result) {
|
|
1641
|
+
return result.registry.status !== RegistryAccessStatus.Ok;
|
|
1642
|
+
}
|
|
1643
|
+
/** Whether the run has a finding that exits 1. */
|
|
1644
|
+
static isFailure(result) {
|
|
1645
|
+
if (DoctorAudit.isRegistryFailed(result)) return true;
|
|
1646
|
+
if (result.mode === DoctorMode.App) return DoctorAudit.staleCount(result.boundaries) > 0;
|
|
1647
|
+
return DoctorAudit.gatingForks(result).length > 0 || result.selfReport?.status === BoundaryProvenanceStatus.Stale;
|
|
1648
|
+
}
|
|
1649
|
+
/** The census reports of packages that resolve to more than one copy. */
|
|
1650
|
+
static forkedReports(audit) {
|
|
1651
|
+
return audit.reports.filter((report) => report.copies.length > 1);
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* The forks that fail a `--self` run: forks of a runtime-reachable package. A default run has none, because
|
|
1655
|
+
* its census is reported and never fails the run.
|
|
1656
|
+
*/
|
|
1657
|
+
static gatingForks(result) {
|
|
1658
|
+
if (result.mode === DoctorMode.App || result.copyAudit === void 0) return [];
|
|
1659
|
+
if (result.copyAudit.originsProbed === 0) return [];
|
|
1660
|
+
const gatingPackages = result.gatingPackages;
|
|
1661
|
+
return DoctorAudit.forkedReports(result.copyAudit).filter((report) => gatingPackages === void 0 || gatingPackages.has(report.packageName));
|
|
1662
|
+
}
|
|
1663
|
+
/** How many reports are stale. */
|
|
1664
|
+
static staleCount(reports) {
|
|
1665
|
+
return reports.filter((report) => report.status === BoundaryProvenanceStatus.Stale).length;
|
|
1666
|
+
}
|
|
1667
|
+
/** The closing sentence of the run. */
|
|
1668
|
+
static summary(result) {
|
|
1669
|
+
if (result.mode === DoctorMode.App) {
|
|
1670
|
+
if (result.boundaries.length === 0) return "No consumed Heximon library packages found — no staleness audit to run.";
|
|
1671
|
+
const staleCount = DoctorAudit.staleCount(result.boundaries);
|
|
1672
|
+
return staleCount > 0 ? `${staleCount} stale package(s) — rebuild them before trusting this app's build.` : `${result.boundaries.length} package(s) audited — no staleness detected.`;
|
|
1673
|
+
}
|
|
1674
|
+
const parts = [];
|
|
1675
|
+
const forked = DoctorAudit.gatingForks(result).length > 0;
|
|
1676
|
+
if (forked) {
|
|
1677
|
+
const hasConsumers = (result.workspaceConsumers?.consumers.length ?? 0) > 0;
|
|
1678
|
+
parts.push(hasConsumers ? "Forked against a workspace consumer — equalize the two packages' installed peer contexts (a one-sided cover can flip the fork, not remove it) and reinstall." : "Forked within this package's own resolution (no workspace consumers found to compare against) — align the versions its own dependency tree resolves and reinstall.");
|
|
1679
|
+
}
|
|
1680
|
+
if (result.selfReport === null) parts.push("This directory is not a packed Heximon library (no dist/heximon.manifest.json) — the peer-closure and copy census above still ran; run `vp pack` to enable the staleness check too.");
|
|
1681
|
+
else if (result.selfReport.status === BoundaryProvenanceStatus.Stale || !forked) parts.push(DoctorAudit.selfProvenanceSummary(result.selfReport));
|
|
1682
|
+
return parts.join(" ");
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* The package names a fork of which is RUNTIME-reachable from the library at `rootDir`: the library itself,
|
|
1686
|
+
* its installed `@heximon/*` dependency closure ({@link PeerClosureReport.closurePackages}), every named peer
|
|
1687
|
+
* pivot of that closure, the curated {@link RuntimeSingletons}, and every installed boundary package
|
|
1688
|
+
* ({@link PackageCopyAuditor.installedBoundaryPackages}). A fork of any other name is confined to build-time
|
|
1689
|
+
* tooling (for example a devDependency-only bundler plugin), so it is reported and does not fail the run.
|
|
1690
|
+
*/
|
|
1691
|
+
static runtimeReachablePackageNames(rootDir, closureReport) {
|
|
1692
|
+
const names = new Set(RuntimeSingletons.all());
|
|
1693
|
+
const libraryName = DoctorAudit.readPackageName(rootDir);
|
|
1694
|
+
if (libraryName !== null) names.add(libraryName);
|
|
1695
|
+
for (const packageName of PackageCopyAuditor.installedBoundaryPackages(rootDir)) names.add(packageName);
|
|
1696
|
+
if (closureReport !== null) {
|
|
1697
|
+
for (const packageName of closureReport.closurePackages) names.add(packageName);
|
|
1698
|
+
for (const pivot of closureReport.pivots) names.add(pivot.packageName);
|
|
1699
|
+
}
|
|
1700
|
+
return names;
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* The `--self` sentence for the own-dist verdict.
|
|
1704
|
+
*
|
|
1705
|
+
* Only {@link BoundaryProvenanceStatus.Fresh} compared anything. The other non-stale verdicts mean the
|
|
1706
|
+
* comparison did not run, so they must never read as a pass.
|
|
1707
|
+
*/
|
|
1708
|
+
static selfProvenanceSummary(report) {
|
|
1709
|
+
switch (report.status) {
|
|
1710
|
+
case BoundaryProvenanceStatus.Fresh: return "This package's dist is current with its src/.";
|
|
1711
|
+
case BoundaryProvenanceStatus.NoProvenance: return "Its packed manifest predates the provenance stamp, so nothing was compared — repack (`vp pack`) to enable the staleness check.";
|
|
1712
|
+
case BoundaryProvenanceStatus.NoSource: return "It ships no local src/, so its packed manifest was not compared with anything.";
|
|
1713
|
+
case BoundaryProvenanceStatus.Stale: return "Your dist is STALE — repack (`vp pack`) before publishing or consuming this package.";
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
/** The default run: the copy census, the platform-tracing check, then the staleness audit. */
|
|
1717
|
+
static async runApp(rootDir, registry) {
|
|
1718
|
+
const copyAudit = PackageCopyAuditor.audit(rootDir);
|
|
1719
|
+
const platformTracing = await PlatformTracingAuditor.audit(rootDir);
|
|
1720
|
+
const boundaries = await ConfigSidecar.during(rootDir, void 0, () => BoundaryProvenanceAuditor.audit(rootDir));
|
|
1721
|
+
return {
|
|
1722
|
+
mode: DoctorMode.App,
|
|
1723
|
+
rootDir,
|
|
1724
|
+
registry,
|
|
1725
|
+
copyAudit,
|
|
1726
|
+
platformTracing,
|
|
1727
|
+
boundaries
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
/** The `--self` run: the peer-closure roster, the library-side census, then the own-dist staleness check. */
|
|
1731
|
+
static async runSelf(rootDir, registry) {
|
|
1732
|
+
let configError;
|
|
1733
|
+
let configDiagnostic;
|
|
1734
|
+
let expectedOpenPeers = [];
|
|
1735
|
+
try {
|
|
1736
|
+
expectedOpenPeers = (await ConfigLoader.load(rootDir))?.expectedOpenPeers ?? [];
|
|
1222
1737
|
} catch (error) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1738
|
+
configError = error instanceof Error ? error.message : String(error);
|
|
1739
|
+
configDiagnostic = await ConfigSidecar.diagnosticOf(rootDir, error, DiagnosticSeverity.Warning, "so its `expectedOpenPeers` acknowledgements are not applied");
|
|
1740
|
+
}
|
|
1741
|
+
const closureReport = PeerClosureAuditor.audit(rootDir, expectedOpenPeers);
|
|
1742
|
+
const workspaceConsumers = WorkspaceConsumerLocator.locate(rootDir);
|
|
1743
|
+
const census = workspaceConsumers === null ? {} : {
|
|
1744
|
+
copyAudit: PackageCopyAuditor.auditLibrary(rootDir, workspaceConsumers.consumers),
|
|
1745
|
+
gatingPackages: DoctorAudit.runtimeReachablePackageNames(rootDir, closureReport)
|
|
1746
|
+
};
|
|
1747
|
+
const selfReport = await ConfigSidecar.during(rootDir, void 0, () => BoundaryProvenanceAuditor.auditSelf(rootDir));
|
|
1748
|
+
return {
|
|
1749
|
+
mode: DoctorMode.Self,
|
|
1750
|
+
rootDir,
|
|
1751
|
+
registry,
|
|
1752
|
+
...configError === void 0 ? {} : { configError },
|
|
1753
|
+
...configDiagnostic === void 0 ? {} : { configDiagnostic },
|
|
1754
|
+
closureReport,
|
|
1755
|
+
workspaceConsumers,
|
|
1756
|
+
...census,
|
|
1757
|
+
selfReport
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
/** The `name` field of a directory's `package.json`, or `null` when absent, unreadable or malformed. */
|
|
1761
|
+
static readPackageName(directory) {
|
|
1762
|
+
try {
|
|
1763
|
+
const parsed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
1764
|
+
if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
|
|
1765
|
+
const { name } = parsed;
|
|
1766
|
+
return typeof name === "string" ? name : null;
|
|
1767
|
+
}
|
|
1768
|
+
} catch {
|
|
1769
|
+
return null;
|
|
1225
1770
|
}
|
|
1771
|
+
return null;
|
|
1226
1772
|
}
|
|
1227
|
-
}
|
|
1773
|
+
};
|
|
1774
|
+
/** Renders a {@link DoctorAuditResult} as the `heximon doctor --json` document. */
|
|
1775
|
+
var DoctorJsonReport = class DoctorJsonReport {
|
|
1776
|
+
/** The fix of a fork: no single command removes one. */
|
|
1777
|
+
static forkHint = "Deduplicate to one copy (align versions, or hoist it).";
|
|
1778
|
+
/** Shell words that need no quotes. */
|
|
1779
|
+
static plainShellWord = /^[\w./@+:=,-]+$/;
|
|
1780
|
+
/**
|
|
1781
|
+
* Build the document of one run.
|
|
1782
|
+
*
|
|
1783
|
+
* @param result - The audit result.
|
|
1784
|
+
* @returns The document. Its `ok` is `false` exactly when {@link DoctorAudit.isFailure} is `true`.
|
|
1785
|
+
*/
|
|
1786
|
+
static toJson(result) {
|
|
1787
|
+
const packages = DoctorJsonReport.packages(result);
|
|
1788
|
+
const diagnostics = [
|
|
1789
|
+
...DoctorJsonReport.registryDiagnostics(result),
|
|
1790
|
+
...DoctorJsonReport.modeDiagnostics(result),
|
|
1791
|
+
...DoctorJsonReport.packageDiagnostics(packages)
|
|
1792
|
+
];
|
|
1793
|
+
const common = {
|
|
1794
|
+
ok: !DoctorAudit.isFailure(result),
|
|
1795
|
+
diagnostics,
|
|
1796
|
+
mode: result.mode,
|
|
1797
|
+
rootDir: result.rootDir,
|
|
1798
|
+
registry: DoctorJsonReport.registry(result.registry),
|
|
1799
|
+
packages,
|
|
1800
|
+
packageCopies: DoctorJsonReport.copyCensus(result),
|
|
1801
|
+
summary: DoctorAudit.summary(result)
|
|
1802
|
+
};
|
|
1803
|
+
if (result.mode === DoctorMode.App) {
|
|
1804
|
+
const tracing = result.platformTracing;
|
|
1805
|
+
return {
|
|
1806
|
+
...common,
|
|
1807
|
+
platformTracing: {
|
|
1808
|
+
status: tracing.status,
|
|
1809
|
+
...tracing.wranglerConfigPath === void 0 ? {} : { wranglerConfigPath: tracing.wranglerConfigPath },
|
|
1810
|
+
tracingNotEnabled: tracing.tracingNotEnabled,
|
|
1811
|
+
...tracing.droppedSamplerRatio === void 0 ? {} : { droppedSamplerRatio: tracing.droppedSamplerRatio }
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const closure = result.closureReport;
|
|
1816
|
+
return {
|
|
1817
|
+
...common,
|
|
1818
|
+
...closure === null ? {} : { peerClosure: {
|
|
1819
|
+
closurePackages: closure.closurePackages,
|
|
1820
|
+
pivots: closure.pivots.map((pivot) => DoctorJsonReport.pivot(pivot)),
|
|
1821
|
+
uncovered: closure.uncovered.map((pivot) => DoctorJsonReport.pivot(pivot)),
|
|
1822
|
+
expectedOpen: closure.expectedOpen.map((pivot) => DoctorJsonReport.pivot(pivot)),
|
|
1823
|
+
staleExpectedOpen: closure.staleExpectedOpen
|
|
1824
|
+
} },
|
|
1825
|
+
packed: result.selfReport !== null
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
/** The registry section. */
|
|
1829
|
+
static registry(report) {
|
|
1830
|
+
return {
|
|
1831
|
+
registry: RegistryAccess.gitHubPackagesRegistry,
|
|
1832
|
+
probePackage: RegistryAccess.probePackage,
|
|
1833
|
+
status: report.status,
|
|
1834
|
+
...report.detail === void 0 ? {} : { detail: report.detail },
|
|
1835
|
+
...report.status === RegistryAccessStatus.Ok ? {} : { instructions: RegistryAccess.fixInstructions(report.status) }
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
/** The census section. */
|
|
1839
|
+
static copyCensus(result) {
|
|
1840
|
+
const workspace = result.mode === DoctorMode.Self && result.workspaceConsumers !== null ? {
|
|
1841
|
+
workspaceRoot: result.workspaceConsumers.workspaceRoot,
|
|
1842
|
+
consumers: result.workspaceConsumers.consumers.map((consumer) => consumer.name)
|
|
1843
|
+
} : {};
|
|
1844
|
+
if (result.copyAudit === void 0) return {
|
|
1845
|
+
status: DoctorCopyCensusStatus.NotRun,
|
|
1846
|
+
originsProbed: 0,
|
|
1847
|
+
packagesProbed: 0,
|
|
1848
|
+
forkedPackages: 0,
|
|
1849
|
+
...workspace
|
|
1850
|
+
};
|
|
1851
|
+
const forkedPackages = result.copyAudit.originsProbed === 0 ? 0 : DoctorAudit.forkedReports(result.copyAudit).length;
|
|
1852
|
+
let status = DoctorCopyCensusStatus.SingleCopies;
|
|
1853
|
+
if (result.copyAudit.originsProbed === 0) status = DoctorCopyCensusStatus.NothingProbed;
|
|
1854
|
+
else if (forkedPackages > 0) status = DoctorCopyCensusStatus.Forked;
|
|
1855
|
+
return {
|
|
1856
|
+
status,
|
|
1857
|
+
originsProbed: result.copyAudit.originsProbed,
|
|
1858
|
+
packagesProbed: result.copyAudit.packagesProbed,
|
|
1859
|
+
forkedPackages,
|
|
1860
|
+
...workspace
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
/** Every audited package, in the order of the text report: the forks first, then the staleness verdicts. */
|
|
1864
|
+
static packages(result) {
|
|
1865
|
+
const packages = [];
|
|
1866
|
+
if (result.copyAudit !== void 0 && result.copyAudit.originsProbed > 0) {
|
|
1867
|
+
const gating = new Set(DoctorAudit.gatingForks(result));
|
|
1868
|
+
for (const report of DoctorAudit.forkedReports(result.copyAudit)) packages.push(DoctorJsonReport.forkEntry(report, gating.has(report)));
|
|
1869
|
+
}
|
|
1870
|
+
if (result.mode === DoctorMode.App) for (const report of result.boundaries) packages.push(DoctorJsonReport.staleness(report, false));
|
|
1871
|
+
else if (result.selfReport !== null) packages.push(DoctorJsonReport.staleness(result.selfReport, true));
|
|
1872
|
+
return packages;
|
|
1873
|
+
}
|
|
1874
|
+
/** One fork entry. */
|
|
1875
|
+
static forkEntry(report, failing) {
|
|
1876
|
+
return {
|
|
1877
|
+
name: report.packageName,
|
|
1878
|
+
status: DoctorPackageStatus.Fork,
|
|
1879
|
+
reason: `${String(report.copies.length)} physical copies. Class identity is the DI token, so these bind nominally yet split at runtime.`,
|
|
1880
|
+
hint: DoctorJsonReport.forkHint,
|
|
1881
|
+
self: false,
|
|
1882
|
+
failing,
|
|
1883
|
+
copies: report.copies.map((copy) => ({
|
|
1884
|
+
directory: copy.directory,
|
|
1885
|
+
...copy.version === void 0 ? {} : { version: copy.version },
|
|
1886
|
+
origins: copy.origins
|
|
1887
|
+
}))
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
/** One staleness entry. `self` marks the package that `--self` audits. */
|
|
1891
|
+
static staleness(report, self) {
|
|
1892
|
+
const freshness = report;
|
|
1893
|
+
const repack = `cd ${DoctorJsonReport.shellWord(report.packageDirectory)} && vp pack`;
|
|
1894
|
+
const details = {
|
|
1895
|
+
self,
|
|
1896
|
+
...report.packageVersion === void 0 ? {} : { version: report.packageVersion },
|
|
1897
|
+
directory: report.packageDirectory,
|
|
1898
|
+
...report.manifestSourceHash === void 0 ? {} : { manifestSourceHash: report.manifestSourceHash },
|
|
1899
|
+
...report.recomputedSourceHash === void 0 ? {} : { recomputedSourceHash: report.recomputedSourceHash },
|
|
1900
|
+
...report.missingSource === void 0 ? {} : { missingSource: report.missingSource },
|
|
1901
|
+
...freshness.currentSource === void 0 ? {} : { currentSource: freshness.currentSource },
|
|
1902
|
+
...report.manifestBundleInputHash === void 0 ? {} : { manifestBundleInputHash: report.manifestBundleInputHash },
|
|
1903
|
+
...report.recomputedBundleInputHash === void 0 ? {} : { recomputedBundleInputHash: report.recomputedBundleInputHash },
|
|
1904
|
+
...report.missingBundleInputs === void 0 ? {} : { missingBundleInputs: report.missingBundleInputs }
|
|
1905
|
+
};
|
|
1906
|
+
switch (report.status) {
|
|
1907
|
+
case BoundaryProvenanceStatus.Fresh: return {
|
|
1908
|
+
name: report.specifier,
|
|
1909
|
+
status: DoctorPackageStatus.Fresh,
|
|
1910
|
+
reason: `The packed manifest matches the local source (${String(report.manifestSourceHash)}).`,
|
|
1911
|
+
failing: false,
|
|
1912
|
+
...details
|
|
1913
|
+
};
|
|
1914
|
+
case BoundaryProvenanceStatus.Stale: return {
|
|
1915
|
+
name: report.specifier,
|
|
1916
|
+
status: DoctorPackageStatus.Stale,
|
|
1917
|
+
reason: DoctorJsonReport.staleReason(freshness),
|
|
1918
|
+
hint: self ? "Repack (`vp pack`) before publishing or consuming it." : `Rebuild it (\`vp pack\` in ${report.packageDirectory}) before trusting this app's build.`,
|
|
1919
|
+
fix: repack,
|
|
1920
|
+
failing: true,
|
|
1921
|
+
...details
|
|
1922
|
+
};
|
|
1923
|
+
case BoundaryProvenanceStatus.NoProvenance: return {
|
|
1924
|
+
name: report.specifier,
|
|
1925
|
+
status: DoctorPackageStatus.NoProvenance,
|
|
1926
|
+
reason: "The packed manifest predates the provenance stamp, so nothing was compared.",
|
|
1927
|
+
hint: "Repack (`vp pack`) to enable the staleness check.",
|
|
1928
|
+
fix: repack,
|
|
1929
|
+
failing: false,
|
|
1930
|
+
...details
|
|
1931
|
+
};
|
|
1932
|
+
case BoundaryProvenanceStatus.NoSource: return {
|
|
1933
|
+
name: report.specifier,
|
|
1934
|
+
status: DoctorPackageStatus.NoSource,
|
|
1935
|
+
reason: "A published package with no local src/, so its provenance is display-only.",
|
|
1936
|
+
failing: false,
|
|
1937
|
+
...details
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/** Why a stale report is stale, in the words and the order of the text report. */
|
|
1942
|
+
static staleReason(report) {
|
|
1943
|
+
if (report.currentSource !== void 0) return `The source folders it was packed from (${(report.missingSource ?? []).join(", ")}) no longer exist, and its source is now in ${report.currentSource}.`;
|
|
1944
|
+
if (report.missingBundleInputs !== void 0) return `Files its bundle read outside its source (${report.missingBundleInputs.join(", ")}) are no longer on disk.`;
|
|
1945
|
+
if (report.recomputedBundleInputHash !== void 0) return `A file its bundle read outside its source changed: the bundle inputs (${report.recomputedBundleInputHash}) no longer match the packed manifest (${String(report.manifestBundleInputHash)}).`;
|
|
1946
|
+
if (report.missingSource !== void 0 && report.missingSource.length > 0) return `The source it was packed from (${report.missingSource.join(", ")}) is no longer on disk.`;
|
|
1947
|
+
return `Its src/ (${String(report.recomputedSourceHash)}) no longer matches the packed manifest (${String(report.manifestSourceHash)}).`;
|
|
1948
|
+
}
|
|
1949
|
+
/** The registry diagnostic, when the probe failed. */
|
|
1950
|
+
static registryDiagnostics(result) {
|
|
1951
|
+
if (!DoctorAudit.isRegistryFailed(result)) return [];
|
|
1952
|
+
return [CliDiagnostics.error(DoctorDiagnosticCode.RegistryAccessFailed, `@heximon/* cannot be read from GitHub Packages (${result.registry.status}).`, { hint: RegistryAccess.fixInstructions(result.registry.status) })];
|
|
1953
|
+
}
|
|
1954
|
+
/** The advisory diagnostics of the mode: the census, the tracing check, the roster and the sidecar. */
|
|
1955
|
+
static modeDiagnostics(result) {
|
|
1956
|
+
const diagnostics = [];
|
|
1957
|
+
if (result.mode === DoctorMode.Self && result.configDiagnostic !== void 0) diagnostics.push(result.configDiagnostic);
|
|
1958
|
+
if (result.mode === DoctorMode.Self && result.closureReport !== null) {
|
|
1959
|
+
for (const pivot of result.closureReport.uncovered) diagnostics.push(DoctorJsonReport.warning(DoctorDiagnosticCode.PeerPivotUndeclared, `${pivot.packageName} is a peer of ${pivot.declaredBy.join(", ")}, and this package's package.json declares it nowhere.`, "Equalize the installed peer sets of this package's consumers, or acknowledge the name in `expectedOpenPeers` (heximon.config.ts)."));
|
|
1960
|
+
for (const name of result.closureReport.staleExpectedOpen) diagnostics.push(DoctorJsonReport.warning(DoctorDiagnosticCode.StalePeerAcknowledgement, `expectedOpenPeers names ${name}, which matches no undeclared peer.`, "Remove the name from `expectedOpenPeers` (heximon.config.ts)."));
|
|
1961
|
+
}
|
|
1962
|
+
if (result.copyAudit !== void 0 && result.copyAudit.originsProbed === 0) diagnostics.push(DoctorJsonReport.warning(DoctorDiagnosticCode.CopiesNotProbed, "The copy census found no resolvable node_modules from this directory, so no fork check ran.", "Install the dependencies, then run heximon doctor again."));
|
|
1963
|
+
if (result.mode === DoctorMode.App && result.platformTracing.status === PlatformTracingStatus.NotCarried) diagnostics.push(DoctorJsonReport.tracingDiagnostic(result.platformTracing));
|
|
1964
|
+
return diagnostics;
|
|
1965
|
+
}
|
|
1966
|
+
/** One diagnostic per failing or forked package, in package order. */
|
|
1967
|
+
static packageDiagnostics(packages) {
|
|
1968
|
+
const diagnostics = [];
|
|
1969
|
+
for (const entry of packages) if (entry.status === DoctorPackageStatus.Stale) diagnostics.push(CliDiagnostics.error(DoctorDiagnosticCode.StalePackage, `${entry.name}: ${entry.reason}`, entry.hint === void 0 ? {} : { hint: entry.hint }));
|
|
1970
|
+
else if (entry.status === DoctorPackageStatus.Fork) {
|
|
1971
|
+
const message = `${entry.name} resolves to ${String(entry.copies?.length ?? 0)} physical copies.`;
|
|
1972
|
+
diagnostics.push(entry.failing ? CliDiagnostics.error(DoctorDiagnosticCode.PackageFork, message, { hint: DoctorJsonReport.forkHint }) : DoctorJsonReport.warning(DoctorDiagnosticCode.PackageFork, message, DoctorJsonReport.forkHint));
|
|
1973
|
+
}
|
|
1974
|
+
return diagnostics;
|
|
1975
|
+
}
|
|
1976
|
+
/** The platform-tracing warning. */
|
|
1977
|
+
static tracingDiagnostic(report) {
|
|
1978
|
+
const target = report.wranglerConfigPath ?? "the committed wrangler config";
|
|
1979
|
+
const hints = [];
|
|
1980
|
+
if (report.tracingNotEnabled) hints.push(`Add "observability": { "traces": { "enabled": true } } to ${target} by hand.`);
|
|
1981
|
+
if (report.droppedSamplerRatio !== void 0) hints.push(`Set observability.traces.head_sampling_rate to ${String(report.droppedSamplerRatio)} in ${target} by hand.`);
|
|
1982
|
+
return DoctorJsonReport.warning(DoctorDiagnosticCode.PlatformTracingNotCarried, "otel.exporter \"platform\" writes its tracing directives into a generated wrangler config, but this host-managed Cloudflare build produced none, so the deployed worker does not get them.", hints.join(" "));
|
|
1983
|
+
}
|
|
1984
|
+
/** One warning diagnostic. */
|
|
1985
|
+
static warning(code, message, hint) {
|
|
1986
|
+
return {
|
|
1987
|
+
severity: DiagnosticSeverity.Warning,
|
|
1988
|
+
code,
|
|
1989
|
+
message,
|
|
1990
|
+
...hint === "" ? {} : { hint }
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
/** One pivot, copied out of the deploy report. */
|
|
1994
|
+
static pivot(pivot) {
|
|
1995
|
+
return {
|
|
1996
|
+
packageName: pivot.packageName,
|
|
1997
|
+
declaredBy: pivot.declaredBy
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
/** A path as one POSIX shell word: as it is when it is plain, else in single quotes. */
|
|
2001
|
+
static shellWord(path) {
|
|
2002
|
+
return DoctorJsonReport.plainShellWord.test(path) ? path : `'${path.replaceAll("'", "'\\''")}'`;
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
1228
2005
|
//#endregion
|
|
1229
2006
|
//#region src/doctor-command.ts
|
|
1230
|
-
/** The
|
|
2007
|
+
/** The text renderer behind {@link doctorCommand}: every audit of a {@link DoctorAuditResult} as clack lines. */
|
|
1231
2008
|
var DoctorReport = class DoctorReport {
|
|
2009
|
+
/**
|
|
2010
|
+
* Print the whole report of one run, ending with its summary as the outro.
|
|
2011
|
+
*
|
|
2012
|
+
* @param result - The audit result.
|
|
2013
|
+
*/
|
|
2014
|
+
static render(result) {
|
|
2015
|
+
if (DoctorAudit.isRegistryFailed(result)) log.error(RegistryAccess.fixInstructions(result.registry.status));
|
|
2016
|
+
else log.success("✔ GitHub Packages — @heximon/* readable");
|
|
2017
|
+
if (result.mode === DoctorMode.Self) {
|
|
2018
|
+
if (result.configError !== void 0) log.warn(`heximon.config.ts could not be loaded, so its \`expectedOpenPeers\` acknowledgements are not applied — every open pivot below is reported: ${result.configError}`);
|
|
2019
|
+
DoctorReport.logPeerClosureRoster(result.closureReport);
|
|
2020
|
+
if (result.copyAudit === void 0) log.info("Package copies — no pnpm workspace found from this directory; census not run.");
|
|
2021
|
+
else DoctorReport.logCopyAudit(result.copyAudit, DoctorAudit.gatingForks(result));
|
|
2022
|
+
if (result.selfReport !== null) DoctorReport.logBoundary(result.selfReport, true);
|
|
2023
|
+
} else {
|
|
2024
|
+
DoctorReport.logCopyAudit(result.copyAudit);
|
|
2025
|
+
DoctorReport.logPlatformTracing(result.platformTracing);
|
|
2026
|
+
for (const report of result.boundaries) DoctorReport.logBoundary(report);
|
|
2027
|
+
}
|
|
2028
|
+
outro(DoctorAudit.summary(result));
|
|
2029
|
+
}
|
|
1232
2030
|
/** Renders one audited boundary as a status line. `self` reframes the wording for the current package. */
|
|
1233
2031
|
static boundaryLine(report, self = false) {
|
|
1234
2032
|
const name = self ? `${report.specifier} (this package)` : report.specifier;
|
|
1235
2033
|
const version = report.packageVersion === void 0 ? "" : ` v${report.packageVersion}`;
|
|
1236
2034
|
switch (report.status) {
|
|
1237
2035
|
case BoundaryProvenanceStatus.Fresh: return self ? `✔ ${name}${version} — dist is current with src/ (${report.manifestSourceHash})` : `✔ ${name}${version} — up to date (${report.manifestSourceHash})`;
|
|
1238
|
-
case BoundaryProvenanceStatus.Stale: return self && report.missingSource === void 0 ? `✖ ${name}${version} — STALE: your src/ (${report.recomputedSourceHash}) no longer matches your own dist manifest (${report.manifestSourceHash}). Repack (\`vp pack\`) before publishing or consuming it.` : LibraryFreshness.staleLine(self ? name : report.specifier, report);
|
|
2036
|
+
case BoundaryProvenanceStatus.Stale: return self && report.missingSource === void 0 && !LibraryFreshness.isBundleStale(report) ? `✖ ${name}${version} — STALE: your src/ (${report.recomputedSourceHash}) no longer matches your own dist manifest (${report.manifestSourceHash}). Repack (\`vp pack\`) before publishing or consuming it.` : LibraryFreshness.staleLine(self ? name : report.specifier, report);
|
|
1239
2037
|
case BoundaryProvenanceStatus.NoProvenance: return `● ${name}${version} — manifest predates the provenance stamp; repack to enable the check.`;
|
|
1240
2038
|
case BoundaryProvenanceStatus.NoSource: return `● ${name}${version} — published package (no local src/); provenance is display-only.`;
|
|
1241
2039
|
}
|
|
@@ -1248,38 +2046,20 @@ var DoctorReport = class DoctorReport {
|
|
|
1248
2046
|
else log.info(line);
|
|
1249
2047
|
}
|
|
1250
2048
|
/**
|
|
1251
|
-
* The `--self` outro sentence for a NON-stale own-dist verdict.
|
|
1252
|
-
*
|
|
1253
|
-
* Only {@link BoundaryProvenanceStatus.Fresh} actually compared anything. The other two verdicts mean the
|
|
1254
|
-
* comparison did not run at all, and summarising them as "dist is current with its src/" would report a
|
|
1255
|
-
* check that never happened as a pass — the one thing a pre-publish gate must never do.
|
|
1256
|
-
*/
|
|
1257
|
-
static selfProvenanceSummary(report) {
|
|
1258
|
-
switch (report.status) {
|
|
1259
|
-
case BoundaryProvenanceStatus.Fresh: return "This package's dist is current with its src/.";
|
|
1260
|
-
case BoundaryProvenanceStatus.NoProvenance: return "Its packed manifest predates the provenance stamp, so nothing was compared — repack (`vp pack`) to enable the staleness check.";
|
|
1261
|
-
case BoundaryProvenanceStatus.NoSource: return "It ships no local src/, so its packed manifest was not compared with anything.";
|
|
1262
|
-
case BoundaryProvenanceStatus.Stale: return "Your dist is STALE — repack (`vp pack`) before publishing or consuming this package.";
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
2049
|
* Logs the physical-copy census: a line per forked package naming every copy and who resolves to it, or one
|
|
1267
2050
|
* summary line when nothing forked. The summary always states how many origins were probed — an audit that
|
|
1268
|
-
* could not look at anything must not read as a clean bill of health. `
|
|
1269
|
-
*
|
|
1270
|
-
* printed above either way, and a fork outside the set gets one extra line noting it is reported, not gated.
|
|
1271
|
-
*
|
|
1272
|
-
* @returns Whether any GATING fork was found (any fork at all, when `gatingPackages` is omitted).
|
|
2051
|
+
* could not look at anything must not read as a clean bill of health. `gatingForks`, given by `--self`, are
|
|
2052
|
+
* the forks that fail the run ({@link DoctorAudit.gatingForks}); every fork is printed either way.
|
|
1273
2053
|
*/
|
|
1274
|
-
static logCopyAudit(audit,
|
|
1275
|
-
const forked =
|
|
2054
|
+
static logCopyAudit(audit, gatingForks) {
|
|
2055
|
+
const forked = DoctorAudit.forkedReports(audit);
|
|
1276
2056
|
if (audit.originsProbed === 0) {
|
|
1277
2057
|
log.warn("Package copies — nothing probed (no resolvable node_modules from this directory), so no fork check ran.");
|
|
1278
|
-
return
|
|
2058
|
+
return;
|
|
1279
2059
|
}
|
|
1280
2060
|
if (forked.length === 0) {
|
|
1281
2061
|
log.success(`Package copies — ${String(audit.packagesProbed)} package(s) probed from ${String(audit.originsProbed)} origin(s); each resolves to a single copy.`);
|
|
1282
|
-
return
|
|
2062
|
+
return;
|
|
1283
2063
|
}
|
|
1284
2064
|
for (const report of forked) {
|
|
1285
2065
|
const copies = report.copies.map((copy) => {
|
|
@@ -1287,17 +2067,15 @@ var DoctorReport = class DoctorReport {
|
|
|
1287
2067
|
}).join("\n");
|
|
1288
2068
|
log.error(`${report.packageName} — ${String(report.copies.length)} physical copies:\n${copies}\n Class identity is the DI token, so these bind nominally yet split at runtime. Deduplicate to one copy (align versions, or hoist it).`);
|
|
1289
2069
|
}
|
|
1290
|
-
|
|
1291
|
-
if (gatingPackages !== void 0 && gatingForks.length === 0) {
|
|
1292
|
-
log.info(`${String(forked.length)} forked package(s) across ${String(audit.originsProbed)} origin(s) sit outside this package's runtime closure (build-time tooling) — reported, not gated.`);
|
|
1293
|
-
return false;
|
|
1294
|
-
}
|
|
1295
|
-
if (gatingPackages === void 0) {
|
|
2070
|
+
if (gatingForks === void 0) {
|
|
1296
2071
|
log.info(`${String(forked.length)} forked package(s) across ${String(audit.originsProbed)} origin(s). Reported, not gated — this does not fail the command.`);
|
|
1297
|
-
return
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
if (gatingForks.length === 0) {
|
|
2075
|
+
log.info(`${String(forked.length)} forked package(s) across ${String(audit.originsProbed)} origin(s) sit outside this package's runtime closure (build-time tooling) — reported, not gated.`);
|
|
2076
|
+
return;
|
|
1298
2077
|
}
|
|
1299
2078
|
log.info(`${String(forked.length)} forked package(s) across ${String(audit.originsProbed)} origin(s), ${String(gatingForks.length)} within this package's runtime closure — reported, and gating this command's exit code.`);
|
|
1300
|
-
return true;
|
|
1301
2079
|
}
|
|
1302
2080
|
/**
|
|
1303
2081
|
* Logs the `@heximon/*` peer-pivot roster ({@link PeerClosureAuditor}): an info line for pivots acknowledged
|
|
@@ -1337,40 +2115,6 @@ var DoctorReport = class DoctorReport {
|
|
|
1337
2115
|
if (report.droppedSamplerRatio !== void 0) lines.push(` \`otel.samplerRatio\` (${String(report.droppedSamplerRatio)}) maps onto the generated config's \`observability.traces.head_sampling_rate\` only, so it does nothing here — carry it in the same section by hand.`);
|
|
1338
2116
|
log.warn(lines.join("\n"));
|
|
1339
2117
|
}
|
|
1340
|
-
/**
|
|
1341
|
-
* The package names a fork of which is RUNTIME-reachable from the library at `rootDir`: the library itself,
|
|
1342
|
-
* every package its installed `@heximon/*` dependency closure actually resolved
|
|
1343
|
-
* ({@link PeerClosureReport.closurePackages}), every named peer pivot of that closure
|
|
1344
|
-
* ({@link PeerClosureReport.pivots}), the curated {@link RuntimeSingletons}, and every installed boundary
|
|
1345
|
-
* package ({@link PackageCopyAuditor.installedBoundaryPackages}) — a fork of a non-`@heximon` boundary
|
|
1346
|
-
* package (e.g. a consumed `@acme/*` library) is exactly as much a DI-identity split as a fork of a
|
|
1347
|
-
* framework package, since the rule is not scope-specific. A fork of a name outside this set is confined to
|
|
1348
|
-
* build-time tooling (e.g. a devDependency-only bundler/pack plugin) and must be reported, never gated.
|
|
1349
|
-
*/
|
|
1350
|
-
static runtimeReachablePackageNames(rootDir, closureReport) {
|
|
1351
|
-
const names = new Set(RuntimeSingletons.all());
|
|
1352
|
-
const libraryName = DoctorReport.readPackageName(rootDir);
|
|
1353
|
-
if (libraryName !== null) names.add(libraryName);
|
|
1354
|
-
for (const packageName of PackageCopyAuditor.installedBoundaryPackages(rootDir)) names.add(packageName);
|
|
1355
|
-
if (closureReport !== null) {
|
|
1356
|
-
for (const packageName of closureReport.closurePackages) names.add(packageName);
|
|
1357
|
-
for (const pivot of closureReport.pivots) names.add(pivot.packageName);
|
|
1358
|
-
}
|
|
1359
|
-
return names;
|
|
1360
|
-
}
|
|
1361
|
-
/** The `name` field of a directory's `package.json`, or `null` when absent/unreadable/malformed. */
|
|
1362
|
-
static readPackageName(directory) {
|
|
1363
|
-
try {
|
|
1364
|
-
const parsed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
1365
|
-
if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
|
|
1366
|
-
const { name } = parsed;
|
|
1367
|
-
return typeof name === "string" ? name : null;
|
|
1368
|
-
}
|
|
1369
|
-
} catch {
|
|
1370
|
-
return null;
|
|
1371
|
-
}
|
|
1372
|
-
return null;
|
|
1373
|
-
}
|
|
1374
2118
|
};
|
|
1375
2119
|
/**
|
|
1376
2120
|
* `heximon doctor` — three read-only audits.
|
|
@@ -1413,6 +2157,9 @@ var DoctorReport = class DoctorReport {
|
|
|
1413
2157
|
* Compile-free and filesystem-local (the `heximon.config.ts` sidecar is loaded only to read
|
|
1414
2158
|
* `expectedOpenPeers`); exits non-zero when this package forks a runtime-reachable package against a
|
|
1415
2159
|
* workspace consumer OR its own dist is stale.
|
|
2160
|
+
*
|
|
2161
|
+
* `--json` prints the same audit result as one `DoctorJsonDocument` on stdout, with each finding as a
|
|
2162
|
+
* diagnostic. Both modes exit 1 exactly when the document has `ok: false`.
|
|
1416
2163
|
*/
|
|
1417
2164
|
const doctorCommand = defineCommand({
|
|
1418
2165
|
meta: {
|
|
@@ -1425,6 +2172,7 @@ const doctorCommand = defineCommand({
|
|
|
1425
2172
|
description: "Project root directory.",
|
|
1426
2173
|
default: "."
|
|
1427
2174
|
},
|
|
2175
|
+
json: CommandOutput.jsonFlag,
|
|
1428
2176
|
self: {
|
|
1429
2177
|
type: "boolean",
|
|
1430
2178
|
description: "Library pre-publish check: this package's own peer-closure roster (advisory), its copy census against workspace consumers, and its packed manifest vs its own src/. Exits non-zero on a fork of a runtime-reachable package (its dependency closure, a named pivot, a curated runtime singleton, an installed boundary package, or itself) or a stale dist — a fork confined to build-time tooling is reported but does not gate.",
|
|
@@ -1432,112 +2180,107 @@ const doctorCommand = defineCommand({
|
|
|
1432
2180
|
}
|
|
1433
2181
|
},
|
|
1434
2182
|
async run({ args }) {
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
return [];
|
|
1445
|
-
});
|
|
1446
|
-
const closureReport = PeerClosureAuditor.audit(rootDir, expectedOpenPeers);
|
|
1447
|
-
DoctorReport.logPeerClosureRoster(closureReport);
|
|
1448
|
-
const workspaceConsumers = WorkspaceConsumerLocator.locate(rootDir);
|
|
1449
|
-
let forked = false;
|
|
1450
|
-
if (workspaceConsumers === null) log.info("Package copies — no pnpm workspace found from this directory; census not run.");
|
|
1451
|
-
else forked = DoctorReport.logCopyAudit(PackageCopyAuditor.auditLibrary(rootDir, workspaceConsumers.consumers), DoctorReport.runtimeReachablePackageNames(rootDir, closureReport));
|
|
1452
|
-
const selfReport = await BoundaryProvenanceAuditor.auditSelf(rootDir);
|
|
1453
|
-
const outroParts = [];
|
|
1454
|
-
let exitFailure = forked || registryFailed;
|
|
1455
|
-
if (forked) {
|
|
1456
|
-
const hasConsumers = (workspaceConsumers?.consumers.length ?? 0) > 0;
|
|
1457
|
-
outroParts.push(hasConsumers ? "Forked against a workspace consumer — equalize the two packages' installed peer contexts (a one-sided cover can flip the fork, not remove it) and reinstall." : "Forked within this package's own resolution (no workspace consumers found to compare against) — align the versions its own dependency tree resolves and reinstall.");
|
|
1458
|
-
}
|
|
1459
|
-
if (selfReport === null) outroParts.push("This directory is not a packed Heximon library (no dist/heximon.manifest.json) — the peer-closure and copy census above still ran; run `vp pack` to enable the staleness check too.");
|
|
1460
|
-
else {
|
|
1461
|
-
DoctorReport.logBoundary(selfReport, true);
|
|
1462
|
-
if (selfReport.status === BoundaryProvenanceStatus.Stale) {
|
|
1463
|
-
exitFailure = true;
|
|
1464
|
-
outroParts.push(DoctorReport.selfProvenanceSummary(selfReport));
|
|
1465
|
-
} else if (!forked) outroParts.push(DoctorReport.selfProvenanceSummary(selfReport));
|
|
1466
|
-
}
|
|
1467
|
-
if (exitFailure) process.exitCode = 1;
|
|
1468
|
-
outro(outroParts.join(" "));
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
DoctorReport.logCopyAudit(PackageCopyAuditor.audit(rootDir));
|
|
1472
|
-
DoctorReport.logPlatformTracing(await PlatformTracingAuditor.audit(rootDir));
|
|
1473
|
-
const reports = await BoundaryProvenanceAuditor.audit(rootDir);
|
|
1474
|
-
if (reports.length === 0) {
|
|
1475
|
-
if (registryFailed) process.exitCode = 1;
|
|
1476
|
-
outro("No consumed Heximon library packages found — no staleness audit to run.");
|
|
1477
|
-
return;
|
|
1478
|
-
}
|
|
1479
|
-
for (const report of reports) DoctorReport.logBoundary(report);
|
|
1480
|
-
const staleCount = reports.filter((report) => report.status === BoundaryProvenanceStatus.Stale).length;
|
|
1481
|
-
if (staleCount > 0 || registryFailed) process.exitCode = 1;
|
|
1482
|
-
if (staleCount > 0) outro(`${staleCount} stale package(s) — rebuild them before trusting this app's build.`);
|
|
1483
|
-
else outro(`${reports.length} package(s) audited — no staleness detected.`);
|
|
2183
|
+
const commandOutput = CommandOutput.resolve("doctor", args);
|
|
2184
|
+
await commandOutput.run(async () => {
|
|
2185
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
2186
|
+
if (!commandOutput.isJson()) intro(`heximon doctor — ${basename(rootDir)}`);
|
|
2187
|
+
const result = await DoctorAudit.run(rootDir, args.self);
|
|
2188
|
+
if (DoctorAudit.isFailure(result)) process.exitCode = 1;
|
|
2189
|
+
if (commandOutput.isJson()) commandOutput.emit(DoctorJsonReport.toJson(result));
|
|
2190
|
+
else DoctorReport.render(result);
|
|
2191
|
+
});
|
|
1484
2192
|
}
|
|
1485
2193
|
});
|
|
1486
2194
|
//#endregion
|
|
2195
|
+
//#region src/graph-json-document.ts
|
|
2196
|
+
/** The stable codes of the failures `heximon graph` raises itself. */
|
|
2197
|
+
const GraphDiagnosticCode = {
|
|
2198
|
+
/** The compile threw before the command could read a module graph. */
|
|
2199
|
+
ReadFailed: "graph.read-failed",
|
|
2200
|
+
/** The `--out` file could not be written. */
|
|
2201
|
+
OutWriteFailed: "graph.out-write-failed"
|
|
2202
|
+
};
|
|
2203
|
+
//#endregion
|
|
1487
2204
|
//#region src/json-output-target.ts
|
|
1488
2205
|
/**
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
* a payload and never branch on the flags themselves.
|
|
2206
|
+
* The `--out` file of an inspection command (`routes`, `graph`): it gets the same document the command prints
|
|
2207
|
+
* on stdout under `--json`. {@link CommandOutput} owns stdout; this class owns only the file.
|
|
1492
2208
|
*/
|
|
1493
2209
|
var JsonOutputTarget = class JsonOutputTarget {
|
|
1494
2210
|
filePath;
|
|
1495
|
-
toStdout;
|
|
1496
2211
|
/**
|
|
1497
2212
|
* @param filePath - The absolute `--out` path, or `undefined` when no file was requested.
|
|
1498
|
-
* @param toStdout - Whether the payload is printed on stdout instead.
|
|
1499
2213
|
*/
|
|
1500
|
-
constructor(filePath
|
|
2214
|
+
constructor(filePath) {
|
|
1501
2215
|
this.filePath = filePath;
|
|
1502
|
-
this.toStdout = toStdout;
|
|
1503
2216
|
}
|
|
1504
2217
|
/**
|
|
1505
|
-
* Resolve the
|
|
1506
|
-
*
|
|
2218
|
+
* Resolve the file. A relative path resolves against the current directory (where the command was typed),
|
|
2219
|
+
* not the `--cwd` project root.
|
|
1507
2220
|
*
|
|
1508
2221
|
* @param outOption - The raw `--out` argument, when the caller passed one.
|
|
1509
|
-
* @
|
|
2222
|
+
* @returns The target; a blank or missing `--out` requests no file.
|
|
1510
2223
|
*/
|
|
1511
|
-
static resolve(outOption
|
|
2224
|
+
static resolve(outOption) {
|
|
1512
2225
|
const requested = outOption?.trim();
|
|
1513
|
-
|
|
1514
|
-
return new JsonOutputTarget(filePath, filePath === void 0 && asJson);
|
|
2226
|
+
return new JsonOutputTarget(requested === void 0 || requested === "" ? void 0 : resolve(process.cwd(), requested));
|
|
1515
2227
|
}
|
|
1516
|
-
/** Whether
|
|
2228
|
+
/** Whether a file was requested — the caller's cue to report where it landed. */
|
|
1517
2229
|
isFile() {
|
|
1518
2230
|
return this.filePath !== void 0;
|
|
1519
2231
|
}
|
|
1520
2232
|
/**
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
* single line `jq` and pipes expect.
|
|
2233
|
+
* Run the body of a command. A failure it throws is written to the file as a `{ ok: false, diagnostics }`
|
|
2234
|
+
* document before it propagates, so the file never keeps the document of an earlier run.
|
|
1524
2235
|
*
|
|
1525
|
-
* @
|
|
1526
|
-
* cue to exit non-zero. A target that emits nowhere reports success.
|
|
2236
|
+
* @param body - The work of the command.
|
|
1527
2237
|
*/
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
2238
|
+
async run(body) {
|
|
2239
|
+
try {
|
|
2240
|
+
await body();
|
|
2241
|
+
} catch (error) {
|
|
2242
|
+
this.write({
|
|
2243
|
+
ok: false,
|
|
2244
|
+
diagnostics: error instanceof CommandFailure ? error.diagnostics : [CliDiagnostics.error(CliDiagnosticCode.UnexpectedError, error instanceof Error ? error.message : String(error))]
|
|
2245
|
+
});
|
|
2246
|
+
throw error;
|
|
1532
2247
|
}
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Write the document to the file, and record it as the stdout document of the run. When the write fails, the
|
|
2251
|
+
* recorded document gets `ok: false` and one more error diagnostic that names the file.
|
|
2252
|
+
*
|
|
2253
|
+
* @param document - The document of the run.
|
|
2254
|
+
* @param commandOutput - The output owner of the run.
|
|
2255
|
+
* @param writeFailureCode - The diagnostic code of a failed write.
|
|
2256
|
+
* @returns Whether the file was written; `true` when no file was requested.
|
|
2257
|
+
*/
|
|
2258
|
+
publish(document, commandOutput, writeFailureCode) {
|
|
2259
|
+
const failure = this.write(document);
|
|
2260
|
+
commandOutput.emit(failure === void 0 ? document : {
|
|
2261
|
+
...document,
|
|
2262
|
+
ok: false,
|
|
2263
|
+
diagnostics: [...document.diagnostics, CliDiagnostics.error(writeFailureCode, `Could not write ${this.filePath}: ${failure}`, { option: "--out" })]
|
|
2264
|
+
});
|
|
2265
|
+
return failure === void 0;
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Write indented JSON to the file, with its parent directory created, so a run can target a CI artifact
|
|
2269
|
+
* directory that does not exist yet and consecutive runs diff line by line.
|
|
2270
|
+
*
|
|
2271
|
+
* @returns The reason of a failed write (also written to stderr), or `undefined` when the write worked or
|
|
2272
|
+
* no file was requested.
|
|
2273
|
+
*/
|
|
2274
|
+
write(document) {
|
|
2275
|
+
if (this.filePath === void 0) return;
|
|
1533
2276
|
try {
|
|
1534
2277
|
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
1535
|
-
writeFileSync(this.filePath, `${JSON.stringify(
|
|
1536
|
-
return
|
|
2278
|
+
writeFileSync(this.filePath, `${JSON.stringify(document, null, 2)}\n`);
|
|
2279
|
+
return;
|
|
1537
2280
|
} catch (error) {
|
|
1538
2281
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1539
2282
|
process.stderr.write(`Could not write ${this.filePath} — ${reason}\n`);
|
|
1540
|
-
return
|
|
2283
|
+
return reason;
|
|
1541
2284
|
}
|
|
1542
2285
|
}
|
|
1543
2286
|
};
|
|
@@ -1572,16 +2315,13 @@ var ModuleGraphReport = class ModuleGraphReport {
|
|
|
1572
2315
|
return lines.join("\n");
|
|
1573
2316
|
}
|
|
1574
2317
|
/**
|
|
1575
|
-
* The
|
|
2318
|
+
* The graph fields of the `--json` document: the raw module view plus (when present) the boundary summaries.
|
|
1576
2319
|
*
|
|
1577
2320
|
* @param view - The compile's module-graph projection.
|
|
1578
2321
|
* @param boundaries - The cross-package boundary summaries, when `--boundaries` asked for them.
|
|
1579
|
-
* @param ok - Whether the compile that produced `view` was clean. A graph built from a failed compile has
|
|
1580
|
-
* unresolved edges, so a consumer reading only `ok` must not read it as a complete answer.
|
|
1581
2322
|
*/
|
|
1582
|
-
static toJson(view, boundaries = []
|
|
2323
|
+
static toJson(view, boundaries = []) {
|
|
1583
2324
|
return {
|
|
1584
|
-
ok,
|
|
1585
2325
|
modules: view.modules,
|
|
1586
2326
|
...boundaries.length > 0 ? { boundaries } : {}
|
|
1587
2327
|
};
|
|
@@ -1647,11 +2387,7 @@ const graphCommand = defineCommand({
|
|
|
1647
2387
|
description: "The heximon.config.ts `$env` name to resolve (matches `vp dev`). Default development.",
|
|
1648
2388
|
default: "development"
|
|
1649
2389
|
},
|
|
1650
|
-
json:
|
|
1651
|
-
type: "boolean",
|
|
1652
|
-
description: "Emit the graph as JSON on stdout instead of a tree.",
|
|
1653
|
-
default: false
|
|
1654
|
-
},
|
|
2390
|
+
json: CommandOutput.jsonFlag,
|
|
1655
2391
|
boundaries: {
|
|
1656
2392
|
type: "boolean",
|
|
1657
2393
|
description: "Also list the cross-package boundary nodes the app composes.",
|
|
@@ -1659,7 +2395,7 @@ const graphCommand = defineCommand({
|
|
|
1659
2395
|
},
|
|
1660
2396
|
out: {
|
|
1661
2397
|
type: "string",
|
|
1662
|
-
description: "
|
|
2398
|
+
description: "Also write the JSON document to this file, creating missing parent directories — the CI-artifact target."
|
|
1663
2399
|
},
|
|
1664
2400
|
src: {
|
|
1665
2401
|
type: "string",
|
|
@@ -1669,73 +2405,85 @@ const graphCommand = defineCommand({
|
|
|
1669
2405
|
app: CompileModeResolver.flags.app
|
|
1670
2406
|
},
|
|
1671
2407
|
async run({ args }) {
|
|
1672
|
-
const
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2408
|
+
const commandOutput = CommandOutput.resolve("graph", args);
|
|
2409
|
+
const outFile = JsonOutputTarget.resolve(args.out);
|
|
2410
|
+
await commandOutput.run(async () => {
|
|
2411
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
2412
|
+
await outFile.run(async () => {
|
|
2413
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
2414
|
+
const resolved = CompileModeResolver.resolve(rootDir, args, sourceDirectories);
|
|
2415
|
+
if (!commandOutput.isJson()) intro(`heximon graph — ${basename(rootDir)} · ${CompileModeResolver.stamp(resolved)}`);
|
|
2416
|
+
let output;
|
|
2417
|
+
try {
|
|
2418
|
+
const session = await ProjectCompileSession.open({
|
|
2419
|
+
rootDir,
|
|
2420
|
+
environment: args.env,
|
|
2421
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
2422
|
+
});
|
|
2423
|
+
output = resolved.mode === CompileMode.Library ? session.compileLibrary() : session.compile();
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
GraphCommand.reportFailure(error instanceof CommandFailure ? error.diagnostics : [CliDiagnostics.error(GraphDiagnosticCode.ReadFailed, error instanceof Error ? error.message : String(error))], commandOutput, outFile);
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
GraphCommand.report(output, {
|
|
2429
|
+
commandOutput,
|
|
2430
|
+
outFile,
|
|
2431
|
+
withBoundaries: args.boundaries,
|
|
2432
|
+
libraryMode: resolved.mode === CompileMode.Library
|
|
2433
|
+
});
|
|
1686
2434
|
});
|
|
1687
|
-
|
|
1688
|
-
} catch (error) {
|
|
1689
|
-
GraphCommand.reportFailure(error, asJson, jsonOutput);
|
|
1690
|
-
process.exitCode = 1;
|
|
1691
|
-
return;
|
|
1692
|
-
}
|
|
1693
|
-
if (!GraphCommand.report(output, {
|
|
1694
|
-
asJson,
|
|
1695
|
-
withBoundaries,
|
|
1696
|
-
jsonOutput,
|
|
1697
|
-
libraryMode: resolved.mode === CompileMode.Library
|
|
1698
|
-
})) process.exitCode = 1;
|
|
2435
|
+
});
|
|
1699
2436
|
}
|
|
1700
2437
|
});
|
|
1701
|
-
/** Output helpers for {@link graphCommand}: the module tree / JSON
|
|
2438
|
+
/** Output helpers for {@link graphCommand}: the module tree / JSON document and the boundary projection. */
|
|
1702
2439
|
var GraphCommand = class GraphCommand {
|
|
1703
2440
|
/**
|
|
1704
|
-
*
|
|
2441
|
+
* Report the resolved graph: the JSON document to stdout under `--json` and to the `--out` file, and a
|
|
2442
|
+
* module-nested tree unless `--json`. A failed compile or a failed `--out` write sets exit status 1.
|
|
1705
2443
|
*
|
|
1706
|
-
* @
|
|
2444
|
+
* @param output - The compile facts.
|
|
2445
|
+
* @param options - The output owners, and what to print.
|
|
1707
2446
|
*/
|
|
1708
2447
|
static report(output, options) {
|
|
1709
2448
|
const view = output.moduleGraph ?? { modules: [] };
|
|
1710
2449
|
const boundaries = options.withBoundaries ? GraphCommand.boundarySummaries(output) : [];
|
|
1711
2450
|
const errors = output.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).length;
|
|
1712
|
-
const
|
|
1713
|
-
|
|
2451
|
+
const document = {
|
|
2452
|
+
ok: errors === 0,
|
|
2453
|
+
diagnostics: CliDiagnostics.fromCompiler(output.diagnostics),
|
|
2454
|
+
...ModuleGraphReport.toJson(view, boundaries),
|
|
1714
2455
|
errors
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
if (written
|
|
2456
|
+
};
|
|
2457
|
+
const written = options.outFile.publish(document, options.commandOutput, GraphDiagnosticCode.OutWriteFailed);
|
|
2458
|
+
if (!written || errors > 0) process.exitCode = 1;
|
|
2459
|
+
if (options.commandOutput.isJson()) return;
|
|
2460
|
+
if (written && options.outFile.isFile()) log.success(`Graph JSON written to ${options.outFile.filePath}.`);
|
|
1718
2461
|
log.message(ModuleGraphReport.renderTree(view, boundaries));
|
|
1719
2462
|
if (options.libraryMode === true) log.info("These nodes are this package's PUBLISHED ROOTS, each folded over the local sub-modules it owns. A package's own sub-modules are not separately composable, so they carry no node of their own.");
|
|
1720
2463
|
const moduleCount = view.modules.length;
|
|
1721
2464
|
const summary = `${moduleCount} module${moduleCount === 1 ? "" : "s"}`;
|
|
1722
2465
|
if (errors > 0) outro(`${summary} — ${errors} compile error${errors === 1 ? "" : "s"}; the graph may be incomplete.`);
|
|
1723
2466
|
else outro(`${summary}.`);
|
|
1724
|
-
return written && errors === 0;
|
|
1725
2467
|
}
|
|
1726
2468
|
/**
|
|
1727
|
-
* Report a config-load / compile failure:
|
|
1728
|
-
* outro unless `--json`. The
|
|
1729
|
-
* artifact rather than leaving a stale success.
|
|
2469
|
+
* Report a config-load / compile failure: a {@link CliFailureDocument} to stdout under `--json` and to the
|
|
2470
|
+
* `--out` file, and a clack error + outro unless `--json`. The file gets the failure too, so a new run
|
|
2471
|
+
* replaces the previous artifact rather than leaving a stale success. Sets exit status 1.
|
|
2472
|
+
*
|
|
2473
|
+
* @param diagnostics - The diagnostics of the failure.
|
|
2474
|
+
* @param commandOutput - The stdout owner of the run.
|
|
2475
|
+
* @param outFile - The `--out` file.
|
|
1730
2476
|
*/
|
|
1731
|
-
static reportFailure(
|
|
1732
|
-
const
|
|
1733
|
-
jsonOutput.emit({
|
|
2477
|
+
static reportFailure(diagnostics, commandOutput, outFile) {
|
|
2478
|
+
const document = {
|
|
1734
2479
|
ok: false,
|
|
1735
|
-
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
|
|
2480
|
+
diagnostics
|
|
2481
|
+
};
|
|
2482
|
+
outFile.publish(document, commandOutput, GraphDiagnosticCode.OutWriteFailed);
|
|
2483
|
+
process.exitCode = 1;
|
|
2484
|
+
if (commandOutput.isJson()) return;
|
|
2485
|
+
const messages = diagnostics.map((diagnostic) => diagnostic.message);
|
|
2486
|
+
log.error(`Could not read the app's module graph:\n${messages.join("\n")}`);
|
|
1739
2487
|
outro("No graph to show.");
|
|
1740
2488
|
}
|
|
1741
2489
|
/** Project the compile's cross-package boundary nodes into display summaries. */
|
|
@@ -1749,12 +2497,45 @@ var GraphCommand = class GraphCommand {
|
|
|
1749
2497
|
}
|
|
1750
2498
|
};
|
|
1751
2499
|
//#endregion
|
|
2500
|
+
//#region src/migrations-json-document.ts
|
|
2501
|
+
/** The stable codes of the failures and warnings `heximon migrations` raises itself. */
|
|
2502
|
+
const MigrationsDiagnosticCode = {
|
|
2503
|
+
/** No drizzle-kit config was detected, and `--config` was not given. */
|
|
2504
|
+
NoConfig: "migrations.no-config",
|
|
2505
|
+
/** A candidate config file could not be loaded. A warning beside {@link NoConfig}; `file` names it. */
|
|
2506
|
+
ConfigLoadFailed: "migrations.config-load-failed",
|
|
2507
|
+
/** The `--env-file` cannot be read. */
|
|
2508
|
+
EnvFileUnreadable: "migrations.env-file-unreadable",
|
|
2509
|
+
/** `drizzle-kit` exited with a non-zero status for one database. */
|
|
2510
|
+
DrizzleKitFailed: "migrations.drizzle-kit-failed",
|
|
2511
|
+
/** `drizzle-kit` could not start, for example because it is not installed. The run stops there. */
|
|
2512
|
+
DrizzleKitNotStarted: "migrations.drizzle-kit-not-started",
|
|
2513
|
+
/** A Cloudflare D1 database was not migrated: `wrangler d1 migrations apply` applies it. A warning. */
|
|
2514
|
+
D1ApplySkipped: "migrations.d1-apply-skipped"
|
|
2515
|
+
};
|
|
2516
|
+
/** What happened to one database in a `generate`, `migrate` or `push` run. */
|
|
2517
|
+
const MigrationsStepStatus = {
|
|
2518
|
+
/** `drizzle-kit` exited 0. */
|
|
2519
|
+
Succeeded: "succeeded",
|
|
2520
|
+
/** `drizzle-kit` exited with a non-zero status. */
|
|
2521
|
+
Failed: "failed",
|
|
2522
|
+
/** `drizzle-kit` could not start. The run stopped, and the databases after this one have no entry. */
|
|
2523
|
+
NotStarted: "not-started",
|
|
2524
|
+
/** The command did not run `drizzle-kit` for this database; {@link MigrationsStepEntry.reason} says why. */
|
|
2525
|
+
Skipped: "skipped"
|
|
2526
|
+
};
|
|
2527
|
+
//#endregion
|
|
1752
2528
|
//#region src/migrations-command.ts
|
|
1753
2529
|
/**
|
|
1754
|
-
* The plumbing every `heximon migrations` subcommand shares: the `.env` + detection prologue,
|
|
1755
|
-
* selection, one captured `drizzle-kit` invocation, and the
|
|
2530
|
+
* The plumbing every `heximon migrations` subcommand shares: the output mode, the `.env` + detection prologue,
|
|
2531
|
+
* the config selection, one captured `drizzle-kit` invocation, and the diagnostics of the `--json` document.
|
|
1756
2532
|
*/
|
|
1757
2533
|
var MigrationsRunner = class MigrationsRunner {
|
|
2534
|
+
commandOutput;
|
|
2535
|
+
rootDir;
|
|
2536
|
+
detection;
|
|
2537
|
+
args;
|
|
2538
|
+
mask;
|
|
1758
2539
|
/** The flags every `heximon migrations` subcommand accepts. */
|
|
1759
2540
|
static baseArgs = {
|
|
1760
2541
|
cwd: {
|
|
@@ -1774,42 +2555,80 @@ var MigrationsRunner = class MigrationsRunner {
|
|
|
1774
2555
|
config: {
|
|
1775
2556
|
type: "string",
|
|
1776
2557
|
description: "Target one drizzle-kit config file, bypassing auto-detection."
|
|
1777
|
-
}
|
|
2558
|
+
},
|
|
2559
|
+
json: CommandOutput.jsonFlag
|
|
1778
2560
|
};
|
|
1779
2561
|
/** The `select` sentinel for "run every detected database". */
|
|
1780
2562
|
static allDatabasesValue = "__all__";
|
|
2563
|
+
/** The message of the "no drizzle-kit config" halt. */
|
|
2564
|
+
static noConfigMessage = "No drizzle-kit config found — expected a `*.config.ts` that `export default`s a Drizzle config (e.g. src/database/database.config.ts), or pass --config <path>.";
|
|
2565
|
+
/** The password of a URL with userinfo (`scheme://user:password@host`), as the `secret` group. */
|
|
2566
|
+
static urlPassword = /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]*:(?<secret>[^\s@/]+)@/dgiu;
|
|
2567
|
+
/** The value of a `password` query parameter (`?password=…`, `&password=…`), as the `secret` group. */
|
|
2568
|
+
static queryPassword = /[?&]password=(?<secret>[^&#\s]+)/dgiu;
|
|
2569
|
+
/** Every diagnostic of the run, in the order the run raised them. */
|
|
2570
|
+
diagnostics = [];
|
|
1781
2571
|
/**
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
2572
|
+
* @param mask - The credentials of the run: each `--env-file` value of at least
|
|
2573
|
+
* {@link SecretMask.minimumFragmentLength} characters, and the `dbCredentials` secrets of each config at any
|
|
2574
|
+
* length. drizzle-kit can echo them, and its output reaches the terminal and the `--json` document.
|
|
1785
2575
|
*/
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2576
|
+
constructor(commandOutput, rootDir, detection, args, mask) {
|
|
2577
|
+
this.commandOutput = commandOutput;
|
|
2578
|
+
this.rootDir = rootDir;
|
|
2579
|
+
this.detection = detection;
|
|
2580
|
+
this.args = args;
|
|
2581
|
+
this.mask = mask;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* The options of one `drizzle-kit` call. They carry no mask: {@link redact} masks the original captured output
|
|
2585
|
+
* with the registered secrets and the URL rules together.
|
|
2586
|
+
*/
|
|
2587
|
+
drizzleKitOptions() {
|
|
2588
|
+
return { rootDir: this.rootDir };
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Run one subcommand: resolve the output mode, print the intro, load the `.env` cascade, detect the
|
|
2592
|
+
* configs, then run the body.
|
|
2593
|
+
*
|
|
2594
|
+
* @param subcommand - The subcommand verb (`generate`, `migrate`, …).
|
|
2595
|
+
* @param args - The parsed arguments.
|
|
2596
|
+
* @param body - The work of the subcommand. It records its `--json` document on the runner's output.
|
|
2597
|
+
*/
|
|
2598
|
+
static async execute(subcommand, args, body) {
|
|
2599
|
+
const commandOutput = CommandOutput.resolve(`migrations ${subcommand}`, args);
|
|
2600
|
+
await commandOutput.run(async () => {
|
|
2601
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
2602
|
+
await MigrationsRunner.guard(commandOutput, async () => {
|
|
2603
|
+
intro(`heximon migrations ${subcommand}`);
|
|
2604
|
+
await body(await MigrationsRunner.prepare(commandOutput, args, rootDir));
|
|
2605
|
+
});
|
|
2606
|
+
});
|
|
1794
2607
|
}
|
|
1795
2608
|
/**
|
|
1796
2609
|
* Choose which detected config(s) to act on: an explicit `--config` wins; a single detected config is used
|
|
1797
2610
|
* silently; multiple prompt an interactive `select`, or — in a non-TTY run — fan out to ALL of them, which
|
|
1798
|
-
* is named out loud because that is several databases the operator never chose.
|
|
1799
|
-
*
|
|
2611
|
+
* is named out loud because that is several databases the operator never chose. Under `--json` several
|
|
2612
|
+
* configs fail with {@link CliDiagnosticCode.OptionRequired}, because `--json` never chooses for the caller.
|
|
2613
|
+
* Reports a text-mode failure itself (no config / cancelled) and returns `undefined` so the caller just
|
|
2614
|
+
* returns.
|
|
1800
2615
|
*/
|
|
1801
|
-
|
|
1802
|
-
if (args.config !== void 0) {
|
|
1803
|
-
const path = resolve(rootDir, args.config);
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2616
|
+
async selectConfigs() {
|
|
2617
|
+
if (this.args.config !== void 0) {
|
|
2618
|
+
const path = resolve(this.rootDir, this.args.config);
|
|
2619
|
+
const classified = await DatabaseConfigDetector.classifyFile(this.rootDir, path);
|
|
2620
|
+
if (classified !== null) MigrationsRunner.registerCredentials(this.mask, classified);
|
|
2621
|
+
return [classified ?? MigrationsRunner.syntheticConfig(path)];
|
|
2622
|
+
}
|
|
2623
|
+
const { configs } = this.detection;
|
|
1807
2624
|
if (configs.length === 0) {
|
|
1808
|
-
MigrationsRunner.
|
|
2625
|
+
if (this.commandOutput.isJson()) throw new CommandFailure(MigrationsRunner.noConfigDiagnostics(this.detection));
|
|
2626
|
+
MigrationsRunner.reportNoConfig(this.detection);
|
|
1809
2627
|
return;
|
|
1810
2628
|
}
|
|
1811
2629
|
if (configs.length === 1) return configs;
|
|
1812
|
-
if (
|
|
2630
|
+
if (this.commandOutput.isJson()) throw new CommandFailure([this.selectionRefusal(configs)]);
|
|
2631
|
+
if (!this.commandOutput.isInteractive()) {
|
|
1813
2632
|
log.warn(`${configs.length} databases detected and no TTY to choose between them — acting on ALL of them: ${configs.map((config) => config.name).join(", ")}. Pass --config <path> to target one.`);
|
|
1814
2633
|
return configs;
|
|
1815
2634
|
}
|
|
@@ -1832,34 +2651,158 @@ var MigrationsRunner = class MigrationsRunner {
|
|
|
1832
2651
|
return configs.filter((config) => config.path === choice);
|
|
1833
2652
|
}
|
|
1834
2653
|
/**
|
|
1835
|
-
* Run one captured `drizzle-kit` invocation behind a spinner, relaying its output.
|
|
2654
|
+
* Run one captured `drizzle-kit` invocation behind a spinner, relaying its output. A failure also records
|
|
2655
|
+
* an error diagnostic.
|
|
1836
2656
|
*
|
|
1837
|
-
* @
|
|
2657
|
+
* @param config - The database the invocation acts on.
|
|
2658
|
+
* @param label - The progress label, for example `Applying migrations for database.config.ts`.
|
|
2659
|
+
* @param invoke - Starts the invocation.
|
|
2660
|
+
* @returns The entry of the database. A `not-started` status means the caller stops its loop.
|
|
1838
2661
|
*/
|
|
1839
|
-
|
|
1840
|
-
const progress = CliSpinner.begin(
|
|
2662
|
+
async runStep(config, label, invoke) {
|
|
2663
|
+
const progress = CliSpinner.begin(this.commandOutput.isInteractive(), `${label}…`);
|
|
1841
2664
|
let result;
|
|
1842
2665
|
try {
|
|
1843
2666
|
result = await invoke();
|
|
1844
2667
|
} catch (error) {
|
|
2668
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1845
2669
|
progress?.stop(`${label} — failed to start.`);
|
|
1846
|
-
|
|
2670
|
+
log.error(reason);
|
|
2671
|
+
this.diagnostics.push(CliDiagnostics.error(MigrationsDiagnosticCode.DrizzleKitNotStarted, `${label}: drizzle-kit could not start. ${reason}`, { hint: "Add drizzle-kit to the devDependencies of the app, then run the command again." }));
|
|
2672
|
+
return {
|
|
2673
|
+
...this.viewOf(config),
|
|
2674
|
+
status: MigrationsStepStatus.NotStarted,
|
|
2675
|
+
reason
|
|
2676
|
+
};
|
|
1847
2677
|
}
|
|
1848
|
-
const
|
|
2678
|
+
const stdout = this.redact(result.stdout);
|
|
2679
|
+
const stderr = this.redact(result.stderr);
|
|
2680
|
+
const output = [stdout.trim(), stderr.trim()].filter((part) => part.length > 0);
|
|
2681
|
+
const drizzleKit = {
|
|
2682
|
+
exitCode: result.exitCode,
|
|
2683
|
+
stdout,
|
|
2684
|
+
stderr
|
|
2685
|
+
};
|
|
1849
2686
|
if (result.exitCode === 0) {
|
|
1850
2687
|
progress?.stop(`${label} — done.`);
|
|
1851
2688
|
if (output.length > 0) log.message(output.join("\n"));
|
|
1852
|
-
return
|
|
2689
|
+
return {
|
|
2690
|
+
...this.viewOf(config),
|
|
2691
|
+
status: MigrationsStepStatus.Succeeded,
|
|
2692
|
+
drizzleKit
|
|
2693
|
+
};
|
|
1853
2694
|
}
|
|
1854
2695
|
progress?.stop(`${label} — failed (exit ${result.exitCode}).`);
|
|
1855
2696
|
if (output.length > 0) log.error(output.join("\n"));
|
|
1856
|
-
|
|
2697
|
+
this.diagnostics.push(CliDiagnostics.error(MigrationsDiagnosticCode.DrizzleKitFailed, `${label}: drizzle-kit exited with status ${result.exitCode}.`, { hint: "Read the drizzle-kit output in the drizzleKit field of the database entry." }));
|
|
2698
|
+
return {
|
|
2699
|
+
...this.viewOf(config),
|
|
2700
|
+
status: MigrationsStepStatus.Failed,
|
|
2701
|
+
drizzleKit
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Record a database that `migrate` does not apply.
|
|
2706
|
+
*
|
|
2707
|
+
* @param config - The skipped database.
|
|
2708
|
+
* @param reason - Why it is skipped, and what applies it.
|
|
2709
|
+
* @param warning - A warning diagnostic to record beside the entry, when the operator must act.
|
|
2710
|
+
* @returns The entry of the database.
|
|
2711
|
+
*/
|
|
2712
|
+
skip(config, reason, warning) {
|
|
2713
|
+
if (warning !== void 0) this.diagnostics.push(warning);
|
|
2714
|
+
return {
|
|
2715
|
+
...this.viewOf(config),
|
|
2716
|
+
status: MigrationsStepStatus.Skipped,
|
|
2717
|
+
reason
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
/** The `.sql` files in the migrations folder of a config, relative to that folder, sorted. */
|
|
2721
|
+
async sqlFilesOf(config) {
|
|
2722
|
+
return (await MigrationInspector.collectSqlFiles(resolve(this.rootDir, MigrationsRunner.migrationsDirectoryOf(config)))).map((file) => file.relativePath);
|
|
2723
|
+
}
|
|
2724
|
+
/** The JSON view of one config. */
|
|
2725
|
+
viewOf(config) {
|
|
2726
|
+
return {
|
|
2727
|
+
name: config.name,
|
|
2728
|
+
configPath: config.path,
|
|
2729
|
+
dialect: config.dialect,
|
|
2730
|
+
...config.driver === void 0 ? {} : { driver: config.driver },
|
|
2731
|
+
applyMode: config.applyMode,
|
|
2732
|
+
migrationsDirectory: MigrationsRunner.migrationsDirectoryOf(config)
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
/** Whether the run recorded no error diagnostic. */
|
|
2736
|
+
isSuccessful() {
|
|
2737
|
+
return !this.diagnostics.some((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* The `--json` document of the run.
|
|
2741
|
+
*
|
|
2742
|
+
* @param databases - One entry for each database the run acted on.
|
|
2743
|
+
* @returns The document, with `ok` from the recorded diagnostics.
|
|
2744
|
+
*/
|
|
2745
|
+
document(databases) {
|
|
2746
|
+
return {
|
|
2747
|
+
ok: this.isSuccessful(),
|
|
2748
|
+
diagnostics: [...this.diagnostics],
|
|
2749
|
+
rootDir: this.rootDir,
|
|
2750
|
+
databases
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
/** The migrations folder of a config: its `out`, else the drizzle-kit default. */
|
|
2754
|
+
static migrationsDirectoryOf(config) {
|
|
2755
|
+
return config.out ?? DatabaseConfigDetector.defaultOutDirectory;
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Load the `.env` cascade into `process.env` (so the spawned `drizzle-kit` inherits `DATABASE_URL`), and
|
|
2759
|
+
* detect the app's drizzle-kit config file(s). An explicit `--env-file` overlays the cascade.
|
|
2760
|
+
*
|
|
2761
|
+
* @param rootDir - The project root {@link ProjectRoot.resolve} accepted.
|
|
2762
|
+
*/
|
|
2763
|
+
static async prepare(commandOutput, args, rootDir) {
|
|
2764
|
+
const overlay = args["env-file"] === void 0 ? void 0 : await MigrationsRunner.readEnvFile(resolve(rootDir, args["env-file"]));
|
|
2765
|
+
DotenvCascade.loadInto(rootDir, args.env, overlay);
|
|
2766
|
+
const mask = new SecretMask();
|
|
2767
|
+
const detection = await DatabaseConfigDetector.detect(rootDir);
|
|
2768
|
+
for (const value of overlay?.values() ?? []) if (value.length >= SecretMask.minimumFragmentLength) mask.register(value);
|
|
2769
|
+
for (const config of detection.configs) MigrationsRunner.registerCredentials(mask, config);
|
|
2770
|
+
return new MigrationsRunner(commandOutput, rootDir, detection, args, mask);
|
|
2771
|
+
}
|
|
2772
|
+
/** Register the `dbCredentials` secrets of one config, and the password inside each URL among them. */
|
|
2773
|
+
static registerCredentials(mask, config) {
|
|
2774
|
+
for (const credential of config.credentials ?? []) {
|
|
2775
|
+
mask.register(credential);
|
|
2776
|
+
for (const match of credential.matchAll(MigrationsRunner.urlPassword)) mask.register(match.groups?.["secret"] ?? "");
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Hide the registered secrets, the password of any URL with userinfo, and any `password` query parameter in
|
|
2781
|
+
* captured output. Every range is found on the original text and replaced in one step, so no rule changes the
|
|
2782
|
+
* text another rule reads.
|
|
2783
|
+
*/
|
|
2784
|
+
redact(text) {
|
|
2785
|
+
return this.mask.redactWithPatterns(text, [MigrationsRunner.urlPassword, MigrationsRunner.queryPassword]);
|
|
2786
|
+
}
|
|
2787
|
+
/** Read the `--env-file`; an unreadable file is an expected failure with its own code. */
|
|
2788
|
+
static async readEnvFile(path) {
|
|
2789
|
+
try {
|
|
2790
|
+
return await EnvFileReader.read(path);
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
if (error instanceof EnvFileUnreadableError) throw CommandFailure.of(MigrationsDiagnosticCode.EnvFileUnreadable, error.message);
|
|
2793
|
+
throw error;
|
|
2794
|
+
}
|
|
1857
2795
|
}
|
|
1858
2796
|
/**
|
|
1859
2797
|
* Wrap a subcommand body so a thrown error (e.g. an unreadable `--env-file`) becomes a clean cancel +
|
|
1860
|
-
* non-zero exit rather than an unhandled stack trace.
|
|
2798
|
+
* non-zero exit rather than an unhandled stack trace. Under `--json` the error reaches
|
|
2799
|
+
* {@link CommandOutput.run}, which prints it as the failure document.
|
|
1861
2800
|
*/
|
|
1862
|
-
static async guard(body) {
|
|
2801
|
+
static async guard(commandOutput, body) {
|
|
2802
|
+
if (commandOutput.isJson()) {
|
|
2803
|
+
await body();
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
1863
2806
|
try {
|
|
1864
2807
|
await body();
|
|
1865
2808
|
} catch (error) {
|
|
@@ -1867,6 +2810,14 @@ var MigrationsRunner = class MigrationsRunner {
|
|
|
1867
2810
|
cancel(error instanceof Error ? error.message : String(error));
|
|
1868
2811
|
}
|
|
1869
2812
|
}
|
|
2813
|
+
/** The `--json` refusal of the database `select`, naming every choice. */
|
|
2814
|
+
selectionRefusal(configs) {
|
|
2815
|
+
const paths = configs.map((config) => relative(this.rootDir, config.path));
|
|
2816
|
+
return CliDiagnostics.error(CliDiagnosticCode.OptionRequired, `${configs.length} databases were detected (${paths.join(", ")}), and --json never chooses one.`, {
|
|
2817
|
+
hint: `Pass --config <path> with one of: ${paths.join(", ")}. Run once for each database to act on all.`,
|
|
2818
|
+
option: "--config"
|
|
2819
|
+
});
|
|
2820
|
+
}
|
|
1870
2821
|
/**
|
|
1871
2822
|
* A stand-in for an explicit `--config` that could not be classified (unloadable / not a drizzle config):
|
|
1872
2823
|
* the command still drives `drizzle-kit` on it and surfaces drizzle-kit's own error, trusting the flag.
|
|
@@ -1887,9 +2838,18 @@ var MigrationsRunner = class MigrationsRunner {
|
|
|
1887
2838
|
*/
|
|
1888
2839
|
static reportNoConfig(detection) {
|
|
1889
2840
|
process.exitCode = 1;
|
|
1890
|
-
cancel(
|
|
2841
|
+
cancel(MigrationsRunner.noConfigMessage);
|
|
1891
2842
|
for (const failure of detection.loadErrors) log.warn(`Could not load ${failure.path}: ${failure.message}`);
|
|
1892
2843
|
}
|
|
2844
|
+
/** The `--json` diagnostics of the "no drizzle-kit config" halt: the error, then one warning per candidate. */
|
|
2845
|
+
static noConfigDiagnostics(detection) {
|
|
2846
|
+
return [CliDiagnostics.error(MigrationsDiagnosticCode.NoConfig, MigrationsRunner.noConfigMessage, { hint: "Add a drizzle-kit config, or pass --config <path>." }), ...detection.loadErrors.map((failure) => ({
|
|
2847
|
+
severity: DiagnosticSeverity.Warning,
|
|
2848
|
+
code: MigrationsDiagnosticCode.ConfigLoadFailed,
|
|
2849
|
+
message: `Could not load ${failure.path}: ${failure.message}`,
|
|
2850
|
+
file: failure.path
|
|
2851
|
+
}))];
|
|
2852
|
+
}
|
|
1893
2853
|
};
|
|
1894
2854
|
const generateCommand = defineCommand({
|
|
1895
2855
|
meta: {
|
|
@@ -1903,16 +2863,24 @@ const generateCommand = defineCommand({
|
|
|
1903
2863
|
description: "Name the migration (drizzle-kit --name)."
|
|
1904
2864
|
}
|
|
1905
2865
|
},
|
|
1906
|
-
run: ({ args }) => MigrationsRunner.
|
|
1907
|
-
const
|
|
1908
|
-
intro("heximon migrations generate");
|
|
1909
|
-
const { rootDir, detection } = await MigrationsRunner.prepare(args);
|
|
1910
|
-
const selected = await MigrationsRunner.selectConfigs(rootDir, detection, args, interactive);
|
|
2866
|
+
run: ({ args }) => MigrationsRunner.execute("generate", args, async (runner) => {
|
|
2867
|
+
const selected = await runner.selectConfigs();
|
|
1911
2868
|
if (selected === void 0) return;
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
for (const config of selected)
|
|
1915
|
-
|
|
2869
|
+
const drizzleKit = new DrizzleKitRunner();
|
|
2870
|
+
const entries = [];
|
|
2871
|
+
for (const config of selected) {
|
|
2872
|
+
const filesBefore = new Set(await runner.sqlFilesOf(config));
|
|
2873
|
+
const entry = await runner.runStep(config, `Generating migration for ${config.name}`, () => drizzleKit.generate(config.path, runner.drizzleKitOptions(), args.name));
|
|
2874
|
+
const filesAfter = await runner.sqlFilesOf(config);
|
|
2875
|
+
entries.push({
|
|
2876
|
+
...entry,
|
|
2877
|
+
generatedFiles: filesAfter.filter((file) => !filesBefore.has(file))
|
|
2878
|
+
});
|
|
2879
|
+
if (entry.status === MigrationsStepStatus.NotStarted) break;
|
|
2880
|
+
}
|
|
2881
|
+
const document = runner.document(entries);
|
|
2882
|
+
runner.commandOutput.emit(document);
|
|
2883
|
+
if (!runner.isSuccessful()) {
|
|
1916
2884
|
process.exitCode = 1;
|
|
1917
2885
|
cancel("Some migrations failed to generate — see above.");
|
|
1918
2886
|
return;
|
|
@@ -1926,26 +2894,37 @@ const migrateCommand = defineCommand({
|
|
|
1926
2894
|
description: "Apply pending migrations (drizzle-kit migrate)."
|
|
1927
2895
|
},
|
|
1928
2896
|
args: MigrationsRunner.baseArgs,
|
|
1929
|
-
run: ({ args }) => MigrationsRunner.
|
|
1930
|
-
const
|
|
1931
|
-
intro("heximon migrations migrate");
|
|
1932
|
-
const { rootDir, detection } = await MigrationsRunner.prepare(args);
|
|
1933
|
-
const selected = await MigrationsRunner.selectConfigs(rootDir, detection, args, interactive);
|
|
2897
|
+
run: ({ args }) => MigrationsRunner.execute("migrate", args, async (runner) => {
|
|
2898
|
+
const selected = await runner.selectConfigs();
|
|
1934
2899
|
if (selected === void 0) return;
|
|
1935
|
-
const
|
|
1936
|
-
|
|
2900
|
+
const drizzleKit = new DrizzleKitRunner();
|
|
2901
|
+
const entries = [];
|
|
1937
2902
|
for (const config of selected) {
|
|
1938
2903
|
if (config.applyMode === "wrangler-d1") {
|
|
1939
|
-
|
|
2904
|
+
const message = `${config.name}: a Cloudflare D1 database — apply it with \`wrangler d1 migrations apply <database> --remote\`, not drizzle-kit.`;
|
|
2905
|
+
log.warn(message);
|
|
2906
|
+
entries.push(runner.skip(config, message, {
|
|
2907
|
+
severity: DiagnosticSeverity.Warning,
|
|
2908
|
+
code: MigrationsDiagnosticCode.D1ApplySkipped,
|
|
2909
|
+
message,
|
|
2910
|
+
hint: "Run `wrangler d1 migrations apply <database> --remote`.",
|
|
2911
|
+
file: config.path
|
|
2912
|
+
}));
|
|
1940
2913
|
continue;
|
|
1941
2914
|
}
|
|
1942
2915
|
if (config.applyMode === "skip") {
|
|
1943
|
-
|
|
2916
|
+
const message = `${config.name}: Durable Object SQLite — migrations bundle in-DO at init; no CLI apply step.`;
|
|
2917
|
+
log.info(message);
|
|
2918
|
+
entries.push(runner.skip(config, message));
|
|
1944
2919
|
continue;
|
|
1945
2920
|
}
|
|
1946
|
-
|
|
2921
|
+
const entry = await runner.runStep(config, `Applying migrations for ${config.name}`, () => drizzleKit.migrate(config.path, runner.drizzleKitOptions()));
|
|
2922
|
+
entries.push(entry);
|
|
2923
|
+
if (entry.status === MigrationsStepStatus.NotStarted) break;
|
|
1947
2924
|
}
|
|
1948
|
-
|
|
2925
|
+
const document = runner.document(entries);
|
|
2926
|
+
runner.commandOutput.emit(document);
|
|
2927
|
+
if (!runner.isSuccessful()) {
|
|
1949
2928
|
process.exitCode = 1;
|
|
1950
2929
|
cancel("Some migrations failed to apply — see above.");
|
|
1951
2930
|
return;
|
|
@@ -1959,21 +2938,25 @@ const statusCommand = defineCommand({
|
|
|
1959
2938
|
description: "List the on-disk migrations per database."
|
|
1960
2939
|
},
|
|
1961
2940
|
args: MigrationsRunner.baseArgs,
|
|
1962
|
-
run: ({ args }) => MigrationsRunner.
|
|
1963
|
-
const
|
|
1964
|
-
intro("heximon migrations status");
|
|
1965
|
-
const { rootDir, detection } = await MigrationsRunner.prepare(args);
|
|
1966
|
-
const selected = await MigrationsRunner.selectConfigs(rootDir, detection, args, interactive);
|
|
2941
|
+
run: ({ args }) => MigrationsRunner.execute("status", args, async (runner) => {
|
|
2942
|
+
const selected = await runner.selectConfigs();
|
|
1967
2943
|
if (selected === void 0) return;
|
|
2944
|
+
const entries = [];
|
|
1968
2945
|
for (const config of selected) {
|
|
1969
|
-
const outFolder = config
|
|
1970
|
-
const files = await
|
|
2946
|
+
const outFolder = MigrationsRunner.migrationsDirectoryOf(config);
|
|
2947
|
+
const files = await runner.sqlFilesOf(config);
|
|
2948
|
+
entries.push({
|
|
2949
|
+
...runner.viewOf(config),
|
|
2950
|
+
migrations: files
|
|
2951
|
+
});
|
|
1971
2952
|
if (files.length === 0) {
|
|
1972
2953
|
log.info(`${config.name}: no migrations in ${outFolder} — run \`heximon migrations generate\`.`);
|
|
1973
2954
|
continue;
|
|
1974
2955
|
}
|
|
1975
|
-
log.message(`${config.name} · ${files.length} migration(s) in ${outFolder}:\n` + files.map((file) => ` ${file
|
|
2956
|
+
log.message(`${config.name} · ${files.length} migration(s) in ${outFolder}:\n` + files.map((file) => ` ${file}`).join("\n"));
|
|
1976
2957
|
}
|
|
2958
|
+
const document = runner.document(entries);
|
|
2959
|
+
runner.commandOutput.emit(document);
|
|
1977
2960
|
log.info("Applied vs. pending state needs a live database query — not shown in this view.");
|
|
1978
2961
|
outro("Status listed.");
|
|
1979
2962
|
})
|
|
@@ -1990,21 +2973,20 @@ const pushCommand = defineCommand({
|
|
|
1990
2973
|
description: "Confirm the destructive push non-interactively."
|
|
1991
2974
|
}
|
|
1992
2975
|
},
|
|
1993
|
-
run: ({ args }) => MigrationsRunner.
|
|
1994
|
-
const
|
|
1995
|
-
intro("heximon migrations push");
|
|
1996
|
-
const { rootDir, detection } = await MigrationsRunner.prepare(args);
|
|
1997
|
-
const selected = await MigrationsRunner.selectConfigs(rootDir, detection, args, interactive);
|
|
2976
|
+
run: ({ args }) => MigrationsRunner.execute("push", args, async (runner) => {
|
|
2977
|
+
const selected = await runner.selectConfigs();
|
|
1998
2978
|
if (selected === void 0) return;
|
|
1999
2979
|
let force = args.yes === true;
|
|
2000
2980
|
if (!force) {
|
|
2001
|
-
|
|
2981
|
+
const question = `Push schema directly to ${selected.map((config) => config.name).join(", ")}? This can DROP columns/tables.`;
|
|
2982
|
+
runner.commandOutput.assertCanPrompt("--yes", question);
|
|
2983
|
+
if (!runner.commandOutput.isInteractive()) {
|
|
2002
2984
|
process.exitCode = 1;
|
|
2003
2985
|
cancel("`push` is destructive (it can drop columns/tables). Re-run with --yes to confirm in a non-interactive shell.");
|
|
2004
2986
|
return;
|
|
2005
2987
|
}
|
|
2006
2988
|
const confirmed = await confirm({
|
|
2007
|
-
message:
|
|
2989
|
+
message: question,
|
|
2008
2990
|
initialValue: false
|
|
2009
2991
|
});
|
|
2010
2992
|
if (isCancel(confirmed) || confirmed !== true) {
|
|
@@ -2014,10 +2996,16 @@ const pushCommand = defineCommand({
|
|
|
2014
2996
|
}
|
|
2015
2997
|
force = true;
|
|
2016
2998
|
}
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
for (const config of selected)
|
|
2020
|
-
|
|
2999
|
+
const drizzleKit = new DrizzleKitRunner();
|
|
3000
|
+
const entries = [];
|
|
3001
|
+
for (const config of selected) {
|
|
3002
|
+
const entry = await runner.runStep(config, `Pushing schema for ${config.name}`, () => drizzleKit.push(config.path, runner.drizzleKitOptions(), force));
|
|
3003
|
+
entries.push(entry);
|
|
3004
|
+
if (entry.status === MigrationsStepStatus.NotStarted) break;
|
|
3005
|
+
}
|
|
3006
|
+
const document = runner.document(entries);
|
|
3007
|
+
runner.commandOutput.emit(document);
|
|
3008
|
+
if (!runner.isSuccessful()) {
|
|
2021
3009
|
process.exitCode = 1;
|
|
2022
3010
|
cancel("Some schema pushes failed — see above.");
|
|
2023
3011
|
return;
|
|
@@ -2043,8 +3031,92 @@ const migrationsCommand = defineCommand({
|
|
|
2043
3031
|
}
|
|
2044
3032
|
});
|
|
2045
3033
|
//#endregion
|
|
3034
|
+
//#region src/provision-json-document.ts
|
|
3035
|
+
/**
|
|
3036
|
+
* The stable codes of the `heximon provision --json` diagnostics. `--profile` uses `cli.invalid-options`,
|
|
3037
|
+
* and a missing credential uses `cli.option-required`.
|
|
3038
|
+
*/
|
|
3039
|
+
const ProvisionDiagnosticCode = {
|
|
3040
|
+
/** The platform has no foundational resources to provision. */
|
|
3041
|
+
Unsupported: "provision.unsupported",
|
|
3042
|
+
/** One or more resources failed to provision; `result.failed` names them. */
|
|
3043
|
+
Failed: "provision.failed",
|
|
3044
|
+
/** No deploy strategy is installed for the platform. */
|
|
3045
|
+
StrategyUnavailable: "provision.strategy-unavailable",
|
|
3046
|
+
/** A Nitro or Nuxt build uses a preset that the deploy cannot ship. */
|
|
3047
|
+
HostManagedUnsupported: "provision.host-managed-unsupported"
|
|
3048
|
+
};
|
|
3049
|
+
//#endregion
|
|
2046
3050
|
//#region src/provision-command.ts
|
|
2047
3051
|
/**
|
|
3052
|
+
* Builds the `heximon provision --json` document. The run fields are set as the run reaches them, so a
|
|
3053
|
+
* failure document still says how far the run got.
|
|
3054
|
+
*/
|
|
3055
|
+
var ProvisionDocument = class ProvisionDocument {
|
|
3056
|
+
environment;
|
|
3057
|
+
dryRun;
|
|
3058
|
+
target;
|
|
3059
|
+
credentials;
|
|
3060
|
+
/**
|
|
3061
|
+
* @param environment - The deploy environment.
|
|
3062
|
+
* @param dryRun - Whether the run creates nothing.
|
|
3063
|
+
*/
|
|
3064
|
+
constructor(environment, dryRun) {
|
|
3065
|
+
this.environment = environment;
|
|
3066
|
+
this.dryRun = dryRun;
|
|
3067
|
+
}
|
|
3068
|
+
/** Record the resolved target. */
|
|
3069
|
+
resolved(target) {
|
|
3070
|
+
this.target = {
|
|
3071
|
+
platform: target.platformName ?? "node",
|
|
3072
|
+
targetClass: target.targetClass,
|
|
3073
|
+
hostManaged: target.hostManaged === true,
|
|
3074
|
+
...target.artifactDir === void 0 ? {} : { artifactDir: target.artifactDir }
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
/** Record the names of the resolved credential variables. */
|
|
3078
|
+
resolvedCredentials(credentials) {
|
|
3079
|
+
this.credentials = [...credentials.keys()];
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* The document of the run.
|
|
3083
|
+
*
|
|
3084
|
+
* @param ok - Whether the command exits 0.
|
|
3085
|
+
* @param diagnostics - The diagnostics of the run.
|
|
3086
|
+
* @param outcome - The provisioning outcome, once the run has one.
|
|
3087
|
+
* @returns The document.
|
|
3088
|
+
*/
|
|
3089
|
+
document(ok, diagnostics, outcome) {
|
|
3090
|
+
return {
|
|
3091
|
+
ok,
|
|
3092
|
+
diagnostics,
|
|
3093
|
+
environment: this.environment,
|
|
3094
|
+
dryRun: this.dryRun,
|
|
3095
|
+
...this.target === void 0 ? {} : { target: this.target },
|
|
3096
|
+
...this.credentials === void 0 ? {} : { credentials: this.credentials },
|
|
3097
|
+
...outcome?.result === void 0 ? {} : { result: ProvisionDocument.resultOf(outcome.result) },
|
|
3098
|
+
...outcome?.savedIds === void 0 ? {} : { savedIds: {
|
|
3099
|
+
path: outcome.savedIds.path,
|
|
3100
|
+
names: outcome.savedIds.names
|
|
3101
|
+
} }
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
/** Copy the provisioning result into its document shape. */
|
|
3105
|
+
static resultOf(result) {
|
|
3106
|
+
return {
|
|
3107
|
+
platform: result.platform,
|
|
3108
|
+
provisioned: result.provisioned.map((provisioned) => ({
|
|
3109
|
+
resource: provisioned.resource,
|
|
3110
|
+
id: provisioned.id,
|
|
3111
|
+
...provisioned.envVar === void 0 ? {} : { envVar: provisioned.envVar }
|
|
3112
|
+
})),
|
|
3113
|
+
existing: result.existing,
|
|
3114
|
+
failed: result.failed,
|
|
3115
|
+
messages: result.messages
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
};
|
|
3119
|
+
/**
|
|
2048
3120
|
* `heximon provision` — the PRE-BUILD verb for resources whose id the build itself needs (a Cloudflare
|
|
2049
3121
|
* Hyperdrive binding with an empty `id` fails the build outright, and provisioning is what mints that id).
|
|
2050
3122
|
* It calls exactly one seam, `strategy.provisionInfrastructure(context)` — never the deploy-time
|
|
@@ -2061,6 +3133,7 @@ const provisionCommand = defineCommand({
|
|
|
2061
3133
|
description: "Project root directory.",
|
|
2062
3134
|
default: "."
|
|
2063
3135
|
},
|
|
3136
|
+
json: CommandOutput.jsonFlag,
|
|
2064
3137
|
env: {
|
|
2065
3138
|
type: "string",
|
|
2066
3139
|
description: "Deploy environment (production / staging / preview:<id>).",
|
|
@@ -2086,61 +3159,97 @@ const provisionCommand = defineCommand({
|
|
|
2086
3159
|
}
|
|
2087
3160
|
},
|
|
2088
3161
|
async run({ args }) {
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
log.info(`Platform: ${target.platformName ?? "node (default)"} · target class: ${target.targetClass}`);
|
|
2101
|
-
const specs = CredentialSelection.inPlay(target.strategy.requiredCredentials(), { provisioning: true });
|
|
2102
|
-
const credentials = await CredentialResolver.resolve(specs, {
|
|
2103
|
-
interactive,
|
|
2104
|
-
prompter: interactive ? new ClackPrompter() : void 0
|
|
2105
|
-
});
|
|
2106
|
-
const progress = CliSpinner.begin(interactive, args["dry-run"] === true ? "Previewing the foundational resources…" : "Provisioning foundational resources…");
|
|
2107
|
-
let outcome;
|
|
2108
|
-
try {
|
|
2109
|
-
outcome = await DeployRunner.provisionInfrastructure({
|
|
2110
|
-
target,
|
|
2111
|
-
rootDir,
|
|
2112
|
-
environment,
|
|
2113
|
-
credentials,
|
|
2114
|
-
dryRun: args["dry-run"],
|
|
2115
|
-
saveIds: args["save-ids"]
|
|
2116
|
-
});
|
|
2117
|
-
} catch (provisionError) {
|
|
2118
|
-
progress?.stop("Provisioning failed — see the error below.");
|
|
2119
|
-
throw provisionError;
|
|
2120
|
-
}
|
|
2121
|
-
progress?.stop("Provision steps complete.");
|
|
2122
|
-
if (outcome.unsupported) {
|
|
2123
|
-
cancel(`Platform ${outcome.platformName ?? "node (default)"} has no foundational resources to provision.`);
|
|
3162
|
+
const commandOutput = CommandOutput.resolve("provision", args);
|
|
3163
|
+
await commandOutput.run(async () => {
|
|
3164
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
3165
|
+
const environment = args.env;
|
|
3166
|
+
const dryRun = args["dry-run"];
|
|
3167
|
+
const interactive = commandOutput.isInteractive();
|
|
3168
|
+
const report = new ProvisionDocument(environment, dryRun);
|
|
3169
|
+
intro(`heximon provision · ${environment}`);
|
|
3170
|
+
if (args.profile !== void 0) {
|
|
3171
|
+
const message = "--profile is not supported by `heximon provision` — provisioning talks to the Cloudflare REST API with CLOUDFLARE_API_TOKEN, which a wrangler auth profile does not carry. Set the token instead.";
|
|
3172
|
+
cancel(message);
|
|
2124
3173
|
process.exitCode = 1;
|
|
3174
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(CliDiagnosticCode.InvalidOptions, message, { hint: "Remove --profile and set CLOUDFLARE_API_TOKEN." })]));
|
|
2125
3175
|
return;
|
|
2126
3176
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
3177
|
+
try {
|
|
3178
|
+
const target = await ConfigSidecar.during(rootDir, environment, () => DeployTargetResolver.resolve(rootDir, environment, args.platform));
|
|
3179
|
+
report.resolved(target);
|
|
3180
|
+
log.info(`Platform: ${target.platformName ?? "node (default)"} · target class: ${target.targetClass}`);
|
|
3181
|
+
const specs = CredentialSelection.inPlay(target.strategy.requiredCredentials(), { provisioning: true });
|
|
3182
|
+
const credentials = await CredentialResolver.resolve(specs, {
|
|
3183
|
+
interactive: interactive || commandOutput.isJson(),
|
|
3184
|
+
prompter: commandOutput.prompter(CredentialResolver.optionFor(specs))
|
|
3185
|
+
});
|
|
3186
|
+
report.resolvedCredentials(credentials);
|
|
3187
|
+
const progress = CliSpinner.begin(interactive, dryRun ? "Previewing the foundational resources…" : "Provisioning foundational resources…");
|
|
3188
|
+
let outcome;
|
|
3189
|
+
try {
|
|
3190
|
+
outcome = await DeployRunner.provisionInfrastructure({
|
|
3191
|
+
target,
|
|
3192
|
+
rootDir,
|
|
3193
|
+
environment,
|
|
3194
|
+
credentials,
|
|
3195
|
+
dryRun,
|
|
3196
|
+
saveIds: args["save-ids"]
|
|
3197
|
+
});
|
|
3198
|
+
} catch (provisionError) {
|
|
3199
|
+
progress?.stop("Provisioning failed — see the error below.");
|
|
3200
|
+
throw provisionError;
|
|
3201
|
+
}
|
|
3202
|
+
progress?.stop("Provision steps complete.");
|
|
3203
|
+
if (outcome.unsupported) {
|
|
3204
|
+
const message = `Platform ${outcome.platformName ?? "node (default)"} has no foundational resources to provision.`;
|
|
3205
|
+
cancel(message);
|
|
3206
|
+
process.exitCode = 1;
|
|
3207
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(ProvisionDiagnosticCode.Unsupported, message)], outcome));
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
const result = outcome.result;
|
|
3211
|
+
for (const message of result?.messages ?? []) log.message(message);
|
|
3212
|
+
for (const provisioned of result?.provisioned ?? []) log.success(`Provisioned ${provisioned.resource} → id ${provisioned.id}.`);
|
|
3213
|
+
if (outcome.savedIds !== void 0) log.success(`Recorded ${outcome.savedIds.names.join(", ")} in ${outcome.savedIds.path} — \`heximon deploy\` injects it into the build.`);
|
|
3214
|
+
if (result !== void 0 && result.failed.length > 0) {
|
|
3215
|
+
process.exitCode = 1;
|
|
3216
|
+
cancel(`Provisioning failed for: ${result.failed.join(", ")} — see above.`);
|
|
3217
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(ProvisionDiagnosticCode.Failed, `Provisioning failed for: ${result.failed.join(", ")}.`, { hint: "The result messages name the cause for each resource." })], outcome));
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
outro(outcome.savedIds !== void 0 ? "Provisioning complete — ids recorded in the deploy ledger; deploy when ready." : "Provisioning complete — record the ids above in your config, then deploy.");
|
|
3221
|
+
commandOutput.emit(report.document(true, [], outcome));
|
|
3222
|
+
} catch (error) {
|
|
3223
|
+
if (DeployCommandFailure.report(error)) {
|
|
3224
|
+
const diagnostic = DeployCommandFailure.diagnosticOf(error, ProvisionDiagnosticCode);
|
|
3225
|
+
if (diagnostic !== void 0) commandOutput.emit(report.document(false, [diagnostic]));
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
if (error instanceof CommandFailure && commandOutput.isJson()) {
|
|
3229
|
+
commandOutput.emit(report.document(false, error.diagnostics));
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
throw error;
|
|
2135
3233
|
}
|
|
2136
|
-
|
|
2137
|
-
} catch (error) {
|
|
2138
|
-
if (DeployCommandFailure.report(error)) return;
|
|
2139
|
-
throw error;
|
|
2140
|
-
}
|
|
3234
|
+
});
|
|
2141
3235
|
}
|
|
2142
3236
|
});
|
|
2143
3237
|
//#endregion
|
|
3238
|
+
//#region src/routes-json-document.ts
|
|
3239
|
+
/** The stable codes of the failures `heximon routes` raises itself. */
|
|
3240
|
+
const RoutesDiagnosticCode = {
|
|
3241
|
+
/** The compile threw before the command could read a route census. */
|
|
3242
|
+
ReadFailed: "routes.read-failed",
|
|
3243
|
+
/** `--boot` could not load or boot the app. A compile error of the boot gives the compiler diagnostics instead. */
|
|
3244
|
+
BootFailed: "routes.boot-failed",
|
|
3245
|
+
/** `--boot` booted the app, but its live route table could not be read. */
|
|
3246
|
+
TableUnreadable: "routes.table-unreadable",
|
|
3247
|
+
/** `--boot` read the route table, but the booted app did not dispose cleanly; a client can still be open. */
|
|
3248
|
+
DisposeFailed: "routes.dispose-failed",
|
|
3249
|
+
/** The `--out` file could not be written. */
|
|
3250
|
+
OutWriteFailed: "routes.out-write-failed"
|
|
3251
|
+
};
|
|
3252
|
+
//#endregion
|
|
2144
3253
|
//#region src/route-table-report.ts
|
|
2145
3254
|
/**
|
|
2146
3255
|
* Renders a {@link RouteCensusRow} list as an aligned, columnar route table for the terminal —
|
|
@@ -2228,18 +3337,39 @@ var TransformBoot = class TransformBoot {
|
|
|
2228
3337
|
return process.env[TransformBoot.transformMarker] === "1";
|
|
2229
3338
|
}
|
|
2230
3339
|
/**
|
|
3340
|
+
* Re-run this exact CLI invocation under `node --experimental-transform-types`, and set the exit status of
|
|
3341
|
+
* the child. The child prints the whole output, `--json` document included.
|
|
3342
|
+
*
|
|
3343
|
+
* A child that does not start printed nothing, so this process prints the failure: a document with
|
|
3344
|
+
* {@link CliDiagnosticCode.ReexecFailed} under `--json` (also written to the `--out` file), else one stderr
|
|
3345
|
+
* line. A child that a signal stops has no result, so it gets a stderr line and no document.
|
|
3346
|
+
*
|
|
3347
|
+
* @param commandLabel - The command name for the stderr lines (`routes --boot`, `task run`).
|
|
3348
|
+
* @param commandOutput - The output owner of the run.
|
|
3349
|
+
* @param outFile - The `--out` file of a command that has one.
|
|
3350
|
+
*/
|
|
3351
|
+
static async relaunch(commandLabel, commandOutput, outFile) {
|
|
3352
|
+
const exit = await TransformBoot.reexecUnderTransform(commandLabel);
|
|
3353
|
+
if (exit.startError === void 0) {
|
|
3354
|
+
process.exitCode = exit.exitCode;
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
const failure = CommandFailure.of(CliDiagnosticCode.ReexecFailed, `Could not start the child process that runs the app's TypeScript source (${process.execPath}): ` + exit.startError.message, "Check that this Node.js executable can run, then run the command again.");
|
|
3358
|
+
const fail = () => Promise.reject(failure);
|
|
3359
|
+
await commandOutput.run(() => outFile === void 0 ? fail() : outFile.run(fail));
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
2231
3362
|
* Re-run this exact CLI invocation under `node --experimental-transform-types`, inheriting stdio so the
|
|
2232
3363
|
* child's output and prompts reach the terminal directly.
|
|
2233
3364
|
*
|
|
2234
3365
|
* The child owns an interrupt: a terminal sends Ctrl-C to both processes, and the child cleans up. A
|
|
2235
3366
|
* `SIGTERM` or `SIGHUP` that reaches only this process is passed on, so the child never outlives it.
|
|
2236
3367
|
*
|
|
2237
|
-
* A child that
|
|
2238
|
-
*
|
|
2239
|
-
* silent run reads as "the command found a problem" rather than "the command never ran".
|
|
3368
|
+
* A child that a signal killed reports no exit code. It becomes `1`, NAMED on stderr first, because a bare
|
|
3369
|
+
* `exit 1` after a silent run reads as "the command found a problem" rather than "the command was stopped".
|
|
2240
3370
|
*
|
|
2241
|
-
* @param commandLabel - The command name for the
|
|
2242
|
-
* @returns The
|
|
3371
|
+
* @param commandLabel - The command name for the stderr line of a stopped child.
|
|
3372
|
+
* @returns The exit code of the child (1 when it did not report one), or the error of a child that did not start.
|
|
2243
3373
|
*/
|
|
2244
3374
|
static reexecUnderTransform(commandLabel) {
|
|
2245
3375
|
return new Promise((resolve) => {
|
|
@@ -2263,19 +3393,24 @@ var TransformBoot = class TransformBoot {
|
|
|
2263
3393
|
process.on(signal, forward);
|
|
2264
3394
|
return [signal, forward];
|
|
2265
3395
|
});
|
|
3396
|
+
let settled = false;
|
|
2266
3397
|
process.on("SIGINT", ignoreInterrupt);
|
|
2267
|
-
const finish = (
|
|
3398
|
+
const finish = (exit) => {
|
|
3399
|
+
if (settled) return;
|
|
3400
|
+
settled = true;
|
|
2268
3401
|
process.off("SIGINT", ignoreInterrupt);
|
|
2269
3402
|
for (const [signal, forward] of forwarded) process.off(signal, forward);
|
|
2270
|
-
resolve(
|
|
3403
|
+
resolve(exit);
|
|
2271
3404
|
};
|
|
2272
3405
|
child.once("error", (error) => {
|
|
2273
|
-
|
|
2274
|
-
|
|
3406
|
+
if (child.pid === void 0) finish({
|
|
3407
|
+
exitCode: 1,
|
|
3408
|
+
startError: error
|
|
3409
|
+
});
|
|
2275
3410
|
});
|
|
2276
3411
|
child.once("close", (code, signal) => {
|
|
2277
|
-
if (signal !== null) process.stderr.write(`heximon ${commandLabel}: the transform-enabled child was killed by ${signal}.\n`);
|
|
2278
|
-
finish(code ?? 1);
|
|
3412
|
+
if (signal !== null && !settled) process.stderr.write(`heximon ${commandLabel}: the transform-enabled child was killed by ${signal}.\n`);
|
|
3413
|
+
finish({ exitCode: code ?? 1 });
|
|
2279
3414
|
});
|
|
2280
3415
|
});
|
|
2281
3416
|
}
|
|
@@ -2397,14 +3532,10 @@ const routesCommand = defineCommand({
|
|
|
2397
3532
|
description: "The `.env.<mode>` overlay to load first; also the config `$env`. Default development.",
|
|
2398
3533
|
default: "development"
|
|
2399
3534
|
},
|
|
2400
|
-
json:
|
|
2401
|
-
type: "boolean",
|
|
2402
|
-
description: "Emit the route table as JSON on stdout instead of a table.",
|
|
2403
|
-
default: false
|
|
2404
|
-
},
|
|
3535
|
+
json: CommandOutput.jsonFlag,
|
|
2405
3536
|
out: {
|
|
2406
3537
|
type: "string",
|
|
2407
|
-
description: "
|
|
3538
|
+
description: "Also write the JSON document to this file, creating missing parent directories — the CI-artifact target."
|
|
2408
3539
|
},
|
|
2409
3540
|
boot: {
|
|
2410
3541
|
type: "boolean",
|
|
@@ -2420,24 +3551,27 @@ const routesCommand = defineCommand({
|
|
|
2420
3551
|
},
|
|
2421
3552
|
async run({ args }) {
|
|
2422
3553
|
const asBoot = args.boot;
|
|
3554
|
+
const commandOutput = CommandOutput.resolve("routes", args);
|
|
3555
|
+
const outFile = JsonOutputTarget.resolve(args.out);
|
|
2423
3556
|
if (asBoot && !TransformBoot.isBootProcess()) {
|
|
2424
|
-
|
|
2425
|
-
return;
|
|
2426
|
-
}
|
|
2427
|
-
const rootDir = resolve(process.cwd(), args.cwd);
|
|
2428
|
-
const asJson = args.json;
|
|
2429
|
-
const jsonOutput = JsonOutputTarget.resolve(args.out, asJson);
|
|
2430
|
-
const resolved = CompileModeResolver.resolve("routes", rootDir, args, SourceDirectories.resolve(rootDir, args.src));
|
|
2431
|
-
if (resolved === void 0) return;
|
|
2432
|
-
if (!asJson) intro(`heximon routes — ${basename(rootDir)} · ${CompileModeResolver.stamp(resolved)}`);
|
|
2433
|
-
if (asBoot) {
|
|
2434
|
-
await RoutesCommand.runBoot(rootDir, args, asJson, jsonOutput);
|
|
3557
|
+
if (await ProjectRoot.checkBeforeRelaunch(args.cwd, commandOutput)) await TransformBoot.relaunch("routes --boot", commandOutput, outFile);
|
|
2435
3558
|
return;
|
|
2436
3559
|
}
|
|
2437
|
-
await
|
|
3560
|
+
await commandOutput.run(async () => {
|
|
3561
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
3562
|
+
await outFile.run(async () => {
|
|
3563
|
+
const resolved = CompileModeResolver.resolve(rootDir, args, SourceDirectories.resolve(rootDir, args.src));
|
|
3564
|
+
if (!commandOutput.isJson()) intro(`heximon routes — ${basename(rootDir)} · ${CompileModeResolver.stamp(resolved)}`);
|
|
3565
|
+
if (asBoot) {
|
|
3566
|
+
await RoutesCommand.runBoot(rootDir, args, commandOutput, outFile);
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3569
|
+
await RoutesCommand.runCompile(rootDir, args, commandOutput, outFile, resolved.mode);
|
|
3570
|
+
});
|
|
3571
|
+
});
|
|
2438
3572
|
}
|
|
2439
3573
|
});
|
|
2440
|
-
/** Output helpers for {@link routesCommand}: the two read paths, the table / JSON
|
|
3574
|
+
/** Output helpers for {@link routesCommand}: the two read paths, the table / JSON document, and the failure summary. */
|
|
2441
3575
|
var RoutesCommand = class RoutesCommand {
|
|
2442
3576
|
/**
|
|
2443
3577
|
* The compile-time read: run the same from-disk compile `heximon check` runs and report the route census
|
|
@@ -2449,8 +3583,8 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2449
3583
|
* route table is readable without composing it into an app. The `requires` a library leaves open for its
|
|
2450
3584
|
* host are not diagnostics there, so nothing floods the report.
|
|
2451
3585
|
*/
|
|
2452
|
-
static async runCompile(rootDir, args,
|
|
2453
|
-
let
|
|
3586
|
+
static async runCompile(rootDir, args, commandOutput, outFile, mode = CompileMode.App) {
|
|
3587
|
+
let compiled;
|
|
2454
3588
|
try {
|
|
2455
3589
|
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
2456
3590
|
DotenvCascade.loadInto(rootDir, args.env);
|
|
@@ -2459,24 +3593,21 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2459
3593
|
environment: args.env,
|
|
2460
3594
|
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
2461
3595
|
});
|
|
2462
|
-
|
|
2463
|
-
const failure = output.diagnostics.find((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
2464
|
-
if (failure !== void 0) {
|
|
2465
|
-
RoutesCommand.reportFailure(new Error(failure.message), asJson, jsonOutput);
|
|
2466
|
-
process.exitCode = 1;
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
|
-
const file = output.files.find((generated) => generated.path === RouteCensus.fileName);
|
|
2470
|
-
census = (file === void 0 ? void 0 : RouteCensus.parse(file.content)) ?? {
|
|
2471
|
-
routes: [],
|
|
2472
|
-
unresolved: []
|
|
2473
|
-
};
|
|
3596
|
+
compiled = mode === CompileMode.Library ? session.compileLibrary() : session.compile();
|
|
2474
3597
|
} catch (error) {
|
|
2475
|
-
RoutesCommand.reportFailure(error,
|
|
2476
|
-
process.exitCode = 1;
|
|
3598
|
+
RoutesCommand.reportFailure(RoutesCommand.diagnosticsOf(RoutesDiagnosticCode.ReadFailed, error), commandOutput, outFile);
|
|
2477
3599
|
return;
|
|
2478
3600
|
}
|
|
2479
|
-
|
|
3601
|
+
if (compiled.diagnostics.some((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error)) {
|
|
3602
|
+
RoutesCommand.reportFailure(CliDiagnostics.fromCompiler(compiled.diagnostics), commandOutput, outFile);
|
|
3603
|
+
return;
|
|
3604
|
+
}
|
|
3605
|
+
const file = compiled.files.find((generated) => generated.path === RouteCensus.fileName);
|
|
3606
|
+
const census = (file === void 0 ? void 0 : RouteCensus.parse(file.content)) ?? {
|
|
3607
|
+
routes: [],
|
|
3608
|
+
unresolved: []
|
|
3609
|
+
};
|
|
3610
|
+
RoutesCommand.report(census.routes, census.unresolved, commandOutput, outFile, {
|
|
2480
3611
|
libraryMode: mode === CompileMode.Library,
|
|
2481
3612
|
...census.scopeEnforcement === void 0 ? {} : { scopeEnforcement: census.scopeEnforcement }
|
|
2482
3613
|
});
|
|
@@ -2494,17 +3625,19 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2494
3625
|
* subcommand thunk while building the completion tree, so citty's lazy `Resolvable` form resolves the
|
|
2495
3626
|
* module anyway.
|
|
2496
3627
|
*/
|
|
2497
|
-
static async runBoot(rootDir, args,
|
|
3628
|
+
static async runBoot(rootDir, args, commandOutput, outFile) {
|
|
2498
3629
|
let app;
|
|
2499
3630
|
let inventory;
|
|
3631
|
+
let testing;
|
|
2500
3632
|
try {
|
|
2501
3633
|
TransformBoot.registerSourceResolver();
|
|
2502
3634
|
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
2503
3635
|
DotenvCascade.loadInto(rootDir, args.env);
|
|
2504
|
-
const config = await
|
|
2505
|
-
const [
|
|
3636
|
+
const config = await ConfigSidecar.load(rootDir, args.env);
|
|
3637
|
+
const [testingModule, { RouteInventory }] = await Promise.all([import("@heximon/testing"), import("@heximon/http")]);
|
|
3638
|
+
testing = testingModule;
|
|
2506
3639
|
inventory = RouteInventory;
|
|
2507
|
-
app = await
|
|
3640
|
+
app = await testingModule.createTestApp({
|
|
2508
3641
|
root: rootDir,
|
|
2509
3642
|
printWarnings: false,
|
|
2510
3643
|
...sourceDirectories === void 0 ? {} : { sourceDirectories },
|
|
@@ -2513,25 +3646,28 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2513
3646
|
...config?.platform === void 0 ? {} : { platform: config.platform }
|
|
2514
3647
|
});
|
|
2515
3648
|
} catch (error) {
|
|
2516
|
-
RoutesCommand.reportFailure(error,
|
|
2517
|
-
process.exitCode = 1;
|
|
3649
|
+
RoutesCommand.reportFailure(testing !== void 0 && error instanceof testing.TestAppCompileError ? CliDiagnostics.fromCompiler(error.diagnostics) : RoutesCommand.diagnosticsOf(RoutesDiagnosticCode.BootFailed, error), commandOutput, outFile);
|
|
2518
3650
|
return;
|
|
2519
3651
|
}
|
|
3652
|
+
let routes;
|
|
3653
|
+
const failures = [];
|
|
2520
3654
|
try {
|
|
2521
|
-
|
|
2522
|
-
RoutesCommand.report(routes, [], asJson, jsonOutput);
|
|
3655
|
+
routes = inventory.describe(app).map(RoutesCommand.toCensusRow);
|
|
2523
3656
|
} catch (error) {
|
|
2524
|
-
RoutesCommand.
|
|
2525
|
-
process.exitCode = 1;
|
|
3657
|
+
failures.push(RoutesCommand.diagnosticOf(RoutesDiagnosticCode.TableUnreadable, error));
|
|
2526
3658
|
} finally {
|
|
2527
3659
|
try {
|
|
2528
3660
|
await app.context.drain({ timeoutMs: 5e3 });
|
|
2529
3661
|
await app[Symbol.asyncDispose]();
|
|
2530
3662
|
} catch (disposeError) {
|
|
2531
|
-
|
|
2532
|
-
if (!asJson) log.warn(`The route table was read, but disposing the booted app failed — a client may still be open: ${disposeError instanceof Error ? disposeError.message : String(disposeError)}`);
|
|
3663
|
+
failures.push(CliDiagnostics.error(RoutesDiagnosticCode.DisposeFailed, `Disposing the booted app failed — a client may still be open: ${disposeError instanceof Error ? disposeError.message : String(disposeError)}`));
|
|
2533
3664
|
}
|
|
2534
3665
|
}
|
|
3666
|
+
if (routes === void 0) {
|
|
3667
|
+
RoutesCommand.reportFailure(failures, commandOutput, outFile);
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
RoutesCommand.report(routes, [], commandOutput, outFile, { failures });
|
|
2535
3671
|
}
|
|
2536
3672
|
/**
|
|
2537
3673
|
* Map one booted route to a census row. An entry whose {@link RouteInventoryEntry.scopes} is defined (every
|
|
@@ -2549,23 +3685,29 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2549
3685
|
};
|
|
2550
3686
|
}
|
|
2551
3687
|
/**
|
|
2552
|
-
*
|
|
3688
|
+
* Report the resolved routes: the JSON document to stdout under `--json` and to the `--out` file, and an
|
|
3689
|
+
* aligned table unless `--json`. A failure or a failed `--out` write sets exit status 1.
|
|
2553
3690
|
*
|
|
2554
3691
|
* @param options - `libraryMode` adds the one fact a package's own table cannot show: the published root's
|
|
2555
3692
|
* default mount is a manifest fact a consuming host applies or replaces, so it is not folded into these
|
|
2556
|
-
* paths. `scopeEnforcement` is the census section of the same name, passed through to the JSON
|
|
2557
|
-
* so a CI job reads the scope-enforcer verdict from the same artifact.
|
|
3693
|
+
* paths. `scopeEnforcement` is the census section of the same name, passed through to the JSON document
|
|
3694
|
+
* so a CI job reads the scope-enforcer verdict from the same artifact. `failures` are errors that fail
|
|
3695
|
+
* the run but keep the table it read.
|
|
2558
3696
|
*/
|
|
2559
|
-
static report(routes, unresolved,
|
|
2560
|
-
const
|
|
2561
|
-
|
|
3697
|
+
static report(routes, unresolved, commandOutput, outFile, options = {}) {
|
|
3698
|
+
const failures = options.failures ?? [];
|
|
3699
|
+
const document = {
|
|
3700
|
+
ok: failures.length === 0,
|
|
3701
|
+
diagnostics: failures,
|
|
2562
3702
|
routes,
|
|
2563
3703
|
unresolved,
|
|
2564
3704
|
...options.scopeEnforcement === void 0 ? {} : { scopeEnforcement: options.scopeEnforcement }
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
if (
|
|
2568
|
-
if (
|
|
3705
|
+
};
|
|
3706
|
+
const written = outFile.publish(document, commandOutput, RoutesDiagnosticCode.OutWriteFailed);
|
|
3707
|
+
if (!written || failures.length > 0) process.exitCode = 1;
|
|
3708
|
+
if (commandOutput.isJson()) return;
|
|
3709
|
+
if (written && outFile.isFile()) log.success(`Route JSON written to ${outFile.filePath}.`);
|
|
3710
|
+
for (const failure of failures) log.warn(failure.message);
|
|
2569
3711
|
if (routes.length === 0 && unresolved.length === 0) {
|
|
2570
3712
|
outro(options.libraryMode === true ? "No HTTP routes — this package serves none, or its HTTP plugin is not active." : "No HTTP routes — this app serves none, or its HTTP plugin is not active.");
|
|
2571
3713
|
return;
|
|
@@ -2580,20 +3722,34 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2580
3722
|
outro(`${routes.length} route${routes.length === 1 ? "" : "s"}; ${unresolved.length} unresolved` + (options.libraryMode === true ? "." : " — re-run with --boot to enumerate them."));
|
|
2581
3723
|
}
|
|
2582
3724
|
/**
|
|
2583
|
-
* Report a compile / boot failure:
|
|
2584
|
-
* unless `--json`. The
|
|
2585
|
-
* rather than leaving a stale success
|
|
3725
|
+
* Report a compile / boot failure: a {@link CliFailureDocument} to stdout under `--json` and to the `--out`
|
|
3726
|
+
* file, and a clack error + outro unless `--json`. The file gets the failure too, so a new run
|
|
3727
|
+
* replaces the previous artifact rather than leaving a stale success. Sets exit status 1.
|
|
3728
|
+
*
|
|
3729
|
+
* @param diagnostics - The diagnostics of the failure: the compiler's, or one the command raised.
|
|
3730
|
+
* @param commandOutput - The stdout owner of the run.
|
|
3731
|
+
* @param outFile - The `--out` file.
|
|
2586
3732
|
*/
|
|
2587
|
-
static reportFailure(
|
|
2588
|
-
const
|
|
2589
|
-
if (!jsonOutput.emit({
|
|
3733
|
+
static reportFailure(diagnostics, commandOutput, outFile) {
|
|
3734
|
+
const document = {
|
|
2590
3735
|
ok: false,
|
|
2591
|
-
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
|
|
3736
|
+
diagnostics
|
|
3737
|
+
};
|
|
3738
|
+
outFile.publish(document, commandOutput, RoutesDiagnosticCode.OutWriteFailed);
|
|
3739
|
+
process.exitCode = 1;
|
|
3740
|
+
if (commandOutput.isJson()) return;
|
|
3741
|
+
const messages = diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).map((diagnostic) => diagnostic.message);
|
|
3742
|
+
log.error(`Could not read the app's routes:\n${messages.join("\n")}`);
|
|
2595
3743
|
outro("No route table to show.");
|
|
2596
3744
|
}
|
|
3745
|
+
/** One error diagnostic of the command, with the message of what was thrown. */
|
|
3746
|
+
static diagnosticOf(code, error) {
|
|
3747
|
+
return CliDiagnostics.error(code, error instanceof Error ? error.message : String(error));
|
|
3748
|
+
}
|
|
3749
|
+
/** The diagnostics of a thrown value: those of a {@link CommandFailure}, else one with `code`. */
|
|
3750
|
+
static diagnosticsOf(code, error) {
|
|
3751
|
+
return error instanceof CommandFailure ? error.diagnostics : [RoutesCommand.diagnosticOf(code, error)];
|
|
3752
|
+
}
|
|
2597
3753
|
/**
|
|
2598
3754
|
* The unresolved-gap block: one line per controller (or package) whose routes the census could not name.
|
|
2599
3755
|
* A composed package whose manifest section IS readable contributes rows instead, so nothing here is a
|
|
@@ -3460,38 +4616,52 @@ var PlatformBindingCensus = class PlatformBindingCensus {
|
|
|
3460
4616
|
}
|
|
3461
4617
|
};
|
|
3462
4618
|
//#endregion
|
|
3463
|
-
//#region src/
|
|
3464
|
-
/**
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
*/
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
4619
|
+
//#region src/task-run-json-document.ts
|
|
4620
|
+
/** The stable codes of the failures `heximon task run --json` reports. */
|
|
4621
|
+
const TaskDiagnosticCode = {
|
|
4622
|
+
/** The `run()` of the task threw. The run exits 2, and the document names the error. */
|
|
4623
|
+
TaskFailed: "task.failed",
|
|
4624
|
+
/** No module or no task of the compiled graph has the name, or the source file does not export the class. */
|
|
4625
|
+
TargetNotFound: "task.target-not-found",
|
|
4626
|
+
/** More than one module, or more than one task of the module, has the name. */
|
|
4627
|
+
TargetAmbiguous: "task.target-ambiguous",
|
|
4628
|
+
/** The named class has no `run()` method. */
|
|
4629
|
+
NotATask: "task.not-a-task",
|
|
4630
|
+
/** The production compile failed. The compiler diagnostics are in the same document. */
|
|
4631
|
+
CompileFailed: "task.compile-failed",
|
|
4632
|
+
/** The project has no app: the compile gave no module graph, or no wiring file. */
|
|
4633
|
+
AppNotFound: "task.app-not-found",
|
|
4634
|
+
/** A Nitro option is computed, so the command cannot read the preset or the source directories. */
|
|
4635
|
+
NitroConfigComputed: "task.nitro-config-computed",
|
|
4636
|
+
/** A packed library that the app imports does not have the factories a production build needs. */
|
|
4637
|
+
LibraryPreflightFailed: "task.library-preflight-failed",
|
|
4638
|
+
/** The graph needs bindings that only its platform has, so nothing was booted. */
|
|
4639
|
+
PlatformBindingsRequired: "task.platform-bindings-required",
|
|
4640
|
+
/** The `@heximon/runtime` of the project is older than the command. */
|
|
4641
|
+
RuntimeOutdated: "task.runtime-outdated",
|
|
4642
|
+
/** The app did not boot. The stack is on stderr. */
|
|
4643
|
+
BootFailed: "task.boot-failed",
|
|
4644
|
+
/** The app could not create the task. */
|
|
4645
|
+
CreateFailed: "task.create-failed",
|
|
4646
|
+
/** The frame of the task did not open, or a context seed threw before `run()`. */
|
|
4647
|
+
StartFailed: "task.start-failed",
|
|
4648
|
+
/** The task returned a value that is not JSON. */
|
|
4649
|
+
ResultNotJson: "task.result-not-json",
|
|
4650
|
+
/** The app did not shut down cleanly after the run. */
|
|
4651
|
+
ShutdownFailed: "task.shutdown-failed"
|
|
3488
4652
|
};
|
|
3489
4653
|
//#endregion
|
|
3490
4654
|
//#region src/task-target-resolver.ts
|
|
3491
4655
|
/** Thrown when the names do not pick exactly one module and one task. The message is ready to print. */
|
|
3492
4656
|
var TaskTargetError = class extends Error {
|
|
3493
|
-
|
|
4657
|
+
code;
|
|
4658
|
+
/**
|
|
4659
|
+
* @param code - {@link TaskDiagnosticCode.TargetNotFound} or {@link TaskDiagnosticCode.TargetAmbiguous}.
|
|
4660
|
+
* @param message - The reason, with the names that come close.
|
|
4661
|
+
*/
|
|
4662
|
+
constructor(code, message) {
|
|
3494
4663
|
super(message);
|
|
4664
|
+
this.code = code;
|
|
3495
4665
|
this.name = "TaskTargetError";
|
|
3496
4666
|
}
|
|
3497
4667
|
};
|
|
@@ -3517,16 +4687,16 @@ var TaskTargetResolver = class TaskTargetResolver {
|
|
|
3517
4687
|
static resolve(graph, moduleName, taskName, boundaries = []) {
|
|
3518
4688
|
const moduleReferences = [...graph.modules.map((node) => node.reference), ...boundaries.map((boundary) => boundary.reference)];
|
|
3519
4689
|
const modules = TaskTargetResolver.distinct(moduleReferences, moduleName);
|
|
3520
|
-
if (modules.length === 0) throw new TaskTargetError(`No module class named "${moduleName}" is in the compiled graph.` + TaskTargetResolver.suggestions(moduleName, moduleReferences.map((reference) => TaskTargetResolver.nameOf(reference)), "Modules in the graph"));
|
|
3521
|
-
if (modules.length > 1) throw new TaskTargetError(`More than one module class is named "${moduleName}":\n` + modules.map((reference) => ` - ${reference.sourceFile}`).join("\n"));
|
|
4690
|
+
if (modules.length === 0) throw new TaskTargetError(TaskDiagnosticCode.TargetNotFound, `No module class named "${moduleName}" is in the compiled graph.` + TaskTargetResolver.suggestions(moduleName, moduleReferences.map((reference) => TaskTargetResolver.nameOf(reference)), "Modules in the graph"));
|
|
4691
|
+
if (modules.length > 1) throw new TaskTargetError(TaskDiagnosticCode.TargetAmbiguous, `More than one module class is named "${moduleName}":\n` + modules.map((reference) => ` - ${reference.sourceFile}`).join("\n"));
|
|
3522
4692
|
const module = modules[0];
|
|
3523
4693
|
const nodes = graph.modules.filter((node) => TaskTargetResolver.isSame(node.reference, module));
|
|
3524
4694
|
const libraries = boundaries.filter((boundary) => TaskTargetResolver.isSame(boundary.reference, module));
|
|
3525
4695
|
const isLibrary = nodes.length === 0 && libraries.length > 0;
|
|
3526
4696
|
const candidates = isLibrary ? libraries.flatMap((boundary) => boundary.exports.map((exported) => exported.token)) : nodes.flatMap((node) => node.providers.map((provider) => provider.token));
|
|
3527
4697
|
const tasks = TaskTargetResolver.distinct(candidates, taskName);
|
|
3528
|
-
if (tasks.length === 0) throw new TaskTargetError((isLibrary ? `The library module ${moduleName} does not export a class named "${taskName}". \`heximon task run\` reaches the tasks that a library module exports: export the task from the module and pack the library again.` : `The module ${moduleName} does not provide a class named "${taskName}".`) + TaskTargetResolver.otherOwner(graph, boundaries, module, taskName) + TaskTargetResolver.suggestions(taskName, candidates.map((token) => TaskTargetResolver.nameOf(token)), isLibrary ? `Exports of ${moduleName}` : `Providers of ${moduleName}`));
|
|
3529
|
-
if (tasks.length > 1) throw new TaskTargetError(`The module ${moduleName} ${isLibrary ? "exports" : "provides"} more than one class named "${taskName}":\n` + tasks.map((reference) => ` - ${reference.sourceFile}`).join("\n"));
|
|
4698
|
+
if (tasks.length === 0) throw new TaskTargetError(TaskDiagnosticCode.TargetNotFound, (isLibrary ? `The library module ${moduleName} does not export a class named "${taskName}". \`heximon task run\` reaches the tasks that a library module exports: export the task from the module and pack the library again.` : `The module ${moduleName} does not provide a class named "${taskName}".`) + TaskTargetResolver.otherOwner(graph, boundaries, module, taskName) + TaskTargetResolver.suggestions(taskName, candidates.map((token) => TaskTargetResolver.nameOf(token)), isLibrary ? `Exports of ${moduleName}` : `Providers of ${moduleName}`));
|
|
4699
|
+
if (tasks.length > 1) throw new TaskTargetError(TaskDiagnosticCode.TargetAmbiguous, `The module ${moduleName} ${isLibrary ? "exports" : "provides"} more than one class named "${taskName}":\n` + tasks.map((reference) => ` - ${reference.sourceFile}`).join("\n"));
|
|
3530
4700
|
return {
|
|
3531
4701
|
module,
|
|
3532
4702
|
task: tasks[0]
|
|
@@ -3687,8 +4857,25 @@ var RefusingInternalClient = class {
|
|
|
3687
4857
|
return Promise.reject(/* @__PURE__ */ new Error(`InternalClient.fetch(${JSON.stringify(input)}) is not available in \`heximon task run\`: the run boots no HTTP surface, so a task cannot call the app's own routes.`));
|
|
3688
4858
|
}
|
|
3689
4859
|
};
|
|
3690
|
-
/** The failure a run reports to the caller; the message is ready to print. */
|
|
3691
|
-
var TaskRunFailure = class extends Error {
|
|
4860
|
+
/** The failure a run reports to the caller; the message is ready to print on stderr. */
|
|
4861
|
+
var TaskRunFailure = class extends Error {
|
|
4862
|
+
code;
|
|
4863
|
+
/** The one-paragraph text of the `--json` diagnostic: the message without a stack trace. */
|
|
4864
|
+
summary;
|
|
4865
|
+
/** How to fix the failure, or where to read more. */
|
|
4866
|
+
hint;
|
|
4867
|
+
/**
|
|
4868
|
+
* @param code - The stable code of the `--json` diagnostic.
|
|
4869
|
+
* @param message - The text for stderr.
|
|
4870
|
+
* @param details - The thrown value, the diagnostic text when it differs from the message, and the hint.
|
|
4871
|
+
*/
|
|
4872
|
+
constructor(code, message, details = {}) {
|
|
4873
|
+
super(message, "cause" in details ? { cause: details.cause } : void 0);
|
|
4874
|
+
this.code = code;
|
|
4875
|
+
this.summary = details.summary ?? message;
|
|
4876
|
+
this.hint = details.hint;
|
|
4877
|
+
}
|
|
4878
|
+
};
|
|
3692
4879
|
/** The task itself threw. The message is ready to print, and the thrown value is the cause. */
|
|
3693
4880
|
var TaskRefusal = class extends TaskRunFailure {};
|
|
3694
4881
|
/**
|
|
@@ -3710,6 +4897,8 @@ var TaskRunner = class TaskRunner {
|
|
|
3710
4897
|
static interruptShutdownMilliseconds = 5e3;
|
|
3711
4898
|
/** The exit code of a run whose task itself threw. Every other failure exits with `1`. */
|
|
3712
4899
|
static refusalExitCode = 2;
|
|
4900
|
+
/** The hint of a diagnostic whose full text, with the stack trace, is on stderr. */
|
|
4901
|
+
static stackHint = "The stack trace is on stderr.";
|
|
3713
4902
|
static wiringFilePattern = /^apps\/.+\.wiring\.js$/;
|
|
3714
4903
|
outputDirectory;
|
|
3715
4904
|
app;
|
|
@@ -3726,6 +4915,10 @@ var TaskRunner = class TaskRunner {
|
|
|
3726
4915
|
stdout = new StdoutRedirect();
|
|
3727
4916
|
/** The result of the task as one line of JSON, printed after the shutdown. */
|
|
3728
4917
|
serialized;
|
|
4918
|
+
/** The diagnostics of the compile, for the `--json` document. */
|
|
4919
|
+
compileDiagnostics = [];
|
|
4920
|
+
/** Why the shutdown failed, when it failed. */
|
|
4921
|
+
shutdownFailure;
|
|
3729
4922
|
disposing;
|
|
3730
4923
|
/** How many package specifiers the run imported, for the file names of {@link ProjectModules.import}. */
|
|
3731
4924
|
packageImports = 0;
|
|
@@ -3733,13 +4926,31 @@ var TaskRunner = class TaskRunner {
|
|
|
3733
4926
|
this.options = options;
|
|
3734
4927
|
}
|
|
3735
4928
|
/**
|
|
3736
|
-
* Run the task.
|
|
4929
|
+
* Run the task and print the text output. Everything the app logs goes to stderr: stdout carries the result
|
|
4930
|
+
* alone.
|
|
3737
4931
|
*
|
|
3738
4932
|
* @returns The process exit code: `0` when the task returned, `2` when the task itself threw (stdout then
|
|
3739
4933
|
* holds one `{"error":{"name":…,"message":…}}` line), `1` for every other outcome. The reason is already
|
|
3740
4934
|
* on stderr.
|
|
3741
4935
|
*/
|
|
3742
4936
|
async run() {
|
|
4937
|
+
let outcome;
|
|
4938
|
+
this.stdout.start();
|
|
4939
|
+
try {
|
|
4940
|
+
outcome = await this.perform();
|
|
4941
|
+
} finally {
|
|
4942
|
+
this.stdout.stop();
|
|
4943
|
+
}
|
|
4944
|
+
if (outcome.line !== void 0) this.stdout.write(`${outcome.line}\n`);
|
|
4945
|
+
return outcome.exitCode;
|
|
4946
|
+
}
|
|
4947
|
+
/**
|
|
4948
|
+
* Run the task without a write to stdout. The caller sends stdout to stderr for the run, because the app
|
|
4949
|
+
* and the host log there; the reason of a failure is already on stderr when this returns.
|
|
4950
|
+
*
|
|
4951
|
+
* @returns The exit code, the text line, and the `--json` document of the run.
|
|
4952
|
+
*/
|
|
4953
|
+
async perform() {
|
|
3743
4954
|
let interrupts = 0;
|
|
3744
4955
|
const interrupt = (signal) => {
|
|
3745
4956
|
interrupts += 1;
|
|
@@ -3748,35 +4959,69 @@ var TaskRunner = class TaskRunner {
|
|
|
3748
4959
|
process.on("SIGINT", interrupt);
|
|
3749
4960
|
process.on("SIGTERM", interrupt);
|
|
3750
4961
|
let exitCode = 0;
|
|
3751
|
-
|
|
4962
|
+
const diagnostics = [];
|
|
4963
|
+
let thrown;
|
|
3752
4964
|
try {
|
|
3753
4965
|
this.outputDirectory = mkdtempSync(join(this.options.rootDir, TaskRunner.outputDirectoryPrefix));
|
|
3754
|
-
this.runtime = await TaskRunner.projectRuntime(this.outputDirectory);
|
|
3755
|
-
const output = await this.compile();
|
|
4966
|
+
this.runtime = await TaskRunner.during(TaskDiagnosticCode.BootFailed, () => TaskRunner.projectRuntime(this.outputDirectory));
|
|
4967
|
+
const output = await TaskRunner.during(TaskDiagnosticCode.CompileFailed, () => this.compile());
|
|
3756
4968
|
const target = this.pickTarget(output);
|
|
3757
4969
|
TaskRunner.refuseBindings(output, this.platformName, this.outputDirectory);
|
|
3758
4970
|
this.materialize(output);
|
|
3759
|
-
await this.boot(output);
|
|
3760
|
-
await this.
|
|
4971
|
+
await TaskRunner.during(TaskDiagnosticCode.BootFailed, () => this.boot(output));
|
|
4972
|
+
await this.runTask(target);
|
|
3761
4973
|
} catch (error) {
|
|
3762
4974
|
TaskRunner.report(error);
|
|
4975
|
+
diagnostics.push(...TaskRunner.diagnosticsOf(error));
|
|
3763
4976
|
if (error instanceof TaskRefusal) {
|
|
3764
4977
|
exitCode = TaskRunner.refusalExitCode;
|
|
3765
|
-
|
|
4978
|
+
thrown = TaskRunner.errorOf(error.cause);
|
|
4979
|
+
this.serialized = JSON.stringify({ error: thrown });
|
|
3766
4980
|
} else exitCode = 1;
|
|
3767
4981
|
} finally {
|
|
3768
4982
|
if (!await this.dispose() && exitCode === 0) exitCode = 1;
|
|
3769
|
-
this.stdout.stop();
|
|
3770
|
-
if (this.serialized !== void 0) this.stdout.write(`${this.serialized}\n`);
|
|
3771
4983
|
process.off("SIGINT", interrupt);
|
|
3772
4984
|
process.off("SIGTERM", interrupt);
|
|
3773
4985
|
}
|
|
3774
|
-
|
|
4986
|
+
if (this.shutdownFailure !== void 0) diagnostics.push(CliDiagnostics.error(TaskDiagnosticCode.ShutdownFailed, this.shutdownFailure));
|
|
4987
|
+
return {
|
|
4988
|
+
exitCode,
|
|
4989
|
+
...this.serialized === void 0 ? {} : { line: this.serialized },
|
|
4990
|
+
document: {
|
|
4991
|
+
ok: exitCode === 0,
|
|
4992
|
+
diagnostics: [...this.compileDiagnostics, ...diagnostics],
|
|
4993
|
+
module: this.options.moduleName,
|
|
4994
|
+
task: this.options.taskName,
|
|
4995
|
+
...thrown === void 0 ? TaskRunner.resultField(this.serialized) : { error: thrown }
|
|
4996
|
+
}
|
|
4997
|
+
};
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
5000
|
+
* Run one step of the run. A failure the step does not name itself becomes a {@link TaskRunFailure} with
|
|
5001
|
+
* `code`; its stderr text stays the stack trace of the thrown value.
|
|
5002
|
+
*/
|
|
5003
|
+
static async during(code, step) {
|
|
5004
|
+
try {
|
|
5005
|
+
return await step();
|
|
5006
|
+
} catch (error) {
|
|
5007
|
+
if (error instanceof TaskRunFailure || error instanceof TaskTargetError || error instanceof CommandFailure) throw error;
|
|
5008
|
+
throw new TaskRunFailure(code, TaskRunner.describe(error), {
|
|
5009
|
+
cause: error,
|
|
5010
|
+
summary: TaskRunner.messageOf(error),
|
|
5011
|
+
hint: TaskRunner.stackHint
|
|
5012
|
+
});
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
/** The `result` field of the document, read back from the printed result so both outputs agree. */
|
|
5016
|
+
static resultField(serialized) {
|
|
5017
|
+
if (serialized === void 0) return {};
|
|
5018
|
+
const parsed = JSON.parse(serialized);
|
|
5019
|
+
return typeof parsed === "object" && parsed !== null && "result" in parsed ? { result: parsed.result } : {};
|
|
3775
5020
|
}
|
|
3776
5021
|
/** The `@heximon/runtime` of the project, the copy the generated wiring imports. */
|
|
3777
5022
|
static async projectRuntime(directory) {
|
|
3778
5023
|
const runtime = await ProjectModules.runtime(directory);
|
|
3779
|
-
if (typeof runtime.StandingWork?.suspend !== "function") throw new TaskRunFailure("The @heximon/runtime of this project is older than this command. Update @heximon/runtime and heximon to one version.");
|
|
5024
|
+
if (typeof runtime.StandingWork?.suspend !== "function") throw new TaskRunFailure(TaskDiagnosticCode.RuntimeOutdated, "The @heximon/runtime of this project is older than this command. Update @heximon/runtime and heximon to one version.");
|
|
3780
5025
|
return runtime;
|
|
3781
5026
|
}
|
|
3782
5027
|
/**
|
|
@@ -3796,13 +5041,13 @@ var TaskRunner = class TaskRunner {
|
|
|
3796
5041
|
if (nitro.isNitroHost) {
|
|
3797
5042
|
const sourceDirectories = TaskRunner.sourceDirectoriesOf(nitro, this.options.isSourceExplicit === true ? this.options.sourceDirectories ?? [] : void 0);
|
|
3798
5043
|
const preset = nitro.preset;
|
|
3799
|
-
const host = await NitroHost.open(rootDir, outputDirectory, {
|
|
5044
|
+
const host = await ConfigSidecar.during(rootDir, "production", () => NitroHost.open(rootDir, outputDirectory, {
|
|
3800
5045
|
preset: preset.kind === "literal" ? preset.value : void 0,
|
|
3801
5046
|
platformFromSidecar: preset.kind === "computed",
|
|
3802
5047
|
aliases: this.aliases,
|
|
3803
5048
|
...sourceDirectories
|
|
3804
|
-
});
|
|
3805
|
-
if (preset.kind === "computed" && !host.hasSidecarPlatform()) throw new TaskRunFailure(`The Nitro preset is computed (${preset.reason}, in ${preset.file}), and the command does not run your config files, so it cannot read the platform: declare \`platform\` in heximon.config.ts (for example \`platform: new CloudflareWorkersStrategy()\`), or set NITRO_PRESET. Nothing was booted.`);
|
|
5049
|
+
}));
|
|
5050
|
+
if (preset.kind === "computed" && !host.hasSidecarPlatform()) throw new TaskRunFailure(TaskDiagnosticCode.NitroConfigComputed, `The Nitro preset is computed (${preset.reason}, in ${preset.file}), and the command does not run your config files, so it cannot read the platform: declare \`platform\` in heximon.config.ts (for example \`platform: new CloudflareWorkersStrategy()\`), or set NITRO_PRESET. Nothing was booted.`);
|
|
3806
5051
|
const compiled = host.compile(rootDir, outputDirectory, this.options.isSourceExplicit === true ? this.options.sourceDirectories : void 0);
|
|
3807
5052
|
({output, compiler} = compiled);
|
|
3808
5053
|
this.platformName = compiled.platformName;
|
|
@@ -3817,10 +5062,13 @@ var TaskRunner = class TaskRunner {
|
|
|
3817
5062
|
compiler = session.compiler;
|
|
3818
5063
|
}
|
|
3819
5064
|
const errors = output.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
3820
|
-
if (output.diagnostics.length > 0)
|
|
3821
|
-
|
|
5065
|
+
if (output.diagnostics.length > 0) {
|
|
5066
|
+
CompileDiagnostics.renderToStderr(rootDir, output.diagnostics);
|
|
5067
|
+
this.compileDiagnostics = CliDiagnostics.fromCompiler(output.diagnostics);
|
|
5068
|
+
}
|
|
5069
|
+
if (errors.length > 0) throw new TaskRunFailure(TaskDiagnosticCode.CompileFailed, `The production graph has ${errors.length} compile error${errors.length === 1 ? "" : "s"}. Nothing was booted.`);
|
|
3822
5070
|
const findings = LibraryFactoryPreflight.inspect(compiler.consumedBoundaryPackages(), LibraryFactoryPreflight.productionBuildOptions);
|
|
3823
|
-
if (findings.length > 0) throw new TaskRunFailure(LibraryFactoryPreflight.explain(findings));
|
|
5071
|
+
if (findings.length > 0) throw new TaskRunFailure(TaskDiagnosticCode.LibraryPreflightFailed, LibraryFactoryPreflight.explain(findings));
|
|
3824
5072
|
return output;
|
|
3825
5073
|
}
|
|
3826
5074
|
/**
|
|
@@ -3829,7 +5077,7 @@ var TaskRunner = class TaskRunner {
|
|
|
3829
5077
|
*/
|
|
3830
5078
|
static sourceDirectoriesOf(nitro, explicit) {
|
|
3831
5079
|
if (explicit !== void 0) return { scanDirs: explicit };
|
|
3832
|
-
for (const [name, value] of [["serverDir", nitro.serverDir], ["scanDirs", nitro.scanDirs]]) if (value.kind === "computed") throw new TaskRunFailure(`The Nitro \`${name}\` is computed (${value.reason}, in ${value.file}), and the command does not run your config files, so it cannot read the source directories. Name them with \`--src <dir>\` (repeat the option, or separate directories with commas). Nothing was booted.`);
|
|
5080
|
+
for (const [name, value] of [["serverDir", nitro.serverDir], ["scanDirs", nitro.scanDirs]]) if (value.kind === "computed") throw new TaskRunFailure(TaskDiagnosticCode.NitroConfigComputed, `The Nitro \`${name}\` is computed (${value.reason}, in ${value.file}), and the command does not run your config files, so it cannot read the source directories. Name them with \`--src <dir>\` (repeat the option, or separate directories with commas). Nothing was booted.`);
|
|
3833
5081
|
return {
|
|
3834
5082
|
...nitro.serverDir.kind === "literal" ? { serverDir: nitro.serverDir.value } : {},
|
|
3835
5083
|
...nitro.scanDirs.kind === "literal" ? { scanDirs: nitro.scanDirs.value } : {}
|
|
@@ -3837,13 +5085,13 @@ var TaskRunner = class TaskRunner {
|
|
|
3837
5085
|
}
|
|
3838
5086
|
/** Pick the module and the task from the compiled graph. */
|
|
3839
5087
|
pickTarget(output) {
|
|
3840
|
-
if (output.moduleGraph === void 0) throw new TaskRunFailure("The compile produced no module graph, so the task cannot be found.");
|
|
5088
|
+
if (output.moduleGraph === void 0) throw new TaskRunFailure(TaskDiagnosticCode.AppNotFound, "The compile produced no module graph, so the task cannot be found.");
|
|
3841
5089
|
return TaskTargetResolver.resolve(output.moduleGraph, this.options.moduleName, this.options.taskName, output.boundaryNodes);
|
|
3842
5090
|
}
|
|
3843
5091
|
/** Refuse a graph that needs bindings only its platform has, before anything boots. */
|
|
3844
5092
|
static refuseBindings(output, platformName, outputDirectory) {
|
|
3845
5093
|
const needs = PlatformBindingCensus.collect(output, platformName, outputDirectory);
|
|
3846
|
-
if (needs.length > 0) throw new TaskRunFailure(PlatformBindingCensus.explain(needs, platformName));
|
|
5094
|
+
if (needs.length > 0) throw new TaskRunFailure(TaskDiagnosticCode.PlatformBindingsRequired, PlatformBindingCensus.explain(needs, platformName));
|
|
3847
5095
|
}
|
|
3848
5096
|
/** Write the generated files into the temporary directory. */
|
|
3849
5097
|
materialize(output) {
|
|
@@ -3856,7 +5104,7 @@ var TaskRunner = class TaskRunner {
|
|
|
3856
5104
|
/** Boot the app with no HTTP surface and without the standing work of the deployed app. */
|
|
3857
5105
|
async boot(output) {
|
|
3858
5106
|
const wiring = output.files.find((file) => TaskRunner.wiringFilePattern.test(file.path));
|
|
3859
|
-
if (wiring === void 0) throw new TaskRunFailure("No app wiring file was generated. Make sure the project has a root module (a module that no other module imports).");
|
|
5107
|
+
if (wiring === void 0) throw new TaskRunFailure(TaskDiagnosticCode.AppNotFound, "No app wiring file was generated. Make sure the project has a root module (a module that no other module imports).");
|
|
3860
5108
|
const runtime = this.runtime;
|
|
3861
5109
|
if (TransformBoot.isBootProcess()) TransformBoot.registerSourceResolver(new Map(output.sourceRewrites.map((rewrite) => [rewrite.filePath, rewrite.content])), this.aliases);
|
|
3862
5110
|
runtime.StandingWork.suspend(runtime.StandingWork.kinds().filter((kind) => !(this.options.withOutboxRelay && kind === runtime.StandingWorkKind.OutboxRelay)));
|
|
@@ -3876,17 +5124,17 @@ var TaskRunner = class TaskRunner {
|
|
|
3876
5124
|
this.app = await wiringModule.createApp(bindings);
|
|
3877
5125
|
this.context = await this.app.from(runtime.CoreModule).get(runtime.Context);
|
|
3878
5126
|
}
|
|
3879
|
-
/** Run the task and
|
|
3880
|
-
async
|
|
5127
|
+
/** Run the task and keep its result as one line of JSON. */
|
|
5128
|
+
async runTask(target) {
|
|
3881
5129
|
const runtime = this.runtime;
|
|
3882
5130
|
const moduleClass = await this.importClass(target.module.sourceFile, target.module.className);
|
|
3883
5131
|
const taskClass = await this.importClass(target.task.sourceFile, target.task.className);
|
|
3884
|
-
if (typeof taskClass.prototype.run !== "function") throw new TaskRunFailure(`${this.options.taskName} is not a task: the class has no run() method. A task extends Task.`);
|
|
5132
|
+
if (typeof taskClass.prototype.run !== "function") throw new TaskRunFailure(TaskDiagnosticCode.NotATask, `${this.options.taskName} is not a task: the class has no run() method. A task extends Task.`);
|
|
3885
5133
|
let task;
|
|
3886
5134
|
try {
|
|
3887
5135
|
task = await this.app.from(moduleClass).get(taskClass);
|
|
3888
5136
|
} catch (error) {
|
|
3889
|
-
throw
|
|
5137
|
+
throw TaskRunner.failureOf(TaskDiagnosticCode.CreateFailed, `The task ${this.options.taskName} could not be created`, error);
|
|
3890
5138
|
}
|
|
3891
5139
|
const event = {
|
|
3892
5140
|
name: this.options.taskName,
|
|
@@ -3899,22 +5147,22 @@ var TaskRunner = class TaskRunner {
|
|
|
3899
5147
|
try {
|
|
3900
5148
|
await seeds?.apply(event, this.context);
|
|
3901
5149
|
} catch (error) {
|
|
3902
|
-
throw
|
|
5150
|
+
throw TaskRunner.failureOf(TaskDiagnosticCode.StartFailed, `The task ${this.options.taskName} did not start`, error);
|
|
3903
5151
|
}
|
|
3904
5152
|
try {
|
|
3905
5153
|
return await task.run(event);
|
|
3906
5154
|
} catch (error) {
|
|
3907
|
-
throw
|
|
5155
|
+
throw TaskRunner.failureOf(TaskDiagnosticCode.TaskFailed, `The task ${this.options.taskName} failed`, error, TaskRefusal);
|
|
3908
5156
|
}
|
|
3909
5157
|
});
|
|
3910
5158
|
} catch (error) {
|
|
3911
5159
|
if (error instanceof TaskRunFailure) throw error;
|
|
3912
|
-
throw
|
|
5160
|
+
throw TaskRunner.failureOf(TaskDiagnosticCode.StartFailed, `The task ${this.options.taskName} did not start`, error);
|
|
3913
5161
|
}
|
|
3914
5162
|
try {
|
|
3915
5163
|
this.serialized = JSON.stringify(outcome);
|
|
3916
5164
|
} catch (error) {
|
|
3917
|
-
throw new TaskRunFailure(`The task ran, but its result is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
5165
|
+
throw new TaskRunFailure(TaskDiagnosticCode.ResultNotJson, `The task ran, but its result is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
3918
5166
|
}
|
|
3919
5167
|
}
|
|
3920
5168
|
/** The seeds the app declares for every background frame, or none. */
|
|
@@ -3934,7 +5182,7 @@ var TaskRunner = class TaskRunner {
|
|
|
3934
5182
|
async importClass(sourceFile, className) {
|
|
3935
5183
|
this.packageImports += 1;
|
|
3936
5184
|
const found = (isAbsolute(sourceFile) ? await import(pathToFileURL(sourceFile).href) : await ProjectModules.import(this.outputDirectory, `project-package-${this.packageImports}.js`, `export * from ${JSON.stringify(sourceFile)};\n`))[className];
|
|
3937
|
-
if (found === void 0) throw new TaskRunFailure(`${sourceFile} does not export a class named ${className}.`);
|
|
5185
|
+
if (found === void 0) throw new TaskRunFailure(TaskDiagnosticCode.TargetNotFound, `${sourceFile} does not export a class named ${className}.`);
|
|
3938
5186
|
return found;
|
|
3939
5187
|
}
|
|
3940
5188
|
/**
|
|
@@ -3954,7 +5202,8 @@ var TaskRunner = class TaskRunner {
|
|
|
3954
5202
|
await this.app?.shutdown();
|
|
3955
5203
|
} catch (error) {
|
|
3956
5204
|
clean = false;
|
|
3957
|
-
|
|
5205
|
+
this.shutdownFailure = "The task finished, but shutting the app down failed — a client may still be open: " + TaskRunner.messageOf(error);
|
|
5206
|
+
process.stderr.write(`${this.shutdownFailure}\n`);
|
|
3958
5207
|
}
|
|
3959
5208
|
this.restoreStandingWork?.();
|
|
3960
5209
|
this.restoreStandingWork = void 0;
|
|
@@ -3988,25 +5237,46 @@ var TaskRunner = class TaskRunner {
|
|
|
3988
5237
|
static describe(error) {
|
|
3989
5238
|
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
3990
5239
|
}
|
|
5240
|
+
/** The message of a thrown value: the error message, else its string form. */
|
|
5241
|
+
static messageOf(error) {
|
|
5242
|
+
return error instanceof Error ? error.message : String(error);
|
|
5243
|
+
}
|
|
5244
|
+
/**
|
|
5245
|
+
* A failure that wraps a thrown value: stderr gets the lead and the stack trace, the `--json` diagnostic the
|
|
5246
|
+
* lead and the message.
|
|
5247
|
+
*/
|
|
5248
|
+
static failureOf(code, lead, error, Failure = TaskRunFailure) {
|
|
5249
|
+
return new Failure(code, `${lead}:\n${TaskRunner.describe(error)}`, {
|
|
5250
|
+
cause: error,
|
|
5251
|
+
summary: `${lead}: ${TaskRunner.messageOf(error)}`,
|
|
5252
|
+
hint: TaskRunner.stackHint
|
|
5253
|
+
});
|
|
5254
|
+
}
|
|
3991
5255
|
/**
|
|
3992
|
-
* The
|
|
3993
|
-
* class name when the class leaves the generic `Error` name.
|
|
5256
|
+
* The error of a refused run, as its stdout line `{"error":{"name":…,"message":…}}` names it. The name is the
|
|
5257
|
+
* error name, or the class name when the class leaves the generic `Error` name.
|
|
3994
5258
|
*/
|
|
3995
|
-
static
|
|
3996
|
-
if (!(thrown instanceof Error)) return
|
|
5259
|
+
static errorOf(thrown) {
|
|
5260
|
+
if (!(thrown instanceof Error)) return {
|
|
3997
5261
|
name: "Error",
|
|
3998
5262
|
message: String(thrown)
|
|
3999
|
-
}
|
|
5263
|
+
};
|
|
4000
5264
|
const className = thrown.constructor?.name;
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
name,
|
|
5265
|
+
return {
|
|
5266
|
+
name: thrown.name === "Error" && className !== void 0 && className !== "" ? className : thrown.name,
|
|
4004
5267
|
message: thrown.message
|
|
4005
|
-
}
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5270
|
+
/** The `--json` diagnostics of a failure. */
|
|
5271
|
+
static diagnosticsOf(error) {
|
|
5272
|
+
if (error instanceof CommandFailure) return error.diagnostics;
|
|
5273
|
+
if (error instanceof TaskRunFailure) return [CliDiagnostics.error(error.code, error.summary, error.hint === void 0 ? {} : { hint: error.hint })];
|
|
5274
|
+
if (error instanceof TaskTargetError) return [CliDiagnostics.error(error.code, error.message)];
|
|
5275
|
+
return [CliDiagnostics.error(CliDiagnosticCode.UnexpectedError, TaskRunner.messageOf(error), { hint: TaskRunner.stackHint })];
|
|
4006
5276
|
}
|
|
4007
5277
|
/** Print a failure on stderr. */
|
|
4008
5278
|
static report(error) {
|
|
4009
|
-
if (error instanceof TaskRunFailure || error instanceof TaskTargetError) {
|
|
5279
|
+
if (error instanceof TaskRunFailure || error instanceof TaskTargetError || error instanceof CommandFailure) {
|
|
4010
5280
|
process.stderr.write(`${error.message}\n`);
|
|
4011
5281
|
return;
|
|
4012
5282
|
}
|
|
@@ -4092,10 +5362,46 @@ var TaskCommand = class TaskCommand {
|
|
|
4092
5362
|
process.exitCode = exitCode;
|
|
4093
5363
|
setTimeout(() => process.exit(exitCode), TaskCommand.exitGraceMilliseconds).unref();
|
|
4094
5364
|
}
|
|
4095
|
-
/**
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
5365
|
+
/**
|
|
5366
|
+
* Run the command in this process: the transform-enabled child, or a test. Without `--json` stdout gets the
|
|
5367
|
+
* text line of the result; with it, one `--json` document. Sets the exit code.
|
|
5368
|
+
*
|
|
5369
|
+
* @param args - The parsed arguments.
|
|
5370
|
+
* @param rawArgs - The arguments after `task run`, for every `--src`.
|
|
5371
|
+
*/
|
|
5372
|
+
static async runInProcess(args, rawArgs) {
|
|
5373
|
+
const commandOutput = CommandOutput.resolve("task run", args);
|
|
5374
|
+
await commandOutput.run(async () => {
|
|
5375
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
5376
|
+
const parsed = TaskCommand.parsePayload(args.payload);
|
|
5377
|
+
if (!parsed.ok) throw TaskCommand.invalidOption("--payload", parsed.reason);
|
|
5378
|
+
const envFile = args["env-file"];
|
|
5379
|
+
if (envFile !== void 0) try {
|
|
5380
|
+
await TaskCommand.loadEnvironmentFile(resolve(process.cwd(), envFile));
|
|
5381
|
+
} catch (error) {
|
|
5382
|
+
throw TaskCommand.invalidOption("--env-file", error instanceof Error ? error.message : String(error));
|
|
5383
|
+
}
|
|
5384
|
+
const sourceOption = TaskCommand.sourceOption(rawArgs);
|
|
5385
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, sourceOption);
|
|
5386
|
+
const runner = new TaskRunner({
|
|
5387
|
+
rootDir,
|
|
5388
|
+
moduleName: args.module,
|
|
5389
|
+
taskName: args.task,
|
|
5390
|
+
withOutboxRelay: args["with-outbox-relay"] === true,
|
|
5391
|
+
isSourceExplicit: sourceOption !== void 0 && sourceOption.trim() !== "",
|
|
5392
|
+
...parsed.payload === void 0 ? {} : { payload: parsed.payload },
|
|
5393
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
5394
|
+
});
|
|
5395
|
+
if (commandOutput.isJson()) {
|
|
5396
|
+
const outcome = await runner.perform();
|
|
5397
|
+
process.exitCode = outcome.exitCode;
|
|
5398
|
+
commandOutput.emit(outcome.document);
|
|
5399
|
+
} else process.exitCode = await runner.run();
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
/** A failure of one option; the text output prints the reason alone, as before. */
|
|
5403
|
+
static invalidOption(option, reason) {
|
|
5404
|
+
return new CommandFailure([CliDiagnostics.error(CliDiagnosticCode.InvalidOptions, reason, { option })]);
|
|
4099
5405
|
}
|
|
4100
5406
|
};
|
|
4101
5407
|
const taskRunCommand = defineCommand({
|
|
@@ -4114,6 +5420,7 @@ const taskRunCommand = defineCommand({
|
|
|
4114
5420
|
description: "The class name of the task.",
|
|
4115
5421
|
required: true
|
|
4116
5422
|
},
|
|
5423
|
+
json: CommandOutput.jsonFlag,
|
|
4117
5424
|
cwd: {
|
|
4118
5425
|
type: "string",
|
|
4119
5426
|
description: "Project root directory.",
|
|
@@ -4139,34 +5446,12 @@ const taskRunCommand = defineCommand({
|
|
|
4139
5446
|
},
|
|
4140
5447
|
async run({ args, rawArgs }) {
|
|
4141
5448
|
if (!TransformBoot.isBootProcess()) {
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
}
|
|
4145
|
-
const rootDir = resolve(process.cwd(), args.cwd);
|
|
4146
|
-
const parsed = TaskCommand.parsePayload(args.payload);
|
|
4147
|
-
if (!parsed.ok) {
|
|
4148
|
-
TaskCommand.refuse(parsed.reason);
|
|
4149
|
-
return;
|
|
4150
|
-
}
|
|
4151
|
-
const envFile = args["env-file"];
|
|
4152
|
-
if (envFile !== void 0) try {
|
|
4153
|
-
await TaskCommand.loadEnvironmentFile(resolve(process.cwd(), envFile));
|
|
4154
|
-
} catch (error) {
|
|
4155
|
-
TaskCommand.refuse(error instanceof Error ? error.message : String(error));
|
|
5449
|
+
const commandOutput = CommandOutput.resolve("task run", args);
|
|
5450
|
+
if (await ProjectRoot.checkBeforeRelaunch(args.cwd, commandOutput)) await TransformBoot.relaunch("task run", commandOutput);
|
|
4156
5451
|
return;
|
|
4157
5452
|
}
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
const exitCode = await new TaskRunner({
|
|
4161
|
-
rootDir,
|
|
4162
|
-
moduleName: args.module,
|
|
4163
|
-
taskName: args.task,
|
|
4164
|
-
withOutboxRelay: args["with-outbox-relay"],
|
|
4165
|
-
isSourceExplicit: sourceOption !== void 0 && sourceOption.trim() !== "",
|
|
4166
|
-
...parsed.payload === void 0 ? {} : { payload: parsed.payload },
|
|
4167
|
-
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
4168
|
-
}).run();
|
|
4169
|
-
TaskCommand.finish(exitCode);
|
|
5453
|
+
await TaskCommand.runInProcess(args, rawArgs);
|
|
5454
|
+
TaskCommand.finish(Number(process.exitCode ?? 0));
|
|
4170
5455
|
}
|
|
4171
5456
|
});
|
|
4172
5457
|
/** `heximon task` — run one-off work of the app. */
|
|
@@ -4193,6 +5478,13 @@ const WorkflowBaselineState = {
|
|
|
4193
5478
|
/** No committed snapshot for this workflow — the gate is unarmed until `heximon check --update` runs. */
|
|
4194
5479
|
Unarmed: "unarmed"
|
|
4195
5480
|
};
|
|
5481
|
+
/** The stable codes of the failures `heximon workflows` raises itself, beside the compiler's own. */
|
|
5482
|
+
const WorkflowsDiagnosticCode = {
|
|
5483
|
+
/** The committed `heximon.upgrade.json` exists but cannot be read, so no workflow state is known. */
|
|
5484
|
+
BaselineUnreadable: "workflows.baseline-unreadable",
|
|
5485
|
+
/** The Cloudflare live instances of one workflow could not be listed, so the fleet picture is incomplete. */
|
|
5486
|
+
EnumerationFailed: "workflows.enumeration-failed"
|
|
5487
|
+
};
|
|
4196
5488
|
/**
|
|
4197
5489
|
* Parses, classifies, and renders the durable-workflow upgrade-safety report — pure functions over the emitted
|
|
4198
5490
|
* baseline, the committed baseline, and the (optional) Cloudflare live-fleet inventories.
|
|
@@ -4271,16 +5563,18 @@ var WorkflowStatusReport = class WorkflowStatusReport {
|
|
|
4271
5563
|
return parts.length === 0 ? `${count} — baseline armed & in sync.` : `${count} — ${parts.join(", ")}.`;
|
|
4272
5564
|
}
|
|
4273
5565
|
/**
|
|
4274
|
-
* The `--
|
|
5566
|
+
* The `--json` document: the per-workflow entries with their inventories (`null` when skipped).
|
|
4275
5567
|
*
|
|
4276
5568
|
* @param options - The classified entries, their inventories, and any upgrade-verdict diagnostics the
|
|
4277
5569
|
* compile raised. An upgrade verdict rides the report rather than replacing it — it is the outcome of a
|
|
4278
|
-
* complete analysis — but it still makes the run report `ok: false`.
|
|
5570
|
+
* complete analysis — but it still makes the run report `ok: false`. So does each enumeration error,
|
|
5571
|
+
* which also becomes an error diagnostic.
|
|
4279
5572
|
*/
|
|
4280
5573
|
static toJson(options) {
|
|
4281
5574
|
const upgradeDiagnostics = options.upgradeDiagnostics ?? [];
|
|
5575
|
+
const enumerationDiagnostics = [...options.enumerationErrors].map(([workflowName, reason]) => CliDiagnostics.error(WorkflowsDiagnosticCode.EnumerationFailed, `Cloudflare enumeration failed for ${workflowName}: ${reason}`, { hint: "Check CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, then run the command again." }));
|
|
4282
5576
|
return {
|
|
4283
|
-
ok:
|
|
5577
|
+
ok: enumerationDiagnostics.length === 0 && upgradeDiagnostics.length === 0,
|
|
4284
5578
|
workflows: options.entries.map((entry) => {
|
|
4285
5579
|
const enumerationError = options.enumerationErrors.get(entry.resourceName);
|
|
4286
5580
|
return {
|
|
@@ -4293,25 +5587,26 @@ var WorkflowStatusReport = class WorkflowStatusReport {
|
|
|
4293
5587
|
enumerated: options.cloudflareSkipped === void 0,
|
|
4294
5588
|
...options.cloudflareSkipped === void 0 ? {} : { skipped: options.cloudflareSkipped }
|
|
4295
5589
|
},
|
|
4296
|
-
|
|
5590
|
+
diagnostics: [...upgradeDiagnostics, ...enumerationDiagnostics]
|
|
4297
5591
|
};
|
|
4298
5592
|
}
|
|
4299
5593
|
/**
|
|
4300
|
-
* The `--
|
|
5594
|
+
* The `--json` document for a run that produced NO per-workflow report — a workflow-free app, a failed
|
|
4301
5595
|
* compile, or an unreadable committed baseline. Same shape as {@link toJson}, so one reader covers both.
|
|
4302
5596
|
*
|
|
4303
|
-
* @param options - Why there is no report, and
|
|
5597
|
+
* @param options - Why there is no report, and the diagnostics of a run that failed. `ok` is `false`
|
|
5598
|
+
* exactly when one of them is an error.
|
|
4304
5599
|
*/
|
|
4305
5600
|
static noReportJson(options) {
|
|
5601
|
+
const diagnostics = options.diagnostics ?? [];
|
|
4306
5602
|
return {
|
|
4307
|
-
ok:
|
|
5603
|
+
ok: !diagnostics.some((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error),
|
|
4308
5604
|
workflows: [],
|
|
4309
5605
|
cloudflare: {
|
|
4310
5606
|
enumerated: false,
|
|
4311
5607
|
skipped: options.reason
|
|
4312
5608
|
},
|
|
4313
|
-
|
|
4314
|
-
...options.diagnostics === void 0 ? {} : { diagnostics: options.diagnostics }
|
|
5609
|
+
diagnostics
|
|
4315
5610
|
};
|
|
4316
5611
|
}
|
|
4317
5612
|
/**
|
|
@@ -4456,9 +5751,10 @@ const workflowsCommand = defineCommand({
|
|
|
4456
5751
|
description: "Project root directory.",
|
|
4457
5752
|
default: "."
|
|
4458
5753
|
},
|
|
5754
|
+
json: CommandOutput.jsonFlag,
|
|
4459
5755
|
format: {
|
|
4460
5756
|
type: "string",
|
|
4461
|
-
description: "Output format: 'pretty' (default) or 'json'.",
|
|
5757
|
+
description: "Output format: 'pretty' (default) or 'json' (the same as --json).",
|
|
4462
5758
|
default: "pretty"
|
|
4463
5759
|
},
|
|
4464
5760
|
env: {
|
|
@@ -4472,91 +5768,87 @@ const workflowsCommand = defineCommand({
|
|
|
4472
5768
|
}
|
|
4473
5769
|
},
|
|
4474
5770
|
async run({ args }) {
|
|
4475
|
-
const
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
if (
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
5771
|
+
const commandOutput = CommandOutput.resolve("workflows", args);
|
|
5772
|
+
await commandOutput.run(async () => {
|
|
5773
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
5774
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
5775
|
+
if (!commandOutput.isJson()) intro(`heximon workflows — ${basename(rootDir)}`);
|
|
5776
|
+
const output = (await ProjectCompileSession.open({
|
|
5777
|
+
rootDir,
|
|
5778
|
+
environment: args.env,
|
|
5779
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
5780
|
+
})).compile();
|
|
5781
|
+
const errors = output.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
5782
|
+
const upgradeErrors = errors.filter((diagnostic) => UpgradeBaseline.isBreaking(diagnostic.code));
|
|
5783
|
+
const blockingErrors = errors.filter((diagnostic) => !UpgradeBaseline.isBreaking(diagnostic.code));
|
|
5784
|
+
if (blockingErrors.length > 0) {
|
|
5785
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkflowStatusReport.noReportJson({
|
|
5786
|
+
reason: "the compile failed — no workflow report to enumerate against.",
|
|
5787
|
+
diagnostics: CliDiagnostics.fromCompiler(blockingErrors)
|
|
5788
|
+
}));
|
|
5789
|
+
else {
|
|
5790
|
+
CompileDiagnostics.renderToStderr(rootDir, blockingErrors);
|
|
5791
|
+
outro(`Compile failed — ${blockingErrors.length} error${blockingErrors.length === 1 ? "" : "s"}; no workflow report.`);
|
|
5792
|
+
}
|
|
5793
|
+
process.exitCode = 1;
|
|
5794
|
+
return;
|
|
4497
5795
|
}
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
if (asJson) process.stdout.write(`${JSON.stringify(WorkflowStatusReport.noReportJson({
|
|
4505
|
-
ok: true,
|
|
4506
|
-
reason: "this app declares no workflows — nothing to enumerate."
|
|
4507
|
-
}))}\n`);
|
|
4508
|
-
else outro("No workflows in this app.");
|
|
4509
|
-
return;
|
|
4510
|
-
}
|
|
4511
|
-
const committed = WorkflowsCommand.readCommittedBaseline(rootDir);
|
|
4512
|
-
if (committed.state === CommittedBaselineState.Unreadable) {
|
|
4513
|
-
const message = `${WorkflowStatusReport.committedBaselineFileName} exists but could not be read (${committed.reason ?? "unknown reason"}), so no workflow's upgrade-safety state is known — this is NOT an unarmed gate. Restore the committed file (e.g. from git) before deploying; \`heximon check --update\` would overwrite it with the current surface.`;
|
|
4514
|
-
if (asJson) process.stdout.write(`${JSON.stringify(WorkflowStatusReport.noReportJson({
|
|
4515
|
-
ok: false,
|
|
4516
|
-
reason: "the committed upgrade-safety baseline is unreadable — nothing to compare against.",
|
|
4517
|
-
error: message
|
|
4518
|
-
}))}\n`);
|
|
4519
|
-
else {
|
|
4520
|
-
log.error(message);
|
|
4521
|
-
outro("Upgrade-safety baseline unreadable — no workflow report.");
|
|
5796
|
+
const generated = output.files.find((file) => file.path === WorkflowStatusReport.generatedBaselinePath);
|
|
5797
|
+
const current = generated === void 0 ? void 0 : WorkflowStatusReport.parseGeneratedText(generated.content);
|
|
5798
|
+
if (current === void 0 || current.workflows.length === 0) {
|
|
5799
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkflowStatusReport.noReportJson({ reason: "this app declares no workflows — nothing to enumerate." }));
|
|
5800
|
+
else outro("No workflows in this app.");
|
|
5801
|
+
return;
|
|
4522
5802
|
}
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
let cloudflareSkipped;
|
|
4530
|
-
const accountId = process.env["CLOUDFLARE_ACCOUNT_ID"];
|
|
4531
|
-
const apiToken = process.env["CLOUDFLARE_API_TOKEN"];
|
|
4532
|
-
if (accountId === void 0 || accountId === "" || apiToken === void 0 || apiToken === "") cloudflareSkipped = "CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN are not both set — Cloudflare live-instance enumeration skipped.";
|
|
4533
|
-
else {
|
|
4534
|
-
const inspector = await CloudflareInspectorLoader.load(rootDir, {
|
|
4535
|
-
accountId,
|
|
4536
|
-
apiToken
|
|
4537
|
-
});
|
|
4538
|
-
if (inspector === void 0) cloudflareSkipped = "@heximon/cloudflare is not installed in this app — Cloudflare live-instance enumeration skipped.";
|
|
4539
|
-
else for (const entry of entries) try {
|
|
4540
|
-
inventories.set(entry.resourceName, await inspector.inspect({
|
|
4541
|
-
workflowName: entry.resourceName,
|
|
4542
|
-
currentFingerprint: entry.fingerprint
|
|
5803
|
+
const committed = WorkflowsCommand.readCommittedBaseline(rootDir);
|
|
5804
|
+
if (committed.state === CommittedBaselineState.Unreadable) {
|
|
5805
|
+
const message = `${WorkflowStatusReport.committedBaselineFileName} exists but could not be read (${committed.reason ?? "unknown reason"}), so no workflow's upgrade-safety state is known — this is NOT an unarmed gate. Restore the committed file (e.g. from git) before deploying; \`heximon check --update\` would overwrite it with the current surface.`;
|
|
5806
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkflowStatusReport.noReportJson({
|
|
5807
|
+
reason: "the committed upgrade-safety baseline is unreadable — nothing to compare against.",
|
|
5808
|
+
diagnostics: [CliDiagnostics.error(WorkflowsDiagnosticCode.BaselineUnreadable, message, { hint: "Restore the committed file, for example from git, before you deploy." })]
|
|
4543
5809
|
}));
|
|
4544
|
-
|
|
4545
|
-
|
|
5810
|
+
else {
|
|
5811
|
+
log.error(message);
|
|
5812
|
+
outro("Upgrade-safety baseline unreadable — no workflow report.");
|
|
5813
|
+
}
|
|
5814
|
+
process.exitCode = 1;
|
|
5815
|
+
return;
|
|
4546
5816
|
}
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
5817
|
+
const entries = WorkflowStatusReport.classify(current.workflows, committed.document);
|
|
5818
|
+
const inventories = /* @__PURE__ */ new Map();
|
|
5819
|
+
const enumerationErrors = /* @__PURE__ */ new Map();
|
|
5820
|
+
let cloudflareSkipped;
|
|
5821
|
+
const accountId = process.env["CLOUDFLARE_ACCOUNT_ID"];
|
|
5822
|
+
const apiToken = process.env["CLOUDFLARE_API_TOKEN"];
|
|
5823
|
+
if (accountId === void 0 || accountId === "" || apiToken === void 0 || apiToken === "") cloudflareSkipped = "CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN are not both set — Cloudflare live-instance enumeration skipped.";
|
|
5824
|
+
else {
|
|
5825
|
+
const inspector = await CloudflareInspectorLoader.load(rootDir, {
|
|
5826
|
+
accountId,
|
|
5827
|
+
apiToken
|
|
5828
|
+
});
|
|
5829
|
+
if (inspector === void 0) cloudflareSkipped = "@heximon/cloudflare is not installed in this app — Cloudflare live-instance enumeration skipped.";
|
|
5830
|
+
else for (const entry of entries) try {
|
|
5831
|
+
inventories.set(entry.resourceName, await inspector.inspect({
|
|
5832
|
+
workflowName: entry.resourceName,
|
|
5833
|
+
currentFingerprint: entry.fingerprint
|
|
5834
|
+
}));
|
|
5835
|
+
} catch (error) {
|
|
5836
|
+
enumerationErrors.set(entry.resourceName, error instanceof Error ? error.message : String(error));
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkflowStatusReport.toJson({
|
|
5840
|
+
entries,
|
|
5841
|
+
inventories,
|
|
5842
|
+
enumerationErrors,
|
|
5843
|
+
cloudflareSkipped,
|
|
5844
|
+
upgradeDiagnostics: CliDiagnostics.fromCompiler(upgradeErrors)
|
|
5845
|
+
}));
|
|
5846
|
+
else {
|
|
5847
|
+
WorkflowsCommand.report(entries, inventories, enumerationErrors, cloudflareSkipped);
|
|
5848
|
+
if (upgradeErrors.length > 0) CompileDiagnostics.renderToStderr(rootDir, upgradeErrors);
|
|
5849
|
+
}
|
|
5850
|
+
if (enumerationErrors.size > 0 || upgradeErrors.length > 0) process.exitCode = 1;
|
|
5851
|
+
});
|
|
4560
5852
|
}
|
|
4561
5853
|
});
|
|
4562
5854
|
/** Output helpers for {@link workflowsCommand}: the committed-baseline read and the pretty report. */
|
|
@@ -4643,6 +5935,6 @@ const main = defineCommand({
|
|
|
4643
5935
|
}
|
|
4644
5936
|
});
|
|
4645
5937
|
await tab(main);
|
|
4646
|
-
await
|
|
5938
|
+
await CliEntry.run(main);
|
|
4647
5939
|
//#endregion
|
|
4648
5940
|
export {};
|