@codemation/cli 0.0.5 → 0.0.11

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-C3ar49fj.js → CliBin-BAnFX1wL.js} +1105 -366
  3. package/dist/bin.js +1 -1
  4. package/dist/index.d.ts +655 -207
  5. package/dist/index.js +1 -1
  6. package/package.json +14 -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 +203 -171
  11. package/src/commands/ServeWebCommand.ts +26 -1
  12. package/src/commands/ServeWorkerCommand.ts +46 -30
  13. package/src/commands/devCommandLifecycle.types.ts +7 -11
  14. package/src/database/ConsumerDatabaseConnectionResolver.ts +55 -9
  15. package/src/database/DatabaseMigrationsApplyService.ts +2 -2
  16. package/src/dev/Builder.ts +1 -14
  17. package/src/dev/CliDevProxyServer.ts +457 -0
  18. package/src/dev/CliDevProxyServerFactory.ts +10 -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 +2 -2
  26. package/src/dev/DevNextHostEnvironmentBuilder.ts +35 -5
  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 +0 -4
  31. package/src/dev/DevSourceChangeClassifier.ts +33 -13
  32. package/src/dev/ListenPortConflictDescriber.ts +83 -0
  33. package/src/dev/WatchRootsResolver.ts +6 -4
  34. package/src/runtime/NextHostConsumerServerCommandFactory.ts +11 -2
  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,25 +1,27 @@
1
1
  import type { Logger } from "@codemation/host/next/server";
2
- import { CodemationPluginDiscovery } from "@codemation/host/server";
3
2
  import { spawn, type ChildProcess } from "node:child_process";
4
3
  import { createRequire } from "node:module";
5
4
  import path from "node:path";
6
5
  import process from "node:process";
7
6
 
8
7
  import type { DatabaseMigrationsApplyService } from "../database/DatabaseMigrationsApplyService";
8
+ import type { DevApiRuntimeFactory, DevApiRuntimeServerHandle } from "../dev/DevApiRuntimeFactory";
9
9
  import type { DevBootstrapSummaryFetcher } from "../dev/DevBootstrapSummaryFetcher";
10
+ import type { CliDevProxyServer } from "../dev/CliDevProxyServer";
11
+ import type { CliDevProxyServerFactory } from "../dev/CliDevProxyServerFactory";
10
12
  import type { DevCliBannerRenderer } from "../dev/DevCliBannerRenderer";
11
13
  import type { DevConsumerPublishBootstrap } from "../dev/DevConsumerPublishBootstrap";
12
14
  import { ConsumerEnvDotenvFilePredicate } from "../dev/ConsumerEnvDotenvFilePredicate";
15
+ import type { DevRebuildQueueFactory } from "../dev/DevRebuildQueueFactory";
13
16
  import type { DevSourceWatcher } from "../dev/DevSourceWatcher";
14
17
  import { DevSessionServices } from "../dev/DevSessionServices";
15
18
  import { DevLockFactory } from "../dev/Factory";
16
19
  import { DevTrackedProcessTreeKiller } from "../dev/DevTrackedProcessTreeKiller";
17
20
  import { DevSourceWatcherFactory } from "../dev/Runner";
21
+ import type { DevResolvedAuthSettings } from "../dev/DevAuthSettingsLoader";
18
22
  import { CliPathResolver, type CliPaths } from "../path/CliPathResolver";
19
- import { TypeScriptRuntimeConfigurator } from "../runtime/TypeScriptRuntimeConfigurator";
20
23
  import { NextHostConsumerServerCommandFactory } from "../runtime/NextHostConsumerServerCommandFactory";
21
-
22
- import type { DevResolvedAuthSettings } from "../dev/DevAuthSettingsLoader";
24
+ import { TypeScriptRuntimeConfigurator } from "../runtime/TypeScriptRuntimeConfigurator";
23
25
 
24
26
  import type { DevMode, DevMutableProcessState, DevPreparedRuntime } from "./devCommandLifecycle.types";
25
27
 
