@codemation/cli 0.0.4 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +20 -26
  2. package/dist/{CliBin-900C8Din.js → CliBin-Bx1lFBi5.js} +1125 -340
  3. package/dist/bin.js +1 -1
  4. package/dist/index.d.ts +669 -197
  5. package/dist/index.js +1 -1
  6. package/package.json +9 -6
  7. package/src/CliProgramFactory.ts +23 -8
  8. package/src/Program.ts +7 -3
  9. package/src/bootstrap/CodemationCliApplicationSession.ts +17 -19
  10. package/src/commands/DevCommand.ts +302 -158
  11. package/src/commands/ServeWebCommand.ts +26 -1
  12. package/src/commands/ServeWorkerCommand.ts +46 -30
  13. package/src/commands/devCommandLifecycle.types.ts +7 -9
  14. package/src/database/ConsumerDatabaseConnectionResolver.ts +55 -9
  15. package/src/database/DatabaseMigrationsApplyService.ts +2 -2
  16. package/src/dev/Builder.ts +3 -14
  17. package/src/dev/CliDevProxyServer.ts +447 -0
  18. package/src/dev/CliDevProxyServerFactory.ts +7 -0
  19. package/src/dev/DevApiRuntimeFactory.ts +44 -0
  20. package/src/dev/DevApiRuntimeHost.ts +130 -0
  21. package/src/dev/DevApiRuntimeServer.ts +107 -0
  22. package/src/dev/DevApiRuntimeTypes.ts +24 -0
  23. package/src/dev/DevAuthSettingsLoader.ts +9 -3
  24. package/src/dev/DevBootstrapSummaryFetcher.ts +1 -1
  25. package/src/dev/DevHttpProbe.ts +8 -4
  26. package/src/dev/DevNextHostEnvironmentBuilder.ts +65 -3
  27. package/src/dev/DevRebuildQueue.ts +54 -0
  28. package/src/dev/DevRebuildQueueFactory.ts +7 -0
  29. package/src/dev/DevSessionPortsResolver.ts +2 -2
  30. package/src/dev/DevSessionServices.ts +2 -4
  31. package/src/dev/DevSourceChangeClassifier.ts +59 -0
  32. package/src/dev/WatchRootsResolver.ts +6 -4
  33. package/src/runtime/NextHostConsumerServerCommandFactory.ts +11 -2
  34. package/src/runtime/TypeScriptRuntimeConfigurator.ts +7 -0
  35. package/src/user/CliDatabaseUrlDescriptor.ts +2 -2
  36. package/src/user/UserAdminCliBootstrap.ts +9 -21
  37. package/codemation-cli-0.0.3.tgz +0 -0
  38. package/src/dev/DevSourceRestartCoordinator.ts +0 -48
  39. package/src/dev/DevelopmentGatewayNotifier.ts +0 -35
  40. package/src/dev/RuntimeToolEntrypointResolver.ts +0 -47
@@ -1,23 +1,28 @@
1
1
  import { createRequire } from "node:module";
2
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";
3
+ import { AppConfigLoader, CodemationConsumerConfigLoader, CodemationFrontendAuthSnapshotFactory, CodemationPluginDiscovery, FrontendAppConfigJsonCodec, WorkflowDiscoveryPathSegmentsComputer, WorkflowModulePathFinder } from "@codemation/host/server";
4
+ import { ApiPaths, AppContainerFactory, AppContainerLifecycle, ApplicationTokens, CodemationFrontendAuthSnapshotFactory as CodemationFrontendAuthSnapshotFactory$1, DatabaseMigrations, FrontendAppConfigJsonCodec as FrontendAppConfigJsonCodec$1, ListUserAccountsQuery, PrismaClient, UpsertLocalBootstrapUserCommand, WorkerRuntime } from "@codemation/host";
5
+ import { AppContainerFactory as AppContainerFactory$1, AppContainerLifecycle as AppContainerLifecycle$1, CodemationHonoApiApp, CodemationPluginListMerger, FrontendRuntime, ServerLoggerFactory, logLevelPolicyFactory } from "@codemation/host/next/server";
5
6
  import { randomUUID } from "node:crypto";
