@codemation/cli 0.0.15 → 0.0.18

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.
@@ -0,0 +1,19 @@
1
+ import type { DevCommand } from "./DevCommand";
2
+ import type { PluginDevConfigFactory } from "../dev/PluginDevConfigFactory";
3
+
4
+ export class DevPluginCommand {
5
+ constructor(
6
+ private readonly pluginDevConfigFactory: PluginDevConfigFactory,
7
+ private readonly devCommand: DevCommand,
8
+ ) {}
9
+
10
+ async execute(args: Readonly<{ pluginRoot: string; watchFramework?: boolean }>): Promise<void> {
11
+ const pluginConfig = await this.pluginDevConfigFactory.prepare(args.pluginRoot);
12
+ await this.devCommand.execute({
13
+ commandName: "dev:plugin",
14
+ configPathOverride: pluginConfig.configPath,
15
+ consumerRoot: args.pluginRoot,
16
+ watchFramework: args.watchFramework,
17
+ });
18
+ }
19
+ }
@@ -1,18 +1,11 @@
1
- import {
2
- CodemationConsumerConfigLoader,
3
- CodemationFrontendAuthSnapshotFactory,
4
- CodemationPluginDiscovery,
5
- FrontendAppConfigJsonCodec,
6
- } from "@codemation/host/server";
1
+ import { CodemationConsumerConfigLoader } from "@codemation/host/server";
7
2
  import { spawn } from "node:child_process";
8
3
  import { createRequire } from "node:module";
9
4
  import path from "node:path";
10
5
  import process from "node:process";
11
6
 
12
- import { ConsumerBuildArtifactsPublisher } from "../build/ConsumerBuildArtifactsPublisher";
13
7
  import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
14
8
  import type { ConsumerBuildOptions } from "../consumer/consumerBuildOptions.types";
15
- import { ConsumerOutputBuilderLoader } from "../consumer/Loader";
16
9
  import { CliPathResolver } from "../path/CliPathResolver";
17
10
  import { ListenPortResolver } from "../runtime/ListenPortResolver";
18
11
  import { NextHostConsumerServerCommandFactory } from "../runtime/NextHostConsumerServerCommandFactory";
