@codemation/cli 0.0.16 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # @codemation/cli
2
+
3
+ ## 0.0.19
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`405c854`](https://github.com/MadeRelevant/codemation/commit/405c8541961f41dcba653f352691a821b0470ca0)]:
8
+ - @codemation/host@0.0.19
9
+ - @codemation/next-host@0.0.19
10
+
11
+ ## 0.0.18
12
+
13
+ ### Patch Changes
14
+
15
+ - f0c6878: Introduce Changesets, a single CI status check for branch protection, and the Codemation pre-stable license across published packages.
16
+ - Updated dependencies [f0c6878]
17
+ - @codemation/host@0.0.18
18
+ - @codemation/next-host@0.0.18
package/LICENSE ADDED
@@ -0,0 +1,37 @@
1
+ Codemation Pre-Stable License
2
+
3
+ Copyright (c) Made Relevant B.V. All rights reserved.
4
+
5
+ 1. Definitions
6
+
7
+ "Software" means the Codemation source code, documentation, and artifacts in this repository and any published npm packages in the Codemation monorepo.
8
+
9
+ "Stable Version" means the first published release of the package `@codemation/core` on the public npm registry with version 1.0.0 or higher.
10
+
11
+ 2. Permitted use (before Stable Version)
12
+
13
+ Until a Stable Version exists, you may use, copy, modify, and distribute the Software only for non-commercial purposes, including personal learning, research, evaluation, and internal use within your organization that does not charge third parties for access to the Software or a product or service whose primary value is the Software.
14
+
15
+ 3. Restrictions (before Stable Version)
16
+
17
+ Until a Stable Version exists, you must not:
18
+
19
+ a) Sell, rent, lease, or sublicense the Software or a derivative work for a fee;
20
+
21
+ b) Offer the Software or a derivative work as part of a paid product or service (including hosting, support, or consulting) where the Software is a material part of the offering;
22
+
23
+ c) Use the Software or a derivative work primarily to generate revenue or commercial advantage for you or others.
24
+
25
+ These restrictions apply to all versions published before a Stable Version, even if a later Stable Version is released under different terms.
26
+
27
+ 4. After Stable Version
28
+
29
+ The maintainers may publish a Stable Version under different license terms. If they do, those terms apply only to that Stable Version and subsequent releases they designate; they do not automatically apply to earlier pre-stable versions.
30
+
31
+ 5. No warranty
32
+
33
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34
+
35
+ 6. Third-party components
36
+
37
+ The Software may include third-party components under their own licenses. Those licenses govern those components.
@@ -3,15 +3,15 @@ import process$1 from "node:process";
3
3
  import { AppConfigLoader, CodemationConsumerConfigLoader, CodemationPluginDiscovery, WorkflowDiscoveryPathSegmentsComputer, WorkflowModulePathFinder } from "@codemation/host/server";
4
4
  import { ApiPaths, AppContainerFactory, AppContainerLifecycle, ApplicationTokens, CodemationPluginPackageMetadata, DatabaseMigrations, ListUserAccountsQuery, PrismaClient, UpsertLocalBootstrapUserCommand, WorkerRuntime } from "@codemation/host";
5
5
  import { AppContainerFactory as AppContainerFactory$1, AppContainerLifecycle as AppContainerLifecycle$1, CodemationHonoApiApp, CodemationPluginListMerger, FrontendRuntime, ServerLoggerFactory, logLevelPolicyFactory } from "@codemation/host/next/server";