6
7
  import { access, copyFile, cp, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
8
  import path from "node:path";
8
9
  import { fileURLToPath, pathToFileURL } from "node:url";
9
10
  import { spawn } from "node:child_process";
10
- import { ApiPaths, ApplicationTokens, CodemationApplication, CodemationBootstrapRequest, ListUserAccountsQuery, PrismaClient, UpsertLocalBootstrapUserCommand } from "@codemation/host";
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { config, parse } from "dotenv";
13
13
  import { watch } from "chokidar";
14
14
  import ts from "typescript";
15
- import { DatabasePersistenceResolver, PrismaMigrationDeployer } from "@codemation/host/persistence";
15
+ import { PrismaMigrationDeployer } from "@codemation/host/persistence";
16
16
  import boxen from "boxen";
17
17
  import chalk from "chalk";
18
18
  import figlet from "figlet";
19
+ import { createServer } from "node:http";
20
+ import httpProxy from "http-proxy";
21
+ import { WebSocket, WebSocketServer } from "ws";
22
+ import { serve } from "@hono/node-server";
23
+ import { Hono } from "hono";
19
24
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
20
- import { createServer } from "node:net";
25
+ import { createServer as createServer$1 } from "node:net";
21
26
  import { Command } from "commander";
22
27
 
23
28
  //#region src/build/ConsumerBuildArtifactsPublisher.ts
@@ -120,9 +125,8 @@ var DbMigrateCommand = class {
120
125
  //#region src/commands/DevCommand.ts
121
126
  var DevCommand = class {
122
127
  require = createRequire(import.meta.url);
123
- constructor(pathResolver, pluginDiscovery, tsRuntime, devLockFactory, devSourceWatcherFactory, cliLogger, session, databaseMigrationsApplyService, devBootstrapSummaryFetcher, devCliBannerRenderer, devConsumerPublishBootstrap, consumerEnvDotenvFilePredicate, devTrackedProcessTreeKiller, nextHostConsumerServerCommandFactory) {
128
+ constructor(pathResolver, tsRuntime, devLockFactory, devSourceWatcherFactory, cliLogger, session, databaseMigrationsApplyService, devBootstrapSummaryFetcher, devCliBannerRenderer, devConsumerPublishBootstrap, consumerEnvDotenvFilePredicate, devTrackedProcessTreeKiller, nextHostConsumerServerCommandFactory, devApiRuntimeFactory, cliDevProxyServerFactory, devRebuildQueueFactory) {
124
129
  this.pathResolver = pathResolver;
125
- this.pluginDiscovery = pluginDiscovery;
126
130
  this.tsRuntime = tsRuntime;
127
131
  this.devLockFactory = devLockFactory;
128
132
  this.devSourceWatcherFactory = devSourceWatcherFactory;
@@ -135,14 +139,17 @@ var DevCommand = class {
135
139
  this.consumerEnvDotenvFilePredicate = consumerEnvDotenvFilePredicate;
136
140
  this.devTrackedProcessTreeKiller = devTrackedProcessTreeKiller;
137
141
  this.nextHostConsumerServerCommandFactory = nextHostConsumerServerCommandFactory;
142
+ this.devApiRuntimeFactory = devApiRuntimeFactory;
143
+ this.cliDevProxyServerFactory = cliDevProxyServerFactory;
144
+ this.devRebuildQueueFactory = devRebuildQueueFactory;
138
145
  }
139
- async execute(consumerRoot) {
140
- const paths = await this.pathResolver.resolve(consumerRoot);
146
+ async execute(args) {
147
+ const paths = await this.pathResolver.resolve(args.consumerRoot);
141
148
  this.devCliBannerRenderer.renderBrandHeader();
142
149
  this.tsRuntime.configure(paths.repoRoot);
143
150
  await this.databaseMigrationsApplyService.applyForConsumer(paths.consumerRoot);
144
151
  await this.devConsumerPublishBootstrap.ensurePublished(paths);
145
- const devMode = this.resolveDevModeFromEnv();
152
+ const devMode = this.resolveDevMode(args);
146
153
  const { nextPort, gatewayPort } = await this.session.sessionPorts.resolve({
147
154
  devMode,
148
155
  portEnv: process$1.env.PORT,
@@ -151,66 +158,56 @@ var DevCommand = class {
151
158
  const devLock = this.devLockFactory.create();
152
159
  await devLock.acquire({
153
160
  consumerRoot: paths.consumerRoot,
154
- nextPort: devMode === "framework" ? nextPort : gatewayPort
161
+ nextPort: devMode === "watch-framework" ? nextPort : gatewayPort
155
162
  });
156
163
  const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
157
164
  const watcher = this.devSourceWatcherFactory.create();
165
+ const processState = this.createInitialProcessState();
166
+ let proxyServer = null;
158
167
  try {
159
168
  const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
160
- const processState = this.createInitialProcessState();
161
169
  const stopPromise = this.wireStopPromise(processState);
162
- const uiProxyBase = await this.startConsumerUiProxyWhenNeeded(prepared, processState);
170
+ const uiProxyBase = await this.startPackagedUiWhenNeeded(prepared, processState);
171
+ proxyServer = await this.startProxyServer(prepared.gatewayPort, uiProxyBase);
163
172
  const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
164
- await this.spawnGatewayChildAndWaitForHealth(prepared, processState, gatewayBaseUrl, uiProxyBase);
173
+ await this.bootInitialRuntime(prepared, processState, proxyServer);
165
174
  await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
166
175
  const initialSummary = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
167
176
  if (initialSummary) this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
168
- this.bindShutdownSignalsToChildProcesses(processState);
169
- this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
170
- await this.startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl);
171
- this.logConsumerDevHintWhenNeeded(devMode, gatewayPort);
177
+ this.bindShutdownSignalsToChildProcesses(processState, proxyServer);
178
+ await this.spawnDevUiWhenNeeded(prepared, processState, gatewayBaseUrl);
179
+ await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl, proxyServer);
180
+ this.logPackagedUiDevHintWhenNeeded(devMode, gatewayPort);
172
181
  await stopPromise;
173
182
  } finally {
183
+ processState.stopRequested = true;
184
+ await this.stopLiveProcesses(processState, proxyServer);
174
185
  await watcher.stop();
175
186
  await devLock.release();
176
187
  }
177
188
  }
178
- resolveDevModeFromEnv() {
179
- return process$1.env.CODEMATION_DEV_MODE === "framework" ? "framework" : "consumer";
189
+ resolveDevMode(args) {
190
+ if (args.watchFramework === true || process$1.env.CODEMATION_DEV_MODE === "framework") return "watch-framework";
191
+ return "packaged-ui";
180
192
  }
181
193
  async prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings) {
182
- const developmentServerToken = this.session.devAuthLoader.resolveDevelopmentServerToken(process$1.env.CODEMATION_DEV_SERVER_TOKEN);
183
- const gatewayEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
184
- packageName: "@codemation/dev-gateway",
185
- repoRoot: paths.repoRoot,
186
- sourceEntrypoint: "packages/dev-gateway/src/bin.ts"
187
- });
188
- const runtimeEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
189
- packageName: "@codemation/runtime-dev",
190
- repoRoot: paths.repoRoot,
191
- sourceEntrypoint: "packages/runtime-dev/src/bin.ts"
192
- });
193
- const runtimeWorkingDirectory = paths.repoRoot ?? paths.consumerRoot;
194
- const consumerEnv = this.session.consumerEnvLoader.load(paths.consumerRoot);
195
194
  return {
196
195
  paths,
197
196
  devMode,
198
197
  nextPort,
199
198
  gatewayPort,
200
199
  authSettings,
201
- developmentServerToken,
202
- gatewayEntrypoint,
203
- runtimeEntrypoint,
204
- runtimeWorkingDirectory,
205
- discoveredPluginPackagesJson: JSON.stringify(await this.pluginDiscovery.discover(paths.consumerRoot)),
206
- consumerEnv
200
+ developmentServerToken: this.session.devAuthLoader.resolveDevelopmentServerToken(process$1.env.CODEMATION_DEV_SERVER_TOKEN),
201
+ consumerEnv: this.session.consumerEnvLoader.load(paths.consumerRoot)
207
202
  };
208
203
  }
209
204
  createInitialProcessState() {
210
205
  return {
211
- currentGateway: null,
212
- currentNextHost: null,
213
- currentUiNext: null,
206
+ currentDevUi: null,
207
+ currentPackagedUi: null,
208
+ currentPackagedUiBaseUrl: null,
209
+ currentRuntime: null,
210
+ isRestartingUi: false,
214
211
  stopRequested: false,
215
212
  stopResolve: null,
216
213
  stopReject: null
@@ -225,92 +222,64 @@ var DevCommand = class {
225
222
  gatewayBaseHttpUrl(gatewayPort) {
226
223
  return `http://127.0.0.1:${gatewayPort}`;
227
224
  }
228
- /**
229
- * Consumer mode: run `next start` for the host UI and wait until it responds, so the gateway can proxy to it.
230
- * Framework mode: no separate UI child (Next runs in dev later).
231
- */
232
- async startConsumerUiProxyWhenNeeded(prepared, state) {
233
- if (prepared.devMode !== "consumer") return "";
225
+ async startPackagedUiWhenNeeded(prepared, state) {
226
+ if (prepared.devMode !== "packaged-ui") return "";
234
227
  const websocketPort = prepared.gatewayPort;
235
- const uiPort = await this.session.loopbackPortAllocator.allocate();
236
- const uiProxyBase = `http://127.0.0.1:${uiPort}`;
228
+ const uiProxyBase = state.currentPackagedUiBaseUrl ?? `http://127.0.0.1:${await this.session.loopbackPortAllocator.allocate()}`;
229
+ state.currentPackagedUiBaseUrl = uiProxyBase;
230
+ await this.spawnPackagedUi(prepared, state, prepared.authSettings, websocketPort, uiProxyBase);
231
+ return uiProxyBase;
232
+ }
233
+ async spawnPackagedUi(prepared, state, authSettings, websocketPort, uiProxyBase) {
237
234
  const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
238
235
  const nextHostRoot = path.dirname(nextHostPackageJsonPath);
239
236
  const nextHostCommand = await this.nextHostConsumerServerCommandFactory.create({ nextHostRoot });
240
237
  const consumerOutputManifestPath = path.resolve(prepared.paths.consumerRoot, ".codemation", "output", "current.json");
241
- state.currentUiNext = spawn(nextHostCommand.command, nextHostCommand.args, {
238
+ const uiPort = Number(new URL(uiProxyBase).port);
239
+ const nextHostEnvironment = this.session.nextHostEnvBuilder.buildConsumerUiProxy({
240
+ authConfigJson: authSettings.authConfigJson,
241
+ authSecret: authSettings.authSecret,
242
+ consumerRoot: prepared.paths.consumerRoot,
243
+ consumerOutputManifestPath,
244
+ developmentServerToken: prepared.developmentServerToken,
245
+ nextPort: uiPort,
246
+ publicBaseUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
247
+ runtimeDevUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
248
+ skipUiAuth: authSettings.skipUiAuth,
249
+ websocketPort
250
+ });
251
+ state.currentPackagedUi = spawn(nextHostCommand.command, nextHostCommand.args, {
242
252
  cwd: nextHostCommand.cwd,
243
253
  ...this.devDetachedChildSpawnOptions(),
244
- env: {
245
- ...process$1.env,
246
- ...prepared.consumerEnv,
247
- PORT: String(uiPort),
248
- CODEMATION_AUTH_CONFIG_JSON: prepared.authSettings.authConfigJson,
249
- CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
250
- CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
251
- CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
252
- AUTH_SECRET: prepared.authSettings.authSecret,
253
- NEXTAUTH_SECRET: prepared.authSettings.authSecret,
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
- }
254
+ env: nextHostEnvironment
262
255
  });
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
- }
256
+ state.currentPackagedUi.on("error", (error) => {
257
+ if (state.stopRequested || state.isRestartingUi) return;
258
+ state.stopRequested = true;
259
+ state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
268
260
  });
269
- state.currentUiNext.on("exit", (code) => {
270
- if (state.stopRequested) return;
261
+ state.currentPackagedUi.on("exit", (code) => {
262
+ if (state.stopRequested || state.isRestartingUi) return;
271
263
  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}.`));
264
+ state.stopReject?.(/* @__PURE__ */ new Error(`next start (packaged UI) exited unexpectedly with code ${code ?? 0}.`));
274
265
  });
275
266
  await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
276
- return uiProxyBase;
277
267
  }
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}.`));
268
+ async startProxyServer(gatewayPort, uiProxyBase) {
269
+ const proxyServer = this.cliDevProxyServerFactory.create(gatewayPort);
270
+ proxyServer.setUiProxyTarget(uiProxyBase.length > 0 ? uiProxyBase : null);
271
+ await proxyServer.start();
272
+ await this.session.devHttpProbe.waitUntilGatewayHealthy(this.gatewayBaseHttpUrl(gatewayPort));
273
+ return proxyServer;
274
+ }
275
+ async bootInitialRuntime(prepared, state, proxyServer) {
276
+ const runtime = await this.createRuntime(prepared);
277
+ state.currentRuntime = runtime;
278
+ await proxyServer.activateRuntime({
279
+ httpPort: runtime.httpPort,
280
+ workflowWebSocketPort: runtime.workflowWebSocketPort
312
281
  });
313
- await this.session.devHttpProbe.waitUntilGatewayHealthy(gatewayBaseUrl);
282
+ proxyServer.setBuildStatus("idle");
314
283
  }
315
284
  devDetachedChildSpawnOptions() {
316
285
  return process$1.platform === "win32" ? {
@@ -322,20 +291,14 @@ var DevCommand = class {
322
291
  detached: true
323
292
  };
324
293
  }
325
- bindShutdownSignalsToChildProcesses(state) {
294
+ bindShutdownSignalsToChildProcesses(state, proxyServer) {
326
295
  let shutdownInProgress = false;
327
296
  const runShutdown = async () => {
328
297
  if (shutdownInProgress) return;
329
298
  shutdownInProgress = true;
330
299
  state.stopRequested = true;
331
300
  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)));
301
+ await this.stopLiveProcesses(state, proxyServer);
339
302
  process$1.stdout.write("[codemation] Stopped.\n");
340
303
  state.stopResolve?.();
341
304
  };
@@ -347,24 +310,24 @@ var DevCommand = class {
347
310
  runShutdown();
348
311
  });
349
312
  }
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;
313
+ async spawnDevUiWhenNeeded(prepared, state, gatewayBaseUrl) {
314
+ if (prepared.devMode !== "watch-framework") return;
315
+ await this.spawnDevUi(prepared, state, gatewayBaseUrl, prepared.authSettings);
316
+ }
317
+ async spawnDevUi(prepared, state, gatewayBaseUrl, authSettings) {
355
318
  const websocketPort = prepared.gatewayPort;
356
319
  const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
357
320
  const nextHostRoot = path.dirname(nextHostPackageJsonPath);
358
321
  const nextHostEnvironment = this.session.nextHostEnvBuilder.build({
359
- authConfigJson: prepared.authSettings.authConfigJson,
322
+ authConfigJson: authSettings.authConfigJson,
360
323
  consumerRoot: prepared.paths.consumerRoot,
361
324
  developmentServerToken: prepared.developmentServerToken,
362
325
  nextPort: prepared.nextPort,
363
- skipUiAuth: prepared.authSettings.skipUiAuth,
326
+ skipUiAuth: authSettings.skipUiAuth,
364
327
  websocketPort,
365
328
  runtimeDevUrl: gatewayBaseUrl
366
329
  });
367
- state.currentNextHost = spawn("pnpm", [
330
+ state.currentDevUi = spawn("pnpm", [
368
331
  "exec",
369
332
  "next",
370
333
  "dev"
@@ -373,26 +336,28 @@ var DevCommand = class {
373
336
  ...this.devDetachedChildSpawnOptions(),
374
337
  env: nextHostEnvironment
375
338
  });
376
- state.currentNextHost.on("exit", (code) => {
339
+ state.currentDevUi.on("exit", (code) => {
377
340
  const normalizedCode = code ?? 0;
378
- if (state.stopRequested) return;
341
+ if (state.stopRequested || state.isRestartingUi) return;
379
342
  if (normalizedCode === 0) {
380
343
  state.stopRequested = true;
381
- if (state.currentGateway?.exitCode === null) this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
382
344
  state.stopResolve?.();
383
345
  return;
384
346
  }
385
347
  state.stopRequested = true;
386
348
  state.stopReject?.(/* @__PURE__ */ new Error(`next host exited with code ${normalizedCode}.`));
387
349
  });
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
- }
350
+ state.currentDevUi.on("error", (error) => {
351
+ if (state.stopRequested || state.isRestartingUi) return;
352
+ state.stopRequested = true;
353
+ state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
393
354
  });
355
+ await this.session.devHttpProbe.waitUntilUrlRespondsOk(`http://127.0.0.1:${prepared.nextPort}/`);
394
356
  }
395
- async startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl) {
357
+ async startWatcherForSourceRestart(prepared, state, watcher, devMode, gatewayBaseUrl, proxyServer) {
358
+ const rebuildQueue = this.devRebuildQueueFactory.create({ run: async (request) => {
359
+ await this.runQueuedRebuild(prepared, state, gatewayBaseUrl, proxyServer, request);
360
+ } });
396
361
  await watcher.start({
397
362
  roots: this.session.watchRootsResolver.resolve({
398
363
  consumerRoot: prepared.paths.consumerRoot,
@@ -401,22 +366,119 @@ var DevCommand = class {
401
366
  }),
402
367
  onChange: async ({ changedPaths }) => {
403
368
  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");
369
+ process$1.stdout.write("\n[codemation] Consumer environment file changed (e.g. .env). Restart the `codemation dev` process so the runtime picks up updated variables (host `process.env` does not hot-reload).\n");
405
370
  return;
406
371
  }
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");
372
+ try {
373
+ const shouldRepublishConsumerOutput = this.session.sourceChangeClassifier.shouldRepublishConsumerOutput({
374
+ changedPaths,
375
+ consumerRoot: prepared.paths.consumerRoot
376
+ });
377
+ const shouldRestartUi = this.session.sourceChangeClassifier.requiresUiRestart({
378
+ changedPaths,
379
+ consumerRoot: prepared.paths.consumerRoot
380
+ });
381
+ process$1.stdout.write(shouldRestartUi ? "\n[codemation] Source change detected — rebuilding consumer, restarting the runtime, and restarting the UI…\n" : "\n[codemation] Source change detected — rebuilding consumer and restarting the runtime…\n");
382
+ await rebuildQueue.enqueue({
383
+ changedPaths,
384
+ shouldRepublishConsumerOutput,
385
+ shouldRestartUi
386
+ });
387
+ } catch (error) {
388
+ await this.failDevSessionAfterIrrecoverableSourceError(state, proxyServer, error);
389
+ }
390
+ }
391
+ });
392
+ }
393
+ async runQueuedRebuild(prepared, state, gatewayBaseUrl, proxyServer, request) {
394
+ proxyServer.setBuildStatus("building");
395
+ proxyServer.broadcastBuildStarted();
396
+ try {
397
+ if (request.shouldRepublishConsumerOutput) await this.devConsumerPublishBootstrap.ensurePublished(prepared.paths);
398
+ await this.stopCurrentRuntime(state, proxyServer);
399
+ process$1.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
400
+ const runtime = await this.createRuntime(prepared);
401
+ state.currentRuntime = runtime;
402
+ await proxyServer.activateRuntime({
403
+ httpPort: runtime.httpPort,
404
+ workflowWebSocketPort: runtime.workflowWebSocketPort
405
+ });
406
+ if (request.shouldRestartUi) await this.restartUiAfterSourceChange(prepared, state, gatewayBaseUrl);
407
+ await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
408
+ const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
409
+ if (json) this.devCliBannerRenderer.renderCompact(json);
410
+ proxyServer.setBuildStatus("idle");
411
+ proxyServer.broadcastBuildCompleted(runtime.buildVersion);
412
+ process$1.stdout.write("[codemation] Runtime ready.\n");
413
+ } catch (error) {
414
+ proxyServer.setBuildStatus("idle");
415
+ proxyServer.broadcastBuildFailed(error instanceof Error ? error.message : String(error));
416
+ throw error;
417
+ }
418
+ }
419
+ async restartUiAfterSourceChange(prepared, state, gatewayBaseUrl) {
420
+ const refreshedAuthSettings = await this.session.devAuthLoader.loadForConsumer(prepared.paths.consumerRoot);
421
+ process$1.stdout.write("[codemation] Restarting the UI process to apply source changes…\n");
422
+ state.isRestartingUi = true;
423
+ try {
424
+ if (prepared.devMode === "packaged-ui") {
425
+ await this.restartPackagedUi(prepared, state, refreshedAuthSettings);
426
+ return;
427
+ }
428
+ await this.restartDevUi(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
429
+ } finally {
430
+ state.isRestartingUi = false;
431
+ }
432
+ }
433
+ async restartPackagedUi(prepared, state, authSettings) {
434
+ if (state.currentPackagedUi && state.currentPackagedUi.exitCode === null && state.currentPackagedUi.signalCode === null) await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentPackagedUi);
435
+ state.currentPackagedUi = null;
436
+ const uiProxyBaseUrl = state.currentPackagedUiBaseUrl;
437
+ if (!uiProxyBaseUrl) throw new Error("Packaged UI proxy base URL is missing during UI restart.");
438
+ await this.spawnPackagedUi(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
439
+ }
440
+ async restartDevUi(prepared, state, gatewayBaseUrl, authSettings) {
441
+ if (state.currentDevUi && state.currentDevUi.exitCode === null && state.currentDevUi.signalCode === null) await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentDevUi);
442
+ state.currentDevUi = null;
443
+ await this.spawnDevUi(prepared, state, gatewayBaseUrl, authSettings);
444
+ }
445
+ async failDevSessionAfterIrrecoverableSourceError(state, proxyServer, error) {
446
+ const exception = error instanceof Error ? error : new Error(String(error));
447
+ state.stopRequested = true;
448
+ await this.stopLiveProcesses(state, proxyServer);
449
+ state.stopReject?.(exception);
450
+ }
451
+ async stopLiveProcesses(state, proxyServer) {
452
+ await this.stopCurrentRuntime(state, proxyServer);
453
+ const children = [];
454
+ for (const child of [state.currentPackagedUi, state.currentDevUi]) if (child && child.exitCode === null && child.signalCode === null) children.push(child);
455
+ await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
456
+ if (proxyServer) await proxyServer.stop();
457
+ }
458
+ async stopCurrentRuntime(state, proxyServer) {
459
+ const runtime = state.currentRuntime;
460
+ state.currentRuntime = null;
461
+ if (proxyServer) await proxyServer.activateRuntime(null);
462
+ if (runtime) await runtime.stop();
463
+ }
464
+ async createRuntime(prepared) {
465
+ const runtimeEnvironment = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(process$1.env, prepared.consumerEnv);
466
+ return await this.devApiRuntimeFactory.create({
467
+ consumerRoot: prepared.paths.consumerRoot,
468
+ runtimeWorkingDirectory: process$1.cwd(),
469
+ env: {
470
+ ...runtimeEnvironment,
471
+ CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
472
+ CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
473
+ NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process$1.env.NODE_OPTIONS),
474
+ WS_NO_BUFFER_UTIL: "1",
475
+ WS_NO_UTF_8_VALIDATE: "1"
414
476
  }
415
477
  });
416
478
  }
417
- logConsumerDevHintWhenNeeded(devMode, gatewayPort) {
418
- if (devMode !== "consumer") return;
419
- this.cliLogger.info(`codemation dev (consumer): open http://127.0.0.1:${gatewayPort} — uses the packaged @codemation/next-host UI. For framework UI HMR, set CODEMATION_DEV_MODE=framework.`);
479
+ logPackagedUiDevHintWhenNeeded(devMode, gatewayPort) {
480
+ if (devMode !== "packaged-ui") return;
481
+ this.cliLogger.info(`codemation dev: open http://127.0.0.1:${gatewayPort} — this uses the packaged @codemation/next-host UI. Use \`codemation dev --watch-framework\` only when working on the framework UI itself.`);
420
482
  }
421
483
  };
422
484
 
@@ -424,8 +486,9 @@ var DevCommand = class {
424
486
  //#region src/commands/ServeWebCommand.ts
425
487
  var ServeWebCommand = class {
426
488
  require = createRequire(import.meta.url);
427
- constructor(pathResolver, pluginDiscovery, artifactsPublisher, tsRuntime, sourceMapNodeOptions, outputBuilderLoader, envLoader, listenPortResolver, nextHostConsumerServerCommandFactory) {
489
+ constructor(pathResolver, configLoader, pluginDiscovery, artifactsPublisher, tsRuntime, sourceMapNodeOptions, outputBuilderLoader, envLoader, listenPortResolver, nextHostConsumerServerCommandFactory, frontendAuthSnapshotFactory, frontendAppConfigJsonCodec) {
428
490
  this.pathResolver = pathResolver;
491
+ this.configLoader = configLoader;
429
492
  this.pluginDiscovery = pluginDiscovery;
430
493
  this.artifactsPublisher = artifactsPublisher;
431
494
  this.tsRuntime = tsRuntime;
@@ -434,6 +497,8 @@ var ServeWebCommand = class {
434
497
  this.envLoader = envLoader;
435
498
  this.listenPortResolver = listenPortResolver;
436
499
  this.nextHostConsumerServerCommandFactory = nextHostConsumerServerCommandFactory;
500
+ this.frontendAuthSnapshotFactory = frontendAuthSnapshotFactory;
501
+ this.frontendAppConfigJsonCodec = frontendAppConfigJsonCodec;
437
502
  }
438
503
  async execute(consumerRoot, buildOptions) {
439
504
  const paths = await this.pathResolver.resolve(consumerRoot);
@@ -444,6 +509,15 @@ var ServeWebCommand = class {
444
509
  const nextHostRoot = path.dirname(this.require.resolve("@codemation/next-host/package.json"));
445
510
  const nextHostCommand = await this.nextHostConsumerServerCommandFactory.create({ nextHostRoot });
446
511
  const consumerEnv = this.envLoader.load(paths.consumerRoot);
512
+ const configResolution = await this.configLoader.load({ consumerRoot: paths.consumerRoot });
513
+ const frontendAuthSnapshot = this.frontendAuthSnapshotFactory.createFromResolvedInputs({
514
+ authConfig: configResolution.config.auth,
515
+ env: {
516
+ ...process$1.env,
517
+ ...consumerEnv
518
+ },
519
+ uiAuthEnabled: !(consumerEnv.NODE_ENV !== "production" && configResolution.config.auth?.allowUnauthenticatedInDevelopment === true)
520
+ });
447
521
  const nextPort = this.listenPortResolver.resolvePrimaryApplicationPort(process$1.env.PORT);
448
522
  const websocketPort = this.listenPortResolver.resolveWebsocketPortRelativeToHttp({
449
523
  nextPort,
@@ -457,6 +531,11 @@ var ServeWebCommand = class {
457
531
  ...process$1.env,
458
532
  ...consumerEnv,
459
533
  PORT: String(nextPort),
534
+ CODEMATION_FRONTEND_APP_CONFIG_JSON: this.frontendAppConfigJsonCodec.serialize({
535
+ auth: frontendAuthSnapshot,
536
+ productName: "Codemation",
537
+ logoUrl: null
538
+ }),
460
539
  CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifest.manifestPath,
461
540
  CODEMATION_CONSUMER_ROOT: paths.consumerRoot,
462
541
  CODEMATION_WS_PORT: String(websocketPort),
@@ -482,34 +561,46 @@ var ServeWebCommand = class {
482
561
  //#endregion
483
562
  //#region src/commands/ServeWorkerCommand.ts
484
563
  var ServeWorkerCommand = class {
485
- require = createRequire(import.meta.url);
486
- constructor(sourceMapNodeOptions) {
487
- this.sourceMapNodeOptions = sourceMapNodeOptions;
564
+ constructor(pathResolver, appConfigLoader, appContainerFactory) {
565
+ this.pathResolver = pathResolver;
566
+ this.appConfigLoader = appConfigLoader;
567
+ this.appContainerFactory = appContainerFactory;
488
568
  }
489
569
  async execute(consumerRoot, configPathOverride) {
490
- const workerPackageRoot = path.dirname(this.require.resolve("@codemation/worker-cli/package.json"));
491
- const args = [path.join(workerPackageRoot, "bin", "codemation-worker.js")];
492
- if (configPathOverride !== void 0 && configPathOverride.trim().length > 0) args.push("--config", path.resolve(process$1.cwd(), configPathOverride.trim()));
493
- args.push("--consumer-root", consumerRoot);
494
- const child = spawn(process$1.execPath, args, {
495
- cwd: consumerRoot,
496
- stdio: "inherit",
497
- env: {
498
- ...process$1.env,
499
- NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process$1.env.NODE_OPTIONS)
500
- }
570
+ const paths = await this.pathResolver.resolve(consumerRoot);
571
+ const loadResult = await this.appConfigLoader.load({
572
+ consumerRoot,
573
+ repoRoot: paths.repoRoot,
574
+ env: process$1.env,
575
+ configPathOverride
501
576
  });
502
- await new Promise((resolve, reject) => {
503
- child.on("exit", (code) => {
504
- if ((code ?? 0) === 0) {
505
- resolve();
506
- return;
507
- }
508
- reject(/* @__PURE__ */ new Error(`codemation-worker exited with code ${code ?? 0}.`));
509
- });
510
- child.on("error", reject);
577
+ if (loadResult.appConfig.scheduler.kind !== "bullmq") throw new Error("Worker mode requires runtime.scheduler.kind = \"bullmq\".");
578
+ const container = await this.appContainerFactory.create({
579
+ appConfig: loadResult.appConfig,
580
+ sharedWorkflowWebsocketServer: null
581
+ });
582
+ const workerQueues = loadResult.appConfig.scheduler.workerQueues.length > 0 ? loadResult.appConfig.scheduler.workerQueues : ["default"];
583
+ const handle = await container.resolve(WorkerRuntime).start(workerQueues);
584
+ await new Promise((resolve) => {
585
+ this.bindSignals(handle.stop, resolve);
511
586
  });
512
587
  }
588
+ bindSignals(stop, resolve) {
589
+ let stopping = false;
590
+ const onSignal = async () => {
591
+ if (stopping) return;
592
+ stopping = true;
593
+ try {
594
+ await stop();
595
+ } finally {
596
+ resolve();
597
+ process$1.exit(0);
598
+ }
599
+ };
600
+ process$1.on("SIGINT", () => void onSignal());
601
+ process$1.on("SIGTERM", () => void onSignal());
602
+ process$1.on("SIGQUIT", () => void onSignal());
603
+ }
513
604
  };
514
605
 
515
606
  //#endregion
@@ -1210,13 +1301,36 @@ var ConsumerOutputBuilderLoader = class {
1210
1301
  * Resolves TCP PostgreSQL vs PGlite vs none from env + {@link CodemationConfig} (same rules as the host runtime).
1211
1302
  */
1212
1303
  var ConsumerDatabaseConnectionResolver = class {
1213
- resolver = new DatabasePersistenceResolver();
1214
1304
  resolve(processEnv, config$1, consumerRoot) {
1215
- return this.resolver.resolve({
1216
- runtimeConfig: config$1.runtime ?? {},
1217
- env: processEnv,
1218
- consumerRoot
1219
- });
1305
+ const database = config$1.runtime?.database;
1306
+ if (!database) return { kind: "none" };
1307
+ if (this.resolveDatabaseKind(database.kind, database.url, processEnv) === "postgresql") {
1308
+ const databaseUrl = database.url?.trim() ?? "";
1309
+ if (!databaseUrl) throw new Error("runtime.database.kind is \"postgresql\" but no database URL was set (runtime.database.url).");
1310
+ return {
1311
+ kind: "postgresql",
1312
+ databaseUrl
1313
+ };
1314
+ }
1315
+ return {
1316
+ kind: "pglite",
1317
+ dataDir: this.resolvePgliteDataDir(database.pgliteDataDir, processEnv, consumerRoot)
1318
+ };
1319
+ }
1320
+ resolveDatabaseKind(configuredKind, databaseUrl, env) {
1321
+ const kindFromEnv = env.CODEMATION_DATABASE_KIND?.trim();
1322
+ if (kindFromEnv === "postgresql" || kindFromEnv === "pglite") return kindFromEnv;
1323
+ if (configuredKind) return configuredKind;
1324
+ const trimmedUrl = databaseUrl?.trim();
1325
+ if (trimmedUrl && (trimmedUrl.startsWith("postgresql://") || trimmedUrl.startsWith("postgres://"))) return "postgresql";
1326
+ return "pglite";
1327
+ }
1328
+ resolvePgliteDataDir(configuredPath, env, consumerRoot) {
1329
+ const envPath = env.CODEMATION_PGLITE_DATA_DIR?.trim();
1330
+ if (envPath && envPath.length > 0) return path.isAbsolute(envPath) ? envPath : path.resolve(consumerRoot, envPath);
1331
+ const trimmedConfiguredPath = configuredPath?.trim();
1332
+ if (trimmedConfiguredPath && trimmedConfiguredPath.length > 0) return path.isAbsolute(trimmedConfiguredPath) ? trimmedConfiguredPath : path.resolve(consumerRoot, trimmedConfiguredPath);
1333
+ return path.resolve(consumerRoot, ".codemation", "pglite");
1220
1334
  }
1221
1335
  };
1222
1336
 
@@ -1292,7 +1406,7 @@ var HostPackageRootResolver = class {
1292
1406
  //#endregion
1293
1407
  //#region src/dev/DevBootstrapSummaryFetcher.ts
1294
1408
  /**
1295
- * Fetches {@link DevBootstrapSummaryJson} from the dev gateway (proxied to runtime-dev).
1409
+ * Fetches {@link DevBootstrapSummaryJson} from the stable CLI-owned dev endpoint.
1296
1410
  */
1297
1411
  var DevBootstrapSummaryFetcher = class {
1298
1412
  async fetch(gatewayBaseUrl) {
@@ -1442,6 +1556,710 @@ var DevConsumerPublishBootstrap = class {
1442
1556
  }
1443
1557
  };
1444
1558
 
1559
+ //#endregion
1560
+ //#region src/dev/CliDevProxyServer.ts
1561
+ var CliDevProxyServer = class {
1562
+ proxy = httpProxy.createProxyServer({
1563
+ ws: true,
1564
+ xfwd: true
1565
+ });
1566
+ devClients = /* @__PURE__ */ new Set();
1567
+ devWss = new WebSocketServer({ noServer: true });
1568
+ workflowClients = /* @__PURE__ */ new Set();
1569
+ workflowWss = new WebSocketServer({ noServer: true });
1570
+ roomIdsByWorkflowClient = /* @__PURE__ */ new Map();
1571
+ workflowClientCountByRoomId = /* @__PURE__ */ new Map();
1572
+ activeRuntime = null;
1573
+ activeBuildStatus = "idle";
1574
+ childWorkflowSocket = null;
1575
+ server = null;
1576
+ uiProxyTarget = null;
1577
+ constructor(listenPort) {
1578
+ this.listenPort = listenPort;
1579
+ }
1580
+ async start() {
1581
+ if (this.server) return;
1582
+ this.bindDevWebSocket();
1583
+ this.bindWorkflowWebSocket();
1584
+ this.proxy.on("error", (error, _req, res) => {
1585
+ if (res && "writeHead" in res && typeof res.writeHead === "function") {
1586
+ const serverResponse = res;
1587
+ if (!serverResponse.headersSent) {
1588
+ serverResponse.writeHead(502, { "content-type": "text/plain" });
1589
+ serverResponse.end(`Bad gateway: ${error.message}`);
1590
+ }
1591
+ }
1592
+ });
1593
+ const server = createServer((req, res) => {
1594
+ this.handleHttpRequest(req, res);
1595
+ });
1596
+ server.on("upgrade", (request, socket, head) => {
1597
+ this.handleUpgrade(request, socket, head);
1598
+ });
1599
+ await new Promise((resolve, reject) => {
1600
+ server.once("error", reject);
1601
+ server.listen(this.listenPort, "127.0.0.1", () => {
1602
+ resolve();
1603
+ });
1604
+ });
1605
+ this.server = server;
1606
+ }
1607
+ async stop() {
1608
+ await this.disconnectChildWorkflowSocket();
1609
+ this.activeRuntime = null;
1610
+ const server = this.server;
1611
+ this.server = null;
1612
+ for (const client of this.devClients) client.terminate();
1613
+ this.devClients.clear();
1614
+ for (const client of this.workflowClients) client.terminate();
1615
+ this.workflowClients.clear();
1616
+ this.roomIdsByWorkflowClient.clear();
1617
+ this.workflowClientCountByRoomId.clear();
1618
+ if (!server) return;
1619
+ await new Promise((resolve, reject) => {
1620
+ server.close((error) => {
1621
+ if (error) {
1622
+ reject(error);
1623
+ return;
1624
+ }
1625
+ resolve();
1626
+ });
1627
+ });
1628
+ }
1629
+ setUiProxyTarget(target) {
1630
+ this.uiProxyTarget = target?.trim() ? target.trim() : null;
1631
+ }
1632
+ async activateRuntime(target) {
1633
+ this.activeRuntime = target;
1634
+ await this.connectChildWorkflowSocket();
1635
+ }
1636
+ setBuildStatus(status) {
1637
+ this.activeBuildStatus = status;
1638
+ }
1639
+ broadcastBuildStarted() {
1640
+ this.broadcastDev({ kind: "devBuildStarted" });
1641
+ this.broadcastWorkflowLifecycleToSubscribedRooms((roomId) => ({
1642
+ kind: "devBuildStarted",
1643
+ workflowId: roomId
1644
+ }));
1645
+ }
1646
+ broadcastBuildCompleted(buildVersion) {
1647
+ this.broadcastDev({
1648
+ kind: "devBuildCompleted",
1649
+ buildVersion
1650
+ });
1651
+ this.broadcastWorkflowLifecycleToSubscribedRooms((roomId) => ({
1652
+ kind: "devBuildCompleted",
1653
+ workflowId: roomId,
1654
+ buildVersion
1655
+ }));
1656
+ }
1657
+ broadcastBuildFailed(message) {
1658
+ this.broadcastDev({
1659
+ kind: "devBuildFailed",
1660
+ message
1661
+ });
1662
+ this.broadcastWorkflowLifecycleToSubscribedRooms((roomId) => ({
1663
+ kind: "devBuildFailed",
1664
+ workflowId: roomId,
1665
+ message
1666
+ }));
1667
+ }
1668
+ bindDevWebSocket() {
1669
+ this.devWss.on("connection", (socket) => {
1670
+ this.devClients.add(socket);
1671
+ socket.on("close", () => {
1672
+ this.devClients.delete(socket);
1673
+ });
1674
+ });
1675
+ }
1676
+ bindWorkflowWebSocket() {
1677
+ this.workflowWss.on("connection", (socket) => {
1678
+ this.connectWorkflowClient(socket);
1679
+ });
1680
+ }
1681
+ async handleHttpRequest(req, res) {
1682
+ const pathname = this.safePathname(req.url ?? "");
1683
+ const uiProxyTarget = this.uiProxyTarget;
1684
+ const activeRuntimeTarget = this.activeRuntime ? `http://127.0.0.1:${this.activeRuntime.httpPort}` : void 0;
1685
+ if (pathname === "/api/dev/health" && req.method === "GET") {
1686
+ res.writeHead(200, { "content-type": "application/json" });
1687
+ res.end(JSON.stringify({
1688
+ ok: true,
1689
+ runtime: { status: this.activeRuntime ? this.activeBuildStatus === "building" ? "building" : "ready" : "stopped" }
1690
+ }));
1691
+ return;
1692
+ }
1693
+ if (pathname === "/api/dev/bootstrap-summary") {
1694
+ if (!activeRuntimeTarget) {
1695
+ res.writeHead(503, { "content-type": "text/plain" });
1696
+ res.end("Runtime is rebuilding.");
1697
+ return;
1698
+ }
1699
+ this.proxy.web(req, res, { target: activeRuntimeTarget });
1700
+ return;
1701
+ }
1702
+ if (uiProxyTarget && pathname.startsWith("/api/auth/")) {
1703
+ this.proxy.web(req, res, { target: uiProxyTarget.replace(/\/$/, "") });
1704
+ return;
1705
+ }
1706
+ if (pathname.startsWith("/api/")) {
1707
+ if (this.activeBuildStatus === "building" || !this.activeRuntime) {
1708
+ res.writeHead(503, { "content-type": "text/plain" });
1709
+ res.end("Runtime is rebuilding.");
1710
+ return;
1711
+ }
1712
+ this.proxy.web(req, res, { target: activeRuntimeTarget });
1713
+ return;
1714
+ }
1715
+ if (uiProxyTarget) {
1716
+ this.proxy.web(req, res, { target: uiProxyTarget.replace(/\/$/, "") });
1717
+ return;
1718
+ }
1719
+ res.writeHead(404, { "content-type": "text/plain" });
1720
+ res.end("Not found.");
1721
+ }
1722
+ handleUpgrade(request, socket, head) {
1723
+ const pathname = this.safePathname(request.url ?? "");
1724
+ if (pathname === ApiPaths.devGatewaySocket()) {
1725
+ this.devWss.handleUpgrade(request, socket, head, (ws) => {
1726
+ this.devWss.emit("connection", ws, request);
1727
+ });
1728
+ return;
1729
+ }
1730
+ if (pathname === ApiPaths.workflowWebsocket()) {
1731
+ this.workflowWss.handleUpgrade(request, socket, head, (ws) => {
1732
+ this.workflowWss.emit("connection", ws, request);
1733
+ });
1734
+ return;
1735
+ }
1736
+ const uiProxyTarget = this.uiProxyTarget;
1737
+ if (uiProxyTarget && !pathname.startsWith("/api/")) {
1738
+ this.proxy.ws(request, socket, head, { target: uiProxyTarget.replace(/\/$/, "") });
1739
+ return;
1740
+ }
1741
+ socket.destroy();
1742
+ }
1743
+ safePathname(url) {
1744
+ try {
1745
+ return new URL(url, "http://127.0.0.1").pathname;
1746
+ } catch {
1747
+ return url.split("?")[0] ?? url;
1748
+ }
1749
+ }
1750
+ broadcastDev(message) {
1751
+ const text = JSON.stringify(message);
1752
+ for (const client of this.devClients) if (client.readyState === WebSocket.OPEN) client.send(text);
1753
+ }
1754
+ broadcastWorkflowLifecycleToSubscribedRooms(createMessage) {
1755
+ for (const roomId of this.workflowClientCountByRoomId.keys()) this.broadcastWorkflowTextToRoom(roomId, JSON.stringify(createMessage(roomId)));
1756
+ }
1757
+ async connectWorkflowClient(socket) {
1758
+ this.workflowClients.add(socket);
1759
+ this.roomIdsByWorkflowClient.set(socket, /* @__PURE__ */ new Set());
1760
+ socket.send(JSON.stringify({ kind: "ready" }));
1761
+ socket.on("message", (rawData) => {
1762
+ this.handleWorkflowClientMessage(socket, rawData);
1763
+ });
1764
+ socket.on("close", () => {
1765
+ this.disconnectWorkflowClient(socket);
1766
+ });
1767
+ socket.on("error", () => {
1768
+ this.disconnectWorkflowClient(socket);
1769
+ });
1770
+ }
1771
+ disconnectWorkflowClient(socket) {
1772
+ const roomIds = this.roomIdsByWorkflowClient.get(socket);
1773
+ if (roomIds) for (const roomId of roomIds) this.releaseWorkflowRoom(roomId);
1774
+ this.roomIdsByWorkflowClient.delete(socket);
1775
+ this.workflowClients.delete(socket);
1776
+ }
1777
+ async handleWorkflowClientMessage(socket, rawData) {
1778
+ try {
1779
+ const message = this.parseWorkflowClientMessage(rawData);
1780
+ if (message.kind === "subscribe") {
1781
+ const roomIds$1 = this.roomIdsByWorkflowClient.get(socket);
1782
+ if (!roomIds$1) return;
1783
+ if (!roomIds$1.has(message.roomId)) {
1784
+ roomIds$1.add(message.roomId);
1785
+ this.retainWorkflowRoom(message.roomId);
1786
+ }
1787
+ socket.send(JSON.stringify({
1788
+ kind: "subscribed",
1789
+ roomId: message.roomId
1790
+ }));
1791
+ return;
1792
+ }
1793
+ const roomIds = this.roomIdsByWorkflowClient.get(socket);
1794
+ if (!roomIds) return;
1795
+ if (roomIds.delete(message.roomId)) this.releaseWorkflowRoom(message.roomId);
1796
+ socket.send(JSON.stringify({
1797
+ kind: "unsubscribed",
1798
+ roomId: message.roomId
1799
+ }));
1800
+ } catch (error) {
1801
+ const exception = error instanceof Error ? error : new Error(String(error));
1802
+ if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({
1803
+ kind: "error",
1804
+ message: exception.message
1805
+ }));
1806
+ }
1807
+ }
1808
+ parseWorkflowClientMessage(rawData) {
1809
+ const value = typeof rawData === "string" ? rawData : Buffer.isBuffer(rawData) ? rawData.toString("utf8") : "";
1810
+ const message = JSON.parse(value);
1811
+ if (message.kind === "subscribe" && typeof message.roomId === "string") return {
1812
+ kind: "subscribe",
1813
+ roomId: message.roomId
1814
+ };
1815
+ if (message.kind === "unsubscribe" && typeof message.roomId === "string") return {
1816
+ kind: "unsubscribe",
1817
+ roomId: message.roomId
1818
+ };
1819
+ throw new Error("Unsupported websocket client message.");
1820
+ }
1821
+ retainWorkflowRoom(roomId) {
1822
+ const nextCount = (this.workflowClientCountByRoomId.get(roomId) ?? 0) + 1;
1823
+ this.workflowClientCountByRoomId.set(roomId, nextCount);
1824
+ if (nextCount === 1) this.sendToChildWorkflowSocket({
1825
+ kind: "subscribe",
1826
+ roomId
1827
+ });
1828
+ }
1829
+ releaseWorkflowRoom(roomId) {
1830
+ const currentCount = this.workflowClientCountByRoomId.get(roomId) ?? 0;
1831
+ if (currentCount <= 1) {
1832
+ this.workflowClientCountByRoomId.delete(roomId);
1833
+ this.sendToChildWorkflowSocket({
1834
+ kind: "unsubscribe",
1835
+ roomId
1836
+ });
1837
+ return;
1838
+ }
1839
+ this.workflowClientCountByRoomId.set(roomId, currentCount - 1);
1840
+ }
1841
+ sendToChildWorkflowSocket(message) {
1842
+ if (!this.childWorkflowSocket || this.childWorkflowSocket.readyState !== WebSocket.OPEN) return;
1843
+ this.childWorkflowSocket.send(JSON.stringify(message));
1844
+ }
1845
+ async connectChildWorkflowSocket() {
1846
+ await this.disconnectChildWorkflowSocket();
1847
+ if (!this.activeRuntime || this.activeBuildStatus === "building") return;
1848
+ const childWorkflowSocket = await this.openChildWorkflowSocket(this.activeRuntime.workflowWebSocketPort);
1849
+ this.childWorkflowSocket = childWorkflowSocket;
1850
+ childWorkflowSocket.on("message", (rawData) => {
1851
+ this.handleChildWorkflowSocketMessage(rawData);
1852
+ });
1853
+ childWorkflowSocket.on("close", () => {
1854
+ if (this.childWorkflowSocket === childWorkflowSocket) this.childWorkflowSocket = null;
1855
+ });
1856
+ childWorkflowSocket.on("error", () => {
1857
+ if (this.childWorkflowSocket === childWorkflowSocket) this.childWorkflowSocket = null;
1858
+ });
1859
+ for (const roomId of this.workflowClientCountByRoomId.keys()) this.sendToChildWorkflowSocket({
1860
+ kind: "subscribe",
1861
+ roomId
1862
+ });
1863
+ }
1864
+ openChildWorkflowSocket(workflowWebSocketPort) {
1865
+ return new Promise((resolve, reject) => {
1866
+ const socket = new WebSocket(`ws://127.0.0.1:${workflowWebSocketPort}${ApiPaths.workflowWebsocket()}`);
1867
+ socket.once("open", () => {
1868
+ resolve(socket);
1869
+ });
1870
+ socket.once("error", (error) => {
1871
+ socket.close();
1872
+ reject(error);
1873
+ });
1874
+ });
1875
+ }
1876
+ async disconnectChildWorkflowSocket() {
1877
+ if (!this.childWorkflowSocket) return;
1878
+ const socket = this.childWorkflowSocket;
1879
+ this.childWorkflowSocket = null;
1880
+ await new Promise((resolve) => {
1881
+ socket.once("close", () => {
1882
+ resolve();
1883
+ });
1884
+ socket.close();
1885
+ });
1886
+ }
1887
+ handleChildWorkflowSocketMessage(rawData) {
1888
+ const text = typeof rawData === "string" ? rawData : Buffer.isBuffer(rawData) ? rawData.toString("utf8") : "";
1889
+ if (text.trim().length === 0) return;
1890
+ try {
1891
+ const message = JSON.parse(text);
1892
+ if (message.kind === "event" && typeof message.event?.workflowId === "string") {
1893
+ this.broadcastWorkflowTextToRoom(message.event.workflowId, text);
1894
+ return;
1895
+ }
1896
+ if ((message.kind === "workflowChanged" || message.kind === "devBuildStarted" || message.kind === "devBuildCompleted" || message.kind === "devBuildFailed") && typeof message.workflowId === "string") {
1897
+ this.broadcastWorkflowTextToRoom(message.workflowId, text);
1898
+ return;
1899
+ }
1900
+ if (message.kind === "error" && typeof message.message === "string") this.broadcastWorkflowTextToAll(text);
1901
+ } catch {}
1902
+ }
1903
+ broadcastWorkflowTextToRoom(roomId, text) {
1904
+ for (const [client, roomIds] of this.roomIdsByWorkflowClient) {
1905
+ if (client.readyState !== WebSocket.OPEN || !roomIds.has(roomId)) continue;
1906
+ client.send(text);
1907
+ }
1908
+ }
1909
+ broadcastWorkflowTextToAll(text) {
1910
+ for (const client of this.workflowClients) if (client.readyState === WebSocket.OPEN) client.send(text);
1911
+ }
1912
+ };
1913
+
1914
+ //#endregion
1915
+ //#region src/dev/CliDevProxyServerFactory.ts
1916
+ var CliDevProxyServerFactory = class {
1917
+ create(gatewayPort) {
1918
+ return new CliDevProxyServer(gatewayPort);
1919
+ }
1920
+ };
1921
+
1922
+ //#endregion
1923
+ //#region ../host/src/presentation/server/CodemationTsyringeParamInfoReader.ts
1924
+ var CodemationTsyringeParamInfoReader = class {
1925
+ static injectionTokenMetadataKey = "injectionTokens";
1926
+ static designParamTypesMetadataKey = "design:paramtypes";
1927
+ static read(target) {
1928
+ const designParamTypes = this.readDesignParamTypes(target);
1929
+ const injectionTokens = this.readInjectionTokens(target);
1930
+ Object.keys(injectionTokens).forEach((key) => {
1931
+ designParamTypes[Number(key)] = injectionTokens[key];
1932
+ });
1933
+ return designParamTypes;
1934
+ }
1935
+ static readDesignParamTypes(target) {
1936
+ const reflected = Reflect.getMetadata?.(this.designParamTypesMetadataKey, target);
1937
+ return Array.isArray(reflected) ? [...reflected] : [];
1938
+ }
1939
+ static readInjectionTokens(target) {
1940
+ const reflected = Reflect.getOwnMetadata?.(this.injectionTokenMetadataKey, target);
1941
+ if (!reflected || typeof reflected !== "object") return {};
1942
+ return reflected;
1943
+ }
1944
+ };
1945
+
1946
+ //#endregion
1947
+ //#region ../host/src/presentation/server/CodemationTsyringeTypeInfoRegistrar.ts
1948
+ var CodemationTsyringeTypeInfoRegistrar = class {
1949
+ visitedTokens = /* @__PURE__ */ new Set();
1950
+ visitedConfigObjects = /* @__PURE__ */ new Set();
1951
+ constructor(container) {
1952
+ this.container = container;
1953
+ }
1954
+ registerWorkflowDefinitions(workflows) {
1955
+ for (const workflow of workflows) for (const node of workflow.nodes) {
1956
+ this.registerTypeToken(node.type);
1957
+ this.registerConfigTokens(node.config);
1958
+ }
1959
+ }
1960
+ registerTypeToken(token) {
1961
+ if (typeof token !== "function" || this.visitedTokens.has(token)) return;
1962
+ this.visitedTokens.add(token);
1963
+ const paramInfo = CodemationTsyringeParamInfoReader.read(token);
1964
+ for (const dependency of paramInfo) this.registerDependency(dependency);
1965
+ this.registerFactoryProvider(token, paramInfo);
1966
+ }
1967
+ registerDependency(dependency) {
1968
+ const token = this.resolveDependencyToken(dependency);
1969
+ if (typeof token !== "function") return;
1970
+ if (!this.container.isRegistered(token, true)) return;
1971
+ this.registerTypeToken(token);
1972
+ }
1973
+ registerConfigTokens(value) {
1974
+ if (Array.isArray(value)) {
1975
+ value.forEach((entry) => this.registerConfigTokens(entry));
1976
+ return;
1977
+ }
1978
+ if (!value || typeof value !== "object") return;
1979
+ if (this.visitedConfigObjects.has(value)) return;
1980
+ this.visitedConfigObjects.add(value);
1981
+ if ("type" in value && typeof value.type === "function") this.registerTypeToken(value.type);
1982
+ Object.values(value).forEach((entry) => this.registerConfigTokens(entry));
1983
+ }
1984
+ registerFactoryProvider(token, paramInfo) {
1985
+ if (this.container.isRegistered(token, true)) return;
1986
+ const classToken = token;
1987
+ const constructorToken = token;
1988
+ this.container.register(classToken, { useFactory: (dependencyContainer) => {
1989
+ return new constructorToken(...paramInfo.map((dependency) => this.resolveFactoryDependency(dependencyContainer, dependency)));
1990
+ } });
1991
+ }
1992
+ resolveDependencyToken(dependency) {
1993
+ if (this.isInjectionDescriptor(dependency)) return dependency.token;
1994
+ return dependency;
1995
+ }
1996
+ resolveFactoryDependency(dependencyContainer, dependency) {
1997
+ const token = this.resolveDependencyToken(dependency);
1998
+ if (typeof token === "function") {
1999
+ if (dependencyContainer.isRegistered(token, true)) try {
2000
+ return dependencyContainer.resolve(token);
2001
+ } catch (error) {
2002
+ if (!this.isMissingTypeInfoError(error)) throw error;
2003
+ }
2004
+ this.registerTypeToken(token);
2005
+ return new token(...CodemationTsyringeParamInfoReader.read(token).map((entry) => this.resolveFactoryDependency(dependencyContainer, entry)));
2006
+ }
2007
+ return dependencyContainer.resolve(token);
2008
+ }
2009
+ isInjectionDescriptor(value) {
2010
+ return value !== null && typeof value === "object" && "token" in value;
2011
+ }
2012
+ isMissingTypeInfoError(error) {
2013
+ return error instanceof Error && error.message.includes("TypeInfo not known for");
2014
+ }
2015
+ };
2016
+
2017
+ //#endregion
2018
+ //#region src/dev/DevApiRuntimeHost.ts
2019
+ var DevApiRuntimeHost = class {
2020
+ pluginListMerger = new CodemationPluginListMerger();
2021
+ contextPromise = null;
2022
+ constructor(configLoader, pluginDiscovery, args) {
2023
+ this.configLoader = configLoader;
2024
+ this.pluginDiscovery = pluginDiscovery;
2025
+ this.args = args;
2026
+ }
2027
+ async prepare() {
2028
+ if (!this.contextPromise) this.contextPromise = this.createContext();
2029
+ return await this.contextPromise;
2030
+ }
2031
+ async stop() {
2032
+ const contextPromise = this.contextPromise;
2033
+ this.contextPromise = null;
2034
+ if (!contextPromise) return;
2035
+ await (await contextPromise).container.resolve(AppContainerLifecycle$1).stop();
2036
+ }
2037
+ async createContext() {
2038
+ const consumerRoot = path.resolve(this.args.consumerRoot);
2039
+ const repoRoot = await this.detectWorkspaceRoot(consumerRoot);
2040
+ const prismaCliOverride = await this.resolvePrismaCliOverride();
2041
+ const hostPackageRoot = path.resolve(repoRoot, "packages", "host");
2042
+ const env = { ...this.args.env };
2043
+ if (prismaCliOverride) env.CODEMATION_PRISMA_CLI_PATH = prismaCliOverride;
2044
+ env.CODEMATION_HOST_PACKAGE_ROOT = hostPackageRoot;
2045
+ env.CODEMATION_PRISMA_CONFIG_PATH = path.resolve(hostPackageRoot, "prisma.config.ts");
2046
+ env.CODEMATION_CONSUMER_ROOT = consumerRoot;
2047
+ const configResolution = await this.configLoader.load({
2048
+ consumerRoot,
2049
+ repoRoot,
2050
+ env
2051
+ });
2052
+ const discoveredPlugins = await this.loadDiscoveredPlugins(consumerRoot);
2053
+ const appConfig = {
2054
+ ...configResolution.appConfig,
2055
+ env,
2056
+ plugins: discoveredPlugins.length > 0 ? this.pluginListMerger.merge(configResolution.appConfig.plugins, discoveredPlugins) : configResolution.appConfig.plugins
2057
+ };
2058
+ const container = await new AppContainerFactory$1().create({
2059
+ appConfig,
2060
+ sharedWorkflowWebsocketServer: null
2061
+ });
2062
+ new CodemationTsyringeTypeInfoRegistrar(container).registerWorkflowDefinitions(appConfig.workflows ?? []);
2063
+ await container.resolve(FrontendRuntime).start();
2064
+ return {
2065
+ buildVersion: this.createBuildVersion(),
2066
+ container,
2067
+ consumerRoot,
2068
+ repoRoot,
2069
+ workflowIds: appConfig.workflows.map((workflow) => workflow.id),
2070
+ workflowSources: appConfig.workflowSources
2071
+ };
2072
+ }
2073
+ async loadDiscoveredPlugins(consumerRoot) {
2074
+ return (await this.pluginDiscovery.resolvePlugins(consumerRoot)).map((resolvedPackage) => resolvedPackage.plugin);
2075
+ }
2076
+ async detectWorkspaceRoot(startDirectory) {
2077
+ let currentDirectory = path.resolve(startDirectory);
2078
+ while (true) {
2079
+ if (await this.exists(path.resolve(currentDirectory, "pnpm-workspace.yaml"))) return currentDirectory;
2080
+ const parentDirectory = path.dirname(currentDirectory);
2081
+ if (parentDirectory === currentDirectory) return startDirectory;
2082
+ currentDirectory = parentDirectory;
2083
+ }
2084
+ }
2085
+ async resolvePrismaCliOverride() {
2086
+ const candidate = path.resolve(this.args.runtimeWorkingDirectory, "node_modules", "prisma", "build", "index.js");
2087
+ return await this.exists(candidate) ? candidate : null;
2088
+ }
2089
+ createBuildVersion() {
2090
+ return `${Date.now()}-${process$1.pid}`;
2091
+ }
2092
+ async exists(filePath) {
2093
+ try {
2094
+ await access(filePath);
2095
+ return true;
2096
+ } catch {
2097
+ return false;
2098
+ }
2099
+ }
2100
+ };
2101
+
2102
+ //#endregion
2103
+ //#region src/dev/DevApiRuntimeServer.ts
2104
+ var DevApiRuntimeServer = class {
2105
+ bootstrapLogger = new ServerLoggerFactory(logLevelPolicyFactory).create("codemation-cli.dev-runtime");
2106
+ server = null;
2107
+ constructor(httpPort, workflowWebSocketPort, host) {
2108
+ this.httpPort = httpPort;
2109
+ this.workflowWebSocketPort = workflowWebSocketPort;
2110
+ this.host = host;
2111
+ }
2112
+ async start() {
2113
+ const root = new Hono();
2114
+ root.get("/health", (c) => c.json({ ok: true }));
2115
+ root.all("*", async (c) => {
2116
+ return (await this.host.prepare()).container.resolve(CodemationHonoApiApp).fetch(c.req.raw);
2117
+ });
2118
+ await this.listen(root);
2119
+ try {
2120
+ return await this.host.prepare();
2121
+ } catch (error) {
2122
+ await this.stop();
2123
+ throw error;
2124
+ }
2125
+ }
2126
+ async stop() {
2127
+ const server = this.server;
2128
+ this.server = null;
2129
+ const failures = [];
2130
+ if (server) try {
2131
+ await this.closeServer(server);
2132
+ } catch (error) {
2133
+ failures.push(this.normalizeError(error));
2134
+ }
2135
+ try {
2136
+ await this.host.stop();
2137
+ } catch (error) {
2138
+ failures.push(this.normalizeError(error));
2139
+ }
2140
+ if (failures.length > 0) throw failures[0];
2141
+ }
2142
+ async listen(root) {
2143
+ if (this.server) return;
2144
+ await new Promise((resolve, reject) => {
2145
+ let resolved = false;
2146
+ const server = serve({
2147
+ fetch: root.fetch,
2148
+ port: this.httpPort,
2149
+ hostname: "127.0.0.1"
2150
+ }, () => {
2151
+ resolved = true;
2152
+ this.server = server;
2153
+ resolve();
2154
+ });
2155
+ server.on("error", (error) => {
2156
+ if (resolved) {
2157
+ this.bootstrapLogger.error("runtime HTTP server error", this.normalizeError(error));
2158
+ return;
2159
+ }
2160
+ reject(error);
2161
+ });
2162
+ });
2163
+ this.bootstrapLogger.debug(`runtime listening httpPort=${this.httpPort} workflowWebSocketPort=${this.workflowWebSocketPort}`);
2164
+ }
2165
+ closeServer(server) {
2166
+ return new Promise((resolve, reject) => {
2167
+ server.close((error) => {
2168
+ if (error) {
2169
+ reject(error);
2170
+ return;
2171
+ }
2172
+ resolve();
2173
+ });
2174
+ });
2175
+ }
2176
+ normalizeError(error) {
2177
+ return error instanceof Error ? error : new Error(String(error));
2178
+ }
2179
+ };
2180
+
2181
+ //#endregion
2182
+ //#region src/dev/DevApiRuntimeFactory.ts
2183
+ var DevApiRuntimeFactory = class {
2184
+ constructor(portAllocator, configLoader, pluginDiscovery) {
2185
+ this.portAllocator = portAllocator;
2186
+ this.configLoader = configLoader;
2187
+ this.pluginDiscovery = pluginDiscovery;
2188
+ }
2189
+ async create(args) {
2190
+ const httpPort = await this.portAllocator.allocate();
2191
+ const workflowWebSocketPort = await this.portAllocator.allocate();
2192
+ const runtime = new DevApiRuntimeServer(httpPort, workflowWebSocketPort, new DevApiRuntimeHost(this.configLoader, this.pluginDiscovery, {
2193
+ consumerRoot: args.consumerRoot,
2194
+ env: {
2195
+ ...args.env,
2196
+ CODEMATION_WS_PORT: String(workflowWebSocketPort),
2197
+ NEXT_PUBLIC_CODEMATION_WS_PORT: String(workflowWebSocketPort)
2198
+ },
2199
+ runtimeWorkingDirectory: args.runtimeWorkingDirectory
2200
+ }));
2201
+ const context = await runtime.start();
2202
+ return {
2203
+ buildVersion: context.buildVersion,
2204
+ httpPort,
2205
+ stop: async () => {
2206
+ await runtime.stop();
2207
+ },
2208
+ workflowIds: context.workflowIds,
2209
+ workflowWebSocketPort
2210
+ };
2211
+ }
2212
+ };
2213
+
2214
+ //#endregion
2215
+ //#region src/dev/DevRebuildQueue.ts
2216
+ var DevRebuildQueue = class {
2217
+ pendingRequest = null;
2218
+ drainPromise = null;
2219
+ constructor(handler) {
2220
+ this.handler = handler;
2221
+ }
2222
+ async enqueue(request) {
2223
+ this.pendingRequest = this.mergePendingRequest(this.pendingRequest, request);
2224
+ if (!this.drainPromise) this.drainPromise = this.drain();
2225
+ return await this.drainPromise;
2226
+ }
2227
+ async drain() {
2228
+ try {
2229
+ while (this.pendingRequest) {
2230
+ const nextRequest = this.pendingRequest;
2231
+ this.pendingRequest = null;
2232
+ await this.handler.run(nextRequest);
2233
+ }
2234
+ } finally {
2235
+ this.drainPromise = null;
2236
+ if (this.pendingRequest) {
2237
+ this.drainPromise = this.drain();
2238
+ await this.drainPromise;
2239
+ }
2240
+ }
2241
+ }
2242
+ mergePendingRequest(current, next) {
2243
+ if (!current) return {
2244
+ ...next,
2245
+ changedPaths: [...next.changedPaths]
2246
+ };
2247
+ return {
2248
+ changedPaths: [...new Set([...current.changedPaths, ...next.changedPaths])],
2249
+ shouldRepublishConsumerOutput: current.shouldRepublishConsumerOutput || next.shouldRepublishConsumerOutput,
2250
+ shouldRestartUi: current.shouldRestartUi || next.shouldRestartUi
2251
+ };
2252
+ }
2253
+ };
2254
+
2255
+ //#endregion
2256
+ //#region src/dev/DevRebuildQueueFactory.ts
2257
+ var DevRebuildQueueFactory = class {
2258
+ create(handler) {
2259
+ return new DevRebuildQueue(handler);
2260
+ }
2261
+ };
2262
+
1445
2263
  //#endregion
1446
2264
  //#region src/runtime/ListenPortResolver.ts
1447
2265
  /**
@@ -1476,36 +2294,13 @@ var SourceMapNodeOptions = class {
1476
2294
  }
1477
2295
  };
1478
2296
 
1479
- //#endregion
1480
- //#region src/dev/DevelopmentGatewayNotifier.ts
1481
- var DevelopmentGatewayNotifier = class {
1482
- constructor(cliLogger) {
1483
- this.cliLogger = cliLogger;
1484
- }
1485
- async notify(args) {
1486
- const targetUrl = `${args.gatewayBaseUrl.replace(/\/$/, "")}${ApiPaths.devGatewayNotify()}`;
1487
- try {
1488
- const response = await fetch(targetUrl, {
1489
- method: "POST",
1490
- headers: {
1491
- "content-type": "application/json",
1492
- "x-codemation-dev-token": args.developmentServerToken
1493
- },
1494
- body: JSON.stringify(args.payload)
1495
- });
1496
- if (!response.ok) this.cliLogger.warn(`failed to notify dev gateway status=${response.status}`);
1497
- } catch (error) {
1498
- this.cliLogger.warn(`failed to notify dev gateway: ${error instanceof Error ? error.message : String(error)}`);
1499
- }
1500
- }
1501
- };
1502
-
1503
2297
  //#endregion
1504
2298
  //#region src/dev/DevAuthSettingsLoader.ts
1505
2299
  var DevAuthSettingsLoader = class DevAuthSettingsLoader {
1506
2300
  static defaultDevelopmentAuthSecret = "codemation-dev-auth-secret-not-for-production";
1507
- constructor(configLoader) {
2301
+ constructor(configLoader, consumerEnvLoader) {
1508
2302
  this.configLoader = configLoader;
2303
+ this.consumerEnvLoader = consumerEnvLoader;
1509
2304
  }
1510
2305
  resolveDevelopmentServerToken(rawToken) {
1511
2306
  if (rawToken && rawToken.trim().length > 0) return rawToken;
@@ -1513,14 +2308,15 @@ var DevAuthSettingsLoader = class DevAuthSettingsLoader {
1513
2308
  }
1514
2309
  async loadForConsumer(consumerRoot) {
1515
2310
  const resolution = await this.configLoader.load({ consumerRoot });
2311
+ const envForAuthSecret = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(consumerRoot, process.env);
1516
2312
  return {
1517
2313
  authConfigJson: JSON.stringify(resolution.config.auth ?? null),
1518
- authSecret: this.resolveDevelopmentAuthSecret(process.env),
2314
+ authSecret: this.resolveDevelopmentAuthSecret(envForAuthSecret),
1519
2315
  skipUiAuth: resolution.config.auth?.allowUnauthenticatedInDevelopment === true
1520
2316
  };
1521
2317
  }
1522
2318
  resolveDevelopmentAuthSecret(env) {
1523
- const configuredSecret = env.AUTH_SECRET ?? env.NEXTAUTH_SECRET;
2319
+ const configuredSecret = env.AUTH_SECRET;
1524
2320
  if (configuredSecret && configuredSecret.trim().length > 0) return configuredSecret;
1525
2321
  return DevAuthSettingsLoader.defaultDevelopmentAuthSecret;
1526
2322
  }
@@ -1532,8 +2328,8 @@ var DevHttpProbe = class {
1532
2328
  async waitUntilUrlRespondsOk(url) {
1533
2329
  for (let attempt = 0; attempt < 200; attempt += 1) {
1534
2330
  try {
1535
- const response = await fetch(url);
1536
- if (response.ok || response.status === 404) return;
2331
+ const response = await fetch(url, { redirect: "manual" });
2332
+ if (response.ok || response.status === 404 || this.isRedirectStatus(response.status)) return;
1537
2333
  } catch {}
1538
2334
  await setTimeout$1(50);
1539
2335
  }
@@ -1547,10 +2343,10 @@ var DevHttpProbe = class {
1547
2343
  } catch {}
1548
2344
  await setTimeout$1(50);
1549
2345
  }
1550
- throw new Error("Timed out waiting for dev gateway HTTP health check.");
2346
+ throw new Error("Timed out waiting for the stable dev HTTP health check.");
1551
2347
  }
1552
2348
  /**
1553
- * Polls until the runtime child serves bootstrap summary (after gateway is up, the disposable runtime may still be wiring).
2349
+ * Polls until the active disposable runtime serves bootstrap summary through the stable CLI dev endpoint.
1554
2350
  */
1555
2351
  async waitUntilBootstrapSummaryReady(gatewayBaseUrl) {
1556
2352
  const url = `${gatewayBaseUrl.replace(/\/$/, "")}/api/dev/bootstrap-summary`;
@@ -1562,26 +2358,60 @@ var DevHttpProbe = class {
1562
2358
  }
1563
2359
  throw new Error("Timed out waiting for dev runtime bootstrap summary.");
1564
2360
  }
2361
+ isRedirectStatus(status) {
2362
+ return status >= 300 && status < 400;
2363
+ }
1565
2364
  };
1566
2365
 
1567
2366
  //#endregion
1568
2367
  //#region src/dev/DevNextHostEnvironmentBuilder.ts
1569
2368
  var DevNextHostEnvironmentBuilder = class {
1570
- constructor(consumerEnvLoader, sourceMapNodeOptions) {
2369
+ constructor(consumerEnvLoader, sourceMapNodeOptions, frontendAuthSnapshotFactory = new CodemationFrontendAuthSnapshotFactory$1(), frontendAppConfigJsonCodec = new FrontendAppConfigJsonCodec$1()) {
1571
2370
  this.consumerEnvLoader = consumerEnvLoader;
1572
2371
  this.sourceMapNodeOptions = sourceMapNodeOptions;
2372
+ this.frontendAuthSnapshotFactory = frontendAuthSnapshotFactory;
2373
+ this.frontendAppConfigJsonCodec = frontendAppConfigJsonCodec;
2374
+ }
2375
+ buildConsumerUiProxy(args) {
2376
+ return {
2377
+ ...this.build({
2378
+ authConfigJson: args.authConfigJson,
2379
+ authSecret: args.authSecret,
2380
+ consumerRoot: args.consumerRoot,
2381
+ developmentServerToken: args.developmentServerToken,
2382
+ nextPort: args.nextPort,
2383
+ runtimeDevUrl: args.runtimeDevUrl,
2384
+ skipUiAuth: args.skipUiAuth,
2385
+ websocketPort: args.websocketPort,
2386
+ consumerOutputManifestPath: args.consumerOutputManifestPath
2387
+ }),
2388
+ HOSTNAME: "127.0.0.1",
2389
+ AUTH_SECRET: args.authSecret,
2390
+ AUTH_URL: args.publicBaseUrl
2391
+ };
1573
2392
  }
1574
2393
  build(args) {
1575
2394
  const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process$1.env);
1576
2395
  const manifestPath = args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
2396
+ const authSecret = args.authSecret ?? merged.AUTH_SECRET;
2397
+ const authSnapshot = this.frontendAuthSnapshotFactory.createFromResolvedInputs({
2398
+ authConfig: this.parseAuthConfig(args.authConfigJson),
2399
+ env: {
2400
+ ...merged,
2401
+ ...typeof authSecret === "string" && authSecret.trim().length > 0 ? { AUTH_SECRET: authSecret } : {}
2402
+ },
2403
+ uiAuthEnabled: !args.skipUiAuth
2404
+ });
1577
2405
  return {
1578
2406
  ...merged,
1579
2407
  PORT: String(args.nextPort),
1580
- CODEMATION_AUTH_CONFIG_JSON: args.authConfigJson,
1581
2408
  CODEMATION_CONSUMER_ROOT: args.consumerRoot,
1582
2409
  CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifestPath,
1583
- CODEMATION_SKIP_UI_AUTH: args.skipUiAuth ? "true" : "false",
1584
- NEXT_PUBLIC_CODEMATION_SKIP_UI_AUTH: args.skipUiAuth ? "true" : "false",
2410
+ CODEMATION_FRONTEND_APP_CONFIG_JSON: this.frontendAppConfigJsonCodec.serialize({
2411
+ auth: authSnapshot,
2412
+ productName: "Codemation",
2413
+ logoUrl: null
2414
+ }),
1585
2415
  CODEMATION_WS_PORT: String(args.websocketPort),
1586
2416
  NEXT_PUBLIC_CODEMATION_WS_PORT: String(args.websocketPort),
1587
2417
  CODEMATION_DEV_SERVER_TOKEN: args.developmentServerToken,
@@ -1592,6 +2422,10 @@ var DevNextHostEnvironmentBuilder = class {
1592
2422
  ...args.runtimeDevUrl !== void 0 && args.runtimeDevUrl.trim().length > 0 ? { CODEMATION_RUNTIME_DEV_URL: args.runtimeDevUrl.trim() } : {}
1593
2423
  };
1594
2424
  }
2425
+ parseAuthConfig(authConfigJson) {
2426
+ if (authConfigJson.trim().length === 0) return;
2427
+ return JSON.parse(authConfigJson) ?? void 0;
2428
+ }
1595
2429
  };
1596
2430
 
1597
2431
  //#endregion
@@ -1605,7 +2439,7 @@ var DevSessionPortsResolver = class {
1605
2439
  const nextPort = this.listenPorts.resolvePrimaryApplicationPort(args.portEnv);
1606
2440
  return {
1607
2441
  nextPort,
1608
- gatewayPort: this.listenPorts.parsePositiveInteger(args.gatewayPortEnv) ?? (args.devMode === "consumer" ? nextPort : await this.loopbackPorts.allocate())
2442
+ gatewayPort: this.listenPorts.parsePositiveInteger(args.gatewayPortEnv) ?? (args.devMode === "packaged-ui" ? nextPort : await this.loopbackPorts.allocate())
1609
2443
  };
1610
2444
  }
1611
2445
  };
@@ -1616,58 +2450,48 @@ var DevSessionPortsResolver = class {
1616
2450
  * Bundles dependencies for {@link DevCommand} so the command stays a thin orchestrator.
1617
2451
  */
1618
2452
  var DevSessionServices = class {
1619
- constructor(consumerEnvLoader, sourceMapNodeOptions, sessionPorts, loopbackPortAllocator, devHttpProbe, runtimeEntrypointResolver, devAuthLoader, nextHostEnvBuilder, watchRootsResolver, sourceRestartCoordinator) {
2453
+ constructor(consumerEnvLoader, sourceMapNodeOptions, sessionPorts, loopbackPortAllocator, devHttpProbe, devAuthLoader, nextHostEnvBuilder, watchRootsResolver, sourceChangeClassifier) {
1620
2454
  this.consumerEnvLoader = consumerEnvLoader;
1621
2455
  this.sourceMapNodeOptions = sourceMapNodeOptions;
1622
2456
  this.sessionPorts = sessionPorts;
1623
2457
  this.loopbackPortAllocator = loopbackPortAllocator;
1624
2458
  this.devHttpProbe = devHttpProbe;
1625
- this.runtimeEntrypointResolver = runtimeEntrypointResolver;
1626
2459
  this.devAuthLoader = devAuthLoader;
1627
2460
  this.nextHostEnvBuilder = nextHostEnvBuilder;
1628
2461
  this.watchRootsResolver = watchRootsResolver;
1629
- this.sourceRestartCoordinator = sourceRestartCoordinator;
2462
+ this.sourceChangeClassifier = sourceChangeClassifier;
1630
2463
  }
1631
2464
  };
1632
2465
 
1633
2466
  //#endregion
1634
- //#region src/dev/DevSourceRestartCoordinator.ts
1635
- var DevSourceRestartCoordinator = class {
1636
- constructor(gatewayNotifier, performanceDiagnosticsLogger, cliLogger) {
1637
- this.gatewayNotifier = gatewayNotifier;
1638
- this.performanceDiagnosticsLogger = performanceDiagnosticsLogger;
1639
- this.cliLogger = cliLogger;
1640
- }
1641
- async runHandshakeAfterSourceChange(gatewayBaseUrl, developmentServerToken) {
1642
- const restartStarted = performance.now();
1643
- try {
1644
- await this.gatewayNotifier.notify({
1645
- gatewayBaseUrl,
1646
- developmentServerToken,
1647
- payload: { kind: "buildStarted" }
1648
- });
1649
- await this.gatewayNotifier.notify({
1650
- gatewayBaseUrl,
1651
- developmentServerToken,
1652
- payload: {
1653
- kind: "buildCompleted",
1654
- buildVersion: `${Date.now()}-${process$1.pid}`
1655
- }
1656
- });
1657
- const totalMs = performance.now() - restartStarted;
1658
- this.performanceDiagnosticsLogger.info(`triggered source-based runtime restart timingMs={total:${totalMs.toFixed(1)}}`);
1659
- } catch (error) {
1660
- const exception = error instanceof Error ? error : new Error(String(error));
1661
- await this.gatewayNotifier.notify({
1662
- gatewayBaseUrl,
1663
- developmentServerToken,
1664
- payload: {
1665
- kind: "buildFailed",
1666
- message: exception.message
1667
- }
1668
- });
1669
- this.cliLogger.error("source-based runtime restart request failed", exception);
1670
- }
2467
+ //#region src/dev/DevSourceChangeClassifier.ts
2468
+ var DevSourceChangeClassifier = class DevSourceChangeClassifier {
2469
+ static configFileNames = new Set([
2470
+ "codemation.config.ts",
2471
+ "codemation.config.js",
2472
+ "codemation.config.mjs"
2473
+ ]);
2474
+ shouldRepublishConsumerOutput(args) {
2475
+ const resolvedConsumerRoot = path.resolve(args.consumerRoot);
2476
+ return args.changedPaths.some((changedPath) => this.isPathInsideDirectory(changedPath, resolvedConsumerRoot));
2477
+ }
2478
+ requiresUiRestart(args) {
2479
+ const resolvedConsumerRoot = path.resolve(args.consumerRoot);
2480
+ return args.changedPaths.some((changedPath) => this.pathRequiresUiRestart(path.resolve(changedPath), resolvedConsumerRoot));
2481
+ }
2482
+ isPathInsideDirectory(filePath, directoryPath) {
2483
+ const resolvedFilePath = path.resolve(filePath);
2484
+ const relativePath = path.relative(directoryPath, resolvedFilePath);
2485
+ return relativePath.length === 0 || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
2486
+ }
2487
+ pathRequiresUiRestart(resolvedPath, consumerRoot) {
2488
+ if (!this.isPathInsideDirectory(resolvedPath, consumerRoot)) return false;
2489
+ const relativePath = path.relative(consumerRoot, resolvedPath);
2490
+ if (DevSourceChangeClassifier.configFileNames.has(path.basename(relativePath))) return true;
2491
+ if (relativePath.startsWith(path.join("src", "workflows"))) return false;
2492
+ if (relativePath.startsWith(path.join("src", "plugins"))) return false;
2493
+ if (relativePath.includes("credential")) return true;
2494
+ return false;
1671
2495
  }
1672
2496
  };
1673
2497
 
@@ -1676,7 +2500,7 @@ var DevSourceRestartCoordinator = class {
1676
2500
  var LoopbackPortAllocator = class {
1677
2501
  async allocate() {
1678
2502
  return await new Promise((resolve, reject) => {
1679
- const server = createServer();
2503
+ const server = createServer$1();
1680
2504
  server.once("error", reject);
1681
2505
  server.listen(0, "127.0.0.1", () => {
1682
2506
  const address = server.address();
@@ -1692,52 +2516,20 @@ var LoopbackPortAllocator = class {
1692
2516
  }
1693
2517
  };
1694
2518
 
1695
- //#endregion
1696
- //#region src/dev/RuntimeToolEntrypointResolver.ts
1697
- var RuntimeToolEntrypointResolver = class {
1698
- require = createRequire(import.meta.url);
1699
- async resolve(args) {
1700
- const sourceEntrypointPath = path.resolve(args.repoRoot, args.sourceEntrypoint);
1701
- if (await this.exists(sourceEntrypointPath)) return {
1702
- command: process$1.execPath,
1703
- args: [
1704
- "--import",
1705
- "tsx",
1706
- sourceEntrypointPath
1707
- ],
1708
- env: { TSX_TSCONFIG_PATH: path.resolve(args.repoRoot, "tsconfig.codemation-tsx.json") }
1709
- };
1710
- return {
1711
- command: process$1.execPath,
1712
- args: [this.require.resolve(args.packageName)],
1713
- env: {}
1714
- };
1715
- }
1716
- async exists(filePath) {
1717
- try {
1718
- await access(filePath);
1719
- return true;
1720
- } catch {
1721
- return false;
1722
- }
1723
- }
1724
- };
1725
-
1726
2519
  //#endregion
1727
2520
  //#region src/dev/WatchRootsResolver.ts
1728
2521
  var WatchRootsResolver = class {
1729
2522
  resolve(args) {
1730
- if (args.devMode === "consumer") return [args.consumerRoot];
2523
+ if (args.devMode === "packaged-ui") return [args.consumerRoot];
1731
2524
  return [
1732
2525
  args.consumerRoot,
2526
+ path.resolve(args.repoRoot, "packages", "cli"),
1733
2527
  path.resolve(args.repoRoot, "packages", "core"),
1734
2528
  path.resolve(args.repoRoot, "packages", "core-nodes"),
1735
2529
  path.resolve(args.repoRoot, "packages", "core-nodes-gmail"),
1736
2530
  path.resolve(args.repoRoot, "packages", "eventbus-redis"),
1737
2531
  path.resolve(args.repoRoot, "packages", "host"),
1738
- path.resolve(args.repoRoot, "packages", "node-example"),
1739
- path.resolve(args.repoRoot, "packages", "queue-bullmq"),
1740
- path.resolve(args.repoRoot, "packages", "runtime-dev")
2532
+ path.resolve(args.repoRoot, "packages", "node-example")
1741
2533
  ];
1742
2534
  }
1743
2535
  };
@@ -1745,16 +2537,12 @@ var WatchRootsResolver = class {
1745
2537
  //#endregion
1746
2538
  //#region src/dev/Builder.ts
1747
2539
  var DevSessionServicesBuilder = class {
1748
- constructor(loggerFactory$1) {
1749
- this.loggerFactory = loggerFactory$1;
1750
- }
1751
2540
  build() {
1752
2541
  const consumerEnvLoader = new ConsumerEnvLoader();
1753
2542
  const sourceMapNodeOptions = new SourceMapNodeOptions();
1754
2543
  const listenPortResolver = new ListenPortResolver();
1755
2544
  const loopbackPortAllocator = new LoopbackPortAllocator();
1756
- const cliLogger = this.loggerFactory.create("codemation-cli");
1757
- 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));
2545
+ return new DevSessionServices(consumerEnvLoader, sourceMapNodeOptions, new DevSessionPortsResolver(listenPortResolver, loopbackPortAllocator), loopbackPortAllocator, new DevHttpProbe(), new DevAuthSettingsLoader(new CodemationConsumerConfigLoader(), consumerEnvLoader), new DevNextHostEnvironmentBuilder(consumerEnvLoader, sourceMapNodeOptions), new WatchRootsResolver(), new DevSourceChangeClassifier());
1758
2546
  }
1759
2547
  };
1760
2548
 
@@ -2027,14 +2815,17 @@ var CliProgram = class {
2027
2815
  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) => {
2028
2816
  await this.buildCommand.execute(resolveConsumerRoot(opts.consumerRoot), this.buildOptionsParser.parse(opts));
2029
2817
  });
2030
- program.command("dev", { isDefault: true }).description("Start the dev gateway and runtime child. Default consumer mode uses the packaged Codemation UI; use CODEMATION_DEV_MODE=framework for Next dev HMR when working on the host itself.").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").action(async (opts) => {
2031
- await this.devCommand.execute(resolveConsumerRoot(opts.consumerRoot));
2818
+ program.command("dev", { isDefault: true }).description("Start the stable dev endpoint and disposable API runtime. Uses the packaged Codemation UI by default.").option("--consumer-root <path>", "Path to the consumer project root (defaults to cwd)").option("--watch-framework", "Use Next dev HMR for framework UI work inside this repository.").action(async (opts) => {
2819
+ await this.devCommand.execute({
2820
+ consumerRoot: resolveConsumerRoot(opts.consumerRoot),
2821
+ watchFramework: opts.watchFramework === true
2822
+ });
2032
2823
  });
2033
- const serve = program.command("serve").description("Run production web or worker processes (no dev watchers).");
2034
- serve.command("web").description("Start the packaged Codemation web host.").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) => {
2824
+ const serve$1 = program.command("serve").description("Run production web or worker processes (no dev watchers).");
2825
+ serve$1.command("web").description("Start the packaged Codemation web host.").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) => {
2035
2826
  await this.serveWebCommand.execute(resolveConsumerRoot(opts.consumerRoot), this.buildOptionsParser.parse(opts));
2036
2827
  });
2037
- 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) => {
2828
+ serve$1.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) => {
2038
2829
  await this.serveWorkerCommand.execute(resolveConsumerRoot(opts.consumerRoot), opts.config);
2039
2830
  });
2040
2831
  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) => {
@@ -2107,7 +2898,9 @@ var NextHostConsumerServerCommandFactory = class {
2107
2898
  args: [
2108
2899
  "exec",
2109
2900
  "next",
2110
- "start"
2901
+ "start",
2902
+ "-H",
2903
+ "127.0.0.1"
2111
2904
  ],
2112
2905
  cwd: args.nextHostRoot
2113
2906
  };
@@ -2126,8 +2919,12 @@ var NextHostConsumerServerCommandFactory = class {
2126
2919
  //#region src/runtime/TypeScriptRuntimeConfigurator.ts
2127
2920
  var TypeScriptRuntimeConfigurator = class {
2128
2921
  configure(repoRoot) {
2922
+ if (this.hasExplicitOverride()) return;
2129
2923
  process$1.env.CODEMATION_TSCONFIG_PATH = path.resolve(repoRoot, "tsconfig.base.json");
2130
2924
  }
2925
+ hasExplicitOverride() {
2926
+ return typeof process$1.env.CODEMATION_TSCONFIG_PATH === "string" && process$1.env.CODEMATION_TSCONFIG_PATH.length > 0;
2927
+ }
2131
2928
  };
2132
2929
 
2133
2930
  //#endregion
@@ -2179,22 +2976,20 @@ var CliDatabaseUrlDescriptor = class {
2179
2976
  //#endregion
2180
2977
  //#region src/bootstrap/CodemationCliApplicationSession.ts
2181
2978
  /**
2182
- * Opens a {@link CodemationApplication} with persistence + command/query buses (no HTTP/WebSocket servers),
2979
+ * Opens an app container with persistence + command/query buses (no HTTP/WebSocket servers),
2183
2980
  * for CLI tools that dispatch application commands or queries (e.g. user admin).
2184
2981
  */
2185
2982
  var CodemationCliApplicationSession = class CodemationCliApplicationSession {
2186
- constructor(application) {
2187
- this.application = application;
2983
+ constructor(container) {
2984
+ this.container = container;
2188
2985
  }
2189
2986
  static async open(args) {
2190
- const app = new CodemationApplication().useConfig(args.resolution.config);
2191
- await app.bootCli(new CodemationBootstrapRequest({
2192
- repoRoot: args.bootstrap.repoRoot,
2193
- consumerRoot: args.bootstrap.consumerRoot,
2194
- env: args.bootstrap.env,
2195
- workflowSources: args.resolution.workflowSources
2196
- }));
2197
- return new CodemationCliApplicationSession(app);
2987
+ const container = await new AppContainerFactory().create({
2988
+ appConfig: args.appConfig,
2989
+ sharedWorkflowWebsocketServer: null
2990
+ });
2991
+ if (args.appConfig.env.CODEMATION_SKIP_STARTUP_MIGRATIONS !== "true") await container.resolve(DatabaseMigrations).migrate();
2992
+ return new CodemationCliApplicationSession(container);
2198
2993
  }
2199
2994
  getPrismaClient() {
2200
2995
  const container = this.getContainer();
@@ -2208,10 +3003,10 @@ var CodemationCliApplicationSession = class CodemationCliApplicationSession {
2208
3003
  return this.getContainer().resolve(ApplicationTokens.QueryBus);
2209
3004
  }
2210
3005
  async close() {
2211
- await this.application.stop({ stopWebsocketServer: false });
3006
+ await this.container.resolve(AppContainerLifecycle).stop({ stopWebsocketServer: false });
2212
3007
  }
2213
3008
  getContainer() {
2214
- return this.application.getContainer();
3009
+ return this.container;
2215
3010
  }
2216
3011
  };
2217
3012
 
@@ -2221,37 +3016,26 @@ var CodemationCliApplicationSession = class CodemationCliApplicationSession {
2221
3016
  * Shared env/config/session wiring for `codemation user *` commands (local auth + database).
2222
3017
  */
2223
3018
  var UserAdminCliBootstrap = class {
2224
- constructor(configLoader, pathResolver, consumerDotenvLoader, tsconfigPreparation, databasePersistenceResolver) {
2225
- this.configLoader = configLoader;
3019
+ constructor(appConfigLoader, pathResolver, consumerDotenvLoader, tsconfigPreparation) {
3020
+ this.appConfigLoader = appConfigLoader;
2226
3021
  this.pathResolver = pathResolver;
2227
3022
  this.consumerDotenvLoader = consumerDotenvLoader;
2228
3023
  this.tsconfigPreparation = tsconfigPreparation;
2229
- this.databasePersistenceResolver = databasePersistenceResolver;
2230
3024
  }
2231
3025
  async withSession(options, fn) {
2232
3026
  const consumerRoot = options.consumerRoot ?? process.cwd();
2233
3027
  this.consumerDotenvLoader.load(consumerRoot);
2234
3028
  this.tsconfigPreparation.applyWorkspaceTsconfigForTsxIfPresent(consumerRoot);
2235
- const resolution = await this.configLoader.load({
3029
+ const paths = await this.pathResolver.resolve(consumerRoot);
3030
+ const loadResult = await this.appConfigLoader.load({
2236
3031
  consumerRoot,
2237
- configPathOverride: options.configPath
2238
- });
2239
- if (resolution.config.auth?.kind !== "local") throw new Error("Codemation user commands require CodemationConfig.auth.kind to be \"local\".");
2240
- if (this.databasePersistenceResolver.resolve({
2241
- runtimeConfig: resolution.config.runtime ?? {},
3032
+ repoRoot: paths.repoRoot,
2242
3033
  env: process.env,
2243
- consumerRoot
2244
- }).kind === "none") throw new Error("Database persistence is not configured. Set CodemationConfig.runtime.database (postgresql URL or PGlite).");
2245
- const paths = await this.pathResolver.resolve(consumerRoot);
2246
- const session = await CodemationCliApplicationSession.open({
2247
- resolution,
2248
- bootstrap: new CodemationBootstrapRequest({
2249
- repoRoot: paths.repoRoot,
2250
- consumerRoot,
2251
- env: process.env,
2252
- workflowSources: resolution.workflowSources
2253
- })
3034
+ configPathOverride: options.configPath
2254
3035
  });
3036
+ if (loadResult.appConfig.auth?.kind !== "local") throw new Error("Codemation user commands require CodemationConfig.auth.kind to be \"local\".");
3037
+ if (loadResult.appConfig.persistence.kind === "none") throw new Error("Database persistence is not configured. Set CodemationConfig.runtime.database (postgresql URL or PGlite).");
3038
+ const session = await CodemationCliApplicationSession.open({ appConfig: loadResult.appConfig });
2255
3039
  try {
2256
3040
  return await fn(session);
2257
3041
  } finally {
@@ -2305,6 +3089,7 @@ const loggerFactory = new ServerLoggerFactory(logLevelPolicyFactory);
2305
3089
  var CliProgramFactory = class {
2306
3090
  create() {
2307
3091
  const cliLogger = loggerFactory.create("codemation-cli");
3092
+ const appConfigLoader = new AppConfigLoader();
2308
3093
  const pathResolver = new CliPathResolver();
2309
3094
  const pluginDiscovery = new CodemationPluginDiscovery();
2310
3095
  const artifactsPublisher = new ConsumerBuildArtifactsPublisher();
@@ -2312,15 +3097,15 @@ var CliProgramFactory = class {
2312
3097
  const outputBuilderLoader = new ConsumerOutputBuilderLoader();
2313
3098
  const sourceMapNodeOptions = new SourceMapNodeOptions();
2314
3099
  const nextHostConsumerServerCommandFactory = new NextHostConsumerServerCommandFactory();
3100
+ const devSessionServices = new DevSessionServicesBuilder().build();
2315
3101
  const tsconfigPreparation = new ConsumerCliTsconfigPreparation();
2316
- const databasePersistenceResolver = new DatabasePersistenceResolver();
2317
- const userAdminBootstrap = new UserAdminCliBootstrap(new CodemationConsumerConfigLoader(), pathResolver, new UserAdminConsumerDotenvLoader(), tsconfigPreparation, databasePersistenceResolver);
3102
+ const userAdminBootstrap = new UserAdminCliBootstrap(appConfigLoader, pathResolver, new UserAdminConsumerDotenvLoader(), tsconfigPreparation);
2318
3103
  const hostPackageRoot = new HostPackageRootResolver().resolveHostPackageRoot();
2319
3104
  const userAdminCliOptionsParser = new UserAdminCliOptionsParser();
2320
3105
  const databaseMigrationsApplyService = new DatabaseMigrationsApplyService(cliLogger, new UserAdminConsumerDotenvLoader(), tsconfigPreparation, new CodemationConsumerConfigLoader(), new ConsumerDatabaseConnectionResolver(), new CliDatabaseUrlDescriptor(), hostPackageRoot, new PrismaMigrationDeployer());
2321
3106
  const buildOptionsParser = new ConsumerBuildOptionsParser();
2322
3107
  const devConsumerPublishBootstrap = new DevConsumerPublishBootstrap(cliLogger, pluginDiscovery, artifactsPublisher, outputBuilderLoader, buildOptionsParser);
2323
- 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(), nextHostConsumerServerCommandFactory), new ServeWebCommand(pathResolver, pluginDiscovery, artifactsPublisher, tsRuntime, sourceMapNodeOptions, outputBuilderLoader, new ConsumerEnvLoader(), new ListenPortResolver(), nextHostConsumerServerCommandFactory), new ServeWorkerCommand(sourceMapNodeOptions), new DbMigrateCommand(databaseMigrationsApplyService), new UserCreateCommand(new LocalUserCreator(userAdminBootstrap), userAdminCliOptionsParser), new UserListCommand(cliLogger, userAdminBootstrap, new CliDatabaseUrlDescriptor(), userAdminCliOptionsParser));
3108
+ return new CliProgram(buildOptionsParser, new BuildCommand(cliLogger, pathResolver, pluginDiscovery, artifactsPublisher, tsRuntime, outputBuilderLoader), new DevCommand(pathResolver, tsRuntime, new DevLockFactory(), new DevSourceWatcherFactory(), cliLogger, devSessionServices, databaseMigrationsApplyService, new DevBootstrapSummaryFetcher(), new DevCliBannerRenderer(), devConsumerPublishBootstrap, new ConsumerEnvDotenvFilePredicate(), new DevTrackedProcessTreeKiller(), nextHostConsumerServerCommandFactory, new DevApiRuntimeFactory(devSessionServices.loopbackPortAllocator, appConfigLoader, pluginDiscovery), new CliDevProxyServerFactory(), new DevRebuildQueueFactory()), new ServeWebCommand(pathResolver, new CodemationConsumerConfigLoader(), pluginDiscovery, artifactsPublisher, tsRuntime, sourceMapNodeOptions, outputBuilderLoader, new ConsumerEnvLoader(), new ListenPortResolver(), nextHostConsumerServerCommandFactory, new CodemationFrontendAuthSnapshotFactory(), new FrontendAppConfigJsonCodec()), new ServeWorkerCommand(pathResolver, appConfigLoader, new AppContainerFactory()), new DbMigrateCommand(databaseMigrationsApplyService), new UserCreateCommand(new LocalUserCreator(userAdminBootstrap), userAdminCliOptionsParser), new UserListCommand(cliLogger, userAdminBootstrap, new CliDatabaseUrlDescriptor(), userAdminCliOptionsParser));
2324
3109
  }
2325
3110
  };
2326
3111