@@ -25,40 +18,21 @@ export class ServeWebCommand {
25
18
  constructor(
26
19
  private readonly pathResolver: CliPathResolver,
27
20
  private readonly configLoader: CodemationConsumerConfigLoader,
28
- private readonly pluginDiscovery: CodemationPluginDiscovery,
29
- private readonly artifactsPublisher: ConsumerBuildArtifactsPublisher,
30
21
  private readonly tsRuntime: TypeScriptRuntimeConfigurator,
31
22
  private readonly sourceMapNodeOptions: SourceMapNodeOptions,
32
- private readonly outputBuilderLoader: ConsumerOutputBuilderLoader,
33
23
  private readonly envLoader: ConsumerEnvLoader,
34
24
  private readonly listenPortResolver: ListenPortResolver,
35
25
  private readonly nextHostConsumerServerCommandFactory: NextHostConsumerServerCommandFactory,
36
- private readonly frontendAuthSnapshotFactory: CodemationFrontendAuthSnapshotFactory,
37
- private readonly frontendAppConfigJsonCodec: FrontendAppConfigJsonCodec,
38
26
  ) {}
39
27
 
40
28
  async execute(consumerRoot: string, buildOptions: ConsumerBuildOptions): Promise<void> {
29
+ void buildOptions;
41
30
  const paths = await this.pathResolver.resolve(consumerRoot);
42
31
  this.tsRuntime.configure(paths.repoRoot);
43
- const builder = this.outputBuilderLoader.create(paths.consumerRoot, buildOptions);
44
- const snapshot = await builder.ensureBuilt();
45
- const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
46
- const manifest = await this.artifactsPublisher.publish(snapshot, discoveredPlugins);
47
32
  const nextHostRoot = path.dirname(this.require.resolve("@codemation/next-host/package.json"));
48
33
  const nextHostCommand = await this.nextHostConsumerServerCommandFactory.create({ nextHostRoot });
49
34
  const consumerEnv = this.envLoader.load(paths.consumerRoot);
50
35
  const configResolution = await this.configLoader.load({ consumerRoot: paths.consumerRoot });
51
- const frontendAuthSnapshot = this.frontendAuthSnapshotFactory.createFromResolvedInputs({
52
- authConfig: configResolution.config.auth,
53
- env: {
54
- ...process.env,
55
- ...consumerEnv,
56
- },
57
- uiAuthEnabled: !(
58
- consumerEnv.NODE_ENV !== "production" &&
59
- configResolution.config.auth?.allowUnauthenticatedInDevelopment === true
60
- ),
61
- });
62
36
  const nextPort = this.listenPortResolver.resolvePrimaryApplicationPort(process.env.PORT);
63
37
  const websocketPort = this.listenPortResolver.resolveWebsocketPortRelativeToHttp({
64
38
  nextPort,
@@ -72,13 +46,13 @@ export class ServeWebCommand {
72
46
  ...process.env,
73
47
  ...consumerEnv,
74
48
  PORT: String(nextPort),
75
- CODEMATION_FRONTEND_APP_CONFIG_JSON: this.frontendAppConfigJsonCodec.serialize({
76
- auth: frontendAuthSnapshot,
77
- productName: "Codemation",
78
- logoUrl: null,
79
- }),
80
- CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifest.manifestPath,
81
49
  CODEMATION_CONSUMER_ROOT: paths.consumerRoot,
50
+ CODEMATION_UI_AUTH_ENABLED: String(
51
+ !(
52
+ consumerEnv.NODE_ENV !== "production" &&
53
+ configResolution.config.auth?.allowUnauthenticatedInDevelopment === true
54
+ ),
55
+ ),
82
56
  CODEMATION_WS_PORT: String(websocketPort),
83
57
  NEXT_PUBLIC_CODEMATION_WS_PORT: String(websocketPort),
84
58
  NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
@@ -1,7 +1,7 @@
1
1
  import type { ChildProcess } from "node:child_process";
2
2
 
3
- import type { DevResolvedAuthSettings } from "../dev/DevAuthSettingsLoader";
4
3
  import type { DevApiRuntimeServerHandle } from "../dev/DevApiRuntimeFactory";
4
+ import type { NextHostEdgeSeed } from "../dev/NextHostEdgeSeedLoader";
5
5
  import type { CliPaths } from "../path/CliPathResolver";
6
6
 
7
7
  export type DevMode = "packaged-ui" | "watch-framework";
@@ -21,10 +21,11 @@ export type DevMutableProcessState = {
21
21
  /** Immutable inputs resolved before any child processes are spawned. */
22
22
  export type DevPreparedRuntime = Readonly<{
23
23
  paths: CliPaths;
24
+ configPathOverride?: string;
24
25
  devMode: DevMode;
25
26
  nextPort: number;
26
27
  gatewayPort: number;
27
- authSettings: DevResolvedAuthSettings;
28
+ authSettings: NextHostEdgeSeed;
28
29
  developmentServerToken: string;
29
30
  consumerEnv: Readonly<Record<string, string>>;
30
31
  }>;
@@ -63,6 +63,7 @@ export class ConsumerOutputBuilder {
63
63
  private readonly consumerRoot: string,
64
64
  logOverride?: Logger,
65
65
  buildOptionsOverride?: ConsumerBuildOptions,
66
+ private readonly configPathOverride?: string,
66
67
  ) {
67
68
  this.log = logOverride ?? defaultConsumerOutputLogger;
68
69
  this.buildOptions = buildOptionsOverride ?? defaultConsumerBuildOptions;
@@ -374,6 +375,7 @@ export class ConsumerOutputBuilder {
374
375
  sourcePath,
375
376
  });
376
377
  }
378
+ await this.emitConfigSourceFile(outputAppRoot, configSourcePath, runtimeSourcePaths);
377
379
  },
378
380
  });
379
381
  }
@@ -463,6 +465,24 @@ export class ConsumerOutputBuilder {
463
465
  }
464
466
  }
465
467
 