6
- import { execFile, spawn } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
7
+ import { access, copyFile, cp, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
8
  import path from "node:path";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { execFile, spawn } from "node:child_process";
8
11
  import { existsSync, readFileSync } from "node:fs";
9
12
  import { config, parse } from "dotenv";
10
13
  import { watch } from "chokidar";
11
- import { randomUUID } from "node:crypto";
12
- import { access, copyFile, cp, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
13
14
  import ts from "typescript";
14
- import { fileURLToPath } from "node:url";
15
15
  import { PrismaMigrationDeployer } from "@codemation/host/persistence";
16
16
  import boxen from "boxen";
17
17
  import chalk from "chalk";
@@ -41,20 +41,74 @@ var ConsumerBuildOptionsParser = class {
41
41
  }
42
42
  };
43
43
 
44
+ //#endregion
45
+ //#region src/build/ConsumerBuildArtifactsPublisher.ts
46
+ var ConsumerBuildArtifactsPublisher = class {
47
+ async publish(snapshot, discoveredPlugins) {
48
+ const pluginEntryPath = await this.writeDiscoveredPluginsOutput(snapshot, discoveredPlugins);
49
+ return await this.writeBuildManifest(snapshot, pluginEntryPath);
50
+ }
51
+ async writeDiscoveredPluginsOutput(snapshot, discoveredPlugins) {
52
+ const outputPath = path.resolve(snapshot.emitOutputRoot, "plugins.js");
53
+ await mkdir(path.dirname(outputPath), { recursive: true });
54
+ const outputLines = ["const codemationDiscoveredPlugins = [];", ""];
55
+ discoveredPlugins.forEach((discoveredPlugin, index) => {
56
+ const pluginFileUrl = pathToFileURL(path.resolve(discoveredPlugin.packageRoot, this.resolvePluginEntry(discoveredPlugin))).href;
57
+ outputLines.push(`const pluginModule${index} = await import(${JSON.stringify(pluginFileUrl)});`);
58
+ outputLines.push(`const pluginValue${index} = pluginModule${index}.default ?? pluginModule${index}.codemationPlugin;`);
59
+ outputLines.push(`if (pluginValue${index} && typeof pluginValue${index}.register === "function") {`);
60
+ outputLines.push(` codemationDiscoveredPlugins.push(pluginValue${index});`);
61
+ outputLines.push(`} else if (typeof pluginValue${index} === "function" && pluginValue${index}.prototype && typeof pluginValue${index}.prototype.register === "function") {`);
62
+ outputLines.push(` codemationDiscoveredPlugins.push(new pluginValue${index}());`);
63
+ outputLines.push("}");
64
+ outputLines.push("");
65
+ });
66
+ outputLines.push("export { codemationDiscoveredPlugins };");
67
+ outputLines.push("export default codemationDiscoveredPlugins;");
68
+ outputLines.push("");
69
+ await writeFile(outputPath, outputLines.join("\n"), "utf8");
70
+ return outputPath;
71
+ }
72
+ async writeBuildManifest(snapshot, pluginEntryPath) {
73
+ const manifest = {
74
+ buildVersion: snapshot.buildVersion,
75
+ consumerRoot: snapshot.consumerRoot,
76
+ entryPath: snapshot.outputEntryPath,
77
+ manifestPath: snapshot.manifestPath,
78
+ pluginEntryPath,
79
+ workflowSourcePaths: snapshot.workflowSourcePaths
80
+ };
81
+ await mkdir(path.dirname(snapshot.manifestPath), { recursive: true });
82
+ const temporaryManifestPath = `${snapshot.manifestPath}.${snapshot.buildVersion}.${randomUUID()}.tmp`;
83
+ await writeFile(temporaryManifestPath, JSON.stringify(manifest, null, 2), "utf8");
84
+ await rename(temporaryManifestPath, snapshot.manifestPath);
85
+ return manifest;
86
+ }
87
+ resolvePluginEntry(discoveredPlugin) {
88
+ if (typeof discoveredPlugin.developmentEntry === "string" && discoveredPlugin.developmentEntry.trim().length > 0) return discoveredPlugin.developmentEntry;
89
+ return discoveredPlugin.pluginEntry;
90
+ }
91
+ };
92
+
44
93
  //#endregion
45
94
  //#region src/commands/BuildCommand.ts
46
95
  var BuildCommand = class {
47
- constructor(cliLogger, pathResolver, consumerOutputBuilderFactory, tsRuntime) {
96
+ constructor(cliLogger, pathResolver, consumerOutputBuilderFactory, pluginDiscovery, consumerBuildArtifactsPublisher, tsRuntime) {
48
97
  this.cliLogger = cliLogger;
49
98
  this.pathResolver = pathResolver;
50
99
  this.consumerOutputBuilderFactory = consumerOutputBuilderFactory;
100
+ this.pluginDiscovery = pluginDiscovery;
101
+ this.consumerBuildArtifactsPublisher = consumerBuildArtifactsPublisher;
51
102
  this.tsRuntime = tsRuntime;
52
103
  }
53
104
  async execute(consumerRoot, buildOptions) {
54
105
  const paths = await this.pathResolver.resolve(consumerRoot);
55
106
  this.tsRuntime.configure(paths.repoRoot);
56
107
  const snapshot = await this.consumerOutputBuilderFactory.create(paths.consumerRoot, { buildOptions }).ensureBuilt();
108
+ const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
109
+ const manifest = await this.consumerBuildArtifactsPublisher.publish(snapshot, discoveredPlugins);
57
110
  this.cliLogger.info(`Built consumer output: ${snapshot.outputEntryPath}`);
111
+ this.cliLogger.info(`Build manifest: ${manifest.manifestPath}`);
58
112
  this.cliLogger.info(`Workflow modules emitted: ${snapshot.workflowSourcePaths.length}`);
59
113
  }
60
114
  };
@@ -74,7 +128,7 @@ var DbMigrateCommand = class {
74
128
  //#region src/commands/DevCommand.ts
75
129
  var DevCommand = class {
76
130
  require = createRequire(import.meta.url);
77
- constructor(pathResolver, tsRuntime, devLockFactory, devSourceWatcherFactory, cliLogger, session, databaseMigrationsApplyService, devBootstrapSummaryFetcher, devCliBannerRenderer, consumerEnvDotenvFilePredicate, devTrackedProcessTreeKiller, nextHostConsumerServerCommandFactory, devApiRuntimeFactory, cliDevProxyServerFactory, devRebuildQueueFactory) {
131
+ constructor(pathResolver, tsRuntime, devLockFactory, devSourceWatcherFactory, cliLogger, session, databaseMigrationsApplyService, consumerOutputBuilderFactory, pluginDiscovery, consumerBuildArtifactsPublisher, devBootstrapSummaryFetcher, devCliBannerRenderer, consumerEnvDotenvFilePredicate, devTrackedProcessTreeKiller, nextHostConsumerServerCommandFactory, devApiRuntimeFactory, cliDevProxyServerFactory, devRebuildQueueFactory) {
78
132
  this.pathResolver = pathResolver;
79
133
  this.tsRuntime = tsRuntime;
80
134
  this.devLockFactory = devLockFactory;
@@ -82,6 +136,9 @@ var DevCommand = class {
82
136
  this.cliLogger = cliLogger;
83
137
  this.session = session;
84
138
  this.databaseMigrationsApplyService = databaseMigrationsApplyService;
139
+ this.consumerOutputBuilderFactory = consumerOutputBuilderFactory;
140
+ this.pluginDiscovery = pluginDiscovery;
141
+ this.consumerBuildArtifactsPublisher = consumerBuildArtifactsPublisher;
85
142
  this.devBootstrapSummaryFetcher = devBootstrapSummaryFetcher;
86
143
  this.devCliBannerRenderer = devCliBannerRenderer;
87
144
  this.consumerEnvDotenvFilePredicate = consumerEnvDotenvFilePredicate;
@@ -94,6 +151,7 @@ var DevCommand = class {
94
151
  async execute(args) {
95
152
  const paths = await this.pathResolver.resolve(args.consumerRoot);
96
153
  const commandName = args.commandName ?? "dev";
154
+ const previousDevelopmentServerToken = process$1.env.CODEMATION_DEV_SERVER_TOKEN;
97
155
  this.devCliBannerRenderer.renderBrandHeader();
98
156
  this.tsRuntime.configure(paths.repoRoot);
99
157
  await this.databaseMigrationsApplyService.applyForConsumer(paths.consumerRoot, { configPath: args.configPathOverride });
@@ -114,6 +172,8 @@ var DevCommand = class {
114
172
  let proxyServer = null;
115
173
  try {
116
174
  const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings, args.configPathOverride);
175
+ if (prepared.devMode === "packaged-ui") await this.publishConsumerArtifacts(prepared.paths, prepared.configPathOverride);
176
+ process$1.env.CODEMATION_DEV_SERVER_TOKEN = prepared.developmentServerToken;
117
177
  const stopPromise = this.wireStopPromise(processState);
118
178
  const uiProxyBase = await this.preparePackagedUiBaseUrlWhenNeeded(prepared, processState);
119
179
  proxyServer = await this.startProxyServer(prepared.gatewayPort, uiProxyBase);
@@ -132,6 +192,8 @@ var DevCommand = class {
132
192
  this.logPackagedUiDevHintWhenNeeded(devMode, gatewayPort, commandName);
133
193
  await stopPromise;
134
194
  } finally {
195
+ if (previousDevelopmentServerToken === void 0) delete process$1.env.CODEMATION_DEV_SERVER_TOKEN;
196
+ else process$1.env.CODEMATION_DEV_SERVER_TOKEN = previousDevelopmentServerToken;
135
197
  processState.stopRequested = true;
136
198
  await this.stopLiveProcesses(processState, proxyServer);
137
199
  await watcher.stop();
@@ -345,6 +407,7 @@ var DevCommand = class {
345
407
  proxyServer.setBuildStatus("building");
346
408
  proxyServer.broadcastBuildStarted();
347
409
  try {
410
+ if (prepared.devMode === "packaged-ui") await this.publishConsumerArtifacts(prepared.paths, request.configPathOverride);
348
411
  await this.stopCurrentRuntime(state, proxyServer);
349
412
  process$1.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
350
413
  const runtime = await this.createRuntime(prepared);
@@ -353,11 +416,11 @@ var DevCommand = class {
353
416
  httpPort: runtime.httpPort,
354
417
  workflowWebSocketPort: runtime.workflowWebSocketPort
355
418
  });
356
- if (request.shouldRestartUi) await this.restartUiAfterSourceChange(prepared, state, gatewayBaseUrl);
357
419
  await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
358
420
  const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
359
421
  if (json) this.devCliBannerRenderer.renderCompact(json);
360
422
  proxyServer.setBuildStatus("idle");
423
+ if (request.shouldRestartUi) await this.restartUiAfterSourceChange(prepared, state, gatewayBaseUrl);
361
424
  proxyServer.broadcastBuildCompleted(runtime.buildVersion);
362
425
  process$1.stdout.write("[codemation] Runtime ready.\n");
363
426
  } catch (error) {
@@ -427,6 +490,12 @@ var DevCommand = class {
427
490
  }
428
491
  });
429
492
  }
493
+ async publishConsumerArtifacts(paths, configPathOverride) {
494
+ const snapshot = await this.consumerOutputBuilderFactory.create(paths.consumerRoot, { configPathOverride }).ensureBuilt();
495
+ const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
496
+ await this.consumerBuildArtifactsPublisher.publish(snapshot, discoveredPlugins);
497
+ this.cliLogger.debug(`Dev: consumer output published (${snapshot.buildVersion}).`);
498
+ }
430
499
  logPackagedUiDevHintWhenNeeded(devMode, gatewayPort, commandName) {
431
500
  if (devMode !== "packaged-ui") return;
432
501
  this.cliLogger.info(`codemation ${commandName}: open http://127.0.0.1:${gatewayPort} — this uses the packaged @codemation/next-host UI. Use \`codemation ${commandName} --watch-framework\` only when working on the framework UI itself.`);
@@ -712,8 +781,9 @@ var ConsumerOutputBuilder = class ConsumerOutputBuilder {
712
781
  lastIssuedBuildVersion = 0;
713
782
  log;
714
783
  buildOptions;
715
- constructor(consumerRoot, logOverride, buildOptionsOverride) {
784
+ constructor(consumerRoot, logOverride, buildOptionsOverride, configPathOverride) {
716
785
  this.consumerRoot = consumerRoot;
786
+ this.configPathOverride = configPathOverride;
717
787
  this.log = logOverride ?? defaultConsumerOutputLogger;
718
788
  this.buildOptions = buildOptionsOverride ?? defaultConsumerBuildOptions;
719
789
  }
@@ -1132,6 +1202,12 @@ var ConsumerOutputBuilder = class ConsumerOutputBuilder {
1132
1202
  return `${nextBuildVersion}-${process$1.pid}`;
1133
1203
  }
1134
1204
  async resolveConfigPath(consumerRoot) {
1205
+ const configuredOverride = this.configPathOverride?.trim();
1206
+ if (configuredOverride && configuredOverride.length > 0) {
1207
+ const resolvedOverride = path.resolve(configuredOverride);
1208
+ if (await this.fileExists(resolvedOverride)) return resolvedOverride;
1209
+ throw new Error(`Codemation config override not found at ${resolvedOverride}.`);
1210
+ }
1135
1211
  for (const candidate of this.getConventionCandidates(consumerRoot)) if (await this.fileExists(candidate)) return candidate;
1136
1212
  return null;
1137
1213
  }
@@ -1248,7 +1324,7 @@ var ConsumerOutputBuilder = class ConsumerOutputBuilder {
1248
1324
  //#region src/consumer/ConsumerOutputBuilderFactory.ts
1249
1325
  var ConsumerOutputBuilderFactory = class {
1250
1326
  create(consumerRoot, args) {
1251
- return new ConsumerOutputBuilder(consumerRoot, args?.logger, args?.buildOptions);
1327
+ return new ConsumerOutputBuilder(consumerRoot, args?.logger, args?.buildOptions, args?.configPathOverride);
1252
1328
  }
1253
1329
  };
1254
1330
 
@@ -1699,12 +1775,27 @@ var CliDevProxyServer = class {
1699
1775
  return url.split("?")[0] ?? url;
1700
1776
  }
1701
1777
  }
1778
+ extractOccupyingPids(listenerDescription) {
1779
+ const seen = /* @__PURE__ */ new Set();
1780
+ const re = /pid=(\d+)/g;
1781
+ let match;
1782
+ while ((match = re.exec(listenerDescription)) !== null) {
1783
+ const pid = Number.parseInt(match[1] ?? "0", 10);
1784
+ if (Number.isFinite(pid) && pid > 0) seen.add(pid);
1785
+ }
1786
+ return [...seen];
1787
+ }
1702
1788
  async rejectListenError(error, reject) {
1703
1789
  if (error.code !== "EADDRINUSE") {
1704
1790
  reject(error);
1705
1791
  return;
1706
1792
  }
1707
1793
  const description = await this.listenPortConflictDescriber.describeLoopbackPort(this.listenPort);
1794
+ const occupyingPids = description !== null ? this.extractOccupyingPids(description) : [];
1795
+ if (occupyingPids.length > 0) {
1796
+ const pidList = occupyingPids.join(", ");
1797
+ process.stderr.write(`[codemation] Dev gateway port ${this.listenPort} is already in use (occupying pid(s): ${pidList}).\n`);
1798
+ }
1708
1799
  const baseMessage = `Dev gateway port ${this.listenPort} is already in use on 127.0.0.1.`;
1709
1800
  const suffix = description === null ? " Stop the process using that port or change the configured Codemation dev port." : ` Listener: ${description}. Stop that process or change the configured Codemation dev port.`;
1710
1801
  reject(new Error(`${baseMessage}${suffix}`, { cause: error instanceof Error ? error : void 0 }));
@@ -1882,12 +1973,19 @@ var ListenPortConflictDescriber = class {
1882
1973
  async describeLoopbackPort(port) {
1883
1974
  if (!Number.isInteger(port) || port <= 0) return null;
1884
1975
  if (this.platform !== "linux" && this.platform !== "darwin") return null;
1885
- const raw = await this.readLsofOutput(port);
1886
- if (raw === null) return null;
1887
- const occupants = this.parseLsofOutput(raw);
1976
+ const occupants = await this.resolveLoopbackOccupants(port);
1888
1977
  if (occupants.length === 0) return null;
1889
1978
  return occupants.map((occupant) => `pid=${occupant.pid} command=${occupant.command} endpoint=${occupant.endpoint}`).join("; ");
1890
1979
  }
1980
+ async resolveLoopbackOccupants(port) {
1981
+ const lsofRaw = await this.readLsofOutput(port);
1982
+ const fromLsof = lsofRaw !== null ? this.parseLsofOutput(lsofRaw) : [];
1983
+ if (fromLsof.length > 0) return fromLsof;
1984
+ if (this.platform !== "linux") return [];
1985
+ const ssRaw = await this.readSsOutput(port);
1986
+ if (ssRaw === null) return [];
1987
+ return this.parseSsListenOutput(ssRaw, port);
1988
+ }
1891
1989
  async readLsofOutput(port) {
1892
1990
  try {
1893
1991
  return await new Promise((resolve, reject) => {
@@ -1933,6 +2031,44 @@ var ListenPortConflictDescriber = class {
1933
2031
  }
1934
2032
  return occupants;
1935
2033
  }
2034
+ async readSsOutput(port) {
2035
+ const filtered = await this.execFileStdout("ss", ["-lntp", `sport = :${port}`]);
2036
+ if (filtered !== null && filtered.trim().length > 0) return filtered;
2037
+ return this.execFileStdout("ss", ["-lntp"]);
2038
+ }
2039
+ async execFileStdout(command, args) {
2040
+ try {
2041
+ return await new Promise((resolve, reject) => {
2042
+ execFile(command, [...args], (error, stdout) => {
2043
+ if (error) {
2044
+ reject(error);
2045
+ return;
2046
+ }
2047
+ resolve(stdout);
2048
+ });
2049
+ });
2050
+ } catch {
2051
+ return null;
2052
+ }
2053
+ }
2054
+ parseSsListenOutput(raw, port) {
2055
+ const occupants = [];
2056
+ const portSuffix = `:${port}`;
2057
+ for (const line of raw.split("\n")) {
2058
+ if (!line.includes("LISTEN") || !line.includes(portSuffix)) continue;
2059
+ const pidMatch = line.match(/pid=(\d+)/);
2060
+ if (!pidMatch) continue;
2061
+ const pid = Number.parseInt(pidMatch[1] ?? "0", 10);
2062
+ const command = line.match(/users:\(\("([^"]*)"/)?.[1] ?? "unknown";
2063
+ const endpoint = line.match(/\s+(\S+:\d+|\[[^\]]+\]:\d+)\s+/)?.[1] ?? `tcp:${String(port)}`;
2064
+ occupants.push({
2065
+ pid,
2066
+ command,
2067
+ endpoint
2068
+ });
2069
+ }
2070
+ return occupants;
2071
+ }
1936
2072
  };
1937
2073
 
1938
2074
  //#endregion
@@ -2387,10 +2523,12 @@ var DevNextHostEnvironmentBuilder = class {
2387
2523
  this.sourceMapNodeOptions = sourceMapNodeOptions;
2388
2524
  }
2389
2525
  buildConsumerUiProxy(args) {
2526
+ const publicWebsocketPort = this.resolvePublicWebsocketPort(args.publicBaseUrl, args.websocketPort);
2390
2527
  return {
2391
2528
  ...this.build({
2392
2529
  authSecret: args.authSecret,
2393
2530
  configPathOverride: args.configPathOverride,
2531
+ consumerOutputManifestPath: args.consumerOutputManifestPath,
2394
2532
  consumerRoot: args.consumerRoot,
2395
2533
  developmentServerToken: args.developmentServerToken,
2396
2534
  nextPort: args.nextPort,
@@ -2400,15 +2538,21 @@ var DevNextHostEnvironmentBuilder = class {
2400
2538
  }),
2401
2539
  HOSTNAME: "127.0.0.1",
2402
2540
  AUTH_SECRET: args.authSecret,
2403
- AUTH_URL: args.publicBaseUrl
2541
+ AUTH_URL: args.publicBaseUrl,
2542
+ CODEMATION_PUBLIC_WS_PORT: String(publicWebsocketPort),
2543
+ NEXT_PUBLIC_CODEMATION_WS_PORT: String(publicWebsocketPort)
2404
2544
  };
2405
2545
  }
2406
2546
  build(args) {
2547
+ const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process$1.env);
2548
+ const consumerOutputManifestPath = args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
2407
2549
  return {
2408
- ...this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process$1.env),
2550
+ ...merged,
2409
2551
  PORT: String(args.nextPort),
2410
2552
  CODEMATION_CONSUMER_ROOT: args.consumerRoot,
2553
+ CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
2411
2554
  CODEMATION_UI_AUTH_ENABLED: String(!args.skipUiAuth),
2555
+ CODEMATION_PUBLIC_WS_PORT: String(args.websocketPort),
2412
2556
  CODEMATION_WS_PORT: String(args.websocketPort),
2413
2557
  NEXT_PUBLIC_CODEMATION_WS_PORT: String(args.websocketPort),
2414
2558
  CODEMATION_DEV_SERVER_TOKEN: args.developmentServerToken,
@@ -2421,6 +2565,14 @@ var DevNextHostEnvironmentBuilder = class {
2421
2565
  ...args.runtimeDevUrl !== void 0 && args.runtimeDevUrl.trim().length > 0 ? { CODEMATION_RUNTIME_DEV_URL: args.runtimeDevUrl.trim() } : {}
2422
2566
  };
2423
2567
  }
2568
+ resolvePublicWebsocketPort(publicBaseUrl, fallbackPort) {
2569
+ try {
2570
+ const parsedUrl = new URL(publicBaseUrl);
2571
+ const parsedPort = Number(parsedUrl.port);
2572
+ if (Number.isInteger(parsedPort) && parsedPort > 0) return parsedPort;
2573
+ } catch {}
2574
+ return fallbackPort;
2575
+ }
2424
2576
  };
2425
2577
 
2426
2578
  //#endregion
@@ -3180,8 +3332,10 @@ var CliProgramFactory = class {
3180
3332
  const userAdminCliOptionsParser = new UserAdminCliOptionsParser();
3181
3333
  const databaseMigrationsApplyService = new DatabaseMigrationsApplyService(cliLogger, new UserAdminConsumerDotenvLoader(), tsconfigPreparation, new CodemationConsumerConfigLoader(), new ConsumerDatabaseConnectionResolver(), new CliDatabaseUrlDescriptor(), hostPackageRoot, new PrismaMigrationDeployer());
3182
3334
  const buildOptionsParser = new ConsumerBuildOptionsParser();
3183
- const devCommand = new DevCommand(pathResolver, tsRuntime, new DevLockFactory(), new DevSourceWatcherFactory(), cliLogger, devSessionServices, databaseMigrationsApplyService, new DevBootstrapSummaryFetcher(), new DevCliBannerRenderer(), new ConsumerEnvDotenvFilePredicate(), new DevTrackedProcessTreeKiller(), nextHostConsumerServerCommandFactory, new DevApiRuntimeFactory(devSessionServices.loopbackPortAllocator, appConfigLoader, pluginDiscovery), new CliDevProxyServerFactory(), new DevRebuildQueueFactory());
3184
- return new CliProgram(buildOptionsParser, new BuildCommand(cliLogger, pathResolver, new ConsumerOutputBuilderFactory(), tsRuntime), devCommand, new DevPluginCommand(new PluginDevConfigFactory(), devCommand), new ServeWebCommand(pathResolver, new CodemationConsumerConfigLoader(), tsRuntime, sourceMapNodeOptions, new ConsumerEnvLoader(), new ListenPortResolver(), nextHostConsumerServerCommandFactory), new ServeWorkerCommand(pathResolver, appConfigLoader, new AppContainerFactory()), new DbMigrateCommand(databaseMigrationsApplyService), new UserCreateCommand(new LocalUserCreator(userAdminBootstrap), userAdminCliOptionsParser), new UserListCommand(cliLogger, userAdminBootstrap, new CliDatabaseUrlDescriptor(), userAdminCliOptionsParser));
3335
+ const consumerOutputBuilderFactory = new ConsumerOutputBuilderFactory();
3336
+ const consumerBuildArtifactsPublisher = new ConsumerBuildArtifactsPublisher();
3337
+ const devCommand = new DevCommand(pathResolver, tsRuntime, new DevLockFactory(), new DevSourceWatcherFactory(), cliLogger, devSessionServices, databaseMigrationsApplyService, consumerOutputBuilderFactory, pluginDiscovery, consumerBuildArtifactsPublisher, new DevBootstrapSummaryFetcher(), new DevCliBannerRenderer(), new ConsumerEnvDotenvFilePredicate(), new DevTrackedProcessTreeKiller(), nextHostConsumerServerCommandFactory, new DevApiRuntimeFactory(devSessionServices.loopbackPortAllocator, appConfigLoader, pluginDiscovery), new CliDevProxyServerFactory(), new DevRebuildQueueFactory());
3338
+ return new CliProgram(buildOptionsParser, new BuildCommand(cliLogger, pathResolver, consumerOutputBuilderFactory, pluginDiscovery, consumerBuildArtifactsPublisher, tsRuntime), devCommand, new DevPluginCommand(new PluginDevConfigFactory(), devCommand), new ServeWebCommand(pathResolver, new CodemationConsumerConfigLoader(), tsRuntime, sourceMapNodeOptions, new ConsumerEnvLoader(), new ListenPortResolver(), nextHostConsumerServerCommandFactory), new ServeWorkerCommand(pathResolver, appConfigLoader, new AppContainerFactory()), new DbMigrateCommand(databaseMigrationsApplyService), new UserCreateCommand(new LocalUserCreator(userAdminBootstrap), userAdminCliOptionsParser), new UserListCommand(cliLogger, userAdminBootstrap, new CliDatabaseUrlDescriptor(), userAdminCliOptionsParser));
3185
3339
  }
3186
3340
  };
3187
3341
 
package/dist/bin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as CliBin } from "./CliBin-C31h2o8w.js";
1
+ import { t as CliBin } from "./CliBin-BYHuUedo.js";
2
2
  import process from "node:process";
3
3
  import "reflect-metadata";
4
4
 
package/dist/index.d.ts CHANGED
@@ -223,6 +223,58 @@ interface PersistedRunState {
223
223
  connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>;
224
224
  }
225
225
  //#endregion
226
+ //#region ../core/src/events/runEvents.d.ts
227
+ type RunEvent = Readonly<{
228
+ kind: "runCreated";
229
+ runId: RunId;
230
+ workflowId: WorkflowId;
231
+ parent?: ParentExecutionRef;
232
+ at: string;
233
+ }> | Readonly<{
234
+ kind: "runSaved";
235
+ runId: RunId;
236
+ workflowId: WorkflowId;
237
+ parent?: ParentExecutionRef;
238
+ at: string;
239
+ state: PersistedRunState;
240
+ }> | Readonly<{
241
+ kind: "nodeQueued";
242
+ runId: RunId;
243
+ workflowId: WorkflowId;
244
+ parent?: ParentExecutionRef;
245
+ at: string;
246
+ snapshot: NodeExecutionSnapshot;
247
+ }> | Readonly<{
248
+ kind: "nodeStarted";
249
+ runId: RunId;
250
+ workflowId: WorkflowId;
251
+ parent?: ParentExecutionRef;
252
+ at: string;
253
+ snapshot: NodeExecutionSnapshot;
254
+ }> | Readonly<{
255
+ kind: "nodeCompleted";
256
+ runId: RunId;
257
+ workflowId: WorkflowId;
258
+ parent?: ParentExecutionRef;
259
+ at: string;
260
+ snapshot: NodeExecutionSnapshot;
261
+ }> | Readonly<{
262
+ kind: "nodeFailed";
263
+ runId: RunId;
264
+ workflowId: WorkflowId;
265
+ parent?: ParentExecutionRef;
266
+ at: string;
267
+ snapshot: NodeExecutionSnapshot;
268
+ }>;
269
+ //#endregion
270
+ //#region ../core/src/policies/executionLimits/EngineExecutionLimitsPolicy.d.ts
271
+ interface EngineExecutionLimitsPolicyConfig {
272
+ readonly defaultMaxNodeActivations: number;
273
+ readonly hardMaxNodeActivations: number;
274
+ readonly defaultMaxSubworkflowDepth: number;
275
+ readonly hardMaxSubworkflowDepth: number;
276
+ }
277
+ //#endregion
226
278
  //#region ../core/src/contracts/runtimeTypes.d.ts
227
279
  interface NodeExecutionStatePublisher {
228
280
  markQueued(args: {
@@ -288,10 +340,10 @@ interface ExecutionContext {
288
340
  binary: ExecutionBinaryService;
289
341
  getCredential<TSession = unknown>(slotKey: string): Promise<TSession>;
290
342
  }
291
- interface NodeExecutionContext<TConfig extends NodeConfigBase = NodeConfigBase> extends ExecutionContext {
343
+ interface NodeExecutionContext<TConfig$1 extends NodeConfigBase = NodeConfigBase> extends ExecutionContext {
292
344
  nodeId: NodeId;
293
345
  activationId: NodeActivationId;
294
- config: TConfig;
346
+ config: TConfig$1;
295
347
  binary: NodeBinaryAttachmentService;
296
348
  }
297
349
  //#endregion
@@ -460,15 +512,15 @@ interface WorkflowErrorContext {
460
512
  readonly finishedAt: string;
461
513
  }
462
514
  type WorkflowErrorHandlerSpec = TypeToken<WorkflowErrorHandler> | WorkflowErrorHandler;
463
- interface NodeErrorHandlerArgs<TConfig extends NodeConfigBase = NodeConfigBase> {
515
+ interface NodeErrorHandlerArgs<TConfig$1 extends NodeConfigBase = NodeConfigBase> {
464
516
  readonly kind: "single" | "multi";
465
517
  readonly items: Items;
466
518
  readonly inputsByPort: Readonly<Record<InputPortKey, Items>> | undefined;
467
- readonly ctx: NodeExecutionContext<TConfig>;
519
+ readonly ctx: NodeExecutionContext<TConfig$1>;
468
520
  readonly error: Error;
469
521
  }
470
522
  interface NodeErrorHandler {
471
- handle<TConfig extends NodeConfigBase>(args: NodeErrorHandlerArgs<TConfig>): Promise<NodeOutputs>;
523
+ handle<TConfig$1 extends NodeConfigBase>(args: NodeErrorHandlerArgs<TConfig$1>): Promise<NodeOutputs>;
472
524
  }
473
525
  type NodeErrorHandlerSpec = TypeToken<NodeErrorHandler> | NodeErrorHandler;
474
526
  //#endregion
@@ -588,58 +640,6 @@ type CredentialType<TPublicConfig extends CredentialJsonRecord = CredentialJsonR
588
640
  */
589
641
  type AnyCredentialType = CredentialType<any, any, unknown>;
590
642
  //#endregion
591
- //#region ../core/src/events/runEvents.d.ts
592
- type RunEvent = Readonly<{
593
- kind: "runCreated";
594
- runId: RunId;
595
- workflowId: WorkflowId;
596
- parent?: ParentExecutionRef;
597
- at: string;
598
- }> | Readonly<{
599
- kind: "runSaved";
600
- runId: RunId;
601
- workflowId: WorkflowId;
602
- parent?: ParentExecutionRef;
603
- at: string;
604
- state: PersistedRunState;
605
- }> | Readonly<{
606
- kind: "nodeQueued";
607
- runId: RunId;
608
- workflowId: WorkflowId;
609
- parent?: ParentExecutionRef;
610
- at: string;
611
- snapshot: NodeExecutionSnapshot;
612
- }> | Readonly<{
613
- kind: "nodeStarted";
614
- runId: RunId;
615
- workflowId: WorkflowId;
616
- parent?: ParentExecutionRef;
617
- at: string;
618
- snapshot: NodeExecutionSnapshot;
619
- }> | Readonly<{
620
- kind: "nodeCompleted";
621
- runId: RunId;
622
- workflowId: WorkflowId;
623
- parent?: ParentExecutionRef;
624
- at: string;
625
- snapshot: NodeExecutionSnapshot;
626
- }> | Readonly<{
627
- kind: "nodeFailed";
628
- runId: RunId;
629
- workflowId: WorkflowId;
630
- parent?: ParentExecutionRef;
631
- at: string;
632
- snapshot: NodeExecutionSnapshot;
633
- }>;
634
- //#endregion
635
- //#region ../core/src/policies/executionLimits/EngineExecutionLimitsPolicy.d.ts
636
- interface EngineExecutionLimitsPolicyConfig {
637
- readonly defaultMaxNodeActivations: number;
638
- readonly hardMaxNodeActivations: number;
639
- readonly defaultMaxSubworkflowDepth: number;
640
- readonly hardMaxSubworkflowDepth: number;
641
- }
642
- //#endregion
643
643
  //#region ../host/src/application/logging/Logger.d.ts
644
644
  interface Logger {
645
645
  info(message: string, exception?: Error): void;
@@ -832,6 +832,7 @@ interface CodemationPluginContext extends CodemationRegistrationContextBase {
832
832
  readonly loggerFactory: LoggerFactory;
833
833
  }
834
834
  interface CodemationPluginConfig {
835
+ readonly pluginPackageId?: string;
835
836
  readonly credentialTypes?: ReadonlyArray<AnyCredentialType>;
836
837
  readonly register?: (context: CodemationPluginContext) => void | Promise<void>;
837
838
  readonly sandbox?: CodemationConfig;
@@ -22975,6 +22976,7 @@ type CodemationConsumerConfigResolution = Readonly<{
22975
22976
  }>;
22976
22977
  declare class CodemationConsumerConfigLoader {
22977
22978
  private static readonly importerRegistrationsByTsconfig;
22979
+ private static readonly importerNamespaceVersionByTsconfig;
22978
22980
  private readonly configExportsResolver;
22979
22981
  private readonly configNormalizer;
22980
22982
  private readonly workflowModulePathFinder;
@@ -22996,6 +22998,7 @@ declare class CodemationConsumerConfigLoader {
22996
22998
  private resolveTsconfigPath;
22997
22999
  private getOrCreateImporter;
22998
23000
  private resetImporter;
23001
+ private nextNamespaceVersion;
22999
23002
  private toNamespace;
23000
23003
  private resolveTsxImporterModuleSpecifier;
23001
23004
  private findNearestTsconfig;
@@ -23127,6 +23130,7 @@ type ConsumerOutputBuildSnapshot = Readonly<{
23127
23130
  }>;
23128
23131
  declare class ConsumerOutputBuilder {
23129
23132
  private readonly consumerRoot;
23133
+ private readonly configPathOverride?;
23130
23134
  private static readonly ignoredDirectoryNames;
23131
23135
  private static readonly supportedSourceExtensions;
23132
23136
  private static readonly watchBuildDebounceMs;
@@ -23143,7 +23147,7 @@ declare class ConsumerOutputBuilder {
23143
23147
  private lastIssuedBuildVersion;
23144
23148
  private readonly log;
23145
23149
  private readonly buildOptions;
23146
- constructor(consumerRoot: string, logOverride?: Logger, buildOptionsOverride?: ConsumerBuildOptions);
23150
+ constructor(consumerRoot: string, logOverride?: Logger, buildOptionsOverride?: ConsumerBuildOptions, configPathOverride?: string | undefined);
23147
23151
  ensureBuilt(): Promise<ConsumerOutputBuildSnapshot>;
23148
23152
  /**
23149
23153
  * Stops the chokidar watcher and clears debounce timers. Safe to call when not watching.
@@ -23208,10 +23212,27 @@ declare class ConsumerOutputBuilder {
23208
23212
  private fileExists;
23209
23213
  }
23210
23214
  //#endregion
23215
+ //#region src/build/ConsumerBuildArtifactsPublisher.d.ts
23216
+ type ConsumerBuildManifest = Readonly<{
23217
+ buildVersion: string;
23218
+ consumerRoot: string;
23219
+ entryPath: string;
23220
+ manifestPath: string;
23221
+ pluginEntryPath: string;
23222
+ workflowSourcePaths: ReadonlyArray<string>;
23223
+ }>;
23224
+ declare class ConsumerBuildArtifactsPublisher {
23225
+ publish(snapshot: ConsumerOutputBuildSnapshot, discoveredPlugins: ReadonlyArray<CodemationDiscoveredPluginPackage>): Promise<ConsumerBuildManifest>;
23226
+ private writeDiscoveredPluginsOutput;
23227
+ private writeBuildManifest;
23228
+ private resolvePluginEntry;
23229
+ }
23230
+ //#endregion
23211
23231
  //#region src/consumer/ConsumerOutputBuilderFactory.d.ts
23212
23232
  declare class ConsumerOutputBuilderFactory {
23213
23233
  create(consumerRoot: string, args?: Readonly<{
23214
23234
  buildOptions?: ConsumerBuildOptions;
23235
+ configPathOverride?: string;
23215
23236
  logger?: Logger;
23216
23237
  }>): ConsumerOutputBuilder;
23217
23238
  }
@@ -23237,8 +23258,10 @@ declare class BuildCommand {
23237
23258
  private readonly cliLogger;
23238
23259
  private readonly pathResolver;
23239
23260
  private readonly consumerOutputBuilderFactory;
23261
+ private readonly pluginDiscovery;
23262
+ private readonly consumerBuildArtifactsPublisher;
23240
23263
  private readonly tsRuntime;
23241
- constructor(cliLogger: Logger, pathResolver: CliPathResolver, consumerOutputBuilderFactory: ConsumerOutputBuilderFactory, tsRuntime: TypeScriptRuntimeConfigurator);
23264
+ constructor(cliLogger: Logger, pathResolver: CliPathResolver, consumerOutputBuilderFactory: ConsumerOutputBuilderFactory, pluginDiscovery: CodemationPluginDiscovery, consumerBuildArtifactsPublisher: ConsumerBuildArtifactsPublisher, tsRuntime: TypeScriptRuntimeConfigurator);
23242
23265
  execute(consumerRoot: string, buildOptions: ConsumerBuildOptions): Promise<void>;
23243
23266
  }
23244
23267
  //#endregion
@@ -23368,8 +23391,12 @@ declare class ListenPortConflictDescriber {
23368
23391
  private readonly platform;
23369
23392
  constructor(platform?: NodeJS.Platform);
23370
23393
  describeLoopbackPort(port: number): Promise<string | null>;
23394
+ private resolveLoopbackOccupants;
23371
23395
  private readLsofOutput;
23372
23396
  private parseLsofOutput;
23397
+ private readSsOutput;
23398
+ private execFileStdout;
23399
+ private parseSsListenOutput;
23373
23400
  }
23374
23401
  //#endregion
23375
23402
  //#region src/dev/CliDevProxyServer.d.ts
@@ -23407,6 +23434,7 @@ declare class CliDevProxyServer {
23407
23434
  private handleHttpRequest;
23408
23435
  private handleUpgrade;
23409
23436
  private safePathname;
23437
+ private extractOccupyingPids;
23410
23438
  private rejectListenError;
23411
23439
  private broadcastDev;
23412
23440
  private broadcastWorkflowLifecycleToSubscribedRooms;
@@ -23529,6 +23557,7 @@ declare class DevNextHostEnvironmentBuilder {
23529
23557
  buildConsumerUiProxy(args: Readonly<{
23530
23558
  authSecret: string;
23531
23559
  configPathOverride?: string;
23560
+ consumerOutputManifestPath?: string;
23532
23561
  consumerRoot: string;
23533
23562
  developmentServerToken: string;
23534
23563
  nextPort: number;
@@ -23540,6 +23569,7 @@ declare class DevNextHostEnvironmentBuilder {
23540
23569
  build(args: Readonly<{
23541
23570
  authSecret?: string;
23542
23571
  configPathOverride?: string;
23572
+ consumerOutputManifestPath?: string;
23543
23573
  consumerRoot: string;
23544
23574
  developmentServerToken: string;
23545
23575
  nextPort: number;
@@ -23547,6 +23577,7 @@ declare class DevNextHostEnvironmentBuilder {
23547
23577
  websocketPort: number;
23548
23578
  runtimeDevUrl?: string;
23549
23579
  }>): NodeJS.ProcessEnv;
23580
+ private resolvePublicWebsocketPort;
23550
23581
  }
23551
23582
  //#endregion
23552
23583
  //#region src/runtime/ListenPortResolver.d.ts
@@ -23719,6 +23750,9 @@ declare class DevCommand {
23719
23750
  private readonly cliLogger;
23720
23751
  private readonly session;
23721
23752
  private readonly databaseMigrationsApplyService;
23753
+ private readonly consumerOutputBuilderFactory;
23754
+ private readonly pluginDiscovery;
23755
+ private readonly consumerBuildArtifactsPublisher;
23722
23756
  private readonly devBootstrapSummaryFetcher;
23723
23757
  private readonly devCliBannerRenderer;
23724
23758
  private readonly consumerEnvDotenvFilePredicate;
@@ -23728,7 +23762,7 @@ declare class DevCommand {
23728
23762
  private readonly cliDevProxyServerFactory;
23729
23763
  private readonly devRebuildQueueFactory;
23730
23764
  private readonly require;
23731
- constructor(pathResolver: CliPathResolver, tsRuntime: TypeScriptRuntimeConfigurator, devLockFactory: DevLockFactory, devSourceWatcherFactory: DevSourceWatcherFactory, cliLogger: Logger, session: DevSessionServices, databaseMigrationsApplyService: DatabaseMigrationsApplyService, devBootstrapSummaryFetcher: DevBootstrapSummaryFetcher, devCliBannerRenderer: DevCliBannerRenderer, consumerEnvDotenvFilePredicate: ConsumerEnvDotenvFilePredicate, devTrackedProcessTreeKiller: DevTrackedProcessTreeKiller, nextHostConsumerServerCommandFactory: NextHostConsumerServerCommandFactory, devApiRuntimeFactory: DevApiRuntimeFactory, cliDevProxyServerFactory: CliDevProxyServerFactory, devRebuildQueueFactory: DevRebuildQueueFactory);
23765
+ constructor(pathResolver: CliPathResolver, tsRuntime: TypeScriptRuntimeConfigurator, devLockFactory: DevLockFactory, devSourceWatcherFactory: DevSourceWatcherFactory, cliLogger: Logger, session: DevSessionServices, databaseMigrationsApplyService: DatabaseMigrationsApplyService, consumerOutputBuilderFactory: ConsumerOutputBuilderFactory, pluginDiscovery: CodemationPluginDiscovery, consumerBuildArtifactsPublisher: ConsumerBuildArtifactsPublisher, devBootstrapSummaryFetcher: DevBootstrapSummaryFetcher, devCliBannerRenderer: DevCliBannerRenderer, consumerEnvDotenvFilePredicate: ConsumerEnvDotenvFilePredicate, devTrackedProcessTreeKiller: DevTrackedProcessTreeKiller, nextHostConsumerServerCommandFactory: NextHostConsumerServerCommandFactory, devApiRuntimeFactory: DevApiRuntimeFactory, cliDevProxyServerFactory: CliDevProxyServerFactory, devRebuildQueueFactory: DevRebuildQueueFactory);
23732
23766
  execute(args: Readonly<{
23733
23767
  consumerRoot: string;
23734
23768
  watchFramework?: boolean;
@@ -23758,6 +23792,7 @@ declare class DevCommand {
23758
23792
  private stopLiveProcesses;
23759
23793
  private stopCurrentRuntime;
23760
23794
  private createRuntime;
23795
+ private publishConsumerArtifacts;
23761
23796
  private logPackagedUiDevHintWhenNeeded;
23762
23797
  }
23763
23798
  //#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-C31h2o8w.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-BYHuUedo.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.16",
3
+ "version": "0.0.19",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -11,6 +11,7 @@
11
11
  "url": "https://github.com/MadeRelevant/codemation",
12
12
  "directory": "packages/cli"
13
13
  },
14
+ "license": "SEE LICENSE IN LICENSE",
14
15
  "type": "module",
15
16
  "main": "./dist/index.js",
16
17
  "types": "./dist/index.d.ts",
@@ -37,8 +38,8 @@
37
38
  "reflect-metadata": "^0.2.2",
38
39
  "typescript": "^5.9.3",
39
40
  "ws": "^8.19.0",
40
- "@codemation/host": "0.0.16",
41
- "@codemation/next-host": "0.0.16"
41
+ "@codemation/host": "0.0.19",
42
+ "@codemation/next-host": "0.0.19"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/http-proxy": "^1.17.15",
@@ -49,7 +50,7 @@
49
50
  "tsx": "^4.21.0",
50
51
  "typescript": "^5.9.3",
51
52
  "vitest": "4.0.18",
52
- "@codemation/eslint-config": "0.0.0"
53
+ "@codemation/eslint-config": "0.0.1"
53
54
  },
54
55
  "peerDependencies": {
55
56
  "tsx": ">=4.0.0"
@@ -3,6 +3,7 @@ import { AppContainerFactory } from "@codemation/host";
3
3
  import { logLevelPolicyFactory, ServerLoggerFactory } from "@codemation/host/next/server";
4
4
 
5
5
  import { ConsumerBuildOptionsParser } from "./build/ConsumerBuildOptionsParser";
6
+ import { ConsumerBuildArtifactsPublisher } from "./build/ConsumerBuildArtifactsPublisher";
6
7
  import { BuildCommand } from "./commands/BuildCommand";
7
8
  import { DbMigrateCommand } from "./commands/DbMigrateCommand";
8
9
  import { DevCommand } from "./commands/DevCommand";
@@ -78,6 +79,8 @@ export class CliProgramFactory {
78
79
  );
79
80
 
80
81
  const buildOptionsParser = new ConsumerBuildOptionsParser();
82
+ const consumerOutputBuilderFactory = new ConsumerOutputBuilderFactory();
83
+ const consumerBuildArtifactsPublisher = new ConsumerBuildArtifactsPublisher();
81
84
  const devCommand = new DevCommand(
82
85
  pathResolver,
83
86
  tsRuntime,
@@ -86,6 +89,9 @@ export class CliProgramFactory {
86
89
  cliLogger,
87
90
  devSessionServices,
88
91
  databaseMigrationsApplyService,
92
+ consumerOutputBuilderFactory,
93
+ pluginDiscovery,
94
+ consumerBuildArtifactsPublisher,
89
95
  new DevBootstrapSummaryFetcher(),
90
96
  new DevCliBannerRenderer(),
91
97
  new ConsumerEnvDotenvFilePredicate(),
@@ -97,7 +103,14 @@ export class CliProgramFactory {
97
103
  );
98
104
  return new CliProgram(
99
105
  buildOptionsParser,
100
- new BuildCommand(cliLogger, pathResolver, new ConsumerOutputBuilderFactory(), tsRuntime),
106
+ new BuildCommand(
107
+ cliLogger,
108
+ pathResolver,
109
+ consumerOutputBuilderFactory,
110
+ pluginDiscovery,
111
+ consumerBuildArtifactsPublisher,
112
+ tsRuntime,
113
+ ),
101
114
  devCommand,
102
115
  new DevPluginCommand(new PluginDevConfigFactory(), devCommand),
103
116
  new ServeWebCommand(
@@ -0,0 +1,82 @@
1
+ import type { CodemationDiscoveredPluginPackage } from "@codemation/host/server";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, rename, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import type { ConsumerOutputBuildSnapshot } from "../consumer/ConsumerOutputBuilder";
8
+
9
+ export type ConsumerBuildManifest = Readonly<{
10
+ buildVersion: string;
11
+ consumerRoot: string;
12
+ entryPath: string;
13
+ manifestPath: string;
14
+ pluginEntryPath: string;
15
+ workflowSourcePaths: ReadonlyArray<string>;
16
+ }>;
17
+
18
+ export class ConsumerBuildArtifactsPublisher {
19
+ async publish(
20
+ snapshot: ConsumerOutputBuildSnapshot,
21
+ discoveredPlugins: ReadonlyArray<CodemationDiscoveredPluginPackage>,
22
+ ): Promise<ConsumerBuildManifest> {
23
+ const pluginEntryPath = await this.writeDiscoveredPluginsOutput(snapshot, discoveredPlugins);
24
+ return await this.writeBuildManifest(snapshot, pluginEntryPath);
25
+ }
26
+
27
+ private async writeDiscoveredPluginsOutput(
28
+ snapshot: ConsumerOutputBuildSnapshot,
29
+ discoveredPlugins: ReadonlyArray<CodemationDiscoveredPluginPackage>,
30
+ ): Promise<string> {
31
+ const outputPath = path.resolve(snapshot.emitOutputRoot, "plugins.js");
32
+ await mkdir(path.dirname(outputPath), { recursive: true });
33
+ const outputLines: string[] = ["const codemationDiscoveredPlugins = [];", ""];
34
+ discoveredPlugins.forEach((discoveredPlugin: CodemationDiscoveredPluginPackage, index: number) => {
35
+ const pluginModulePath = path.resolve(discoveredPlugin.packageRoot, this.resolvePluginEntry(discoveredPlugin));
36
+ const pluginFileUrl = pathToFileURL(pluginModulePath).href;
37
+ outputLines.push(`const pluginModule${index} = await import(${JSON.stringify(pluginFileUrl)});`);
38
+ outputLines.push(
39
+ `const pluginValue${index} = pluginModule${index}.default ?? pluginModule${index}.codemationPlugin;`,
40
+ );
41
+ outputLines.push(`if (pluginValue${index} && typeof pluginValue${index}.register === "function") {`);
42
+ outputLines.push(` codemationDiscoveredPlugins.push(pluginValue${index});`);
43
+ outputLines.push(
44
+ `} else if (typeof pluginValue${index} === "function" && pluginValue${index}.prototype && typeof pluginValue${index}.prototype.register === "function") {`,
45
+ );
46
+ outputLines.push(` codemationDiscoveredPlugins.push(new pluginValue${index}());`);
47
+ outputLines.push("}");
48
+ outputLines.push("");
49
+ });
50
+ outputLines.push("export { codemationDiscoveredPlugins };");
51
+ outputLines.push("export default codemationDiscoveredPlugins;");
52
+ outputLines.push("");
53
+ await writeFile(outputPath, outputLines.join("\n"), "utf8");
54
+ return outputPath;
55
+ }
56
+
57
+ private async writeBuildManifest(
58
+ snapshot: ConsumerOutputBuildSnapshot,
59
+ pluginEntryPath: string,
60
+ ): Promise<ConsumerBuildManifest> {
61
+ const manifest: ConsumerBuildManifest = {
62
+ buildVersion: snapshot.buildVersion,
63
+ consumerRoot: snapshot.consumerRoot,
64
+ entryPath: snapshot.outputEntryPath,
65
+ manifestPath: snapshot.manifestPath,
66
+ pluginEntryPath,
67
+ workflowSourcePaths: snapshot.workflowSourcePaths,
68
+ };
69
+ await mkdir(path.dirname(snapshot.manifestPath), { recursive: true });
70
+ const temporaryManifestPath = `${snapshot.manifestPath}.${snapshot.buildVersion}.${randomUUID()}.tmp`;
71
+ await writeFile(temporaryManifestPath, JSON.stringify(manifest, null, 2), "utf8");
72
+ await rename(temporaryManifestPath, snapshot.manifestPath);
73
+ return manifest;
74
+ }
75
+
76
+ private resolvePluginEntry(discoveredPlugin: CodemationDiscoveredPluginPackage): string {
77
+ if (typeof discoveredPlugin.developmentEntry === "string" && discoveredPlugin.developmentEntry.trim().length > 0) {
78
+ return discoveredPlugin.developmentEntry;
79
+ }
80
+ return discoveredPlugin.pluginEntry;
81
+ }
82
+ }
@@ -1,5 +1,7 @@
1
+ import type { CodemationPluginDiscovery } from "@codemation/host/server";
1
2
  import type { Logger } from "@codemation/host/next/server";
2
3
 
4
+ import type { ConsumerBuildArtifactsPublisher } from "../build/ConsumerBuildArtifactsPublisher";
3
5
  import type { ConsumerOutputBuilderFactory } from "../consumer/ConsumerOutputBuilderFactory";
4
6
  import type { ConsumerBuildOptions } from "../consumer/consumerBuildOptions.types";
5
7
  import { CliPathResolver } from "../path/CliPathResolver";
@@ -10,6 +12,8 @@ export class BuildCommand {
10
12
  private readonly cliLogger: Logger,
11
13
  private readonly pathResolver: CliPathResolver,
12
14
  private readonly consumerOutputBuilderFactory: ConsumerOutputBuilderFactory,
15
+ private readonly pluginDiscovery: CodemationPluginDiscovery,
16
+ private readonly consumerBuildArtifactsPublisher: ConsumerBuildArtifactsPublisher,
13
17
  private readonly tsRuntime: TypeScriptRuntimeConfigurator,
14
18
  ) {}
15
19
 
@@ -18,7 +22,10 @@ export class BuildCommand {
18
22
  this.tsRuntime.configure(paths.repoRoot);
19
23
  const builder = this.consumerOutputBuilderFactory.create(paths.consumerRoot, { buildOptions });
20
24
  const snapshot = await builder.ensureBuilt();
25
+ const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
26
+ const manifest = await this.consumerBuildArtifactsPublisher.publish(snapshot, discoveredPlugins);
21
27
  this.cliLogger.info(`Built consumer output: ${snapshot.outputEntryPath}`);
28
+ this.cliLogger.info(`Build manifest: ${manifest.manifestPath}`);
22
29
  this.cliLogger.info(`Workflow modules emitted: ${snapshot.workflowSourcePaths.length}`);
23
30
  }
24
31
  }
@@ -1,9 +1,12 @@
1
+ import type { CodemationPluginDiscovery } from "@codemation/host/server";
1
2
  import type { Logger } from "@codemation/host/next/server";
2
3
  import { spawn, type ChildProcess } from "node:child_process";
3
4
  import { createRequire } from "node:module";
4
5
  import path from "node:path";
5
6
  import process from "node:process";
6
7
 
8
+ import type { ConsumerBuildArtifactsPublisher } from "../build/ConsumerBuildArtifactsPublisher";
9
+ import type { ConsumerOutputBuilderFactory } from "../consumer/ConsumerOutputBuilderFactory";
7
10
  import type { DatabaseMigrationsApplyService } from "../database/DatabaseMigrationsApplyService";
8
11
  import type { DevApiRuntimeFactory, DevApiRuntimeServerHandle } from "../dev/DevApiRuntimeFactory";
9
12
  import type { DevBootstrapSummaryFetcher } from "../dev/DevBootstrapSummaryFetcher";
@@ -35,6 +38,9 @@ export class DevCommand {
35
38
  private readonly cliLogger: Logger,
36
39
  private readonly session: DevSessionServices,
37
40
  private readonly databaseMigrationsApplyService: DatabaseMigrationsApplyService,
41
+ private readonly consumerOutputBuilderFactory: ConsumerOutputBuilderFactory,
42
+ private readonly pluginDiscovery: CodemationPluginDiscovery,
43
+ private readonly consumerBuildArtifactsPublisher: ConsumerBuildArtifactsPublisher,
38
44
  private readonly devBootstrapSummaryFetcher: DevBootstrapSummaryFetcher,
39
45
  private readonly devCliBannerRenderer: DevCliBannerRenderer,
40
46
  private readonly consumerEnvDotenvFilePredicate: ConsumerEnvDotenvFilePredicate,
@@ -55,6 +61,7 @@ export class DevCommand {
55
61
  ): Promise<void> {
56
62
  const paths = await this.pathResolver.resolve(args.consumerRoot);
57
63
  const commandName = args.commandName ?? "dev";
64
+ const previousDevelopmentServerToken = process.env.CODEMATION_DEV_SERVER_TOKEN;
58
65
  this.devCliBannerRenderer.renderBrandHeader();
59
66
  this.tsRuntime.configure(paths.repoRoot);
60
67
  await this.databaseMigrationsApplyService.applyForConsumer(paths.consumerRoot, {
@@ -86,6 +93,12 @@ export class DevCommand {
86
93
  authSettings,
87
94
  args.configPathOverride,
88
95
  );
96
+ if (prepared.devMode === "packaged-ui") {
97
+ await this.publishConsumerArtifacts(prepared.paths, prepared.configPathOverride);
98
+ }
99
+ // The disposable runtime is created in-process, so config reloads must see the same token in
100
+ // `process.env` that we also pass through the child-facing env object.
101
+ process.env.CODEMATION_DEV_SERVER_TOKEN = prepared.developmentServerToken;
89
102
  const stopPromise = this.wireStopPromise(processState);
90
103
  const uiProxyBase = await this.preparePackagedUiBaseUrlWhenNeeded(prepared, processState);
91
104
  proxyServer = await this.startProxyServer(prepared.gatewayPort, uiProxyBase);
@@ -106,6 +119,11 @@ export class DevCommand {
106
119
  this.logPackagedUiDevHintWhenNeeded(devMode, gatewayPort, commandName);
107
120
  await stopPromise;
108
121
  } finally {
122
+ if (previousDevelopmentServerToken === undefined) {
123
+ delete process.env.CODEMATION_DEV_SERVER_TOKEN;
124
+ } else {
125
+ process.env.CODEMATION_DEV_SERVER_TOKEN = previousDevelopmentServerToken;
126
+ }
109
127
  processState.stopRequested = true;
110
128
  await this.stopLiveProcesses(processState, proxyServer);
111
129
  await watcher.stop();
@@ -416,6 +434,9 @@ export class DevCommand {
416
434
  proxyServer.setBuildStatus("building");
417
435
  proxyServer.broadcastBuildStarted();
418
436
  try {
437
+ if (prepared.devMode === "packaged-ui") {
438
+ await this.publishConsumerArtifacts(prepared.paths, request.configPathOverride);
439
+ }
419
440
  await this.stopCurrentRuntime(state, proxyServer);
420
441
  process.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
421
442
  const runtime = await this.createRuntime(prepared);
@@ -424,15 +445,18 @@ export class DevCommand {
424
445
  httpPort: runtime.httpPort,
425
446
  workflowWebSocketPort: runtime.workflowWebSocketPort,
426
447
  });
427
- if (request.shouldRestartUi) {
428
- await this.restartUiAfterSourceChange(prepared, state, gatewayBaseUrl);
429
- }
430
448
  await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
431
449
  const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
432
450
  if (json) {
433
451
  this.devCliBannerRenderer.renderCompact(json);
434
452
  }
435
453
  proxyServer.setBuildStatus("idle");
454
+ // Let the new runtime become queryable through the stable gateway before restarting the
455
+ // packaged UI; otherwise the UI bootstrap hits `/api/bootstrap/*` while the gateway still
456
+ // reports "Runtime is rebuilding" and the restart can deadlock indefinitely.
457
+ if (request.shouldRestartUi) {
458
+ await this.restartUiAfterSourceChange(prepared, state, gatewayBaseUrl);
459
+ }
436
460
  proxyServer.broadcastBuildCompleted(runtime.buildVersion);
437
461
  process.stdout.write("[codemation] Runtime ready.\n");
438
462
  } catch (error) {
@@ -558,6 +582,16 @@ export class DevCommand {
558
582
  });
559
583
  }
560
584
 
585
+ private async publishConsumerArtifacts(paths: CliPaths, configPathOverride?: string): Promise<void> {
586
+ const builder = this.consumerOutputBuilderFactory.create(paths.consumerRoot, {
587
+ configPathOverride,
588
+ });
589
+ const snapshot = await builder.ensureBuilt();
590
+ const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
591
+ await this.consumerBuildArtifactsPublisher.publish(snapshot, discoveredPlugins);
592
+ this.cliLogger.debug(`Dev: consumer output published (${snapshot.buildVersion}).`);
593
+ }
594
+
561
595
  private logPackagedUiDevHintWhenNeeded(
562
596
  devMode: DevMode,
563
597
  gatewayPort: number,
@@ -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;
@@ -716,6 +717,14 @@ export class ConsumerOutputBuilder {
716
717
  }
717
718
 
718
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
+ }
719
728
  for (const candidate of this.getConventionCandidates(consumerRoot)) {
720
729
  if (await this.fileExists(candidate)) {
721
730
  return candidate;
@@ -8,9 +8,10 @@ export class ConsumerOutputBuilderFactory {
8
8
  consumerRoot: string,
9
9
  args?: Readonly<{
10
10
  buildOptions?: ConsumerBuildOptions;
11
+ configPathOverride?: string;
11
12
  logger?: Logger;
12
13
  }>,
13
14
  ): ConsumerOutputBuilder {
14
- return new ConsumerOutputBuilder(consumerRoot, args?.logger, args?.buildOptions);
15
+ return new ConsumerOutputBuilder(consumerRoot, args?.logger, args?.buildOptions, args?.configPathOverride);
15
16
  }
16
17
  }
@@ -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
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import process from "node:process";
2
3
 
3
4
  import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
@@ -13,6 +14,7 @@ export class DevNextHostEnvironmentBuilder {
13
14
  args: Readonly<{
14
15
  authSecret: string;
15
16
  configPathOverride?: string;
17
+ consumerOutputManifestPath?: string;
16
18
  consumerRoot: string;
17
19
  developmentServerToken: string;
18
20
  nextPort: number;
@@ -22,10 +24,12 @@ export class DevNextHostEnvironmentBuilder {
22
24
  websocketPort: number;
23
25
  }>,
24
26
  ): NodeJS.ProcessEnv {
27
+ const publicWebsocketPort = this.resolvePublicWebsocketPort(args.publicBaseUrl, args.websocketPort);
25
28
  return {
26
29
  ...this.build({
27
30
  authSecret: args.authSecret,
28
31
  configPathOverride: args.configPathOverride,
32
+ consumerOutputManifestPath: args.consumerOutputManifestPath,
29
33
  consumerRoot: args.consumerRoot,
30
34
  developmentServerToken: args.developmentServerToken,
31
35
  nextPort: args.nextPort,
@@ -38,6 +42,8 @@ export class DevNextHostEnvironmentBuilder {
38
42
  HOSTNAME: "127.0.0.1",
39
43
  AUTH_SECRET: args.authSecret,
40
44
  AUTH_URL: args.publicBaseUrl,
45
+ CODEMATION_PUBLIC_WS_PORT: String(publicWebsocketPort),
46
+ NEXT_PUBLIC_CODEMATION_WS_PORT: String(publicWebsocketPort),
41
47
  };
42
48
  }
43
49
 
@@ -45,6 +51,7 @@ export class DevNextHostEnvironmentBuilder {
45
51
  args: Readonly<{
46
52
  authSecret?: string;
47
53
  configPathOverride?: string;
54
+ consumerOutputManifestPath?: string;
48
55
  consumerRoot: string;
49
56
  developmentServerToken: string;
50
57
  nextPort: number;
@@ -54,11 +61,15 @@ export class DevNextHostEnvironmentBuilder {
54
61
  }>,
55
62
  ): NodeJS.ProcessEnv {
56
63
  const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process.env);
64
+ const consumerOutputManifestPath =
65
+ args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
57
66
  return {
58
67
  ...merged,
59
68
  PORT: String(args.nextPort),
60
69
  CODEMATION_CONSUMER_ROOT: args.consumerRoot,
70
+ CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
61
71
  CODEMATION_UI_AUTH_ENABLED: String(!args.skipUiAuth),
72
+ CODEMATION_PUBLIC_WS_PORT: String(args.websocketPort),
62
73
  CODEMATION_WS_PORT: String(args.websocketPort),
63
74
  NEXT_PUBLIC_CODEMATION_WS_PORT: String(args.websocketPort),
64
75
  CODEMATION_DEV_SERVER_TOKEN: args.developmentServerToken,
@@ -75,4 +86,17 @@ export class DevNextHostEnvironmentBuilder {
75
86
  : {}),
76
87
  };
77
88
  }
89
+
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.
99
+ }
100
+ return fallbackPort;
101
+ }
78
102
  }
@@ -18,11 +18,7 @@ export class ListenPortConflictDescriber {
18
18
  return null;
19
19
  }
20
20
 
21
- const raw = await this.readLsofOutput(port);
22
- if (raw === null) {
23
- return null;
24
- }
25
- const occupants = this.parseLsofOutput(raw);
21
+ const occupants = await this.resolveLoopbackOccupants(port);
26
22
  if (occupants.length === 0) {
27
23
  return null;
28
24
  }
@@ -32,6 +28,22 @@ export class ListenPortConflictDescriber {
32
28
  .join("; ");
33
29
  }
34
30
 
31
+ private async resolveLoopbackOccupants(port: number): Promise<ReadonlyArray<PortOccupant>> {
32
+ const lsofRaw = await this.readLsofOutput(port);
33
+ const fromLsof = lsofRaw !== null ? this.parseLsofOutput(lsofRaw) : [];
34
+ if (fromLsof.length > 0) {
35
+ return fromLsof;
36
+ }
37
+ if (this.platform !== "linux") {
38
+ return [];
39
+ }
40
+ const ssRaw = await this.readSsOutput(port);
41
+ if (ssRaw === null) {
42
+ return [];
43
+ }
44
+ return this.parseSsListenOutput(ssRaw, port);
45
+ }
46
+
35
47
  private async readLsofOutput(port: number): Promise<string | null> {
36
48
  try {
37
49
  return await new Promise<string>((resolve, reject) => {
@@ -80,4 +92,53 @@ export class ListenPortConflictDescriber {
80
92
 
81
93
  return occupants;
82
94
  }
95
+
96
+ private async readSsOutput(port: number): Promise<string | null> {
97
+ const filtered = await this.execFileStdout("ss", ["-lntp", `sport = :${port}`]);
98
+ if (filtered !== null && filtered.trim().length > 0) {
99
+ return filtered;
100
+ }
101
+ return this.execFileStdout("ss", ["-lntp"]);
102
+ }
103
+
104
+ private async execFileStdout(command: string, args: readonly string[]): Promise<string | null> {
105
+ try {
106
+ return await new Promise<string>((resolve, reject) => {
107
+ execFile(command, [...args], (error, stdout) => {
108
+ if (error) {
109
+ reject(error);
110
+ return;
111
+ }
112
+ resolve(stdout);
113
+ });
114
+ });
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ private parseSsListenOutput(raw: string, port: number): ReadonlyArray<PortOccupant> {
121
+ const occupants: PortOccupant[] = [];
122
+ const portSuffix = `:${port}`;
123
+ for (const line of raw.split("\n")) {
124
+ if (!line.includes("LISTEN") || !line.includes(portSuffix)) {
125
+ continue;
126
+ }
127
+ const pidMatch = line.match(/pid=(\d+)/);
128
+ if (!pidMatch) {
129
+ continue;
130
+ }
131
+ const pid = Number.parseInt(pidMatch[1] ?? "0", 10);
132
+ const cmdMatch = line.match(/users:\(\("([^"]*)"/);
133
+ const command = cmdMatch?.[1] ?? "unknown";
134
+ const localAddrMatch = line.match(/\s+(\S+:\d+|\[[^\]]+\]:\d+)\s+/);
135
+ const endpoint = localAddrMatch?.[1] ?? `tcp:${String(port)}`;
136
+ occupants.push({
137
+ pid,
138
+ command,
139
+ endpoint,
140
+ });
141
+ }
142
+ return occupants;
143
+ }
83
144
  }