@codemation/cli 0.0.4 → 0.0.5

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.
@@ -155,9 +155,9 @@ var DevCommand = class {
155
155
  });
156
156
  const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
157
157
  const watcher = this.devSourceWatcherFactory.create();
158
+ const processState = this.createInitialProcessState();
158
159
  try {
159
160
  const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
160
- const processState = this.createInitialProcessState();
161
161
  const stopPromise = this.wireStopPromise(processState);
162
162
  const uiProxyBase = await this.startConsumerUiProxyWhenNeeded(prepared, processState);
163
163
  const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
@@ -166,11 +166,13 @@ var DevCommand = class {
166
166
  const initialSummary = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
167
167
  if (initialSummary) this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
168
168
  this.bindShutdownSignalsToChildProcesses(processState);
169
- this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
170
- await this.startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl);
169
+ await this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
170
+ await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl);
171
171
  this.logConsumerDevHintWhenNeeded(devMode, gatewayPort);
172
172
  await stopPromise;
173
173
  } finally {
174
+ processState.stopRequested = true;
175
+ await this.stopLiveChildProcesses(processState);
174
176
  await watcher.stop();
175
177
  await devLock.release();
176
178
  }
@@ -211,6 +213,8 @@ var DevCommand = class {
211
213
  currentGateway: null,
212
214
  currentNextHost: null,
213
215
  currentUiNext: null,
216
+ currentUiProxyBaseUrl: null,
217
+ isRestartingNextHost: false,
214
218
  stopRequested: false,
215
219
  stopResolve: null,
216
220
  stopReject: null
@@ -232,48 +236,46 @@ var DevCommand = class {
232
236
  async startConsumerUiProxyWhenNeeded(prepared, state) {
233
237
  if (prepared.devMode !== "consumer") return "";
234
238
  const websocketPort = prepared.gatewayPort;
235
- const uiPort = await this.session.loopbackPortAllocator.allocate();
236
- const uiProxyBase = `http://127.0.0.1:${uiPort}`;
239
+ const uiProxyBase = state.currentUiProxyBaseUrl ?? `http://127.0.0.1:${await this.session.loopbackPortAllocator.allocate()}`;
240
+ state.currentUiProxyBaseUrl = uiProxyBase;
241
+ await this.spawnConsumerUiProxy(prepared, state, prepared.authSettings, websocketPort, uiProxyBase);
242
+ return uiProxyBase;
243
+ }
244
+ async spawnConsumerUiProxy(prepared, state, authSettings, websocketPort, uiProxyBase) {
237
245
  const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
238
246
  const nextHostRoot = path.dirname(nextHostPackageJsonPath);
239
247
  const nextHostCommand = await this.nextHostConsumerServerCommandFactory.create({ nextHostRoot });
240
248
  const consumerOutputManifestPath = path.resolve(prepared.paths.consumerRoot, ".codemation", "output", "current.json");
249
+ const uiPort = Number(new URL(uiProxyBase).port);
250
+ const nextHostEnvironment = this.session.nextHostEnvBuilder.buildConsumerUiProxy({
251
+ authConfigJson: authSettings.authConfigJson,
252
+ authSecret: authSettings.authSecret,
253
+ consumerRoot: prepared.paths.consumerRoot,
254
+ consumerOutputManifestPath,
255
+ developmentServerToken: prepared.developmentServerToken,
256
+ nextPort: uiPort,
257
+ publicBaseUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
258
+ runtimeDevUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
259
+ skipUiAuth: authSettings.skipUiAuth,
260
+ websocketPort
261
+ });
241
262
  state.currentUiNext = spawn(nextHostCommand.command, nextHostCommand.args, {
242
263
  cwd: nextHostCommand.cwd,
243
264
  ...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
- }
265
+ env: nextHostEnvironment
262
266
  });
263
267
  state.currentUiNext.on("error", (error) => {
264
- if (!state.stopRequested) {
265
- state.stopRequested = true;
266
- state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
267
- }
268
+ if (state.stopRequested || state.isRestartingNextHost) return;
269
+ state.stopRequested = true;
270
+ state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
268
271
  });
269
272
  state.currentUiNext.on("exit", (code) => {
270
- if (state.stopRequested) return;
273
+ if (state.stopRequested || state.isRestartingNextHost) return;
271
274
  state.stopRequested = true;
272
275
  if (state.currentGateway?.exitCode === null) this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
273
276
  state.stopReject?.(/* @__PURE__ */ new Error(`next start (consumer UI) exited unexpectedly with code ${code ?? 0}.`));
274
277
  });
275
278
  await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
276
- return uiProxyBase;
277
279
  }
278
280
  async spawnGatewayChildAndWaitForHealth(prepared, state, gatewayBaseUrl, uiProxyBase) {
279
281
  const gatewayProcessEnv = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(process$1.env, prepared.consumerEnv);
@@ -350,17 +352,20 @@ var DevCommand = class {
350
352
  /**
351
353
  * Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
352
354
  */
353
- spawnFrameworkNextHostWhenNeeded(prepared, state, gatewayBaseUrl) {
355
+ async spawnFrameworkNextHostWhenNeeded(prepared, state, gatewayBaseUrl) {
354
356
  if (prepared.devMode !== "framework") return;
357
+ await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, prepared.authSettings);
358
+ }
359
+ async spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings) {
355
360
  const websocketPort = prepared.gatewayPort;
356
361
  const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
357
362
  const nextHostRoot = path.dirname(nextHostPackageJsonPath);
358
363
  const nextHostEnvironment = this.session.nextHostEnvBuilder.build({
359
- authConfigJson: prepared.authSettings.authConfigJson,
364
+ authConfigJson: authSettings.authConfigJson,
360
365
  consumerRoot: prepared.paths.consumerRoot,
361
366
  developmentServerToken: prepared.developmentServerToken,
362
367
  nextPort: prepared.nextPort,
363
- skipUiAuth: prepared.authSettings.skipUiAuth,
368
+ skipUiAuth: authSettings.skipUiAuth,
364
369
  websocketPort,
365
370
  runtimeDevUrl: gatewayBaseUrl
366
371
  });
@@ -375,7 +380,7 @@ var DevCommand = class {
375
380
  });
376
381
  state.currentNextHost.on("exit", (code) => {
377
382
  const normalizedCode = code ?? 0;
378
- if (state.stopRequested) return;
383
+ if (state.stopRequested || state.isRestartingNextHost) return;
379
384
  if (normalizedCode === 0) {
380
385
  state.stopRequested = true;
381
386
  if (state.currentGateway?.exitCode === null) this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
@@ -386,13 +391,13 @@ var DevCommand = class {
386
391
  state.stopReject?.(/* @__PURE__ */ new Error(`next host exited with code ${normalizedCode}.`));
387
392
  });
388
393
  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
- }
394
+ if (state.stopRequested || state.isRestartingNextHost) return;
395
+ state.stopRequested = true;
396
+ state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
393
397
  });
398
+ await this.session.devHttpProbe.waitUntilUrlRespondsOk(`http://127.0.0.1:${prepared.nextPort}/`);
394
399
  }
