@codemation/cli 0.0.1
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 +148 -0
- package/bin/codemation.js +24 -0
- package/bin/codemation.ts +5 -0
- package/dist/CliBin-vjSSUDWE.js +2304 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/index.d.ts +23456 -0
- package/dist/index.js +4 -0
- package/package.json +56 -0
- package/src/CliBin.ts +17 -0
- package/src/CliProgramFactory.ts +118 -0
- package/src/Program.ts +157 -0
- package/src/bin.ts +6 -0
- package/src/bootstrap/CodemationCliApplicationSession.ts +60 -0
- package/src/build/ConsumerBuildArtifactsPublisher.ts +77 -0
- package/src/build/ConsumerBuildOptionsParser.ts +26 -0
- package/src/commands/BuildCommand.ts +31 -0
- package/src/commands/DbMigrateCommand.ts +19 -0
- package/src/commands/DevCommand.ts +391 -0
- package/src/commands/ServeWebCommand.ts +72 -0
- package/src/commands/ServeWorkerCommand.ts +40 -0
- package/src/commands/UserCreateCommand.ts +25 -0
- package/src/commands/UserListCommand.ts +59 -0
- package/src/commands/devCommandLifecycle.types.ts +32 -0
- package/src/consumer/ConsumerCliTsconfigPreparation.ts +26 -0
- package/src/consumer/ConsumerEnvLoader.ts +47 -0
- package/src/consumer/ConsumerOutputBuilder.ts +898 -0
- package/src/consumer/Loader.ts +8 -0
- package/src/consumer/consumerBuildOptions.types.ts +12 -0
- package/src/database/ConsumerDatabaseConnectionResolver.ts +18 -0
- package/src/database/DatabaseMigrationsApplyService.ts +76 -0
- package/src/database/HostPackageRootResolver.ts +26 -0
- package/src/database/PrismaMigrateDeployInvoker.ts +24 -0
- package/src/dev/Builder.ts +45 -0
- package/src/dev/ConsumerEnvDotenvFilePredicate.ts +12 -0
- package/src/dev/DevAuthSettingsLoader.ts +27 -0
- package/src/dev/DevBootstrapSummaryFetcher.ts +15 -0
- package/src/dev/DevCliBannerRenderer.ts +106 -0
- package/src/dev/DevConsumerPublishBootstrap.ts +30 -0
- package/src/dev/DevHttpProbe.ts +54 -0
- package/src/dev/DevLock.ts +98 -0
- package/src/dev/DevNextHostEnvironmentBuilder.ts +49 -0
- package/src/dev/DevSessionPortsResolver.ts +23 -0
- package/src/dev/DevSessionServices.ts +29 -0
- package/src/dev/DevSourceRestartCoordinator.ts +48 -0
- package/src/dev/DevSourceWatcher.ts +102 -0
- package/src/dev/DevTrackedProcessTreeKiller.ts +107 -0
- package/src/dev/DevelopmentGatewayNotifier.ts +35 -0
- package/src/dev/Factory.ts +7 -0
- package/src/dev/LoopbackPortAllocator.ts +20 -0
- package/src/dev/Runner.ts +7 -0
- package/src/dev/RuntimeToolEntrypointResolver.ts +47 -0
- package/src/dev/WatchRootsResolver.ts +26 -0
- package/src/index.ts +12 -0
- package/src/path/CliPathResolver.ts +41 -0
- package/src/runtime/ListenPortResolver.ts +35 -0
- package/src/runtime/SourceMapNodeOptions.ts +12 -0
- package/src/runtime/TypeScriptRuntimeConfigurator.ts +8 -0
- package/src/user/CliDatabaseUrlDescriptor.ts +33 -0
- package/src/user/LocalUserCreator.ts +29 -0
- package/src/user/UserAdminCliBootstrap.ts +67 -0
- package/src/user/UserAdminCliOptionsParser.ts +24 -0
- package/src/user/UserAdminConsumerDotenvLoader.ts +24 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,2304 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import process$1 from "node:process";
|
|
3
|
+
import { CodemationConsumerConfigLoader, CodemationPluginDiscovery, WorkflowDiscoveryPathSegmentsComputer, WorkflowModulePathFinder } from "@codemation/host/server";
|
|
4
|
+
import { ServerLoggerFactory, logLevelPolicyFactory } from "@codemation/host/next/server";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { access, copyFile, cp, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { ApiPaths, ApplicationTokens, CodemationApplication, CodemationBootstrapRequest, ListUserAccountsQuery, PrismaClient, UpsertLocalBootstrapUserCommand } from "@codemation/host";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { config, parse } from "dotenv";
|
|
13
|
+
import { watch } from "chokidar";
|
|
14
|
+
import ts from "typescript";
|
|
15
|
+
import { DatabasePersistenceResolver, PrismaMigrationDeployer } from "@codemation/host/persistence";
|
|
16
|
+
import boxen from "boxen";
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import figlet from "figlet";
|
|
19
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
20
|
+
import { createServer } from "node:net";
|
|
21
|
+
import { Command } from "commander";
|
|
22
|
+
|
|
23
|
+
//#region src/build/ConsumerBuildArtifactsPublisher.ts
|
|
24
|
+
var ConsumerBuildArtifactsPublisher = class {
|
|
25
|
+
async publish(snapshot, discoveredPlugins) {
|
|
26
|
+
const pluginEntryPath = await this.writeDiscoveredPluginsOutput(snapshot, discoveredPlugins);
|
|
27
|
+
return await this.writeBuildManifest(snapshot, pluginEntryPath);
|
|
28
|
+
}
|
|
29
|
+
async writeDiscoveredPluginsOutput(snapshot, discoveredPlugins) {
|
|
30
|
+
const outputPath = path.resolve(snapshot.emitOutputRoot, "plugins.js");
|
|
31
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
32
|
+
const outputLines = ["const codemationDiscoveredPlugins = [];", ""];
|
|
33
|
+
discoveredPlugins.forEach((discoveredPlugin, index) => {
|
|
34
|
+
const pluginFileUrl = pathToFileURL(path.resolve(discoveredPlugin.packageRoot, discoveredPlugin.manifest.entry)).href;
|
|
35
|
+
const exportNameAccessor = discoveredPlugin.manifest.exportName ? `pluginModule${index}[${JSON.stringify(discoveredPlugin.manifest.exportName)}]` : `pluginModule${index}.default ?? pluginModule${index}.codemationPlugin`;
|
|
36
|
+
outputLines.push(`const pluginModule${index} = await import(${JSON.stringify(pluginFileUrl)});`);
|
|
37
|
+
outputLines.push(`const pluginValue${index} = ${exportNameAccessor};`);
|
|
38
|
+
outputLines.push(`if (pluginValue${index} && typeof pluginValue${index}.register === "function") {`);
|
|
39
|
+
outputLines.push(` codemationDiscoveredPlugins.push(pluginValue${index});`);
|
|
40
|
+
outputLines.push(`} else if (typeof pluginValue${index} === "function" && pluginValue${index}.prototype && typeof pluginValue${index}.prototype.register === "function") {`);
|
|
41
|
+
outputLines.push(` codemationDiscoveredPlugins.push(new pluginValue${index}());`);
|
|
42
|
+
outputLines.push("}");
|
|
43
|
+
outputLines.push("");
|
|
44
|
+
});
|
|
45
|
+
outputLines.push("export { codemationDiscoveredPlugins };");
|
|
46
|
+
outputLines.push("export default codemationDiscoveredPlugins;");
|
|
47
|
+
outputLines.push("");
|
|
48
|
+
await writeFile(outputPath, outputLines.join("\n"), "utf8");
|
|
49
|
+
return outputPath;
|
|
50
|
+
}
|
|
51
|
+
async writeBuildManifest(snapshot, pluginEntryPath) {
|
|
52
|
+
const manifest = {
|
|
53
|
+
buildVersion: snapshot.buildVersion,
|
|
54
|
+
consumerRoot: snapshot.consumerRoot,
|
|
55
|
+
entryPath: snapshot.outputEntryPath,
|
|
56
|
+
manifestPath: snapshot.manifestPath,
|
|
57
|
+
pluginEntryPath,
|
|
58
|
+
workflowSourcePaths: snapshot.workflowSourcePaths
|
|
59
|
+
};
|
|
60
|
+
await mkdir(path.dirname(snapshot.manifestPath), { recursive: true });
|
|
61
|
+
const temporaryManifestPath = `${snapshot.manifestPath}.${snapshot.buildVersion}.${randomUUID()}.tmp`;
|
|
62
|
+
await writeFile(temporaryManifestPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
63
|
+
await rename(temporaryManifestPath, snapshot.manifestPath);
|
|
64
|
+
return manifest;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/build/ConsumerBuildOptionsParser.ts
|
|
70
|
+
var ConsumerBuildOptionsParser = class {
|
|
71
|
+
parse(args) {
|
|
72
|
+
return {
|
|
73
|
+
sourceMaps: args.noSourceMaps !== true,
|
|
74
|
+
target: this.parseTarget(args.target)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
parseTarget(raw) {
|
|
78
|
+
if (raw === void 0 || raw.trim() === "") return "es2022";
|
|
79
|
+
const normalized = raw.trim();
|
|
80
|
+
if (normalized === "es2020" || normalized === "es2022") return normalized;
|
|
81
|
+
throw new Error(`Invalid --target "${raw}". Use es2020 or es2022.`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/commands/BuildCommand.ts
|
|
87
|
+
var BuildCommand = class {
|
|
88
|
+
constructor(cliLogger, pathResolver, pluginDiscovery, artifactsPublisher, tsRuntime, outputBuilderLoader) {
|
|
89
|
+
this.cliLogger = cliLogger;
|
|
90
|
+
this.pathResolver = pathResolver;
|
|
91
|
+
this.pluginDiscovery = pluginDiscovery;
|
|
92
|
+
this.artifactsPublisher = artifactsPublisher;
|
|
93
|
+
this.tsRuntime = tsRuntime;
|
|
94
|
+
this.outputBuilderLoader = outputBuilderLoader;
|
|
95
|
+
}
|
|
96
|
+
async execute(consumerRoot, buildOptions) {
|
|
97
|
+
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
98
|
+
this.tsRuntime.configure(paths.repoRoot);
|
|
99
|
+
const snapshot = await this.outputBuilderLoader.create(paths.consumerRoot, buildOptions).ensureBuilt();
|
|
100
|
+
const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
|
|
101
|
+
const manifest = await this.artifactsPublisher.publish(snapshot, discoveredPlugins);
|
|
102
|
+
this.cliLogger.info(`Built consumer output: ${snapshot.outputEntryPath}`);
|
|
103
|
+
this.cliLogger.info(`Discovered plugins: ${discoveredPlugins.length}`);
|
|
104
|
+
this.cliLogger.info(`Published build: ${manifest.buildVersion}`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/commands/DbMigrateCommand.ts
|
|
110
|
+
var DbMigrateCommand = class {
|
|
111
|
+
constructor(databaseMigrationsApplyService) {
|
|
112
|
+
this.databaseMigrationsApplyService = databaseMigrationsApplyService;
|
|
113
|
+
}
|
|
114
|
+
async execute(options) {
|
|
115
|
+
await this.databaseMigrationsApplyService.applyForConsumerRequiringPersistence(options.consumerRoot ?? process.cwd(), { configPath: options.configPath });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/commands/DevCommand.ts
|
|
121
|
+
var DevCommand = class {
|
|
122
|
+
require = createRequire(import.meta.url);
|
|
123
|
+
constructor(pathResolver, pluginDiscovery, tsRuntime, devLockFactory, devSourceWatcherFactory, cliLogger, session, databaseMigrationsApplyService, devBootstrapSummaryFetcher, devCliBannerRenderer, devConsumerPublishBootstrap, consumerEnvDotenvFilePredicate, devTrackedProcessTreeKiller) {
|
|
124
|
+
this.pathResolver = pathResolver;
|
|
125
|
+
this.pluginDiscovery = pluginDiscovery;
|
|
126
|
+
this.tsRuntime = tsRuntime;
|
|
127
|
+
this.devLockFactory = devLockFactory;
|
|
128
|
+
this.devSourceWatcherFactory = devSourceWatcherFactory;
|
|
129
|
+
this.cliLogger = cliLogger;
|
|
130
|
+
this.session = session;
|
|
131
|
+
this.databaseMigrationsApplyService = databaseMigrationsApplyService;
|
|
132
|
+
this.devBootstrapSummaryFetcher = devBootstrapSummaryFetcher;
|
|
133
|
+
this.devCliBannerRenderer = devCliBannerRenderer;
|
|
134
|
+
this.devConsumerPublishBootstrap = devConsumerPublishBootstrap;
|
|
135
|
+
this.consumerEnvDotenvFilePredicate = consumerEnvDotenvFilePredicate;
|
|
136
|
+
this.devTrackedProcessTreeKiller = devTrackedProcessTreeKiller;
|
|
137
|
+
}
|
|
138
|
+
async execute(consumerRoot) {
|
|
139
|
+
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
140
|
+
this.devCliBannerRenderer.renderBrandHeader();
|
|
141
|
+
this.tsRuntime.configure(paths.repoRoot);
|
|
142
|
+
await this.databaseMigrationsApplyService.applyForConsumer(paths.consumerRoot);
|
|
143
|
+
await this.devConsumerPublishBootstrap.ensurePublished(paths);
|
|
144
|
+
const devMode = this.resolveDevModeFromEnv();
|
|
145
|
+
const { nextPort, gatewayPort } = await this.session.sessionPorts.resolve({
|
|
146
|
+
devMode,
|
|
147
|
+
portEnv: process$1.env.PORT,
|
|
148
|
+
gatewayPortEnv: process$1.env.CODEMATION_DEV_GATEWAY_HTTP_PORT
|
|
149
|
+
});
|
|
150
|
+
const devLock = this.devLockFactory.create();
|
|
151
|
+
await devLock.acquire({
|
|
152
|
+
consumerRoot: paths.consumerRoot,
|
|
153
|
+
nextPort: devMode === "framework" ? nextPort : gatewayPort
|
|
154
|
+
});
|
|
155
|
+
const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
|
|
156
|
+
const watcher = this.devSourceWatcherFactory.create();
|
|
157
|
+
try {
|
|
158
|
+
const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
|
|
159
|
+
const processState = this.createInitialProcessState();
|
|
160
|
+
const stopPromise = this.wireStopPromise(processState);
|
|
161
|
+
const uiProxyBase = await this.startConsumerUiProxyWhenNeeded(prepared, processState);
|
|
162
|
+
const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
|
|
163
|
+
await this.spawnGatewayChildAndWaitForHealth(prepared, processState, gatewayBaseUrl, uiProxyBase);
|
|
164
|
+
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
165
|
+
const initialSummary = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
166
|
+
if (initialSummary) this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
|
|
167
|
+
this.bindShutdownSignalsToChildProcesses(processState);
|
|
168
|
+
this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
|
|
169
|
+
await this.startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl);
|
|
170
|
+
this.logConsumerDevHintWhenNeeded(devMode, gatewayPort);
|
|
171
|
+
await stopPromise;
|
|
172
|
+
} finally {
|
|
173
|
+
await watcher.stop();
|
|
174
|
+
await devLock.release();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
resolveDevModeFromEnv() {
|
|
178
|
+
return process$1.env.CODEMATION_DEV_MODE === "framework" ? "framework" : "consumer";
|
|
179
|
+
}
|
|
180
|
+
async prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings) {
|
|
181
|
+
const developmentServerToken = this.session.devAuthLoader.resolveDevelopmentServerToken(process$1.env.CODEMATION_DEV_SERVER_TOKEN);
|
|
182
|
+
const gatewayEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
|
|
183
|
+
packageName: "@codemation/dev-gateway",
|
|
184
|
+
repoRoot: paths.repoRoot,
|
|
185
|
+
sourceEntrypoint: "packages/dev-gateway/src/bin.ts"
|
|
186
|
+
});
|
|
187
|
+
const runtimeEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
|
|
188
|
+
packageName: "@codemation/runtime-dev",
|
|
189
|
+
repoRoot: paths.repoRoot,
|
|
190
|
+
sourceEntrypoint: "packages/runtime-dev/src/bin.ts"
|
|
191
|
+
});
|
|
192
|
+
const runtimeWorkingDirectory = paths.repoRoot ?? paths.consumerRoot;
|
|
193
|
+
const consumerEnv = this.session.consumerEnvLoader.load(paths.consumerRoot);
|
|
194
|
+
return {
|
|
195
|
+
paths,
|
|
196
|
+
devMode,
|
|
197
|
+
nextPort,
|
|
198
|
+
gatewayPort,
|
|
199
|
+
authSettings,
|
|
200
|
+
developmentServerToken,
|
|
201
|
+
gatewayEntrypoint,
|
|
202
|
+
runtimeEntrypoint,
|
|
203
|
+
runtimeWorkingDirectory,
|
|
204
|
+
discoveredPluginPackagesJson: JSON.stringify(await this.pluginDiscovery.discover(paths.consumerRoot)),
|
|
205
|
+
consumerEnv
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
createInitialProcessState() {
|
|
209
|
+
return {
|
|
210
|
+
currentGateway: null,
|
|
211
|
+
currentNextHost: null,
|
|
212
|
+
currentUiNext: null,
|
|
213
|
+
stopRequested: false,
|
|
214
|
+
stopResolve: null,
|
|
215
|
+
stopReject: null
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
wireStopPromise(state) {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
state.stopResolve = resolve;
|
|
221
|
+
state.stopReject = reject;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
gatewayBaseHttpUrl(gatewayPort) {
|
|
225
|
+
return `http://127.0.0.1:${gatewayPort}`;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Consumer mode: run `next start` for the host UI and wait until it responds, so the gateway can proxy to it.
|
|
229
|
+
* Framework mode: no separate UI child (Next runs in dev later).
|
|
230
|
+
*/
|
|
231
|
+
async startConsumerUiProxyWhenNeeded(prepared, state) {
|
|
232
|
+
if (prepared.devMode !== "consumer") return "";
|
|
233
|
+
const websocketPort = prepared.gatewayPort;
|
|
234
|
+
const uiPort = await this.session.loopbackPortAllocator.allocate();
|
|
235
|
+
const uiProxyBase = `http://127.0.0.1:${uiPort}`;
|
|
236
|
+
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
237
|
+
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
238
|
+
const consumerOutputManifestPath = path.resolve(prepared.paths.consumerRoot, ".codemation", "output", "current.json");
|
|
239
|
+
state.currentUiNext = spawn("pnpm", [
|
|
240
|
+
"exec",
|
|
241
|
+
"next",
|
|
242
|
+
"start"
|
|
243
|
+
], {
|
|
244
|
+
cwd: nextHostRoot,
|
|
245
|
+
...this.devDetachedChildSpawnOptions(),
|
|
246
|
+
env: {
|
|
247
|
+
...process$1.env,
|
|
248
|
+
...prepared.consumerEnv,
|
|
249
|
+
PORT: String(uiPort),
|
|
250
|
+
CODEMATION_AUTH_CONFIG_JSON: prepared.authSettings.authConfigJson,
|
|
251
|
+
CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
|
|
252
|
+
CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
|
|
253
|
+
CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
|
|
254
|
+
NEXT_PUBLIC_CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
|
|
255
|
+
CODEMATION_WS_PORT: String(websocketPort),
|
|
256
|
+
NEXT_PUBLIC_CODEMATION_WS_PORT: String(websocketPort),
|
|
257
|
+
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
258
|
+
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process$1.env.NODE_OPTIONS),
|
|
259
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
260
|
+
WS_NO_UTF_8_VALIDATE: "1"
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
state.currentUiNext.on("error", (error) => {
|
|
264
|
+
if (!state.stopRequested) {
|
|
265
|
+
state.stopRequested = true;
|
|
266
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
state.currentUiNext.on("exit", (code) => {
|
|
270
|
+
if (state.stopRequested) return;
|
|
271
|
+
state.stopRequested = true;
|
|
272
|
+
if (state.currentGateway?.exitCode === null) this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
273
|
+
state.stopReject?.(/* @__PURE__ */ new Error(`next start (consumer UI) exited unexpectedly with code ${code ?? 0}.`));
|
|
274
|
+
});
|
|
275
|
+
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
|
|
276
|
+
return uiProxyBase;
|
|
277
|
+
}
|
|
278
|
+
async spawnGatewayChildAndWaitForHealth(prepared, state, gatewayBaseUrl, uiProxyBase) {
|
|
279
|
+
const gatewayProcessEnv = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(process$1.env, prepared.consumerEnv);
|
|
280
|
+
state.currentGateway = spawn(prepared.gatewayEntrypoint.command, prepared.gatewayEntrypoint.args, {
|
|
281
|
+
cwd: prepared.runtimeWorkingDirectory,
|
|
282
|
+
...this.devDetachedChildSpawnOptions(),
|
|
283
|
+
env: {
|
|
284
|
+
...gatewayProcessEnv,
|
|
285
|
+
...prepared.gatewayEntrypoint.env,
|
|
286
|
+
CODEMATION_DEV_GATEWAY_HTTP_PORT: String(prepared.gatewayPort),
|
|
287
|
+
CODEMATION_RUNTIME_CHILD_BIN: prepared.runtimeEntrypoint.command,
|
|
288
|
+
CODEMATION_RUNTIME_CHILD_ARGS_JSON: JSON.stringify(prepared.runtimeEntrypoint.args),
|
|
289
|
+
CODEMATION_RUNTIME_CHILD_ENV_JSON: JSON.stringify(prepared.runtimeEntrypoint.env),
|
|
290
|
+
CODEMATION_RUNTIME_CHILD_CWD: prepared.runtimeWorkingDirectory,
|
|
291
|
+
CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
|
|
292
|
+
CODEMATION_DISCOVERED_PLUGIN_PACKAGES_JSON: prepared.discoveredPluginPackagesJson,
|
|
293
|
+
CODEMATION_PREFER_PLUGIN_SOURCE_ENTRY: "true",
|
|
294
|
+
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
295
|
+
CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
|
|
296
|
+
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process$1.env.NODE_OPTIONS),
|
|
297
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
298
|
+
WS_NO_UTF_8_VALIDATE: "1",
|
|
299
|
+
...uiProxyBase.length > 0 ? { CODEMATION_DEV_UI_PROXY_TARGET: uiProxyBase } : {}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
state.currentGateway.on("error", (error) => {
|
|
303
|
+
if (!state.stopRequested) {
|
|
304
|
+
state.stopRequested = true;
|
|
305
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
state.currentGateway.on("exit", (code) => {
|
|
309
|
+
if (state.stopRequested) return;
|
|
310
|
+
state.stopRequested = true;
|
|
311
|
+
state.stopReject?.(/* @__PURE__ */ new Error(`codemation dev-gateway exited unexpectedly with code ${code ?? 0}.`));
|
|
312
|
+
});
|
|
313
|
+
await this.session.devHttpProbe.waitUntilGatewayHealthy(gatewayBaseUrl);
|
|
314
|
+
}
|
|
315
|
+
devDetachedChildSpawnOptions() {
|
|
316
|
+
return process$1.platform === "win32" ? {
|
|
317
|
+
stdio: "inherit",
|
|
318
|
+
detached: true,
|
|
319
|
+
windowsHide: true
|
|
320
|
+
} : {
|
|
321
|
+
stdio: "inherit",
|
|
322
|
+
detached: true
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
bindShutdownSignalsToChildProcesses(state) {
|
|
326
|
+
let shutdownInProgress = false;
|
|
327
|
+
const runShutdown = async () => {
|
|
328
|
+
if (shutdownInProgress) return;
|
|
329
|
+
shutdownInProgress = true;
|
|
330
|
+
state.stopRequested = true;
|
|
331
|
+
process$1.stdout.write("\n[codemation] Stopping..\n");
|
|
332
|
+
const children = [];
|
|
333
|
+
for (const child of [
|
|
334
|
+
state.currentUiNext,
|
|
335
|
+
state.currentNextHost,
|
|
336
|
+
state.currentGateway
|
|
337
|
+
]) if (child && child.exitCode === null && child.signalCode === null) children.push(child);
|
|
338
|
+
await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
|
|
339
|
+
process$1.stdout.write("[codemation] Stopped.\n");
|
|
340
|
+
state.stopResolve?.();
|
|
341
|
+
};
|
|
342
|
+
for (const signal of [
|
|
343
|
+
"SIGINT",
|
|
344
|
+
"SIGTERM",
|
|
345
|
+
"SIGQUIT"
|
|
346
|
+
]) process$1.on(signal, () => {
|
|
347
|
+
runShutdown();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
|
|
352
|
+
*/
|
|
353
|
+
spawnFrameworkNextHostWhenNeeded(prepared, state, gatewayBaseUrl) {
|
|
354
|
+
if (prepared.devMode !== "framework") return;
|
|
355
|
+
const websocketPort = prepared.gatewayPort;
|
|
356
|
+
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
357
|
+
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
358
|
+
const nextHostEnvironment = this.session.nextHostEnvBuilder.build({
|
|
359
|
+
authConfigJson: prepared.authSettings.authConfigJson,
|
|
360
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
361
|
+
developmentServerToken: prepared.developmentServerToken,
|
|
362
|
+
nextPort: prepared.nextPort,
|
|
363
|
+
skipUiAuth: prepared.authSettings.skipUiAuth,
|
|
364
|
+
websocketPort,
|
|
365
|
+
runtimeDevUrl: gatewayBaseUrl
|
|
366
|
+
});
|
|
367
|
+
state.currentNextHost = spawn("pnpm", [
|
|
368
|
+
"exec",
|
|
369
|
+
"next",
|
|
370
|
+
"dev"
|
|
371
|
+
], {
|
|
372
|
+
cwd: nextHostRoot,
|
|
373
|
+
...this.devDetachedChildSpawnOptions(),
|
|
374
|
+
env: nextHostEnvironment
|
|
375
|
+
});
|
|
376
|
+
state.currentNextHost.on("exit", (code) => {
|
|
377
|
+
const normalizedCode = code ?? 0;
|
|
378
|
+
if (state.stopRequested) return;
|
|
379
|
+
if (normalizedCode === 0) {
|
|
380
|
+
state.stopRequested = true;
|
|
381
|
+
if (state.currentGateway?.exitCode === null) this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
382
|
+
state.stopResolve?.();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
state.stopRequested = true;
|
|
386
|
+
state.stopReject?.(/* @__PURE__ */ new Error(`next host exited with code ${normalizedCode}.`));
|
|
387
|
+
});
|
|
388
|
+
state.currentNextHost.on("error", (error) => {
|
|
389
|
+
if (!state.stopRequested) {
|
|
390
|
+
state.stopRequested = true;
|
|
391
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl) {
|
|
396
|
+
await watcher.start({
|
|
397
|
+
roots: this.session.watchRootsResolver.resolve({
|
|
398
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
399
|
+
devMode,
|
|
400
|
+
repoRoot: prepared.paths.repoRoot
|
|
401
|
+
}),
|
|
402
|
+
onChange: async ({ changedPaths }) => {
|
|
403
|
+
if (changedPaths.length > 0 && changedPaths.every((p) => this.consumerEnvDotenvFilePredicate.matches(p))) {
|
|
404
|
+
process$1.stdout.write("\n[codemation] Consumer environment file changed (e.g. .env). Restart the `codemation dev` process so the gateway and runtime pick up updated variables (host `process.env` does not hot-reload).\n");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
process$1.stdout.write("\n[codemation] Source change detected — rebuilding consumer…\n");
|
|
408
|
+
await this.session.sourceRestartCoordinator.runHandshakeAfterSourceChange(gatewayBaseUrl, prepared.developmentServerToken);
|
|
409
|
+
process$1.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
|
|
410
|
+
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
411
|
+
const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
412
|
+
if (json) this.devCliBannerRenderer.renderCompact(json);
|
|
413
|
+
process$1.stdout.write("[codemation] Runtime ready.\n");
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
logConsumerDevHintWhenNeeded(devMode, gatewayPort) {
|
|
418
|
+
if (devMode !== "consumer") return;
|
|
419
|
+
this.cliLogger.info(`codemation dev (consumer): open http://127.0.0.1:${gatewayPort} — requires a built @codemation/next-host (next build). For Next HMR use CODEMATION_DEV_MODE=framework.`);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/commands/ServeWebCommand.ts
|
|
425
|
+
var ServeWebCommand = class {
|
|
426
|
+
require = createRequire(import.meta.url);
|
|
427
|
+
constructor(pathResolver, pluginDiscovery, artifactsPublisher, tsRuntime, sourceMapNodeOptions, outputBuilderLoader, envLoader, listenPortResolver) {
|
|
428
|
+
this.pathResolver = pathResolver;
|
|
429
|
+
this.pluginDiscovery = pluginDiscovery;
|
|
430
|
+
this.artifactsPublisher = artifactsPublisher;
|
|
431
|
+
this.tsRuntime = tsRuntime;
|
|
432
|
+
this.sourceMapNodeOptions = sourceMapNodeOptions;
|
|
433
|
+
this.outputBuilderLoader = outputBuilderLoader;
|
|
434
|
+
this.envLoader = envLoader;
|
|
435
|
+
this.listenPortResolver = listenPortResolver;
|
|
436
|
+
}
|
|
437
|
+
async execute(consumerRoot, buildOptions) {
|
|
438
|
+
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
439
|
+
this.tsRuntime.configure(paths.repoRoot);
|
|
440
|
+
const snapshot = await this.outputBuilderLoader.create(paths.consumerRoot, buildOptions).ensureBuilt();
|
|
441
|
+
const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
|
|
442
|
+
const manifest = await this.artifactsPublisher.publish(snapshot, discoveredPlugins);
|
|
443
|
+
const nextHostRoot = path.dirname(this.require.resolve("@codemation/next-host/package.json"));
|
|
444
|
+
const consumerEnv = this.envLoader.load(paths.consumerRoot);
|
|
445
|
+
const nextPort = this.listenPortResolver.resolvePrimaryApplicationPort(process$1.env.PORT);
|
|
446
|
+
const websocketPort = this.listenPortResolver.resolveWebsocketPortRelativeToHttp({
|
|
447
|
+
nextPort,
|
|
448
|
+
publicWebsocketPort: process$1.env.NEXT_PUBLIC_CODEMATION_WS_PORT,
|
|
449
|
+
websocketPort: process$1.env.CODEMATION_WS_PORT
|
|
450
|
+
});
|
|
451
|
+
const child = spawn("pnpm", [
|
|
452
|
+
"exec",
|
|
453
|
+
"next",
|
|
454
|
+
"start"
|
|
455
|
+
], {
|
|
456
|
+
cwd: nextHostRoot,
|
|
457
|
+
stdio: "inherit",
|
|
458
|
+
env: {
|
|
459
|
+
...process$1.env,
|
|
460
|
+
...consumerEnv,
|
|
461
|
+
PORT: String(nextPort),
|
|
462
|
+
CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifest.manifestPath,
|
|
463
|
+
CODEMATION_CONSUMER_ROOT: paths.consumerRoot,
|
|
464
|
+
CODEMATION_WS_PORT: String(websocketPort),
|
|
465
|
+
NEXT_PUBLIC_CODEMATION_WS_PORT: String(websocketPort),
|
|
466
|
+
NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process$1.env.NODE_OPTIONS),
|
|
467
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
468
|
+
WS_NO_UTF_8_VALIDATE: "1"
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
await new Promise((resolve, reject) => {
|
|
472
|
+
child.on("exit", (code) => {
|
|
473
|
+
if ((code ?? 0) === 0) {
|
|
474
|
+
resolve();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
reject(/* @__PURE__ */ new Error(`next start exited with code ${code ?? 0}.`));
|
|
478
|
+
});
|
|
479
|
+
child.on("error", reject);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
//#endregion
|
|
485
|
+
//#region src/commands/ServeWorkerCommand.ts
|
|
486
|
+
var ServeWorkerCommand = class {
|
|
487
|
+
require = createRequire(import.meta.url);
|
|
488
|
+
constructor(sourceMapNodeOptions) {
|
|
489
|
+
this.sourceMapNodeOptions = sourceMapNodeOptions;
|
|
490
|
+
}
|
|
491
|
+
async execute(consumerRoot, configPathOverride) {
|
|
492
|
+
const workerPackageRoot = path.dirname(this.require.resolve("@codemation/worker-cli/package.json"));
|
|
493
|
+
const args = [path.join(workerPackageRoot, "bin", "codemation-worker.js")];
|
|
494
|
+
if (configPathOverride !== void 0 && configPathOverride.trim().length > 0) args.push("--config", path.resolve(process$1.cwd(), configPathOverride.trim()));
|
|
495
|
+
args.push("--consumer-root", consumerRoot);
|
|
496
|
+
const child = spawn(process$1.execPath, args, {
|
|
497
|
+
cwd: consumerRoot,
|
|
498
|
+
stdio: "inherit",
|
|
499
|
+
env: {
|
|
500
|
+
...process$1.env,
|
|
501
|
+
NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process$1.env.NODE_OPTIONS)
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
await new Promise((resolve, reject) => {
|
|
505
|
+
child.on("exit", (code) => {
|
|
506
|
+
if ((code ?? 0) === 0) {
|
|
507
|
+
resolve();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
reject(/* @__PURE__ */ new Error(`codemation-worker exited with code ${code ?? 0}.`));
|
|
511
|
+
});
|
|
512
|
+
child.on("error", reject);
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/commands/UserCreateCommand.ts
|
|
519
|
+
var UserCreateCommand = class {
|
|
520
|
+
constructor(localUserCreator, userAdminCliOptionsParser) {
|
|
521
|
+
this.localUserCreator = localUserCreator;
|
|
522
|
+
this.userAdminCliOptionsParser = userAdminCliOptionsParser;
|
|
523
|
+
}
|
|
524
|
+
async execute(opts) {
|
|
525
|
+
const options = {
|
|
526
|
+
...this.userAdminCliOptionsParser.parse(opts),
|
|
527
|
+
email: opts.email,
|
|
528
|
+
password: opts.password
|
|
529
|
+
};
|
|
530
|
+
await this.localUserCreator.run(options);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/commands/UserListCommand.ts
|
|
536
|
+
var UserListCommand = class {
|
|
537
|
+
constructor(cliLogger, userAdminBootstrap, databaseUrlDescriptor, userAdminCliOptionsParser) {
|
|
538
|
+
this.cliLogger = cliLogger;
|
|
539
|
+
this.userAdminBootstrap = userAdminBootstrap;
|
|
540
|
+
this.databaseUrlDescriptor = databaseUrlDescriptor;
|
|
541
|
+
this.userAdminCliOptionsParser = userAdminCliOptionsParser;
|
|
542
|
+
}
|
|
543
|
+
async execute(opts) {
|
|
544
|
+
await this.userAdminBootstrap.withSession(this.userAdminCliOptionsParser.parse(opts), async (session) => {
|
|
545
|
+
const where = this.databaseUrlDescriptor.describeForDisplay(process.env.DATABASE_URL);
|
|
546
|
+
const users = await session.getQueryBus().execute(new ListUserAccountsQuery());
|
|
547
|
+
if (users.length === 0) {
|
|
548
|
+
this.cliLogger.info(`No users found (${where}). If this is the wrong database, fix DATABASE_URL or CodemationConfig.runtime.database.url.`);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
this.cliLogger.info(`${where}\n${this.formatUserAccountsTable(users)}`);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
formatUserAccountsTable(users) {
|
|
555
|
+
const headers = [
|
|
556
|
+
"Email",
|
|
557
|
+
"Status",
|
|
558
|
+
"Id",
|
|
559
|
+
"Login methods"
|
|
560
|
+
];
|
|
561
|
+
const rows = users.map((user) => [
|
|
562
|
+
user.email,
|
|
563
|
+
user.status,
|
|
564
|
+
user.id,
|
|
565
|
+
user.loginMethods.length > 0 ? user.loginMethods.join(", ") : "—"
|
|
566
|
+
]);
|
|
567
|
+
const columnCount = headers.length;
|
|
568
|
+
const widths = [];
|
|
569
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
570
|
+
const headerWidth = headers[columnIndex].length;
|
|
571
|
+
const cellWidths = rows.map((row) => row[columnIndex].length);
|
|
572
|
+
widths.push(Math.max(headerWidth, ...cellWidths, 3));
|
|
573
|
+
}
|
|
574
|
+
const padCell = (text, columnIndex) => text.padEnd(widths[columnIndex] ?? text.length);
|
|
575
|
+
const horizontal = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
|
|
576
|
+
const formatRow = (cells) => `| ${cells.map((cell, index) => padCell(cell, index)).join(" | ")} |`;
|
|
577
|
+
return [
|
|
578
|
+
horizontal,
|
|
579
|
+
formatRow([...headers]),
|
|
580
|
+
horizontal,
|
|
581
|
+
...rows.map((row) => formatRow([...row])),
|
|
582
|
+
horizontal
|
|
583
|
+
].join("\n");
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/consumer/ConsumerCliTsconfigPreparation.ts
|
|
589
|
+
/**
|
|
590
|
+
* Ensures `CODEMATION_TSCONFIG_PATH` points at the repo's `tsconfig.codemation-tsx.json` when present,
|
|
591
|
+
* so tsx can load consumer `codemation.config.ts` files that import decorator-using workspace packages.
|
|
592
|
+
*/
|
|
593
|
+
var ConsumerCliTsconfigPreparation = class {
|
|
594
|
+
applyWorkspaceTsconfigForTsxIfPresent(consumerRoot) {
|
|
595
|
+
if (process.env.CODEMATION_TSCONFIG_PATH && process.env.CODEMATION_TSCONFIG_PATH.trim().length > 0) return;
|
|
596
|
+
const resolvedRoot = path.resolve(consumerRoot);
|
|
597
|
+
const candidates = [
|
|
598
|
+
path.resolve(resolvedRoot, "tsconfig.codemation-tsx.json"),
|
|
599
|
+
path.resolve(resolvedRoot, "..", "tsconfig.codemation-tsx.json"),
|
|
600
|
+
path.resolve(resolvedRoot, "..", "..", "tsconfig.codemation-tsx.json")
|
|
601
|
+
];
|
|
602
|
+
for (const candidate of candidates) if (existsSync(candidate)) {
|
|
603
|
+
process.env.CODEMATION_TSCONFIG_PATH = candidate;
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/consumer/ConsumerEnvLoader.ts
|
|
611
|
+
/**
|
|
612
|
+
* Loads the consumer project's dotenv files so `codemation dev` can forward them to the Next host.
|
|
613
|
+
* Next.js runs from `packages/next-host` and does not read `apps/<consumer>/.env` automatically.
|
|
614
|
+
*/
|
|
615
|
+
var ConsumerEnvLoader = class {
|
|
616
|
+
load(consumerRoot) {
|
|
617
|
+
const merged = {};
|
|
618
|
+
for (const relativeName of [".env", ".env.local"]) {
|
|
619
|
+
const absolutePath = path.resolve(consumerRoot, relativeName);
|
|
620
|
+
if (!existsSync(absolutePath)) continue;
|
|
621
|
+
const parsed = parse(readFileSync(absolutePath, "utf8"));
|
|
622
|
+
for (const [key, value] of Object.entries(parsed)) if (value !== void 0) merged[key] = value;
|
|
623
|
+
}
|
|
624
|
+
return merged;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Merges consumer `.env` / `.env.local` values into a process environment snapshot.
|
|
628
|
+
* Consumer keys override the base snapshot for most variables. `DATABASE_URL` and `AUTH_SECRET`
|
|
629
|
+
* prefer the base (shell) when set, matching the dev Next host spawn behavior.
|
|
630
|
+
*/
|
|
631
|
+
mergeIntoProcessEnvironment(processEnv, consumerEnv) {
|
|
632
|
+
return {
|
|
633
|
+
...processEnv,
|
|
634
|
+
...consumerEnv,
|
|
635
|
+
DATABASE_URL: processEnv.DATABASE_URL ?? consumerEnv.DATABASE_URL,
|
|
636
|
+
AUTH_SECRET: processEnv.AUTH_SECRET ?? consumerEnv.AUTH_SECRET
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
mergeConsumerRootIntoProcessEnvironment(consumerRoot, processEnv) {
|
|
640
|
+
return this.mergeIntoProcessEnvironment(processEnv, this.load(consumerRoot));
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/consumer/ConsumerOutputBuilder.ts
|
|
646
|
+
const defaultConsumerOutputLogger = new ServerLoggerFactory(logLevelPolicyFactory).create("codemation-cli.consumer-output");
|
|
647
|
+
const defaultConsumerBuildOptions = Object.freeze({
|
|
648
|
+
sourceMaps: true,
|
|
649
|
+
target: "es2022"
|
|
650
|
+
});
|
|
651
|
+
var ConsumerOutputBuilder = class ConsumerOutputBuilder {
|
|
652
|
+
static ignoredDirectoryNames = new Set([
|
|
653
|
+
".codemation",
|
|
654
|
+
".git",
|
|
655
|
+
"dist",
|
|
656
|
+
"node_modules"
|
|
657
|
+
]);
|
|
658
|
+
static supportedSourceExtensions = new Set([
|
|
659
|
+
".ts",
|
|
660
|
+
".tsx",
|
|
661
|
+
".mts",
|
|
662
|
+
".cts"
|
|
663
|
+
]);
|
|
664
|
+
static watchBuildDebounceMs = 75;
|
|
665
|
+
workflowModulePathFinder = new WorkflowModulePathFinder();
|
|
666
|
+
/** Last promoted build output used to copy-forward unchanged emitted files on incremental watch builds. */
|
|
667
|
+
lastPromotedSnapshot = null;
|
|
668
|
+
pendingWatchEvents = [];
|
|
669
|
+
activeBuildPromise = null;
|
|
670
|
+
watcher = null;
|
|
671
|
+
watchBuildLoopPromise = null;
|
|
672
|
+
watchBuildDebounceTimeout = null;
|
|
673
|
+
hasQueuedWatchEvent = false;
|
|
674
|
+
hasPendingWatchBuild = false;
|
|
675
|
+
lastIssuedBuildVersion = 0;
|
|
676
|
+
log;
|
|
677
|
+
buildOptions;
|
|
678
|
+
constructor(consumerRoot, logOverride, buildOptionsOverride) {
|
|
679
|
+
this.consumerRoot = consumerRoot;
|
|
680
|
+
this.log = logOverride ?? defaultConsumerOutputLogger;
|
|
681
|
+
this.buildOptions = buildOptionsOverride ?? defaultConsumerBuildOptions;
|
|
682
|
+
}
|
|
683
|
+
async ensureBuilt() {
|
|
684
|
+
if (!this.activeBuildPromise) this.activeBuildPromise = this.buildInternal();
|
|
685
|
+
return await this.activeBuildPromise;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Stops the chokidar watcher and clears debounce timers. Safe to call when not watching.
|
|
689
|
+
* Used by tests and for clean shutdown when tearing down a dev session.
|
|
690
|
+
*/
|
|
691
|
+
async disposeWatching() {
|
|
692
|
+
if (this.watchBuildDebounceTimeout) {
|
|
693
|
+
clearTimeout(this.watchBuildDebounceTimeout);
|
|
694
|
+
this.watchBuildDebounceTimeout = null;
|
|
695
|
+
}
|
|
696
|
+
if (this.watcher) {
|
|
697
|
+
await this.watcher.close();
|
|
698
|
+
this.watcher = null;
|
|
699
|
+
}
|
|
700
|
+
this.watchBuildLoopPromise = null;
|
|
701
|
+
}
|
|
702
|
+
async ensureWatching(args) {
|
|
703
|
+
if (this.watcher) return;
|
|
704
|
+
this.watcher = watch([this.consumerRoot], {
|
|
705
|
+
ignoreInitial: true,
|
|
706
|
+
ignored: this.createIgnoredMatcher()
|
|
707
|
+
});
|
|
708
|
+
this.watcher.on("all", (eventName, rawPath) => {
|
|
709
|
+
if (typeof rawPath === "string" && rawPath.length > 0) this.pendingWatchEvents.push({
|
|
710
|
+
event: eventName,
|
|
711
|
+
path: path.resolve(rawPath)
|
|
712
|
+
});
|
|
713
|
+
this.scheduleWatchBuild(args);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
async flushWatchBuilds(args) {
|
|
717
|
+
try {
|
|
718
|
+
while (this.hasPendingWatchBuild) {
|
|
719
|
+
this.hasPendingWatchBuild = false;
|
|
720
|
+
if (args.onBuildStarted) await args.onBuildStarted();
|
|
721
|
+
try {
|
|
722
|
+
const watchEvents = this.takePendingWatchEvents();
|
|
723
|
+
this.activeBuildPromise = this.buildInternal({ watchEvents });
|
|
724
|
+
await args.onBuildCompleted(await this.activeBuildPromise);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
const exception = error instanceof Error ? error : new Error(String(error));
|
|
727
|
+
if (args.onBuildFailed && !this.hasPendingWatchBuild && !this.hasQueuedWatchEvent) await args.onBuildFailed(exception);
|
|
728
|
+
this.log.error("consumer output rebuild failed", exception);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} finally {
|
|
732
|
+
this.watchBuildLoopPromise = null;
|
|
733
|
+
if (this.hasPendingWatchBuild) this.watchBuildLoopPromise = this.flushWatchBuilds(args);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
takePendingWatchEvents() {
|
|
737
|
+
const events = [...this.pendingWatchEvents];
|
|
738
|
+
this.pendingWatchEvents = [];
|
|
739
|
+
return events;
|
|
740
|
+
}
|
|
741
|
+
async buildInternal(options) {
|
|
742
|
+
const watchEvents = options?.watchEvents ?? [];
|
|
743
|
+
const configSourcePath = await this.resolveConfigPath(this.consumerRoot);
|
|
744
|
+
if (watchEvents.length > 0 && this.lastPromotedSnapshot !== null && configSourcePath !== null && !this.requiresFullConsumerRebuild(watchEvents, configSourcePath)) {
|
|
745
|
+
const changedSourcePaths = this.resolveIncrementalEmitSourcePaths(watchEvents);
|
|
746
|
+
if (changedSourcePaths.length > 0) try {
|
|
747
|
+
await access(this.lastPromotedSnapshot.emitOutputRoot);
|
|
748
|
+
const snapshot$1 = await this.buildInternalIncremental(changedSourcePaths);
|
|
749
|
+
this.lastPromotedSnapshot = snapshot$1;
|
|
750
|
+
return snapshot$1;
|
|
751
|
+
} catch {}
|
|
752
|
+
}
|
|
753
|
+
const snapshot = await this.buildInternalFull();
|
|
754
|
+
this.lastPromotedSnapshot = snapshot;
|
|
755
|
+
return snapshot;
|
|
756
|
+
}
|
|
757
|
+
requiresFullConsumerRebuild(events, configSourcePath) {
|
|
758
|
+
const resolvedConfig = path.resolve(configSourcePath);
|
|
759
|
+
for (const entry of events) {
|
|
760
|
+
if (entry.event !== "change") return true;
|
|
761
|
+
if (path.resolve(entry.path) === resolvedConfig) return true;
|
|
762
|
+
if (this.shouldCopyAssetPath(entry.path)) return true;
|
|
763
|
+
}
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
resolveIncrementalEmitSourcePaths(events) {
|
|
767
|
+
const uniquePaths = /* @__PURE__ */ new Set();
|
|
768
|
+
for (const entry of events) {
|
|
769
|
+
if (entry.event !== "change") return [];
|
|
770
|
+
if (this.shouldCopyAssetPath(entry.path)) return [];
|
|
771
|
+
if (!this.shouldEmitSourcePath(entry.path)) continue;
|
|
772
|
+
uniquePaths.add(path.resolve(entry.path));
|
|
773
|
+
}
|
|
774
|
+
return [...uniquePaths];
|
|
775
|
+
}
|
|
776
|
+
async prepareEmitBuildSnapshot(args) {
|
|
777
|
+
const outputRoot = this.resolveOutputRoot();
|
|
778
|
+
const finalBuildRoot = this.resolveFinalBuildOutputRoot();
|
|
779
|
+
const stagingBuildRoot = this.resolveStagingBuildRoot(args.buildVersion);
|
|
780
|
+
const outputAppRoot = path.resolve(stagingBuildRoot, "app");
|
|
781
|
+
const configMetadata = await this.loadConfigMetadata(args.configSourcePath);
|
|
782
|
+
const workflowSourcePaths = await this.resolveWorkflowSources(this.consumerRoot, configMetadata);
|
|
783
|
+
const pathSegmentsComputer = new WorkflowDiscoveryPathSegmentsComputer();
|
|
784
|
+
const workflowDiscoveryPathSegmentsList = workflowSourcePaths.map((sourcePath) => {
|
|
785
|
+
return pathSegmentsComputer.compute({
|
|
786
|
+
consumerRoot: this.consumerRoot,
|
|
787
|
+
workflowDiscoveryDirectories: configMetadata.workflowDiscoveryDirectories,
|
|
788
|
+
absoluteWorkflowModulePath: sourcePath
|
|
789
|
+
}) ?? [];
|
|
790
|
+
});
|
|
791
|
+
return {
|
|
792
|
+
stagedSnapshot: {
|
|
793
|
+
buildVersion: args.buildVersion,
|
|
794
|
+
configSourcePath: args.configSourcePath,
|
|
795
|
+
consumerRoot: this.consumerRoot,
|
|
796
|
+
manifestPath: this.resolveCurrentManifestPath(),
|
|
797
|
+
outputEntryPath: path.resolve(stagingBuildRoot, "index.js"),
|
|
798
|
+
outputRoot,
|
|
799
|
+
emitOutputRoot: stagingBuildRoot,
|
|
800
|
+
workflowSourcePaths,
|
|
801
|
+
workflowDiscoveryPathSegmentsList
|
|
802
|
+
},
|
|
803
|
+
outputAppRoot,
|
|
804
|
+
finalBuildRoot,
|
|
805
|
+
stagingBuildRoot
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
async emitStagingBuildAndPromote(args) {
|
|
809
|
+
let promoted = false;
|
|
810
|
+
try {
|
|
811
|
+
await args.emitOutputFiles();
|
|
812
|
+
await this.writeEntryFile({
|
|
813
|
+
configSourcePath: args.configSourcePath,
|
|
814
|
+
outputAppRoot: args.outputAppRoot,
|
|
815
|
+
snapshot: args.stagedSnapshot
|
|
816
|
+
});
|
|
817
|
+
await this.promoteStagingToFinalBuild({
|
|
818
|
+
finalBuildRoot: args.finalBuildRoot,
|
|
819
|
+
stagingBuildRoot: args.stagingBuildRoot
|
|
820
|
+
});
|
|
821
|
+
promoted = true;
|
|
822
|
+
return {
|
|
823
|
+
...args.stagedSnapshot,
|
|
824
|
+
outputEntryPath: path.resolve(args.finalBuildRoot, "index.js"),
|
|
825
|
+
emitOutputRoot: args.finalBuildRoot
|
|
826
|
+
};
|
|
827
|
+
} finally {
|
|
828
|
+
if (!promoted) await rm(args.stagingBuildRoot, {
|
|
829
|
+
force: true,
|
|
830
|
+
recursive: true
|
|
831
|
+
}).catch(() => null);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async buildInternalIncremental(changedSourcePaths) {
|
|
835
|
+
const previous = this.lastPromotedSnapshot;
|
|
836
|
+
if (!previous) throw new Error("Incremental consumer build requires a previous successful build output.");
|
|
837
|
+
const configSourcePath = await this.resolveConfigPath(this.consumerRoot);
|
|
838
|
+
if (!configSourcePath) throw new Error("Codemation config not found. Expected \"codemation.config.ts\" in the consumer project root or \"src/\".");
|
|
839
|
+
const runtimeSourcePaths = await this.collectRuntimeSourcePaths();
|
|
840
|
+
const runtimeSourceSet = new Set(runtimeSourcePaths.map((sourcePath) => path.resolve(sourcePath)));
|
|
841
|
+
for (const changedPath of changedSourcePaths) if (!runtimeSourceSet.has(path.resolve(changedPath))) throw new Error("Incremental build saw a changed path outside the current runtime source set; rebuild full.");
|
|
842
|
+
const buildVersion = this.createBuildVersion();
|
|
843
|
+
const { stagedSnapshot, outputAppRoot, finalBuildRoot, stagingBuildRoot } = await this.prepareEmitBuildSnapshot({
|
|
844
|
+
configSourcePath,
|
|
845
|
+
buildVersion
|
|
846
|
+
});
|
|
847
|
+
return await this.emitStagingBuildAndPromote({
|
|
848
|
+
configSourcePath,
|
|
849
|
+
stagedSnapshot,
|
|
850
|
+
outputAppRoot,
|
|
851
|
+
finalBuildRoot,
|
|
852
|
+
stagingBuildRoot,
|
|
853
|
+
emitOutputFiles: async () => {
|
|
854
|
+
await cp(previous.emitOutputRoot, stagingBuildRoot, { recursive: true });
|
|
855
|
+
for (const sourcePath of changedSourcePaths) await this.emitSourceFile({
|
|
856
|
+
outputAppRoot,
|
|
857
|
+
sourcePath
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
async buildInternalFull() {
|
|
863
|
+
const configSourcePath = await this.resolveConfigPath(this.consumerRoot);
|
|
864
|
+
if (!configSourcePath) throw new Error("Codemation config not found. Expected \"codemation.config.ts\" in the consumer project root or \"src/\".");
|
|
865
|
+
const runtimeSourcePaths = await this.collectRuntimeSourcePaths();
|
|
866
|
+
const buildVersion = this.createBuildVersion();
|
|
867
|
+
const { stagedSnapshot, outputAppRoot, finalBuildRoot, stagingBuildRoot } = await this.prepareEmitBuildSnapshot({
|
|
868
|
+
configSourcePath,
|
|
869
|
+
buildVersion
|
|
870
|
+
});
|
|
871
|
+
return await this.emitStagingBuildAndPromote({
|
|
872
|
+
configSourcePath,
|
|
873
|
+
stagedSnapshot,
|
|
874
|
+
outputAppRoot,
|
|
875
|
+
finalBuildRoot,
|
|
876
|
+
stagingBuildRoot,
|
|
877
|
+
emitOutputFiles: async () => {
|
|
878
|
+
for (const sourcePath of runtimeSourcePaths) {
|
|
879
|
+
if (this.shouldCopyAssetPath(sourcePath)) {
|
|
880
|
+
await this.copyAssetFile({
|
|
881
|
+
outputAppRoot,
|
|
882
|
+
sourcePath
|
|
883
|
+
});
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
await this.emitSourceFile({
|
|
887
|
+
outputAppRoot,
|
|
888
|
+
sourcePath
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
scheduleWatchBuild(args) {
|
|
895
|
+
this.hasQueuedWatchEvent = true;
|
|
896
|
+
if (this.watchBuildDebounceTimeout) clearTimeout(this.watchBuildDebounceTimeout);
|
|
897
|
+
this.watchBuildDebounceTimeout = setTimeout(() => {
|
|
898
|
+
this.watchBuildDebounceTimeout = null;
|
|
899
|
+
this.hasQueuedWatchEvent = false;
|
|
900
|
+
this.hasPendingWatchBuild = true;
|
|
901
|
+
if (!this.watchBuildLoopPromise) this.watchBuildLoopPromise = this.flushWatchBuilds(args);
|
|
902
|
+
}, ConsumerOutputBuilder.watchBuildDebounceMs);
|
|
903
|
+
}
|
|
904
|
+
async collectRuntimeSourcePaths() {
|
|
905
|
+
const sourcePaths = [];
|
|
906
|
+
await this.collectSourcePathsRecursively(this.consumerRoot, sourcePaths);
|
|
907
|
+
return sourcePaths.filter((sourcePath) => this.shouldEmitSourcePath(sourcePath)).sort((left, right) => left.localeCompare(right));
|
|
908
|
+
}
|
|
909
|
+
async collectSourcePathsRecursively(directoryPath, sourcePaths) {
|
|
910
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
911
|
+
for (const entry of entries) {
|
|
912
|
+
const entryPath = path.resolve(directoryPath, entry.name);
|
|
913
|
+
if (entry.isDirectory()) {
|
|
914
|
+
if (ConsumerOutputBuilder.ignoredDirectoryNames.has(entry.name)) continue;
|
|
915
|
+
await this.collectSourcePathsRecursively(entryPath, sourcePaths);
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
sourcePaths.push(entryPath);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
shouldEmitSourcePath(sourcePath) {
|
|
922
|
+
if (this.shouldCopyAssetPath(sourcePath)) return true;
|
|
923
|
+
if (sourcePath.endsWith(".d.ts")) return false;
|
|
924
|
+
const extension = path.extname(sourcePath);
|
|
925
|
+
return ConsumerOutputBuilder.supportedSourceExtensions.has(extension);
|
|
926
|
+
}
|
|
927
|
+
shouldCopyAssetPath(sourcePath) {
|
|
928
|
+
const fileName = path.basename(sourcePath);
|
|
929
|
+
return fileName === ".env" || fileName.startsWith(".env.");
|
|
930
|
+
}
|
|
931
|
+
async copyAssetFile(args) {
|
|
932
|
+
const outputPath = path.resolve(args.outputAppRoot, this.toConsumerRelativePath(args.sourcePath));
|
|
933
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
934
|
+
await copyFile(args.sourcePath, outputPath);
|
|
935
|
+
}
|
|
936
|
+
async emitSourceFile(args) {
|
|
937
|
+
const sourceText = await readFile(args.sourcePath, "utf8");
|
|
938
|
+
const transpiledOutput = ts.transpileModule(sourceText, {
|
|
939
|
+
compilerOptions: this.createCompilerOptions(),
|
|
940
|
+
fileName: args.sourcePath,
|
|
941
|
+
reportDiagnostics: false
|
|
942
|
+
});
|
|
943
|
+
const rewrittenOutputText = await this.rewriteRelativeImportSpecifiers(args.sourcePath, transpiledOutput.outputText);
|
|
944
|
+
const outputPath = this.resolveOutputPath(args.outputAppRoot, args.sourcePath);
|
|
945
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
946
|
+
await writeFile(outputPath, rewrittenOutputText, "utf8");
|
|
947
|
+
if (transpiledOutput.sourceMapText) await writeFile(`${outputPath}.map`, transpiledOutput.sourceMapText, "utf8");
|
|
948
|
+
}
|
|
949
|
+
createCompilerOptions() {
|
|
950
|
+
const scriptTarget = this.buildOptions.target === "es2020" ? ts.ScriptTarget.ES2020 : ts.ScriptTarget.ES2022;
|
|
951
|
+
return {
|
|
952
|
+
emitDecoratorMetadata: true,
|
|
953
|
+
esModuleInterop: true,
|
|
954
|
+
experimentalDecorators: true,
|
|
955
|
+
inlineSources: this.buildOptions.sourceMaps,
|
|
956
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
957
|
+
module: ts.ModuleKind.ESNext,
|
|
958
|
+
sourceMap: this.buildOptions.sourceMaps,
|
|
959
|
+
target: scriptTarget,
|
|
960
|
+
useDefineForClassFields: false
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async writeEntryFile(args) {
|
|
964
|
+
const configImportPath = this.resolveOutputImportPath(args.outputAppRoot, args.snapshot.outputEntryPath, args.configSourcePath);
|
|
965
|
+
if (!configImportPath) throw new Error("Consumer output build requires a resolved codemation config source.");
|
|
966
|
+
const workflowImportBlocks = args.snapshot.workflowSourcePaths.map((workflowSourcePath, index) => {
|
|
967
|
+
const importPath = this.resolveOutputImportPath(args.outputAppRoot, args.snapshot.outputEntryPath, workflowSourcePath);
|
|
968
|
+
if (!importPath) throw new Error(`Could not resolve workflow output path: ${workflowSourcePath}`);
|
|
969
|
+
return `import * as workflowModule${index} from "${importPath}";`;
|
|
970
|
+
}).join("\n");
|
|
971
|
+
const workflowModulesExpression = args.snapshot.workflowSourcePaths.length > 0 ? `[${args.snapshot.workflowSourcePaths.map((_, index) => `workflowModule${index}`).join(", ")}]` : "[]";
|
|
972
|
+
const workflowSourcePathsExpression = args.snapshot.workflowSourcePaths.length > 0 ? `[${args.snapshot.workflowSourcePaths.map((workflowSourcePath) => JSON.stringify(workflowSourcePath)).join(", ")}]` : "[]";
|
|
973
|
+
const workflowDiscoveryPathSegmentsListExpression = JSON.stringify(args.snapshot.workflowDiscoveryPathSegmentsList);
|
|
974
|
+
const outputText = [
|
|
975
|
+
`import * as configModule from "${configImportPath}";`,
|
|
976
|
+
"import { CodemationConsumerAppResolver } from \"@codemation/host/consumer\";",
|
|
977
|
+
workflowImportBlocks,
|
|
978
|
+
"const resolver = new CodemationConsumerAppResolver();",
|
|
979
|
+
`export const codemationConsumerBuildVersion = ${JSON.stringify(args.snapshot.buildVersion)};`,
|
|
980
|
+
`export const codemationConsumerApp = resolver.resolve({`,
|
|
981
|
+
" configModule,",
|
|
982
|
+
` workflowModules: ${workflowModulesExpression},`,
|
|
983
|
+
` workflowSourcePaths: ${workflowSourcePathsExpression},`,
|
|
984
|
+
` workflowDiscoveryPathSegmentsList: ${workflowDiscoveryPathSegmentsListExpression},`,
|
|
985
|
+
"});",
|
|
986
|
+
"export default codemationConsumerApp;",
|
|
987
|
+
""
|
|
988
|
+
].filter((line) => line.length > 0).join("\n");
|
|
989
|
+
await writeFile(args.snapshot.outputEntryPath, outputText, "utf8");
|
|
990
|
+
}
|
|
991
|
+
async rewriteRelativeImportSpecifiers(sourcePath, outputText) {
|
|
992
|
+
let nextOutputText = await this.rewritePatternMatches(sourcePath, outputText, /(from\s+["'])(\.{1,2}\/[^"']+)(["'])/g);
|
|
993
|
+
nextOutputText = await this.rewritePatternMatches(sourcePath, nextOutputText, /(import\s+["'])(\.{1,2}\/[^"']+)(["'])/g);
|
|
994
|
+
nextOutputText = await this.rewritePatternMatches(sourcePath, nextOutputText, /(import\s*\(\s*["'])(\.{1,2}\/[^"']+)(["']\s*\))/g);
|
|
995
|
+
return nextOutputText;
|
|
996
|
+
}
|
|
997
|
+
async rewritePatternMatches(sourcePath, outputText, pattern) {
|
|
998
|
+
const matches = [...outputText.matchAll(pattern)];
|
|
999
|
+
let rewrittenText = outputText;
|
|
1000
|
+
for (const match of matches) {
|
|
1001
|
+
const currentSpecifier = match[2];
|
|
1002
|
+
const nextSpecifier = await this.resolveRelativeRuntimeSpecifier(sourcePath, currentSpecifier);
|
|
1003
|
+
if (nextSpecifier === currentSpecifier) continue;
|
|
1004
|
+
rewrittenText = rewrittenText.replace(`${match[1]}${currentSpecifier}${match[3]}`, `${match[1]}${nextSpecifier}${match[3]}`);
|
|
1005
|
+
}
|
|
1006
|
+
return rewrittenText;
|
|
1007
|
+
}
|
|
1008
|
+
async resolveRelativeRuntimeSpecifier(sourcePath, importSpecifier) {
|
|
1009
|
+
if (!importSpecifier.startsWith(".")) return importSpecifier;
|
|
1010
|
+
const extension = path.extname(importSpecifier);
|
|
1011
|
+
if (this.isRuntimeExtension(extension)) return importSpecifier;
|
|
1012
|
+
if (this.isSourceExtension(extension)) return `${importSpecifier.slice(0, importSpecifier.length - extension.length)}${this.toJavascriptExtension(extension)}`;
|
|
1013
|
+
const resolvedSpecifier = await this.resolveFileImportSpecifier(sourcePath, importSpecifier);
|
|
1014
|
+
if (resolvedSpecifier) return resolvedSpecifier;
|
|
1015
|
+
return await this.resolveIndexImportSpecifier(sourcePath, importSpecifier) ?? importSpecifier;
|
|
1016
|
+
}
|
|
1017
|
+
async resolveFileImportSpecifier(sourcePath, importSpecifier) {
|
|
1018
|
+
const resolvedBasePath = path.resolve(path.dirname(sourcePath), importSpecifier);
|
|
1019
|
+
for (const sourceExtension of ConsumerOutputBuilder.supportedSourceExtensions) if (await this.fileExists(`${resolvedBasePath}${sourceExtension}`)) return `${importSpecifier}${this.toJavascriptExtension(sourceExtension)}`;
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
async resolveIndexImportSpecifier(sourcePath, importSpecifier) {
|
|
1023
|
+
const resolvedDirectoryPath = path.resolve(path.dirname(sourcePath), importSpecifier);
|
|
1024
|
+
for (const sourceExtension of ConsumerOutputBuilder.supportedSourceExtensions) {
|
|
1025
|
+
const indexSourcePath = path.resolve(resolvedDirectoryPath, `index${sourceExtension}`);
|
|
1026
|
+
if (await this.fileExists(indexSourcePath)) return `${importSpecifier}/index${this.toJavascriptExtension(sourceExtension)}`;
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
resolveOutputImportPath(outputAppRoot, outputEntryPath, sourcePath) {
|
|
1031
|
+
if (!sourcePath) return null;
|
|
1032
|
+
const outputPath = this.resolveOutputPath(outputAppRoot, sourcePath);
|
|
1033
|
+
const relativePath = path.relative(path.dirname(outputEntryPath), outputPath).replace(/\\/g, "/");
|
|
1034
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
1035
|
+
}
|
|
1036
|
+
resolveOutputPath(outputAppRoot, sourcePath) {
|
|
1037
|
+
const relativePath = this.toConsumerRelativePath(sourcePath);
|
|
1038
|
+
const nextExtension = this.toJavascriptExtension(path.extname(relativePath));
|
|
1039
|
+
const pathWithoutExtension = relativePath.slice(0, relativePath.length - path.extname(relativePath).length);
|
|
1040
|
+
return path.resolve(outputAppRoot, `${pathWithoutExtension}${nextExtension}`);
|
|
1041
|
+
}
|
|
1042
|
+
resolveOutputRoot() {
|
|
1043
|
+
return path.resolve(this.consumerRoot, ".codemation", "output");
|
|
1044
|
+
}
|
|
1045
|
+
resolveFinalBuildOutputRoot() {
|
|
1046
|
+
return path.resolve(this.resolveOutputRoot(), "build");
|
|
1047
|
+
}
|
|
1048
|
+
resolveStagingBuildRoot(buildVersion) {
|
|
1049
|
+
return path.resolve(this.resolveOutputRoot(), "staging", `${buildVersion}-${randomUUID()}`);
|
|
1050
|
+
}
|
|
1051
|
+
resolveCurrentManifestPath() {
|
|
1052
|
+
return path.resolve(this.resolveOutputRoot(), "current.json");
|
|
1053
|
+
}
|
|
1054
|
+
async promoteStagingToFinalBuild(args) {
|
|
1055
|
+
await mkdir(path.dirname(args.finalBuildRoot), { recursive: true });
|
|
1056
|
+
await rm(args.finalBuildRoot, {
|
|
1057
|
+
force: true,
|
|
1058
|
+
recursive: true
|
|
1059
|
+
}).catch(() => null);
|
|
1060
|
+
await rename(args.stagingBuildRoot, args.finalBuildRoot);
|
|
1061
|
+
}
|
|
1062
|
+
isRuntimeExtension(extension) {
|
|
1063
|
+
return extension === ".cjs" || extension === ".js" || extension === ".json" || extension === ".mjs";
|
|
1064
|
+
}
|
|
1065
|
+
isSourceExtension(extension) {
|
|
1066
|
+
return ConsumerOutputBuilder.supportedSourceExtensions.has(extension);
|
|
1067
|
+
}
|
|
1068
|
+
toJavascriptExtension(extension) {
|
|
1069
|
+
if (extension === ".cts") return ".cjs";
|
|
1070
|
+
if (extension === ".mts") return ".mjs";
|
|
1071
|
+
return ".js";
|
|
1072
|
+
}
|
|
1073
|
+
createIgnoredMatcher() {
|
|
1074
|
+
return (watchPath) => {
|
|
1075
|
+
const relativePath = path.relative(this.consumerRoot, watchPath);
|
|
1076
|
+
if (relativePath.startsWith("..")) return false;
|
|
1077
|
+
return relativePath.replace(/\\/g, "/").split("/").some((segment) => ConsumerOutputBuilder.ignoredDirectoryNames.has(segment));
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
toConsumerRelativePath(filePath) {
|
|
1081
|
+
return path.relative(this.consumerRoot, filePath);
|
|
1082
|
+
}
|
|
1083
|
+
createBuildVersion() {
|
|
1084
|
+
const nextBuildVersion = Math.max(Date.now(), this.lastIssuedBuildVersion + 1);
|
|
1085
|
+
this.lastIssuedBuildVersion = nextBuildVersion;
|
|
1086
|
+
return `${nextBuildVersion}-${process$1.pid}`;
|
|
1087
|
+
}
|
|
1088
|
+
async resolveConfigPath(consumerRoot) {
|
|
1089
|
+
for (const candidate of this.getConventionCandidates(consumerRoot)) if (await this.fileExists(candidate)) return candidate;
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
getConventionCandidates(consumerRoot) {
|
|
1093
|
+
return [
|
|
1094
|
+
path.resolve(consumerRoot, "codemation.config.ts"),
|
|
1095
|
+
path.resolve(consumerRoot, "codemation.config.js"),
|
|
1096
|
+
path.resolve(consumerRoot, "src", "codemation.config.ts"),
|
|
1097
|
+
path.resolve(consumerRoot, "src", "codemation.config.js")
|
|
1098
|
+
];
|
|
1099
|
+
}
|
|
1100
|
+
async loadConfigMetadata(configSourcePath) {
|
|
1101
|
+
const sourceText = await readFile(configSourcePath, "utf8");
|
|
1102
|
+
const sourceFile = ts.createSourceFile(configSourcePath, sourceText, ts.ScriptTarget.Latest, true, this.resolveScriptKind(configSourcePath));
|
|
1103
|
+
const configObjectLiteral = this.resolveConfigObjectLiteral(sourceFile);
|
|
1104
|
+
if (!configObjectLiteral) return {
|
|
1105
|
+
hasInlineWorkflows: false,
|
|
1106
|
+
workflowDiscoveryDirectories: [...WorkflowModulePathFinder.defaultWorkflowDirectories]
|
|
1107
|
+
};
|
|
1108
|
+
const workflowDiscovery = this.readObjectLiteralProperty(configObjectLiteral, "workflowDiscovery");
|
|
1109
|
+
const workflowDiscoveryDirectories = this.readStringArrayProperty(workflowDiscovery, "directories");
|
|
1110
|
+
return {
|
|
1111
|
+
hasInlineWorkflows: this.hasProperty(configObjectLiteral, "workflows"),
|
|
1112
|
+
workflowDiscoveryDirectories: workflowDiscoveryDirectories.length > 0 ? workflowDiscoveryDirectories : [...WorkflowModulePathFinder.defaultWorkflowDirectories]
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
resolveScriptKind(filePath) {
|
|
1116
|
+
const extension = path.extname(filePath);
|
|
1117
|
+
if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return ts.ScriptKind.JS;
|
|
1118
|
+
if (extension === ".tsx") return ts.ScriptKind.TSX;
|
|
1119
|
+
return ts.ScriptKind.TS;
|
|
1120
|
+
}
|
|
1121
|
+
resolveConfigObjectLiteral(sourceFile) {
|
|
1122
|
+
const objectLiteralsByIdentifier = /* @__PURE__ */ new Map();
|
|
1123
|
+
const exportedObjectLiterals = [];
|
|
1124
|
+
for (const statement of sourceFile.statements) {
|
|
1125
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
1126
|
+
const isExported = this.hasExportModifier(statement);
|
|
1127
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1128
|
+
if (!ts.isIdentifier(declaration.name)) continue;
|
|
1129
|
+
const objectLiteral = this.unwrapObjectLiteralExpression(declaration.initializer);
|
|
1130
|
+
if (!objectLiteral) continue;
|
|
1131
|
+
objectLiteralsByIdentifier.set(declaration.name.text, objectLiteral);
|
|
1132
|
+
if (isExported) exportedObjectLiterals.push(objectLiteral);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
for (const statement of sourceFile.statements) {
|
|
1136
|
+
if (!ts.isExportAssignment(statement)) continue;
|
|
1137
|
+
const directObjectLiteral = this.unwrapObjectLiteralExpression(statement.expression);
|
|
1138
|
+
if (directObjectLiteral) return directObjectLiteral;
|
|
1139
|
+
if (ts.isIdentifier(statement.expression)) {
|
|
1140
|
+
const resolvedObjectLiteral = objectLiteralsByIdentifier.get(statement.expression.text);
|
|
1141
|
+
if (resolvedObjectLiteral) return resolvedObjectLiteral;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const namedConfigLiteral = objectLiteralsByIdentifier.get("codemationHost") ?? objectLiteralsByIdentifier.get("config");
|
|
1145
|
+
if (namedConfigLiteral) return namedConfigLiteral;
|
|
1146
|
+
return exportedObjectLiterals[0] ?? null;
|
|
1147
|
+
}
|
|
1148
|
+
hasExportModifier(statement) {
|
|
1149
|
+
return ts.canHaveModifiers(statement) ? ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false : false;
|
|
1150
|
+
}
|
|
1151
|
+
unwrapObjectLiteralExpression(node) {
|
|
1152
|
+
if (!node) return null;
|
|
1153
|
+
if (ts.isObjectLiteralExpression(node)) return node;
|
|
1154
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) return this.unwrapObjectLiteralExpression(node.expression);
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
hasProperty(objectLiteral, propertyName) {
|
|
1158
|
+
return this.getPropertyAssignment(objectLiteral, propertyName) !== null;
|
|
1159
|
+
}
|
|
1160
|
+
readObjectLiteralProperty(objectLiteral, propertyName) {
|
|
1161
|
+
const property = this.getPropertyAssignment(objectLiteral, propertyName);
|
|
1162
|
+
return this.unwrapObjectLiteralExpression(property?.initializer);
|
|
1163
|
+
}
|
|
1164
|
+
readStringArrayProperty(objectLiteral, propertyName) {
|
|
1165
|
+
if (!objectLiteral) return [];
|
|
1166
|
+
const property = this.getPropertyAssignment(objectLiteral, propertyName);
|
|
1167
|
+
if (!property || !ts.isArrayLiteralExpression(property.initializer)) return [];
|
|
1168
|
+
const values = [];
|
|
1169
|
+
for (const element of property.initializer.elements) if (ts.isStringLiteralLike(element)) values.push(element.text);
|
|
1170
|
+
return values;
|
|
1171
|
+
}
|
|
1172
|
+
getPropertyAssignment(objectLiteral, propertyName) {
|
|
1173
|
+
for (const property of objectLiteral.properties) {
|
|
1174
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
1175
|
+
if (this.readPropertyName(property.name) === propertyName) return property;
|
|
1176
|
+
}
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
readPropertyName(propertyName) {
|
|
1180
|
+
if (ts.isIdentifier(propertyName) || ts.isStringLiteralLike(propertyName)) return propertyName.text;
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
async resolveWorkflowSources(consumerRoot, configMetadata) {
|
|
1184
|
+
if (configMetadata.hasInlineWorkflows) return [];
|
|
1185
|
+
return [...await this.workflowModulePathFinder.discoverModulePaths({
|
|
1186
|
+
consumerRoot,
|
|
1187
|
+
workflowDirectories: configMetadata.workflowDiscoveryDirectories,
|
|
1188
|
+
exists: (absolutePath) => this.fileExists(absolutePath)
|
|
1189
|
+
})].sort((left, right) => left.localeCompare(right));
|
|
1190
|
+
}
|
|
1191
|
+
async fileExists(filePath) {
|
|
1192
|
+
try {
|
|
1193
|
+
await stat(filePath);
|
|
1194
|
+
return true;
|
|
1195
|
+
} catch {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region src/consumer/Loader.ts
|
|
1203
|
+
var ConsumerOutputBuilderLoader = class {
|
|
1204
|
+
create(consumerRoot, buildOptions) {
|
|
1205
|
+
return new ConsumerOutputBuilder(consumerRoot, void 0, buildOptions);
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
//#endregion
|
|
1210
|
+
//#region src/database/ConsumerDatabaseConnectionResolver.ts
|
|
1211
|
+
/**
|
|
1212
|
+
* Resolves TCP PostgreSQL vs PGlite vs none from env + {@link CodemationConfig} (same rules as the host runtime).
|
|
1213
|
+
*/
|
|
1214
|
+
var ConsumerDatabaseConnectionResolver = class {
|
|
1215
|
+
resolver = new DatabasePersistenceResolver();
|
|
1216
|
+
resolve(processEnv, config$1, consumerRoot) {
|
|
1217
|
+
return this.resolver.resolve({
|
|
1218
|
+
runtimeConfig: config$1.runtime ?? {},
|
|
1219
|
+
env: processEnv,
|
|
1220
|
+
consumerRoot
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
//#endregion
|
|
1226
|
+
//#region src/database/DatabaseMigrationsApplyService.ts
|
|
1227
|
+
/**
|
|
1228
|
+
* Loads consumer config + env, resolves persistence, and runs Prisma migrations.
|
|
1229
|
+
* Shared by `codemation db migrate` and `codemation dev` (cold start only).
|
|
1230
|
+
*/
|
|
1231
|
+
var DatabaseMigrationsApplyService = class {
|
|
1232
|
+
constructor(cliLogger, consumerDotenvLoader, tsconfigPreparation, configLoader, databaseConnectionResolver, databaseUrlDescriptor, hostPackageRoot, migrationDeployer) {
|
|
1233
|
+
this.cliLogger = cliLogger;
|
|
1234
|
+
this.consumerDotenvLoader = consumerDotenvLoader;
|
|
1235
|
+
this.tsconfigPreparation = tsconfigPreparation;
|
|
1236
|
+
this.configLoader = configLoader;
|
|
1237
|
+
this.databaseConnectionResolver = databaseConnectionResolver;
|
|
1238
|
+
this.databaseUrlDescriptor = databaseUrlDescriptor;
|
|
1239
|
+
this.hostPackageRoot = hostPackageRoot;
|
|
1240
|
+
this.migrationDeployer = migrationDeployer;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Applies migrations when persistence is configured; no-op when there is no database (in-memory dev).
|
|
1244
|
+
*/
|
|
1245
|
+
async applyForConsumer(consumerRoot, options) {
|
|
1246
|
+
await this.applyInternal(consumerRoot, options, false);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Same as {@link applyForConsumer} but throws when no database is configured (for `db migrate`).
|
|
1250
|
+
*/
|
|
1251
|
+
async applyForConsumerRequiringPersistence(consumerRoot, options) {
|
|
1252
|
+
await this.applyInternal(consumerRoot, options, true);
|
|
1253
|
+
}
|
|
1254
|
+
async applyInternal(consumerRoot, options, requirePersistence) {
|
|
1255
|
+
this.consumerDotenvLoader.load(consumerRoot);
|
|
1256
|
+
this.tsconfigPreparation.applyWorkspaceTsconfigForTsxIfPresent(consumerRoot);
|
|
1257
|
+
const resolution = await this.configLoader.load({
|
|
1258
|
+
consumerRoot,
|
|
1259
|
+
configPathOverride: options?.configPath
|
|
1260
|
+
});
|
|
1261
|
+
const persistence = this.databaseConnectionResolver.resolve(process.env, resolution.config, consumerRoot);
|
|
1262
|
+
if (persistence.kind === "none") {
|
|
1263
|
+
if (requirePersistence) throw new Error("Database persistence is not configured. Set CodemationConfig.runtime.database (postgresql URL or PGlite).");
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
process.env.CODEMATION_HOST_PACKAGE_ROOT = this.hostPackageRoot;
|
|
1267
|
+
this.cliLogger.debug(`Applying database migrations (${this.databaseUrlDescriptor.describePersistence(persistence)})`);
|
|
1268
|
+
await this.migrationDeployer.deployPersistence(persistence, process.env);
|
|
1269
|
+
this.cliLogger.info(`Database migrations applied (${this.databaseUrlDescriptor.describePersistence(persistence)}).`);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/database/HostPackageRootResolver.ts
|
|
1275
|
+
/**
|
|
1276
|
+
* Locates the installed `@codemation/host` package root (contains `prisma/` and Prisma CLI).
|
|
1277
|
+
* Uses ESM resolution (`@codemation/host` does not expose a CJS `require` entry).
|
|
1278
|
+
*/
|
|
1279
|
+
var HostPackageRootResolver = class {
|
|
1280
|
+
resolveHostPackageRoot() {
|
|
1281
|
+
const entry = fileURLToPath(import.meta.resolve("@codemation/host"));
|
|
1282
|
+
let dir = path.dirname(entry);
|
|
1283
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
1284
|
+
if (existsSync(path.join(dir, "prisma", "schema.prisma"))) return dir;
|
|
1285
|
+
const parent = path.dirname(dir);
|
|
1286
|
+
if (parent === dir) break;
|
|
1287
|
+
dir = parent;
|
|
1288
|
+
}
|
|
1289
|
+
throw new Error(`Could not locate prisma/schema.prisma near @codemation/host entry: ${entry}`);
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
//#endregion
|
|
1294
|
+
//#region src/dev/DevBootstrapSummaryFetcher.ts
|
|
1295
|
+
/**
|
|
1296
|
+
* Fetches {@link DevBootstrapSummaryJson} from the dev gateway (proxied to runtime-dev).
|
|
1297
|
+
*/
|
|
1298
|
+
var DevBootstrapSummaryFetcher = class {
|
|
1299
|
+
async fetch(gatewayBaseUrl) {
|
|
1300
|
+
const normalized = gatewayBaseUrl.replace(/\/$/, "");
|
|
1301
|
+
const response = await fetch(`${normalized}/api/dev/bootstrap-summary`);
|
|
1302
|
+
if (!response.ok) return null;
|
|
1303
|
+
return await response.json();
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/dev/DevCliBannerRenderer.ts
|
|
1309
|
+
/**
|
|
1310
|
+
* Dev-only stdout branding (not the structured host logger).
|
|
1311
|
+
* Renders the figlet banner once on cold start; append-only compact block after hot reload.
|
|
1312
|
+
*/
|
|
1313
|
+
var DevCliBannerRenderer = class {
|
|
1314
|
+
/**
|
|
1315
|
+
* Figlet header only — call early so branding appears before migrations / gateway work.
|
|
1316
|
+
*/
|
|
1317
|
+
renderBrandHeader() {
|
|
1318
|
+
const headerBox = boxen(`${this.renderFigletTitle()}\n${chalk.dim.italic("AI Automation framework")}`, {
|
|
1319
|
+
padding: {
|
|
1320
|
+
top: 0,
|
|
1321
|
+
bottom: 1,
|
|
1322
|
+
left: 1,
|
|
1323
|
+
right: 1
|
|
1324
|
+
},
|
|
1325
|
+
margin: { bottom: 0 },
|
|
1326
|
+
borderStyle: "double",
|
|
1327
|
+
borderColor: "cyan",
|
|
1328
|
+
textAlignment: "center"
|
|
1329
|
+
});
|
|
1330
|
+
process.stdout.write(`${headerBox}\n`);
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Runtime detail + active workflows (after bootstrap summary is available).
|
|
1334
|
+
*/
|
|
1335
|
+
renderRuntimeSummary(summary) {
|
|
1336
|
+
const detailBox = boxen(this.buildDetailBody(summary), {
|
|
1337
|
+
padding: {
|
|
1338
|
+
top: 0,
|
|
1339
|
+
bottom: 0,
|
|
1340
|
+
left: 1,
|
|
1341
|
+
right: 1
|
|
1342
|
+
},
|
|
1343
|
+
margin: {
|
|
1344
|
+
top: 1,
|
|
1345
|
+
bottom: 0
|
|
1346
|
+
},
|
|
1347
|
+
borderStyle: "round",
|
|
1348
|
+
borderColor: "gray",
|
|
1349
|
+
dimBorder: true,
|
|
1350
|
+
title: chalk.bold("Runtime"),
|
|
1351
|
+
titleAlignment: "center"
|
|
1352
|
+
});
|
|
1353
|
+
const activeSection = this.buildActiveWorkflowsSection(summary);
|
|
1354
|
+
process.stdout.write(`${detailBox}\n${activeSection}\n`);
|
|
1355
|
+
}
|
|
1356
|
+
renderFull(summary) {
|
|
1357
|
+
this.renderBrandHeader();
|
|
1358
|
+
this.renderRuntimeSummary(summary);
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Shown after hot reload / watcher restarts (no figlet).
|
|
1362
|
+
*/
|
|
1363
|
+
renderCompact(summary) {
|
|
1364
|
+
const detailBox = boxen(this.buildDetailBody(summary), {
|
|
1365
|
+
padding: {
|
|
1366
|
+
top: 0,
|
|
1367
|
+
bottom: 0,
|
|
1368
|
+
left: 1,
|
|
1369
|
+
right: 1
|
|
1370
|
+
},
|
|
1371
|
+
margin: {
|
|
1372
|
+
top: 1,
|
|
1373
|
+
bottom: 0
|
|
1374
|
+
},
|
|
1375
|
+
borderStyle: "round",
|
|
1376
|
+
borderColor: "gray",
|
|
1377
|
+
dimBorder: true,
|
|
1378
|
+
title: chalk.bold("Runtime (updated)"),
|
|
1379
|
+
titleAlignment: "center"
|
|
1380
|
+
});
|
|
1381
|
+
const activeSection = this.buildActiveWorkflowsSection(summary);
|
|
1382
|
+
process.stdout.write(`\n${detailBox}\n${activeSection}\n`);
|
|
1383
|
+
}
|
|
1384
|
+
renderFigletTitle() {
|
|
1385
|
+
try {
|
|
1386
|
+
return chalk.cyan(figlet.textSync("Codemation", { font: "Slant" }));
|
|
1387
|
+
} catch {
|
|
1388
|
+
return chalk.cyan.bold("Codemation");
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
buildDetailBody(summary) {
|
|
1392
|
+
const label = (text) => chalk.hex("#9ca3af")(text);
|
|
1393
|
+
const value = (text) => chalk.whiteBright(text);
|
|
1394
|
+
const lines = [
|
|
1395
|
+
`${label("Log level")} ${value(summary.logLevel)}`,
|
|
1396
|
+
`${label("Database")} ${value(summary.databaseLabel)}`,
|
|
1397
|
+
`${label("Scheduler")} ${value(summary.schedulerLabel)}`,
|
|
1398
|
+
`${label("Event bus")} ${value(summary.eventBusLabel)}`
|
|
1399
|
+
];
|
|
1400
|
+
if (summary.redisUrlRedacted) lines.push(`${label("Redis")} ${value(summary.redisUrlRedacted)}`);
|
|
1401
|
+
return lines.join("\n");
|
|
1402
|
+
}
|
|
1403
|
+
buildActiveWorkflowsSection(summary) {
|
|
1404
|
+
return boxen((summary.activeWorkflows.length === 0 ? [chalk.dim(" (none active)")] : summary.activeWorkflows.map((w) => `${chalk.whiteBright(` • ${w.name} `)}${chalk.dim(`(${w.id})`)}`)).join("\n"), {
|
|
1405
|
+
padding: {
|
|
1406
|
+
top: 0,
|
|
1407
|
+
bottom: 0,
|
|
1408
|
+
left: 0,
|
|
1409
|
+
right: 0
|
|
1410
|
+
},
|
|
1411
|
+
margin: {
|
|
1412
|
+
top: 1,
|
|
1413
|
+
bottom: 0
|
|
1414
|
+
},
|
|
1415
|
+
borderStyle: "single",
|
|
1416
|
+
borderColor: "magenta",
|
|
1417
|
+
title: chalk.bold("Active workflows"),
|
|
1418
|
+
titleAlignment: "left"
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
//#endregion
|
|
1424
|
+
//#region src/dev/DevConsumerPublishBootstrap.ts
|
|
1425
|
+
/**
|
|
1426
|
+
* Ensures `.codemation/output/current.json` and transpiled consumer config exist before the Next host boots.
|
|
1427
|
+
* Without this, `codemation dev` can serve a stale built `codemation.config.js` (e.g. missing whitelabel).
|
|
1428
|
+
*/
|
|
1429
|
+
var DevConsumerPublishBootstrap = class {
|
|
1430
|
+
constructor(cliLogger, pluginDiscovery, artifactsPublisher, outputBuilderLoader, buildOptionsParser) {
|
|
1431
|
+
this.cliLogger = cliLogger;
|
|
1432
|
+
this.pluginDiscovery = pluginDiscovery;
|
|
1433
|
+
this.artifactsPublisher = artifactsPublisher;
|
|
1434
|
+
this.outputBuilderLoader = outputBuilderLoader;
|
|
1435
|
+
this.buildOptionsParser = buildOptionsParser;
|
|
1436
|
+
}
|
|
1437
|
+
async ensurePublished(paths) {
|
|
1438
|
+
const buildOptions = this.buildOptionsParser.parse({});
|
|
1439
|
+
const snapshot = await this.outputBuilderLoader.create(paths.consumerRoot, buildOptions).ensureBuilt();
|
|
1440
|
+
const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
|
|
1441
|
+
await this.artifactsPublisher.publish(snapshot, discoveredPlugins);
|
|
1442
|
+
this.cliLogger.debug(`Dev: consumer output published (${snapshot.buildVersion}).`);
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
|
|
1446
|
+
//#endregion
|
|
1447
|
+
//#region src/runtime/ListenPortResolver.ts
|
|
1448
|
+
/**
|
|
1449
|
+
* Shared HTTP listen port parsing for CLI commands (dev server, serve web, etc.).
|
|
1450
|
+
*/
|
|
1451
|
+
var ListenPortResolver = class {
|
|
1452
|
+
resolvePrimaryApplicationPort(rawPort) {
|
|
1453
|
+
const parsedPort = Number(rawPort);
|
|
1454
|
+
if (Number.isInteger(parsedPort) && parsedPort > 0) return parsedPort;
|
|
1455
|
+
return 3e3;
|
|
1456
|
+
}
|
|
1457
|
+
parsePositiveInteger(raw) {
|
|
1458
|
+
const parsed = Number(raw);
|
|
1459
|
+
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
resolveWebsocketPortRelativeToHttp(args) {
|
|
1463
|
+
const explicit = this.parsePositiveInteger(args.publicWebsocketPort) ?? this.parsePositiveInteger(args.websocketPort);
|
|
1464
|
+
if (explicit !== null) return explicit;
|
|
1465
|
+
return args.nextPort + 1;
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region src/runtime/SourceMapNodeOptions.ts
|
|
1471
|
+
var SourceMapNodeOptions = class {
|
|
1472
|
+
appendToNodeOptions(existingNodeOptions) {
|
|
1473
|
+
const sourceMapOption = "--enable-source-maps";
|
|
1474
|
+
if (!existingNodeOptions || existingNodeOptions.trim().length === 0) return sourceMapOption;
|
|
1475
|
+
if (existingNodeOptions.includes(sourceMapOption)) return existingNodeOptions;
|
|
1476
|
+
return `${existingNodeOptions} ${sourceMapOption}`.trim();
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
//#endregion
|
|
1481
|
+
//#region src/dev/DevelopmentGatewayNotifier.ts
|
|
1482
|
+
var DevelopmentGatewayNotifier = class {
|
|
1483
|
+
constructor(cliLogger) {
|
|
1484
|
+
this.cliLogger = cliLogger;
|
|
1485
|
+
}
|
|
1486
|
+
async notify(args) {
|
|
1487
|
+
const targetUrl = `${args.gatewayBaseUrl.replace(/\/$/, "")}${ApiPaths.devGatewayNotify()}`;
|
|
1488
|
+
try {
|
|
1489
|
+
const response = await fetch(targetUrl, {
|
|
1490
|
+
method: "POST",
|
|
1491
|
+
headers: {
|
|
1492
|
+
"content-type": "application/json",
|
|
1493
|
+
"x-codemation-dev-token": args.developmentServerToken
|
|
1494
|
+
},
|
|
1495
|
+
body: JSON.stringify(args.payload)
|
|
1496
|
+
});
|
|
1497
|
+
if (!response.ok) this.cliLogger.warn(`failed to notify dev gateway status=${response.status}`);
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
this.cliLogger.warn(`failed to notify dev gateway: ${error instanceof Error ? error.message : String(error)}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
//#endregion
|
|
1505
|
+
//#region src/dev/DevAuthSettingsLoader.ts
|
|
1506
|
+
var DevAuthSettingsLoader = class {
|
|
1507
|
+
constructor(configLoader) {
|
|
1508
|
+
this.configLoader = configLoader;
|
|
1509
|
+
}
|
|
1510
|
+
resolveDevelopmentServerToken(rawToken) {
|
|
1511
|
+
if (rawToken && rawToken.trim().length > 0) return rawToken;
|
|
1512
|
+
return randomUUID();
|
|
1513
|
+
}
|
|
1514
|
+
async loadForConsumer(consumerRoot) {
|
|
1515
|
+
const resolution = await this.configLoader.load({ consumerRoot });
|
|
1516
|
+
return {
|
|
1517
|
+
authConfigJson: JSON.stringify(resolution.config.auth ?? null),
|
|
1518
|
+
skipUiAuth: resolution.config.auth?.allowUnauthenticatedInDevelopment === true
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/dev/DevHttpProbe.ts
|
|
1525
|
+
var DevHttpProbe = class {
|
|
1526
|
+
async waitUntilUrlRespondsOk(url) {
|
|
1527
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
1528
|
+
try {
|
|
1529
|
+
const response = await fetch(url);
|
|
1530
|
+
if (response.ok || response.status === 404) return;
|
|
1531
|
+
} catch {}
|
|
1532
|
+
await setTimeout$1(50);
|
|
1533
|
+
}
|
|
1534
|
+
throw new Error(`Timed out waiting for HTTP response from ${url}`);
|
|
1535
|
+
}
|
|
1536
|
+
async waitUntilGatewayHealthy(gatewayBaseUrl) {
|
|
1537
|
+
const normalizedBase = gatewayBaseUrl.replace(/\/$/, "");
|
|
1538
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
1539
|
+
try {
|
|
1540
|
+
if ((await fetch(`${normalizedBase}/api/dev/health`)).ok) return;
|
|
1541
|
+
} catch {}
|
|
1542
|
+
await setTimeout$1(50);
|
|
1543
|
+
}
|
|
1544
|
+
throw new Error("Timed out waiting for dev gateway HTTP health check.");
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Polls until the runtime child serves bootstrap summary (after gateway is up, the disposable runtime may still be wiring).
|
|
1548
|
+
*/
|
|
1549
|
+
async waitUntilBootstrapSummaryReady(gatewayBaseUrl) {
|
|
1550
|
+
const url = `${gatewayBaseUrl.replace(/\/$/, "")}/api/dev/bootstrap-summary`;
|
|
1551
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
1552
|
+
try {
|
|
1553
|
+
if ((await fetch(url)).ok) return;
|
|
1554
|
+
} catch {}
|
|
1555
|
+
await setTimeout$1(50);
|
|
1556
|
+
}
|
|
1557
|
+
throw new Error("Timed out waiting for dev runtime bootstrap summary.");
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
//#endregion
|
|
1562
|
+
//#region src/dev/DevNextHostEnvironmentBuilder.ts
|
|
1563
|
+
var DevNextHostEnvironmentBuilder = class {
|
|
1564
|
+
constructor(consumerEnvLoader, sourceMapNodeOptions) {
|
|
1565
|
+
this.consumerEnvLoader = consumerEnvLoader;
|
|
1566
|
+
this.sourceMapNodeOptions = sourceMapNodeOptions;
|
|
1567
|
+
}
|
|
1568
|
+
build(args) {
|
|
1569
|
+
const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process$1.env);
|
|
1570
|
+
const manifestPath = args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
|
|
1571
|
+
return {
|
|
1572
|
+
...merged,
|
|
1573
|
+
PORT: String(args.nextPort),
|
|
1574
|
+
CODEMATION_AUTH_CONFIG_JSON: args.authConfigJson,
|
|
1575
|
+
CODEMATION_CONSUMER_ROOT: args.consumerRoot,
|
|
1576
|
+
CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifestPath,
|
|
1577
|
+
CODEMATION_SKIP_UI_AUTH: args.skipUiAuth ? "true" : "false",
|
|
1578
|
+
NEXT_PUBLIC_CODEMATION_SKIP_UI_AUTH: args.skipUiAuth ? "true" : "false",
|
|
1579
|
+
CODEMATION_WS_PORT: String(args.websocketPort),
|
|
1580
|
+
NEXT_PUBLIC_CODEMATION_WS_PORT: String(args.websocketPort),
|
|
1581
|
+
CODEMATION_DEV_SERVER_TOKEN: args.developmentServerToken,
|
|
1582
|
+
CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
|
|
1583
|
+
NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process$1.env.NODE_OPTIONS),
|
|
1584
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
1585
|
+
WS_NO_UTF_8_VALIDATE: "1",
|
|
1586
|
+
...args.runtimeDevUrl !== void 0 && args.runtimeDevUrl.trim().length > 0 ? { CODEMATION_RUNTIME_DEV_URL: args.runtimeDevUrl.trim() } : {}
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
//#endregion
|
|
1592
|
+
//#region src/dev/DevSessionPortsResolver.ts
|
|
1593
|
+
var DevSessionPortsResolver = class {
|
|
1594
|
+
constructor(listenPorts, loopbackPorts) {
|
|
1595
|
+
this.listenPorts = listenPorts;
|
|
1596
|
+
this.loopbackPorts = loopbackPorts;
|
|
1597
|
+
}
|
|
1598
|
+
async resolve(args) {
|
|
1599
|
+
const nextPort = this.listenPorts.resolvePrimaryApplicationPort(args.portEnv);
|
|
1600
|
+
return {
|
|
1601
|
+
nextPort,
|
|
1602
|
+
gatewayPort: this.listenPorts.parsePositiveInteger(args.gatewayPortEnv) ?? (args.devMode === "consumer" ? nextPort : await this.loopbackPorts.allocate())
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
//#endregion
|
|
1608
|
+
//#region src/dev/DevSessionServices.ts
|
|
1609
|
+
/**
|
|
1610
|
+
* Bundles dependencies for {@link DevCommand} so the command stays a thin orchestrator.
|
|
1611
|
+
*/
|
|
1612
|
+
var DevSessionServices = class {
|
|
1613
|
+
constructor(consumerEnvLoader, sourceMapNodeOptions, sessionPorts, loopbackPortAllocator, devHttpProbe, runtimeEntrypointResolver, devAuthLoader, nextHostEnvBuilder, watchRootsResolver, sourceRestartCoordinator) {
|
|
1614
|
+
this.consumerEnvLoader = consumerEnvLoader;
|
|
1615
|
+
this.sourceMapNodeOptions = sourceMapNodeOptions;
|
|
1616
|
+
this.sessionPorts = sessionPorts;
|
|
1617
|
+
this.loopbackPortAllocator = loopbackPortAllocator;
|
|
1618
|
+
this.devHttpProbe = devHttpProbe;
|
|
1619
|
+
this.runtimeEntrypointResolver = runtimeEntrypointResolver;
|
|
1620
|
+
this.devAuthLoader = devAuthLoader;
|
|
1621
|
+
this.nextHostEnvBuilder = nextHostEnvBuilder;
|
|
1622
|
+
this.watchRootsResolver = watchRootsResolver;
|
|
1623
|
+
this.sourceRestartCoordinator = sourceRestartCoordinator;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
//#endregion
|
|
1628
|
+
//#region src/dev/DevSourceRestartCoordinator.ts
|
|
1629
|
+
var DevSourceRestartCoordinator = class {
|
|
1630
|
+
constructor(gatewayNotifier, performanceDiagnosticsLogger, cliLogger) {
|
|
1631
|
+
this.gatewayNotifier = gatewayNotifier;
|
|
1632
|
+
this.performanceDiagnosticsLogger = performanceDiagnosticsLogger;
|
|
1633
|
+
this.cliLogger = cliLogger;
|
|
1634
|
+
}
|
|
1635
|
+
async runHandshakeAfterSourceChange(gatewayBaseUrl, developmentServerToken) {
|
|
1636
|
+
const restartStarted = performance.now();
|
|
1637
|
+
try {
|
|
1638
|
+
await this.gatewayNotifier.notify({
|
|
1639
|
+
gatewayBaseUrl,
|
|
1640
|
+
developmentServerToken,
|
|
1641
|
+
payload: { kind: "buildStarted" }
|
|
1642
|
+
});
|
|
1643
|
+
await this.gatewayNotifier.notify({
|
|
1644
|
+
gatewayBaseUrl,
|
|
1645
|
+
developmentServerToken,
|
|
1646
|
+
payload: {
|
|
1647
|
+
kind: "buildCompleted",
|
|
1648
|
+
buildVersion: `${Date.now()}-${process$1.pid}`
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
const totalMs = performance.now() - restartStarted;
|
|
1652
|
+
this.performanceDiagnosticsLogger.info(`triggered source-based runtime restart timingMs={total:${totalMs.toFixed(1)}}`);
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
const exception = error instanceof Error ? error : new Error(String(error));
|
|
1655
|
+
await this.gatewayNotifier.notify({
|
|
1656
|
+
gatewayBaseUrl,
|
|
1657
|
+
developmentServerToken,
|
|
1658
|
+
payload: {
|
|
1659
|
+
kind: "buildFailed",
|
|
1660
|
+
message: exception.message
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
this.cliLogger.error("source-based runtime restart request failed", exception);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
//#endregion
|
|
1669
|
+
//#region src/dev/LoopbackPortAllocator.ts
|
|
1670
|
+
var LoopbackPortAllocator = class {
|
|
1671
|
+
async allocate() {
|
|
1672
|
+
return await new Promise((resolve, reject) => {
|
|
1673
|
+
const server = createServer();
|
|
1674
|
+
server.once("error", reject);
|
|
1675
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1676
|
+
const address = server.address();
|
|
1677
|
+
server.close(() => {
|
|
1678
|
+
if (address && typeof address === "object") {
|
|
1679
|
+
resolve(address.port);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
reject(/* @__PURE__ */ new Error("Failed to resolve a free TCP port."));
|
|
1683
|
+
});
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
//#endregion
|
|
1690
|
+
//#region src/dev/RuntimeToolEntrypointResolver.ts
|
|
1691
|
+
var RuntimeToolEntrypointResolver = class {
|
|
1692
|
+
require = createRequire(import.meta.url);
|
|
1693
|
+
async resolve(args) {
|
|
1694
|
+
const sourceEntrypointPath = path.resolve(args.repoRoot, args.sourceEntrypoint);
|
|
1695
|
+
if (await this.exists(sourceEntrypointPath)) return {
|
|
1696
|
+
command: process$1.execPath,
|
|
1697
|
+
args: [
|
|
1698
|
+
"--import",
|
|
1699
|
+
"tsx",
|
|
1700
|
+
sourceEntrypointPath
|
|
1701
|
+
],
|
|
1702
|
+
env: { TSX_TSCONFIG_PATH: path.resolve(args.repoRoot, "tsconfig.codemation-tsx.json") }
|
|
1703
|
+
};
|
|
1704
|
+
return {
|
|
1705
|
+
command: process$1.execPath,
|
|
1706
|
+
args: [this.require.resolve(args.packageName)],
|
|
1707
|
+
env: {}
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
async exists(filePath) {
|
|
1711
|
+
try {
|
|
1712
|
+
await access(filePath);
|
|
1713
|
+
return true;
|
|
1714
|
+
} catch {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region src/dev/WatchRootsResolver.ts
|
|
1722
|
+
var WatchRootsResolver = class {
|
|
1723
|
+
resolve(args) {
|
|
1724
|
+
if (args.devMode === "consumer") return [args.consumerRoot];
|
|
1725
|
+
return [
|
|
1726
|
+
args.consumerRoot,
|
|
1727
|
+
path.resolve(args.repoRoot, "packages", "core"),
|
|
1728
|
+
path.resolve(args.repoRoot, "packages", "core-nodes"),
|
|
1729
|
+
path.resolve(args.repoRoot, "packages", "core-nodes-gmail"),
|
|
1730
|
+
path.resolve(args.repoRoot, "packages", "eventbus-redis"),
|
|
1731
|
+
path.resolve(args.repoRoot, "packages", "host"),
|
|
1732
|
+
path.resolve(args.repoRoot, "packages", "node-example"),
|
|
1733
|
+
path.resolve(args.repoRoot, "packages", "queue-bullmq"),
|
|
1734
|
+
path.resolve(args.repoRoot, "packages", "runtime-dev")
|
|
1735
|
+
];
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
//#endregion
|
|
1740
|
+
//#region src/dev/Builder.ts
|
|
1741
|
+
var DevSessionServicesBuilder = class {
|
|
1742
|
+
constructor(loggerFactory$1) {
|
|
1743
|
+
this.loggerFactory = loggerFactory$1;
|
|
1744
|
+
}
|
|
1745
|
+
build() {
|
|
1746
|
+
const consumerEnvLoader = new ConsumerEnvLoader();
|
|
1747
|
+
const sourceMapNodeOptions = new SourceMapNodeOptions();
|
|
1748
|
+
const listenPortResolver = new ListenPortResolver();
|
|
1749
|
+
const loopbackPortAllocator = new LoopbackPortAllocator();
|
|
1750
|
+
const cliLogger = this.loggerFactory.create("codemation-cli");
|
|
1751
|
+
return new DevSessionServices(consumerEnvLoader, sourceMapNodeOptions, new DevSessionPortsResolver(listenPortResolver, loopbackPortAllocator), loopbackPortAllocator, new DevHttpProbe(), new RuntimeToolEntrypointResolver(), new DevAuthSettingsLoader(new CodemationConsumerConfigLoader()), new DevNextHostEnvironmentBuilder(consumerEnvLoader, sourceMapNodeOptions), new WatchRootsResolver(), new DevSourceRestartCoordinator(new DevelopmentGatewayNotifier(cliLogger), this.loggerFactory.createPerformanceDiagnostics("codemation-cli.performance"), cliLogger));
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
//#endregion
|
|
1756
|
+
//#region src/dev/DevLock.ts
|
|
1757
|
+
var DevLock = class {
|
|
1758
|
+
lockPath = null;
|
|
1759
|
+
async acquire(args) {
|
|
1760
|
+
const lockPath = this.resolveLockPath(args.consumerRoot);
|
|
1761
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
1762
|
+
const record = {
|
|
1763
|
+
pid: process$1.pid,
|
|
1764
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1765
|
+
consumerRoot: args.consumerRoot,
|
|
1766
|
+
nextPort: args.nextPort
|
|
1767
|
+
};
|
|
1768
|
+
try {
|
|
1769
|
+
await this.writeExclusive(lockPath, JSON.stringify(record, null, 2));
|
|
1770
|
+
this.lockPath = lockPath;
|
|
1771
|
+
return;
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
if (error.code !== "EEXIST") throw error;
|
|
1774
|
+
}
|
|
1775
|
+
const existingRecord = await this.readExistingRecord(lockPath);
|
|
1776
|
+
if (existingRecord && this.isProcessAlive(existingRecord.pid)) throw new Error(`codemation dev is already running for ${args.consumerRoot} (pid=${existingRecord.pid}, port=${existingRecord.nextPort}). Stop it before starting a new dev server.`);
|
|
1777
|
+
await rm(lockPath, { force: true }).catch(() => null);
|
|
1778
|
+
await this.writeExclusive(lockPath, JSON.stringify(record, null, 2));
|
|
1779
|
+
this.lockPath = lockPath;
|
|
1780
|
+
}
|
|
1781
|
+
async release() {
|
|
1782
|
+
if (!this.lockPath) return;
|
|
1783
|
+
const lockPath = this.lockPath;
|
|
1784
|
+
this.lockPath = null;
|
|
1785
|
+
await rm(lockPath, { force: true }).catch(() => null);
|
|
1786
|
+
}
|
|
1787
|
+
resolveLockPath(consumerRoot) {
|
|
1788
|
+
return path.resolve(consumerRoot, ".codemation", "dev.lock");
|
|
1789
|
+
}
|
|
1790
|
+
async writeExclusive(filePath, contents) {
|
|
1791
|
+
const handle = await open(filePath, "wx");
|
|
1792
|
+
try {
|
|
1793
|
+
await handle.writeFile(contents, "utf8");
|
|
1794
|
+
} finally {
|
|
1795
|
+
await handle.close().catch(() => null);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async readExistingRecord(lockPath) {
|
|
1799
|
+
try {
|
|
1800
|
+
const raw = await readFile(lockPath, "utf8");
|
|
1801
|
+
const parsed = JSON.parse(raw);
|
|
1802
|
+
if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string" || typeof parsed.consumerRoot !== "string" || typeof parsed.nextPort !== "number") return null;
|
|
1803
|
+
return parsed;
|
|
1804
|
+
} catch {
|
|
1805
|
+
return null;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
isProcessAlive(pid) {
|
|
1809
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1810
|
+
try {
|
|
1811
|
+
process$1.kill(pid, 0);
|
|
1812
|
+
return true;
|
|
1813
|
+
} catch {
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
//#endregion
|
|
1820
|
+
//#region src/dev/Factory.ts
|
|
1821
|
+
var DevLockFactory = class {
|
|
1822
|
+
create() {
|
|
1823
|
+
return new DevLock();
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
//#endregion
|
|
1828
|
+
//#region src/dev/ConsumerEnvDotenvFilePredicate.ts
|
|
1829
|
+
/**
|
|
1830
|
+
* True when `filePath` names a consumer dotenv file (`.env`, `.env.local`, …).
|
|
1831
|
+
* Used by `codemation dev` to distinguish env-only changes from source rebuilds.
|
|
1832
|
+
*/
|
|
1833
|
+
var ConsumerEnvDotenvFilePredicate = class {
|
|
1834
|
+
matches(filePath) {
|
|
1835
|
+
const fileName = path.basename(filePath);
|
|
1836
|
+
return fileName === ".env" || fileName.startsWith(".env.");
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
//#endregion
|
|
1841
|
+
//#region src/dev/DevTrackedProcessTreeKiller.ts
|
|
1842
|
+
/**
|
|
1843
|
+
* Stops a spawned dev child and every descendant process.
|
|
1844
|
+
*
|
|
1845
|
+
* On Unix, children are expected to have been created with `spawn({ detached: true })` so the root
|
|
1846
|
+
* child is the process-group leader; we first send `SIGTERM` to the whole group and only escalate to
|
|
1847
|
+
* `SIGKILL` when the process does not exit within the grace period.
|
|
1848
|
+
* On Windows, uses `taskkill /F /T` to terminate the process tree.
|
|
1849
|
+
*/
|
|
1850
|
+
var DevTrackedProcessTreeKiller = class {
|
|
1851
|
+
constructor(terminationGracePeriodMs = 1500) {
|
|
1852
|
+
this.terminationGracePeriodMs = terminationGracePeriodMs;
|
|
1853
|
+
}
|
|
1854
|
+
async killProcessTreeRootedAt(child) {
|
|
1855
|
+
const pid = child.pid;
|
|
1856
|
+
if (pid === void 0) {
|
|
1857
|
+
if (!await this.trySigTerm(child)) {
|
|
1858
|
+
this.trySigKill(child);
|
|
1859
|
+
await this.waitForExit(child);
|
|
1860
|
+
}
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (process$1.platform === "win32") {
|
|
1864
|
+
await this.killWindowsProcessTree(pid);
|
|
1865
|
+
await this.waitForExit(child);
|
|
1866
|
+
} else if (!await this.trySigTermProcessGroup(pid, child)) {
|
|
1867
|
+
this.trySigKill(child);
|
|
1868
|
+
this.trySigKillProcessGroup(pid);
|
|
1869
|
+
await this.waitForExit(child);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
trySigKill(child) {
|
|
1873
|
+
try {
|
|
1874
|
+
child.kill("SIGKILL");
|
|
1875
|
+
} catch {}
|
|
1876
|
+
}
|
|
1877
|
+
killWindowsProcessTree(pid) {
|
|
1878
|
+
return new Promise((resolve) => {
|
|
1879
|
+
const proc = spawn("taskkill", [
|
|
1880
|
+
"/F",
|
|
1881
|
+
"/T",
|
|
1882
|
+
"/PID",
|
|
1883
|
+
String(pid)
|
|
1884
|
+
], {
|
|
1885
|
+
stdio: "ignore",
|
|
1886
|
+
windowsHide: true
|
|
1887
|
+
});
|
|
1888
|
+
proc.once("exit", () => {
|
|
1889
|
+
resolve();
|
|
1890
|
+
});
|
|
1891
|
+
proc.once("error", () => {
|
|
1892
|
+
resolve();
|
|
1893
|
+
});
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
async trySigTerm(child) {
|
|
1897
|
+
try {
|
|
1898
|
+
child.kill("SIGTERM");
|
|
1899
|
+
} catch {
|
|
1900
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
1901
|
+
}
|
|
1902
|
+
return await this.waitForExit(child, this.terminationGracePeriodMs);
|
|
1903
|
+
}
|
|
1904
|
+
async trySigTermProcessGroup(pid, child) {
|
|
1905
|
+
try {
|
|
1906
|
+
process$1.kill(-pid, "SIGTERM");
|
|
1907
|
+
} catch {
|
|
1908
|
+
return await this.trySigTerm(child);
|
|
1909
|
+
}
|
|
1910
|
+
return await this.waitForExit(child, this.terminationGracePeriodMs);
|
|
1911
|
+
}
|
|
1912
|
+
trySigKillProcessGroup(pid) {
|
|
1913
|
+
try {
|
|
1914
|
+
process$1.kill(-pid, "SIGKILL");
|
|
1915
|
+
} catch {}
|
|
1916
|
+
}
|
|
1917
|
+
waitForExit(child, timeoutMs) {
|
|
1918
|
+
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
|
|
1919
|
+
return new Promise((resolve) => {
|
|
1920
|
+
let timeout;
|
|
1921
|
+
const onExit = () => {
|
|
1922
|
+
if (timeout) clearTimeout(timeout);
|
|
1923
|
+
resolve(true);
|
|
1924
|
+
};
|
|
1925
|
+
child.once("exit", onExit);
|
|
1926
|
+
if (timeoutMs === void 0) return;
|
|
1927
|
+
timeout = setTimeout(() => {
|
|
1928
|
+
child.removeListener("exit", onExit);
|
|
1929
|
+
resolve(child.exitCode !== null || child.signalCode !== null);
|
|
1930
|
+
}, timeoutMs);
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
//#endregion
|
|
1936
|
+
//#region src/dev/DevSourceWatcher.ts
|
|
1937
|
+
var DevSourceWatcher = class DevSourceWatcher {
|
|
1938
|
+
static ignoredDirectoryNames = new Set([
|
|
1939
|
+
".codemation",
|
|
1940
|
+
".git",
|
|
1941
|
+
".next",
|
|
1942
|
+
"coverage",
|
|
1943
|
+
"dist",
|
|
1944
|
+
"node_modules"
|
|
1945
|
+
]);
|
|
1946
|
+
watcher = null;
|
|
1947
|
+
debounceTimeout = null;
|
|
1948
|
+
changedPathsBuffer = /* @__PURE__ */ new Set();
|
|
1949
|
+
async start(args) {
|
|
1950
|
+
if (this.watcher) return;
|
|
1951
|
+
this.watcher = watch([...args.roots], {
|
|
1952
|
+
ignoreInitial: true,
|
|
1953
|
+
ignored: (watchPath) => this.isIgnoredPath(watchPath)
|
|
1954
|
+
});
|
|
1955
|
+
this.watcher.on("all", (_eventName, watchPath) => {
|
|
1956
|
+
if (typeof watchPath !== "string" || watchPath.length === 0) return;
|
|
1957
|
+
if (!this.isRelevantPath(watchPath)) return;
|
|
1958
|
+
this.changedPathsBuffer.add(path.resolve(watchPath));
|
|
1959
|
+
this.scheduleDebouncedChange(args.onChange);
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
async stop() {
|
|
1963
|
+
if (this.debounceTimeout) {
|
|
1964
|
+
clearTimeout(this.debounceTimeout);
|
|
1965
|
+
this.debounceTimeout = null;
|
|
1966
|
+
}
|
|
1967
|
+
if (this.watcher) {
|
|
1968
|
+
await this.watcher.close();
|
|
1969
|
+
this.watcher = null;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
scheduleDebouncedChange(onChange) {
|
|
1973
|
+
if (this.debounceTimeout) clearTimeout(this.debounceTimeout);
|
|
1974
|
+
this.debounceTimeout = setTimeout(() => {
|
|
1975
|
+
this.debounceTimeout = null;
|
|
1976
|
+
this.flushPendingChange(onChange);
|
|
1977
|
+
}, 75);
|
|
1978
|
+
}
|
|
1979
|
+
async flushPendingChange(onChange) {
|
|
1980
|
+
if (this.changedPathsBuffer.size === 0) return;
|
|
1981
|
+
const changedPaths = [...this.changedPathsBuffer];
|
|
1982
|
+
this.changedPathsBuffer.clear();
|
|
1983
|
+
await onChange({ changedPaths });
|
|
1984
|
+
}
|
|
1985
|
+
isIgnoredPath(watchPath) {
|
|
1986
|
+
return path.resolve(watchPath).replace(/\\/g, "/").split("/").some((segment) => DevSourceWatcher.ignoredDirectoryNames.has(segment));
|
|
1987
|
+
}
|
|
1988
|
+
isRelevantPath(watchPath) {
|
|
1989
|
+
const fileName = path.basename(watchPath);
|
|
1990
|
+
if (fileName === ".env" || fileName.startsWith(".env.")) return true;
|
|
1991
|
+
const extension = path.extname(watchPath).toLowerCase();
|
|
1992
|
+
return extension === ".cts" || extension === ".cjs" || extension === ".js" || extension === ".json" || extension === ".jsx" || extension === ".mts" || extension === ".mjs" || extension === ".prisma" || extension === ".ts" || extension === ".tsx";
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
//#endregion
|
|
1997
|
+
//#region src/dev/Runner.ts
|
|
1998
|
+
var DevSourceWatcherFactory = class {
|
|
1999
|
+
create() {
|
|
2000
|
+
return new DevSourceWatcher();
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
|
|
2004
|
+
//#endregion
|
|
2005
|
+
//#region src/Program.ts
|
|
2006
|
+
var CliProgram = class {
|
|
2007
|
+
constructor(buildOptionsParser, buildCommand, devCommand, serveWebCommand, serveWorkerCommand, dbMigrateCommand, userCreateCommand, userListCommand) {
|
|
2008
|
+
this.buildOptionsParser = buildOptionsParser;
|
|
2009
|
+
this.buildCommand = buildCommand;
|
|
2010
|
+
this.devCommand = devCommand;
|
|
2011
|
+
this.serveWebCommand = serveWebCommand;
|
|
2012
|
+
this.serveWorkerCommand = serveWorkerCommand;
|
|
2013
|
+
this.dbMigrateCommand = dbMigrateCommand;
|
|
2014
|
+
this.userCreateCommand = userCreateCommand;
|
|
2015
|
+
this.userListCommand = userListCommand;
|
|
2016
|
+
}
|
|
2017
|
+
async run(argv) {
|
|
2018
|
+
const program = new Command();
|
|
2019
|
+
program.name("codemation").description("Build and run the Codemation Next host against a consumer project.").version(this.readCliPackageVersion(), "-V, --version", "Output CLI version").showHelpAfterError("(add --help for usage)").configureHelp({ sortSubcommands: true });
|
|
2020
|
+
const resolveConsumerRoot = (raw) => raw !== void 0 && raw.trim().length > 0 ? path.resolve(process$1.cwd(), raw.trim()) : process$1.cwd();
|
|
2021
|
+
program.command("build").description("Build consumer workflows/plugins output and write the manifest.").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").option("--no-source-maps", "Disable .js.map files for emitted workflow modules (recommended for locked-down production bundles).").option("--target <es2020|es2022>", "ECMAScript language version for emitted workflow JavaScript (default: es2022).", "es2022").action(async (opts) => {
|
|
2022
|
+
await this.buildCommand.execute(resolveConsumerRoot(opts.consumerRoot), this.buildOptionsParser.parse(opts));
|
|
2023
|
+
});
|
|
2024
|
+
program.command("dev", { isDefault: true }).description("Start the dev gateway and runtime child. Use CODEMATION_DEV_MODE=framework with Next dev for framework UI HMR; default consumer mode serves API/WebSocket from the gateway only.").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").action(async (opts) => {
|
|
2025
|
+
await this.devCommand.execute(resolveConsumerRoot(opts.consumerRoot));
|
|
2026
|
+
});
|
|
2027
|
+
const serve = program.command("serve").description("Run production web or worker processes (no dev watchers).");
|
|
2028
|
+
serve.command("web").description("Start the built Next.js Codemation host (next start).").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").option("--no-source-maps", "Disable .js.map files for emitted workflow modules when this command runs the consumer build step.").option("--target <es2020|es2022>", "ECMAScript language version for emitted workflow JavaScript when building consumer output (default: es2022).", "es2022").action(async (opts) => {
|
|
2029
|
+
await this.serveWebCommand.execute(resolveConsumerRoot(opts.consumerRoot), this.buildOptionsParser.parse(opts));
|
|
2030
|
+
});
|
|
2031
|
+
serve.command("worker").description("Start the Codemation worker process.").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").option("--config <path>", "Override path to codemation.config.ts / .js").action(async (opts) => {
|
|
2032
|
+
await this.serveWorkerCommand.execute(resolveConsumerRoot(opts.consumerRoot), opts.config);
|
|
2033
|
+
});
|
|
2034
|
+
program.command("db").description("Database utilities (PostgreSQL / Prisma).").command("migrate").description("Apply pending Prisma migrations using the consumer database URL (DATABASE_URL in `.env`, or CodemationConfig.runtime.database.url).").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").option("--config <path>", "Override path to codemation.config.ts / .js").action(async (opts) => {
|
|
2035
|
+
await this.dbMigrateCommand.execute({
|
|
2036
|
+
consumerRoot: resolveConsumerRoot(opts.consumerRoot),
|
|
2037
|
+
configPath: opts.config
|
|
2038
|
+
});
|
|
2039
|
+
});
|
|
2040
|
+
const user = program.command("user").description("User administration (local auth)");
|
|
2041
|
+
user.command("create").description("Create or update a user in the database when CodemationConfig.auth.kind is \"local\". Uses DATABASE_URL or configured database URL.").requiredOption("--email <email>", "Login email").requiredOption("--password <password>", "Plain password (stored as a bcrypt hash)").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").option("--config <path>", "Override path to codemation.config.ts / .js").action(async (opts) => {
|
|
2042
|
+
await this.userCreateCommand.execute(opts);
|
|
2043
|
+
});
|
|
2044
|
+
user.command("list").description("List users in the database when CodemationConfig.auth.kind is \"local\". Uses DATABASE_URL or configured database URL.").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").option("--config <path>", "Override path to codemation.config.ts / .js").action(async (opts) => {
|
|
2045
|
+
await this.userListCommand.execute(opts);
|
|
2046
|
+
});
|
|
2047
|
+
await program.parseAsync(argv, { from: "user" });
|
|
2048
|
+
}
|
|
2049
|
+
readCliPackageVersion() {
|
|
2050
|
+
try {
|
|
2051
|
+
const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
2052
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
2053
|
+
return typeof parsed.version === "string" ? parsed.version : "0.0.0";
|
|
2054
|
+
} catch {
|
|
2055
|
+
return "0.0.0";
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
|
|
2060
|
+
//#endregion
|
|
2061
|
+
//#region src/path/CliPathResolver.ts
|
|
2062
|
+
var CliPathResolver = class {
|
|
2063
|
+
async resolve(consumerStartPath) {
|
|
2064
|
+
const consumerRoot = path.resolve(consumerStartPath);
|
|
2065
|
+
return {
|
|
2066
|
+
consumerRoot,
|
|
2067
|
+
repoRoot: await this.detectWorkspaceRoot(consumerRoot) ?? consumerRoot
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
async detectWorkspaceRoot(startDirectory) {
|
|
2071
|
+
let currentDirectory = path.resolve(startDirectory);
|
|
2072
|
+
while (true) {
|
|
2073
|
+
if (await this.exists(path.resolve(currentDirectory, "pnpm-workspace.yaml"))) return currentDirectory;
|
|
2074
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
2075
|
+
if (parentDirectory === currentDirectory) return null;
|
|
2076
|
+
currentDirectory = parentDirectory;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
async exists(filePath) {
|
|
2080
|
+
try {
|
|
2081
|
+
await access(filePath);
|
|
2082
|
+
return true;
|
|
2083
|
+
} catch {
|
|
2084
|
+
return false;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
|
|
2089
|
+
//#endregion
|
|
2090
|
+
//#region src/runtime/TypeScriptRuntimeConfigurator.ts
|
|
2091
|
+
var TypeScriptRuntimeConfigurator = class {
|
|
2092
|
+
configure(repoRoot) {
|
|
2093
|
+
process$1.env.CODEMATION_TSCONFIG_PATH = path.resolve(repoRoot, "tsconfig.base.json");
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
|
|
2097
|
+
//#endregion
|
|
2098
|
+
//#region src/user/LocalUserCreator.ts
|
|
2099
|
+
var LocalUserCreator = class {
|
|
2100
|
+
log = new ServerLoggerFactory(logLevelPolicyFactory).create("codemation-cli.user");
|
|
2101
|
+
constructor(userAdminBootstrap) {
|
|
2102
|
+
this.userAdminBootstrap = userAdminBootstrap;
|
|
2103
|
+
}
|
|
2104
|
+
async run(options) {
|
|
2105
|
+
const email = options.email;
|
|
2106
|
+
const password = options.password;
|
|
2107
|
+
await this.userAdminBootstrap.withSession({
|
|
2108
|
+
consumerRoot: options.consumerRoot,
|
|
2109
|
+
configPath: options.configPath
|
|
2110
|
+
}, async (session) => {
|
|
2111
|
+
const result = await session.getCommandBus().execute(new UpsertLocalBootstrapUserCommand(email, password));
|
|
2112
|
+
this.log.info(result.outcome === "created" ? `Created local user: ${email}` : `Updated local user: ${email}`);
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
//#endregion
|
|
2118
|
+
//#region src/user/CliDatabaseUrlDescriptor.ts
|
|
2119
|
+
/**
|
|
2120
|
+
* Formats a database URL for CLI messages without exposing credentials (no user/password).
|
|
2121
|
+
*/
|
|
2122
|
+
var CliDatabaseUrlDescriptor = class {
|
|
2123
|
+
describePersistence(persistence) {
|
|
2124
|
+
if (persistence.kind === "none") return "none";
|
|
2125
|
+
if (persistence.kind === "postgresql") return this.describeForDisplay(persistence.databaseUrl);
|
|
2126
|
+
return `PGlite (${persistence.dataDir})`;
|
|
2127
|
+
}
|
|
2128
|
+
describeForDisplay(databaseUrl) {
|
|
2129
|
+
if (!databaseUrl || databaseUrl.trim().length === 0) return "unknown database target";
|
|
2130
|
+
try {
|
|
2131
|
+
const u = new URL(databaseUrl);
|
|
2132
|
+
const pathPart = u.pathname.replace(/^\//, "").split(/[?#]/)[0] ?? "";
|
|
2133
|
+
const databaseName = pathPart.length > 0 ? pathPart : "(default)";
|
|
2134
|
+
const defaultPort = u.protocol === "postgresql:" || u.protocol === "postgres:" ? "5432" : "";
|
|
2135
|
+
const port = u.port || defaultPort;
|
|
2136
|
+
return `database "${databaseName}" on ${port ? `${u.hostname}:${port}` : u.hostname}`;
|
|
2137
|
+
} catch {
|
|
2138
|
+
return "configured database (URL not shown)";
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2143
|
+
//#endregion
|
|
2144
|
+
//#region src/bootstrap/CodemationCliApplicationSession.ts
|
|
2145
|
+
/**
|
|
2146
|
+
* Opens a {@link CodemationApplication} with persistence + command/query buses (no HTTP/WebSocket servers),
|
|
2147
|
+
* for CLI tools that dispatch application commands or queries (e.g. user admin).
|
|
2148
|
+
*/
|
|
2149
|
+
var CodemationCliApplicationSession = class CodemationCliApplicationSession {
|
|
2150
|
+
constructor(application) {
|
|
2151
|
+
this.application = application;
|
|
2152
|
+
}
|
|
2153
|
+
static async open(args) {
|
|
2154
|
+
const app = new CodemationApplication().useConfig(args.resolution.config);
|
|
2155
|
+
await app.bootCli(new CodemationBootstrapRequest({
|
|
2156
|
+
repoRoot: args.bootstrap.repoRoot,
|
|
2157
|
+
consumerRoot: args.bootstrap.consumerRoot,
|
|
2158
|
+
env: args.bootstrap.env,
|
|
2159
|
+
workflowSources: args.resolution.workflowSources
|
|
2160
|
+
}));
|
|
2161
|
+
return new CodemationCliApplicationSession(app);
|
|
2162
|
+
}
|
|
2163
|
+
getPrismaClient() {
|
|
2164
|
+
const container = this.getContainer();
|
|
2165
|
+
if (!container.isRegistered(PrismaClient, true)) return;
|
|
2166
|
+
return container.resolve(PrismaClient);
|
|
2167
|
+
}
|
|
2168
|
+
getCommandBus() {
|
|
2169
|
+
return this.getContainer().resolve(ApplicationTokens.CommandBus);
|
|
2170
|
+
}
|
|
2171
|
+
getQueryBus() {
|
|
2172
|
+
return this.getContainer().resolve(ApplicationTokens.QueryBus);
|
|
2173
|
+
}
|
|
2174
|
+
async close() {
|
|
2175
|
+
await this.application.stop({ stopWebsocketServer: false });
|
|
2176
|
+
}
|
|
2177
|
+
getContainer() {
|
|
2178
|
+
return this.application.getContainer();
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
|
|
2182
|
+
//#endregion
|
|
2183
|
+
//#region src/user/UserAdminCliBootstrap.ts
|
|
2184
|
+
/**
|
|
2185
|
+
* Shared env/config/session wiring for `codemation user *` commands (local auth + database).
|
|
2186
|
+
*/
|
|
2187
|
+
var UserAdminCliBootstrap = class {
|
|
2188
|
+
constructor(configLoader, pathResolver, consumerDotenvLoader, tsconfigPreparation, databasePersistenceResolver) {
|
|
2189
|
+
this.configLoader = configLoader;
|
|
2190
|
+
this.pathResolver = pathResolver;
|
|
2191
|
+
this.consumerDotenvLoader = consumerDotenvLoader;
|
|
2192
|
+
this.tsconfigPreparation = tsconfigPreparation;
|
|
2193
|
+
this.databasePersistenceResolver = databasePersistenceResolver;
|
|
2194
|
+
}
|
|
2195
|
+
async withSession(options, fn) {
|
|
2196
|
+
const consumerRoot = options.consumerRoot ?? process.cwd();
|
|
2197
|
+
this.consumerDotenvLoader.load(consumerRoot);
|
|
2198
|
+
this.tsconfigPreparation.applyWorkspaceTsconfigForTsxIfPresent(consumerRoot);
|
|
2199
|
+
const resolution = await this.configLoader.load({
|
|
2200
|
+
consumerRoot,
|
|
2201
|
+
configPathOverride: options.configPath
|
|
2202
|
+
});
|
|
2203
|
+
if (resolution.config.auth?.kind !== "local") throw new Error("Codemation user commands require CodemationConfig.auth.kind to be \"local\".");
|
|
2204
|
+
if (this.databasePersistenceResolver.resolve({
|
|
2205
|
+
runtimeConfig: resolution.config.runtime ?? {},
|
|
2206
|
+
env: process.env,
|
|
2207
|
+
consumerRoot
|
|
2208
|
+
}).kind === "none") throw new Error("Database persistence is not configured. Set CodemationConfig.runtime.database (postgresql URL or PGlite).");
|
|
2209
|
+
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
2210
|
+
const session = await CodemationCliApplicationSession.open({
|
|
2211
|
+
resolution,
|
|
2212
|
+
bootstrap: new CodemationBootstrapRequest({
|
|
2213
|
+
repoRoot: paths.repoRoot,
|
|
2214
|
+
consumerRoot,
|
|
2215
|
+
env: process.env,
|
|
2216
|
+
workflowSources: resolution.workflowSources
|
|
2217
|
+
})
|
|
2218
|
+
});
|
|
2219
|
+
try {
|
|
2220
|
+
return await fn(session);
|
|
2221
|
+
} finally {
|
|
2222
|
+
await session.close();
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
//#endregion
|
|
2228
|
+
//#region src/user/UserAdminCliOptionsParser.ts
|
|
2229
|
+
var UserAdminCliOptionsParser = class {
|
|
2230
|
+
parse(opts) {
|
|
2231
|
+
return {
|
|
2232
|
+
consumerRoot: opts.consumerRoot !== void 0 && opts.consumerRoot.trim().length > 0 ? path.resolve(process$1.cwd(), opts.consumerRoot.trim()) : void 0,
|
|
2233
|
+
configPath: opts.config !== void 0 && opts.config.trim().length > 0 ? opts.config.trim() : void 0
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
//#endregion
|
|
2239
|
+
//#region src/user/UserAdminConsumerDotenvLoader.ts
|
|
2240
|
+
/**
|
|
2241
|
+
* Loads the consumer project's `.env` for `codemation user *` commands.
|
|
2242
|
+
* Uses {@link loadDotenv}'s `override: true` so values in the consumer file win over variables
|
|
2243
|
+
* already present in the process environment (e.g. a leftover `DATABASE_URL` from the shell).
|
|
2244
|
+
* Otherwise a misconfigured `.env` is silently ignored and the CLI may connect to a different database,
|
|
2245
|
+
* surfacing misleading results such as "No users found."
|
|
2246
|
+
*/
|
|
2247
|
+
var UserAdminConsumerDotenvLoader = class {
|
|
2248
|
+
load(consumerRoot) {
|
|
2249
|
+
config({
|
|
2250
|
+
path: path.resolve(consumerRoot, ".env"),
|
|
2251
|
+
override: true,
|
|
2252
|
+
quiet: true
|
|
2253
|
+
});
|
|
2254
|
+
config({
|
|
2255
|
+
path: path.resolve(consumerRoot, ".env.local"),
|
|
2256
|
+
override: true,
|
|
2257
|
+
quiet: true
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
|
|
2262
|
+
//#endregion
|
|
2263
|
+
//#region src/CliProgramFactory.ts
|
|
2264
|
+
const loggerFactory = new ServerLoggerFactory(logLevelPolicyFactory);
|
|
2265
|
+
/**
|
|
2266
|
+
* Single composition root for the CLI: constructs the object graph and returns {@link CliProgram}.
|
|
2267
|
+
* No tsyringe; keeps the package thin while commands remain constructor-injected.
|
|
2268
|
+
*/
|
|
2269
|
+
var CliProgramFactory = class {
|
|
2270
|
+
create() {
|
|
2271
|
+
const cliLogger = loggerFactory.create("codemation-cli");
|
|
2272
|
+
const pathResolver = new CliPathResolver();
|
|
2273
|
+
const pluginDiscovery = new CodemationPluginDiscovery();
|
|
2274
|
+
const artifactsPublisher = new ConsumerBuildArtifactsPublisher();
|
|
2275
|
+
const tsRuntime = new TypeScriptRuntimeConfigurator();
|
|
2276
|
+
const outputBuilderLoader = new ConsumerOutputBuilderLoader();
|
|
2277
|
+
const sourceMapNodeOptions = new SourceMapNodeOptions();
|
|
2278
|
+
const tsconfigPreparation = new ConsumerCliTsconfigPreparation();
|
|
2279
|
+
const databasePersistenceResolver = new DatabasePersistenceResolver();
|
|
2280
|
+
const userAdminBootstrap = new UserAdminCliBootstrap(new CodemationConsumerConfigLoader(), pathResolver, new UserAdminConsumerDotenvLoader(), tsconfigPreparation, databasePersistenceResolver);
|
|
2281
|
+
const hostPackageRoot = new HostPackageRootResolver().resolveHostPackageRoot();
|
|
2282
|
+
const userAdminCliOptionsParser = new UserAdminCliOptionsParser();
|
|
2283
|
+
const databaseMigrationsApplyService = new DatabaseMigrationsApplyService(cliLogger, new UserAdminConsumerDotenvLoader(), tsconfigPreparation, new CodemationConsumerConfigLoader(), new ConsumerDatabaseConnectionResolver(), new CliDatabaseUrlDescriptor(), hostPackageRoot, new PrismaMigrationDeployer());
|
|
2284
|
+
const buildOptionsParser = new ConsumerBuildOptionsParser();
|
|
2285
|
+
const devConsumerPublishBootstrap = new DevConsumerPublishBootstrap(cliLogger, pluginDiscovery, artifactsPublisher, outputBuilderLoader, buildOptionsParser);
|
|
2286
|
+
return new CliProgram(buildOptionsParser, new BuildCommand(cliLogger, pathResolver, pluginDiscovery, artifactsPublisher, tsRuntime, outputBuilderLoader), new DevCommand(pathResolver, pluginDiscovery, tsRuntime, new DevLockFactory(), new DevSourceWatcherFactory(), cliLogger, new DevSessionServicesBuilder(loggerFactory).build(), databaseMigrationsApplyService, new DevBootstrapSummaryFetcher(), new DevCliBannerRenderer(), devConsumerPublishBootstrap, new ConsumerEnvDotenvFilePredicate(), new DevTrackedProcessTreeKiller()), new ServeWebCommand(pathResolver, pluginDiscovery, artifactsPublisher, tsRuntime, sourceMapNodeOptions, outputBuilderLoader, new ConsumerEnvLoader(), new ListenPortResolver()), new ServeWorkerCommand(sourceMapNodeOptions), new DbMigrateCommand(databaseMigrationsApplyService), new UserCreateCommand(new LocalUserCreator(userAdminBootstrap), userAdminCliOptionsParser), new UserListCommand(cliLogger, userAdminBootstrap, new CliDatabaseUrlDescriptor(), userAdminCliOptionsParser));
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
//#endregion
|
|
2291
|
+
//#region src/CliBin.ts
|
|
2292
|
+
var CliBin = class {
|
|
2293
|
+
static async run(argv) {
|
|
2294
|
+
try {
|
|
2295
|
+
await new CliProgramFactory().create().run([...argv]);
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
console.error("codemation:", error);
|
|
2298
|
+
process$1.exitCode = 1;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
|
|
2303
|
+
//#endregion
|
|
2304
|
+
export { CliProgram as a, CliPathResolver as i, CliProgramFactory as n, ConsumerOutputBuilder as o, CodemationCliApplicationSession as r, ConsumerBuildOptionsParser as s, CliBin as t };
|