@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.
@@ -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
  }
@@ -4,13 +4,12 @@ import { CodemationConsumerConfigLoader } from "@codemation/host/server";
4
4
 
5
5
  import type { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
6
6
 
7
- export type DevResolvedAuthSettings = Readonly<{
8
- authConfigJson: string;
7
+ export type NextHostEdgeSeed = Readonly<{
9
8
  authSecret: string;
10
- skipUiAuth: boolean;
9
+ uiAuthEnabled: boolean;
11
10
  }>;
12
11
 
13
- export class DevAuthSettingsLoader {
12
+ export class NextHostEdgeSeedLoader {
14
13
  static readonly defaultDevelopmentAuthSecret = "codemation-dev-auth-secret-not-for-production";
15
14
 
16
15
  constructor(
@@ -25,13 +24,18 @@ export class DevAuthSettingsLoader {
25
24
  return randomUUID();
26
25
  }
27
26
 
28
- async loadForConsumer(consumerRoot: string): Promise<DevResolvedAuthSettings> {
29
- const resolution = await this.configLoader.load({ consumerRoot });
27
+ async loadForConsumer(
28
+ consumerRoot: string,
29
+ options?: Readonly<{ configPathOverride?: string }>,
30
+ ): Promise<NextHostEdgeSeed> {
31
+ const resolution = await this.configLoader.load({
32
+ consumerRoot,
33
+ configPathOverride: options?.configPathOverride,
34
+ });
30
35
  const envForAuthSecret = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(consumerRoot, process.env);
31
36
  return {
32
- authConfigJson: JSON.stringify(resolution.config.auth ?? null),
33
37
  authSecret: this.resolveDevelopmentAuthSecret(envForAuthSecret),
34
- skipUiAuth: resolution.config.auth?.allowUnauthenticatedInDevelopment === true,
38
+ uiAuthEnabled: resolution.config.auth?.allowUnauthenticatedInDevelopment !== true,
35
39
  };
36
40
  }
37
41
 
@@ -40,6 +44,6 @@ export class DevAuthSettingsLoader {
40
44
  if (configuredSecret && configuredSecret.trim().length > 0) {
41
45
  return configuredSecret;
42
46
  }
43
- return DevAuthSettingsLoader.defaultDevelopmentAuthSecret;
47
+ return NextHostEdgeSeedLoader.defaultDevelopmentAuthSecret;
44
48
  }
45
49
  }
@@ -0,0 +1,64 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export type PluginDevConfigBootstrap = Readonly<{
5
+ configPath: string;
6
+ }>;
7
+
8
+ export class PluginDevConfigFactory {
9
+ async prepare(pluginRoot: string): Promise<PluginDevConfigBootstrap> {
10
+ const pluginEntryPath = await this.resolvePluginEntryPath(pluginRoot);
11
+ const configPath = path.resolve(pluginRoot, ".codemation", "plugin-dev", "codemation.config.ts");
12
+ await mkdir(path.dirname(configPath), { recursive: true });
13
+ await writeFile(configPath, this.createConfigSource(configPath, pluginEntryPath), "utf8");
14
+ return {
15
+ configPath,
16
+ };
17
+ }
18
+
19
+ private async resolvePluginEntryPath(pluginRoot: string): Promise<string> {
20
+ const candidates = [
21
+ path.resolve(pluginRoot, "codemation.plugin.ts"),
22
+ path.resolve(pluginRoot, "codemation.plugin.js"),
23
+ path.resolve(pluginRoot, "src", "codemation.plugin.ts"),
24
+ path.resolve(pluginRoot, "src", "codemation.plugin.js"),
25
+ ];
26
+ for (const candidate of candidates) {
27
+ if (await this.exists(candidate)) {
28
+ return candidate;
29
+ }
30
+ }
31
+ throw new Error('Plugin config not found. Expected "codemation.plugin.ts" in the plugin root or "src/".');
32
+ }
33
+
34
+ private createConfigSource(configPath: string, pluginEntryPath: string): string {
35
+ const relativeImportPath = this.toRelativeImportPath(configPath, pluginEntryPath);
36
+ return [
37
+ 'import type { CodemationConfig } from "@codemation/host";',
38
+ `import plugin from ${JSON.stringify(relativeImportPath)};`,
39
+ "",
40
+ "const sandbox = plugin.sandbox ?? {};",
41
+ "const config: CodemationConfig = {",
42
+ " ...sandbox,",
43
+ " plugins: [...(sandbox.plugins ?? []), plugin],",
44
+ "};",
45
+ "",
46
+ "export default config;",
47
+ "",
48
+ ].join("\n");
49
+ }
50
+
51
+ private toRelativeImportPath(fromPath: string, targetPath: string): string {
52
+ const relativePath = path.relative(path.dirname(fromPath), targetPath).replace(/\\/g, "/");
53
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
54
+ }
55
+
56
+ private async exists(filePath: string): Promise<boolean> {
57
+ try {
58
+ await access(filePath);
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ }
@@ -1,8 +0,0 @@
1
- import type { ConsumerBuildOptions } from "./consumerBuildOptions.types";
2
- import { ConsumerOutputBuilder } from "./ConsumerOutputBuilder";
3
-
4
- export class ConsumerOutputBuilderLoader {
5
- create(consumerRoot: string, buildOptions: ConsumerBuildOptions): ConsumerOutputBuilder {
6
- return new ConsumerOutputBuilder(consumerRoot, undefined, buildOptions);
7
- }
8
- }
@@ -1,30 +0,0 @@
1
- import type { Logger } from "@codemation/host/next/server";
2
- import { CodemationPluginDiscovery } from "@codemation/host/server";
3
-
4
- import { ConsumerBuildArtifactsPublisher } from "../build/ConsumerBuildArtifactsPublisher";
5
- import { ConsumerBuildOptionsParser } from "../build/ConsumerBuildOptionsParser";
6
- import { ConsumerOutputBuilderLoader } from "../consumer/Loader";
7
- import type { CliPaths } from "../path/CliPathResolver";
8
-
9
- /**
10
- * Ensures `.codemation/output/current.json` and transpiled consumer config exist before the Next host boots.
11
- * Without this, `codemation dev` can serve a stale built `codemation.config.js` (e.g. missing whitelabel).
12
- */
13
- export class DevConsumerPublishBootstrap {
14
- constructor(
15
- private readonly cliLogger: Logger,
16
- private readonly pluginDiscovery: CodemationPluginDiscovery,
17
- private readonly artifactsPublisher: ConsumerBuildArtifactsPublisher,
18
- private readonly outputBuilderLoader: ConsumerOutputBuilderLoader,
19
- private readonly buildOptionsParser: ConsumerBuildOptionsParser,
20
- ) {}
21
-
22
- async ensurePublished(paths: CliPaths): Promise<void> {
23
- const buildOptions = this.buildOptionsParser.parse({});
24
- const builder = this.outputBuilderLoader.create(paths.consumerRoot, buildOptions);
25
- const snapshot = await builder.ensureBuilt();
26
- const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
27
- await this.artifactsPublisher.publish(snapshot, discoveredPlugins);
28
- this.cliLogger.debug(`Dev: consumer output published (${snapshot.buildVersion}).`);
29
- }
30
- }