395
- async startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl) {
400
+ async startWatcherForSourceRestart(prepared, state, watcher, devMode, gatewayBaseUrl) {
396
401
  await watcher.start({
397
402
  roots: this.session.watchRootsResolver.resolve({
398
403
  consumerRoot: prepared.paths.consumerRoot,
@@ -404,16 +409,71 @@ var DevCommand = class {
404
409
  process$1.stdout.write("\n[codemation] Consumer environment file changed (e.g. .env). Restart the `codemation dev` process so the gateway and runtime pick up updated variables (host `process.env` does not hot-reload).\n");
405
410
  return;
406
411
  }
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");
412
+ try {
413
+ const shouldRepublishConsumerOutput = this.session.sourceChangeClassifier.shouldRepublishConsumerOutput({
414
+ changedPaths,
415
+ consumerRoot: prepared.paths.consumerRoot
416
+ });
417
+ const shouldRestartNextHost = this.session.sourceChangeClassifier.requiresNextHostRestart({
418
+ changedPaths,
419
+ consumerRoot: prepared.paths.consumerRoot
420
+ });
421
+ process$1.stdout.write(shouldRestartNextHost ? "\n[codemation] Consumer config change detected — rebuilding consumer, restarting runtime, and restarting Next host…\n" : "\n[codemation] Source change detected — rebuilding consumer and restarting runtime…\n");
422
+ if (shouldRepublishConsumerOutput) await this.devConsumerPublishBootstrap.ensurePublished(prepared.paths);
423
+ await this.session.sourceRestartCoordinator.runHandshakeAfterSourceChange(gatewayBaseUrl, prepared.developmentServerToken);
424
+ process$1.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
425
+ await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
426
+ if (shouldRestartNextHost) await this.restartNextHostForConfigChange(prepared, state, gatewayBaseUrl);
427
+ const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
428
+ if (json) this.devCliBannerRenderer.renderCompact(json);
429
+ process$1.stdout.write("[codemation] Runtime ready.\n");
430
+ } catch (error) {
431
+ await this.failDevSessionAfterIrrecoverableSourceError(state, error);
432
+ }
414
433
  }
415
434
  });
416
435
  }
436
+ async restartNextHostForConfigChange(prepared, state, gatewayBaseUrl) {
437
+ const refreshedAuthSettings = await this.session.devAuthLoader.loadForConsumer(prepared.paths.consumerRoot);
438
+ process$1.stdout.write("[codemation] Restarting Next host to apply auth/database/scheduler/branding changes…\n");
439
+ state.isRestartingNextHost = true;
440
+ try {
441
+ if (prepared.devMode === "consumer") {
442
+ await this.restartConsumerUiProxy(prepared, state, refreshedAuthSettings);
443
+ return;
444
+ }
445
+ await this.restartFrameworkNextHost(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
446
+ } finally {
447
+ state.isRestartingNextHost = false;
448
+ }
449
+ }
450
+ async restartConsumerUiProxy(prepared, state, authSettings) {
451
+ if (state.currentUiNext && state.currentUiNext.exitCode === null && state.currentUiNext.signalCode === null) await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentUiNext);
452
+ state.currentUiNext = null;
453
+ const uiProxyBaseUrl = state.currentUiProxyBaseUrl;
454
+ if (!uiProxyBaseUrl) throw new Error("Consumer UI proxy base URL is missing during Next host restart.");
455
+ await this.spawnConsumerUiProxy(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
456
+ }
457
+ async restartFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings) {
458
+ if (state.currentNextHost && state.currentNextHost.exitCode === null && state.currentNextHost.signalCode === null) await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentNextHost);
459
+ state.currentNextHost = null;
460
+ await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings);
461
+ }
462
+ async failDevSessionAfterIrrecoverableSourceError(state, error) {
463
+ const exception = error instanceof Error ? error : new Error(String(error));
464
+ state.stopRequested = true;
465
+ await this.stopLiveChildProcesses(state);
466
+ state.stopReject?.(exception);
467
+ }
468
+ async stopLiveChildProcesses(state) {
469
+ const children = [];
470
+ for (const child of [
471
+ state.currentUiNext,
472
+ state.currentNextHost,
473
+ state.currentGateway
474
+ ]) if (child && child.exitCode === null && child.signalCode === null) children.push(child);
475
+ await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
476
+ }
417
477
  logConsumerDevHintWhenNeeded(devMode, gatewayPort) {
418
478
  if (devMode !== "consumer") return;
419
479
  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.`);
@@ -1532,8 +1592,8 @@ var DevHttpProbe = class {
1532
1592
  async waitUntilUrlRespondsOk(url) {
1533
1593
  for (let attempt = 0; attempt < 200; attempt += 1) {
1534
1594
  try {
1535
- const response = await fetch(url);
1536
- if (response.ok || response.status === 404) return;
1595
+ const response = await fetch(url, { redirect: "manual" });
1596
+ if (response.ok || response.status === 404 || this.isRedirectStatus(response.status)) return;
1537
1597
  } catch {}
1538
1598
  await setTimeout$1(50);
1539
1599
  }
@@ -1562,6 +1622,9 @@ var DevHttpProbe = class {
1562
1622
  }
1563
1623
  throw new Error("Timed out waiting for dev runtime bootstrap summary.");
1564
1624
  }
1625
+ isRedirectStatus(status) {
1626
+ return status >= 300 && status < 400;
1627
+ }
1565
1628
  };
1566
1629
 
1567
1630
  //#endregion
@@ -1571,6 +1634,24 @@ var DevNextHostEnvironmentBuilder = class {
1571
1634
  this.consumerEnvLoader = consumerEnvLoader;
1572
1635
  this.sourceMapNodeOptions = sourceMapNodeOptions;
1573
1636
  }
1637
+ buildConsumerUiProxy(args) {
1638
+ return {
1639
+ ...this.build({
1640
+ authConfigJson: args.authConfigJson,
1641
+ consumerRoot: args.consumerRoot,
1642
+ developmentServerToken: args.developmentServerToken,
1643
+ nextPort: args.nextPort,
1644
+ runtimeDevUrl: args.runtimeDevUrl,
1645
+ skipUiAuth: args.skipUiAuth,
1646
+ websocketPort: args.websocketPort,
1647
+ consumerOutputManifestPath: args.consumerOutputManifestPath
1648
+ }),
1649
+ AUTH_SECRET: args.authSecret,
1650
+ AUTH_URL: args.publicBaseUrl,
1651
+ NEXTAUTH_SECRET: args.authSecret,
1652
+ NEXTAUTH_URL: args.publicBaseUrl
1653
+ };
1654
+ }
1574
1655
  build(args) {
1575
1656
  const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process$1.env);
1576
1657
  const manifestPath = args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
@@ -1616,7 +1697,7 @@ var DevSessionPortsResolver = class {
1616
1697
  * Bundles dependencies for {@link DevCommand} so the command stays a thin orchestrator.
1617
1698
  */
1618
1699
  var DevSessionServices = class {
1619
- constructor(consumerEnvLoader, sourceMapNodeOptions, sessionPorts, loopbackPortAllocator, devHttpProbe, runtimeEntrypointResolver, devAuthLoader, nextHostEnvBuilder, watchRootsResolver, sourceRestartCoordinator) {
1700
+ constructor(consumerEnvLoader, sourceMapNodeOptions, sessionPorts, loopbackPortAllocator, devHttpProbe, runtimeEntrypointResolver, devAuthLoader, nextHostEnvBuilder, watchRootsResolver, sourceChangeClassifier, sourceRestartCoordinator) {
1620
1701
  this.consumerEnvLoader = consumerEnvLoader;
1621
1702
  this.sourceMapNodeOptions = sourceMapNodeOptions;
1622
1703
  this.sessionPorts = sessionPorts;
@@ -1626,10 +1707,38 @@ var DevSessionServices = class {
1626
1707
  this.devAuthLoader = devAuthLoader;
1627
1708
  this.nextHostEnvBuilder = nextHostEnvBuilder;
1628
1709
  this.watchRootsResolver = watchRootsResolver;
1710
+ this.sourceChangeClassifier = sourceChangeClassifier;
1629
1711
  this.sourceRestartCoordinator = sourceRestartCoordinator;
1630
1712
  }
1631
1713
  };
1632
1714
 
1715
+ //#endregion
1716
+ //#region src/dev/DevSourceChangeClassifier.ts
1717
+ var DevSourceChangeClassifier = class {
1718
+ shouldRepublishConsumerOutput(args) {
1719
+ const resolvedConsumerRoot = path.resolve(args.consumerRoot);
1720
+ return args.changedPaths.some((changedPath) => this.isPathInsideDirectory(changedPath, resolvedConsumerRoot));
1721
+ }
1722
+ requiresNextHostRestart(args) {
1723
+ const configPaths = new Set(this.resolveConfigPaths(args.consumerRoot));
1724
+ return args.changedPaths.some((changedPath) => configPaths.has(path.resolve(changedPath)));
1725
+ }
1726
+ resolveConfigPaths(consumerRoot) {
1727
+ const resolvedConsumerRoot = path.resolve(consumerRoot);
1728
+ return [
1729
+ path.resolve(resolvedConsumerRoot, "codemation.config.ts"),
1730
+ path.resolve(resolvedConsumerRoot, "codemation.config.js"),
1731
+ path.resolve(resolvedConsumerRoot, "src", "codemation.config.ts"),
1732
+ path.resolve(resolvedConsumerRoot, "src", "codemation.config.js")
1733
+ ];
1734
+ }
1735
+ isPathInsideDirectory(filePath, directoryPath) {
1736
+ const resolvedFilePath = path.resolve(filePath);
1737
+ const relativePath = path.relative(directoryPath, resolvedFilePath);
1738
+ return relativePath.length === 0 || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
1739
+ }
1740
+ };
1741
+
1633
1742
  //#endregion
1634
1743
  //#region src/dev/DevSourceRestartCoordinator.ts
1635
1744
  var DevSourceRestartCoordinator = class {
@@ -1754,7 +1863,7 @@ var DevSessionServicesBuilder = class {
1754
1863
  const listenPortResolver = new ListenPortResolver();
1755
1864
  const loopbackPortAllocator = new LoopbackPortAllocator();
1756
1865
  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));
1866
+ 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 DevSourceChangeClassifier(), new DevSourceRestartCoordinator(new DevelopmentGatewayNotifier(cliLogger), this.loggerFactory.createPerformanceDiagnostics("codemation-cli.performance"), cliLogger));
1758
1867
  }
1759
1868
  };
1760
1869
 
package/dist/bin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as CliBin } from "./CliBin-900C8Din.js";
1
+ import { t as CliBin } from "./CliBin-C3ar49fj.js";
2
2
  import process from "node:process";
3
3
  import "reflect-metadata";
4
4
 
package/dist/index.d.ts CHANGED
@@ -23118,6 +23118,7 @@ declare class DevHttpProbe {
23118
23118
  * Polls until the runtime child serves bootstrap summary (after gateway is up, the disposable runtime may still be wiring).
23119
23119
  */
23120
23120
  waitUntilBootstrapSummaryReady(gatewayBaseUrl: string): Promise<void>;
23121
+ private isRedirectStatus;
23121
23122
  }
23122
23123
  //#endregion
23123
23124
  //#region src/dev/DevNextHostEnvironmentBuilder.d.ts
@@ -23125,6 +23126,18 @@ declare class DevNextHostEnvironmentBuilder {
23125
23126
  private readonly consumerEnvLoader;
23126
23127
  private readonly sourceMapNodeOptions;
23127
23128
  constructor(consumerEnvLoader: ConsumerEnvLoader, sourceMapNodeOptions: SourceMapNodeOptions);
23129
+ buildConsumerUiProxy(args: Readonly<{
23130
+ authConfigJson: string;
23131
+ authSecret: string;
23132
+ consumerRoot: string;
23133
+ developmentServerToken: string;
23134
+ nextPort: number;
23135
+ publicBaseUrl: string;
23136
+ runtimeDevUrl: string;
23137
+ skipUiAuth: boolean;
23138
+ websocketPort: number;
23139
+ consumerOutputManifestPath?: string;
23140
+ }>): NodeJS.ProcessEnv;
23128
23141
  build(args: Readonly<{
23129
23142
  authConfigJson: string;
23130
23143
  consumerRoot: string;
@@ -23172,6 +23185,20 @@ declare class DevSessionPortsResolver {
23172
23185
  }>>;
23173
23186
  }
23174
23187
  //#endregion
23188
+ //#region src/dev/DevSourceChangeClassifier.d.ts
23189
+ declare class DevSourceChangeClassifier {
23190
+ shouldRepublishConsumerOutput(args: Readonly<{
23191
+ changedPaths: ReadonlyArray<string>;
23192
+ consumerRoot: string;
23193
+ }>): boolean;
23194
+ requiresNextHostRestart(args: Readonly<{
23195
+ changedPaths: ReadonlyArray<string>;
23196
+ consumerRoot: string;
23197
+ }>): boolean;
23198
+ private resolveConfigPaths;
23199
+ private isPathInsideDirectory;
23200
+ }
23201
+ //#endregion
23175
23202
  //#region src/dev/DevelopmentGatewayNotifier.d.ts
23176
23203
  declare class DevelopmentGatewayNotifier {
23177
23204
  private readonly cliLogger;
@@ -23235,8 +23262,9 @@ declare class DevSessionServices {
23235
23262
  readonly devAuthLoader: DevAuthSettingsLoader;
23236
23263
  readonly nextHostEnvBuilder: DevNextHostEnvironmentBuilder;
23237
23264
  readonly watchRootsResolver: WatchRootsResolver;
23265
+ readonly sourceChangeClassifier: DevSourceChangeClassifier;
23238
23266
  readonly sourceRestartCoordinator: DevSourceRestartCoordinator;
23239
- constructor(consumerEnvLoader: ConsumerEnvLoader, sourceMapNodeOptions: SourceMapNodeOptions, sessionPorts: DevSessionPortsResolver, loopbackPortAllocator: LoopbackPortAllocator, devHttpProbe: DevHttpProbe, runtimeEntrypointResolver: RuntimeToolEntrypointResolver, devAuthLoader: DevAuthSettingsLoader, nextHostEnvBuilder: DevNextHostEnvironmentBuilder, watchRootsResolver: WatchRootsResolver, sourceRestartCoordinator: DevSourceRestartCoordinator);
23267
+ constructor(consumerEnvLoader: ConsumerEnvLoader, sourceMapNodeOptions: SourceMapNodeOptions, sessionPorts: DevSessionPortsResolver, loopbackPortAllocator: LoopbackPortAllocator, devHttpProbe: DevHttpProbe, runtimeEntrypointResolver: RuntimeToolEntrypointResolver, devAuthLoader: DevAuthSettingsLoader, nextHostEnvBuilder: DevNextHostEnvironmentBuilder, watchRootsResolver: WatchRootsResolver, sourceChangeClassifier: DevSourceChangeClassifier, sourceRestartCoordinator: DevSourceRestartCoordinator);
23240
23268
  }
23241
23269
  //#endregion
23242
23270
  //#region src/dev/DevLock.d.ts
@@ -23345,6 +23373,7 @@ declare class DevCommand {
23345
23373
  * Framework mode: no separate UI child (Next runs in dev later).
23346
23374
  */
23347
23375
  private startConsumerUiProxyWhenNeeded;
23376
+ private spawnConsumerUiProxy;
23348
23377
  private spawnGatewayChildAndWaitForHealth;
23349
23378
  private devDetachedChildSpawnOptions;
23350
23379
  private bindShutdownSignalsToChildProcesses;
@@ -23352,7 +23381,13 @@ declare class DevCommand {
23352
23381
  * Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
23353
23382
  */
23354
23383
  private spawnFrameworkNextHostWhenNeeded;
23384
+ private spawnFrameworkNextHost;
23355
23385
  private startWatcherForSourceRestart;
23386
+ private restartNextHostForConfigChange;
23387
+ private restartConsumerUiProxy;
23388
+ private restartFrameworkNextHost;
23389
+ private failDevSessionAfterIrrecoverableSourceError;
23390
+ private stopLiveChildProcesses;
23356
23391
  private logConsumerDevHintWhenNeeded;
23357
23392
  }
23358
23393
  //#endregion
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as CliProgram, i as CliPathResolver, n as CliProgramFactory, o as ConsumerOutputBuilder, r as CodemationCliApplicationSession, s as ConsumerBuildOptionsParser, t as CliBin } from "./CliBin-900C8Din.js";
1
+ import { a as CliProgram, i as CliPathResolver, n as CliProgramFactory, o as ConsumerOutputBuilder, r as CodemationCliApplicationSession, s as ConsumerBuildOptionsParser, t as CliBin } from "./CliBin-C3ar49fj.js";
2
2
  import { CodemationPluginDiscovery } from "@codemation/host/server";
3
3
 
4
4
  export { CliBin, CliPathResolver, CliProgram, CliProgramFactory, CodemationCliApplicationSession, CodemationPluginDiscovery, ConsumerBuildOptionsParser, ConsumerOutputBuilder };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -28,11 +28,11 @@
28
28
  "figlet": "^1.11.0",
29
29
  "reflect-metadata": "^0.2.2",
30
30
  "typescript": "^5.9.3",
31
- "@codemation/dev-gateway": "0.0.4",
32
- "@codemation/host": "0.0.4",
33
- "@codemation/next-host": "0.0.4",
34
- "@codemation/worker-cli": "0.0.4",
35
- "@codemation/runtime-dev": "0.0.4"
31
+ "@codemation/dev-gateway": "0.0.5",
32
+ "@codemation/host": "0.0.5",
33
+ "@codemation/next-host": "0.0.5",
34
+ "@codemation/runtime-dev": "0.0.5",
35
+ "@codemation/worker-cli": "0.0.5"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.3.5",
@@ -62,9 +62,9 @@ export class DevCommand {
62
62
  });
63
63
  const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
64
64
  const watcher = this.devSourceWatcherFactory.create();
65
+ const processState = this.createInitialProcessState();
65
66
  try {
66
67
  const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
67
- const processState = this.createInitialProcessState();
68
68
  const stopPromise = this.wireStopPromise(processState);
69
69
  const uiProxyBase = await this.startConsumerUiProxyWhenNeeded(prepared, processState);
70
70
  const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
@@ -75,11 +75,13 @@ export class DevCommand {
75
75
  this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
76
76
  }
77
77
  this.bindShutdownSignalsToChildProcesses(processState);
78
- this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
79
- await this.startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl);
78
+ await this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
79
+ await this.startWatcherForSourceRestart(prepared, processState, watcher, devMode, gatewayBaseUrl);
80
80
  this.logConsumerDevHintWhenNeeded(devMode, gatewayPort);
81
81
  await stopPromise;
82
82
  } finally {
83
+ processState.stopRequested = true;
84
+ await this.stopLiveChildProcesses(processState);
83
85
  await watcher.stop();
84
86
  await devLock.release();
85
87
  }
@@ -132,6 +134,8 @@ export class DevCommand {
132
134
  currentGateway: null,
133
135
  currentNextHost: null,
134
136
  currentUiNext: null,
137
+ currentUiProxyBaseUrl: null,
138
+ isRestartingNextHost: false,
135
139
  stopRequested: false,
136
140
  stopResolve: null,
137
141
  stopReject: null,
@@ -161,8 +165,19 @@ export class DevCommand {
161
165
  return "";
162
166
  }
163
167
  const websocketPort = prepared.gatewayPort;
164
- const uiPort = await this.session.loopbackPortAllocator.allocate();
165
- const uiProxyBase = `http://127.0.0.1:${uiPort}`;
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);
171
+ return uiProxyBase;
172
+ }
173
+
174
+ private async spawnConsumerUiProxy(
175
+ prepared: DevPreparedRuntime,
176
+ state: DevMutableProcessState,
177
+ authSettings: DevResolvedAuthSettings,
178
+ websocketPort: number,
179
+ uiProxyBase: string,
180
+ ): Promise<void> {
166
181
  const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
167
182
  const nextHostRoot = path.dirname(nextHostPackageJsonPath);
168
183
  const nextHostCommand = await this.nextHostConsumerServerCommandFactory.create({ nextHostRoot });
@@ -172,36 +187,33 @@ export class DevCommand {
172
187
  "output",
173
188
  "current.json",
174
189
  );
190
+ const uiPort = Number(new URL(uiProxyBase).port);
191
+ const nextHostEnvironment = this.session.nextHostEnvBuilder.buildConsumerUiProxy({
192
+ authConfigJson: authSettings.authConfigJson,
193
+ authSecret: authSettings.authSecret,
194
+ consumerRoot: prepared.paths.consumerRoot,
195
+ consumerOutputManifestPath,
196
+ developmentServerToken: prepared.developmentServerToken,
197
+ nextPort: uiPort,
198
+ publicBaseUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
199
+ runtimeDevUrl: this.gatewayBaseHttpUrl(prepared.gatewayPort),
200
+ skipUiAuth: authSettings.skipUiAuth,
201
+ websocketPort,
202
+ });
175
203
  state.currentUiNext = spawn(nextHostCommand.command, nextHostCommand.args, {
176
204
  cwd: nextHostCommand.cwd,
177
205
  ...this.devDetachedChildSpawnOptions(),
178
- env: {
179
- ...process.env,
180
- ...prepared.consumerEnv,
181
- PORT: String(uiPort),
182
- CODEMATION_AUTH_CONFIG_JSON: prepared.authSettings.authConfigJson,
183
- CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
184
- CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
185
- CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
186
- AUTH_SECRET: prepared.authSettings.authSecret,
187
- NEXTAUTH_SECRET: prepared.authSettings.authSecret,
188
- NEXT_PUBLIC_CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
189
- CODEMATION_WS_PORT: String(websocketPort),
190
- NEXT_PUBLIC_CODEMATION_WS_PORT: String(websocketPort),
191
- CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
192
- NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
193
- WS_NO_BUFFER_UTIL: "1",
194
- WS_NO_UTF_8_VALIDATE: "1",
195
- },
206
+ env: nextHostEnvironment,
196
207
  });
197
208
  state.currentUiNext.on("error", (error) => {
198
- if (!state.stopRequested) {
199
- state.stopRequested = true;
200
- state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
209
+ if (state.stopRequested || state.isRestartingNextHost) {
210
+ return;
201
211
  }
212
+ state.stopRequested = true;
213
+ state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
202
214
  });
203
215
  state.currentUiNext.on("exit", (code) => {
204
- if (state.stopRequested) {
216
+ if (state.stopRequested || state.isRestartingNextHost) {
205
217
  return;
206
218
  }
207
219
  state.stopRequested = true;
@@ -211,7 +223,6 @@ export class DevCommand {
211
223
  state.stopReject?.(new Error(`next start (consumer UI) exited unexpectedly with code ${code ?? 0}.`));
212
224
  });
213
225
  await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
214
- return uiProxyBase;
215
226
  }
216
227
 
217
228
  private async spawnGatewayChildAndWaitForHealth(
@@ -301,23 +312,32 @@ export class DevCommand {
301
312
  /**
302
313
  * Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
303
314
  */
304
- private spawnFrameworkNextHostWhenNeeded(
315
+ private async spawnFrameworkNextHostWhenNeeded(
305
316
  prepared: DevPreparedRuntime,
306
317
  state: DevMutableProcessState,
307
318
  gatewayBaseUrl: string,
308
- ): void {
319
+ ): Promise<void> {
309
320
  if (prepared.devMode !== "framework") {
310
321
  return;
311
322
  }
323
+ await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, prepared.authSettings);
324
+ }
325
+
326
+ private async spawnFrameworkNextHost(
327
+ prepared: DevPreparedRuntime,
328
+ state: DevMutableProcessState,
329
+ gatewayBaseUrl: string,
330
+ authSettings: DevResolvedAuthSettings,
331
+ ): Promise<void> {
312
332
  const websocketPort = prepared.gatewayPort;
313
333
  const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
314
334
  const nextHostRoot = path.dirname(nextHostPackageJsonPath);
315
335
  const nextHostEnvironment = this.session.nextHostEnvBuilder.build({
316
- authConfigJson: prepared.authSettings.authConfigJson,
336
+ authConfigJson: authSettings.authConfigJson,
317
337
  consumerRoot: prepared.paths.consumerRoot,
318
338
  developmentServerToken: prepared.developmentServerToken,
319
339
  nextPort: prepared.nextPort,
320
- skipUiAuth: prepared.authSettings.skipUiAuth,
340
+ skipUiAuth: authSettings.skipUiAuth,
321
341
  websocketPort,
322
342
  runtimeDevUrl: gatewayBaseUrl,
323
343
  });
@@ -328,7 +348,7 @@ export class DevCommand {
328
348
  });
329
349
  state.currentNextHost.on("exit", (code) => {
330
350
  const normalizedCode = code ?? 0;
331
- if (state.stopRequested) {
351
+ if (state.stopRequested || state.isRestartingNextHost) {
332
352
  return;
333
353
  }
334
354
  if (normalizedCode === 0) {
@@ -343,15 +363,18 @@ export class DevCommand {
343
363
  state.stopReject?.(new Error(`next host exited with code ${normalizedCode}.`));
344
364
  });
345
365
  state.currentNextHost.on("error", (error) => {
346
- if (!state.stopRequested) {
347
- state.stopRequested = true;
348
- state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
366
+ if (state.stopRequested || state.isRestartingNextHost) {
367
+ return;
349
368
  }
369
+ state.stopRequested = true;
370
+ state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
350
371
  });
372
+ await this.session.devHttpProbe.waitUntilUrlRespondsOk(`http://127.0.0.1:${prepared.nextPort}/`);
351
373
  }
352
374
 
353
375
  private async startWatcherForSourceRestart(
354
376
  prepared: DevPreparedRuntime,
377
+ state: DevMutableProcessState,
355
378
  watcher: DevSourceWatcher,
356
379
  devMode: DevMode,
357
380
  gatewayBaseUrl: string,
@@ -369,22 +392,112 @@ export class DevCommand {
369
392
  );
370
393
  return;
371
394
  }
372
- process.stdout.write("\n[codemation] Source change detected — rebuilding consumer…\n");
373
- await this.session.sourceRestartCoordinator.runHandshakeAfterSourceChange(
374
- gatewayBaseUrl,
375
- prepared.developmentServerToken,
376
- );
377
- process.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
378
- await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
379
- const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
380
- if (json) {
381
- this.devCliBannerRenderer.renderCompact(json);
395
+ try {
396
+ const shouldRepublishConsumerOutput = this.session.sourceChangeClassifier.shouldRepublishConsumerOutput({
397
+ changedPaths,
398
+ consumerRoot: prepared.paths.consumerRoot,
399
+ });
400
+ const shouldRestartNextHost = this.session.sourceChangeClassifier.requiresNextHostRestart({
401
+ changedPaths,
402
+ consumerRoot: prepared.paths.consumerRoot,
403
+ });
404
+ 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",
408
+ );
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");
426
+ } catch (error) {
427
+ await this.failDevSessionAfterIrrecoverableSourceError(state, error);
382
428
  }
383
- process.stdout.write("[codemation] Runtime ready.\n");
384
429
  },
385
430
  });
386
431
  }
387
432
 
433
+ private async restartNextHostForConfigChange(
434
+ prepared: DevPreparedRuntime,
435
+ state: DevMutableProcessState,
436
+ gatewayBaseUrl: string,
437
+ ): Promise<void> {
438
+ 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;
441
+ try {
442
+ if (prepared.devMode === "consumer") {
443
+ await this.restartConsumerUiProxy(prepared, state, refreshedAuthSettings);
444
+ return;
445
+ }
446
+ await this.restartFrameworkNextHost(prepared, state, gatewayBaseUrl, refreshedAuthSettings);
447
+ } finally {
448
+ state.isRestartingNextHost = false;
449
+ }
450
+ }
451
+
452
+ private async restartConsumerUiProxy(
453
+ prepared: DevPreparedRuntime,
454
+ state: DevMutableProcessState,
455
+ authSettings: DevResolvedAuthSettings,
456
+ ): Promise<void> {
457
+ if (state.currentUiNext && state.currentUiNext.exitCode === null && state.currentUiNext.signalCode === null) {
458
+ await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentUiNext);
459
+ }
460
+ state.currentUiNext = null;
461
+ const uiProxyBaseUrl = state.currentUiProxyBaseUrl;
462
+ if (!uiProxyBaseUrl) {
463
+ throw new Error("Consumer UI proxy base URL is missing during Next host restart.");
464
+ }
465
+ await this.spawnConsumerUiProxy(prepared, state, authSettings, prepared.gatewayPort, uiProxyBaseUrl);
466
+ }
467
+
468
+ private async restartFrameworkNextHost(
469
+ prepared: DevPreparedRuntime,
470
+ state: DevMutableProcessState,
471
+ gatewayBaseUrl: string,
472
+ authSettings: DevResolvedAuthSettings,
473
+ ): Promise<void> {
474
+ if (state.currentNextHost && state.currentNextHost.exitCode === null && state.currentNextHost.signalCode === null) {
475
+ await this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentNextHost);
476
+ }
477
+ state.currentNextHost = null;
478
+ await this.spawnFrameworkNextHost(prepared, state, gatewayBaseUrl, authSettings);
479
+ }
480
+
481
+ private async failDevSessionAfterIrrecoverableSourceError(
482
+ state: DevMutableProcessState,
483
+ error: unknown,
484
+ ): Promise<void> {
485
+ const exception = error instanceof Error ? error : new Error(String(error));
486
+ state.stopRequested = true;
487
+ await this.stopLiveChildProcesses(state);
488
+ state.stopReject?.(exception);
489
+ }
490
+
491
+ private async stopLiveChildProcesses(state: DevMutableProcessState): Promise<void> {
492
+ const children: ChildProcess[] = [];
493
+ for (const child of [state.currentUiNext, state.currentNextHost, state.currentGateway]) {
494
+ if (child && child.exitCode === null && child.signalCode === null) {
495
+ children.push(child);
496
+ }
497
+ }
498
+ await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
499
+ }
500
+
388
501
  private logConsumerDevHintWhenNeeded(devMode: DevMode, gatewayPort: number): void {
389
502
  if (devMode !== "consumer") {
390
503
  return;
@@ -11,6 +11,8 @@ export type DevMutableProcessState = {
11
11
  currentGateway: ChildProcess | null;
12
12
  currentNextHost: ChildProcess | null;
13
13
  currentUiNext: ChildProcess | null;
14
+ currentUiProxyBaseUrl: string | null;
15
+ isRestartingNextHost: boolean;
14
16
  stopRequested: boolean;
15
17
  stopResolve: (() => void) | null;
16
18
  stopReject: ((error: Error) => void) | null;
@@ -11,6 +11,7 @@ import { DevHttpProbe } from "./DevHttpProbe";
11
11
  import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
12
12
  import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
13
13
  import { DevSessionServices } from "./DevSessionServices";
14
+ import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
14
15
  import { DevSourceRestartCoordinator } from "./DevSourceRestartCoordinator";
15
16
  import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
16
17
  import { RuntimeToolEntrypointResolver } from "./RuntimeToolEntrypointResolver";
@@ -35,6 +36,7 @@ export class DevSessionServicesBuilder {
35
36
  new DevAuthSettingsLoader(new CodemationConsumerConfigLoader()),
36
37
  new DevNextHostEnvironmentBuilder(consumerEnvLoader, sourceMapNodeOptions),
37
38
  new WatchRootsResolver(),
39
+ new DevSourceChangeClassifier(),
38
40
  new DevSourceRestartCoordinator(
39
41
  new DevelopmentGatewayNotifier(cliLogger),
40
42
  this.loggerFactory.createPerformanceDiagnostics("codemation-cli.performance"),
@@ -4,8 +4,8 @@ export class DevHttpProbe {
4
4
  async waitUntilUrlRespondsOk(url: string): Promise<void> {
5
5
  for (let attempt = 0; attempt < 200; attempt += 1) {
6
6
  try {
7
- const response = await fetch(url);
8
- if (response.ok || response.status === 404) {
7
+ const response = await fetch(url, { redirect: "manual" });
8
+ if (response.ok || response.status === 404 || this.isRedirectStatus(response.status)) {
9
9
  return;
10
10
  }
11
11
  } catch {
@@ -51,4 +51,8 @@ export class DevHttpProbe {
51
51
  }
52
52
  throw new Error("Timed out waiting for dev runtime bootstrap summary.");
53
53
  }
54
+
55
+ private isRedirectStatus(status: number): boolean {
56
+ return status >= 300 && status < 400;
57
+ }
54
58
  }
@@ -10,6 +10,38 @@ export class DevNextHostEnvironmentBuilder {
10
10
  private readonly sourceMapNodeOptions: SourceMapNodeOptions,
11
11
  ) {}
12
12
 
13
+ buildConsumerUiProxy(
14
+ args: Readonly<{
15
+ authConfigJson: string;
16
+ authSecret: string;
17
+ consumerRoot: string;
18
+ developmentServerToken: string;
19
+ nextPort: number;
20
+ publicBaseUrl: string;
21
+ runtimeDevUrl: string;
22
+ skipUiAuth: boolean;
23
+ websocketPort: number;
24
+ consumerOutputManifestPath?: string;
25
+ }>,
26
+ ): NodeJS.ProcessEnv {
27
+ return {
28
+ ...this.build({
29
+ authConfigJson: args.authConfigJson,
30
+ consumerRoot: args.consumerRoot,
31
+ developmentServerToken: args.developmentServerToken,
32
+ nextPort: args.nextPort,
33
+ runtimeDevUrl: args.runtimeDevUrl,
34
+ skipUiAuth: args.skipUiAuth,
35
+ websocketPort: args.websocketPort,
36
+ consumerOutputManifestPath: args.consumerOutputManifestPath,
37
+ }),
38
+ AUTH_SECRET: args.authSecret,
39
+ AUTH_URL: args.publicBaseUrl,
40
+ NEXTAUTH_SECRET: args.authSecret,
41
+ NEXTAUTH_URL: args.publicBaseUrl,
42
+ };
43
+ }
44
+
13
45
  build(
14
46
  args: Readonly<{
15
47
  authConfigJson: string;
@@ -5,6 +5,7 @@ import { DevAuthSettingsLoader } from "./DevAuthSettingsLoader";
5
5
  import { DevHttpProbe } from "./DevHttpProbe";
6
6
  import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
7
7
  import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
8
+ import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
8
9
  import { DevSourceRestartCoordinator } from "./DevSourceRestartCoordinator";
9
10
  import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
10
11
  import { RuntimeToolEntrypointResolver } from "./RuntimeToolEntrypointResolver";
@@ -24,6 +25,7 @@ export class DevSessionServices {
24
25
  readonly devAuthLoader: DevAuthSettingsLoader,
25
26
  readonly nextHostEnvBuilder: DevNextHostEnvironmentBuilder,
26
27
  readonly watchRootsResolver: WatchRootsResolver,
28
+ readonly sourceChangeClassifier: DevSourceChangeClassifier,
27
29
  readonly sourceRestartCoordinator: DevSourceRestartCoordinator,
28
30
  ) {}
29
31
  }
@@ -0,0 +1,39 @@
1
+ import path from "node:path";
2
+
3
+ export class DevSourceChangeClassifier {
4
+ shouldRepublishConsumerOutput(
5
+ args: Readonly<{
6
+ changedPaths: ReadonlyArray<string>;
7
+ consumerRoot: string;
8
+ }>,
9
+ ): boolean {
10
+ const resolvedConsumerRoot = path.resolve(args.consumerRoot);
11
+ return args.changedPaths.some((changedPath) => this.isPathInsideDirectory(changedPath, resolvedConsumerRoot));
12
+ }
13
+
14
+ requiresNextHostRestart(
15
+ args: Readonly<{
16
+ changedPaths: ReadonlyArray<string>;
17
+ consumerRoot: string;
18
+ }>,
19
+ ): boolean {
20
+ const configPaths = new Set(this.resolveConfigPaths(args.consumerRoot));
21
+ return args.changedPaths.some((changedPath) => configPaths.has(path.resolve(changedPath)));
22
+ }
23
+
24
+ private resolveConfigPaths(consumerRoot: string): ReadonlyArray<string> {
25
+ const resolvedConsumerRoot = path.resolve(consumerRoot);
26
+ return [
27
+ path.resolve(resolvedConsumerRoot, "codemation.config.ts"),
28
+ path.resolve(resolvedConsumerRoot, "codemation.config.js"),
29
+ path.resolve(resolvedConsumerRoot, "src", "codemation.config.ts"),
30
+ path.resolve(resolvedConsumerRoot, "src", "codemation.config.js"),
31
+ ];
32
+ }
33
+
34
+ private isPathInsideDirectory(filePath: string, directoryPath: string): boolean {
35
+ const resolvedFilePath = path.resolve(filePath);
36
+ const relativePath = path.relative(directoryPath, resolvedFilePath);
37
+ return relativePath.length === 0 || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
38
+ }
39
+ }