@@ -28,7 +30,6 @@ export class DevCommand {
28
30
 
29
31
  constructor(
30
32
  private readonly pathResolver: CliPathResolver,
31
- private readonly pluginDiscovery: CodemationPluginDiscovery,
32
33
  private readonly tsRuntime: TypeScriptRuntimeConfigurator,
33
34
  private readonly devLockFactory: DevLockFactory,
34
35
  private readonly devSourceWatcherFactory: DevSourceWatcherFactory,
@@ -41,15 +42,18 @@ export class DevCommand {
41
42
  private readonly consumerEnvDotenvFilePredicate: ConsumerEnvDotenvFilePredicate,
42
43
  private readonly devTrackedProcessTreeKiller: DevTrackedProcessTreeKiller,
43
44
  private readonly nextHostConsumerServerCommandFactory: NextHostConsumerServerCommandFactory,
45
+ private readonly devApiRuntimeFactory: DevApiRuntimeFactory,
46
+ private readonly cliDevProxyServerFactory: CliDevProxyServerFactory,
47
+ private readonly devRebuildQueueFactory: DevRebuildQueueFactory,
44
48
  ) {}
45
49
 
46
- async execute(consumerRoot: string): Promise<void> {
47
- const paths = await this.pathResolver.resolve(consumerRoot);
50
+ async execute(args: Readonly<{ consumerRoot: string; watchFramework?: boolean }>): Promise<void> {
51
+ const paths = await this.pathResolver.resolve(args.consumerRoot);
48
52
  this.devCliBannerRenderer.renderBrandHeader();
49
53
  this.tsRuntime.configure(paths.repoRoot);
50
54
  await this.databaseMigrationsApplyService.applyForConsumer(paths.consumerRoot);
51
55
  await this.devConsumerPublishBootstrap.ensurePublished(paths);
52
- const devMode = this.resolveDevModeFromEnv();
56
+ const devMode = this.resolveDevMode(args);
53
57
  const { nextPort, gatewayPort } = await this.session.sessionPorts.resolve({
54
58
  devMode,
55
59
  portEnv: process.env.PORT,
@@ -58,37 +62,42 @@ export class DevCommand {
58
62
  const devLock = this.devLockFactory.create();
59
63
  await devLock.acquire({
60
64
  consumerRoot: paths.consumerRoot,
61
- nextPort: devMode === "framework" ? nextPort : gatewayPort,
65
+ nextPort: devMode === "watch-framework" ? nextPort : gatewayPort,
62
66
  });
63
67
  const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
64
68
  const watcher = this.devSourceWatcherFactory.create();
65
69
  const processState = this.createInitialProcessState();
70
+ let proxyServer: CliDevProxyServer | null = null;
66
71
  try {
67
72
  const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
68
73
  const stopPromise = this.wireStopPromise(processState);
69
- const uiProxyBase = await this.startConsumerUiProxyWhenNeeded(prepared, processState);
74
+ const uiProxyBase = await this.startPackagedUiWhenNeeded(prepared, processState);
75
+ proxyServer = await this.startProxyServer(prepared.gatewayPort, uiProxyBase);
70
76
  const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
71
- await this.spawnGatewayChildAndWaitForHealth(prepared, processState, gatewayBaseUrl, uiProxyBase);
77
+ await this.bootInitialRuntime(prepared, processState, proxyServer);
72
78
  await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
73
79
  const initialSummary = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
74
80
  if (initialSummary) {
75
81
  this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
76
82
  }
77
- this.bindShutdownSignalsToChildProcesses(processState);
78
- await this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
79
- await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl);
80
- this.logConsumerDevHintWhenNeeded(devMode, gatewayPort);
83
+ this.bindShutdownSignalsToChildProcesses(processState, proxyServer);
84
+ await this.spawnDevUiWhenNeeded(prepared, processState, gatewayBaseUrl);
85
+ await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl, proxyServer);
86
+ this.logPackagedUiDevHintWhenNeeded(devMode, gatewayPort);
81
87
  await stopPromise;
82
88
  } finally {
83
89
  processState.stopRequested = true;
84
- await this.stopLiveChildProcesses(processState);
90
+ await this.stopLiveProcesses(processState, proxyServer);
85
91
  await watcher.stop();
86
92
  await devLock.release();
87
93
  }
88
94
  }
89
95
 
90
- private resolveDevModeFromEnv(): DevMode {
91
- return process.env.CODEMATION_DEV_MODE === "framework" ? "framework" : "consumer";
96
+ private resolveDevMode(args: Readonly<{ watchFramework?: boolean }>): DevMode {
97
+ if (args.watchFramework === true || process.env.CODEMATION_DEV_MODE === "framework") {
98
+ return "watch-framework";
99
+ }
100
+ return "packaged-ui";
92
101
  }
93
102
 
94
103
  private async prepareDevRuntime(
@@ -101,19 +110,7 @@ export class DevCommand {
101
110
  const developmentServerToken = this.session.devAuthLoader.resolveDevelopmentServerToken(
102
111
  process.env.CODEMATION_DEV_SERVER_TOKEN,
103
112
  );
104
- const gatewayEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
105
- packageName: "@codemation/dev-gateway",
106
- repoRoot: paths.repoRoot,
107
- sourceEntrypoint: "packages/dev-gateway/src/bin.ts",
108
- });
109
- const runtimeEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
110
- packageName: "@codemation/runtime-dev",
111
- repoRoot: paths.repoRoot,
112
- sourceEntrypoint: "packages/runtime-dev/src/bin.ts",
113
- });
114
- const runtimeWorkingDirectory = paths.repoRoot ?? paths.consumerRoot;
115
113
  const consumerEnv = this.session.consumerEnvLoader.load(paths.consumerRoot);
116
- const discoveredPluginPackagesJson = JSON.stringify(await this.pluginDiscovery.discover(paths.consumerRoot));
117
114
  return {
118
115
  paths,
119
116
  devMode,
@@ -121,21 +118,17 @@ export class DevCommand {
121
118
  gatewayPort,
122
119
  authSettings,
123
120
  developmentServerToken,
124
- gatewayEntrypoint,
125
- runtimeEntrypoint,
126
- runtimeWorkingDirectory,
127
- discoveredPluginPackagesJson,
128
121
  consumerEnv,
129
122
  };
130
123
  }
131
124
 
132
125
  private createInitialProcessState(): DevMutableProcessState {
133
126
  return {
134
- currentGateway: null,
135
- currentNextHost: null,
136
- currentUiNext: null,
137
- currentUiProxyBaseUrl: null,
138
- isRestartingNextHost: false,
127
+ currentDevUi: null,
128
+ currentPackagedUi: null,
129
+ currentPackagedUiBaseUrl: null,
130
+ currentRuntime: null,
131
+ isRestartingUi: false,
139
132
  stopRequested: false,
140
133
  stopResolve: null,
141
134
  stopReject: null,
@@ -153,25 +146,22 @@ export class DevCommand {
153
146
  return `http://127.0.0.1:${gatewayPort}`;
154
147
  }
155
148
 
156
- /**
157
- * Consumer mode: run `next start` for the host UI and wait until it responds, so the gateway can proxy to it.
158
- * Framework mode: no separate UI child (Next runs in dev later).
159
- */
160
- private async startConsumerUiProxyWhenNeeded(
149
+ private async startPackagedUiWhenNeeded(
161
150
  prepared: DevPreparedRuntime,
162
151
  state: DevMutableProcessState,
163
152
  ): Promise<string> {
164
- if (prepared.devMode !== "consumer") {
153
+ if (prepared.devMode !== "packaged-ui") {
165
154
  return "";
166
155
  }
167
156
  const websocketPort = prepared.gatewayPort;
168
- const uiProxyBase = state.currentUiProxyBaseUrl ?? `http://127.0.0.1:${await this.session.loopbackPortAllocator.allocate()}`;
169
- state.currentUiProxyBaseUrl = uiProxyBase;
170
- await this.spawnConsumerUiProxy(prepared, state, prepared.authSettings, websocketPort, uiProxyBase);
157
+ const uiProxyBase =
158
+ state.currentPackagedUiBaseUrl ?? `http://127.0.0.1:${await this.session.loopbackPortAllocator.allocate()}`;
159
+ state.currentPackagedUiBaseUrl = uiProxyBase;
160
+ await this.spawnPackagedUi(prepared, state, prepared.authSettings, websocketPort, uiProxyBase);
171
161
  return uiProxyBase;
172
162
  }
173
163
 
174
- private async spawnConsumerUiProxy(
164
+ private async spawnPackagedUi(
175
165
  prepared: DevPreparedRuntime,
176
166
  state: DevMutableProcessState,
177
167
  authSettings: DevResolvedAuthSettings,
@@ -200,77 +190,48 @@ export class DevCommand {
200
190
  skipUiAuth: authSettings.skipUiAuth,
201
191
  websocketPort,
202
192
  });
203
- state.currentUiNext = spawn(nextHostCommand.command, nextHostCommand.args, {
193
+ state.currentPackagedUi = spawn(nextHostCommand.command, nextHostCommand.args, {
204
194
  cwd: nextHostCommand.cwd,
205
195
  ...this.devDetachedChildSpawnOptions(),
206
196
  env: nextHostEnvironment,
207
197
  });
208
- state.currentUiNext.on("error", (error) => {
209
- if (state.stopRequested || state.isRestartingNextHost) {
198
+ state.currentPackagedUi.on("error", (error) => {
199
+ if (state.stopRequested || state.isRestartingUi) {
210
200
  return;
211
201
  }
212
202
  state.stopRequested = true;
213
203
  state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
214
204
  });
215
- state.currentUiNext.on("exit", (code) => {
216
- if (state.stopRequested || state.isRestartingNextHost) {
205
+ state.currentPackagedUi.on("exit", (code) => {
206
+ if (state.stopRequested || state.isRestartingUi) {
217
207
  return;
218
208
  }
219
209
  state.stopRequested = true;
220
- if (state.currentGateway?.exitCode === null) {
221
- void this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
222
- }
223
- state.stopReject?.(new Error(`next start (consumer UI) exited unexpectedly with code ${code ?? 0}.`));
210
+ state.stopReject?.(new Error(`next start (packaged UI) exited unexpectedly with code ${code ?? 0}.`));
224
211
  });
225
212
  await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
226
213
  }
227
214
 
228
- private async spawnGatewayChildAndWaitForHealth(
215
+ private async startProxyServer(gatewayPort: number, uiProxyBase: string): Promise<CliDevProxyServer> {
216
+ const proxyServer = this.cliDevProxyServerFactory.create(gatewayPort);
217
+ proxyServer.setUiProxyTarget(uiProxyBase.length > 0 ? uiProxyBase : null);
218
+ await proxyServer.start();
219
+ await this.session.devHttpProbe.waitUntilGatewayHealthy(this.gatewayBaseHttpUrl(gatewayPort));
220
+ return proxyServer;
221
+ }
222
+
223
+ private async bootInitialRuntime(
229
224
  prepared: DevPreparedRuntime,
230
225
  state: DevMutableProcessState,
231
- gatewayBaseUrl: string,
232
- uiProxyBase: string,
226
+ proxyServer: CliDevProxyServer,
233
227
  ): Promise<void> {
234
- const gatewayProcessEnv = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(
235
- process.env,
236
- prepared.consumerEnv,
237
- );
238
- state.currentGateway = spawn(prepared.gatewayEntrypoint.command, prepared.gatewayEntrypoint.args, {
239
- cwd: prepared.runtimeWorkingDirectory,
240
- ...this.devDetachedChildSpawnOptions(),
241
- env: {
242
- ...gatewayProcessEnv,
243
- ...prepared.gatewayEntrypoint.env,
244
- CODEMATION_DEV_GATEWAY_HTTP_PORT: String(prepared.gatewayPort),
245
- CODEMATION_RUNTIME_CHILD_BIN: prepared.runtimeEntrypoint.command,
246
- CODEMATION_RUNTIME_CHILD_ARGS_JSON: JSON.stringify(prepared.runtimeEntrypoint.args),
247
- CODEMATION_RUNTIME_CHILD_ENV_JSON: JSON.stringify(prepared.runtimeEntrypoint.env),
248
- CODEMATION_RUNTIME_CHILD_CWD: prepared.runtimeWorkingDirectory,
249
- CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
250
- CODEMATION_DISCOVERED_PLUGIN_PACKAGES_JSON: prepared.discoveredPluginPackagesJson,
251
- CODEMATION_PREFER_PLUGIN_SOURCE_ENTRY: "true",
252
- CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
253
- CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
254
- NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
255
- WS_NO_BUFFER_UTIL: "1",
256
- WS_NO_UTF_8_VALIDATE: "1",
257
- ...(uiProxyBase.length > 0 ? { CODEMATION_DEV_UI_PROXY_TARGET: uiProxyBase } : {}),
258
- },
259
- });
260
- state.currentGateway.on("error", (error) => {
261
- if (!state.stopRequested) {
262
- state.stopRequested = true;
263
- state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
264
- }
265
- });
266
- state.currentGateway.on("exit", (code) => {
267
- if (state.stopRequested) {
268
- return;
269
- }
270
- state.stopRequested = true;
271
- state.stopReject?.(new Error(`codemation dev-gateway exited unexpectedly with code ${code ?? 0}.`));
228
+ const runtime = await this.createRuntime(prepared);
229
+ state.currentRuntime = runtime;
230
+ await proxyServer.activateRuntime({
231
+ httpPort: runtime.httpPort,
232
+ workflowWebSocketPort: runtime.workflowWebSocketPort,
272
233
  });
273
- await this.session.devHttpProbe.waitUntilGatewayHealthy(gatewayBaseUrl);
234
+ proxyServer.setBuildStatus("idle");
274
235
  }
275
236
 
276
237
  private devDetachedChildSpawnOptions(): Readonly<{
@@ -283,7 +244,10 @@ export class DevCommand {
283
244
  : { stdio: "inherit", detached: true };
284
245
  }
285
246
 
286
- private bindShutdownSignalsToChildProcesses(state: DevMutableProcessState): void {
247
+ private bindShutdownSignalsToChildProcesses(
248
+ state: DevMutableProcessState,
249
+ proxyServer: CliDevProxyServer | null,
250
+ ): void {
287
251
  let shutdownInProgress = false;
288
252
  const runShutdown = async (): Promise<void> => {
289
253
  if (shutdownInProgress) {
@@ -292,13 +256,7 @@ export class DevCommand {
292
256
  shutdownInProgress = true;
293
257
  state.stopRequested = true;
294
258
  process.stdout.write("\n[codemation] Stopping..\n");
295
- const children: ChildProcess[] = [];
296
- for (const child of [state.currentUiNext, state.currentNextHost, state.currentGateway]) {
297
- if (child && child.exitCode === null && child.signalCode === null) {
298
- children.push(child);
299
- }
300
- }
301
- await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
259
+ await this.stopLiveProcesses(state, proxyServer);
302
260
  process.stdout.write("[codemation] Stopped.\n");
303
261
  state.stopResolve?.();
304
262
  };
@@ -309,21 +267,18 @@ export class DevCommand {
309
267
  }
310
268
  }
311
269
 
312
- /**
313
- * Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
314
- */
315
- private async spawnFrameworkNextHostWhenNeeded(
270
+ private async spawnDevUiWhenNeeded(
316
271
  prepared: DevPreparedRuntime,
317
272
  state: DevMutableProcessState,
318
273
  gatewayBaseUrl: string,
319
274
  ): Promise<void> {
320
- if (prepared.devMode !== "framework") {
275
+ if (prepared.devMode !== "watch-framework") {
321
276
  return;
322
277
  }
323
- await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, prepared.authSettings);
278
+ await this.spawnDevUi(prepared, state, gatewayBaseUrl, prepared.authSettings);
324
279
  }
325
280
 
326
- private async spawnFrameworkNextHost(
281
+ private async spawnDevUi(
327
282
  prepared: DevPreparedRuntime,
328
283
  state: DevMutableProcessState,
329
284
  gatewayBaseUrl: string,
@@ -341,29 +296,26 @@ export class DevCommand {
341
296
  websocketPort,
342
297
  runtimeDevUrl: gatewayBaseUrl,
343
298
  });
344
- state.currentNextHost = spawn("pnpm", ["exec", "next", "dev"], {
299
+ state.currentDevUi = spawn("pnpm", ["exec", "next", "dev"], {
345
300
  cwd: nextHostRoot,
346
301
  ...this.devDetachedChildSpawnOptions(),
347
302
  env: nextHostEnvironment,
348
303
  });
349
- state.currentNextHost.on("exit", (code) => {
304
+ state.currentDevUi.on("exit", (code) => {
350
305
  const normalizedCode = code ?? 0;
351
- if (state.stopRequested || state.isRestartingNextHost) {
306
+ if (state.stopRequested || state.isRestartingUi) {
352
307
  return;
353
308
  }
354
309
  if (normalizedCode === 0) {
355
310
  state.stopRequested = true;
356
- if (state.currentGateway?.exitCode === null) {
357
- void this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
358
- }
359
311
  state.stopResolve?.();
360
312
  return;
361
313
  }
362
314
  state.stopRequested = true;
363
315
  state.stopReject?.(new Error(`next host exited with code ${normalizedCode}.`));
364
316
  });
365
- state.currentNextHost.on("error", (error) => {
366
- if (state.stopRequested || state.isRestartingNextHost) {
317
+ state.currentDevUi.on("error", (error) => {
318
+ if (state.stopRequested || state.isRestartingUi) {
367
319
  return;
368
320
  }
369
321
  state.stopRequested = true;
@@ -378,7 +330,13 @@ export class DevCommand {
378
330
  watcher: DevSourceWatcher,
379
331
  devMode: DevMode,
380
332
  gatewayBaseUrl: string,
333
+ proxyServer: CliDevProxyServer,
381
334
  ): Promise<void> {
335
+ const rebuildQueue = this.devRebuildQueueFactory.create({
336
+ run: async (request) => {
337
+ await this.runQueuedRebuild(prepared, state, gatewayBaseUrl, proxyServer, request);
338
+ },
339
+ });
382
340
  await watcher.start({
383
341
  roots: this.session.watchRootsResolver.resolve({
384
342
  consumerRoot: prepared.paths.consumerRoot,
@@ -388,7 +346,7 @@ export class DevCommand {
388
346
  onChange: async ({ changedPaths }) => {
389
347
  if (changedPaths.length > 0 && changedPaths.every((p) => this.consumerEnvDotenvFilePredicate.matches(p))) {
390
348
  process.stdout.write(
391
- "\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",
349
+ "\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",
392
350
  );
393
351
  return;
394
352
  }
@@ -397,113 +355,187 @@ export class DevCommand {
397
355
  changedPaths,
398
356
  consumerRoot: prepared.paths.consumerRoot,
399
357
  });
400
- const shouldRestartNextHost = this.session.sourceChangeClassifier.requiresNextHostRestart({
358
+ const shouldRestartUi = this.session.sourceChangeClassifier.requiresUiRestart({
401
359
  changedPaths,
402
360
  consumerRoot: prepared.paths.consumerRoot,
403
361
  });
404
362
  process.stdout.write(
405
- shouldRestartNextHost
406
- ? "\n[codemation] Consumer config change detected — rebuilding consumer, restarting runtime, and restarting Next host…\n"
407
- : "\n[codemation] Source change detected — rebuilding consumer and restarting runtime…\n",
363
+ shouldRestartUi
364
+ ? "\n[codemation] Source change detected — rebuilding consumer, restarting the runtime, and restarting the UI…\n"
365
+ : "\n[codemation] Source change detected — rebuilding consumer and restarting the runtime…\n",
408
366
  );
409
- if (shouldRepublishConsumerOutput) {
410
- await this.devConsumerPublishBootstrap.ensurePublished(prepared.paths);
411
- }
412
- await this.session.sourceRestartCoordinator.runHandshakeAfterSourceChange(
413
- gatewayBaseUrl,
414
- prepared.developmentServerToken,
415
- );
416
- process.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
417
- await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
418
- if (shouldRestartNextHost) {
419
- await this.restartNextHostForConfigChange(prepared, state, gatewayBaseUrl);
420
- }
421
- const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
422
- if (json) {
423
- this.devCliBannerRenderer.renderCompact(json);
424
- }
425
- process.stdout.write("[codemation] Runtime ready.\n");
367
+ await rebuildQueue.enqueue({
368
+ changedPaths,
369
+ shouldRepublishConsumerOutput,
370
+ shouldRestartUi,
371
+ });
426
372
  } catch (error) {
427
- await this.failDevSessionAfterIrrecoverableSourceError(state, error);
373
+ await this.failDevSessionAfterIrrecoverableSourceError(state, proxyServer, error);
428
374
  }
429
375
  },
430
376
  });
431
377
  }
432
378
 
433
- private async restartNextHostForConfigChange(
379
+ private async runQueuedRebuild(
380
+ prepared: DevPreparedRuntime,
381
+ state: DevMutableProcessState,
382
+ gatewayBaseUrl: string,
383
+ proxyServer: CliDevProxyServer,
384
+ request: Readonly<{
385
+ changedPaths: ReadonlyArray<string>;
386
+ shouldRepublishConsumerOutput: boolean;
387
+ shouldRestartUi: boolean;
388
+ }>,
389
+ ): Promise<void> {
390
+ void request.changedPaths;
391
+ proxyServer.setBuildStatus("building");
392
+ proxyServer.broadcastBuildStarted();
393
+ try {
394
+ if (request.shouldRepublishConsumerOutput) {
395
+ await this.devConsumerPublishBootstrap.ensurePublished(prepared.paths);
396
+ }
397
+ await this.stopCurrentRuntime(state, proxyServer);
398
+ process.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
399
+ const runtime = await this.createRuntime(prepared);
400
+ state.currentRuntime = runtime;
401
+ await proxyServer.activateRuntime({
402
+ httpPort: runtime.httpPort,
403
+ workflowWebSocketPort: runtime.workflowWebSocketPort,
404
+ });
405
+ if (request.shouldRestartUi) {
406
+ await this.restartUiAfterSourceChange(prepared, state, gatewayBaseUrl);
407
+ }
408
+ await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
409
+ const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
410
+ if (json) {
411
+ this.devCliBannerRenderer.renderCompact(json);
412
+ }
413
+ proxyServer.setBuildStatus("idle");
414
+ proxyServer.broadcastBuildCompleted(runtime.buildVersion);
415
+ process.stdout.write("[codemation] Runtime ready.\n");
416
+ } catch (error) {
417
+ proxyServer.setBuildStatus("idle");
418
+ proxyServer.broadcastBuildFailed(error instanceof Error ? error.message : String(error));
419
+ throw error;
420
+ }
421
+ }
422
+
423
+ private async restartUiAfterSourceChange(
434
424
  prepared: DevPreparedRuntime,
435
425
  state: DevMutableProcessState,
436
426
  gatewayBaseUrl: string,
437
427
  ): Promise<void> {
438
428
  const refreshedAuthSettings = await this.session.devAuthLoader.loadForConsumer(prepared.paths.consumerRoot);
439
- process.stdout.write("[codemation] Restarting Next host to apply auth/database/scheduler/branding changes…\n");
440
- state.isRestartingNextHost = true;
429
+ process.stdout.write("[codemation] Restarting the UI process to apply source changes…\n");
430
+ state.isRestartingUi = true;
441
431
  try {
442
- if (prepared.devMode === "consumer") {
443
- await this.restartConsumerUiProxy(prepared, state, refreshedAuthSettings);
432
+ if (prepared.devMode === "packaged-ui") {
433
+ await this.restartPackagedUi(prepared, state, refreshedAuthSettings);
444
434
  return;
445
435
  }
446
- await this.restartFrameworkNextHost(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
436
+ await this.restartDevUi(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
447
437
  } finally {
448
- state.isRestartingNextHost = false;
438
+ state.isRestartingUi = false;
449
439
  }
450
440
  }
451
441
 
452
- private async restartConsumerUiProxy(
442
+ private async restartPackagedUi(
453
443
  prepared: DevPreparedRuntime,
454
444
  state: DevMutableProcessState,
455
445
  authSettings: DevResolvedAuthSettings,
456
446
  ): Promise<void> {
457
- if (state.currentUiNext && state.currentUiNext.exitCode === null && state.currentUiNext.signalCode === null) {
458
- await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentUiNext);
447
+ if (
448
+ state.currentPackagedUi &&
449
+ state.currentPackagedUi.exitCode === null &&
450
+ state.currentPackagedUi.signalCode === null
451
+ ) {
452
+ await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentPackagedUi);
459
453
  }
460
- state.currentUiNext = null;
461
- const uiProxyBaseUrl = state.currentUiProxyBaseUrl;
454
+ state.currentPackagedUi = null;
455
+ const uiProxyBaseUrl = state.currentPackagedUiBaseUrl;
462
456
  if (!uiProxyBaseUrl) {
463
- throw new Error("Consumer UI proxy base URL is missing during Next host restart.");
457
+ throw new Error("Packaged UI proxy base URL is missing during UI restart.");
464
458
  }
465
- await this.spawnConsumerUiProxy(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
459
+ await this.spawnPackagedUi(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
466
460
  }
467
461
 
468
- private async restartFrameworkNextHost(
462
+ private async restartDevUi(
469
463
  prepared: DevPreparedRuntime,
470
464
  state: DevMutableProcessState,
471
465
  gatewayBaseUrl: string,
472
466
  authSettings: DevResolvedAuthSettings,
473
467
  ): Promise<void> {
474
- if (state.currentNextHost && state.currentNextHost.exitCode === null && state.currentNextHost.signalCode === null) {
475
- await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentNextHost);
468
+ if (state.currentDevUi && state.currentDevUi.exitCode === null && state.currentDevUi.signalCode === null) {
469
+ await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentDevUi);
476
470
  }
477
- state.currentNextHost = null;
478
- await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings);
471
+ state.currentDevUi = null;
472
+ await this.spawnDevUi(prepared, state, gatewayBaseUrl, authSettings);
479
473
  }
480
474
 
481
475
  private async failDevSessionAfterIrrecoverableSourceError(
482
476
  state: DevMutableProcessState,
477
+ proxyServer: CliDevProxyServer | null,
483
478
  error: unknown,
484
479
  ): Promise<void> {
485
480
  const exception = error instanceof Error ? error : new Error(String(error));
486
481
  state.stopRequested = true;
487
- await this.stopLiveChildProcesses(state);
482
+ await this.stopLiveProcesses(state, proxyServer);
488
483
  state.stopReject?.(exception);
489
484
  }
490
485
 
491
- private async stopLiveChildProcesses(state: DevMutableProcessState): Promise<void> {
486
+ private async stopLiveProcesses(state: DevMutableProcessState, proxyServer: CliDevProxyServer | null): Promise<void> {
487
+ await this.stopCurrentRuntime(state, proxyServer);
492
488
  const children: ChildProcess[] = [];
493
- for (const child of [state.currentUiNext, state.currentNextHost, state.currentGateway]) {
489
+ for (const child of [state.currentPackagedUi, state.currentDevUi]) {
494
490
  if (child && child.exitCode === null && child.signalCode === null) {
495
491
  children.push(child);
496
492
  }
497
493
  }
498
494
  await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
495
+ if (proxyServer) {
496
+ await proxyServer.stop();
497
+ }
498
+ }
499
+
500
+ private async stopCurrentRuntime(
501
+ state: DevMutableProcessState,
502
+ proxyServer: CliDevProxyServer | null,
503
+ ): Promise<void> {
504
+ const runtime = state.currentRuntime;
505
+ state.currentRuntime = null;
506
+ if (proxyServer) {
507
+ await proxyServer.activateRuntime(null);
508
+ }
509
+ if (runtime) {
510
+ await runtime.stop();
511
+ }
512
+ }
513
+
514
+ private async createRuntime(prepared: DevPreparedRuntime): Promise<DevApiRuntimeServerHandle> {
515
+ const runtimeEnvironment = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(
516
+ process.env,
517
+ prepared.consumerEnv,
518
+ );
519
+ return await this.devApiRuntimeFactory.create({
520
+ consumerRoot: prepared.paths.consumerRoot,
521
+ runtimeWorkingDirectory: process.cwd(),
522
+ env: {
523
+ ...runtimeEnvironment,
524
+ CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
525
+ CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
526
+ NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
527
+ WS_NO_BUFFER_UTIL: "1",
528
+ WS_NO_UTF_8_VALIDATE: "1",
529
+ },
530
+ });
499
531
  }
500
532
 
501
- private logConsumerDevHintWhenNeeded(devMode: DevMode, gatewayPort: number): void {
502
- if (devMode !== "consumer") {
533
+ private logPackagedUiDevHintWhenNeeded(devMode: DevMode, gatewayPort: number): void {
534
+ if (devMode !== "packaged-ui") {
503
535
  return;
504
536
  }
505
537
  this.cliLogger.info(
506
- `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.`,
538
+ `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.`,
507
539
  );
508
540
  }
509
541
  }