468
+ private async emitConfigSourceFile(
469
+ outputAppRoot: string,
470
+ configSourcePath: string,
471
+ runtimeSourcePaths: ReadonlyArray<string>,
472
+ ): Promise<void> {
473
+ const normalizedConfigSourcePath = path.resolve(configSourcePath);
474
+ const alreadyEmitted = runtimeSourcePaths.some(
475
+ (sourcePath) => path.resolve(sourcePath) === normalizedConfigSourcePath,
476
+ );
477
+ if (alreadyEmitted) {
478
+ return;
479
+ }
480
+ await this.emitSourceFile({
481
+ outputAppRoot,
482
+ sourcePath: normalizedConfigSourcePath,
483
+ });
484
+ }
485
+
466
486
  private createCompilerOptions(): ts.CompilerOptions {
467
487
  const scriptTarget = this.buildOptions.target === "es2020" ? ts.ScriptTarget.ES2020 : ts.ScriptTarget.ES2022;
468
488
  return {
@@ -697,6 +717,14 @@ export class ConsumerOutputBuilder {
697
717
  }
698
718
 
699
719
  private async resolveConfigPath(consumerRoot: string): Promise<string | null> {
720
+ const configuredOverride = this.configPathOverride?.trim();
721
+ if (configuredOverride && configuredOverride.length > 0) {
722
+ const resolvedOverride = path.resolve(configuredOverride);
723
+ if (await this.fileExists(resolvedOverride)) {
724
+ return resolvedOverride;
725
+ }
726
+ throw new Error(`Codemation config override not found at ${resolvedOverride}.`);
727
+ }
700
728
  for (const candidate of this.getConventionCandidates(consumerRoot)) {
701
729
  if (await this.fileExists(candidate)) {
702
730
  return candidate;
@@ -0,0 +1,17 @@
1
+ import type { Logger } from "@codemation/host/next/server";
2
+
3
+ import { ConsumerOutputBuilder } from "./ConsumerOutputBuilder";
4
+ import type { ConsumerBuildOptions } from "./consumerBuildOptions.types";
5
+
6
+ export class ConsumerOutputBuilderFactory {
7
+ create(
8
+ consumerRoot: string,
9
+ args?: Readonly<{
10
+ buildOptions?: ConsumerBuildOptions;
11
+ configPathOverride?: string;
12
+ logger?: Logger;
13
+ }>,
14
+ ): ConsumerOutputBuilder {
15
+ return new ConsumerOutputBuilder(consumerRoot, args?.logger, args?.buildOptions, args?.configPathOverride);
16
+ }
17
+ }
@@ -4,13 +4,13 @@ import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
4
4
  import { ListenPortResolver } from "../runtime/ListenPortResolver";
5
5
  import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
6
6
 
7
- import { DevAuthSettingsLoader } from "./DevAuthSettingsLoader";
8
7
  import { DevHttpProbe } from "./DevHttpProbe";
9
8
  import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
10
9
  import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
11
10
  import { DevSessionServices } from "./DevSessionServices";
12
11
  import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
13
12
  import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
13
+ import { NextHostEdgeSeedLoader } from "./NextHostEdgeSeedLoader";
14
14
  import { WatchRootsResolver } from "./WatchRootsResolver";
15
15
 
16
16
  export class DevSessionServicesBuilder {
@@ -25,7 +25,7 @@ export class DevSessionServicesBuilder {
25
25
  new DevSessionPortsResolver(listenPortResolver, loopbackPortAllocator),
26
26
  loopbackPortAllocator,
27
27
  new DevHttpProbe(),
28
- new DevAuthSettingsLoader(new CodemationConsumerConfigLoader(), consumerEnvLoader),
28
+ new NextHostEdgeSeedLoader(new CodemationConsumerConfigLoader(), consumerEnvLoader),
29
29
  new DevNextHostEnvironmentBuilder(consumerEnvLoader, sourceMapNodeOptions),
30
30
  new WatchRootsResolver(),
31
31
  new DevSourceChangeClassifier(),
@@ -231,6 +231,19 @@ export class CliDevProxyServer {
231
231
  }
232
232
  }
233
233
 
234
+ private extractOccupyingPids(listenerDescription: string): ReadonlyArray<number> {
235
+ const seen = new Set<number>();
236
+ const re = /pid=(\d+)/g;
237
+ let match: RegExpExecArray | null;
238
+ while ((match = re.exec(listenerDescription)) !== null) {
239
+ const pid = Number.parseInt(match[1] ?? "0", 10);
240
+ if (Number.isFinite(pid) && pid > 0) {
241
+ seen.add(pid);
242
+ }
243
+ }
244
+ return [...seen];
245
+ }
246
+
234
247
  private async rejectListenError(error: unknown, reject: (reason?: unknown) => void): Promise<void> {
235
248
  const errorWithCode = error as Error & Readonly<{ code?: unknown }>;
236
249
  if (errorWithCode.code !== "EADDRINUSE") {
@@ -239,6 +252,13 @@ export class CliDevProxyServer {
239
252
  }
240
253
 
241
254
  const description = await this.listenPortConflictDescriber.describeLoopbackPort(this.listenPort);
255
+ const occupyingPids = description !== null ? this.extractOccupyingPids(description) : [];
256
+ if (occupyingPids.length > 0) {
257
+ const pidList = occupyingPids.join(", ");
258
+ process.stderr.write(
259
+ `[codemation] Dev gateway port ${this.listenPort} is already in use (occupying pid(s): ${pidList}).\n`,
260
+ );
261
+ }
242
262
  const baseMessage = `Dev gateway port ${this.listenPort} is already in use on 127.0.0.1.`;
243
263
  const suffix =
244
264
  description === null
@@ -21,6 +21,7 @@ export class DevApiRuntimeFactory {
21
21
  httpPort,
22
22
  workflowWebSocketPort,
23
23
  new DevApiRuntimeHost(this.configLoader, this.pluginDiscovery, {
24
+ configPathOverride: args.configPathOverride,
24
25
  consumerRoot: args.consumerRoot,
25
26
  env: {
26
27
  ...args.env,
@@ -1,4 +1,5 @@
1
- import type { CodemationPlugin } from "@codemation/host";
1
+ import type { AppConfig, AppPluginLoadSummary, CodemationPlugin } from "@codemation/host";
2
+ import { CodemationPluginPackageMetadata } from "@codemation/host";
2
3
  import {
3
4
  AppContainerFactory,
4
5
  AppContainerLifecycle,
@@ -18,13 +19,15 @@ import { CodemationTsyringeTypeInfoRegistrar } from "@codemation/host-src/presen
18
19
  import type { DevApiRuntimeContext } from "./DevApiRuntimeTypes";
19
20
 
20
21
  export class DevApiRuntimeHost {
21
- private readonly pluginListMerger = new CodemationPluginListMerger();
22
+ private readonly pluginPackageMetadata = new CodemationPluginPackageMetadata();
23
+ private readonly pluginListMerger = new CodemationPluginListMerger(this.pluginPackageMetadata);
22
24
  private contextPromise: Promise<DevApiRuntimeContext> | null = null;
23
25
 
24
26
  constructor(
25
27
  private readonly configLoader: AppConfigLoader,
26
28
  private readonly pluginDiscovery: CodemationPluginDiscovery,
27
29
  private readonly args: Readonly<{
30
+ configPathOverride?: string;
28
31
  consumerRoot: string;
29
32
  env: NodeJS.ProcessEnv;
30
33
  runtimeWorkingDirectory: string;
@@ -62,17 +65,24 @@ export class DevApiRuntimeHost {
62
65
  env.CODEMATION_CONSUMER_ROOT = consumerRoot;
63
66
  const configResolution = await this.configLoader.load({
64
67
  consumerRoot,
68
+ configPathOverride: this.args.configPathOverride,
65
69
  repoRoot,
66
70
  env,
67
71
  });
68
72
  const discoveredPlugins = await this.loadDiscoveredPlugins(consumerRoot);
73
+ const mergedPlugins =
74
+ discoveredPlugins.length > 0
75
+ ? this.pluginListMerger.merge(configResolution.appConfig.plugins, discoveredPlugins)
76
+ : configResolution.appConfig.plugins;
69
77
  const appConfig = {
70
78
  ...configResolution.appConfig,
71
79
  env,
72
- plugins:
73
- discoveredPlugins.length > 0
74
- ? this.pluginListMerger.merge(configResolution.appConfig.plugins, discoveredPlugins)
75
- : configResolution.appConfig.plugins,
80
+ plugins: mergedPlugins,
81
+ pluginLoadSummary: this.createPluginLoadSummary(
82
+ configResolution.appConfig.plugins,
83
+ discoveredPlugins,
84
+ mergedPlugins,
85
+ ),
76
86
  };
77
87
  const container = await new AppContainerFactory().create({
78
88
  appConfig,
@@ -96,6 +106,27 @@ export class DevApiRuntimeHost {
96
106
  return resolvedPackages.map((resolvedPackage: CodemationResolvedPluginPackage) => resolvedPackage.plugin);
97
107
  }
98
108
 
109
+ private createPluginLoadSummary(
110
+ configuredPlugins: ReadonlyArray<CodemationPlugin>,
111
+ discoveredPlugins: ReadonlyArray<CodemationPlugin>,
112
+ mergedPlugins: ReadonlyArray<CodemationPlugin>,
113
+ ): AppConfig["pluginLoadSummary"] {
114
+ const configuredPluginSet = new Set(configuredPlugins);
115
+ const discoveredPluginSet = new Set(discoveredPlugins);
116
+ const summaries: AppPluginLoadSummary[] = [];
117
+ for (const plugin of mergedPlugins) {
118
+ const packageName = this.pluginPackageMetadata.readPackageName(plugin);
119
+ if (!packageName) {
120
+ continue;
121
+ }
122
+ summaries.push({
123
+ packageName,
124
+ source: configuredPluginSet.has(plugin) || !discoveredPluginSet.has(plugin) ? "configured" : "discovered",
125
+ });
126
+ }
127
+ return summaries;
128
+ }
129
+
99
130
  private async detectWorkspaceRoot(startDirectory: string): Promise<string> {
100
131
  let currentDirectory = path.resolve(startDirectory);
101
132
  while (true) {
@@ -10,6 +10,7 @@ export type DevApiRuntimeContext = Readonly<{
10
10
  }>;
11
11
 
12
12
  export type DevApiRuntimeFactoryArgs = Readonly<{
13
+ configPathOverride?: string;
13
14
  consumerRoot: string;
14
15
  env: NodeJS.ProcessEnv;
15
16
  runtimeWorkingDirectory: string;
@@ -39,8 +39,9 @@ export class DevCliBannerRenderer {
39
39
  title: chalk.bold("Runtime"),
40
40
  titleAlignment: "center",
41
41
  });
42
+ const pluginsSection = this.buildPluginsSection(summary);
42
43
  const activeSection = this.buildActiveWorkflowsSection(summary);
43
- process.stdout.write(`${detailBox}\n${activeSection}\n`);
44
+ process.stdout.write(`${detailBox}\n${pluginsSection}\n${activeSection}\n`);
44
45
  }
45
46
 
46
47
  renderFull(summary: DevBootstrapSummaryJson): void {
@@ -62,8 +63,9 @@ export class DevCliBannerRenderer {
62
63
  title: chalk.bold("Runtime (updated)"),
63
64
  titleAlignment: "center",
64
65
  });
66
+ const pluginsSection = this.buildPluginsSection(summary);
65
67
  const activeSection = this.buildActiveWorkflowsSection(summary);
66
- process.stdout.write(`\n${detailBox}\n${activeSection}\n`);
68
+ process.stdout.write(`\n${detailBox}\n${pluginsSection}\n${activeSection}\n`);
67
69
  }
68
70
 
69
71
  private renderFigletTitle(): string {
@@ -103,4 +105,21 @@ export class DevCliBannerRenderer {
103
105
  titleAlignment: "left",
104
106
  });
105
107
  }
108
+
109
+ private buildPluginsSection(summary: DevBootstrapSummaryJson): string {
110
+ const lines =
111
+ summary.plugins.length === 0
112
+ ? [chalk.dim(" (none discovered or configured)")]
113
+ : summary.plugins.map(
114
+ (plugin) => `${chalk.whiteBright(` • ${plugin.packageName} `)}${chalk.dim(`(${plugin.source})`)}`,
115
+ );
116
+ return boxen(lines.join("\n"), {
117
+ padding: { top: 0, bottom: 0, left: 0, right: 0 },
118
+ margin: { top: 1, bottom: 0 },
119
+ borderStyle: "single",
120
+ borderColor: "cyan",
121
+ title: chalk.bold("Plugins"),
122
+ titleAlignment: "left",
123
+ });
124
+ }
106
125
  }
@@ -1,7 +1,5 @@
1
1
  import path from "node:path";
2
2
  import process from "node:process";
3
- import type { CodemationAuthConfig } from "@codemation/host";
4
- import { CodemationFrontendAuthSnapshotFactory, FrontendAppConfigJsonCodec } from "@codemation/host";
5
3
 
6
4
  import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
7
5
  import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
@@ -10,14 +8,13 @@ export class DevNextHostEnvironmentBuilder {
10
8
  constructor(
11
9
  private readonly consumerEnvLoader: ConsumerEnvLoader,
12
10
  private readonly sourceMapNodeOptions: SourceMapNodeOptions,
13
- private readonly frontendAuthSnapshotFactory: CodemationFrontendAuthSnapshotFactory = new CodemationFrontendAuthSnapshotFactory(),
14
- private readonly frontendAppConfigJsonCodec: FrontendAppConfigJsonCodec = new FrontendAppConfigJsonCodec(),
15
11
  ) {}
16
12
 
17
13
  buildConsumerUiProxy(
18
14
  args: Readonly<{
19
- authConfigJson: string;
20
15
  authSecret: string;
16
+ configPathOverride?: string;
17
+ consumerOutputManifestPath?: string;
21
18
  consumerRoot: string;
22
19
  developmentServerToken: string;
23
20
  nextPort: number;
@@ -25,69 +22,54 @@ export class DevNextHostEnvironmentBuilder {
25
22
  runtimeDevUrl: string;
26
23
  skipUiAuth: boolean;
27
24
  websocketPort: number;
28
- consumerOutputManifestPath?: string;
29
25
  }>,
30
26
  ): NodeJS.ProcessEnv {
27
+ const publicWebsocketPort = this.resolvePublicWebsocketPort(args.publicBaseUrl, args.websocketPort);
31
28
  return {
32
29
  ...this.build({
33
- authConfigJson: args.authConfigJson,
34
30
  authSecret: args.authSecret,
31
+ configPathOverride: args.configPathOverride,
32
+ consumerOutputManifestPath: args.consumerOutputManifestPath,
35
33
  consumerRoot: args.consumerRoot,
36
34
  developmentServerToken: args.developmentServerToken,
37
35
  nextPort: args.nextPort,
38
36
  runtimeDevUrl: args.runtimeDevUrl,
39
37
  skipUiAuth: args.skipUiAuth,
40
38
  websocketPort: args.websocketPort,
41
- consumerOutputManifestPath: args.consumerOutputManifestPath,
42
39
  }),
43
40
  // Standalone `server.js` uses `process.env.HOSTNAME || '0.0.0.0'` for bind; Docker sets HOSTNAME to the
44
41
  // container id, which breaks loopback health checks — force IPv4 loopback for the UI child only.
45
42
  HOSTNAME: "127.0.0.1",
46
43
  AUTH_SECRET: args.authSecret,
47
44
  AUTH_URL: args.publicBaseUrl,
45
+ CODEMATION_PUBLIC_WS_PORT: String(publicWebsocketPort),
46
+ NEXT_PUBLIC_CODEMATION_WS_PORT: String(publicWebsocketPort),
48
47
  };
49
48
  }
50
49
 
51
50
  build(
52
51
  args: Readonly<{
53
- authConfigJson: string;
54
52
  authSecret?: string;
53
+ configPathOverride?: string;
54
+ consumerOutputManifestPath?: string;
55
55
  consumerRoot: string;
56
56
  developmentServerToken: string;
57
57
  nextPort: number;
58
58
  skipUiAuth: boolean;
59
59
  websocketPort: number;
60
60
  runtimeDevUrl?: string;
61
- /** Same manifest as `codemation build` / serve-web so @codemation/next-host can load consumer config (whitelabel, etc.). */
62
- consumerOutputManifestPath?: string;
63
61
  }>,
64
62
  ): NodeJS.ProcessEnv {
65
63
  const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process.env);
66
- const manifestPath =
64
+ const consumerOutputManifestPath =
67
65
  args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
68
- const authSecret = args.authSecret ?? merged.AUTH_SECRET;
69
- const authSnapshot = this.frontendAuthSnapshotFactory.createFromResolvedInputs({
70
- authConfig: this.parseAuthConfig(args.authConfigJson),
71
- env: {
72
- ...merged,
73
- ...(typeof authSecret === "string" && authSecret.trim().length > 0
74
- ? {
75
- AUTH_SECRET: authSecret,
76
- }
77
- : {}),
78
- },
79
- uiAuthEnabled: !args.skipUiAuth,
80
- });
81
66
  return {
82
67
  ...merged,
83
68
  PORT: String(args.nextPort),
84
69
  CODEMATION_CONSUMER_ROOT: args.consumerRoot,
85
- CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifestPath,
86
- CODEMATION_FRONTEND_APP_CONFIG_JSON: this.frontendAppConfigJsonCodec.serialize({
87
- auth: authSnapshot,
88
- productName: "Codemation",
89
- logoUrl: null,
90
- }),
70
+ CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
71
+ CODEMATION_UI_AUTH_ENABLED: String(!args.skipUiAuth),
72
+ CODEMATION_PUBLIC_WS_PORT: String(args.websocketPort),
91
73
  CODEMATION_WS_PORT: String(args.websocketPort),
92
74
  NEXT_PUBLIC_CODEMATION_WS_PORT: String(args.websocketPort),
93
75
  CODEMATION_DEV_SERVER_TOKEN: args.developmentServerToken,
@@ -95,17 +77,26 @@ export class DevNextHostEnvironmentBuilder {
95
77
  NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
96
78
  WS_NO_BUFFER_UTIL: "1",
97
79
  WS_NO_UTF_8_VALIDATE: "1",
80
+ ...(args.authSecret && args.authSecret.trim().length > 0 ? { AUTH_SECRET: args.authSecret.trim() } : {}),
81
+ ...(args.configPathOverride && args.configPathOverride.trim().length > 0
82
+ ? { CODEMATION_CONFIG_PATH: args.configPathOverride }
83
+ : {}),
98
84
  ...(args.runtimeDevUrl !== undefined && args.runtimeDevUrl.trim().length > 0
99
85
  ? { CODEMATION_RUNTIME_DEV_URL: args.runtimeDevUrl.trim() }
100
86
  : {}),
101
87
  };
102
88
  }
103
89
 
104
- private parseAuthConfig(authConfigJson: string): CodemationAuthConfig | undefined {
105
- if (authConfigJson.trim().length === 0) {
106
- return undefined;
90
+ private resolvePublicWebsocketPort(publicBaseUrl: string, fallbackPort: number): number {
91
+ try {
92
+ const parsedUrl = new URL(publicBaseUrl);
93
+ const parsedPort = Number(parsedUrl.port);
94
+ if (Number.isInteger(parsedPort) && parsedPort > 0) {
95
+ return parsedPort;
96
+ }
97
+ } catch {
98
+ // Fall back to the runtime websocket port when the public URL is malformed.
107
99
  }
108
- const parsed = JSON.parse(authConfigJson) as CodemationAuthConfig | null;
109
- return parsed ?? undefined;
100
+ return fallbackPort;
110
101
  }
111
102
  }
@@ -1,6 +1,6 @@
1
1
  export type DevRebuildRequest = Readonly<{
2
2
  changedPaths: ReadonlyArray<string>;
3
- shouldRepublishConsumerOutput: boolean;
3
+ configPathOverride?: string;
4
4
  shouldRestartUi: boolean;
5
5
  }>;
6
6
 
@@ -47,7 +47,7 @@ export class DevRebuildQueue {
47
47
  }
48
48
  return {
49
49
  changedPaths: [...new Set([...current.changedPaths, ...next.changedPaths])],
50
- shouldRepublishConsumerOutput: current.shouldRepublishConsumerOutput || next.shouldRepublishConsumerOutput,
50
+ configPathOverride: next.configPathOverride ?? current.configPathOverride,
51
51
  shouldRestartUi: current.shouldRestartUi || next.shouldRestartUi,
52
52
  };
53
53
  }
@@ -1,12 +1,12 @@
1
1
  import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
2
2
  import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
3
3
 
4
- import { DevAuthSettingsLoader } from "./DevAuthSettingsLoader";
5
4
  import { DevHttpProbe } from "./DevHttpProbe";
6
5
  import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
7
6
  import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
8
7
  import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
9
8
  import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
9
+ import { NextHostEdgeSeedLoader } from "./NextHostEdgeSeedLoader";
10
10
  import { WatchRootsResolver } from "./WatchRootsResolver";
11
11
 
12
12
  /**
@@ -19,7 +19,7 @@ export class DevSessionServices {
19
19
  readonly sessionPorts: DevSessionPortsResolver,
20
20
  readonly loopbackPortAllocator: LoopbackPortAllocator,
21
21
  readonly devHttpProbe: DevHttpProbe,
22
- readonly devAuthLoader: DevAuthSettingsLoader,
22
+ readonly nextHostEdgeSeedLoader: NextHostEdgeSeedLoader,
23
23
  readonly nextHostEnvBuilder: DevNextHostEnvironmentBuilder,
24
24
  readonly watchRootsResolver: WatchRootsResolver,
25
25
  readonly sourceChangeClassifier: DevSourceChangeClassifier,
@@ -1,21 +1,16 @@
1
1
  import path from "node:path";
2
2
 
3
3
  export class DevSourceChangeClassifier {
4
- private static readonly configFileNames = new Set([
4
+ private static readonly uiConfigFileNames = new Set([
5
5
  "codemation.config.ts",
6
6
  "codemation.config.js",
7
7
  "codemation.config.mjs",
8
8
  ]);
9
-
10
- shouldRepublishConsumerOutput(
11
- args: Readonly<{
12
- changedPaths: ReadonlyArray<string>;
13
- consumerRoot: string;
14
- }>,
15
- ): boolean {
16
- const resolvedConsumerRoot = path.resolve(args.consumerRoot);
17
- return args.changedPaths.some((changedPath) => this.isPathInsideDirectory(changedPath, resolvedConsumerRoot));
18
- }
9
+ private static readonly pluginConfigFileNames = new Set([
10
+ "codemation.plugin.ts",
11
+ "codemation.plugin.js",
12
+ "codemation.plugin.mjs",
13
+ ]);
19
14
 
20
15
  requiresUiRestart(
21
16
  args: Readonly<{
@@ -40,10 +35,15 @@ export class DevSourceChangeClassifier {
40
35
  return false;
41
36
  }
42
37
  const relativePath = path.relative(consumerRoot, resolvedPath);
43
- if (DevSourceChangeClassifier.configFileNames.has(path.basename(relativePath))) {
38
+ const fileName = path.basename(relativePath);
39
+ if (DevSourceChangeClassifier.uiConfigFileNames.has(fileName)) {
44
40
  // Config changes affect auth and branding projections consumed by the packaged Next host.
45
41
  return true;
46
42
  }
43
+ if (DevSourceChangeClassifier.pluginConfigFileNames.has(fileName)) {
44
+ // Plugin sandbox configs also define workflows, so keep them on the cheap runtime-only reload path.
45
+ return false;
46
+ }
47
47
  if (relativePath.startsWith(path.join("src", "workflows"))) {
48
48
  return false;
49
49
  }