@codemation/cli 0.0.7 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ import { randomUUID } from "node:crypto";
7
7
  import { access, copyFile, cp, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath, pathToFileURL } from "node:url";
10
- import { spawn } from "node:child_process";
10
+ import { execFile, spawn } from "node:child_process";
11
11
  import { existsSync, readFileSync } from "node:fs";
12
12
  import { config, parse } from "dotenv";
13
13
  import { watch } from "chokidar";
@@ -391,6 +391,7 @@ var DevCommand = class {
391
391
  });
392
392
  }
393
393
  async runQueuedRebuild(prepared, state, gatewayBaseUrl, proxyServer, request) {
394
+ request.changedPaths;
394
395
  proxyServer.setBuildStatus("building");
395
396
  proxyServer.broadcastBuildStarted();
396
397
  try {
@@ -1574,8 +1575,9 @@ var CliDevProxyServer = class {
1574
1575
  childWorkflowSocket = null;
1575
1576
  server = null;
1576
1577
  uiProxyTarget = null;
1577
- constructor(listenPort) {
1578
+ constructor(listenPort, listenPortConflictDescriber) {
1578
1579
  this.listenPort = listenPort;
1580
+ this.listenPortConflictDescriber = listenPortConflictDescriber;
1579
1581
  }
1580
1582
  async start() {
1581
1583
  if (this.server) return;
@@ -1597,7 +1599,9 @@ var CliDevProxyServer = class {
1597
1599
  this.handleUpgrade(request, socket, head);
1598
1600
  });
1599
1601
  await new Promise((resolve, reject) => {
1600
- server.once("error", reject);
1602
+ server.once("error", (error) => {
1603
+ this.rejectListenError(error, reject);
1604
+ });
1601
1605
  server.listen(this.listenPort, "127.0.0.1", () => {
1602
1606
  resolve();
1603
1607
  });
@@ -1681,7 +1685,6 @@ var CliDevProxyServer = class {
1681
1685
  async handleHttpRequest(req, res) {
1682
1686
  const pathname = this.safePathname(req.url ?? "");
1683
1687
  const uiProxyTarget = this.uiProxyTarget;
1684
- const activeRuntimeTarget = this.activeRuntime ? `http://127.0.0.1:${this.activeRuntime.httpPort}` : void 0;
1685
1688
  if (pathname === "/api/dev/health" && req.method === "GET") {
1686
1689
  res.writeHead(200, { "content-type": "application/json" });
1687
1690
  res.end(JSON.stringify({
@@ -1690,15 +1693,6 @@ var CliDevProxyServer = class {
1690
1693
  }));
1691
1694
  return;
1692
1695
  }
1693
- if (pathname === "/api/dev/bootstrap-summary") {
1694
- if (!activeRuntimeTarget) {
1695
- res.writeHead(503, { "content-type": "text/plain" });
1696
- res.end("Runtime is rebuilding.");
1697
- return;
1698
- }
1699
- this.proxy.web(req, res, { target: activeRuntimeTarget });
1700
- return;
1701
- }
1702
1696
  if (uiProxyTarget && pathname.startsWith("/api/auth/")) {
1703
1697
  this.proxy.web(req, res, { target: uiProxyTarget.replace(/\/$/, "") });
1704
1698
  return;
@@ -1709,7 +1703,7 @@ var CliDevProxyServer = class {
1709
1703
  res.end("Runtime is rebuilding.");
1710
1704
  return;
1711
1705
  }
1712
- this.proxy.web(req, res, { target: activeRuntimeTarget });
1706
+ this.proxy.web(req, res, { target: `http://127.0.0.1:${this.activeRuntime.httpPort}` });
1713
1707
  return;
1714
1708
  }
1715
1709
  if (uiProxyTarget) {
@@ -1747,6 +1741,16 @@ var CliDevProxyServer = class {
1747
1741
  return url.split("?")[0] ?? url;
1748
1742
  }
1749
1743
  }
1744
+ async rejectListenError(error, reject) {
1745
+ if (error.code !== "EADDRINUSE") {
1746
+ reject(error);
1747
+ return;
1748
+ }
1749
+ const description = await this.listenPortConflictDescriber.describeLoopbackPort(this.listenPort);
1750
+ const baseMessage = `Dev gateway port ${this.listenPort} is already in use on 127.0.0.1.`;
1751
+ 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.`;
1752
+ reject(new Error(`${baseMessage}${suffix}`, { cause: error instanceof Error ? error : void 0 }));
1753
+ }
1750
1754
  broadcastDev(message) {
1751
1755
  const text = JSON.stringify(message);
1752
1756
  for (const client of this.devClients) if (client.readyState === WebSocket.OPEN) client.send(text);
@@ -1911,11 +1915,74 @@ var CliDevProxyServer = class {
1911
1915
  }
1912
1916
  };
1913
1917
 
1918
+ //#endregion
1919
+ //#region src/dev/ListenPortConflictDescriber.ts
1920
+ var ListenPortConflictDescriber = class {
1921
+ constructor(platform = process$1.platform) {
1922
+ this.platform = platform;
1923
+ }
1924
+ async describeLoopbackPort(port) {
1925
+ if (!Number.isInteger(port) || port <= 0) return null;
1926
+ if (this.platform !== "linux" && this.platform !== "darwin") return null;
1927
+ const raw = await this.readLsofOutput(port);
1928
+ if (raw === null) return null;
1929
+ const occupants = this.parseLsofOutput(raw);
1930
+ if (occupants.length === 0) return null;
1931
+ return occupants.map((occupant) => `pid=${occupant.pid} command=${occupant.command} endpoint=${occupant.endpoint}`).join("; ");
1932
+ }
1933
+ async readLsofOutput(port) {
1934
+ try {
1935
+ return await new Promise((resolve, reject) => {
1936
+ execFile("lsof", [
1937
+ "-nP",
1938
+ `-iTCP:${port}`,
1939
+ "-sTCP:LISTEN",
1940
+ "-Fpcn"
1941
+ ], (error, stdout) => {
1942
+ if (error) {
1943
+ reject(error);
1944
+ return;
1945
+ }
1946
+ resolve(stdout);
1947
+ });
1948
+ });
1949
+ } catch {
1950
+ return null;
1951
+ }
1952
+ }
1953
+ parseLsofOutput(raw) {
1954
+ const occupants = [];
1955
+ let currentPid = null;
1956
+ let currentCommand = null;
1957
+ for (const line of raw.split("\n")) {
1958
+ if (line.length < 2) continue;
1959
+ const prefix = line[0];
1960
+ const value = line.slice(1).trim();
1961
+ if (prefix === "p") {
1962
+ currentPid = Number.parseInt(value, 10);
1963
+ currentCommand = null;
1964
+ continue;
1965
+ }
1966
+ if (prefix === "c") {
1967
+ currentCommand = value;
1968
+ continue;
1969
+ }
1970
+ if (prefix === "n" && currentPid !== null && currentCommand !== null) occupants.push({
1971
+ pid: currentPid,
1972
+ command: currentCommand,
1973
+ endpoint: value
1974
+ });
1975
+ }
1976
+ return occupants;
1977
+ }
1978
+ };
1979
+
1914
1980
  //#endregion
1915
1981
  //#region src/dev/CliDevProxyServerFactory.ts
1916
1982
  var CliDevProxyServerFactory = class {
1983
+ listenPortConflictDescriber = new ListenPortConflictDescriber();
1917
1984
  create(gatewayPort) {
1918
- return new CliDevProxyServer(gatewayPort);
1985
+ return new CliDevProxyServer(gatewayPort, this.listenPortConflictDescriber);
1919
1986
  }
1920
1987
  };
1921
1988
 
@@ -2919,12 +2986,8 @@ var NextHostConsumerServerCommandFactory = class {
2919
2986
  //#region src/runtime/TypeScriptRuntimeConfigurator.ts
2920
2987
  var TypeScriptRuntimeConfigurator = class {
2921
2988
  configure(repoRoot) {
2922
- if (this.hasExplicitOverride()) return;
2923
2989
  process$1.env.CODEMATION_TSCONFIG_PATH = path.resolve(repoRoot, "tsconfig.base.json");
2924
2990
  }
2925
- hasExplicitOverride() {
2926
- return typeof process$1.env.CODEMATION_TSCONFIG_PATH === "string" && process$1.env.CODEMATION_TSCONFIG_PATH.length > 0;
2927
- }
2928
2991
  };
2929
2992
 
2930
2993
  //#endregion
package/dist/bin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as CliBin } from "./CliBin-Bx1lFBi5.js";
1
+ import { t as CliBin } from "./CliBin-BAnFX1wL.js";
2
2
  import process from "node:process";
3
3
  import "reflect-metadata";
4
4
 
package/dist/index.d.ts CHANGED
@@ -23283,7 +23283,6 @@ declare class CliPathResolver {
23283
23283
  //#region src/runtime/TypeScriptRuntimeConfigurator.d.ts
23284
23284
  declare class TypeScriptRuntimeConfigurator {
23285
23285
  configure(repoRoot: string): void;
23286
- private hasExplicitOverride;
23287
23286
  }
23288
23287
  //#endregion
23289
23288
  //#region src/commands/BuildCommand.d.ts
@@ -23418,6 +23417,15 @@ declare class DevBootstrapSummaryFetcher {
23418
23417
  fetch(gatewayBaseUrl: string): Promise<DevBootstrapSummaryJson | null>;
23419
23418
  }
23420
23419
  //#endregion
23420
+ //#region src/dev/ListenPortConflictDescriber.d.ts
23421
+ declare class ListenPortConflictDescriber {
23422
+ private readonly platform;
23423
+ constructor(platform?: NodeJS.Platform);
23424
+ describeLoopbackPort(port: number): Promise<string | null>;
23425
+ private readLsofOutput;
23426
+ private parseLsofOutput;
23427
+ }
23428
+ //#endregion
23421
23429
  //#region src/dev/CliDevProxyServer.d.ts
23422
23430
  type ProxyRuntimeTarget = Readonly<{
23423
23431
  httpPort: number;
@@ -23426,6 +23434,7 @@ type ProxyRuntimeTarget = Readonly<{
23426
23434
  type BuildStatus = "idle" | "building";
23427
23435
  declare class CliDevProxyServer {
23428
23436
  private readonly listenPort;
23437
+ private readonly listenPortConflictDescriber;
23429
23438
  private readonly proxy;
23430
23439
  private readonly devClients;
23431
23440
  private readonly devWss;
@@ -23438,7 +23447,7 @@ declare class CliDevProxyServer {
23438
23447
  private childWorkflowSocket;
23439
23448
  private server;
23440
23449
  private uiProxyTarget;
23441
- constructor(listenPort: number);
23450
+ constructor(listenPort: number, listenPortConflictDescriber: ListenPortConflictDescriber);
23442
23451
  start(): Promise<void>;
23443
23452
  stop(): Promise<void>;
23444
23453
  setUiProxyTarget(target: string | null): void;
@@ -23452,6 +23461,7 @@ declare class CliDevProxyServer {
23452
23461
  private handleHttpRequest;
23453
23462
  private handleUpgrade;
23454
23463
  private safePathname;
23464
+ private rejectListenError;
23455
23465
  private broadcastDev;
23456
23466
  private broadcastWorkflowLifecycleToSubscribedRooms;
23457
23467
  private connectWorkflowClient;
@@ -23471,6 +23481,7 @@ declare class CliDevProxyServer {
23471
23481
  //#endregion
23472
23482
  //#region src/dev/CliDevProxyServerFactory.d.ts
23473
23483
  declare class CliDevProxyServerFactory {
23484
+ private readonly listenPortConflictDescriber;
23474
23485
  create(gatewayPort: number): CliDevProxyServer;
23475
23486
  }
23476
23487
  //#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-Bx1lFBi5.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-BAnFX1wL.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,11 +1,16 @@
1
1
  {
2
2
  "name": "@codemation/cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.11",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "author": "Made Relevant B.V.",
8
8
  "homepage": "https://www.maderelevant.com",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/MadeRelevant/codemation",
12
+ "directory": "packages/cli"
13
+ },
9
14
  "type": "module",
10
15
  "main": "./dist/index.js",
11
16
  "types": "./dist/index.d.ts",
@@ -32,8 +37,8 @@
32
37
  "reflect-metadata": "^0.2.2",
33
38
  "typescript": "^5.9.3",
34
39
  "ws": "^8.19.0",
35
- "@codemation/host": "0.0.7",
36
- "@codemation/next-host": "0.0.7"
40
+ "@codemation/host": "0.0.11",
41
+ "@codemation/next-host": "0.0.11"
37
42
  },
38
43
  "devDependencies": {
39
44
  "@types/http-proxy": "^1.17.15",
@@ -387,6 +387,7 @@ export class DevCommand {
387
387
  shouldRestartUi: boolean;
388
388
  }>,
389
389
  ): Promise<void> {
390
+ void request.changedPaths;
390
391
  proxyServer.setBuildStatus("building");
391
392
  proxyServer.broadcastBuildStarted();
392
393
  try {
@@ -3,6 +3,7 @@ import { ApiPaths } from "@codemation/host";
3
3
  import httpProxy from "http-proxy";
4
4
  import type { Duplex } from "node:stream";
5
5
  import { WebSocket, WebSocketServer } from "ws";
6
+ import type { ListenPortConflictDescriber } from "./ListenPortConflictDescriber";
6
7
 
7
8
  type WorkflowClientMessage =
8
9
  | Readonly<{ kind: "subscribe"; roomId: string }>
@@ -29,7 +30,10 @@ export class CliDevProxyServer {
29
30
  private server: HttpServer | null = null;
30
31
  private uiProxyTarget: string | null = null;
31
32
 
32
- constructor(private readonly listenPort: number) {}
33
+ constructor(
34
+ private readonly listenPort: number,
35
+ private readonly listenPortConflictDescriber: ListenPortConflictDescriber,
36
+ ) {}
33
37
 
34
38
  async start(): Promise<void> {
35
39
  if (this.server) {
@@ -53,7 +57,9 @@ export class CliDevProxyServer {
53
57
  this.handleUpgrade(request, socket, head);
54
58
  });
55
59
  await new Promise<void>((resolve, reject) => {
56
- server.once("error", reject);
60
+ server.once("error", (error) => {
61
+ void this.rejectListenError(error, reject);
62
+ });
57
63
  server.listen(this.listenPort, "127.0.0.1", () => {
58
64
  resolve();
59
65
  });
@@ -147,7 +153,6 @@ export class CliDevProxyServer {
147
153
  private async handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
148
154
  const pathname = this.safePathname(req.url ?? "");
149
155
  const uiProxyTarget = this.uiProxyTarget;
150
- const activeRuntimeTarget = this.activeRuntime ? `http://127.0.0.1:${this.activeRuntime.httpPort}` : undefined;
151
156
  if (pathname === "/api/dev/health" && req.method === "GET") {
152
157
  res.writeHead(200, { "content-type": "application/json" });
153
158
  res.end(
@@ -160,17 +165,6 @@ export class CliDevProxyServer {
160
165
  );
161
166
  return;
162
167
  }
163
- if (pathname === "/api/dev/bootstrap-summary") {
164
- if (!activeRuntimeTarget) {
165
- res.writeHead(503, { "content-type": "text/plain" });
166
- res.end("Runtime is rebuilding.");
167
- return;
168
- }
169
- this.proxy.web(req, res, {
170
- target: activeRuntimeTarget,
171
- });
172
- return;
173
- }
174
168
  if (uiProxyTarget && pathname.startsWith("/api/auth/")) {
175
169
  this.proxy.web(req, res, {
176
170
  target: uiProxyTarget.replace(/\/$/, ""),
@@ -184,7 +178,7 @@ export class CliDevProxyServer {
184
178
  return;
185
179
  }
186
180
  this.proxy.web(req, res, {
187
- target: activeRuntimeTarget,
181
+ target: `http://127.0.0.1:${this.activeRuntime.httpPort}`,
188
182
  });
189
183
  return;
190
184
  }
@@ -230,6 +224,22 @@ export class CliDevProxyServer {
230
224
  }
231
225
  }
232
226
 
227
+ private async rejectListenError(error: unknown, reject: (reason?: unknown) => void): Promise<void> {
228
+ const errorWithCode = error as Error & Readonly<{ code?: unknown }>;
229
+ if (errorWithCode.code !== "EADDRINUSE") {
230
+ reject(error);
231
+ return;
232
+ }
233
+
234
+ const description = await this.listenPortConflictDescriber.describeLoopbackPort(this.listenPort);
235
+ const baseMessage = `Dev gateway port ${this.listenPort} is already in use on 127.0.0.1.`;
236
+ const suffix =
237
+ description === null
238
+ ? " Stop the process using that port or change the configured Codemation dev port."
239
+ : ` Listener: ${description}. Stop that process or change the configured Codemation dev port.`;
240
+ reject(new Error(`${baseMessage}${suffix}`, { cause: error instanceof Error ? error : undefined }));
241
+ }
242
+
233
243
  private broadcastDev(message: Readonly<Record<string, unknown>>): void {
234
244
  const text = JSON.stringify(message);
235
245
  for (const client of this.devClients) {
@@ -1,7 +1,10 @@
1
1
  import { CliDevProxyServer } from "./CliDevProxyServer";
2
+ import { ListenPortConflictDescriber } from "./ListenPortConflictDescriber";
2
3
 
3
4
  export class CliDevProxyServerFactory {
5
+ private readonly listenPortConflictDescriber = new ListenPortConflictDescriber();
6
+
4
7
  create(gatewayPort: number): CliDevProxyServer {
5
- return new CliDevProxyServer(gatewayPort);
8
+ return new CliDevProxyServer(gatewayPort, this.listenPortConflictDescriber);
6
9
  }
7
10
  }
@@ -0,0 +1,83 @@
1
+ import { execFile } from "node:child_process";
2
+ import process from "node:process";
3
+
4
+ type PortOccupant = Readonly<{
5
+ pid: number;
6
+ command: string;
7
+ endpoint: string;
8
+ }>;
9
+
10
+ export class ListenPortConflictDescriber {
11
+ constructor(private readonly platform: NodeJS.Platform = process.platform) {}
12
+
13
+ async describeLoopbackPort(port: number): Promise<string | null> {
14
+ if (!Number.isInteger(port) || port <= 0) {
15
+ return null;
16
+ }
17
+ if (this.platform !== "linux" && this.platform !== "darwin") {
18
+ return null;
19
+ }
20
+
21
+ const raw = await this.readLsofOutput(port);
22
+ if (raw === null) {
23
+ return null;
24
+ }
25
+ const occupants = this.parseLsofOutput(raw);
26
+ if (occupants.length === 0) {
27
+ return null;
28
+ }
29
+
30
+ return occupants
31
+ .map((occupant) => `pid=${occupant.pid} command=${occupant.command} endpoint=${occupant.endpoint}`)
32
+ .join("; ");
33
+ }
34
+
35
+ private async readLsofOutput(port: number): Promise<string | null> {
36
+ try {
37
+ return await new Promise<string>((resolve, reject) => {
38
+ execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fpcn"], (error, stdout) => {
39
+ if (error) {
40
+ reject(error);
41
+ return;
42
+ }
43
+ resolve(stdout);
44
+ });
45
+ });
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ private parseLsofOutput(raw: string): ReadonlyArray<PortOccupant> {
52
+ const occupants: PortOccupant[] = [];
53
+ let currentPid: number | null = null;
54
+ let currentCommand: string | null = null;
55
+
56
+ for (const line of raw.split("\n")) {
57
+ if (line.length < 2) {
58
+ continue;
59
+ }
60
+ const prefix = line[0];
61
+ const value = line.slice(1).trim();
62
+
63
+ if (prefix === "p") {
64
+ currentPid = Number.parseInt(value, 10);
65
+ currentCommand = null;
66
+ continue;
67
+ }
68
+ if (prefix === "c") {
69
+ currentCommand = value;
70
+ continue;
71
+ }
72
+ if (prefix === "n" && currentPid !== null && currentCommand !== null) {
73
+ occupants.push({
74
+ pid: currentPid,
75
+ command: currentCommand,
76
+ endpoint: value,
77
+ });
78
+ }
79
+ }
80
+
81
+ return occupants;
82
+ }
83
+ }
@@ -3,13 +3,6 @@ import process from "node:process";
3
3
 
4
4
  export class TypeScriptRuntimeConfigurator {
5
5
  configure(repoRoot: string): void {
6
- if (this.hasExplicitOverride()) {
7
- return;
8
- }
9
6
  process.env.CODEMATION_TSCONFIG_PATH = path.resolve(repoRoot, "tsconfig.base.json");
10
7
  }
11
-
12
- private hasExplicitOverride(): boolean {
13
- return typeof process.env.CODEMATION_TSCONFIG_PATH === "string" && process.env.CODEMATION_TSCONFIG_PATH.length > 0;
14
- }
15
8
  }