heximon 0.9.0 → 0.11.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 +61 -4
- package/dist/bin.mjs +2576 -823
- package/dist/index.d.mts +1097 -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,28 @@
|
|
|
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, globSync, 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 {
|
|
11
|
-
import { AstNodeType, CompileEnvironment, CompileMode, DeployPlatform, DiagnosticCode, DiagnosticRenderMode, DiagnosticRenderer, DiagnosticSeverity, ImportKind, ReexportIndex, RuntimeConfigFile, RuntimeSingletons, TerminalStyle, UpgradeBaselineFile } from "@heximon/compiler";
|
|
12
|
-
import { ConfigLoader, LibraryEmitter, LibraryFactoryPreflight, LibraryFreshness, ProjectCompiler, RuntimeConfigLoader, SourceModuleResolution, SourceParser, WorkspaceTopologyReader } from "@heximon/compiler/project";
|
|
12
|
+
import { ConfigLoader, LibraryEmitter, LibraryFactoryPreflight, LibraryFreshness, LibraryFreshnessStatus, PnpmWorkspaceManifest, 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
|
+
import { matchesGlob } from "node:path";
|
|
16
17
|
import { RouteCensus, RouteScopeGap } from "@heximon/http/compiler";
|
|
17
18
|
import { pathToFileURL } from "node:url";
|
|
18
19
|
import { homedir } from "node:os";
|
|
20
|
+
//#region src/check-json-document.ts
|
|
21
|
+
/** The stable codes of the failures `heximon check` raises itself, beside the compiler's own. */
|
|
22
|
+
const CheckDiagnosticCode = {
|
|
23
|
+
/** A consumed workspace library changed since its last `vp pack`, so the compile read an old manifest. */
|
|
24
|
+
StaleLibrary: "check.stale-library" };
|
|
25
|
+
//#endregion
|
|
19
26
|
//#region src/compile-diagnostics.ts
|
|
20
27
|
/**
|
|
21
28
|
* Renders compile diagnostics to stderr with the same codeframe layout the dev server shows — a box-drawing
|
|
@@ -63,29 +70,19 @@ var CompileModeResolver = class {
|
|
|
63
70
|
}
|
|
64
71
|
};
|
|
65
72
|
/**
|
|
66
|
-
* Resolve one command's compile mode.
|
|
73
|
+
* Resolve one command's compile mode, or throw the refusal.
|
|
67
74
|
*
|
|
68
|
-
* @param commandName - The verb, for the refusal messages (`routes`, `graph`, `check`).
|
|
69
75
|
* @param rootDir - The absolute project root the shape is detected from.
|
|
70
76
|
* @param args - The parsed `--library` / `--app` flags.
|
|
71
77
|
* @param sourceDirectories - The resolved `--src` directories, checked against the library single-root rule.
|
|
72
|
-
* @returns The resolved mode
|
|
73
|
-
*
|
|
78
|
+
* @returns The resolved mode.
|
|
79
|
+
* @throws CommandFailure with {@link CliDiagnosticCode.InvalidOptions} when no mode can honour the flags.
|
|
74
80
|
*/
|
|
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
|
-
}
|
|
81
|
+
static resolve(rootDir, args, sourceDirectories) {
|
|
82
|
+
if (args.library === true && args.app === true) throw CommandFailure.of(CliDiagnosticCode.InvalidOptions, "--library and --app are mutually exclusive — pass at most one.");
|
|
81
83
|
const forcedMode = args.library === true ? CompileMode.Library : args.app === true ? CompileMode.App : void 0;
|
|
82
84
|
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
|
-
}
|
|
85
|
+
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
86
|
return {
|
|
90
87
|
mode,
|
|
91
88
|
forced: forcedMode !== void 0
|
|
@@ -97,6 +94,65 @@ var CompileModeResolver = class {
|
|
|
97
94
|
}
|
|
98
95
|
};
|
|
99
96
|
//#endregion
|
|
97
|
+
//#region src/config-sidecar.ts
|
|
98
|
+
/**
|
|
99
|
+
* Loads the `heximon.config.ts` sidecar for a command. A sidecar that throws is the same failure in every
|
|
100
|
+
* command: one {@link CliDiagnosticCode.ConfigUnreadable} diagnostic whose `file` is the sidecar.
|
|
101
|
+
*/
|
|
102
|
+
var ConfigSidecar = class ConfigSidecar {
|
|
103
|
+
/** The fix hint of every config failure. */
|
|
104
|
+
static hint = "Fix the error in the config file, then run the command again.";
|
|
105
|
+
/**
|
|
106
|
+
* Load the sidecar.
|
|
107
|
+
*
|
|
108
|
+
* @param rootDir - The absolute project root.
|
|
109
|
+
* @param environment - The `$env` name to resolve; `NODE_ENV` when omitted.
|
|
110
|
+
* @returns The resolved config, or `undefined` when the project has no sidecar.
|
|
111
|
+
* @throws CommandFailure with {@link CliDiagnosticCode.ConfigUnreadable} when the sidecar throws.
|
|
112
|
+
*/
|
|
113
|
+
static async load(rootDir, environment) {
|
|
114
|
+
try {
|
|
115
|
+
return await ConfigLoader.load(rootDir, environment);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new CommandFailure([await ConfigSidecar.diagnosticOf(rootDir, error, DiagnosticSeverity.Error)]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run work that loads the sidecar inside another library (a deploy target, a Nitro host). When the work
|
|
122
|
+
* throws, the sidecar is loaded again: if it throws too, the shared failure replaces the error.
|
|
123
|
+
*
|
|
124
|
+
* @param rootDir - The absolute project root.
|
|
125
|
+
* @param environment - The `$env` name the work resolves.
|
|
126
|
+
* @param work - The work.
|
|
127
|
+
* @returns What the work returned.
|
|
128
|
+
*/
|
|
129
|
+
static async during(rootDir, environment, work) {
|
|
130
|
+
try {
|
|
131
|
+
return await work();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (!(error instanceof CommandFailure)) await ConfigSidecar.load(rootDir, environment);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The diagnostic of a sidecar that threw.
|
|
139
|
+
*
|
|
140
|
+
* @param rootDir - The absolute project root.
|
|
141
|
+
* @param error - What the load threw.
|
|
142
|
+
* @param severity - `error` when the command stops; `warning` when it goes on without the config.
|
|
143
|
+
* @param consequence - What the command does without the config, added to the message.
|
|
144
|
+
* @returns The diagnostic, with `file` set when the sidecar file is found.
|
|
145
|
+
*/
|
|
146
|
+
static async diagnosticOf(rootDir, error, severity, consequence) {
|
|
147
|
+
const file = await ConfigLoader.resolveFile(rootDir);
|
|
148
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
149
|
+
return CliDiagnostics.of(severity, CliDiagnosticCode.ConfigUnreadable, `heximon.config.ts could not be loaded${consequence === void 0 ? "" : `, ${consequence}`}: ${reason}`, {
|
|
150
|
+
hint: ConfigSidecar.hint,
|
|
151
|
+
...file === void 0 ? {} : { file }
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
//#endregion
|
|
100
156
|
//#region src/project-compile-session.ts
|
|
101
157
|
/**
|
|
102
158
|
* The from-disk compile prologue every inspection command shares (`check`, `graph`, `routes`, `workflows`):
|
|
@@ -144,9 +200,10 @@ var ProjectCompileSession = class ProjectCompileSession {
|
|
|
144
200
|
*
|
|
145
201
|
* @param options - The project root, the `$env` name, and the resolved source directories.
|
|
146
202
|
* @returns The session the caller compiles through.
|
|
203
|
+
* @throws CommandFailure with `cli.config-unreadable` when the sidecar throws.
|
|
147
204
|
*/
|
|
148
205
|
static async open(options) {
|
|
149
|
-
const config = await
|
|
206
|
+
const config = await ConfigSidecar.load(options.rootDir, options.environment);
|
|
150
207
|
const strategy = NodeCompileSeam.resolveStrategy(config?.platform);
|
|
151
208
|
return new ProjectCompileSession({
|
|
152
209
|
rootDir: options.rootDir,
|
|
@@ -519,9 +576,10 @@ const checkCommand = defineCommand({
|
|
|
519
576
|
description: "Project root directory.",
|
|
520
577
|
default: "."
|
|
521
578
|
},
|
|
579
|
+
json: CommandOutput.jsonFlag,
|
|
522
580
|
format: {
|
|
523
581
|
type: "string",
|
|
524
|
-
description: "Output format: 'pretty' (default) or 'json'.",
|
|
582
|
+
description: "Output format: 'pretty' (default) or 'json' (the same as --json).",
|
|
525
583
|
default: "pretty"
|
|
526
584
|
},
|
|
527
585
|
strict: {
|
|
@@ -557,120 +615,123 @@ const checkCommand = defineCommand({
|
|
|
557
615
|
}
|
|
558
616
|
},
|
|
559
617
|
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
|
|
618
|
+
const commandOutput = CommandOutput.resolve("check", args);
|
|
619
|
+
await commandOutput.run(async () => {
|
|
620
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
621
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
622
|
+
const resolved = CompileModeResolver.resolve(rootDir, args, sourceDirectories);
|
|
623
|
+
const { mode } = resolved;
|
|
624
|
+
const verifyBaseline = args["verify-baseline"] === true;
|
|
625
|
+
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.");
|
|
626
|
+
if (!commandOutput.isJson()) intro(`heximon check — ${basename(rootDir)} · ${CompileModeResolver.stamp(resolved)}`);
|
|
627
|
+
const session = await ProjectCompileSession.open({
|
|
628
|
+
rootDir,
|
|
629
|
+
environment: args.env,
|
|
630
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
631
|
+
});
|
|
632
|
+
let checked;
|
|
633
|
+
if (mode === CompileMode.Library) {
|
|
634
|
+
const emitted = session.compileLibrary({ pruneVerdicts: true });
|
|
635
|
+
const staleLibraries = await session.staleLibraryDiagnostics();
|
|
636
|
+
checked = staleLibraries.length > 0 ? {
|
|
637
|
+
diagnostics: staleLibraries,
|
|
638
|
+
files: [],
|
|
639
|
+
staleLibraries: true,
|
|
640
|
+
...emitted.prunedVerdicts === void 0 ? {} : { prunedVerdicts: emitted.prunedVerdicts }
|
|
634
641
|
} : {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
642
|
+
diagnostics: emitted.diagnostics,
|
|
643
|
+
files: emitted.files,
|
|
644
|
+
...emitted.prunedVerdicts === void 0 ? {} : { prunedVerdicts: emitted.prunedVerdicts }
|
|
645
|
+
};
|
|
646
|
+
} else {
|
|
647
|
+
const output = session.compile(true, { verifyBaseline });
|
|
648
|
+
const staleLibraries = await session.staleLibraryDiagnostics();
|
|
649
|
+
if (staleLibraries.length > 0) checked = {
|
|
650
|
+
diagnostics: staleLibraries,
|
|
651
|
+
files: [],
|
|
652
|
+
staleLibraries: true
|
|
653
|
+
};
|
|
654
|
+
else {
|
|
655
|
+
session.compiler.writeTypeVerdicts(output);
|
|
656
|
+
session.compiler.healTsconfigInclude(session.typeVerdictTarget());
|
|
657
|
+
const emitted = args.emit || session.config?.debug === true;
|
|
658
|
+
if (emitted) {
|
|
659
|
+
session.compiler.write(session.outputDirectory, output.files);
|
|
660
|
+
await session.compiler.formatOnDisk(rootDir, session.outputDirectory, output.files);
|
|
661
|
+
}
|
|
662
|
+
checked = {
|
|
663
|
+
diagnostics: output.diagnostics,
|
|
664
|
+
files: output.files,
|
|
665
|
+
...emitted ? { emittedTo: session.outputDirectory } : {}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
658
668
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
669
|
+
const blockingErrors = checked.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error && diagnostic.escalatedFrom === void 0 && !UpgradeBaseline.isBreaking(diagnostic.code)).length;
|
|
670
|
+
const rebaselined = mode === CompileMode.App && args.update && blockingErrors === 0 ? UpgradeBaseline.update(rootDir, checked.files) : void 0;
|
|
671
|
+
if (rebaselined?.changed === true && checked.diagnostics.some((diagnostic) => UpgradeBaseline.isBreaking(diagnostic.code))) session.compiler.writeTypeVerdicts(session.compile(true, { verifyBaseline }));
|
|
672
|
+
const diagnostics = rebaselined !== void 0 ? checked.diagnostics.filter((diagnostic) => !UpgradeBaseline.isBreaking(diagnostic.code)) : checked.diagnostics;
|
|
673
|
+
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).length;
|
|
674
|
+
const warningCount = diagnostics.length - errorCount;
|
|
675
|
+
const failed = errorCount > 0 || args.strict && warningCount > 0;
|
|
676
|
+
if (commandOutput.isJson()) {
|
|
677
|
+
const census = TransactionCensusReport.read(checked.files);
|
|
678
|
+
const document = {
|
|
679
|
+
ok: !failed,
|
|
680
|
+
mode,
|
|
681
|
+
errors: errorCount,
|
|
682
|
+
warnings: warningCount,
|
|
683
|
+
diagnostics: CliDiagnostics.fromCompiler(diagnostics).map((diagnostic) => checked.staleLibraries === true ? {
|
|
684
|
+
...diagnostic,
|
|
685
|
+
code: CheckDiagnosticCode.StaleLibrary
|
|
686
|
+
} : diagnostic),
|
|
687
|
+
...census === void 0 ? {} : { transactionCensus: census },
|
|
688
|
+
...checked.prunedVerdicts === void 0 ? {} : { prunedVerdicts: checked.prunedVerdicts },
|
|
689
|
+
...checked.emittedTo === void 0 ? {} : { emittedTo: checked.emittedTo },
|
|
690
|
+
...args.update === true ? { upgradeBaseline: rebaselined === void 0 ? {
|
|
691
|
+
written: false,
|
|
692
|
+
blockingErrors
|
|
693
|
+
} : {
|
|
694
|
+
written: true,
|
|
695
|
+
changed: rebaselined.changed,
|
|
696
|
+
path: rebaselined.target
|
|
697
|
+
} } : {}
|
|
698
|
+
};
|
|
699
|
+
commandOutput.emit(document);
|
|
665
700
|
} else {
|
|
666
|
-
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
701
|
+
if (checked.emittedTo !== void 0) log.info(`Wrote the generated wiring to ${checked.emittedTo}`);
|
|
702
|
+
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.`);
|
|
703
|
+
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\`.`);
|
|
704
|
+
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.`);
|
|
705
|
+
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.`);
|
|
706
|
+
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.`);
|
|
707
|
+
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.`);
|
|
708
|
+
else if (args.update === true) log.info("Nothing durable to baseline — no workflows, queue channels or other durable surface found.");
|
|
709
|
+
const census = TransactionCensusReport.read(checked.files);
|
|
710
|
+
const censusSummary = census === void 0 ? void 0 : TransactionCensusReport.summarize(census);
|
|
711
|
+
if (census !== void 0 && censusSummary !== void 0) {
|
|
712
|
+
const sections = [censusSummary];
|
|
713
|
+
const composed = TransactionCensusReport.composedWraps(census);
|
|
714
|
+
const unwrapped = TransactionCensusReport.unwrapped(census);
|
|
715
|
+
if (composed.length > 0) sections.push(`Composed, wrapped on the database the package handed over:\n${composed.map((line) => ` ${line}`).join("\n")}`);
|
|
716
|
+
if (unwrapped.length > 0) sections.push(`Not wrapped:\n${unwrapped.map((line) => ` ${line}`).join("\n")}`);
|
|
717
|
+
log.info(sections.join("\n"));
|
|
718
|
+
}
|
|
719
|
+
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.`);
|
|
720
|
+
if (diagnostics.length > 0) CompileDiagnostics.renderToStderr(rootDir, diagnostics);
|
|
721
|
+
if (failed) {
|
|
722
|
+
const parts = [`${errorCount} error${errorCount === 1 ? "" : "s"}`];
|
|
723
|
+
if (warningCount > 0) parts.push(`${warningCount} warning${warningCount === 1 ? "" : "s"}`);
|
|
724
|
+
outro(`Check failed — ${parts.join(", ")}.`);
|
|
725
|
+
} else {
|
|
726
|
+
const closing = mode === CompileMode.Library ? "Library compiled." : "Verdicts refreshed.";
|
|
727
|
+
if (warningCount > 0) {
|
|
728
|
+
log.warn(`${warningCount} warning${warningCount === 1 ? "" : "s"} — not failing (pass --strict to fail on warnings too).`);
|
|
729
|
+
outro(closing);
|
|
730
|
+
} else outro(`No issues — ${closing.toLowerCase()}`);
|
|
731
|
+
}
|
|
671
732
|
}
|
|
672
|
-
|
|
673
|
-
|
|
733
|
+
if (failed) process.exitCode = 1;
|
|
734
|
+
});
|
|
674
735
|
}
|
|
675
736
|
});
|
|
676
737
|
//#endregion
|
|
@@ -711,10 +772,13 @@ var CliSpinner = class {
|
|
|
711
772
|
* message rather than hanging on a prompt no one can answer). Optional creds (e.g. a Cloudflare token when
|
|
712
773
|
* `wrangler login` already authenticates) are never prompted, so a logged-in deploy is not interrupted.
|
|
713
774
|
*
|
|
775
|
+
* Under `--json` the prompter refuses every prompt: the resolver then fails once, with one `cli.option-required`
|
|
776
|
+
* diagnostic for each required credential that is not set.
|
|
777
|
+
*
|
|
714
778
|
* This whole ladder is CLI-specific. A non-CLI caller (a dashboard) resolves the same `CredentialSpec`s its own way
|
|
715
779
|
* — from a vault / secret store — and hands `DeployRunner` an equivalent map; it never constructs this resolver.
|
|
716
780
|
*/
|
|
717
|
-
var CredentialResolver = class {
|
|
781
|
+
var CredentialResolver = class CredentialResolver {
|
|
718
782
|
/**
|
|
719
783
|
* Resolve the given credential specs into a value map.
|
|
720
784
|
*
|
|
@@ -722,10 +786,12 @@ var CredentialResolver = class {
|
|
|
722
786
|
* @param options - Interactivity, the prompter, and the environment source.
|
|
723
787
|
* @returns The resolved `envVar → value` map (holds secrets — never log it).
|
|
724
788
|
* @throws PromptCancelledError when the operator cancels an interactive prompt.
|
|
789
|
+
* @throws CommandFailure when the prompter refused one or more prompts (`--json`).
|
|
725
790
|
*/
|
|
726
791
|
static async resolve(specs, options) {
|
|
727
792
|
const env = options.env ?? process.env;
|
|
728
793
|
const values = /* @__PURE__ */ new Map();
|
|
794
|
+
const refused = [];
|
|
729
795
|
for (const spec of specs) {
|
|
730
796
|
const fromEnv = env[spec.envVar];
|
|
731
797
|
if (fromEnv !== void 0 && fromEnv.length > 0) {
|
|
@@ -733,13 +799,47 @@ var CredentialResolver = class {
|
|
|
733
799
|
continue;
|
|
734
800
|
}
|
|
735
801
|
if (spec.required && options.interactive && options.prompter !== void 0) {
|
|
736
|
-
const label =
|
|
737
|
-
|
|
802
|
+
const label = CredentialResolver.labelOf(spec);
|
|
803
|
+
let prompted;
|
|
804
|
+
try {
|
|
805
|
+
prompted = spec.secret ? await options.prompter.secret(label) : await options.prompter.text(label);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (!(error instanceof PromptRefusedError)) throw error;
|
|
808
|
+
refused.push(spec);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
738
811
|
if (prompted.length > 0) values.set(spec.envVar, prompted);
|
|
739
812
|
}
|
|
740
813
|
}
|
|
814
|
+
if (refused.length > 0) throw new CommandFailure(refused.map((spec) => CredentialResolver.missing(spec)));
|
|
741
815
|
return values;
|
|
742
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* The prompt label of one credential: its name and its environment variable.
|
|
819
|
+
*
|
|
820
|
+
* @param spec - The credential.
|
|
821
|
+
* @returns The label, for example `Cloudflare API token (CLOUDFLARE_API_TOKEN)`.
|
|
822
|
+
*/
|
|
823
|
+
static labelOf(spec) {
|
|
824
|
+
return `${spec.name} (${spec.envVar})`;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Map a prompt label back to the environment variable that answers it, for a refusing prompter.
|
|
828
|
+
*
|
|
829
|
+
* @param specs - The credentials the run resolves.
|
|
830
|
+
* @returns The map function; an unknown label gives the label itself.
|
|
831
|
+
*/
|
|
832
|
+
static optionFor(specs) {
|
|
833
|
+
const envVarByLabel = new Map(specs.map((spec) => [CredentialResolver.labelOf(spec), spec.envVar]));
|
|
834
|
+
return (message) => envVarByLabel.get(message) ?? message;
|
|
835
|
+
}
|
|
836
|
+
/** The `cli.option-required` diagnostic of one required credential that is not set. */
|
|
837
|
+
static missing(spec) {
|
|
838
|
+
return CliDiagnostics.error(CliDiagnosticCode.OptionRequired, `${spec.name} is not set, and the command never prompts for it under --json.`, {
|
|
839
|
+
hint: `Set ${spec.envVar}.`,
|
|
840
|
+
option: spec.envVar
|
|
841
|
+
});
|
|
842
|
+
}
|
|
743
843
|
};
|
|
744
844
|
//#endregion
|
|
745
845
|
//#region src/deploy-command-failure.ts
|
|
@@ -764,6 +864,48 @@ var DeployCommandFailure = class {
|
|
|
764
864
|
}
|
|
765
865
|
return false;
|
|
766
866
|
}
|
|
867
|
+
/**
|
|
868
|
+
* The `--json` diagnostic of an expected halt. A cancelled prompt has none: `--json` never prompts.
|
|
869
|
+
*
|
|
870
|
+
* @param error - The error {@link report} accepted.
|
|
871
|
+
* @param codes - The codes of the command.
|
|
872
|
+
* @returns The error diagnostic, or `undefined` when the error has no code in `codes`.
|
|
873
|
+
*/
|
|
874
|
+
static diagnosticOf(error, codes) {
|
|
875
|
+
const code = error instanceof DeployStrategyUnavailableError ? codes.StrategyUnavailable : error instanceof HostManagedDeployUnsupportedError ? codes.HostManagedUnsupported : error instanceof EnvFileUnreadableError ? codes.EnvFileUnreadable : void 0;
|
|
876
|
+
return code === void 0 || !(error instanceof Error) ? void 0 : CliDiagnostics.error(code, error.message);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/deploy-json-document.ts
|
|
881
|
+
/**
|
|
882
|
+
* The stable codes of the `heximon deploy --json` diagnostics. A flag conflict uses
|
|
883
|
+
* `cli.invalid-options`, and a missing answer (`--yes`, a credential variable) uses `cli.option-required`.
|
|
884
|
+
*/
|
|
885
|
+
const DeployDiagnosticCode = {
|
|
886
|
+
/**
|
|
887
|
+
* A finding of the deploy run (preflight, migrations, build, infrastructure, provisioning, ship), at its own
|
|
888
|
+
* severity. A dry run that goes on past an error finding reports it as a warning, so `ok` stays `true`.
|
|
889
|
+
*/
|
|
890
|
+
Finding: "deploy.finding",
|
|
891
|
+
/** The run stopped before the upload. Work that ran before the stop is not rolled back. */
|
|
892
|
+
Halted: "deploy.halted",
|
|
893
|
+
/** The vendor rejected the upload, so the artifact is not live. */
|
|
894
|
+
ShipFailed: "deploy.ship-failed",
|
|
895
|
+
/** The app shipped, but the `--verify` liveness probe got no 2xx answer. */
|
|
896
|
+
NotLive: "deploy.not-live",
|
|
897
|
+
/** The app shipped, but a migration after the ship failed to apply. */
|
|
898
|
+
MigrationFailed: "deploy.migration-failed",
|
|
899
|
+
/** `--migrate` found no database config to apply. */
|
|
900
|
+
NoMigrationConfig: "deploy.no-migration-config",
|
|
901
|
+
/** `--destroy` on a platform that has no teardown step. */
|
|
902
|
+
DestroyUnsupported: "deploy.destroy-unsupported",
|
|
903
|
+
/** No deploy strategy is installed for the platform. */
|
|
904
|
+
StrategyUnavailable: "deploy.strategy-unavailable",
|
|
905
|
+
/** The `--env-file` file cannot be read. */
|
|
906
|
+
EnvFileUnreadable: "deploy.env-file-unreadable",
|
|
907
|
+
/** A Nitro or Nuxt build uses a preset that the deploy cannot ship. */
|
|
908
|
+
HostManagedUnsupported: "deploy.host-managed-unsupported"
|
|
767
909
|
};
|
|
768
910
|
//#endregion
|
|
769
911
|
//#region src/deploy-command.ts
|
|
@@ -818,6 +960,8 @@ var DeployArgumentGuards = class DeployArgumentGuards {
|
|
|
818
960
|
* failure, which a local test cannot stage.
|
|
819
961
|
*/
|
|
820
962
|
var DeployReport = class DeployReport {
|
|
963
|
+
/** The hint of an error finding that a dry run went on past. */
|
|
964
|
+
static dryRunFindingHint = "The dry run went on, but a deploy without --dry-run stops on this finding.";
|
|
821
965
|
/**
|
|
822
966
|
* Print the whole outcome and decide the command's exit.
|
|
823
967
|
*
|
|
@@ -837,8 +981,7 @@ var DeployReport = class DeployReport {
|
|
|
837
981
|
return false;
|
|
838
982
|
}
|
|
839
983
|
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("; ")}.`);
|
|
984
|
+
cancel(DeployReport.haltMessage(outcome));
|
|
842
985
|
return false;
|
|
843
986
|
}
|
|
844
987
|
for (const message of outcome.result.messages) log.message(message);
|
|
@@ -866,6 +1009,112 @@ var DeployReport = class DeployReport {
|
|
|
866
1009
|
static hasFailedMigration(outcome) {
|
|
867
1010
|
return (outcome.migrations ?? []).some((migrated) => migrated.diagnostics.some((diagnostic) => diagnostic.severity === DeployDiagnosticSeverity.Error));
|
|
868
1011
|
}
|
|
1012
|
+
/**
|
|
1013
|
+
* The `--json` diagnostics of an outcome: each finding of the runner, each missing credential, and one
|
|
1014
|
+
* closing error when {@link render} fails the run.
|
|
1015
|
+
*
|
|
1016
|
+
* A run that succeeds exits 0, so it has no error diagnostic: a dry run goes on past a finding that would
|
|
1017
|
+
* stop a real deploy, and that finding is a warning here, with a hint that says so.
|
|
1018
|
+
*
|
|
1019
|
+
* @param outcome - What the runner reported.
|
|
1020
|
+
* @param succeeded - What {@link render} returned for the outcome.
|
|
1021
|
+
* @returns The diagnostics, in the order the text report prints them.
|
|
1022
|
+
*/
|
|
1023
|
+
static diagnostics(outcome, succeeded) {
|
|
1024
|
+
const diagnostics = outcome.diagnostics.map((diagnostic) => DeployReport.finding(diagnostic, succeeded));
|
|
1025
|
+
for (const credential of outcome.missingCredentials) diagnostics.push({
|
|
1026
|
+
...CliDiagnostics.error(CliDiagnosticCode.OptionRequired, `${credential.name} is not set.`, {
|
|
1027
|
+
hint: `Set ${credential.envVar}.`,
|
|
1028
|
+
option: credential.envVar
|
|
1029
|
+
}),
|
|
1030
|
+
severity: outcome.halted ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning
|
|
1031
|
+
});
|
|
1032
|
+
for (const diagnostic of outcome.verification?.diagnostics ?? []) diagnostics.push(DeployReport.finding(diagnostic, succeeded));
|
|
1033
|
+
const closing = DeployReport.closingDiagnostic(outcome);
|
|
1034
|
+
if (closing !== void 0) diagnostics.push(closing);
|
|
1035
|
+
return diagnostics;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* The `ship` section of the `--json` document.
|
|
1039
|
+
*
|
|
1040
|
+
* @param outcome - What the runner reported.
|
|
1041
|
+
* @param run - The facts the command recorded around the runner.
|
|
1042
|
+
* @returns The section.
|
|
1043
|
+
*/
|
|
1044
|
+
static ship(outcome, run) {
|
|
1045
|
+
return {
|
|
1046
|
+
halted: outcome.halted,
|
|
1047
|
+
...run,
|
|
1048
|
+
missingCredentials: outcome.missingCredentials.map((credential) => ({
|
|
1049
|
+
name: credential.name,
|
|
1050
|
+
envVar: credential.envVar
|
|
1051
|
+
})),
|
|
1052
|
+
...outcome.targetEnsure === void 0 ? {} : { targetBootstrap: DeployReport.resources(outcome.targetEnsure) },
|
|
1053
|
+
...outcome.provision === void 0 ? {} : { provision: DeployReport.resources(outcome.provision) },
|
|
1054
|
+
migrations: (outcome.migrations ?? []).map((migrated) => ({
|
|
1055
|
+
target: migrated.target,
|
|
1056
|
+
phase: migrated.phase,
|
|
1057
|
+
applied: migrated.applied,
|
|
1058
|
+
failed: migrated.diagnostics.some((diagnostic) => diagnostic.severity === DeployDiagnosticSeverity.Error)
|
|
1059
|
+
})),
|
|
1060
|
+
...outcome.result === void 0 ? {} : { result: {
|
|
1061
|
+
platform: outcome.result.platform,
|
|
1062
|
+
shipped: outcome.result.shipped,
|
|
1063
|
+
failed: outcome.result.failed === true,
|
|
1064
|
+
...outcome.result.url === void 0 ? {} : { url: outcome.result.url },
|
|
1065
|
+
messages: outcome.result.messages
|
|
1066
|
+
} },
|
|
1067
|
+
...outcome.verification === void 0 ? {} : { verification: {
|
|
1068
|
+
url: outcome.verification.url,
|
|
1069
|
+
live: outcome.verification.live
|
|
1070
|
+
} },
|
|
1071
|
+
completedBeforeHalt: outcome.halted ? DeployReport.completedBeforeHalt(outcome) : []
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* The error that closes a failed run, in the same order of checks as {@link render}; `undefined` when
|
|
1076
|
+
* the run succeeded.
|
|
1077
|
+
*/
|
|
1078
|
+
static closingDiagnostic(outcome) {
|
|
1079
|
+
if (outcome.result?.failed === true) return CliDiagnostics.error(DeployDiagnosticCode.ShipFailed, `The ship to ${outcome.result.platform} failed, so the artifact is not live.`);
|
|
1080
|
+
if (outcome.halted || outcome.result === void 0) return CliDiagnostics.error(DeployDiagnosticCode.Halted, DeployReport.haltMessage(outcome));
|
|
1081
|
+
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.`);
|
|
1082
|
+
if (DeployReport.hasFailedMigration(outcome)) return CliDiagnostics.error(DeployDiagnosticCode.MigrationFailed, `Deployed to ${outcome.result.platform}, but a migration failed to apply.`);
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* The halt line. Only the UPLOAD is known not to have happened: target bootstrap, provisioning and a
|
|
1086
|
+
* pre-ship migration run earlier and are never rolled back, so the line names them instead of denying them.
|
|
1087
|
+
*/
|
|
1088
|
+
static haltMessage(outcome) {
|
|
1089
|
+
const completed = DeployReport.completedBeforeHalt(outcome);
|
|
1090
|
+
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("; ")}.`;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* One runner finding as a `deploy.finding` diagnostic, at its own severity. In a run that succeeds, an
|
|
1094
|
+
* error finding is a warning, because the run went on past it.
|
|
1095
|
+
*/
|
|
1096
|
+
static finding(diagnostic, succeeded) {
|
|
1097
|
+
if (diagnostic.severity !== DeployDiagnosticSeverity.Error) return {
|
|
1098
|
+
severity: DiagnosticSeverity.Warning,
|
|
1099
|
+
code: DeployDiagnosticCode.Finding,
|
|
1100
|
+
message: diagnostic.message
|
|
1101
|
+
};
|
|
1102
|
+
return {
|
|
1103
|
+
severity: succeeded ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error,
|
|
1104
|
+
code: DeployDiagnosticCode.Finding,
|
|
1105
|
+
message: diagnostic.message,
|
|
1106
|
+
...succeeded ? { hint: DeployReport.dryRunFindingHint } : {}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
/** Copy a phase's resource outcome into its document shape. */
|
|
1110
|
+
static resources(result) {
|
|
1111
|
+
return {
|
|
1112
|
+
created: result.created,
|
|
1113
|
+
existing: result.existing,
|
|
1114
|
+
failed: result.failed,
|
|
1115
|
+
messages: result.messages
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
869
1118
|
/** The runner's own findings, at the severity each carries. */
|
|
870
1119
|
static logDiagnostics(diagnostics) {
|
|
871
1120
|
for (const diagnostic of diagnostics ?? []) if (diagnostic.severity === DeployDiagnosticSeverity.Error) log.error(diagnostic.message);
|
|
@@ -923,18 +1172,70 @@ var DeployReport = class DeployReport {
|
|
|
923
1172
|
return false;
|
|
924
1173
|
}
|
|
925
1174
|
};
|
|
1175
|
+
/**
|
|
1176
|
+
* Builds the `heximon deploy --json` document. The run fields are set as the run reaches them, so a
|
|
1177
|
+
* failure document still says how far the run got.
|
|
1178
|
+
*/
|
|
1179
|
+
var DeployDocument = class {
|
|
1180
|
+
args;
|
|
1181
|
+
target;
|
|
1182
|
+
credentials;
|
|
1183
|
+
/**
|
|
1184
|
+
* @param args - The parsed deploy flags.
|
|
1185
|
+
*/
|
|
1186
|
+
constructor(args) {
|
|
1187
|
+
this.args = args;
|
|
1188
|
+
}
|
|
1189
|
+
/** Record the resolved target. */
|
|
1190
|
+
resolved(target) {
|
|
1191
|
+
this.target = {
|
|
1192
|
+
platform: target.platformName ?? "node",
|
|
1193
|
+
targetClass: target.targetClass,
|
|
1194
|
+
hostManaged: target.hostManaged === true,
|
|
1195
|
+
...target.artifactDir === void 0 ? {} : { artifactDir: target.artifactDir }
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
/** Record the names of the resolved credential variables. */
|
|
1199
|
+
resolvedCredentials(credentials) {
|
|
1200
|
+
this.credentials = [...credentials.keys()];
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* The document of the run.
|
|
1204
|
+
*
|
|
1205
|
+
* @param ok - Whether the command exits 0.
|
|
1206
|
+
* @param diagnostics - The diagnostics of the run.
|
|
1207
|
+
* @param sections - The `ship` or `destroy` section, once the run has one.
|
|
1208
|
+
* @returns The document.
|
|
1209
|
+
*/
|
|
1210
|
+
document(ok, diagnostics, sections = {}) {
|
|
1211
|
+
return {
|
|
1212
|
+
ok,
|
|
1213
|
+
diagnostics,
|
|
1214
|
+
action: this.args.destroy ? "destroy" : "ship",
|
|
1215
|
+
environment: this.args.env,
|
|
1216
|
+
dryRun: this.args["dry-run"],
|
|
1217
|
+
...this.target === void 0 ? {} : { target: this.target },
|
|
1218
|
+
...this.credentials === void 0 ? {} : { credentials: this.credentials },
|
|
1219
|
+
...sections
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
926
1223
|
/** The `--destroy` teardown and the pre-ship discovery/migration composition the ship path runs. */
|
|
927
|
-
var DeployPhases = class {
|
|
1224
|
+
var DeployPhases = class DeployPhases {
|
|
1225
|
+
/** The halt line of a `--migrate` run that found no database config. */
|
|
1226
|
+
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
1227
|
/**
|
|
929
|
-
* Confirm and run the teardown.
|
|
1228
|
+
* Confirm and run the teardown, and record its document.
|
|
930
1229
|
*
|
|
931
1230
|
* @returns Whether the command should exit non-zero.
|
|
932
1231
|
*/
|
|
933
|
-
static async destroy(args, target, rootDir, credentials,
|
|
1232
|
+
static async destroy(args, target, rootDir, credentials, commandOutput, report) {
|
|
934
1233
|
const platformName = target.platformName ?? "node";
|
|
1234
|
+
const interactive = commandOutput.isInteractive();
|
|
1235
|
+
const question = `Remove the deployed app on ${platformName} (env: ${args.env})? This cannot be undone.`;
|
|
935
1236
|
if (interactive) {
|
|
936
1237
|
const answer = await confirm({
|
|
937
|
-
message:
|
|
1238
|
+
message: question,
|
|
938
1239
|
initialValue: false
|
|
939
1240
|
});
|
|
940
1241
|
if (isCancel(answer)) {
|
|
@@ -946,6 +1247,7 @@ var DeployPhases = class {
|
|
|
946
1247
|
return false;
|
|
947
1248
|
}
|
|
948
1249
|
} else if (!args.yes) {
|
|
1250
|
+
commandOutput.assertCanPrompt("--yes", question);
|
|
949
1251
|
cancel("Refusing to --destroy non-interactively without --yes.");
|
|
950
1252
|
return true;
|
|
951
1253
|
}
|
|
@@ -965,12 +1267,21 @@ var DeployPhases = class {
|
|
|
965
1267
|
throw teardownError;
|
|
966
1268
|
}
|
|
967
1269
|
removing?.stop(args["dry-run"] ? "Dry run — nothing removed." : "Teardown step complete.");
|
|
1270
|
+
const destroy = {
|
|
1271
|
+
platform: teardown.result?.platform ?? teardown.platformName ?? "node",
|
|
1272
|
+
unsupported: teardown.unsupported,
|
|
1273
|
+
destroyed: teardown.result?.destroyed === true,
|
|
1274
|
+
messages: teardown.result?.messages ?? []
|
|
1275
|
+
};
|
|
968
1276
|
if (teardown.unsupported) {
|
|
969
|
-
|
|
1277
|
+
const message = `Platform ${teardown.platformName ?? "node (default)"} has nothing to remove (no destroy step).`;
|
|
1278
|
+
cancel(message);
|
|
1279
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(DeployDiagnosticCode.DestroyUnsupported, message)], { destroy }));
|
|
970
1280
|
return true;
|
|
971
1281
|
}
|
|
972
1282
|
for (const message of teardown.result?.messages ?? []) log.message(message);
|
|
973
1283
|
outro(teardown.result?.destroyed === true ? `Removed from ${teardown.result.platform}.` : `Dry run — nothing removed (${teardown.result?.platform ?? target.platformName ?? "node"}).`);
|
|
1284
|
+
commandOutput.emit(report.document(true, [], { destroy }));
|
|
974
1285
|
return false;
|
|
975
1286
|
}
|
|
976
1287
|
/**
|
|
@@ -1046,7 +1357,7 @@ var DeployPhases = class {
|
|
|
1046
1357
|
};
|
|
1047
1358
|
} 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
1359
|
else {
|
|
1049
|
-
cancel(
|
|
1360
|
+
cancel(DeployPhases.noMigrationConfig);
|
|
1050
1361
|
return null;
|
|
1051
1362
|
}
|
|
1052
1363
|
if (args.migrations !== void 0) log.info("--migrations <dir> is superseded by the auto-detected database config(s) under --migrate.");
|
|
@@ -1070,6 +1381,7 @@ const deployCommand = defineCommand({
|
|
|
1070
1381
|
description: "Project root directory.",
|
|
1071
1382
|
default: "."
|
|
1072
1383
|
},
|
|
1384
|
+
json: CommandOutput.jsonFlag,
|
|
1073
1385
|
env: {
|
|
1074
1386
|
type: "string",
|
|
1075
1387
|
description: "Deploy environment (production / staging / preview:<id>).",
|
|
@@ -1141,145 +1453,668 @@ const deployCommand = defineCommand({
|
|
|
1141
1453
|
}
|
|
1142
1454
|
},
|
|
1143
1455
|
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);
|
|
1162
|
-
process.exitCode = 1;
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
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) {
|
|
1456
|
+
const commandOutput = CommandOutput.resolve("deploy", args);
|
|
1457
|
+
await commandOutput.run(async () => {
|
|
1458
|
+
const deployArgs = args;
|
|
1459
|
+
const rootDir = ProjectRoot.resolve(deployArgs.cwd);
|
|
1460
|
+
const environment = deployArgs.env;
|
|
1461
|
+
const interactive = commandOutput.isInteractive();
|
|
1462
|
+
const report = new DeployDocument(deployArgs);
|
|
1463
|
+
intro(`heximon deploy · ${environment}`);
|
|
1464
|
+
const incoherence = DeployArgumentGuards.incoherence(deployArgs);
|
|
1465
|
+
if (incoherence !== void 0) {
|
|
1466
|
+
cancel(incoherence);
|
|
1187
1467
|
process.exitCode = 1;
|
|
1468
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(CliDiagnosticCode.InvalidOptions, incoherence)]));
|
|
1188
1469
|
return;
|
|
1189
1470
|
}
|
|
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
1471
|
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 }
|
|
1472
|
+
const target = await ConfigSidecar.during(rootDir, environment, () => DeployTargetResolver.resolve(rootDir, environment, deployArgs.platform));
|
|
1473
|
+
report.resolved(target);
|
|
1474
|
+
log.info(`Platform: ${target.platformName ?? "node (default)"} · target class: ${target.targetClass}`);
|
|
1475
|
+
if (target.hostManaged) log.info(`Host-managed build detected (${target.artifactDir}) — shipping it, skipping \`vp build\`.`);
|
|
1476
|
+
const profileMismatch = DeployArgumentGuards.profileMismatch(deployArgs.profile, target.platformName);
|
|
1477
|
+
if (profileMismatch !== void 0) {
|
|
1478
|
+
cancel(profileMismatch);
|
|
1479
|
+
process.exitCode = 1;
|
|
1480
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(CliDiagnosticCode.InvalidOptions, profileMismatch)]));
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const specs = CredentialSelection.inPlay(target.strategy.requiredCredentials(), { provisioning: deployArgs.provision });
|
|
1484
|
+
const credentials = await CredentialResolver.resolve(specs, {
|
|
1485
|
+
interactive: interactive || commandOutput.isJson() && !deployArgs["dry-run"],
|
|
1486
|
+
prompter: commandOutput.prompter(CredentialResolver.optionFor(specs))
|
|
1215
1487
|
});
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1488
|
+
report.resolvedCredentials(credentials);
|
|
1489
|
+
if (credentials.size > 0) log.success(`Resolved ${credentials.size} credential(s).`);
|
|
1490
|
+
if (deployArgs.destroy) {
|
|
1491
|
+
if (await DeployPhases.destroy(deployArgs, target, rootDir, credentials, commandOutput, report)) process.exitCode = 1;
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
let secrets;
|
|
1495
|
+
if (deployArgs["env-file"] !== void 0) {
|
|
1496
|
+
secrets = await EnvFileReader.read(resolve(rootDir, deployArgs["env-file"]));
|
|
1497
|
+
log.info(`Loaded ${secrets.size} secret(s) from ${deployArgs["env-file"]}.`);
|
|
1498
|
+
}
|
|
1499
|
+
const buildSkipped = deployArgs["skip-build"] || target.hostManaged === true;
|
|
1500
|
+
let ledgerValues = [];
|
|
1501
|
+
if (!buildSkipped) {
|
|
1502
|
+
const injectable = DeployLedger.injectableEntries(await DeployLedger.read(rootDir, environment));
|
|
1503
|
+
ledgerValues = [...injectable.keys()];
|
|
1504
|
+
if (injectable.size > 0) log.info(`Injecting ${injectable.size} deploy-ledger value(s) into the build: ${ledgerValues.join(", ")}.`);
|
|
1505
|
+
}
|
|
1506
|
+
DeployPhases.logHarvest(await DeployPhases.harvest(rootDir), deployArgs.migrate);
|
|
1507
|
+
const migrations = await DeployPhases.composeMigrations(deployArgs, rootDir, secrets);
|
|
1508
|
+
if (migrations === null) {
|
|
1509
|
+
process.exitCode = 1;
|
|
1510
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(DeployDiagnosticCode.NoMigrationConfig, DeployPhases.noMigrationConfig)]));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const progress = CliSpinner.begin(interactive, "Preparing the deploy…");
|
|
1514
|
+
const phases = [];
|
|
1515
|
+
const onProgress = (event) => {
|
|
1516
|
+
phases.push(event.phase);
|
|
1517
|
+
if (progress === void 0) log.info(event.message);
|
|
1518
|
+
else progress.message(event.message);
|
|
1519
|
+
};
|
|
1520
|
+
let outcome;
|
|
1521
|
+
try {
|
|
1522
|
+
outcome = await DeployRunner.run({
|
|
1523
|
+
target,
|
|
1524
|
+
rootDir,
|
|
1525
|
+
environment,
|
|
1526
|
+
credentials,
|
|
1527
|
+
dryRun: deployArgs["dry-run"],
|
|
1528
|
+
skipBuild: buildSkipped,
|
|
1529
|
+
...target.artifactDir === void 0 ? {} : { artifactDir: target.artifactDir },
|
|
1530
|
+
provision: deployArgs.provision,
|
|
1531
|
+
...secrets === void 0 ? {} : { secrets },
|
|
1532
|
+
...deployArgs.create === void 0 ? {} : { createTarget: {
|
|
1533
|
+
name: deployArgs.create,
|
|
1534
|
+
...deployArgs.team === void 0 ? {} : { team: deployArgs.team }
|
|
1535
|
+
} },
|
|
1536
|
+
verify: deployArgs.verify,
|
|
1537
|
+
...deployArgs.profile === void 0 ? {} : { profile: deployArgs.profile },
|
|
1538
|
+
onProgress,
|
|
1539
|
+
...migrations === void 0 ? {} : { migrations }
|
|
1540
|
+
});
|
|
1541
|
+
} catch (runError) {
|
|
1542
|
+
progress?.stop("Deploy failed — see the error below.");
|
|
1543
|
+
throw runError;
|
|
1544
|
+
}
|
|
1545
|
+
progress?.stop(outcome.halted ? "Stopped — details below." : "Deploy steps complete.");
|
|
1546
|
+
const succeeded = DeployReport.render(outcome, interactive);
|
|
1547
|
+
if (!succeeded) process.exitCode = 1;
|
|
1548
|
+
commandOutput.emit(report.document(succeeded, DeployReport.diagnostics(outcome, succeeded), { ship: DeployReport.ship(outcome, {
|
|
1549
|
+
phases,
|
|
1550
|
+
buildSkipped,
|
|
1551
|
+
ledgerValues,
|
|
1552
|
+
migrationTargets: migrations?.apply === true ? migrations.targets.map((migrationTarget) => migrationTarget.name) : []
|
|
1553
|
+
}) }));
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
if (DeployCommandFailure.report(error)) {
|
|
1556
|
+
const diagnostic = DeployCommandFailure.diagnosticOf(error, DeployDiagnosticCode);
|
|
1557
|
+
if (diagnostic !== void 0) commandOutput.emit(report.document(false, [diagnostic]));
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (error instanceof CommandFailure && commandOutput.isJson()) {
|
|
1561
|
+
commandOutput.emit(report.document(false, error.diagnostics));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
throw error;
|
|
1219
1565
|
}
|
|
1220
|
-
|
|
1221
|
-
if (!DeployReport.render(outcome, interactive)) process.exitCode = 1;
|
|
1222
|
-
} catch (error) {
|
|
1223
|
-
if (DeployCommandFailure.report(error)) return;
|
|
1224
|
-
throw error;
|
|
1225
|
-
}
|
|
1566
|
+
});
|
|
1226
1567
|
}
|
|
1227
1568
|
});
|
|
1228
1569
|
//#endregion
|
|
1229
|
-
//#region src/doctor-
|
|
1230
|
-
/**
|
|
1231
|
-
|
|
1232
|
-
/**
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1570
|
+
//#region src/doctor-json-document.ts
|
|
1571
|
+
/** Which audit set one `heximon doctor` run ran. */
|
|
1572
|
+
const DoctorMode = {
|
|
1573
|
+
/** The default run: the audits of an app and the libraries it consumes. */
|
|
1574
|
+
App: "app",
|
|
1575
|
+
/** `--self`: the pre-publish check of the current library package. */
|
|
1576
|
+
Self: "self"
|
|
1577
|
+
};
|
|
1578
|
+
/** The verdict on one audited package in a {@link DoctorJsonDocument}. */
|
|
1579
|
+
const DoctorPackageStatus = {
|
|
1580
|
+
/** The packed manifest matches the local source of the package. */
|
|
1581
|
+
Fresh: "fresh",
|
|
1582
|
+
/**
|
|
1583
|
+
* The local source changed since the last pack, some recorded source is gone, or a file the bundle read outside
|
|
1584
|
+
* the source changed or is gone.
|
|
1585
|
+
*/
|
|
1586
|
+
Stale: "stale",
|
|
1587
|
+
/** The packed manifest has no provenance stamp, so nothing was compared. */
|
|
1588
|
+
NoProvenance: "no-provenance",
|
|
1589
|
+
/** The package has no local source (a published package), so nothing was compared. */
|
|
1590
|
+
NoSource: "no-source",
|
|
1591
|
+
/** The package resolves to more than one physical copy. */
|
|
1592
|
+
Fork: "fork"
|
|
1593
|
+
};
|
|
1594
|
+
/** The outcome of the physical-copy census in a {@link DoctorJsonDocument}. */
|
|
1595
|
+
const DoctorCopyCensusStatus = {
|
|
1596
|
+
/** Every probed package resolves to one copy. */
|
|
1597
|
+
SingleCopies: "single-copies",
|
|
1598
|
+
/** At least one probed package resolves to more than one copy. */
|
|
1599
|
+
Forked: "forked",
|
|
1600
|
+
/** The census ran but found no `node_modules` to resolve from, so it could not look. */
|
|
1601
|
+
NothingProbed: "nothing-probed",
|
|
1602
|
+
/** `--self` found no pnpm workspace, so the census did not run. */
|
|
1603
|
+
NotRun: "not-run"
|
|
1604
|
+
};
|
|
1605
|
+
/**
|
|
1606
|
+
* The stable codes of the `heximon doctor` diagnostics. A `heximon.config.ts` that `--self` cannot load is the
|
|
1607
|
+
* shared `cli.config-unreadable`, as a warning: the run goes on without its `expectedOpenPeers`.
|
|
1608
|
+
*/
|
|
1609
|
+
const DoctorDiagnosticCode = {
|
|
1610
|
+
/** `@heximon/*` cannot be read from GitHub Packages. Error. */
|
|
1611
|
+
RegistryAccessFailed: "doctor.registry-access-failed",
|
|
1612
|
+
/** A packed library no longer matches its local source. Error. */
|
|
1613
|
+
StalePackage: "doctor.stale-package",
|
|
1614
|
+
/** A package resolves to more than one physical copy. Error when it fails the run, else warning. */
|
|
1615
|
+
PackageFork: "doctor.package-fork",
|
|
1616
|
+
/** The copy census found nothing to resolve from. Warning. */
|
|
1617
|
+
CopiesNotProbed: "doctor.copies-not-probed",
|
|
1618
|
+
/** The deployed wrangler config does not carry the tracing directives. Warning. */
|
|
1619
|
+
PlatformTracingNotCarried: "doctor.platform-tracing-not-carried",
|
|
1620
|
+
/** A `@heximon/*` peer of the dependency closure is declared nowhere in `package.json`. Warning. */
|
|
1621
|
+
PeerPivotUndeclared: "doctor.peer-pivot-undeclared",
|
|
1622
|
+
/** An `expectedOpenPeers` name matches no undeclared peer. Warning. */
|
|
1623
|
+
StalePeerAcknowledgement: "doctor.stale-peer-acknowledgement"
|
|
1624
|
+
};
|
|
1625
|
+
//#endregion
|
|
1626
|
+
//#region src/doctor-audit.ts
|
|
1627
|
+
/** Runs the `heximon doctor` audits, and owns the verdicts that the exit status and both renderers read. */
|
|
1628
|
+
var DoctorAudit = class DoctorAudit {
|
|
1629
|
+
/**
|
|
1630
|
+
* Run every audit of one mode. Read-only.
|
|
1631
|
+
*
|
|
1632
|
+
* @param rootDir - The absolute project root.
|
|
1633
|
+
* @param self - Run the `--self` pre-publish check instead of the app audits.
|
|
1634
|
+
* @returns The audit result.
|
|
1635
|
+
*/
|
|
1636
|
+
static async run(rootDir, self) {
|
|
1637
|
+
const registry = await new RegistryAccess().probe();
|
|
1638
|
+
return self ? DoctorAudit.runSelf(rootDir, registry) : DoctorAudit.runApp(rootDir, registry);
|
|
1639
|
+
}
|
|
1640
|
+
/** Whether the registry probe failed. */
|
|
1641
|
+
static isRegistryFailed(result) {
|
|
1642
|
+
return result.registry.status !== RegistryAccessStatus.Ok;
|
|
1643
|
+
}
|
|
1644
|
+
/** Whether the run has a finding that exits 1. */
|
|
1645
|
+
static isFailure(result) {
|
|
1646
|
+
if (DoctorAudit.isRegistryFailed(result)) return true;
|
|
1647
|
+
if (result.mode === DoctorMode.App) return DoctorAudit.staleCount(result.boundaries) > 0;
|
|
1648
|
+
return DoctorAudit.gatingForks(result).length > 0 || result.selfReport?.status === BoundaryProvenanceStatus.Stale;
|
|
1649
|
+
}
|
|
1650
|
+
/** The census reports of packages that resolve to more than one copy. */
|
|
1651
|
+
static forkedReports(audit) {
|
|
1652
|
+
return audit.reports.filter((report) => report.copies.length > 1);
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* The forks that fail a `--self` run: forks of a runtime-reachable package. A default run has none, because
|
|
1656
|
+
* its census is reported and never fails the run.
|
|
1657
|
+
*/
|
|
1658
|
+
static gatingForks(result) {
|
|
1659
|
+
if (result.mode === DoctorMode.App || result.copyAudit === void 0) return [];
|
|
1660
|
+
if (result.copyAudit.originsProbed === 0) return [];
|
|
1661
|
+
const gatingPackages = result.gatingPackages;
|
|
1662
|
+
return DoctorAudit.forkedReports(result.copyAudit).filter((report) => gatingPackages === void 0 || gatingPackages.has(report.packageName));
|
|
1663
|
+
}
|
|
1664
|
+
/** How many reports are stale. */
|
|
1665
|
+
static staleCount(reports) {
|
|
1666
|
+
return reports.filter((report) => report.status === BoundaryProvenanceStatus.Stale).length;
|
|
1667
|
+
}
|
|
1668
|
+
/** The closing sentence of the run. */
|
|
1669
|
+
static summary(result) {
|
|
1670
|
+
if (result.mode === DoctorMode.App) {
|
|
1671
|
+
if (result.boundaries.length === 0) return "No consumed Heximon library packages found — no staleness audit to run.";
|
|
1672
|
+
const staleCount = DoctorAudit.staleCount(result.boundaries);
|
|
1673
|
+
return staleCount > 0 ? `${staleCount} stale package(s) — rebuild them before trusting this app's build.` : `${result.boundaries.length} package(s) audited — no staleness detected.`;
|
|
1241
1674
|
}
|
|
1675
|
+
const parts = [];
|
|
1676
|
+
const forked = DoctorAudit.gatingForks(result).length > 0;
|
|
1677
|
+
if (forked) {
|
|
1678
|
+
const hasConsumers = (result.workspaceConsumers?.consumers.length ?? 0) > 0;
|
|
1679
|
+
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.");
|
|
1680
|
+
}
|
|
1681
|
+
if (result.selfReport === null) parts.push("This directory is not a packed Heximon library (no dist/heximon.manifest.json and no dist/heximon.inputs.json) — the peer-closure and copy census above still ran; run `vp pack` to enable the staleness check too.");
|
|
1682
|
+
else if (result.selfReport.status === BoundaryProvenanceStatus.Stale || !forked) parts.push(DoctorAudit.selfProvenanceSummary(result.selfReport));
|
|
1683
|
+
return parts.join(" ");
|
|
1242
1684
|
}
|
|
1243
|
-
/**
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1685
|
+
/**
|
|
1686
|
+
* The package names a fork of which is RUNTIME-reachable from the library at `rootDir`: the library itself,
|
|
1687
|
+
* its installed `@heximon/*` dependency closure ({@link PeerClosureReport.closurePackages}), every named peer
|
|
1688
|
+
* pivot of that closure, the curated {@link RuntimeSingletons}, and every installed boundary package
|
|
1689
|
+
* ({@link PackageCopyAuditor.installedBoundaryPackages}). A fork of any other name is confined to build-time
|
|
1690
|
+
* tooling (for example a devDependency-only bundler plugin), so it is reported and does not fail the run.
|
|
1691
|
+
*/
|
|
1692
|
+
static runtimeReachablePackageNames(rootDir, closureReport) {
|
|
1693
|
+
const names = new Set(RuntimeSingletons.all());
|
|
1694
|
+
const libraryName = DoctorAudit.readPackageName(rootDir);
|
|
1695
|
+
if (libraryName !== null) names.add(libraryName);
|
|
1696
|
+
for (const packageName of PackageCopyAuditor.installedBoundaryPackages(rootDir)) names.add(packageName);
|
|
1697
|
+
if (closureReport !== null) {
|
|
1698
|
+
for (const packageName of closureReport.closurePackages) names.add(packageName);
|
|
1699
|
+
for (const pivot of closureReport.pivots) names.add(pivot.packageName);
|
|
1700
|
+
}
|
|
1701
|
+
return names;
|
|
1249
1702
|
}
|
|
1250
1703
|
/**
|
|
1251
|
-
* The `--self`
|
|
1704
|
+
* The `--self` sentence for the own-dist verdict.
|
|
1252
1705
|
*
|
|
1253
|
-
* Only {@link BoundaryProvenanceStatus.Fresh}
|
|
1254
|
-
* comparison did not run
|
|
1255
|
-
* check that never happened as a pass — the one thing a pre-publish gate must never do.
|
|
1706
|
+
* Only {@link BoundaryProvenanceStatus.Fresh} compared anything. The other non-stale verdicts mean the
|
|
1707
|
+
* comparison did not run, so they must never read as a pass.
|
|
1256
1708
|
*/
|
|
1257
1709
|
static selfProvenanceSummary(report) {
|
|
1258
1710
|
switch (report.status) {
|
|
1259
|
-
case BoundaryProvenanceStatus.Fresh: return "This package's dist is current with its src/.";
|
|
1711
|
+
case BoundaryProvenanceStatus.Fresh: return report.manifestSourceHash === void 0 ? "This package's dist is current with its build inputs." : "This package's dist is current with its src/.";
|
|
1260
1712
|
case BoundaryProvenanceStatus.NoProvenance: return "Its packed manifest predates the provenance stamp, so nothing was compared — repack (`vp pack`) to enable the staleness check.";
|
|
1261
1713
|
case BoundaryProvenanceStatus.NoSource: return "It ships no local src/, so its packed manifest was not compared with anything.";
|
|
1262
1714
|
case BoundaryProvenanceStatus.Stale: return "Your dist is STALE — repack (`vp pack`) before publishing or consuming this package.";
|
|
1263
1715
|
}
|
|
1264
1716
|
}
|
|
1717
|
+
/** The default run: the copy census, the platform-tracing check, then the staleness audit. */
|
|
1718
|
+
static async runApp(rootDir, registry) {
|
|
1719
|
+
const copyAudit = PackageCopyAuditor.audit(rootDir);
|
|
1720
|
+
const platformTracing = await PlatformTracingAuditor.audit(rootDir);
|
|
1721
|
+
const boundaries = await ConfigSidecar.during(rootDir, void 0, () => BoundaryProvenanceAuditor.audit(rootDir));
|
|
1722
|
+
return {
|
|
1723
|
+
mode: DoctorMode.App,
|
|
1724
|
+
rootDir,
|
|
1725
|
+
registry,
|
|
1726
|
+
copyAudit,
|
|
1727
|
+
platformTracing,
|
|
1728
|
+
boundaries
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
/** The `--self` run: the peer-closure roster, the library-side census, then the own-dist staleness check. */
|
|
1732
|
+
static async runSelf(rootDir, registry) {
|
|
1733
|
+
let configError;
|
|
1734
|
+
let configDiagnostic;
|
|
1735
|
+
let expectedOpenPeers = [];
|
|
1736
|
+
try {
|
|
1737
|
+
expectedOpenPeers = (await ConfigLoader.load(rootDir))?.expectedOpenPeers ?? [];
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
configError = error instanceof Error ? error.message : String(error);
|
|
1740
|
+
configDiagnostic = await ConfigSidecar.diagnosticOf(rootDir, error, DiagnosticSeverity.Warning, "so its `expectedOpenPeers` acknowledgements are not applied");
|
|
1741
|
+
}
|
|
1742
|
+
const closureReport = PeerClosureAuditor.audit(rootDir, expectedOpenPeers);
|
|
1743
|
+
const workspaceConsumers = WorkspaceConsumerLocator.locate(rootDir);
|
|
1744
|
+
const census = workspaceConsumers === null ? {} : {
|
|
1745
|
+
copyAudit: PackageCopyAuditor.auditLibrary(rootDir, workspaceConsumers.consumers),
|
|
1746
|
+
gatingPackages: DoctorAudit.runtimeReachablePackageNames(rootDir, closureReport)
|
|
1747
|
+
};
|
|
1748
|
+
const selfReport = await ConfigSidecar.during(rootDir, void 0, () => BoundaryProvenanceAuditor.auditSelf(rootDir));
|
|
1749
|
+
return {
|
|
1750
|
+
mode: DoctorMode.Self,
|
|
1751
|
+
rootDir,
|
|
1752
|
+
registry,
|
|
1753
|
+
...configError === void 0 ? {} : { configError },
|
|
1754
|
+
...configDiagnostic === void 0 ? {} : { configDiagnostic },
|
|
1755
|
+
closureReport,
|
|
1756
|
+
workspaceConsumers,
|
|
1757
|
+
...census,
|
|
1758
|
+
selfReport
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
/** The `name` field of a directory's `package.json`, or `null` when absent, unreadable or malformed. */
|
|
1762
|
+
static readPackageName(directory) {
|
|
1763
|
+
try {
|
|
1764
|
+
const parsed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
1765
|
+
if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
|
|
1766
|
+
const { name } = parsed;
|
|
1767
|
+
return typeof name === "string" ? name : null;
|
|
1768
|
+
}
|
|
1769
|
+
} catch {
|
|
1770
|
+
return null;
|
|
1771
|
+
}
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
/** Renders a {@link DoctorAuditResult} as the `heximon doctor --json` document. */
|
|
1776
|
+
var DoctorJsonReport = class DoctorJsonReport {
|
|
1777
|
+
/** The fix of a fork: no single command removes one. */
|
|
1778
|
+
static forkHint = "Deduplicate to one copy (align versions, or hoist it).";
|
|
1779
|
+
/** Shell words that need no quotes. */
|
|
1780
|
+
static plainShellWord = /^[\w./@+:=,-]+$/;
|
|
1265
1781
|
/**
|
|
1266
|
-
*
|
|
1267
|
-
* 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. `gatingPackages`, when given, scopes
|
|
1269
|
-
* which forks count as GATING ({@link DoctorReport.runtimeReachablePackageNames}) — every fork is still
|
|
1270
|
-
* printed above either way, and a fork outside the set gets one extra line noting it is reported, not gated.
|
|
1782
|
+
* Build the document of one run.
|
|
1271
1783
|
*
|
|
1272
|
-
* @
|
|
1784
|
+
* @param result - The audit result.
|
|
1785
|
+
* @returns The document. Its `ok` is `false` exactly when {@link DoctorAudit.isFailure} is `true`.
|
|
1273
1786
|
*/
|
|
1274
|
-
static
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1787
|
+
static toJson(result) {
|
|
1788
|
+
const packages = DoctorJsonReport.packages(result);
|
|
1789
|
+
const diagnostics = [
|
|
1790
|
+
...DoctorJsonReport.registryDiagnostics(result),
|
|
1791
|
+
...DoctorJsonReport.modeDiagnostics(result),
|
|
1792
|
+
...DoctorJsonReport.packageDiagnostics(packages)
|
|
1793
|
+
];
|
|
1794
|
+
const common = {
|
|
1795
|
+
ok: !DoctorAudit.isFailure(result),
|
|
1796
|
+
diagnostics,
|
|
1797
|
+
mode: result.mode,
|
|
1798
|
+
rootDir: result.rootDir,
|
|
1799
|
+
registry: DoctorJsonReport.registry(result.registry),
|
|
1800
|
+
packages,
|
|
1801
|
+
packageCopies: DoctorJsonReport.copyCensus(result),
|
|
1802
|
+
summary: DoctorAudit.summary(result)
|
|
1803
|
+
};
|
|
1804
|
+
if (result.mode === DoctorMode.App) {
|
|
1805
|
+
const tracing = result.platformTracing;
|
|
1806
|
+
return {
|
|
1807
|
+
...common,
|
|
1808
|
+
platformTracing: {
|
|
1809
|
+
status: tracing.status,
|
|
1810
|
+
...tracing.wranglerConfigPath === void 0 ? {} : { wranglerConfigPath: tracing.wranglerConfigPath },
|
|
1811
|
+
tracingNotEnabled: tracing.tracingNotEnabled,
|
|
1812
|
+
...tracing.droppedSamplerRatio === void 0 ? {} : { droppedSamplerRatio: tracing.droppedSamplerRatio }
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1279
1815
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1816
|
+
const closure = result.closureReport;
|
|
1817
|
+
return {
|
|
1818
|
+
...common,
|
|
1819
|
+
...closure === null ? {} : { peerClosure: {
|
|
1820
|
+
closurePackages: closure.closurePackages,
|
|
1821
|
+
pivots: closure.pivots.map((pivot) => DoctorJsonReport.pivot(pivot)),
|
|
1822
|
+
uncovered: closure.uncovered.map((pivot) => DoctorJsonReport.pivot(pivot)),
|
|
1823
|
+
expectedOpen: closure.expectedOpen.map((pivot) => DoctorJsonReport.pivot(pivot)),
|
|
1824
|
+
staleExpectedOpen: closure.staleExpectedOpen
|
|
1825
|
+
} },
|
|
1826
|
+
packed: result.selfReport !== null
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
/** The registry section. */
|
|
1830
|
+
static registry(report) {
|
|
1831
|
+
return {
|
|
1832
|
+
registry: RegistryAccess.gitHubPackagesRegistry,
|
|
1833
|
+
probePackage: RegistryAccess.probePackage,
|
|
1834
|
+
status: report.status,
|
|
1835
|
+
...report.detail === void 0 ? {} : { detail: report.detail },
|
|
1836
|
+
...report.status === RegistryAccessStatus.Ok ? {} : { instructions: RegistryAccess.fixInstructions(report.status) }
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
/** The census section. */
|
|
1840
|
+
static copyCensus(result) {
|
|
1841
|
+
const workspace = result.mode === DoctorMode.Self && result.workspaceConsumers !== null ? {
|
|
1842
|
+
workspaceRoot: result.workspaceConsumers.workspaceRoot,
|
|
1843
|
+
consumers: result.workspaceConsumers.consumers.map((consumer) => consumer.name)
|
|
1844
|
+
} : {};
|
|
1845
|
+
if (result.copyAudit === void 0) return {
|
|
1846
|
+
status: DoctorCopyCensusStatus.NotRun,
|
|
1847
|
+
originsProbed: 0,
|
|
1848
|
+
packagesProbed: 0,
|
|
1849
|
+
forkedPackages: 0,
|
|
1850
|
+
...workspace
|
|
1851
|
+
};
|
|
1852
|
+
const forkedPackages = result.copyAudit.originsProbed === 0 ? 0 : DoctorAudit.forkedReports(result.copyAudit).length;
|
|
1853
|
+
let status = DoctorCopyCensusStatus.SingleCopies;
|
|
1854
|
+
if (result.copyAudit.originsProbed === 0) status = DoctorCopyCensusStatus.NothingProbed;
|
|
1855
|
+
else if (forkedPackages > 0) status = DoctorCopyCensusStatus.Forked;
|
|
1856
|
+
return {
|
|
1857
|
+
status,
|
|
1858
|
+
originsProbed: result.copyAudit.originsProbed,
|
|
1859
|
+
packagesProbed: result.copyAudit.packagesProbed,
|
|
1860
|
+
forkedPackages,
|
|
1861
|
+
...workspace
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
/** Every audited package, in the order of the text report: the forks first, then the staleness verdicts. */
|
|
1865
|
+
static packages(result) {
|
|
1866
|
+
const packages = [];
|
|
1867
|
+
if (result.copyAudit !== void 0 && result.copyAudit.originsProbed > 0) {
|
|
1868
|
+
const gating = new Set(DoctorAudit.gatingForks(result));
|
|
1869
|
+
for (const report of DoctorAudit.forkedReports(result.copyAudit)) packages.push(DoctorJsonReport.forkEntry(report, gating.has(report)));
|
|
1870
|
+
}
|
|
1871
|
+
if (result.mode === DoctorMode.App) for (const report of result.boundaries) packages.push(DoctorJsonReport.staleness(report, false));
|
|
1872
|
+
else if (result.selfReport !== null) packages.push(DoctorJsonReport.staleness(result.selfReport, true));
|
|
1873
|
+
return packages;
|
|
1874
|
+
}
|
|
1875
|
+
/** One fork entry. */
|
|
1876
|
+
static forkEntry(report, failing) {
|
|
1877
|
+
return {
|
|
1878
|
+
name: report.packageName,
|
|
1879
|
+
status: DoctorPackageStatus.Fork,
|
|
1880
|
+
reason: `${String(report.copies.length)} physical copies. Class identity is the DI token, so these bind nominally yet split at runtime.`,
|
|
1881
|
+
hint: DoctorJsonReport.forkHint,
|
|
1882
|
+
self: false,
|
|
1883
|
+
failing,
|
|
1884
|
+
copies: report.copies.map((copy) => ({
|
|
1885
|
+
directory: copy.directory,
|
|
1886
|
+
...copy.version === void 0 ? {} : { version: copy.version },
|
|
1887
|
+
origins: copy.origins
|
|
1888
|
+
}))
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
/** One staleness entry. `self` marks the package that `--self` audits. */
|
|
1892
|
+
static staleness(report, self) {
|
|
1893
|
+
const freshness = report;
|
|
1894
|
+
const repack = DoctorJsonReport.repackCommand(report.packageDirectory);
|
|
1895
|
+
const verdict = DoctorJsonReport.verdictOf(report.status, self ? "Repack (`vp pack`) before publishing or consuming it." : `Rebuild it (\`vp pack\` in ${report.packageDirectory}) before trusting this app's build.`, repack);
|
|
1896
|
+
return {
|
|
1897
|
+
name: report.specifier,
|
|
1898
|
+
status: verdict.status,
|
|
1899
|
+
reason: DoctorJsonReport.reasonOf(freshness),
|
|
1900
|
+
...verdict.hint === void 0 ? {} : { hint: verdict.hint },
|
|
1901
|
+
...verdict.fix === void 0 ? {} : { fix: verdict.fix },
|
|
1902
|
+
failing: verdict.failing,
|
|
1903
|
+
self,
|
|
1904
|
+
...report.packageVersion === void 0 ? {} : { version: report.packageVersion },
|
|
1905
|
+
directory: report.packageDirectory,
|
|
1906
|
+
...DoctorJsonReport.freshnessDetails(freshness)
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* The status, the hint, the fix and whether it fails the run, for a verdict: a stale package fails and is
|
|
1911
|
+
* repacked, a package with no provenance is repacked, and the others need nothing.
|
|
1912
|
+
*
|
|
1913
|
+
* @param status - The status of a {@link LibraryFreshness} verdict.
|
|
1914
|
+
* @param staleHint - The hint of a stale package.
|
|
1915
|
+
* @param repack - The command that repacks the package.
|
|
1916
|
+
* @returns The fields.
|
|
1917
|
+
*/
|
|
1918
|
+
static verdictOf(status, staleHint, repack) {
|
|
1919
|
+
switch (status) {
|
|
1920
|
+
case LibraryFreshnessStatus.Fresh: return {
|
|
1921
|
+
status: DoctorPackageStatus.Fresh,
|
|
1922
|
+
failing: false
|
|
1923
|
+
};
|
|
1924
|
+
case LibraryFreshnessStatus.Stale: return {
|
|
1925
|
+
status: DoctorPackageStatus.Stale,
|
|
1926
|
+
hint: staleHint,
|
|
1927
|
+
fix: repack,
|
|
1928
|
+
failing: true
|
|
1929
|
+
};
|
|
1930
|
+
case LibraryFreshnessStatus.NoProvenance: return {
|
|
1931
|
+
status: DoctorPackageStatus.NoProvenance,
|
|
1932
|
+
hint: "Repack (`vp pack`) to enable the staleness check.",
|
|
1933
|
+
fix: repack,
|
|
1934
|
+
failing: false
|
|
1935
|
+
};
|
|
1936
|
+
case LibraryFreshnessStatus.NoSource: return {
|
|
1937
|
+
status: DoctorPackageStatus.NoSource,
|
|
1938
|
+
failing: false
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* The fields of a verdict that the JSON documents print beside its status: the hashes, the missing and the
|
|
1944
|
+
* current source, and the stale build entries. A field the verdict does not have is left out.
|
|
1945
|
+
*
|
|
1946
|
+
* @param report - A {@link LibraryFreshness} verdict.
|
|
1947
|
+
* @returns The fields.
|
|
1948
|
+
*/
|
|
1949
|
+
static freshnessDetails(report) {
|
|
1950
|
+
return {
|
|
1951
|
+
...report.manifestSourceHash === void 0 ? {} : { manifestSourceHash: report.manifestSourceHash },
|
|
1952
|
+
...report.recomputedSourceHash === void 0 ? {} : { recomputedSourceHash: report.recomputedSourceHash },
|
|
1953
|
+
...report.missingSource === void 0 ? {} : { missingSource: report.missingSource },
|
|
1954
|
+
...report.currentSource === void 0 ? {} : { currentSource: report.currentSource },
|
|
1955
|
+
...report.manifestBundleInputHash === void 0 ? {} : { manifestBundleInputHash: report.manifestBundleInputHash },
|
|
1956
|
+
...report.recomputedBundleInputHash === void 0 ? {} : { recomputedBundleInputHash: report.recomputedBundleInputHash },
|
|
1957
|
+
...report.missingBundleInputs === void 0 ? {} : { missingBundleInputs: report.missingBundleInputs },
|
|
1958
|
+
...report.staleBuildEntries === void 0 ? {} : { staleBuildEntries: report.staleBuildEntries }
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Why a package has the status of its verdict, as one sentence.
|
|
1963
|
+
*
|
|
1964
|
+
* @param report - A {@link LibraryFreshness} verdict.
|
|
1965
|
+
* @returns The reason.
|
|
1966
|
+
*/
|
|
1967
|
+
static reasonOf(report) {
|
|
1968
|
+
switch (report.status) {
|
|
1969
|
+
case LibraryFreshnessStatus.Fresh: return report.manifestSourceHash === void 0 ? "The pack inputs file matches the local build inputs." : `The packed manifest matches the local source (${report.manifestSourceHash}).`;
|
|
1970
|
+
case LibraryFreshnessStatus.Stale: return DoctorJsonReport.staleReason(report);
|
|
1971
|
+
case LibraryFreshnessStatus.NoProvenance: return "The packed manifest predates the provenance stamp, so nothing was compared.";
|
|
1972
|
+
case LibraryFreshnessStatus.NoSource: return "A published package with no local src/, so its provenance is display-only.";
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* The shell command that repacks a package.
|
|
1977
|
+
*
|
|
1978
|
+
* @param packageDirectory - The package root.
|
|
1979
|
+
* @returns `cd <directory> && vp pack`, with the directory quoted when it needs quotes.
|
|
1980
|
+
*/
|
|
1981
|
+
static repackCommand(packageDirectory) {
|
|
1982
|
+
return `cd ${DoctorJsonReport.shellWord(packageDirectory)} && vp pack`;
|
|
1983
|
+
}
|
|
1984
|
+
/** Why a stale report is stale, in the words and the order of the text report. */
|
|
1985
|
+
static staleReason(report) {
|
|
1986
|
+
const entries = LibraryFreshness.staleBuildEntriesClause(report);
|
|
1987
|
+
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}${entries}.`;
|
|
1988
|
+
if (report.missingSource !== void 0 && report.missingSource.length > 0) return `The source it was packed from (${report.missingSource.join(", ")}) is no longer on disk${entries}.`;
|
|
1989
|
+
if (report.recomputedSourceHash !== void 0 && report.manifestSourceHash !== void 0 && report.recomputedSourceHash !== report.manifestSourceHash) return DoctorJsonReport.sourceChangedReason(report);
|
|
1990
|
+
if (report.staleBuildEntries !== void 0 && report.staleBuildEntries.length > 0) {
|
|
1991
|
+
const reason = LibraryFreshness.staleBuildEntriesReason(report.staleBuildEntries);
|
|
1992
|
+
return `${reason.charAt(0).toUpperCase()}${reason.slice(1)}.`;
|
|
1993
|
+
}
|
|
1994
|
+
if (report.missingBundleInputs !== void 0) return `Files its build read outside its source (${report.missingBundleInputs.join(", ")}) are no longer on disk.`;
|
|
1995
|
+
if (report.recomputedBundleInputHash !== void 0) return `A file its build read outside its source changed: the build inputs (${report.recomputedBundleInputHash}) no longer match the packed manifest (${String(report.manifestBundleInputHash)}).`;
|
|
1996
|
+
return DoctorJsonReport.sourceChangedReason(report);
|
|
1997
|
+
}
|
|
1998
|
+
/** The reason for a source fingerprint that no longer matches the manifest. */
|
|
1999
|
+
static sourceChangedReason(report) {
|
|
2000
|
+
return `Its src/ (${String(report.recomputedSourceHash)}) no longer matches the packed manifest (${String(report.manifestSourceHash)})${LibraryFreshness.staleBuildEntriesClause(report)}.`;
|
|
2001
|
+
}
|
|
2002
|
+
/** The registry diagnostic, when the probe failed. */
|
|
2003
|
+
static registryDiagnostics(result) {
|
|
2004
|
+
if (!DoctorAudit.isRegistryFailed(result)) return [];
|
|
2005
|
+
return [CliDiagnostics.error(DoctorDiagnosticCode.RegistryAccessFailed, `@heximon/* cannot be read from GitHub Packages (${result.registry.status}).`, { hint: RegistryAccess.fixInstructions(result.registry.status) })];
|
|
2006
|
+
}
|
|
2007
|
+
/** The advisory diagnostics of the mode: the census, the tracing check, the roster and the sidecar. */
|
|
2008
|
+
static modeDiagnostics(result) {
|
|
2009
|
+
const diagnostics = [];
|
|
2010
|
+
if (result.mode === DoctorMode.Self && result.configDiagnostic !== void 0) diagnostics.push(result.configDiagnostic);
|
|
2011
|
+
if (result.mode === DoctorMode.Self && result.closureReport !== null) {
|
|
2012
|
+
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)."));
|
|
2013
|
+
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)."));
|
|
2014
|
+
}
|
|
2015
|
+
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."));
|
|
2016
|
+
if (result.mode === DoctorMode.App && result.platformTracing.status === PlatformTracingStatus.NotCarried) diagnostics.push(DoctorJsonReport.tracingDiagnostic(result.platformTracing));
|
|
2017
|
+
return diagnostics;
|
|
2018
|
+
}
|
|
2019
|
+
/** One diagnostic per failing or forked package, in package order. */
|
|
2020
|
+
static packageDiagnostics(packages) {
|
|
2021
|
+
const diagnostics = [];
|
|
2022
|
+
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 }));
|
|
2023
|
+
else if (entry.status === DoctorPackageStatus.Fork) {
|
|
2024
|
+
const message = `${entry.name} resolves to ${String(entry.copies?.length ?? 0)} physical copies.`;
|
|
2025
|
+
diagnostics.push(entry.failing ? CliDiagnostics.error(DoctorDiagnosticCode.PackageFork, message, { hint: DoctorJsonReport.forkHint }) : DoctorJsonReport.warning(DoctorDiagnosticCode.PackageFork, message, DoctorJsonReport.forkHint));
|
|
2026
|
+
}
|
|
2027
|
+
return diagnostics;
|
|
2028
|
+
}
|
|
2029
|
+
/** The platform-tracing warning. */
|
|
2030
|
+
static tracingDiagnostic(report) {
|
|
2031
|
+
const target = report.wranglerConfigPath ?? "the committed wrangler config";
|
|
2032
|
+
const hints = [];
|
|
2033
|
+
if (report.tracingNotEnabled) hints.push(`Add "observability": { "traces": { "enabled": true } } to ${target} by hand.`);
|
|
2034
|
+
if (report.droppedSamplerRatio !== void 0) hints.push(`Set observability.traces.head_sampling_rate to ${String(report.droppedSamplerRatio)} in ${target} by hand.`);
|
|
2035
|
+
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(" "));
|
|
2036
|
+
}
|
|
2037
|
+
/** One warning diagnostic. */
|
|
2038
|
+
static warning(code, message, hint) {
|
|
2039
|
+
return {
|
|
2040
|
+
severity: DiagnosticSeverity.Warning,
|
|
2041
|
+
code,
|
|
2042
|
+
message,
|
|
2043
|
+
...hint === "" ? {} : { hint }
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
/** One pivot, copied out of the deploy report. */
|
|
2047
|
+
static pivot(pivot) {
|
|
2048
|
+
return {
|
|
2049
|
+
packageName: pivot.packageName,
|
|
2050
|
+
declaredBy: pivot.declaredBy
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
/** A path as one POSIX shell word: as it is when it is plain, else in single quotes. */
|
|
2054
|
+
static shellWord(path) {
|
|
2055
|
+
return DoctorJsonReport.plainShellWord.test(path) ? path : `'${path.replaceAll("'", "'\\''")}'`;
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
//#endregion
|
|
2059
|
+
//#region src/doctor-command.ts
|
|
2060
|
+
/** The text renderer behind {@link doctorCommand}: every audit of a {@link DoctorAuditResult} as clack lines. */
|
|
2061
|
+
var DoctorReport = class DoctorReport {
|
|
2062
|
+
/**
|
|
2063
|
+
* Print the whole report of one run, ending with its summary as the outro.
|
|
2064
|
+
*
|
|
2065
|
+
* @param result - The audit result.
|
|
2066
|
+
*/
|
|
2067
|
+
static render(result) {
|
|
2068
|
+
if (DoctorAudit.isRegistryFailed(result)) log.error(RegistryAccess.fixInstructions(result.registry.status));
|
|
2069
|
+
else log.success("✔ GitHub Packages — @heximon/* readable");
|
|
2070
|
+
if (result.mode === DoctorMode.Self) {
|
|
2071
|
+
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}`);
|
|
2072
|
+
DoctorReport.logPeerClosureRoster(result.closureReport);
|
|
2073
|
+
if (result.copyAudit === void 0) log.info("Package copies — no pnpm workspace found from this directory; census not run.");
|
|
2074
|
+
else DoctorReport.logCopyAudit(result.copyAudit, DoctorAudit.gatingForks(result));
|
|
2075
|
+
if (result.selfReport !== null) DoctorReport.logBoundary(result.selfReport, true);
|
|
2076
|
+
} else {
|
|
2077
|
+
DoctorReport.logCopyAudit(result.copyAudit);
|
|
2078
|
+
DoctorReport.logPlatformTracing(result.platformTracing);
|
|
2079
|
+
for (const report of result.boundaries) DoctorReport.logBoundary(report);
|
|
2080
|
+
}
|
|
2081
|
+
outro(DoctorAudit.summary(result));
|
|
2082
|
+
}
|
|
2083
|
+
/** Renders one audited boundary as a status line. `self` reframes the wording for the current package. */
|
|
2084
|
+
static boundaryLine(report, self = false) {
|
|
2085
|
+
const name = self ? `${report.specifier} (this package)` : report.specifier;
|
|
2086
|
+
const version = report.packageVersion === void 0 ? "" : ` v${report.packageVersion}`;
|
|
2087
|
+
switch (report.status) {
|
|
2088
|
+
case BoundaryProvenanceStatus.Fresh:
|
|
2089
|
+
if (report.manifestSourceHash === void 0) return `✔ ${name}${version} — dist is current with its build inputs`;
|
|
2090
|
+
return self ? `✔ ${name}${version} — dist is current with src/ (${report.manifestSourceHash})` : `✔ ${name}${version} — up to date (${report.manifestSourceHash})`;
|
|
2091
|
+
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})${LibraryFreshness.staleBuildEntriesClause(report)}. Repack (\`vp pack\`) before publishing or consuming it.` : LibraryFreshness.staleLine(self ? name : report.specifier, report);
|
|
2092
|
+
case BoundaryProvenanceStatus.NoProvenance: return `● ${name}${version} — manifest predates the provenance stamp; repack to enable the check.`;
|
|
2093
|
+
case BoundaryProvenanceStatus.NoSource: return `● ${name}${version} — published package (no local src/); provenance is display-only.`;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
/** Logs one report at the severity its status warrants (error for stale, success for fresh, info otherwise). */
|
|
2097
|
+
static logBoundary(report, self = false) {
|
|
2098
|
+
const line = DoctorReport.boundaryLine(report, self);
|
|
2099
|
+
if (report.status === BoundaryProvenanceStatus.Stale) log.error(line);
|
|
2100
|
+
else if (report.status === BoundaryProvenanceStatus.Fresh) log.success(line);
|
|
2101
|
+
else log.info(line);
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Logs the physical-copy census: a line per forked package naming every copy and who resolves to it, or one
|
|
2105
|
+
* summary line when nothing forked. The summary always states how many origins were probed — an audit that
|
|
2106
|
+
* could not look at anything must not read as a clean bill of health. `gatingForks`, given by `--self`, are
|
|
2107
|
+
* the forks that fail the run ({@link DoctorAudit.gatingForks}); every fork is printed either way.
|
|
2108
|
+
*/
|
|
2109
|
+
static logCopyAudit(audit, gatingForks) {
|
|
2110
|
+
const forked = DoctorAudit.forkedReports(audit);
|
|
2111
|
+
if (audit.originsProbed === 0) {
|
|
2112
|
+
log.warn("Package copies — nothing probed (no resolvable node_modules from this directory), so no fork check ran.");
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
if (forked.length === 0) {
|
|
2116
|
+
log.success(`Package copies — ${String(audit.packagesProbed)} package(s) probed from ${String(audit.originsProbed)} origin(s); each resolves to a single copy.`);
|
|
2117
|
+
return;
|
|
1283
2118
|
}
|
|
1284
2119
|
for (const report of forked) {
|
|
1285
2120
|
const copies = report.copies.map((copy) => {
|
|
@@ -1287,17 +2122,15 @@ var DoctorReport = class DoctorReport {
|
|
|
1287
2122
|
}).join("\n");
|
|
1288
2123
|
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
2124
|
}
|
|
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) {
|
|
2125
|
+
if (gatingForks === void 0) {
|
|
1296
2126
|
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
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
if (gatingForks.length === 0) {
|
|
2130
|
+
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.`);
|
|
2131
|
+
return;
|
|
1298
2132
|
}
|
|
1299
2133
|
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
2134
|
}
|
|
1302
2135
|
/**
|
|
1303
2136
|
* Logs the `@heximon/*` peer-pivot roster ({@link PeerClosureAuditor}): an info line for pivots acknowledged
|
|
@@ -1337,40 +2170,6 @@ var DoctorReport = class DoctorReport {
|
|
|
1337
2170
|
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
2171
|
log.warn(lines.join("\n"));
|
|
1339
2172
|
}
|
|
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
2173
|
};
|
|
1375
2174
|
/**
|
|
1376
2175
|
* `heximon doctor` — three read-only audits.
|
|
@@ -1413,6 +2212,9 @@ var DoctorReport = class DoctorReport {
|
|
|
1413
2212
|
* Compile-free and filesystem-local (the `heximon.config.ts` sidecar is loaded only to read
|
|
1414
2213
|
* `expectedOpenPeers`); exits non-zero when this package forks a runtime-reachable package against a
|
|
1415
2214
|
* workspace consumer OR its own dist is stale.
|
|
2215
|
+
*
|
|
2216
|
+
* `--json` prints the same audit result as one `DoctorJsonDocument` on stdout, with each finding as a
|
|
2217
|
+
* diagnostic. Both modes exit 1 exactly when the document has `ok: false`.
|
|
1416
2218
|
*/
|
|
1417
2219
|
const doctorCommand = defineCommand({
|
|
1418
2220
|
meta: {
|
|
@@ -1425,6 +2227,7 @@ const doctorCommand = defineCommand({
|
|
|
1425
2227
|
description: "Project root directory.",
|
|
1426
2228
|
default: "."
|
|
1427
2229
|
},
|
|
2230
|
+
json: CommandOutput.jsonFlag,
|
|
1428
2231
|
self: {
|
|
1429
2232
|
type: "boolean",
|
|
1430
2233
|
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 +2235,512 @@ const doctorCommand = defineCommand({
|
|
|
1432
2235
|
}
|
|
1433
2236
|
},
|
|
1434
2237
|
async run({ args }) {
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2238
|
+
const commandOutput = CommandOutput.resolve("doctor", args);
|
|
2239
|
+
await commandOutput.run(async () => {
|
|
2240
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
2241
|
+
if (!commandOutput.isJson()) intro(`heximon doctor — ${basename(rootDir)}`);
|
|
2242
|
+
const result = await DoctorAudit.run(rootDir, args.self);
|
|
2243
|
+
if (DoctorAudit.isFailure(result)) process.exitCode = 1;
|
|
2244
|
+
if (commandOutput.isJson()) commandOutput.emit(DoctorJsonReport.toJson(result));
|
|
2245
|
+
else DoctorReport.render(result);
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
});
|
|
2249
|
+
//#endregion
|
|
2250
|
+
//#region src/freshness-json-document.ts
|
|
2251
|
+
/**
|
|
2252
|
+
* The verdict on one workspace package in a {@link FreshnessJsonDocument}. The first four are the staleness
|
|
2253
|
+
* values of `DoctorPackageStatus`.
|
|
2254
|
+
*/
|
|
2255
|
+
const FreshnessPackageStatus = {
|
|
2256
|
+
/** Every recorded input of the package matches the files on disk. */
|
|
2257
|
+
Fresh: "fresh",
|
|
2258
|
+
/** The source or another recorded input changed since the last pack, or is gone. */
|
|
2259
|
+
Stale: "stale",
|
|
2260
|
+
/** The packed manifest has no provenance stamp, so nothing was compared. */
|
|
2261
|
+
NoProvenance: "no-provenance",
|
|
2262
|
+
/** None of the recorded source is on disk, so nothing was compared. */
|
|
2263
|
+
NoSource: "no-source",
|
|
2264
|
+
/** The check of the package failed, for example because its `heximon.config.ts` threw. */
|
|
2265
|
+
Error: "error"
|
|
2266
|
+
};
|
|
2267
|
+
/** The stable codes of the `heximon freshness` diagnostics. */
|
|
2268
|
+
const FreshnessDiagnosticCode = {
|
|
2269
|
+
/** A workspace package changed since its last pack. Error. */
|
|
2270
|
+
StalePackage: "freshness.stale-package",
|
|
2271
|
+
/** The check of one package failed; the message has the error. Error. */
|
|
2272
|
+
CheckFailed: "freshness.check-failed",
|
|
2273
|
+
/** The `pnpm-workspace.yaml` or the root `package.json` cannot be read or parsed. Error. */
|
|
2274
|
+
WorkspaceUnreadable: "freshness.workspace-unreadable"
|
|
2275
|
+
};
|
|
2276
|
+
//#endregion
|
|
2277
|
+
//#region src/workspace-packages.ts
|
|
2278
|
+
/** Where the member list of a {@link Workspace} comes from. */
|
|
2279
|
+
const WorkspaceKind = {
|
|
2280
|
+
/** The `packages:` list of a `pnpm-workspace.yaml`. */
|
|
2281
|
+
Pnpm: "pnpm",
|
|
2282
|
+
/** The `workspaces` field of a root `package.json` (npm, Yarn, Bun). */
|
|
2283
|
+
PackageJson: "package-json",
|
|
2284
|
+
/** No workspace file: the nearest package is the only member. */
|
|
2285
|
+
SinglePackage: "single-package",
|
|
2286
|
+
/** No workspace file and no `package.json` from the start directory up. */
|
|
2287
|
+
None: "none"
|
|
2288
|
+
};
|
|
2289
|
+
/**
|
|
2290
|
+
* Finds the workspace that holds a directory and lists its packages. It walks up to the nearest directory with a
|
|
2291
|
+
* `pnpm-workspace.yaml`, or with a `package.json` that has a `workspaces` field (an array, or Yarn's
|
|
2292
|
+
* `{ packages }` form). With neither, the nearest package is a one-package workspace. The root package is a
|
|
2293
|
+
* member when it has a `package.json`. A `!` pattern removes the members it matches, and `node_modules` is never
|
|
2294
|
+
* searched. A folder that two paths reach (a symlink) is one member, under the path that sorts first.
|
|
2295
|
+
*/
|
|
2296
|
+
var WorkspacePackages = class WorkspacePackages {
|
|
2297
|
+
/** The package manifest file name. */
|
|
2298
|
+
static packageJsonFileName = "package.json";
|
|
2299
|
+
/** The folder name the member search never enters. */
|
|
2300
|
+
static installedDirectoryName = "node_modules";
|
|
2301
|
+
/**
|
|
2302
|
+
* Find the workspace of a directory.
|
|
2303
|
+
*
|
|
2304
|
+
* @param startDirectory - The directory to start the walk from.
|
|
2305
|
+
* @returns The workspace and its member packages.
|
|
2306
|
+
* @throws When a `pnpm-workspace.yaml` or a root `package.json` on the walk cannot be read or parsed.
|
|
2307
|
+
*/
|
|
2308
|
+
static locate(startDirectory) {
|
|
2309
|
+
const start = resolve(startDirectory);
|
|
2310
|
+
let nearestPackage;
|
|
2311
|
+
let current = start;
|
|
2312
|
+
while (true) {
|
|
2313
|
+
if (existsSync(join(current, PnpmWorkspaceManifest.fileName))) {
|
|
2314
|
+
const manifestPath = join(current, PnpmWorkspaceManifest.fileName);
|
|
2315
|
+
const patterns = PnpmWorkspaceManifest.parsePackagePatterns(readFileSync(manifestPath, "utf8"), manifestPath);
|
|
2316
|
+
return WorkspacePackages.workspaceOf(current, WorkspaceKind.Pnpm, patterns);
|
|
1458
2317
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
2318
|
+
const packageJsonPath = join(current, WorkspacePackages.packageJsonFileName);
|
|
2319
|
+
if (existsSync(packageJsonPath)) {
|
|
2320
|
+
nearestPackage ??= current;
|
|
2321
|
+
const patterns = WorkspacePackages.workspacePatternsOf(packageJsonPath);
|
|
2322
|
+
if (patterns !== void 0) return WorkspacePackages.workspaceOf(current, WorkspaceKind.PackageJson, patterns);
|
|
2323
|
+
}
|
|
2324
|
+
const parent = dirname(current);
|
|
2325
|
+
if (parent === current) break;
|
|
2326
|
+
current = parent;
|
|
2327
|
+
}
|
|
2328
|
+
return nearestPackage === void 0 ? {
|
|
2329
|
+
root: start,
|
|
2330
|
+
kind: WorkspaceKind.None,
|
|
2331
|
+
members: []
|
|
2332
|
+
} : {
|
|
2333
|
+
root: nearestPackage,
|
|
2334
|
+
kind: WorkspaceKind.SinglePackage,
|
|
2335
|
+
members: [{
|
|
2336
|
+
directory: WorkspacePackages.realPathOf(nearestPackage),
|
|
2337
|
+
path: "."
|
|
2338
|
+
}]
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* The `workspaces` patterns of a `package.json`.
|
|
2343
|
+
*
|
|
2344
|
+
* @returns The patterns, or `undefined` when the file has no `workspaces` field in a known form.
|
|
2345
|
+
* @throws When the file is not valid JSON.
|
|
2346
|
+
*/
|
|
2347
|
+
static workspacePatternsOf(packageJsonPath) {
|
|
2348
|
+
let parsed;
|
|
2349
|
+
try {
|
|
2350
|
+
parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
2351
|
+
} catch (cause) {
|
|
2352
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
2353
|
+
throw new Error(`${packageJsonPath} could not be parsed as JSON: ${reason}`, { cause });
|
|
2354
|
+
}
|
|
2355
|
+
if (typeof parsed !== "object" || parsed === null || !("workspaces" in parsed)) return;
|
|
2356
|
+
const workspaces = parsed.workspaces;
|
|
2357
|
+
const patterns = typeof workspaces === "object" && workspaces !== null && "packages" in workspaces ? workspaces.packages : workspaces;
|
|
2358
|
+
return Array.isArray(patterns) ? patterns.filter((pattern) => typeof pattern === "string") : void 0;
|
|
2359
|
+
}
|
|
2360
|
+
/** The workspace at a root: the root package, then every directory the patterns name that has a package. */
|
|
2361
|
+
static workspaceOf(root, kind, patterns) {
|
|
2362
|
+
const includes = patterns.filter((pattern) => !pattern.trim().startsWith("!")).map((pattern) => WorkspacePackages.normalizedPattern(pattern)).filter((pattern) => pattern !== "");
|
|
2363
|
+
const excludes = patterns.filter((pattern) => pattern.trim().startsWith("!")).map((pattern) => WorkspacePackages.normalizedPattern(pattern.trim().slice(1))).filter((pattern) => pattern !== "");
|
|
2364
|
+
const members = (includes.length === 0 ? [] : globSync(includes, {
|
|
2365
|
+
cwd: root,
|
|
2366
|
+
exclude: (entry) => basename(String(entry)) === WorkspacePackages.installedDirectoryName
|
|
2367
|
+
})).map((match) => relative(root, resolve(root, match))).filter((path) => !WorkspacePackages.isExcluded(path, excludes)).sort();
|
|
2368
|
+
const workspaceMembers = [];
|
|
2369
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2370
|
+
for (const memberPath of ["", ...members]) {
|
|
2371
|
+
const directory = resolve(root, memberPath);
|
|
2372
|
+
if (!WorkspacePackages.isPackageDirectory(directory)) continue;
|
|
2373
|
+
const realDirectory = WorkspacePackages.realPathOf(directory);
|
|
2374
|
+
if (!seen.has(realDirectory)) {
|
|
2375
|
+
seen.add(realDirectory);
|
|
2376
|
+
workspaceMembers.push({
|
|
2377
|
+
directory: realDirectory,
|
|
2378
|
+
path: memberPath || "."
|
|
2379
|
+
});
|
|
1466
2380
|
}
|
|
1467
|
-
if (exitFailure) process.exitCode = 1;
|
|
1468
|
-
outro(outroParts.join(" "));
|
|
1469
|
-
return;
|
|
1470
2381
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2382
|
+
return {
|
|
2383
|
+
root,
|
|
2384
|
+
kind,
|
|
2385
|
+
members: workspaceMembers
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
/** A pattern without a leading `./` and a trailing `/`, so it matches the paths the glob returns. */
|
|
2389
|
+
static normalizedPattern(pattern) {
|
|
2390
|
+
return pattern.trim().replace(/^(?:\.\/)+/, "").replace(/\/+$/, "");
|
|
2391
|
+
}
|
|
2392
|
+
/** Whether a `!` pattern removes a member path. A pattern that ends in a globstar also removes its folder. */
|
|
2393
|
+
static isExcluded(memberPath, excludes) {
|
|
2394
|
+
return excludes.some((pattern) => matchesGlob(memberPath, pattern) || matchesGlob(`${memberPath}/`, pattern));
|
|
2395
|
+
}
|
|
2396
|
+
/** Whether a path is a directory that holds a `package.json`. */
|
|
2397
|
+
static isPackageDirectory(directory) {
|
|
2398
|
+
try {
|
|
2399
|
+
return statSync(directory).isDirectory() && existsSync(join(directory, WorkspacePackages.packageJsonFileName));
|
|
2400
|
+
} catch {
|
|
2401
|
+
return false;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
/** The real path of a directory, or the path itself when it cannot be resolved. */
|
|
2405
|
+
static realPathOf(directory) {
|
|
2406
|
+
try {
|
|
2407
|
+
return realpathSync(directory);
|
|
2408
|
+
} catch {
|
|
2409
|
+
return directory;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
//#endregion
|
|
2414
|
+
//#region src/workspace-freshness.ts
|
|
2415
|
+
/**
|
|
2416
|
+
* Judges every package of a workspace with {@link LibraryFreshness}: each package that has a Heximon manifest or
|
|
2417
|
+
* a `dist/heximon.inputs.json`. It compiles nothing and runs no other audit, so it only stats and hashes files.
|
|
2418
|
+
* A package whose check throws (a `heximon.config.ts` that fails to load) gets an `error` verdict.
|
|
2419
|
+
*/
|
|
2420
|
+
var WorkspaceFreshness = class WorkspaceFreshness {
|
|
2421
|
+
/** The fix hint of a package whose check failed. */
|
|
2422
|
+
static checkFailedHint = "Fix the error, for example in the package's heximon.config.ts, then run heximon freshness again.";
|
|
2423
|
+
/**
|
|
2424
|
+
* Find the workspace of a directory and judge each of its packages.
|
|
2425
|
+
*
|
|
2426
|
+
* @param startDirectory - Any directory inside the workspace.
|
|
2427
|
+
* @returns The verdicts.
|
|
2428
|
+
* @throws CommandFailure with {@link FreshnessDiagnosticCode.WorkspaceUnreadable} when the workspace file
|
|
2429
|
+
* cannot be read.
|
|
2430
|
+
*/
|
|
2431
|
+
static async run(startDirectory) {
|
|
2432
|
+
const workspace = WorkspaceFreshness.locate(startDirectory);
|
|
2433
|
+
const freshness = new LibraryFreshness();
|
|
2434
|
+
const packages = [];
|
|
2435
|
+
for (const member of workspace.members) {
|
|
2436
|
+
const identity = WorkspaceFreshness.identityOf(member);
|
|
2437
|
+
const recordProblem = LibraryFreshness.recordProblemOf(member.directory);
|
|
2438
|
+
if (recordProblem !== void 0) {
|
|
2439
|
+
packages.push({
|
|
2440
|
+
...identity,
|
|
2441
|
+
error: recordProblem,
|
|
2442
|
+
recordUnreadable: true
|
|
2443
|
+
});
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
try {
|
|
2447
|
+
const report = await freshness.inspect(member.directory);
|
|
2448
|
+
if (report !== null) packages.push({
|
|
2449
|
+
...identity,
|
|
2450
|
+
report
|
|
2451
|
+
});
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
packages.push({
|
|
2454
|
+
...identity,
|
|
2455
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
return {
|
|
2460
|
+
workspace,
|
|
2461
|
+
packages,
|
|
2462
|
+
skippedPackages: workspace.members.length - packages.length
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
/** Whether a verdict fails the run: a stale package, or one whose check failed. */
|
|
2466
|
+
static isFailing(verdict) {
|
|
2467
|
+
return verdict.error !== void 0 || verdict.report?.status === LibraryFreshnessStatus.Stale;
|
|
2468
|
+
}
|
|
2469
|
+
/** Whether the run exits 1. */
|
|
2470
|
+
static isFailure(result) {
|
|
2471
|
+
return result.packages.some((verdict) => WorkspaceFreshness.isFailing(verdict));
|
|
2472
|
+
}
|
|
2473
|
+
/** The status of a verdict. */
|
|
2474
|
+
static statusOf(verdict) {
|
|
2475
|
+
return verdict.report === void 0 ? FreshnessPackageStatus.Error : verdict.report.status;
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* The text line of one package: its name, version and path, its status, and for a stale package the
|
|
2479
|
+
* {@link LibraryFreshness.staleLine} that names the changed and missing inputs.
|
|
2480
|
+
*
|
|
2481
|
+
* @param verdict - The verdict.
|
|
2482
|
+
* @returns The line.
|
|
2483
|
+
*/
|
|
2484
|
+
static lineOf(verdict) {
|
|
2485
|
+
const version = verdict.version === void 0 ? "" : ` v${verdict.version}`;
|
|
2486
|
+
const label = `${verdict.name}${version} (${verdict.path})`;
|
|
2487
|
+
const report = verdict.report;
|
|
2488
|
+
if (report === void 0) return `✖ ${label} — ERROR: ${String(verdict.error)}`;
|
|
2489
|
+
switch (report.status) {
|
|
2490
|
+
case LibraryFreshnessStatus.Fresh: return `✔ ${label} — fresh`;
|
|
2491
|
+
case LibraryFreshnessStatus.Stale: return LibraryFreshness.staleLine(label, {
|
|
2492
|
+
...report,
|
|
2493
|
+
packageVersion: void 0
|
|
2494
|
+
}, { forApp: false });
|
|
2495
|
+
case LibraryFreshnessStatus.NoProvenance: return `● ${label} — no provenance: the manifest predates the provenance stamp. Repack to enable the check.`;
|
|
2496
|
+
case LibraryFreshnessStatus.NoSource: return `● ${label} — no source: none of its recorded source is on disk, so nothing was compared.`;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
/** The closing sentence of the run. */
|
|
2500
|
+
static summary(result) {
|
|
2501
|
+
const skipped = result.skippedPackages === 0 ? "" : ` ${String(result.skippedPackages)} package(s) without a record were skipped.`;
|
|
2502
|
+
if (result.packages.length === 0) return result.workspace.members.length === 0 ? `No package found from ${result.workspace.root}.` : `No workspace package has a freshness record.${skipped}`;
|
|
2503
|
+
const count = (status) => result.packages.filter((verdict) => WorkspaceFreshness.statusOf(verdict) === status).length;
|
|
2504
|
+
const parts = [`${String(count(FreshnessPackageStatus.Stale))} stale`, `${String(count(FreshnessPackageStatus.Fresh))} fresh`];
|
|
2505
|
+
const notCompared = count(FreshnessPackageStatus.NoProvenance) + count(FreshnessPackageStatus.NoSource);
|
|
2506
|
+
if (notCompared > 0) parts.push(`${String(notCompared)} not compared`);
|
|
2507
|
+
if (count(FreshnessPackageStatus.Error) > 0) parts.push(`${String(count(FreshnessPackageStatus.Error))} failed`);
|
|
2508
|
+
return `${String(result.packages.length)} package(s) with a record: ${parts.join(", ")}.${skipped}`;
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Build the `--json` document of one run.
|
|
2512
|
+
*
|
|
2513
|
+
* @param result - The verdicts.
|
|
2514
|
+
* @param staleOnly - List only the entries that fail the run.
|
|
2515
|
+
* @returns The document. Its `ok` is `false` exactly when {@link isFailure} is `true`.
|
|
2516
|
+
*/
|
|
2517
|
+
static toJson(result, staleOnly) {
|
|
2518
|
+
const packages = result.packages.map((verdict) => WorkspaceFreshness.entryOf(verdict));
|
|
2519
|
+
return {
|
|
2520
|
+
ok: !WorkspaceFreshness.isFailure(result),
|
|
2521
|
+
diagnostics: packages.flatMap((entry) => WorkspaceFreshness.diagnosticsOf(entry)),
|
|
2522
|
+
workspaceRoot: result.workspace.root,
|
|
2523
|
+
workspaceKind: result.workspace.kind,
|
|
2524
|
+
packages: staleOnly ? packages.filter((entry) => entry.failing) : packages,
|
|
2525
|
+
skippedPackages: result.skippedPackages,
|
|
2526
|
+
summary: WorkspaceFreshness.summary(result)
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
/** The workspace of a directory, with a file that cannot be read as the command's failure. */
|
|
2530
|
+
static locate(startDirectory) {
|
|
2531
|
+
try {
|
|
2532
|
+
return WorkspacePackages.locate(startDirectory);
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
throw new CommandFailure([CliDiagnostics.error(FreshnessDiagnosticCode.WorkspaceUnreadable, error instanceof Error ? error.message : String(error), { hint: "Fix the workspace file, then run heximon freshness again." })]);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
/** One JSON package entry. */
|
|
2538
|
+
static entryOf(verdict) {
|
|
2539
|
+
const location = {
|
|
2540
|
+
...verdict.version === void 0 ? {} : { version: verdict.version },
|
|
2541
|
+
path: verdict.path,
|
|
2542
|
+
directory: verdict.directory
|
|
2543
|
+
};
|
|
2544
|
+
if (verdict.report === void 0) return {
|
|
2545
|
+
name: verdict.name,
|
|
2546
|
+
status: FreshnessPackageStatus.Error,
|
|
2547
|
+
reason: String(verdict.error),
|
|
2548
|
+
...verdict.recordUnreadable === true ? {
|
|
2549
|
+
hint: `Repack it (\`vp pack\` in ${verdict.directory}) to write the record again.`,
|
|
2550
|
+
fix: DoctorJsonReport.repackCommand(verdict.directory)
|
|
2551
|
+
} : { hint: WorkspaceFreshness.checkFailedHint },
|
|
2552
|
+
failing: true,
|
|
2553
|
+
...location
|
|
2554
|
+
};
|
|
2555
|
+
const fields = DoctorJsonReport.verdictOf(verdict.report.status, `Rebuild it (\`vp pack\` in ${verdict.directory}).`, DoctorJsonReport.repackCommand(verdict.directory));
|
|
2556
|
+
return {
|
|
2557
|
+
name: verdict.name,
|
|
2558
|
+
status: verdict.report.status,
|
|
2559
|
+
reason: DoctorJsonReport.reasonOf(verdict.report),
|
|
2560
|
+
...fields.hint === void 0 ? {} : { hint: fields.hint },
|
|
2561
|
+
...fields.fix === void 0 ? {} : { fix: fields.fix },
|
|
2562
|
+
failing: fields.failing,
|
|
2563
|
+
...location,
|
|
2564
|
+
...DoctorJsonReport.freshnessDetails(verdict.report)
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
/** The error diagnostic of an entry that fails the run. */
|
|
2568
|
+
static diagnosticsOf(entry) {
|
|
2569
|
+
if (!entry.failing) return [];
|
|
2570
|
+
return [CliDiagnostics.error(entry.status === FreshnessPackageStatus.Error ? FreshnessDiagnosticCode.CheckFailed : FreshnessDiagnosticCode.StalePackage, `${entry.name} (${entry.path}): ${entry.reason}`, entry.hint === void 0 ? {} : { hint: entry.hint })];
|
|
2571
|
+
}
|
|
2572
|
+
/** The name, version and paths of a package, from its `package.json`. */
|
|
2573
|
+
static identityOf(member) {
|
|
2574
|
+
const directory = member.directory;
|
|
2575
|
+
let name;
|
|
2576
|
+
let version;
|
|
2577
|
+
try {
|
|
2578
|
+
const parsed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
2579
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2580
|
+
const fields = parsed;
|
|
2581
|
+
name = typeof fields.name === "string" ? fields.name : void 0;
|
|
2582
|
+
version = typeof fields.version === "string" ? fields.version : void 0;
|
|
2583
|
+
}
|
|
2584
|
+
} catch {}
|
|
2585
|
+
return {
|
|
2586
|
+
name: name ?? basename(directory),
|
|
2587
|
+
...version === void 0 ? {} : { version },
|
|
2588
|
+
path: member.path,
|
|
2589
|
+
directory
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
//#endregion
|
|
2594
|
+
//#region src/freshness-command.ts
|
|
2595
|
+
/** The text renderer behind {@link freshnessCommand}: one clack line per package, then the summary. */
|
|
2596
|
+
var FreshnessReport = class {
|
|
2597
|
+
/**
|
|
2598
|
+
* Print the report of one run.
|
|
2599
|
+
*
|
|
2600
|
+
* @param result - The verdicts.
|
|
2601
|
+
* @param staleOnly - Print only the packages that fail the run.
|
|
2602
|
+
*/
|
|
2603
|
+
static render(result, staleOnly) {
|
|
2604
|
+
for (const verdict of result.packages) {
|
|
2605
|
+
const failing = WorkspaceFreshness.isFailing(verdict);
|
|
2606
|
+
if (staleOnly && !failing) continue;
|
|
2607
|
+
const line = WorkspaceFreshness.lineOf(verdict);
|
|
2608
|
+
if (failing) log.error(line);
|
|
2609
|
+
else if (verdict.report?.status === LibraryFreshnessStatus.Fresh) log.success(line);
|
|
2610
|
+
else log.info(line);
|
|
2611
|
+
}
|
|
2612
|
+
outro(WorkspaceFreshness.summary(result));
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2615
|
+
/**
|
|
2616
|
+
* `heximon freshness` — lists every package of the workspace that has a freshness record, with its status. It
|
|
2617
|
+
* finds the workspace root from any directory inside it, judges each package with the rule of `heximon doctor`
|
|
2618
|
+
* ({@link LibraryFreshness}), and exits 1 when a package is stale or its check failed. It compiles nothing and
|
|
2619
|
+
* runs no other audit.
|
|
2620
|
+
*/
|
|
2621
|
+
const freshnessCommand = defineCommand({
|
|
2622
|
+
meta: {
|
|
2623
|
+
name: "freshness",
|
|
2624
|
+
description: "List every workspace package with a freshness record (a Heximon manifest or dist/heximon.inputs.json), its status, and the input that made it stale. Works from any directory of the workspace. Exits 1 when a package is stale."
|
|
2625
|
+
},
|
|
2626
|
+
args: {
|
|
2627
|
+
cwd: {
|
|
2628
|
+
type: "string",
|
|
2629
|
+
description: "A directory inside the workspace; the command walks up to the workspace root.",
|
|
2630
|
+
default: "."
|
|
2631
|
+
},
|
|
2632
|
+
json: CommandOutput.jsonFlag,
|
|
2633
|
+
stale: {
|
|
2634
|
+
type: "boolean",
|
|
2635
|
+
description: "List only the packages that fail the run: the stale ones, and the ones whose check failed.",
|
|
2636
|
+
default: false
|
|
1478
2637
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
2638
|
+
},
|
|
2639
|
+
async run({ args }) {
|
|
2640
|
+
const commandOutput = CommandOutput.resolve("freshness", args);
|
|
2641
|
+
await commandOutput.run(async () => {
|
|
2642
|
+
const startDirectory = ProjectRoot.resolve(args.cwd);
|
|
2643
|
+
const result = await WorkspaceFreshness.run(startDirectory);
|
|
2644
|
+
const staleOnly = args.stale === true;
|
|
2645
|
+
if (WorkspaceFreshness.isFailure(result)) process.exitCode = 1;
|
|
2646
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkspaceFreshness.toJson(result, staleOnly));
|
|
2647
|
+
else {
|
|
2648
|
+
intro(`heximon freshness — ${basename(result.workspace.root)}`);
|
|
2649
|
+
FreshnessReport.render(result, staleOnly);
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
1484
2652
|
}
|
|
1485
2653
|
});
|
|
1486
2654
|
//#endregion
|
|
2655
|
+
//#region src/graph-json-document.ts
|
|
2656
|
+
/** The stable codes of the failures `heximon graph` raises itself. */
|
|
2657
|
+
const GraphDiagnosticCode = {
|
|
2658
|
+
/** The compile threw before the command could read a module graph. */
|
|
2659
|
+
ReadFailed: "graph.read-failed",
|
|
2660
|
+
/** The `--out` file could not be written. */
|
|
2661
|
+
OutWriteFailed: "graph.out-write-failed"
|
|
2662
|
+
};
|
|
2663
|
+
//#endregion
|
|
1487
2664
|
//#region src/json-output-target.ts
|
|
1488
2665
|
/**
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
* a payload and never branch on the flags themselves.
|
|
2666
|
+
* The `--out` file of an inspection command (`routes`, `graph`): it gets the same document the command prints
|
|
2667
|
+
* on stdout under `--json`. {@link CommandOutput} owns stdout; this class owns only the file.
|
|
1492
2668
|
*/
|
|
1493
2669
|
var JsonOutputTarget = class JsonOutputTarget {
|
|
1494
2670
|
filePath;
|
|
1495
|
-
toStdout;
|
|
1496
2671
|
/**
|
|
1497
2672
|
* @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
2673
|
*/
|
|
1500
|
-
constructor(filePath
|
|
2674
|
+
constructor(filePath) {
|
|
1501
2675
|
this.filePath = filePath;
|
|
1502
|
-
this.toStdout = toStdout;
|
|
1503
2676
|
}
|
|
1504
2677
|
/**
|
|
1505
|
-
* Resolve the
|
|
1506
|
-
*
|
|
2678
|
+
* Resolve the file. A relative path resolves against the current directory (where the command was typed),
|
|
2679
|
+
* not the `--cwd` project root.
|
|
1507
2680
|
*
|
|
1508
2681
|
* @param outOption - The raw `--out` argument, when the caller passed one.
|
|
1509
|
-
* @
|
|
2682
|
+
* @returns The target; a blank or missing `--out` requests no file.
|
|
1510
2683
|
*/
|
|
1511
|
-
static resolve(outOption
|
|
2684
|
+
static resolve(outOption) {
|
|
1512
2685
|
const requested = outOption?.trim();
|
|
1513
|
-
|
|
1514
|
-
return new JsonOutputTarget(filePath, filePath === void 0 && asJson);
|
|
2686
|
+
return new JsonOutputTarget(requested === void 0 || requested === "" ? void 0 : resolve(process.cwd(), requested));
|
|
1515
2687
|
}
|
|
1516
|
-
/** Whether
|
|
2688
|
+
/** Whether a file was requested — the caller's cue to report where it landed. */
|
|
1517
2689
|
isFile() {
|
|
1518
2690
|
return this.filePath !== void 0;
|
|
1519
2691
|
}
|
|
1520
2692
|
/**
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
* single line `jq` and pipes expect.
|
|
2693
|
+
* Run the body of a command. A failure it throws is written to the file as a `{ ok: false, diagnostics }`
|
|
2694
|
+
* document before it propagates, so the file never keeps the document of an earlier run.
|
|
1524
2695
|
*
|
|
1525
|
-
* @
|
|
1526
|
-
* cue to exit non-zero. A target that emits nowhere reports success.
|
|
2696
|
+
* @param body - The work of the command.
|
|
1527
2697
|
*/
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
2698
|
+
async run(body) {
|
|
2699
|
+
try {
|
|
2700
|
+
await body();
|
|
2701
|
+
} catch (error) {
|
|
2702
|
+
this.write({
|
|
2703
|
+
ok: false,
|
|
2704
|
+
diagnostics: error instanceof CommandFailure ? error.diagnostics : [CliDiagnostics.error(CliDiagnosticCode.UnexpectedError, error instanceof Error ? error.message : String(error))]
|
|
2705
|
+
});
|
|
2706
|
+
throw error;
|
|
1532
2707
|
}
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Write the document to the file, and record it as the stdout document of the run. When the write fails, the
|
|
2711
|
+
* recorded document gets `ok: false` and one more error diagnostic that names the file.
|
|
2712
|
+
*
|
|
2713
|
+
* @param document - The document of the run.
|
|
2714
|
+
* @param commandOutput - The output owner of the run.
|
|
2715
|
+
* @param writeFailureCode - The diagnostic code of a failed write.
|
|
2716
|
+
* @returns Whether the file was written; `true` when no file was requested.
|
|
2717
|
+
*/
|
|
2718
|
+
publish(document, commandOutput, writeFailureCode) {
|
|
2719
|
+
const failure = this.write(document);
|
|
2720
|
+
commandOutput.emit(failure === void 0 ? document : {
|
|
2721
|
+
...document,
|
|
2722
|
+
ok: false,
|
|
2723
|
+
diagnostics: [...document.diagnostics, CliDiagnostics.error(writeFailureCode, `Could not write ${this.filePath}: ${failure}`, { option: "--out" })]
|
|
2724
|
+
});
|
|
2725
|
+
return failure === void 0;
|
|
2726
|
+
}
|
|
2727
|
+
/**
|
|
2728
|
+
* Write indented JSON to the file, with its parent directory created, so a run can target a CI artifact
|
|
2729
|
+
* directory that does not exist yet and consecutive runs diff line by line.
|
|
2730
|
+
*
|
|
2731
|
+
* @returns The reason of a failed write (also written to stderr), or `undefined` when the write worked or
|
|
2732
|
+
* no file was requested.
|
|
2733
|
+
*/
|
|
2734
|
+
write(document) {
|
|
2735
|
+
if (this.filePath === void 0) return;
|
|
1533
2736
|
try {
|
|
1534
2737
|
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
1535
|
-
writeFileSync(this.filePath, `${JSON.stringify(
|
|
1536
|
-
return
|
|
2738
|
+
writeFileSync(this.filePath, `${JSON.stringify(document, null, 2)}\n`);
|
|
2739
|
+
return;
|
|
1537
2740
|
} catch (error) {
|
|
1538
2741
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1539
2742
|
process.stderr.write(`Could not write ${this.filePath} — ${reason}\n`);
|
|
1540
|
-
return
|
|
2743
|
+
return reason;
|
|
1541
2744
|
}
|
|
1542
2745
|
}
|
|
1543
2746
|
};
|
|
@@ -1572,16 +2775,13 @@ var ModuleGraphReport = class ModuleGraphReport {
|
|
|
1572
2775
|
return lines.join("\n");
|
|
1573
2776
|
}
|
|
1574
2777
|
/**
|
|
1575
|
-
* The
|
|
2778
|
+
* The graph fields of the `--json` document: the raw module view plus (when present) the boundary summaries.
|
|
1576
2779
|
*
|
|
1577
2780
|
* @param view - The compile's module-graph projection.
|
|
1578
2781
|
* @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
2782
|
*/
|
|
1582
|
-
static toJson(view, boundaries = []
|
|
2783
|
+
static toJson(view, boundaries = []) {
|
|
1583
2784
|
return {
|
|
1584
|
-
ok,
|
|
1585
2785
|
modules: view.modules,
|
|
1586
2786
|
...boundaries.length > 0 ? { boundaries } : {}
|
|
1587
2787
|
};
|
|
@@ -1647,11 +2847,7 @@ const graphCommand = defineCommand({
|
|
|
1647
2847
|
description: "The heximon.config.ts `$env` name to resolve (matches `vp dev`). Default development.",
|
|
1648
2848
|
default: "development"
|
|
1649
2849
|
},
|
|
1650
|
-
json:
|
|
1651
|
-
type: "boolean",
|
|
1652
|
-
description: "Emit the graph as JSON on stdout instead of a tree.",
|
|
1653
|
-
default: false
|
|
1654
|
-
},
|
|
2850
|
+
json: CommandOutput.jsonFlag,
|
|
1655
2851
|
boundaries: {
|
|
1656
2852
|
type: "boolean",
|
|
1657
2853
|
description: "Also list the cross-package boundary nodes the app composes.",
|
|
@@ -1659,7 +2855,7 @@ const graphCommand = defineCommand({
|
|
|
1659
2855
|
},
|
|
1660
2856
|
out: {
|
|
1661
2857
|
type: "string",
|
|
1662
|
-
description: "
|
|
2858
|
+
description: "Also write the JSON document to this file, creating missing parent directories — the CI-artifact target."
|
|
1663
2859
|
},
|
|
1664
2860
|
src: {
|
|
1665
2861
|
type: "string",
|
|
@@ -1669,73 +2865,85 @@ const graphCommand = defineCommand({
|
|
|
1669
2865
|
app: CompileModeResolver.flags.app
|
|
1670
2866
|
},
|
|
1671
2867
|
async run({ args }) {
|
|
1672
|
-
const
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2868
|
+
const commandOutput = CommandOutput.resolve("graph", args);
|
|
2869
|
+
const outFile = JsonOutputTarget.resolve(args.out);
|
|
2870
|
+
await commandOutput.run(async () => {
|
|
2871
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
2872
|
+
await outFile.run(async () => {
|
|
2873
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
2874
|
+
const resolved = CompileModeResolver.resolve(rootDir, args, sourceDirectories);
|
|
2875
|
+
if (!commandOutput.isJson()) intro(`heximon graph — ${basename(rootDir)} · ${CompileModeResolver.stamp(resolved)}`);
|
|
2876
|
+
let output;
|
|
2877
|
+
try {
|
|
2878
|
+
const session = await ProjectCompileSession.open({
|
|
2879
|
+
rootDir,
|
|
2880
|
+
environment: args.env,
|
|
2881
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
2882
|
+
});
|
|
2883
|
+
output = resolved.mode === CompileMode.Library ? session.compileLibrary() : session.compile();
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
GraphCommand.reportFailure(error instanceof CommandFailure ? error.diagnostics : [CliDiagnostics.error(GraphDiagnosticCode.ReadFailed, error instanceof Error ? error.message : String(error))], commandOutput, outFile);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
GraphCommand.report(output, {
|
|
2889
|
+
commandOutput,
|
|
2890
|
+
outFile,
|
|
2891
|
+
withBoundaries: args.boundaries,
|
|
2892
|
+
libraryMode: resolved.mode === CompileMode.Library
|
|
2893
|
+
});
|
|
1686
2894
|
});
|
|
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;
|
|
2895
|
+
});
|
|
1699
2896
|
}
|
|
1700
2897
|
});
|
|
1701
|
-
/** Output helpers for {@link graphCommand}: the module tree / JSON
|
|
2898
|
+
/** Output helpers for {@link graphCommand}: the module tree / JSON document and the boundary projection. */
|
|
1702
2899
|
var GraphCommand = class GraphCommand {
|
|
1703
2900
|
/**
|
|
1704
|
-
*
|
|
2901
|
+
* Report the resolved graph: the JSON document to stdout under `--json` and to the `--out` file, and a
|
|
2902
|
+
* module-nested tree unless `--json`. A failed compile or a failed `--out` write sets exit status 1.
|
|
1705
2903
|
*
|
|
1706
|
-
* @
|
|
2904
|
+
* @param output - The compile facts.
|
|
2905
|
+
* @param options - The output owners, and what to print.
|
|
1707
2906
|
*/
|
|
1708
2907
|
static report(output, options) {
|
|
1709
2908
|
const view = output.moduleGraph ?? { modules: [] };
|
|
1710
2909
|
const boundaries = options.withBoundaries ? GraphCommand.boundarySummaries(output) : [];
|
|
1711
2910
|
const errors = output.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).length;
|
|
1712
|
-
const
|
|
1713
|
-
|
|
2911
|
+
const document = {
|
|
2912
|
+
ok: errors === 0,
|
|
2913
|
+
diagnostics: CliDiagnostics.fromCompiler(output.diagnostics),
|
|
2914
|
+
...ModuleGraphReport.toJson(view, boundaries),
|
|
1714
2915
|
errors
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
if (written
|
|
2916
|
+
};
|
|
2917
|
+
const written = options.outFile.publish(document, options.commandOutput, GraphDiagnosticCode.OutWriteFailed);
|
|
2918
|
+
if (!written || errors > 0) process.exitCode = 1;
|
|
2919
|
+
if (options.commandOutput.isJson()) return;
|
|
2920
|
+
if (written && options.outFile.isFile()) log.success(`Graph JSON written to ${options.outFile.filePath}.`);
|
|
1718
2921
|
log.message(ModuleGraphReport.renderTree(view, boundaries));
|
|
1719
2922
|
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
2923
|
const moduleCount = view.modules.length;
|
|
1721
2924
|
const summary = `${moduleCount} module${moduleCount === 1 ? "" : "s"}`;
|
|
1722
2925
|
if (errors > 0) outro(`${summary} — ${errors} compile error${errors === 1 ? "" : "s"}; the graph may be incomplete.`);
|
|
1723
2926
|
else outro(`${summary}.`);
|
|
1724
|
-
return written && errors === 0;
|
|
1725
2927
|
}
|
|
1726
2928
|
/**
|
|
1727
|
-
* Report a config-load / compile failure:
|
|
1728
|
-
* outro unless `--json`. The
|
|
1729
|
-
* artifact rather than leaving a stale success.
|
|
2929
|
+
* Report a config-load / compile failure: a {@link CliFailureDocument} to stdout under `--json` and to the
|
|
2930
|
+
* `--out` file, and a clack error + outro unless `--json`. The file gets the failure too, so a new run
|
|
2931
|
+
* replaces the previous artifact rather than leaving a stale success. Sets exit status 1.
|
|
2932
|
+
*
|
|
2933
|
+
* @param diagnostics - The diagnostics of the failure.
|
|
2934
|
+
* @param commandOutput - The stdout owner of the run.
|
|
2935
|
+
* @param outFile - The `--out` file.
|
|
1730
2936
|
*/
|
|
1731
|
-
static reportFailure(
|
|
1732
|
-
const
|
|
1733
|
-
jsonOutput.emit({
|
|
2937
|
+
static reportFailure(diagnostics, commandOutput, outFile) {
|
|
2938
|
+
const document = {
|
|
1734
2939
|
ok: false,
|
|
1735
|
-
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
|
|
2940
|
+
diagnostics
|
|
2941
|
+
};
|
|
2942
|
+
outFile.publish(document, commandOutput, GraphDiagnosticCode.OutWriteFailed);
|
|
2943
|
+
process.exitCode = 1;
|
|
2944
|
+
if (commandOutput.isJson()) return;
|
|
2945
|
+
const messages = diagnostics.map((diagnostic) => diagnostic.message);
|
|
2946
|
+
log.error(`Could not read the app's module graph:\n${messages.join("\n")}`);
|
|
1739
2947
|
outro("No graph to show.");
|
|
1740
2948
|
}
|
|
1741
2949
|
/** Project the compile's cross-package boundary nodes into display summaries. */
|
|
@@ -1749,12 +2957,45 @@ var GraphCommand = class GraphCommand {
|
|
|
1749
2957
|
}
|
|
1750
2958
|
};
|
|
1751
2959
|
//#endregion
|
|
2960
|
+
//#region src/migrations-json-document.ts
|
|
2961
|
+
/** The stable codes of the failures and warnings `heximon migrations` raises itself. */
|
|
2962
|
+
const MigrationsDiagnosticCode = {
|
|
2963
|
+
/** No drizzle-kit config was detected, and `--config` was not given. */
|
|
2964
|
+
NoConfig: "migrations.no-config",
|
|
2965
|
+
/** A candidate config file could not be loaded. A warning beside {@link NoConfig}; `file` names it. */
|
|
2966
|
+
ConfigLoadFailed: "migrations.config-load-failed",
|
|
2967
|
+
/** The `--env-file` cannot be read. */
|
|
2968
|
+
EnvFileUnreadable: "migrations.env-file-unreadable",
|
|
2969
|
+
/** `drizzle-kit` exited with a non-zero status for one database. */
|
|
2970
|
+
DrizzleKitFailed: "migrations.drizzle-kit-failed",
|
|
2971
|
+
/** `drizzle-kit` could not start, for example because it is not installed. The run stops there. */
|
|
2972
|
+
DrizzleKitNotStarted: "migrations.drizzle-kit-not-started",
|
|
2973
|
+
/** A Cloudflare D1 database was not migrated: `wrangler d1 migrations apply` applies it. A warning. */
|
|
2974
|
+
D1ApplySkipped: "migrations.d1-apply-skipped"
|
|
2975
|
+
};
|
|
2976
|
+
/** What happened to one database in a `generate`, `migrate` or `push` run. */
|
|
2977
|
+
const MigrationsStepStatus = {
|
|
2978
|
+
/** `drizzle-kit` exited 0. */
|
|
2979
|
+
Succeeded: "succeeded",
|
|
2980
|
+
/** `drizzle-kit` exited with a non-zero status. */
|
|
2981
|
+
Failed: "failed",
|
|
2982
|
+
/** `drizzle-kit` could not start. The run stopped, and the databases after this one have no entry. */
|
|
2983
|
+
NotStarted: "not-started",
|
|
2984
|
+
/** The command did not run `drizzle-kit` for this database; {@link MigrationsStepEntry.reason} says why. */
|
|
2985
|
+
Skipped: "skipped"
|
|
2986
|
+
};
|
|
2987
|
+
//#endregion
|
|
1752
2988
|
//#region src/migrations-command.ts
|
|
1753
2989
|
/**
|
|
1754
|
-
* The plumbing every `heximon migrations` subcommand shares: the `.env` + detection prologue,
|
|
1755
|
-
* selection, one captured `drizzle-kit` invocation, and the
|
|
2990
|
+
* The plumbing every `heximon migrations` subcommand shares: the output mode, the `.env` + detection prologue,
|
|
2991
|
+
* the config selection, one captured `drizzle-kit` invocation, and the diagnostics of the `--json` document.
|
|
1756
2992
|
*/
|
|
1757
2993
|
var MigrationsRunner = class MigrationsRunner {
|
|
2994
|
+
commandOutput;
|
|
2995
|
+
rootDir;
|
|
2996
|
+
detection;
|
|
2997
|
+
args;
|
|
2998
|
+
mask;
|
|
1758
2999
|
/** The flags every `heximon migrations` subcommand accepts. */
|
|
1759
3000
|
static baseArgs = {
|
|
1760
3001
|
cwd: {
|
|
@@ -1774,42 +3015,80 @@ var MigrationsRunner = class MigrationsRunner {
|
|
|
1774
3015
|
config: {
|
|
1775
3016
|
type: "string",
|
|
1776
3017
|
description: "Target one drizzle-kit config file, bypassing auto-detection."
|
|
1777
|
-
}
|
|
3018
|
+
},
|
|
3019
|
+
json: CommandOutput.jsonFlag
|
|
1778
3020
|
};
|
|
1779
3021
|
/** The `select` sentinel for "run every detected database". */
|
|
1780
3022
|
static allDatabasesValue = "__all__";
|
|
3023
|
+
/** The message of the "no drizzle-kit config" halt. */
|
|
3024
|
+
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>.";
|
|
3025
|
+
/** The password of a URL with userinfo (`scheme://user:password@host`), as the `secret` group. */
|
|
3026
|
+
static urlPassword = /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]*:(?<secret>[^\s@/]+)@/dgiu;
|
|
3027
|
+
/** The value of a `password` query parameter (`?password=…`, `&password=…`), as the `secret` group. */
|
|
3028
|
+
static queryPassword = /[?&]password=(?<secret>[^&#\s]+)/dgiu;
|
|
3029
|
+
/** Every diagnostic of the run, in the order the run raised them. */
|
|
3030
|
+
diagnostics = [];
|
|
1781
3031
|
/**
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
3032
|
+
* @param mask - The credentials of the run: each `--env-file` value of at least
|
|
3033
|
+
* {@link SecretMask.minimumFragmentLength} characters, and the `dbCredentials` secrets of each config at any
|
|
3034
|
+
* length. drizzle-kit can echo them, and its output reaches the terminal and the `--json` document.
|
|
1785
3035
|
*/
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
3036
|
+
constructor(commandOutput, rootDir, detection, args, mask) {
|
|
3037
|
+
this.commandOutput = commandOutput;
|
|
3038
|
+
this.rootDir = rootDir;
|
|
3039
|
+
this.detection = detection;
|
|
3040
|
+
this.args = args;
|
|
3041
|
+
this.mask = mask;
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* The options of one `drizzle-kit` call. They carry no mask: {@link redact} masks the original captured output
|
|
3045
|
+
* with the registered secrets and the URL rules together.
|
|
3046
|
+
*/
|
|
3047
|
+
drizzleKitOptions() {
|
|
3048
|
+
return { rootDir: this.rootDir };
|
|
3049
|
+
}
|
|
3050
|
+
/**
|
|
3051
|
+
* Run one subcommand: resolve the output mode, print the intro, load the `.env` cascade, detect the
|
|
3052
|
+
* configs, then run the body.
|
|
3053
|
+
*
|
|
3054
|
+
* @param subcommand - The subcommand verb (`generate`, `migrate`, …).
|
|
3055
|
+
* @param args - The parsed arguments.
|
|
3056
|
+
* @param body - The work of the subcommand. It records its `--json` document on the runner's output.
|
|
3057
|
+
*/
|
|
3058
|
+
static async execute(subcommand, args, body) {
|
|
3059
|
+
const commandOutput = CommandOutput.resolve(`migrations ${subcommand}`, args);
|
|
3060
|
+
await commandOutput.run(async () => {
|
|
3061
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
3062
|
+
await MigrationsRunner.guard(commandOutput, async () => {
|
|
3063
|
+
intro(`heximon migrations ${subcommand}`);
|
|
3064
|
+
await body(await MigrationsRunner.prepare(commandOutput, args, rootDir));
|
|
3065
|
+
});
|
|
3066
|
+
});
|
|
1794
3067
|
}
|
|
1795
3068
|
/**
|
|
1796
3069
|
* Choose which detected config(s) to act on: an explicit `--config` wins; a single detected config is used
|
|
1797
3070
|
* 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
|
-
*
|
|
3071
|
+
* is named out loud because that is several databases the operator never chose. Under `--json` several
|
|
3072
|
+
* configs fail with {@link CliDiagnosticCode.OptionRequired}, because `--json` never chooses for the caller.
|
|
3073
|
+
* Reports a text-mode failure itself (no config / cancelled) and returns `undefined` so the caller just
|
|
3074
|
+
* returns.
|
|
1800
3075
|
*/
|
|
1801
|
-
|
|
1802
|
-
if (args.config !== void 0) {
|
|
1803
|
-
const path = resolve(rootDir, args.config);
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
3076
|
+
async selectConfigs() {
|
|
3077
|
+
if (this.args.config !== void 0) {
|
|
3078
|
+
const path = resolve(this.rootDir, this.args.config);
|
|
3079
|
+
const classified = await DatabaseConfigDetector.classifyFile(this.rootDir, path);
|
|
3080
|
+
if (classified !== null) MigrationsRunner.registerCredentials(this.mask, classified);
|
|
3081
|
+
return [classified ?? MigrationsRunner.syntheticConfig(path)];
|
|
3082
|
+
}
|
|
3083
|
+
const { configs } = this.detection;
|
|
1807
3084
|
if (configs.length === 0) {
|
|
1808
|
-
MigrationsRunner.
|
|
3085
|
+
if (this.commandOutput.isJson()) throw new CommandFailure(MigrationsRunner.noConfigDiagnostics(this.detection));
|
|
3086
|
+
MigrationsRunner.reportNoConfig(this.detection);
|
|
1809
3087
|
return;
|
|
1810
3088
|
}
|
|
1811
3089
|
if (configs.length === 1) return configs;
|
|
1812
|
-
if (
|
|
3090
|
+
if (this.commandOutput.isJson()) throw new CommandFailure([this.selectionRefusal(configs)]);
|
|
3091
|
+
if (!this.commandOutput.isInteractive()) {
|
|
1813
3092
|
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
3093
|
return configs;
|
|
1815
3094
|
}
|
|
@@ -1832,34 +3111,158 @@ var MigrationsRunner = class MigrationsRunner {
|
|
|
1832
3111
|
return configs.filter((config) => config.path === choice);
|
|
1833
3112
|
}
|
|
1834
3113
|
/**
|
|
1835
|
-
* Run one captured `drizzle-kit` invocation behind a spinner, relaying its output.
|
|
3114
|
+
* Run one captured `drizzle-kit` invocation behind a spinner, relaying its output. A failure also records
|
|
3115
|
+
* an error diagnostic.
|
|
1836
3116
|
*
|
|
1837
|
-
* @
|
|
3117
|
+
* @param config - The database the invocation acts on.
|
|
3118
|
+
* @param label - The progress label, for example `Applying migrations for database.config.ts`.
|
|
3119
|
+
* @param invoke - Starts the invocation.
|
|
3120
|
+
* @returns The entry of the database. A `not-started` status means the caller stops its loop.
|
|
1838
3121
|
*/
|
|
1839
|
-
|
|
1840
|
-
const progress = CliSpinner.begin(
|
|
3122
|
+
async runStep(config, label, invoke) {
|
|
3123
|
+
const progress = CliSpinner.begin(this.commandOutput.isInteractive(), `${label}…`);
|
|
1841
3124
|
let result;
|
|
1842
3125
|
try {
|
|
1843
3126
|
result = await invoke();
|
|
1844
3127
|
} catch (error) {
|
|
3128
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1845
3129
|
progress?.stop(`${label} — failed to start.`);
|
|
1846
|
-
|
|
3130
|
+
log.error(reason);
|
|
3131
|
+
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." }));
|
|
3132
|
+
return {
|
|
3133
|
+
...this.viewOf(config),
|
|
3134
|
+
status: MigrationsStepStatus.NotStarted,
|
|
3135
|
+
reason
|
|
3136
|
+
};
|
|
1847
3137
|
}
|
|
1848
|
-
const
|
|
3138
|
+
const stdout = this.redact(result.stdout);
|
|
3139
|
+
const stderr = this.redact(result.stderr);
|
|
3140
|
+
const output = [stdout.trim(), stderr.trim()].filter((part) => part.length > 0);
|
|
3141
|
+
const drizzleKit = {
|
|
3142
|
+
exitCode: result.exitCode,
|
|
3143
|
+
stdout,
|
|
3144
|
+
stderr
|
|
3145
|
+
};
|
|
1849
3146
|
if (result.exitCode === 0) {
|
|
1850
3147
|
progress?.stop(`${label} — done.`);
|
|
1851
3148
|
if (output.length > 0) log.message(output.join("\n"));
|
|
1852
|
-
return
|
|
3149
|
+
return {
|
|
3150
|
+
...this.viewOf(config),
|
|
3151
|
+
status: MigrationsStepStatus.Succeeded,
|
|
3152
|
+
drizzleKit
|
|
3153
|
+
};
|
|
1853
3154
|
}
|
|
1854
3155
|
progress?.stop(`${label} — failed (exit ${result.exitCode}).`);
|
|
1855
3156
|
if (output.length > 0) log.error(output.join("\n"));
|
|
1856
|
-
|
|
3157
|
+
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." }));
|
|
3158
|
+
return {
|
|
3159
|
+
...this.viewOf(config),
|
|
3160
|
+
status: MigrationsStepStatus.Failed,
|
|
3161
|
+
drizzleKit
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Record a database that `migrate` does not apply.
|
|
3166
|
+
*
|
|
3167
|
+
* @param config - The skipped database.
|
|
3168
|
+
* @param reason - Why it is skipped, and what applies it.
|
|
3169
|
+
* @param warning - A warning diagnostic to record beside the entry, when the operator must act.
|
|
3170
|
+
* @returns The entry of the database.
|
|
3171
|
+
*/
|
|
3172
|
+
skip(config, reason, warning) {
|
|
3173
|
+
if (warning !== void 0) this.diagnostics.push(warning);
|
|
3174
|
+
return {
|
|
3175
|
+
...this.viewOf(config),
|
|
3176
|
+
status: MigrationsStepStatus.Skipped,
|
|
3177
|
+
reason
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
/** The `.sql` files in the migrations folder of a config, relative to that folder, sorted. */
|
|
3181
|
+
async sqlFilesOf(config) {
|
|
3182
|
+
return (await MigrationInspector.collectSqlFiles(resolve(this.rootDir, MigrationsRunner.migrationsDirectoryOf(config)))).map((file) => file.relativePath);
|
|
3183
|
+
}
|
|
3184
|
+
/** The JSON view of one config. */
|
|
3185
|
+
viewOf(config) {
|
|
3186
|
+
return {
|
|
3187
|
+
name: config.name,
|
|
3188
|
+
configPath: config.path,
|
|
3189
|
+
dialect: config.dialect,
|
|
3190
|
+
...config.driver === void 0 ? {} : { driver: config.driver },
|
|
3191
|
+
applyMode: config.applyMode,
|
|
3192
|
+
migrationsDirectory: MigrationsRunner.migrationsDirectoryOf(config)
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
/** Whether the run recorded no error diagnostic. */
|
|
3196
|
+
isSuccessful() {
|
|
3197
|
+
return !this.diagnostics.some((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* The `--json` document of the run.
|
|
3201
|
+
*
|
|
3202
|
+
* @param databases - One entry for each database the run acted on.
|
|
3203
|
+
* @returns The document, with `ok` from the recorded diagnostics.
|
|
3204
|
+
*/
|
|
3205
|
+
document(databases) {
|
|
3206
|
+
return {
|
|
3207
|
+
ok: this.isSuccessful(),
|
|
3208
|
+
diagnostics: [...this.diagnostics],
|
|
3209
|
+
rootDir: this.rootDir,
|
|
3210
|
+
databases
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
/** The migrations folder of a config: its `out`, else the drizzle-kit default. */
|
|
3214
|
+
static migrationsDirectoryOf(config) {
|
|
3215
|
+
return config.out ?? DatabaseConfigDetector.defaultOutDirectory;
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Load the `.env` cascade into `process.env` (so the spawned `drizzle-kit` inherits `DATABASE_URL`), and
|
|
3219
|
+
* detect the app's drizzle-kit config file(s). An explicit `--env-file` overlays the cascade.
|
|
3220
|
+
*
|
|
3221
|
+
* @param rootDir - The project root {@link ProjectRoot.resolve} accepted.
|
|
3222
|
+
*/
|
|
3223
|
+
static async prepare(commandOutput, args, rootDir) {
|
|
3224
|
+
const overlay = args["env-file"] === void 0 ? void 0 : await MigrationsRunner.readEnvFile(resolve(rootDir, args["env-file"]));
|
|
3225
|
+
DotenvCascade.loadInto(rootDir, args.env, overlay);
|
|
3226
|
+
const mask = new SecretMask();
|
|
3227
|
+
const detection = await DatabaseConfigDetector.detect(rootDir);
|
|
3228
|
+
for (const value of overlay?.values() ?? []) if (value.length >= SecretMask.minimumFragmentLength) mask.register(value);
|
|
3229
|
+
for (const config of detection.configs) MigrationsRunner.registerCredentials(mask, config);
|
|
3230
|
+
return new MigrationsRunner(commandOutput, rootDir, detection, args, mask);
|
|
3231
|
+
}
|
|
3232
|
+
/** Register the `dbCredentials` secrets of one config, and the password inside each URL among them. */
|
|
3233
|
+
static registerCredentials(mask, config) {
|
|
3234
|
+
for (const credential of config.credentials ?? []) {
|
|
3235
|
+
mask.register(credential);
|
|
3236
|
+
for (const match of credential.matchAll(MigrationsRunner.urlPassword)) mask.register(match.groups?.["secret"] ?? "");
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Hide the registered secrets, the password of any URL with userinfo, and any `password` query parameter in
|
|
3241
|
+
* captured output. Every range is found on the original text and replaced in one step, so no rule changes the
|
|
3242
|
+
* text another rule reads.
|
|
3243
|
+
*/
|
|
3244
|
+
redact(text) {
|
|
3245
|
+
return this.mask.redactWithPatterns(text, [MigrationsRunner.urlPassword, MigrationsRunner.queryPassword]);
|
|
3246
|
+
}
|
|
3247
|
+
/** Read the `--env-file`; an unreadable file is an expected failure with its own code. */
|
|
3248
|
+
static async readEnvFile(path) {
|
|
3249
|
+
try {
|
|
3250
|
+
return await EnvFileReader.read(path);
|
|
3251
|
+
} catch (error) {
|
|
3252
|
+
if (error instanceof EnvFileUnreadableError) throw CommandFailure.of(MigrationsDiagnosticCode.EnvFileUnreadable, error.message);
|
|
3253
|
+
throw error;
|
|
3254
|
+
}
|
|
1857
3255
|
}
|
|
1858
3256
|
/**
|
|
1859
3257
|
* 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.
|
|
3258
|
+
* non-zero exit rather than an unhandled stack trace. Under `--json` the error reaches
|
|
3259
|
+
* {@link CommandOutput.run}, which prints it as the failure document.
|
|
1861
3260
|
*/
|
|
1862
|
-
static async guard(body) {
|
|
3261
|
+
static async guard(commandOutput, body) {
|
|
3262
|
+
if (commandOutput.isJson()) {
|
|
3263
|
+
await body();
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
1863
3266
|
try {
|
|
1864
3267
|
await body();
|
|
1865
3268
|
} catch (error) {
|
|
@@ -1867,6 +3270,14 @@ var MigrationsRunner = class MigrationsRunner {
|
|
|
1867
3270
|
cancel(error instanceof Error ? error.message : String(error));
|
|
1868
3271
|
}
|
|
1869
3272
|
}
|
|
3273
|
+
/** The `--json` refusal of the database `select`, naming every choice. */
|
|
3274
|
+
selectionRefusal(configs) {
|
|
3275
|
+
const paths = configs.map((config) => relative(this.rootDir, config.path));
|
|
3276
|
+
return CliDiagnostics.error(CliDiagnosticCode.OptionRequired, `${configs.length} databases were detected (${paths.join(", ")}), and --json never chooses one.`, {
|
|
3277
|
+
hint: `Pass --config <path> with one of: ${paths.join(", ")}. Run once for each database to act on all.`,
|
|
3278
|
+
option: "--config"
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
1870
3281
|
/**
|
|
1871
3282
|
* A stand-in for an explicit `--config` that could not be classified (unloadable / not a drizzle config):
|
|
1872
3283
|
* the command still drives `drizzle-kit` on it and surfaces drizzle-kit's own error, trusting the flag.
|
|
@@ -1887,9 +3298,18 @@ var MigrationsRunner = class MigrationsRunner {
|
|
|
1887
3298
|
*/
|
|
1888
3299
|
static reportNoConfig(detection) {
|
|
1889
3300
|
process.exitCode = 1;
|
|
1890
|
-
cancel(
|
|
3301
|
+
cancel(MigrationsRunner.noConfigMessage);
|
|
1891
3302
|
for (const failure of detection.loadErrors) log.warn(`Could not load ${failure.path}: ${failure.message}`);
|
|
1892
3303
|
}
|
|
3304
|
+
/** The `--json` diagnostics of the "no drizzle-kit config" halt: the error, then one warning per candidate. */
|
|
3305
|
+
static noConfigDiagnostics(detection) {
|
|
3306
|
+
return [CliDiagnostics.error(MigrationsDiagnosticCode.NoConfig, MigrationsRunner.noConfigMessage, { hint: "Add a drizzle-kit config, or pass --config <path>." }), ...detection.loadErrors.map((failure) => ({
|
|
3307
|
+
severity: DiagnosticSeverity.Warning,
|
|
3308
|
+
code: MigrationsDiagnosticCode.ConfigLoadFailed,
|
|
3309
|
+
message: `Could not load ${failure.path}: ${failure.message}`,
|
|
3310
|
+
file: failure.path
|
|
3311
|
+
}))];
|
|
3312
|
+
}
|
|
1893
3313
|
};
|
|
1894
3314
|
const generateCommand = defineCommand({
|
|
1895
3315
|
meta: {
|
|
@@ -1903,16 +3323,24 @@ const generateCommand = defineCommand({
|
|
|
1903
3323
|
description: "Name the migration (drizzle-kit --name)."
|
|
1904
3324
|
}
|
|
1905
3325
|
},
|
|
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);
|
|
3326
|
+
run: ({ args }) => MigrationsRunner.execute("generate", args, async (runner) => {
|
|
3327
|
+
const selected = await runner.selectConfigs();
|
|
1911
3328
|
if (selected === void 0) return;
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
for (const config of selected)
|
|
1915
|
-
|
|
3329
|
+
const drizzleKit = new DrizzleKitRunner();
|
|
3330
|
+
const entries = [];
|
|
3331
|
+
for (const config of selected) {
|
|
3332
|
+
const filesBefore = new Set(await runner.sqlFilesOf(config));
|
|
3333
|
+
const entry = await runner.runStep(config, `Generating migration for ${config.name}`, () => drizzleKit.generate(config.path, runner.drizzleKitOptions(), args.name));
|
|
3334
|
+
const filesAfter = await runner.sqlFilesOf(config);
|
|
3335
|
+
entries.push({
|
|
3336
|
+
...entry,
|
|
3337
|
+
generatedFiles: filesAfter.filter((file) => !filesBefore.has(file))
|
|
3338
|
+
});
|
|
3339
|
+
if (entry.status === MigrationsStepStatus.NotStarted) break;
|
|
3340
|
+
}
|
|
3341
|
+
const document = runner.document(entries);
|
|
3342
|
+
runner.commandOutput.emit(document);
|
|
3343
|
+
if (!runner.isSuccessful()) {
|
|
1916
3344
|
process.exitCode = 1;
|
|
1917
3345
|
cancel("Some migrations failed to generate — see above.");
|
|
1918
3346
|
return;
|
|
@@ -1926,26 +3354,37 @@ const migrateCommand = defineCommand({
|
|
|
1926
3354
|
description: "Apply pending migrations (drizzle-kit migrate)."
|
|
1927
3355
|
},
|
|
1928
3356
|
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);
|
|
3357
|
+
run: ({ args }) => MigrationsRunner.execute("migrate", args, async (runner) => {
|
|
3358
|
+
const selected = await runner.selectConfigs();
|
|
1934
3359
|
if (selected === void 0) return;
|
|
1935
|
-
const
|
|
1936
|
-
|
|
3360
|
+
const drizzleKit = new DrizzleKitRunner();
|
|
3361
|
+
const entries = [];
|
|
1937
3362
|
for (const config of selected) {
|
|
1938
3363
|
if (config.applyMode === "wrangler-d1") {
|
|
1939
|
-
|
|
3364
|
+
const message = `${config.name}: a Cloudflare D1 database — apply it with \`wrangler d1 migrations apply <database> --remote\`, not drizzle-kit.`;
|
|
3365
|
+
log.warn(message);
|
|
3366
|
+
entries.push(runner.skip(config, message, {
|
|
3367
|
+
severity: DiagnosticSeverity.Warning,
|
|
3368
|
+
code: MigrationsDiagnosticCode.D1ApplySkipped,
|
|
3369
|
+
message,
|
|
3370
|
+
hint: "Run `wrangler d1 migrations apply <database> --remote`.",
|
|
3371
|
+
file: config.path
|
|
3372
|
+
}));
|
|
1940
3373
|
continue;
|
|
1941
3374
|
}
|
|
1942
3375
|
if (config.applyMode === "skip") {
|
|
1943
|
-
|
|
3376
|
+
const message = `${config.name}: Durable Object SQLite — migrations bundle in-DO at init; no CLI apply step.`;
|
|
3377
|
+
log.info(message);
|
|
3378
|
+
entries.push(runner.skip(config, message));
|
|
1944
3379
|
continue;
|
|
1945
3380
|
}
|
|
1946
|
-
|
|
3381
|
+
const entry = await runner.runStep(config, `Applying migrations for ${config.name}`, () => drizzleKit.migrate(config.path, runner.drizzleKitOptions()));
|
|
3382
|
+
entries.push(entry);
|
|
3383
|
+
if (entry.status === MigrationsStepStatus.NotStarted) break;
|
|
1947
3384
|
}
|
|
1948
|
-
|
|
3385
|
+
const document = runner.document(entries);
|
|
3386
|
+
runner.commandOutput.emit(document);
|
|
3387
|
+
if (!runner.isSuccessful()) {
|
|
1949
3388
|
process.exitCode = 1;
|
|
1950
3389
|
cancel("Some migrations failed to apply — see above.");
|
|
1951
3390
|
return;
|
|
@@ -1959,21 +3398,25 @@ const statusCommand = defineCommand({
|
|
|
1959
3398
|
description: "List the on-disk migrations per database."
|
|
1960
3399
|
},
|
|
1961
3400
|
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);
|
|
3401
|
+
run: ({ args }) => MigrationsRunner.execute("status", args, async (runner) => {
|
|
3402
|
+
const selected = await runner.selectConfigs();
|
|
1967
3403
|
if (selected === void 0) return;
|
|
3404
|
+
const entries = [];
|
|
1968
3405
|
for (const config of selected) {
|
|
1969
|
-
const outFolder = config
|
|
1970
|
-
const files = await
|
|
3406
|
+
const outFolder = MigrationsRunner.migrationsDirectoryOf(config);
|
|
3407
|
+
const files = await runner.sqlFilesOf(config);
|
|
3408
|
+
entries.push({
|
|
3409
|
+
...runner.viewOf(config),
|
|
3410
|
+
migrations: files
|
|
3411
|
+
});
|
|
1971
3412
|
if (files.length === 0) {
|
|
1972
3413
|
log.info(`${config.name}: no migrations in ${outFolder} — run \`heximon migrations generate\`.`);
|
|
1973
3414
|
continue;
|
|
1974
3415
|
}
|
|
1975
|
-
log.message(`${config.name} · ${files.length} migration(s) in ${outFolder}:\n` + files.map((file) => ` ${file
|
|
3416
|
+
log.message(`${config.name} · ${files.length} migration(s) in ${outFolder}:\n` + files.map((file) => ` ${file}`).join("\n"));
|
|
1976
3417
|
}
|
|
3418
|
+
const document = runner.document(entries);
|
|
3419
|
+
runner.commandOutput.emit(document);
|
|
1977
3420
|
log.info("Applied vs. pending state needs a live database query — not shown in this view.");
|
|
1978
3421
|
outro("Status listed.");
|
|
1979
3422
|
})
|
|
@@ -1990,21 +3433,20 @@ const pushCommand = defineCommand({
|
|
|
1990
3433
|
description: "Confirm the destructive push non-interactively."
|
|
1991
3434
|
}
|
|
1992
3435
|
},
|
|
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);
|
|
3436
|
+
run: ({ args }) => MigrationsRunner.execute("push", args, async (runner) => {
|
|
3437
|
+
const selected = await runner.selectConfigs();
|
|
1998
3438
|
if (selected === void 0) return;
|
|
1999
3439
|
let force = args.yes === true;
|
|
2000
3440
|
if (!force) {
|
|
2001
|
-
|
|
3441
|
+
const question = `Push schema directly to ${selected.map((config) => config.name).join(", ")}? This can DROP columns/tables.`;
|
|
3442
|
+
runner.commandOutput.assertCanPrompt("--yes", question);
|
|
3443
|
+
if (!runner.commandOutput.isInteractive()) {
|
|
2002
3444
|
process.exitCode = 1;
|
|
2003
3445
|
cancel("`push` is destructive (it can drop columns/tables). Re-run with --yes to confirm in a non-interactive shell.");
|
|
2004
3446
|
return;
|
|
2005
3447
|
}
|
|
2006
3448
|
const confirmed = await confirm({
|
|
2007
|
-
message:
|
|
3449
|
+
message: question,
|
|
2008
3450
|
initialValue: false
|
|
2009
3451
|
});
|
|
2010
3452
|
if (isCancel(confirmed) || confirmed !== true) {
|
|
@@ -2014,10 +3456,16 @@ const pushCommand = defineCommand({
|
|
|
2014
3456
|
}
|
|
2015
3457
|
force = true;
|
|
2016
3458
|
}
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
for (const config of selected)
|
|
2020
|
-
|
|
3459
|
+
const drizzleKit = new DrizzleKitRunner();
|
|
3460
|
+
const entries = [];
|
|
3461
|
+
for (const config of selected) {
|
|
3462
|
+
const entry = await runner.runStep(config, `Pushing schema for ${config.name}`, () => drizzleKit.push(config.path, runner.drizzleKitOptions(), force));
|
|
3463
|
+
entries.push(entry);
|
|
3464
|
+
if (entry.status === MigrationsStepStatus.NotStarted) break;
|
|
3465
|
+
}
|
|
3466
|
+
const document = runner.document(entries);
|
|
3467
|
+
runner.commandOutput.emit(document);
|
|
3468
|
+
if (!runner.isSuccessful()) {
|
|
2021
3469
|
process.exitCode = 1;
|
|
2022
3470
|
cancel("Some schema pushes failed — see above.");
|
|
2023
3471
|
return;
|
|
@@ -2043,8 +3491,92 @@ const migrationsCommand = defineCommand({
|
|
|
2043
3491
|
}
|
|
2044
3492
|
});
|
|
2045
3493
|
//#endregion
|
|
3494
|
+
//#region src/provision-json-document.ts
|
|
3495
|
+
/**
|
|
3496
|
+
* The stable codes of the `heximon provision --json` diagnostics. `--profile` uses `cli.invalid-options`,
|
|
3497
|
+
* and a missing credential uses `cli.option-required`.
|
|
3498
|
+
*/
|
|
3499
|
+
const ProvisionDiagnosticCode = {
|
|
3500
|
+
/** The platform has no foundational resources to provision. */
|
|
3501
|
+
Unsupported: "provision.unsupported",
|
|
3502
|
+
/** One or more resources failed to provision; `result.failed` names them. */
|
|
3503
|
+
Failed: "provision.failed",
|
|
3504
|
+
/** No deploy strategy is installed for the platform. */
|
|
3505
|
+
StrategyUnavailable: "provision.strategy-unavailable",
|
|
3506
|
+
/** A Nitro or Nuxt build uses a preset that the deploy cannot ship. */
|
|
3507
|
+
HostManagedUnsupported: "provision.host-managed-unsupported"
|
|
3508
|
+
};
|
|
3509
|
+
//#endregion
|
|
2046
3510
|
//#region src/provision-command.ts
|
|
2047
3511
|
/**
|
|
3512
|
+
* Builds the `heximon provision --json` document. The run fields are set as the run reaches them, so a
|
|
3513
|
+
* failure document still says how far the run got.
|
|
3514
|
+
*/
|
|
3515
|
+
var ProvisionDocument = class ProvisionDocument {
|
|
3516
|
+
environment;
|
|
3517
|
+
dryRun;
|
|
3518
|
+
target;
|
|
3519
|
+
credentials;
|
|
3520
|
+
/**
|
|
3521
|
+
* @param environment - The deploy environment.
|
|
3522
|
+
* @param dryRun - Whether the run creates nothing.
|
|
3523
|
+
*/
|
|
3524
|
+
constructor(environment, dryRun) {
|
|
3525
|
+
this.environment = environment;
|
|
3526
|
+
this.dryRun = dryRun;
|
|
3527
|
+
}
|
|
3528
|
+
/** Record the resolved target. */
|
|
3529
|
+
resolved(target) {
|
|
3530
|
+
this.target = {
|
|
3531
|
+
platform: target.platformName ?? "node",
|
|
3532
|
+
targetClass: target.targetClass,
|
|
3533
|
+
hostManaged: target.hostManaged === true,
|
|
3534
|
+
...target.artifactDir === void 0 ? {} : { artifactDir: target.artifactDir }
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
/** Record the names of the resolved credential variables. */
|
|
3538
|
+
resolvedCredentials(credentials) {
|
|
3539
|
+
this.credentials = [...credentials.keys()];
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* The document of the run.
|
|
3543
|
+
*
|
|
3544
|
+
* @param ok - Whether the command exits 0.
|
|
3545
|
+
* @param diagnostics - The diagnostics of the run.
|
|
3546
|
+
* @param outcome - The provisioning outcome, once the run has one.
|
|
3547
|
+
* @returns The document.
|
|
3548
|
+
*/
|
|
3549
|
+
document(ok, diagnostics, outcome) {
|
|
3550
|
+
return {
|
|
3551
|
+
ok,
|
|
3552
|
+
diagnostics,
|
|
3553
|
+
environment: this.environment,
|
|
3554
|
+
dryRun: this.dryRun,
|
|
3555
|
+
...this.target === void 0 ? {} : { target: this.target },
|
|
3556
|
+
...this.credentials === void 0 ? {} : { credentials: this.credentials },
|
|
3557
|
+
...outcome?.result === void 0 ? {} : { result: ProvisionDocument.resultOf(outcome.result) },
|
|
3558
|
+
...outcome?.savedIds === void 0 ? {} : { savedIds: {
|
|
3559
|
+
path: outcome.savedIds.path,
|
|
3560
|
+
names: outcome.savedIds.names
|
|
3561
|
+
} }
|
|
3562
|
+
};
|
|
3563
|
+
}
|
|
3564
|
+
/** Copy the provisioning result into its document shape. */
|
|
3565
|
+
static resultOf(result) {
|
|
3566
|
+
return {
|
|
3567
|
+
platform: result.platform,
|
|
3568
|
+
provisioned: result.provisioned.map((provisioned) => ({
|
|
3569
|
+
resource: provisioned.resource,
|
|
3570
|
+
id: provisioned.id,
|
|
3571
|
+
...provisioned.envVar === void 0 ? {} : { envVar: provisioned.envVar }
|
|
3572
|
+
})),
|
|
3573
|
+
existing: result.existing,
|
|
3574
|
+
failed: result.failed,
|
|
3575
|
+
messages: result.messages
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
};
|
|
3579
|
+
/**
|
|
2048
3580
|
* `heximon provision` — the PRE-BUILD verb for resources whose id the build itself needs (a Cloudflare
|
|
2049
3581
|
* Hyperdrive binding with an empty `id` fails the build outright, and provisioning is what mints that id).
|
|
2050
3582
|
* It calls exactly one seam, `strategy.provisionInfrastructure(context)` — never the deploy-time
|
|
@@ -2061,6 +3593,7 @@ const provisionCommand = defineCommand({
|
|
|
2061
3593
|
description: "Project root directory.",
|
|
2062
3594
|
default: "."
|
|
2063
3595
|
},
|
|
3596
|
+
json: CommandOutput.jsonFlag,
|
|
2064
3597
|
env: {
|
|
2065
3598
|
type: "string",
|
|
2066
3599
|
description: "Deploy environment (production / staging / preview:<id>).",
|
|
@@ -2086,61 +3619,97 @@ const provisionCommand = defineCommand({
|
|
|
2086
3619
|
}
|
|
2087
3620
|
},
|
|
2088
3621
|
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.`);
|
|
3622
|
+
const commandOutput = CommandOutput.resolve("provision", args);
|
|
3623
|
+
await commandOutput.run(async () => {
|
|
3624
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
3625
|
+
const environment = args.env;
|
|
3626
|
+
const dryRun = args["dry-run"];
|
|
3627
|
+
const interactive = commandOutput.isInteractive();
|
|
3628
|
+
const report = new ProvisionDocument(environment, dryRun);
|
|
3629
|
+
intro(`heximon provision · ${environment}`);
|
|
3630
|
+
if (args.profile !== void 0) {
|
|
3631
|
+
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.";
|
|
3632
|
+
cancel(message);
|
|
2124
3633
|
process.exitCode = 1;
|
|
3634
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(CliDiagnosticCode.InvalidOptions, message, { hint: "Remove --profile and set CLOUDFLARE_API_TOKEN." })]));
|
|
2125
3635
|
return;
|
|
2126
3636
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
3637
|
+
try {
|
|
3638
|
+
const target = await ConfigSidecar.during(rootDir, environment, () => DeployTargetResolver.resolve(rootDir, environment, args.platform));
|
|
3639
|
+
report.resolved(target);
|
|
3640
|
+
log.info(`Platform: ${target.platformName ?? "node (default)"} · target class: ${target.targetClass}`);
|
|
3641
|
+
const specs = CredentialSelection.inPlay(target.strategy.requiredCredentials(), { provisioning: true });
|
|
3642
|
+
const credentials = await CredentialResolver.resolve(specs, {
|
|
3643
|
+
interactive: interactive || commandOutput.isJson(),
|
|
3644
|
+
prompter: commandOutput.prompter(CredentialResolver.optionFor(specs))
|
|
3645
|
+
});
|
|
3646
|
+
report.resolvedCredentials(credentials);
|
|
3647
|
+
const progress = CliSpinner.begin(interactive, dryRun ? "Previewing the foundational resources…" : "Provisioning foundational resources…");
|
|
3648
|
+
let outcome;
|
|
3649
|
+
try {
|
|
3650
|
+
outcome = await DeployRunner.provisionInfrastructure({
|
|
3651
|
+
target,
|
|
3652
|
+
rootDir,
|
|
3653
|
+
environment,
|
|
3654
|
+
credentials,
|
|
3655
|
+
dryRun,
|
|
3656
|
+
saveIds: args["save-ids"]
|
|
3657
|
+
});
|
|
3658
|
+
} catch (provisionError) {
|
|
3659
|
+
progress?.stop("Provisioning failed — see the error below.");
|
|
3660
|
+
throw provisionError;
|
|
3661
|
+
}
|
|
3662
|
+
progress?.stop("Provision steps complete.");
|
|
3663
|
+
if (outcome.unsupported) {
|
|
3664
|
+
const message = `Platform ${outcome.platformName ?? "node (default)"} has no foundational resources to provision.`;
|
|
3665
|
+
cancel(message);
|
|
3666
|
+
process.exitCode = 1;
|
|
3667
|
+
commandOutput.emit(report.document(false, [CliDiagnostics.error(ProvisionDiagnosticCode.Unsupported, message)], outcome));
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
const result = outcome.result;
|
|
3671
|
+
for (const message of result?.messages ?? []) log.message(message);
|
|
3672
|
+
for (const provisioned of result?.provisioned ?? []) log.success(`Provisioned ${provisioned.resource} → id ${provisioned.id}.`);
|
|
3673
|
+
if (outcome.savedIds !== void 0) log.success(`Recorded ${outcome.savedIds.names.join(", ")} in ${outcome.savedIds.path} — \`heximon deploy\` injects it into the build.`);
|
|
3674
|
+
if (result !== void 0 && result.failed.length > 0) {
|
|
3675
|
+
process.exitCode = 1;
|
|
3676
|
+
cancel(`Provisioning failed for: ${result.failed.join(", ")} — see above.`);
|
|
3677
|
+
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));
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
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.");
|
|
3681
|
+
commandOutput.emit(report.document(true, [], outcome));
|
|
3682
|
+
} catch (error) {
|
|
3683
|
+
if (DeployCommandFailure.report(error)) {
|
|
3684
|
+
const diagnostic = DeployCommandFailure.diagnosticOf(error, ProvisionDiagnosticCode);
|
|
3685
|
+
if (diagnostic !== void 0) commandOutput.emit(report.document(false, [diagnostic]));
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
if (error instanceof CommandFailure && commandOutput.isJson()) {
|
|
3689
|
+
commandOutput.emit(report.document(false, error.diagnostics));
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
throw error;
|
|
2135
3693
|
}
|
|
2136
|
-
|
|
2137
|
-
} catch (error) {
|
|
2138
|
-
if (DeployCommandFailure.report(error)) return;
|
|
2139
|
-
throw error;
|
|
2140
|
-
}
|
|
3694
|
+
});
|
|
2141
3695
|
}
|
|
2142
3696
|
});
|
|
2143
3697
|
//#endregion
|
|
3698
|
+
//#region src/routes-json-document.ts
|
|
3699
|
+
/** The stable codes of the failures `heximon routes` raises itself. */
|
|
3700
|
+
const RoutesDiagnosticCode = {
|
|
3701
|
+
/** The compile threw before the command could read a route census. */
|
|
3702
|
+
ReadFailed: "routes.read-failed",
|
|
3703
|
+
/** `--boot` could not load or boot the app. A compile error of the boot gives the compiler diagnostics instead. */
|
|
3704
|
+
BootFailed: "routes.boot-failed",
|
|
3705
|
+
/** `--boot` booted the app, but its live route table could not be read. */
|
|
3706
|
+
TableUnreadable: "routes.table-unreadable",
|
|
3707
|
+
/** `--boot` read the route table, but the booted app did not dispose cleanly; a client can still be open. */
|
|
3708
|
+
DisposeFailed: "routes.dispose-failed",
|
|
3709
|
+
/** The `--out` file could not be written. */
|
|
3710
|
+
OutWriteFailed: "routes.out-write-failed"
|
|
3711
|
+
};
|
|
3712
|
+
//#endregion
|
|
2144
3713
|
//#region src/route-table-report.ts
|
|
2145
3714
|
/**
|
|
2146
3715
|
* Renders a {@link RouteCensusRow} list as an aligned, columnar route table for the terminal —
|
|
@@ -2228,18 +3797,39 @@ var TransformBoot = class TransformBoot {
|
|
|
2228
3797
|
return process.env[TransformBoot.transformMarker] === "1";
|
|
2229
3798
|
}
|
|
2230
3799
|
/**
|
|
3800
|
+
* Re-run this exact CLI invocation under `node --experimental-transform-types`, and set the exit status of
|
|
3801
|
+
* the child. The child prints the whole output, `--json` document included.
|
|
3802
|
+
*
|
|
3803
|
+
* A child that does not start printed nothing, so this process prints the failure: a document with
|
|
3804
|
+
* {@link CliDiagnosticCode.ReexecFailed} under `--json` (also written to the `--out` file), else one stderr
|
|
3805
|
+
* line. A child that a signal stops has no result, so it gets a stderr line and no document.
|
|
3806
|
+
*
|
|
3807
|
+
* @param commandLabel - The command name for the stderr lines (`routes --boot`, `task run`).
|
|
3808
|
+
* @param commandOutput - The output owner of the run.
|
|
3809
|
+
* @param outFile - The `--out` file of a command that has one.
|
|
3810
|
+
*/
|
|
3811
|
+
static async relaunch(commandLabel, commandOutput, outFile) {
|
|
3812
|
+
const exit = await TransformBoot.reexecUnderTransform(commandLabel);
|
|
3813
|
+
if (exit.startError === void 0) {
|
|
3814
|
+
process.exitCode = exit.exitCode;
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
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.");
|
|
3818
|
+
const fail = () => Promise.reject(failure);
|
|
3819
|
+
await commandOutput.run(() => outFile === void 0 ? fail() : outFile.run(fail));
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
2231
3822
|
* Re-run this exact CLI invocation under `node --experimental-transform-types`, inheriting stdio so the
|
|
2232
3823
|
* child's output and prompts reach the terminal directly.
|
|
2233
3824
|
*
|
|
2234
3825
|
* The child owns an interrupt: a terminal sends Ctrl-C to both processes, and the child cleans up. A
|
|
2235
3826
|
* `SIGTERM` or `SIGHUP` that reaches only this process is passed on, so the child never outlives it.
|
|
2236
3827
|
*
|
|
2237
|
-
* A child that
|
|
2238
|
-
*
|
|
2239
|
-
* silent run reads as "the command found a problem" rather than "the command never ran".
|
|
3828
|
+
* A child that a signal killed reports no exit code. It becomes `1`, NAMED on stderr first, because a bare
|
|
3829
|
+
* `exit 1` after a silent run reads as "the command found a problem" rather than "the command was stopped".
|
|
2240
3830
|
*
|
|
2241
|
-
* @param commandLabel - The command name for the
|
|
2242
|
-
* @returns The
|
|
3831
|
+
* @param commandLabel - The command name for the stderr line of a stopped child.
|
|
3832
|
+
* @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
3833
|
*/
|
|
2244
3834
|
static reexecUnderTransform(commandLabel) {
|
|
2245
3835
|
return new Promise((resolve) => {
|
|
@@ -2263,19 +3853,24 @@ var TransformBoot = class TransformBoot {
|
|
|
2263
3853
|
process.on(signal, forward);
|
|
2264
3854
|
return [signal, forward];
|
|
2265
3855
|
});
|
|
3856
|
+
let settled = false;
|
|
2266
3857
|
process.on("SIGINT", ignoreInterrupt);
|
|
2267
|
-
const finish = (
|
|
3858
|
+
const finish = (exit) => {
|
|
3859
|
+
if (settled) return;
|
|
3860
|
+
settled = true;
|
|
2268
3861
|
process.off("SIGINT", ignoreInterrupt);
|
|
2269
3862
|
for (const [signal, forward] of forwarded) process.off(signal, forward);
|
|
2270
|
-
resolve(
|
|
3863
|
+
resolve(exit);
|
|
2271
3864
|
};
|
|
2272
3865
|
child.once("error", (error) => {
|
|
2273
|
-
|
|
2274
|
-
|
|
3866
|
+
if (child.pid === void 0) finish({
|
|
3867
|
+
exitCode: 1,
|
|
3868
|
+
startError: error
|
|
3869
|
+
});
|
|
2275
3870
|
});
|
|
2276
3871
|
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);
|
|
3872
|
+
if (signal !== null && !settled) process.stderr.write(`heximon ${commandLabel}: the transform-enabled child was killed by ${signal}.\n`);
|
|
3873
|
+
finish({ exitCode: code ?? 1 });
|
|
2279
3874
|
});
|
|
2280
3875
|
});
|
|
2281
3876
|
}
|
|
@@ -2397,14 +3992,10 @@ const routesCommand = defineCommand({
|
|
|
2397
3992
|
description: "The `.env.<mode>` overlay to load first; also the config `$env`. Default development.",
|
|
2398
3993
|
default: "development"
|
|
2399
3994
|
},
|
|
2400
|
-
json:
|
|
2401
|
-
type: "boolean",
|
|
2402
|
-
description: "Emit the route table as JSON on stdout instead of a table.",
|
|
2403
|
-
default: false
|
|
2404
|
-
},
|
|
3995
|
+
json: CommandOutput.jsonFlag,
|
|
2405
3996
|
out: {
|
|
2406
3997
|
type: "string",
|
|
2407
|
-
description: "
|
|
3998
|
+
description: "Also write the JSON document to this file, creating missing parent directories — the CI-artifact target."
|
|
2408
3999
|
},
|
|
2409
4000
|
boot: {
|
|
2410
4001
|
type: "boolean",
|
|
@@ -2420,24 +4011,27 @@ const routesCommand = defineCommand({
|
|
|
2420
4011
|
},
|
|
2421
4012
|
async run({ args }) {
|
|
2422
4013
|
const asBoot = args.boot;
|
|
4014
|
+
const commandOutput = CommandOutput.resolve("routes", args);
|
|
4015
|
+
const outFile = JsonOutputTarget.resolve(args.out);
|
|
2423
4016
|
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);
|
|
4017
|
+
if (await ProjectRoot.checkBeforeRelaunch(args.cwd, commandOutput)) await TransformBoot.relaunch("routes --boot", commandOutput, outFile);
|
|
2435
4018
|
return;
|
|
2436
4019
|
}
|
|
2437
|
-
await
|
|
4020
|
+
await commandOutput.run(async () => {
|
|
4021
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
4022
|
+
await outFile.run(async () => {
|
|
4023
|
+
const resolved = CompileModeResolver.resolve(rootDir, args, SourceDirectories.resolve(rootDir, args.src));
|
|
4024
|
+
if (!commandOutput.isJson()) intro(`heximon routes — ${basename(rootDir)} · ${CompileModeResolver.stamp(resolved)}`);
|
|
4025
|
+
if (asBoot) {
|
|
4026
|
+
await RoutesCommand.runBoot(rootDir, args, commandOutput, outFile);
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
await RoutesCommand.runCompile(rootDir, args, commandOutput, outFile, resolved.mode);
|
|
4030
|
+
});
|
|
4031
|
+
});
|
|
2438
4032
|
}
|
|
2439
4033
|
});
|
|
2440
|
-
/** Output helpers for {@link routesCommand}: the two read paths, the table / JSON
|
|
4034
|
+
/** Output helpers for {@link routesCommand}: the two read paths, the table / JSON document, and the failure summary. */
|
|
2441
4035
|
var RoutesCommand = class RoutesCommand {
|
|
2442
4036
|
/**
|
|
2443
4037
|
* The compile-time read: run the same from-disk compile `heximon check` runs and report the route census
|
|
@@ -2449,8 +4043,8 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2449
4043
|
* route table is readable without composing it into an app. The `requires` a library leaves open for its
|
|
2450
4044
|
* host are not diagnostics there, so nothing floods the report.
|
|
2451
4045
|
*/
|
|
2452
|
-
static async runCompile(rootDir, args,
|
|
2453
|
-
let
|
|
4046
|
+
static async runCompile(rootDir, args, commandOutput, outFile, mode = CompileMode.App) {
|
|
4047
|
+
let compiled;
|
|
2454
4048
|
try {
|
|
2455
4049
|
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
2456
4050
|
DotenvCascade.loadInto(rootDir, args.env);
|
|
@@ -2459,24 +4053,21 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2459
4053
|
environment: args.env,
|
|
2460
4054
|
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
2461
4055
|
});
|
|
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
|
-
};
|
|
4056
|
+
compiled = mode === CompileMode.Library ? session.compileLibrary() : session.compile();
|
|
2474
4057
|
} catch (error) {
|
|
2475
|
-
RoutesCommand.reportFailure(error,
|
|
2476
|
-
process.exitCode = 1;
|
|
4058
|
+
RoutesCommand.reportFailure(RoutesCommand.diagnosticsOf(RoutesDiagnosticCode.ReadFailed, error), commandOutput, outFile);
|
|
2477
4059
|
return;
|
|
2478
4060
|
}
|
|
2479
|
-
|
|
4061
|
+
if (compiled.diagnostics.some((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error)) {
|
|
4062
|
+
RoutesCommand.reportFailure(CliDiagnostics.fromCompiler(compiled.diagnostics), commandOutput, outFile);
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
const file = compiled.files.find((generated) => generated.path === RouteCensus.fileName);
|
|
4066
|
+
const census = (file === void 0 ? void 0 : RouteCensus.parse(file.content)) ?? {
|
|
4067
|
+
routes: [],
|
|
4068
|
+
unresolved: []
|
|
4069
|
+
};
|
|
4070
|
+
RoutesCommand.report(census.routes, census.unresolved, commandOutput, outFile, {
|
|
2480
4071
|
libraryMode: mode === CompileMode.Library,
|
|
2481
4072
|
...census.scopeEnforcement === void 0 ? {} : { scopeEnforcement: census.scopeEnforcement }
|
|
2482
4073
|
});
|
|
@@ -2494,17 +4085,19 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2494
4085
|
* subcommand thunk while building the completion tree, so citty's lazy `Resolvable` form resolves the
|
|
2495
4086
|
* module anyway.
|
|
2496
4087
|
*/
|
|
2497
|
-
static async runBoot(rootDir, args,
|
|
4088
|
+
static async runBoot(rootDir, args, commandOutput, outFile) {
|
|
2498
4089
|
let app;
|
|
2499
4090
|
let inventory;
|
|
4091
|
+
let testing;
|
|
2500
4092
|
try {
|
|
2501
4093
|
TransformBoot.registerSourceResolver();
|
|
2502
4094
|
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
2503
4095
|
DotenvCascade.loadInto(rootDir, args.env);
|
|
2504
|
-
const config = await
|
|
2505
|
-
const [
|
|
4096
|
+
const config = await ConfigSidecar.load(rootDir, args.env);
|
|
4097
|
+
const [testingModule, { RouteInventory }] = await Promise.all([import("@heximon/testing"), import("@heximon/http")]);
|
|
4098
|
+
testing = testingModule;
|
|
2506
4099
|
inventory = RouteInventory;
|
|
2507
|
-
app = await
|
|
4100
|
+
app = await testingModule.createTestApp({
|
|
2508
4101
|
root: rootDir,
|
|
2509
4102
|
printWarnings: false,
|
|
2510
4103
|
...sourceDirectories === void 0 ? {} : { sourceDirectories },
|
|
@@ -2513,25 +4106,28 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2513
4106
|
...config?.platform === void 0 ? {} : { platform: config.platform }
|
|
2514
4107
|
});
|
|
2515
4108
|
} catch (error) {
|
|
2516
|
-
RoutesCommand.reportFailure(error,
|
|
2517
|
-
process.exitCode = 1;
|
|
4109
|
+
RoutesCommand.reportFailure(testing !== void 0 && error instanceof testing.TestAppCompileError ? CliDiagnostics.fromCompiler(error.diagnostics) : RoutesCommand.diagnosticsOf(RoutesDiagnosticCode.BootFailed, error), commandOutput, outFile);
|
|
2518
4110
|
return;
|
|
2519
4111
|
}
|
|
4112
|
+
let routes;
|
|
4113
|
+
const failures = [];
|
|
2520
4114
|
try {
|
|
2521
|
-
|
|
2522
|
-
RoutesCommand.report(routes, [], asJson, jsonOutput);
|
|
4115
|
+
routes = inventory.describe(app).map(RoutesCommand.toCensusRow);
|
|
2523
4116
|
} catch (error) {
|
|
2524
|
-
RoutesCommand.
|
|
2525
|
-
process.exitCode = 1;
|
|
4117
|
+
failures.push(RoutesCommand.diagnosticOf(RoutesDiagnosticCode.TableUnreadable, error));
|
|
2526
4118
|
} finally {
|
|
2527
4119
|
try {
|
|
2528
4120
|
await app.context.drain({ timeoutMs: 5e3 });
|
|
2529
4121
|
await app[Symbol.asyncDispose]();
|
|
2530
4122
|
} 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)}`);
|
|
4123
|
+
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
4124
|
}
|
|
2534
4125
|
}
|
|
4126
|
+
if (routes === void 0) {
|
|
4127
|
+
RoutesCommand.reportFailure(failures, commandOutput, outFile);
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
RoutesCommand.report(routes, [], commandOutput, outFile, { failures });
|
|
2535
4131
|
}
|
|
2536
4132
|
/**
|
|
2537
4133
|
* Map one booted route to a census row. An entry whose {@link RouteInventoryEntry.scopes} is defined (every
|
|
@@ -2549,23 +4145,29 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2549
4145
|
};
|
|
2550
4146
|
}
|
|
2551
4147
|
/**
|
|
2552
|
-
*
|
|
4148
|
+
* Report the resolved routes: the JSON document to stdout under `--json` and to the `--out` file, and an
|
|
4149
|
+
* aligned table unless `--json`. A failure or a failed `--out` write sets exit status 1.
|
|
2553
4150
|
*
|
|
2554
4151
|
* @param options - `libraryMode` adds the one fact a package's own table cannot show: the published root's
|
|
2555
4152
|
* 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.
|
|
4153
|
+
* paths. `scopeEnforcement` is the census section of the same name, passed through to the JSON document
|
|
4154
|
+
* so a CI job reads the scope-enforcer verdict from the same artifact. `failures` are errors that fail
|
|
4155
|
+
* the run but keep the table it read.
|
|
2558
4156
|
*/
|
|
2559
|
-
static report(routes, unresolved,
|
|
2560
|
-
const
|
|
2561
|
-
|
|
4157
|
+
static report(routes, unresolved, commandOutput, outFile, options = {}) {
|
|
4158
|
+
const failures = options.failures ?? [];
|
|
4159
|
+
const document = {
|
|
4160
|
+
ok: failures.length === 0,
|
|
4161
|
+
diagnostics: failures,
|
|
2562
4162
|
routes,
|
|
2563
4163
|
unresolved,
|
|
2564
4164
|
...options.scopeEnforcement === void 0 ? {} : { scopeEnforcement: options.scopeEnforcement }
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
if (
|
|
2568
|
-
if (
|
|
4165
|
+
};
|
|
4166
|
+
const written = outFile.publish(document, commandOutput, RoutesDiagnosticCode.OutWriteFailed);
|
|
4167
|
+
if (!written || failures.length > 0) process.exitCode = 1;
|
|
4168
|
+
if (commandOutput.isJson()) return;
|
|
4169
|
+
if (written && outFile.isFile()) log.success(`Route JSON written to ${outFile.filePath}.`);
|
|
4170
|
+
for (const failure of failures) log.warn(failure.message);
|
|
2569
4171
|
if (routes.length === 0 && unresolved.length === 0) {
|
|
2570
4172
|
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
4173
|
return;
|
|
@@ -2580,20 +4182,34 @@ var RoutesCommand = class RoutesCommand {
|
|
|
2580
4182
|
outro(`${routes.length} route${routes.length === 1 ? "" : "s"}; ${unresolved.length} unresolved` + (options.libraryMode === true ? "." : " — re-run with --boot to enumerate them."));
|
|
2581
4183
|
}
|
|
2582
4184
|
/**
|
|
2583
|
-
* Report a compile / boot failure:
|
|
2584
|
-
* unless `--json`. The
|
|
2585
|
-
* rather than leaving a stale success
|
|
4185
|
+
* Report a compile / boot failure: a {@link CliFailureDocument} to stdout under `--json` and to the `--out`
|
|
4186
|
+
* file, and a clack error + outro unless `--json`. The file gets the failure too, so a new run
|
|
4187
|
+
* replaces the previous artifact rather than leaving a stale success. Sets exit status 1.
|
|
4188
|
+
*
|
|
4189
|
+
* @param diagnostics - The diagnostics of the failure: the compiler's, or one the command raised.
|
|
4190
|
+
* @param commandOutput - The stdout owner of the run.
|
|
4191
|
+
* @param outFile - The `--out` file.
|
|
2586
4192
|
*/
|
|
2587
|
-
static reportFailure(
|
|
2588
|
-
const
|
|
2589
|
-
if (!jsonOutput.emit({
|
|
4193
|
+
static reportFailure(diagnostics, commandOutput, outFile) {
|
|
4194
|
+
const document = {
|
|
2590
4195
|
ok: false,
|
|
2591
|
-
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
|
-
|
|
4196
|
+
diagnostics
|
|
4197
|
+
};
|
|
4198
|
+
outFile.publish(document, commandOutput, RoutesDiagnosticCode.OutWriteFailed);
|
|
4199
|
+
process.exitCode = 1;
|
|
4200
|
+
if (commandOutput.isJson()) return;
|
|
4201
|
+
const messages = diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).map((diagnostic) => diagnostic.message);
|
|
4202
|
+
log.error(`Could not read the app's routes:\n${messages.join("\n")}`);
|
|
2595
4203
|
outro("No route table to show.");
|
|
2596
4204
|
}
|
|
4205
|
+
/** One error diagnostic of the command, with the message of what was thrown. */
|
|
4206
|
+
static diagnosticOf(code, error) {
|
|
4207
|
+
return CliDiagnostics.error(code, error instanceof Error ? error.message : String(error));
|
|
4208
|
+
}
|
|
4209
|
+
/** The diagnostics of a thrown value: those of a {@link CommandFailure}, else one with `code`. */
|
|
4210
|
+
static diagnosticsOf(code, error) {
|
|
4211
|
+
return error instanceof CommandFailure ? error.diagnostics : [RoutesCommand.diagnosticOf(code, error)];
|
|
4212
|
+
}
|
|
2597
4213
|
/**
|
|
2598
4214
|
* The unresolved-gap block: one line per controller (or package) whose routes the census could not name.
|
|
2599
4215
|
* A composed package whose manifest section IS readable contributes rows instead, so nothing here is a
|
|
@@ -3460,38 +5076,52 @@ var PlatformBindingCensus = class PlatformBindingCensus {
|
|
|
3460
5076
|
}
|
|
3461
5077
|
};
|
|
3462
5078
|
//#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
|
-
|
|
5079
|
+
//#region src/task-run-json-document.ts
|
|
5080
|
+
/** The stable codes of the failures `heximon task run --json` reports. */
|
|
5081
|
+
const TaskDiagnosticCode = {
|
|
5082
|
+
/** The `run()` of the task threw. The run exits 2, and the document names the error. */
|
|
5083
|
+
TaskFailed: "task.failed",
|
|
5084
|
+
/** No module or no task of the compiled graph has the name, or the source file does not export the class. */
|
|
5085
|
+
TargetNotFound: "task.target-not-found",
|
|
5086
|
+
/** More than one module, or more than one task of the module, has the name. */
|
|
5087
|
+
TargetAmbiguous: "task.target-ambiguous",
|
|
5088
|
+
/** The named class has no `run()` method. */
|
|
5089
|
+
NotATask: "task.not-a-task",
|
|
5090
|
+
/** The production compile failed. The compiler diagnostics are in the same document. */
|
|
5091
|
+
CompileFailed: "task.compile-failed",
|
|
5092
|
+
/** The project has no app: the compile gave no module graph, or no wiring file. */
|
|
5093
|
+
AppNotFound: "task.app-not-found",
|
|
5094
|
+
/** A Nitro option is computed, so the command cannot read the preset or the source directories. */
|
|
5095
|
+
NitroConfigComputed: "task.nitro-config-computed",
|
|
5096
|
+
/** A packed library that the app imports does not have the factories a production build needs. */
|
|
5097
|
+
LibraryPreflightFailed: "task.library-preflight-failed",
|
|
5098
|
+
/** The graph needs bindings that only its platform has, so nothing was booted. */
|
|
5099
|
+
PlatformBindingsRequired: "task.platform-bindings-required",
|
|
5100
|
+
/** The `@heximon/runtime` of the project is older than the command. */
|
|
5101
|
+
RuntimeOutdated: "task.runtime-outdated",
|
|
5102
|
+
/** The app did not boot. The stack is on stderr. */
|
|
5103
|
+
BootFailed: "task.boot-failed",
|
|
5104
|
+
/** The app could not create the task. */
|
|
5105
|
+
CreateFailed: "task.create-failed",
|
|
5106
|
+
/** The frame of the task did not open, or a context seed threw before `run()`. */
|
|
5107
|
+
StartFailed: "task.start-failed",
|
|
5108
|
+
/** The task returned a value that is not JSON. */
|
|
5109
|
+
ResultNotJson: "task.result-not-json",
|
|
5110
|
+
/** The app did not shut down cleanly after the run. */
|
|
5111
|
+
ShutdownFailed: "task.shutdown-failed"
|
|
3488
5112
|
};
|
|
3489
5113
|
//#endregion
|
|
3490
5114
|
//#region src/task-target-resolver.ts
|
|
3491
5115
|
/** Thrown when the names do not pick exactly one module and one task. The message is ready to print. */
|
|
3492
5116
|
var TaskTargetError = class extends Error {
|
|
3493
|
-
|
|
5117
|
+
code;
|
|
5118
|
+
/**
|
|
5119
|
+
* @param code - {@link TaskDiagnosticCode.TargetNotFound} or {@link TaskDiagnosticCode.TargetAmbiguous}.
|
|
5120
|
+
* @param message - The reason, with the names that come close.
|
|
5121
|
+
*/
|
|
5122
|
+
constructor(code, message) {
|
|
3494
5123
|
super(message);
|
|
5124
|
+
this.code = code;
|
|
3495
5125
|
this.name = "TaskTargetError";
|
|
3496
5126
|
}
|
|
3497
5127
|
};
|
|
@@ -3517,16 +5147,16 @@ var TaskTargetResolver = class TaskTargetResolver {
|
|
|
3517
5147
|
static resolve(graph, moduleName, taskName, boundaries = []) {
|
|
3518
5148
|
const moduleReferences = [...graph.modules.map((node) => node.reference), ...boundaries.map((boundary) => boundary.reference)];
|
|
3519
5149
|
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"));
|
|
5150
|
+
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"));
|
|
5151
|
+
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
5152
|
const module = modules[0];
|
|
3523
5153
|
const nodes = graph.modules.filter((node) => TaskTargetResolver.isSame(node.reference, module));
|
|
3524
5154
|
const libraries = boundaries.filter((boundary) => TaskTargetResolver.isSame(boundary.reference, module));
|
|
3525
5155
|
const isLibrary = nodes.length === 0 && libraries.length > 0;
|
|
3526
5156
|
const candidates = isLibrary ? libraries.flatMap((boundary) => boundary.exports.map((exported) => exported.token)) : nodes.flatMap((node) => node.providers.map((provider) => provider.token));
|
|
3527
5157
|
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"));
|
|
5158
|
+
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}`));
|
|
5159
|
+
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
5160
|
return {
|
|
3531
5161
|
module,
|
|
3532
5162
|
task: tasks[0]
|
|
@@ -3687,8 +5317,25 @@ var RefusingInternalClient = class {
|
|
|
3687
5317
|
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
5318
|
}
|
|
3689
5319
|
};
|
|
3690
|
-
/** The failure a run reports to the caller; the message is ready to print. */
|
|
3691
|
-
var TaskRunFailure = class extends Error {
|
|
5320
|
+
/** The failure a run reports to the caller; the message is ready to print on stderr. */
|
|
5321
|
+
var TaskRunFailure = class extends Error {
|
|
5322
|
+
code;
|
|
5323
|
+
/** The one-paragraph text of the `--json` diagnostic: the message without a stack trace. */
|
|
5324
|
+
summary;
|
|
5325
|
+
/** How to fix the failure, or where to read more. */
|
|
5326
|
+
hint;
|
|
5327
|
+
/**
|
|
5328
|
+
* @param code - The stable code of the `--json` diagnostic.
|
|
5329
|
+
* @param message - The text for stderr.
|
|
5330
|
+
* @param details - The thrown value, the diagnostic text when it differs from the message, and the hint.
|
|
5331
|
+
*/
|
|
5332
|
+
constructor(code, message, details = {}) {
|
|
5333
|
+
super(message, "cause" in details ? { cause: details.cause } : void 0);
|
|
5334
|
+
this.code = code;
|
|
5335
|
+
this.summary = details.summary ?? message;
|
|
5336
|
+
this.hint = details.hint;
|
|
5337
|
+
}
|
|
5338
|
+
};
|
|
3692
5339
|
/** The task itself threw. The message is ready to print, and the thrown value is the cause. */
|
|
3693
5340
|
var TaskRefusal = class extends TaskRunFailure {};
|
|
3694
5341
|
/**
|
|
@@ -3710,6 +5357,8 @@ var TaskRunner = class TaskRunner {
|
|
|
3710
5357
|
static interruptShutdownMilliseconds = 5e3;
|
|
3711
5358
|
/** The exit code of a run whose task itself threw. Every other failure exits with `1`. */
|
|
3712
5359
|
static refusalExitCode = 2;
|
|
5360
|
+
/** The hint of a diagnostic whose full text, with the stack trace, is on stderr. */
|
|
5361
|
+
static stackHint = "The stack trace is on stderr.";
|
|
3713
5362
|
static wiringFilePattern = /^apps\/.+\.wiring\.js$/;
|
|
3714
5363
|
outputDirectory;
|
|
3715
5364
|
app;
|
|
@@ -3726,6 +5375,10 @@ var TaskRunner = class TaskRunner {
|
|
|
3726
5375
|
stdout = new StdoutRedirect();
|
|
3727
5376
|
/** The result of the task as one line of JSON, printed after the shutdown. */
|
|
3728
5377
|
serialized;
|
|
5378
|
+
/** The diagnostics of the compile, for the `--json` document. */
|
|
5379
|
+
compileDiagnostics = [];
|
|
5380
|
+
/** Why the shutdown failed, when it failed. */
|
|
5381
|
+
shutdownFailure;
|
|
3729
5382
|
disposing;
|
|
3730
5383
|
/** How many package specifiers the run imported, for the file names of {@link ProjectModules.import}. */
|
|
3731
5384
|
packageImports = 0;
|
|
@@ -3733,13 +5386,31 @@ var TaskRunner = class TaskRunner {
|
|
|
3733
5386
|
this.options = options;
|
|
3734
5387
|
}
|
|
3735
5388
|
/**
|
|
3736
|
-
* Run the task.
|
|
5389
|
+
* Run the task and print the text output. Everything the app logs goes to stderr: stdout carries the result
|
|
5390
|
+
* alone.
|
|
3737
5391
|
*
|
|
3738
5392
|
* @returns The process exit code: `0` when the task returned, `2` when the task itself threw (stdout then
|
|
3739
5393
|
* holds one `{"error":{"name":…,"message":…}}` line), `1` for every other outcome. The reason is already
|
|
3740
5394
|
* on stderr.
|
|
3741
5395
|
*/
|
|
3742
5396
|
async run() {
|
|
5397
|
+
let outcome;
|
|
5398
|
+
this.stdout.start();
|
|
5399
|
+
try {
|
|
5400
|
+
outcome = await this.perform();
|
|
5401
|
+
} finally {
|
|
5402
|
+
this.stdout.stop();
|
|
5403
|
+
}
|
|
5404
|
+
if (outcome.line !== void 0) this.stdout.write(`${outcome.line}\n`);
|
|
5405
|
+
return outcome.exitCode;
|
|
5406
|
+
}
|
|
5407
|
+
/**
|
|
5408
|
+
* Run the task without a write to stdout. The caller sends stdout to stderr for the run, because the app
|
|
5409
|
+
* and the host log there; the reason of a failure is already on stderr when this returns.
|
|
5410
|
+
*
|
|
5411
|
+
* @returns The exit code, the text line, and the `--json` document of the run.
|
|
5412
|
+
*/
|
|
5413
|
+
async perform() {
|
|
3743
5414
|
let interrupts = 0;
|
|
3744
5415
|
const interrupt = (signal) => {
|
|
3745
5416
|
interrupts += 1;
|
|
@@ -3748,35 +5419,69 @@ var TaskRunner = class TaskRunner {
|
|
|
3748
5419
|
process.on("SIGINT", interrupt);
|
|
3749
5420
|
process.on("SIGTERM", interrupt);
|
|
3750
5421
|
let exitCode = 0;
|
|
3751
|
-
|
|
5422
|
+
const diagnostics = [];
|
|
5423
|
+
let thrown;
|
|
3752
5424
|
try {
|
|
3753
5425
|
this.outputDirectory = mkdtempSync(join(this.options.rootDir, TaskRunner.outputDirectoryPrefix));
|
|
3754
|
-
this.runtime = await TaskRunner.projectRuntime(this.outputDirectory);
|
|
3755
|
-
const output = await this.compile();
|
|
5426
|
+
this.runtime = await TaskRunner.during(TaskDiagnosticCode.BootFailed, () => TaskRunner.projectRuntime(this.outputDirectory));
|
|
5427
|
+
const output = await TaskRunner.during(TaskDiagnosticCode.CompileFailed, () => this.compile());
|
|
3756
5428
|
const target = this.pickTarget(output);
|
|
3757
5429
|
TaskRunner.refuseBindings(output, this.platformName, this.outputDirectory);
|
|
3758
5430
|
this.materialize(output);
|
|
3759
|
-
await this.boot(output);
|
|
3760
|
-
await this.
|
|
5431
|
+
await TaskRunner.during(TaskDiagnosticCode.BootFailed, () => this.boot(output));
|
|
5432
|
+
await this.runTask(target);
|
|
3761
5433
|
} catch (error) {
|
|
3762
5434
|
TaskRunner.report(error);
|
|
5435
|
+
diagnostics.push(...TaskRunner.diagnosticsOf(error));
|
|
3763
5436
|
if (error instanceof TaskRefusal) {
|
|
3764
5437
|
exitCode = TaskRunner.refusalExitCode;
|
|
3765
|
-
|
|
5438
|
+
thrown = TaskRunner.errorOf(error.cause);
|
|
5439
|
+
this.serialized = JSON.stringify({ error: thrown });
|
|
3766
5440
|
} else exitCode = 1;
|
|
3767
5441
|
} finally {
|
|
3768
5442
|
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
5443
|
process.off("SIGINT", interrupt);
|
|
3772
5444
|
process.off("SIGTERM", interrupt);
|
|
3773
5445
|
}
|
|
3774
|
-
|
|
5446
|
+
if (this.shutdownFailure !== void 0) diagnostics.push(CliDiagnostics.error(TaskDiagnosticCode.ShutdownFailed, this.shutdownFailure));
|
|
5447
|
+
return {
|
|
5448
|
+
exitCode,
|
|
5449
|
+
...this.serialized === void 0 ? {} : { line: this.serialized },
|
|
5450
|
+
document: {
|
|
5451
|
+
ok: exitCode === 0,
|
|
5452
|
+
diagnostics: [...this.compileDiagnostics, ...diagnostics],
|
|
5453
|
+
module: this.options.moduleName,
|
|
5454
|
+
task: this.options.taskName,
|
|
5455
|
+
...thrown === void 0 ? TaskRunner.resultField(this.serialized) : { error: thrown }
|
|
5456
|
+
}
|
|
5457
|
+
};
|
|
5458
|
+
}
|
|
5459
|
+
/**
|
|
5460
|
+
* Run one step of the run. A failure the step does not name itself becomes a {@link TaskRunFailure} with
|
|
5461
|
+
* `code`; its stderr text stays the stack trace of the thrown value.
|
|
5462
|
+
*/
|
|
5463
|
+
static async during(code, step) {
|
|
5464
|
+
try {
|
|
5465
|
+
return await step();
|
|
5466
|
+
} catch (error) {
|
|
5467
|
+
if (error instanceof TaskRunFailure || error instanceof TaskTargetError || error instanceof CommandFailure) throw error;
|
|
5468
|
+
throw new TaskRunFailure(code, TaskRunner.describe(error), {
|
|
5469
|
+
cause: error,
|
|
5470
|
+
summary: TaskRunner.messageOf(error),
|
|
5471
|
+
hint: TaskRunner.stackHint
|
|
5472
|
+
});
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
/** The `result` field of the document, read back from the printed result so both outputs agree. */
|
|
5476
|
+
static resultField(serialized) {
|
|
5477
|
+
if (serialized === void 0) return {};
|
|
5478
|
+
const parsed = JSON.parse(serialized);
|
|
5479
|
+
return typeof parsed === "object" && parsed !== null && "result" in parsed ? { result: parsed.result } : {};
|
|
3775
5480
|
}
|
|
3776
5481
|
/** The `@heximon/runtime` of the project, the copy the generated wiring imports. */
|
|
3777
5482
|
static async projectRuntime(directory) {
|
|
3778
5483
|
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.");
|
|
5484
|
+
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
5485
|
return runtime;
|
|
3781
5486
|
}
|
|
3782
5487
|
/**
|
|
@@ -3796,13 +5501,13 @@ var TaskRunner = class TaskRunner {
|
|
|
3796
5501
|
if (nitro.isNitroHost) {
|
|
3797
5502
|
const sourceDirectories = TaskRunner.sourceDirectoriesOf(nitro, this.options.isSourceExplicit === true ? this.options.sourceDirectories ?? [] : void 0);
|
|
3798
5503
|
const preset = nitro.preset;
|
|
3799
|
-
const host = await NitroHost.open(rootDir, outputDirectory, {
|
|
5504
|
+
const host = await ConfigSidecar.during(rootDir, "production", () => NitroHost.open(rootDir, outputDirectory, {
|
|
3800
5505
|
preset: preset.kind === "literal" ? preset.value : void 0,
|
|
3801
5506
|
platformFromSidecar: preset.kind === "computed",
|
|
3802
5507
|
aliases: this.aliases,
|
|
3803
5508
|
...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.`);
|
|
5509
|
+
}));
|
|
5510
|
+
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
5511
|
const compiled = host.compile(rootDir, outputDirectory, this.options.isSourceExplicit === true ? this.options.sourceDirectories : void 0);
|
|
3807
5512
|
({output, compiler} = compiled);
|
|
3808
5513
|
this.platformName = compiled.platformName;
|
|
@@ -3817,10 +5522,13 @@ var TaskRunner = class TaskRunner {
|
|
|
3817
5522
|
compiler = session.compiler;
|
|
3818
5523
|
}
|
|
3819
5524
|
const errors = output.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
3820
|
-
if (output.diagnostics.length > 0)
|
|
3821
|
-
|
|
5525
|
+
if (output.diagnostics.length > 0) {
|
|
5526
|
+
CompileDiagnostics.renderToStderr(rootDir, output.diagnostics);
|
|
5527
|
+
this.compileDiagnostics = CliDiagnostics.fromCompiler(output.diagnostics);
|
|
5528
|
+
}
|
|
5529
|
+
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
5530
|
const findings = LibraryFactoryPreflight.inspect(compiler.consumedBoundaryPackages(), LibraryFactoryPreflight.productionBuildOptions);
|
|
3823
|
-
if (findings.length > 0) throw new TaskRunFailure(LibraryFactoryPreflight.explain(findings));
|
|
5531
|
+
if (findings.length > 0) throw new TaskRunFailure(TaskDiagnosticCode.LibraryPreflightFailed, LibraryFactoryPreflight.explain(findings));
|
|
3824
5532
|
return output;
|
|
3825
5533
|
}
|
|
3826
5534
|
/**
|
|
@@ -3829,7 +5537,7 @@ var TaskRunner = class TaskRunner {
|
|
|
3829
5537
|
*/
|
|
3830
5538
|
static sourceDirectoriesOf(nitro, explicit) {
|
|
3831
5539
|
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.`);
|
|
5540
|
+
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
5541
|
return {
|
|
3834
5542
|
...nitro.serverDir.kind === "literal" ? { serverDir: nitro.serverDir.value } : {},
|
|
3835
5543
|
...nitro.scanDirs.kind === "literal" ? { scanDirs: nitro.scanDirs.value } : {}
|
|
@@ -3837,13 +5545,13 @@ var TaskRunner = class TaskRunner {
|
|
|
3837
5545
|
}
|
|
3838
5546
|
/** Pick the module and the task from the compiled graph. */
|
|
3839
5547
|
pickTarget(output) {
|
|
3840
|
-
if (output.moduleGraph === void 0) throw new TaskRunFailure("The compile produced no module graph, so the task cannot be found.");
|
|
5548
|
+
if (output.moduleGraph === void 0) throw new TaskRunFailure(TaskDiagnosticCode.AppNotFound, "The compile produced no module graph, so the task cannot be found.");
|
|
3841
5549
|
return TaskTargetResolver.resolve(output.moduleGraph, this.options.moduleName, this.options.taskName, output.boundaryNodes);
|
|
3842
5550
|
}
|
|
3843
5551
|
/** Refuse a graph that needs bindings only its platform has, before anything boots. */
|
|
3844
5552
|
static refuseBindings(output, platformName, outputDirectory) {
|
|
3845
5553
|
const needs = PlatformBindingCensus.collect(output, platformName, outputDirectory);
|
|
3846
|
-
if (needs.length > 0) throw new TaskRunFailure(PlatformBindingCensus.explain(needs, platformName));
|
|
5554
|
+
if (needs.length > 0) throw new TaskRunFailure(TaskDiagnosticCode.PlatformBindingsRequired, PlatformBindingCensus.explain(needs, platformName));
|
|
3847
5555
|
}
|
|
3848
5556
|
/** Write the generated files into the temporary directory. */
|
|
3849
5557
|
materialize(output) {
|
|
@@ -3856,7 +5564,7 @@ var TaskRunner = class TaskRunner {
|
|
|
3856
5564
|
/** Boot the app with no HTTP surface and without the standing work of the deployed app. */
|
|
3857
5565
|
async boot(output) {
|
|
3858
5566
|
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).");
|
|
5567
|
+
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
5568
|
const runtime = this.runtime;
|
|
3861
5569
|
if (TransformBoot.isBootProcess()) TransformBoot.registerSourceResolver(new Map(output.sourceRewrites.map((rewrite) => [rewrite.filePath, rewrite.content])), this.aliases);
|
|
3862
5570
|
runtime.StandingWork.suspend(runtime.StandingWork.kinds().filter((kind) => !(this.options.withOutboxRelay && kind === runtime.StandingWorkKind.OutboxRelay)));
|
|
@@ -3876,17 +5584,17 @@ var TaskRunner = class TaskRunner {
|
|
|
3876
5584
|
this.app = await wiringModule.createApp(bindings);
|
|
3877
5585
|
this.context = await this.app.from(runtime.CoreModule).get(runtime.Context);
|
|
3878
5586
|
}
|
|
3879
|
-
/** Run the task and
|
|
3880
|
-
async
|
|
5587
|
+
/** Run the task and keep its result as one line of JSON. */
|
|
5588
|
+
async runTask(target) {
|
|
3881
5589
|
const runtime = this.runtime;
|
|
3882
5590
|
const moduleClass = await this.importClass(target.module.sourceFile, target.module.className);
|
|
3883
5591
|
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.`);
|
|
5592
|
+
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
5593
|
let task;
|
|
3886
5594
|
try {
|
|
3887
5595
|
task = await this.app.from(moduleClass).get(taskClass);
|
|
3888
5596
|
} catch (error) {
|
|
3889
|
-
throw
|
|
5597
|
+
throw TaskRunner.failureOf(TaskDiagnosticCode.CreateFailed, `The task ${this.options.taskName} could not be created`, error);
|
|
3890
5598
|
}
|
|
3891
5599
|
const event = {
|
|
3892
5600
|
name: this.options.taskName,
|
|
@@ -3899,22 +5607,22 @@ var TaskRunner = class TaskRunner {
|
|
|
3899
5607
|
try {
|
|
3900
5608
|
await seeds?.apply(event, this.context);
|
|
3901
5609
|
} catch (error) {
|
|
3902
|
-
throw
|
|
5610
|
+
throw TaskRunner.failureOf(TaskDiagnosticCode.StartFailed, `The task ${this.options.taskName} did not start`, error);
|
|
3903
5611
|
}
|
|
3904
5612
|
try {
|
|
3905
5613
|
return await task.run(event);
|
|
3906
5614
|
} catch (error) {
|
|
3907
|
-
throw
|
|
5615
|
+
throw TaskRunner.failureOf(TaskDiagnosticCode.TaskFailed, `The task ${this.options.taskName} failed`, error, TaskRefusal);
|
|
3908
5616
|
}
|
|
3909
5617
|
});
|
|
3910
5618
|
} catch (error) {
|
|
3911
5619
|
if (error instanceof TaskRunFailure) throw error;
|
|
3912
|
-
throw
|
|
5620
|
+
throw TaskRunner.failureOf(TaskDiagnosticCode.StartFailed, `The task ${this.options.taskName} did not start`, error);
|
|
3913
5621
|
}
|
|
3914
5622
|
try {
|
|
3915
5623
|
this.serialized = JSON.stringify(outcome);
|
|
3916
5624
|
} catch (error) {
|
|
3917
|
-
throw new TaskRunFailure(`The task ran, but its result is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
5625
|
+
throw new TaskRunFailure(TaskDiagnosticCode.ResultNotJson, `The task ran, but its result is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
3918
5626
|
}
|
|
3919
5627
|
}
|
|
3920
5628
|
/** The seeds the app declares for every background frame, or none. */
|
|
@@ -3934,7 +5642,7 @@ var TaskRunner = class TaskRunner {
|
|
|
3934
5642
|
async importClass(sourceFile, className) {
|
|
3935
5643
|
this.packageImports += 1;
|
|
3936
5644
|
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}.`);
|
|
5645
|
+
if (found === void 0) throw new TaskRunFailure(TaskDiagnosticCode.TargetNotFound, `${sourceFile} does not export a class named ${className}.`);
|
|
3938
5646
|
return found;
|
|
3939
5647
|
}
|
|
3940
5648
|
/**
|
|
@@ -3954,7 +5662,8 @@ var TaskRunner = class TaskRunner {
|
|
|
3954
5662
|
await this.app?.shutdown();
|
|
3955
5663
|
} catch (error) {
|
|
3956
5664
|
clean = false;
|
|
3957
|
-
|
|
5665
|
+
this.shutdownFailure = "The task finished, but shutting the app down failed — a client may still be open: " + TaskRunner.messageOf(error);
|
|
5666
|
+
process.stderr.write(`${this.shutdownFailure}\n`);
|
|
3958
5667
|
}
|
|
3959
5668
|
this.restoreStandingWork?.();
|
|
3960
5669
|
this.restoreStandingWork = void 0;
|
|
@@ -3988,25 +5697,46 @@ var TaskRunner = class TaskRunner {
|
|
|
3988
5697
|
static describe(error) {
|
|
3989
5698
|
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
3990
5699
|
}
|
|
5700
|
+
/** The message of a thrown value: the error message, else its string form. */
|
|
5701
|
+
static messageOf(error) {
|
|
5702
|
+
return error instanceof Error ? error.message : String(error);
|
|
5703
|
+
}
|
|
5704
|
+
/**
|
|
5705
|
+
* A failure that wraps a thrown value: stderr gets the lead and the stack trace, the `--json` diagnostic the
|
|
5706
|
+
* lead and the message.
|
|
5707
|
+
*/
|
|
5708
|
+
static failureOf(code, lead, error, Failure = TaskRunFailure) {
|
|
5709
|
+
return new Failure(code, `${lead}:\n${TaskRunner.describe(error)}`, {
|
|
5710
|
+
cause: error,
|
|
5711
|
+
summary: `${lead}: ${TaskRunner.messageOf(error)}`,
|
|
5712
|
+
hint: TaskRunner.stackHint
|
|
5713
|
+
});
|
|
5714
|
+
}
|
|
3991
5715
|
/**
|
|
3992
|
-
* The
|
|
3993
|
-
* class name when the class leaves the generic `Error` name.
|
|
5716
|
+
* The error of a refused run, as its stdout line `{"error":{"name":…,"message":…}}` names it. The name is the
|
|
5717
|
+
* error name, or the class name when the class leaves the generic `Error` name.
|
|
3994
5718
|
*/
|
|
3995
|
-
static
|
|
3996
|
-
if (!(thrown instanceof Error)) return
|
|
5719
|
+
static errorOf(thrown) {
|
|
5720
|
+
if (!(thrown instanceof Error)) return {
|
|
3997
5721
|
name: "Error",
|
|
3998
5722
|
message: String(thrown)
|
|
3999
|
-
}
|
|
5723
|
+
};
|
|
4000
5724
|
const className = thrown.constructor?.name;
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
name,
|
|
5725
|
+
return {
|
|
5726
|
+
name: thrown.name === "Error" && className !== void 0 && className !== "" ? className : thrown.name,
|
|
4004
5727
|
message: thrown.message
|
|
4005
|
-
}
|
|
5728
|
+
};
|
|
5729
|
+
}
|
|
5730
|
+
/** The `--json` diagnostics of a failure. */
|
|
5731
|
+
static diagnosticsOf(error) {
|
|
5732
|
+
if (error instanceof CommandFailure) return error.diagnostics;
|
|
5733
|
+
if (error instanceof TaskRunFailure) return [CliDiagnostics.error(error.code, error.summary, error.hint === void 0 ? {} : { hint: error.hint })];
|
|
5734
|
+
if (error instanceof TaskTargetError) return [CliDiagnostics.error(error.code, error.message)];
|
|
5735
|
+
return [CliDiagnostics.error(CliDiagnosticCode.UnexpectedError, TaskRunner.messageOf(error), { hint: TaskRunner.stackHint })];
|
|
4006
5736
|
}
|
|
4007
5737
|
/** Print a failure on stderr. */
|
|
4008
5738
|
static report(error) {
|
|
4009
|
-
if (error instanceof TaskRunFailure || error instanceof TaskTargetError) {
|
|
5739
|
+
if (error instanceof TaskRunFailure || error instanceof TaskTargetError || error instanceof CommandFailure) {
|
|
4010
5740
|
process.stderr.write(`${error.message}\n`);
|
|
4011
5741
|
return;
|
|
4012
5742
|
}
|
|
@@ -4092,10 +5822,46 @@ var TaskCommand = class TaskCommand {
|
|
|
4092
5822
|
process.exitCode = exitCode;
|
|
4093
5823
|
setTimeout(() => process.exit(exitCode), TaskCommand.exitGraceMilliseconds).unref();
|
|
4094
5824
|
}
|
|
4095
|
-
/**
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
5825
|
+
/**
|
|
5826
|
+
* Run the command in this process: the transform-enabled child, or a test. Without `--json` stdout gets the
|
|
5827
|
+
* text line of the result; with it, one `--json` document. Sets the exit code.
|
|
5828
|
+
*
|
|
5829
|
+
* @param args - The parsed arguments.
|
|
5830
|
+
* @param rawArgs - The arguments after `task run`, for every `--src`.
|
|
5831
|
+
*/
|
|
5832
|
+
static async runInProcess(args, rawArgs) {
|
|
5833
|
+
const commandOutput = CommandOutput.resolve("task run", args);
|
|
5834
|
+
await commandOutput.run(async () => {
|
|
5835
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
5836
|
+
const parsed = TaskCommand.parsePayload(args.payload);
|
|
5837
|
+
if (!parsed.ok) throw TaskCommand.invalidOption("--payload", parsed.reason);
|
|
5838
|
+
const envFile = args["env-file"];
|
|
5839
|
+
if (envFile !== void 0) try {
|
|
5840
|
+
await TaskCommand.loadEnvironmentFile(resolve(process.cwd(), envFile));
|
|
5841
|
+
} catch (error) {
|
|
5842
|
+
throw TaskCommand.invalidOption("--env-file", error instanceof Error ? error.message : String(error));
|
|
5843
|
+
}
|
|
5844
|
+
const sourceOption = TaskCommand.sourceOption(rawArgs);
|
|
5845
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, sourceOption);
|
|
5846
|
+
const runner = new TaskRunner({
|
|
5847
|
+
rootDir,
|
|
5848
|
+
moduleName: args.module,
|
|
5849
|
+
taskName: args.task,
|
|
5850
|
+
withOutboxRelay: args["with-outbox-relay"] === true,
|
|
5851
|
+
isSourceExplicit: sourceOption !== void 0 && sourceOption.trim() !== "",
|
|
5852
|
+
...parsed.payload === void 0 ? {} : { payload: parsed.payload },
|
|
5853
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
5854
|
+
});
|
|
5855
|
+
if (commandOutput.isJson()) {
|
|
5856
|
+
const outcome = await runner.perform();
|
|
5857
|
+
process.exitCode = outcome.exitCode;
|
|
5858
|
+
commandOutput.emit(outcome.document);
|
|
5859
|
+
} else process.exitCode = await runner.run();
|
|
5860
|
+
});
|
|
5861
|
+
}
|
|
5862
|
+
/** A failure of one option; the text output prints the reason alone, as before. */
|
|
5863
|
+
static invalidOption(option, reason) {
|
|
5864
|
+
return new CommandFailure([CliDiagnostics.error(CliDiagnosticCode.InvalidOptions, reason, { option })]);
|
|
4099
5865
|
}
|
|
4100
5866
|
};
|
|
4101
5867
|
const taskRunCommand = defineCommand({
|
|
@@ -4114,6 +5880,7 @@ const taskRunCommand = defineCommand({
|
|
|
4114
5880
|
description: "The class name of the task.",
|
|
4115
5881
|
required: true
|
|
4116
5882
|
},
|
|
5883
|
+
json: CommandOutput.jsonFlag,
|
|
4117
5884
|
cwd: {
|
|
4118
5885
|
type: "string",
|
|
4119
5886
|
description: "Project root directory.",
|
|
@@ -4139,34 +5906,12 @@ const taskRunCommand = defineCommand({
|
|
|
4139
5906
|
},
|
|
4140
5907
|
async run({ args, rawArgs }) {
|
|
4141
5908
|
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));
|
|
5909
|
+
const commandOutput = CommandOutput.resolve("task run", args);
|
|
5910
|
+
if (await ProjectRoot.checkBeforeRelaunch(args.cwd, commandOutput)) await TransformBoot.relaunch("task run", commandOutput);
|
|
4156
5911
|
return;
|
|
4157
5912
|
}
|
|
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);
|
|
5913
|
+
await TaskCommand.runInProcess(args, rawArgs);
|
|
5914
|
+
TaskCommand.finish(Number(process.exitCode ?? 0));
|
|
4170
5915
|
}
|
|
4171
5916
|
});
|
|
4172
5917
|
/** `heximon task` — run one-off work of the app. */
|
|
@@ -4193,6 +5938,13 @@ const WorkflowBaselineState = {
|
|
|
4193
5938
|
/** No committed snapshot for this workflow — the gate is unarmed until `heximon check --update` runs. */
|
|
4194
5939
|
Unarmed: "unarmed"
|
|
4195
5940
|
};
|
|
5941
|
+
/** The stable codes of the failures `heximon workflows` raises itself, beside the compiler's own. */
|
|
5942
|
+
const WorkflowsDiagnosticCode = {
|
|
5943
|
+
/** The committed `heximon.upgrade.json` exists but cannot be read, so no workflow state is known. */
|
|
5944
|
+
BaselineUnreadable: "workflows.baseline-unreadable",
|
|
5945
|
+
/** The Cloudflare live instances of one workflow could not be listed, so the fleet picture is incomplete. */
|
|
5946
|
+
EnumerationFailed: "workflows.enumeration-failed"
|
|
5947
|
+
};
|
|
4196
5948
|
/**
|
|
4197
5949
|
* Parses, classifies, and renders the durable-workflow upgrade-safety report — pure functions over the emitted
|
|
4198
5950
|
* baseline, the committed baseline, and the (optional) Cloudflare live-fleet inventories.
|
|
@@ -4271,16 +6023,18 @@ var WorkflowStatusReport = class WorkflowStatusReport {
|
|
|
4271
6023
|
return parts.length === 0 ? `${count} — baseline armed & in sync.` : `${count} — ${parts.join(", ")}.`;
|
|
4272
6024
|
}
|
|
4273
6025
|
/**
|
|
4274
|
-
* The `--
|
|
6026
|
+
* The `--json` document: the per-workflow entries with their inventories (`null` when skipped).
|
|
4275
6027
|
*
|
|
4276
6028
|
* @param options - The classified entries, their inventories, and any upgrade-verdict diagnostics the
|
|
4277
6029
|
* 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`.
|
|
6030
|
+
* complete analysis — but it still makes the run report `ok: false`. So does each enumeration error,
|
|
6031
|
+
* which also becomes an error diagnostic.
|
|
4279
6032
|
*/
|
|
4280
6033
|
static toJson(options) {
|
|
4281
6034
|
const upgradeDiagnostics = options.upgradeDiagnostics ?? [];
|
|
6035
|
+
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
6036
|
return {
|
|
4283
|
-
ok:
|
|
6037
|
+
ok: enumerationDiagnostics.length === 0 && upgradeDiagnostics.length === 0,
|
|
4284
6038
|
workflows: options.entries.map((entry) => {
|
|
4285
6039
|
const enumerationError = options.enumerationErrors.get(entry.resourceName);
|
|
4286
6040
|
return {
|
|
@@ -4293,25 +6047,26 @@ var WorkflowStatusReport = class WorkflowStatusReport {
|
|
|
4293
6047
|
enumerated: options.cloudflareSkipped === void 0,
|
|
4294
6048
|
...options.cloudflareSkipped === void 0 ? {} : { skipped: options.cloudflareSkipped }
|
|
4295
6049
|
},
|
|
4296
|
-
|
|
6050
|
+
diagnostics: [...upgradeDiagnostics, ...enumerationDiagnostics]
|
|
4297
6051
|
};
|
|
4298
6052
|
}
|
|
4299
6053
|
/**
|
|
4300
|
-
* The `--
|
|
6054
|
+
* The `--json` document for a run that produced NO per-workflow report — a workflow-free app, a failed
|
|
4301
6055
|
* compile, or an unreadable committed baseline. Same shape as {@link toJson}, so one reader covers both.
|
|
4302
6056
|
*
|
|
4303
|
-
* @param options - Why there is no report, and
|
|
6057
|
+
* @param options - Why there is no report, and the diagnostics of a run that failed. `ok` is `false`
|
|
6058
|
+
* exactly when one of them is an error.
|
|
4304
6059
|
*/
|
|
4305
6060
|
static noReportJson(options) {
|
|
6061
|
+
const diagnostics = options.diagnostics ?? [];
|
|
4306
6062
|
return {
|
|
4307
|
-
ok:
|
|
6063
|
+
ok: !diagnostics.some((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error),
|
|
4308
6064
|
workflows: [],
|
|
4309
6065
|
cloudflare: {
|
|
4310
6066
|
enumerated: false,
|
|
4311
6067
|
skipped: options.reason
|
|
4312
6068
|
},
|
|
4313
|
-
|
|
4314
|
-
...options.diagnostics === void 0 ? {} : { diagnostics: options.diagnostics }
|
|
6069
|
+
diagnostics
|
|
4315
6070
|
};
|
|
4316
6071
|
}
|
|
4317
6072
|
/**
|
|
@@ -4456,9 +6211,10 @@ const workflowsCommand = defineCommand({
|
|
|
4456
6211
|
description: "Project root directory.",
|
|
4457
6212
|
default: "."
|
|
4458
6213
|
},
|
|
6214
|
+
json: CommandOutput.jsonFlag,
|
|
4459
6215
|
format: {
|
|
4460
6216
|
type: "string",
|
|
4461
|
-
description: "Output format: 'pretty' (default) or 'json'.",
|
|
6217
|
+
description: "Output format: 'pretty' (default) or 'json' (the same as --json).",
|
|
4462
6218
|
default: "pretty"
|
|
4463
6219
|
},
|
|
4464
6220
|
env: {
|
|
@@ -4472,91 +6228,87 @@ const workflowsCommand = defineCommand({
|
|
|
4472
6228
|
}
|
|
4473
6229
|
},
|
|
4474
6230
|
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
|
-
|
|
6231
|
+
const commandOutput = CommandOutput.resolve("workflows", args);
|
|
6232
|
+
await commandOutput.run(async () => {
|
|
6233
|
+
const rootDir = ProjectRoot.resolve(args.cwd);
|
|
6234
|
+
const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
|
|
6235
|
+
if (!commandOutput.isJson()) intro(`heximon workflows — ${basename(rootDir)}`);
|
|
6236
|
+
const output = (await ProjectCompileSession.open({
|
|
6237
|
+
rootDir,
|
|
6238
|
+
environment: args.env,
|
|
6239
|
+
...sourceDirectories === void 0 ? {} : { sourceDirectories }
|
|
6240
|
+
})).compile();
|
|
6241
|
+
const errors = output.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
|
|
6242
|
+
const upgradeErrors = errors.filter((diagnostic) => UpgradeBaseline.isBreaking(diagnostic.code));
|
|
6243
|
+
const blockingErrors = errors.filter((diagnostic) => !UpgradeBaseline.isBreaking(diagnostic.code));
|
|
6244
|
+
if (blockingErrors.length > 0) {
|
|
6245
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkflowStatusReport.noReportJson({
|
|
6246
|
+
reason: "the compile failed — no workflow report to enumerate against.",
|
|
6247
|
+
diagnostics: CliDiagnostics.fromCompiler(blockingErrors)
|
|
6248
|
+
}));
|
|
6249
|
+
else {
|
|
6250
|
+
CompileDiagnostics.renderToStderr(rootDir, blockingErrors);
|
|
6251
|
+
outro(`Compile failed — ${blockingErrors.length} error${blockingErrors.length === 1 ? "" : "s"}; no workflow report.`);
|
|
6252
|
+
}
|
|
6253
|
+
process.exitCode = 1;
|
|
6254
|
+
return;
|
|
4497
6255
|
}
|
|
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.");
|
|
6256
|
+
const generated = output.files.find((file) => file.path === WorkflowStatusReport.generatedBaselinePath);
|
|
6257
|
+
const current = generated === void 0 ? void 0 : WorkflowStatusReport.parseGeneratedText(generated.content);
|
|
6258
|
+
if (current === void 0 || current.workflows.length === 0) {
|
|
6259
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkflowStatusReport.noReportJson({ reason: "this app declares no workflows — nothing to enumerate." }));
|
|
6260
|
+
else outro("No workflows in this app.");
|
|
6261
|
+
return;
|
|
4522
6262
|
}
|
|
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
|
|
6263
|
+
const committed = WorkflowsCommand.readCommittedBaseline(rootDir);
|
|
6264
|
+
if (committed.state === CommittedBaselineState.Unreadable) {
|
|
6265
|
+
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.`;
|
|
6266
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkflowStatusReport.noReportJson({
|
|
6267
|
+
reason: "the committed upgrade-safety baseline is unreadable — nothing to compare against.",
|
|
6268
|
+
diagnostics: [CliDiagnostics.error(WorkflowsDiagnosticCode.BaselineUnreadable, message, { hint: "Restore the committed file, for example from git, before you deploy." })]
|
|
4543
6269
|
}));
|
|
4544
|
-
|
|
4545
|
-
|
|
6270
|
+
else {
|
|
6271
|
+
log.error(message);
|
|
6272
|
+
outro("Upgrade-safety baseline unreadable — no workflow report.");
|
|
6273
|
+
}
|
|
6274
|
+
process.exitCode = 1;
|
|
6275
|
+
return;
|
|
4546
6276
|
}
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
6277
|
+
const entries = WorkflowStatusReport.classify(current.workflows, committed.document);
|
|
6278
|
+
const inventories = /* @__PURE__ */ new Map();
|
|
6279
|
+
const enumerationErrors = /* @__PURE__ */ new Map();
|
|
6280
|
+
let cloudflareSkipped;
|
|
6281
|
+
const accountId = process.env["CLOUDFLARE_ACCOUNT_ID"];
|
|
6282
|
+
const apiToken = process.env["CLOUDFLARE_API_TOKEN"];
|
|
6283
|
+
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.";
|
|
6284
|
+
else {
|
|
6285
|
+
const inspector = await CloudflareInspectorLoader.load(rootDir, {
|
|
6286
|
+
accountId,
|
|
6287
|
+
apiToken
|
|
6288
|
+
});
|
|
6289
|
+
if (inspector === void 0) cloudflareSkipped = "@heximon/cloudflare is not installed in this app — Cloudflare live-instance enumeration skipped.";
|
|
6290
|
+
else for (const entry of entries) try {
|
|
6291
|
+
inventories.set(entry.resourceName, await inspector.inspect({
|
|
6292
|
+
workflowName: entry.resourceName,
|
|
6293
|
+
currentFingerprint: entry.fingerprint
|
|
6294
|
+
}));
|
|
6295
|
+
} catch (error) {
|
|
6296
|
+
enumerationErrors.set(entry.resourceName, error instanceof Error ? error.message : String(error));
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
6299
|
+
if (commandOutput.isJson()) commandOutput.emit(WorkflowStatusReport.toJson({
|
|
6300
|
+
entries,
|
|
6301
|
+
inventories,
|
|
6302
|
+
enumerationErrors,
|
|
6303
|
+
cloudflareSkipped,
|
|
6304
|
+
upgradeDiagnostics: CliDiagnostics.fromCompiler(upgradeErrors)
|
|
6305
|
+
}));
|
|
6306
|
+
else {
|
|
6307
|
+
WorkflowsCommand.report(entries, inventories, enumerationErrors, cloudflareSkipped);
|
|
6308
|
+
if (upgradeErrors.length > 0) CompileDiagnostics.renderToStderr(rootDir, upgradeErrors);
|
|
6309
|
+
}
|
|
6310
|
+
if (enumerationErrors.size > 0 || upgradeErrors.length > 0) process.exitCode = 1;
|
|
6311
|
+
});
|
|
4560
6312
|
}
|
|
4561
6313
|
});
|
|
4562
6314
|
/** Output helpers for {@link workflowsCommand}: the committed-baseline read and the pretty report. */
|
|
@@ -4635,6 +6387,7 @@ const main = defineCommand({
|
|
|
4635
6387
|
routes: routesCommand,
|
|
4636
6388
|
graph: graphCommand,
|
|
4637
6389
|
doctor: doctorCommand,
|
|
6390
|
+
freshness: freshnessCommand,
|
|
4638
6391
|
provision: provisionCommand,
|
|
4639
6392
|
migrations: migrationsCommand,
|
|
4640
6393
|
workflows: workflowsCommand,
|
|
@@ -4643,6 +6396,6 @@ const main = defineCommand({
|
|
|
4643
6396
|
}
|
|
4644
6397
|
});
|
|
4645
6398
|
await tab(main);
|
|
4646
|
-
await
|
|
6399
|
+
await CliEntry.run(main);
|
|
4647
6400
|
//#endregion
|
|
4648
6401
|
export {};
|