@codemation/cli 0.0.5 → 0.0.7
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/README.md +20 -26
- package/dist/{CliBin-C3ar49fj.js → CliBin-Bx1lFBi5.js} +1041 -365
- package/dist/bin.js +1 -1
- package/dist/index.d.ts +644 -207
- package/dist/index.js +1 -1
- package/package.json +9 -6
- package/src/CliProgramFactory.ts +23 -8
- package/src/Program.ts +7 -3
- package/src/bootstrap/CodemationCliApplicationSession.ts +17 -19
- package/src/commands/DevCommand.ts +202 -171
- package/src/commands/ServeWebCommand.ts +26 -1
- package/src/commands/ServeWorkerCommand.ts +46 -30
- package/src/commands/devCommandLifecycle.types.ts +7 -11
- package/src/database/ConsumerDatabaseConnectionResolver.ts +55 -9
- package/src/database/DatabaseMigrationsApplyService.ts +2 -2
- package/src/dev/Builder.ts +1 -14
- package/src/dev/CliDevProxyServer.ts +447 -0
- package/src/dev/CliDevProxyServerFactory.ts +7 -0
- package/src/dev/DevApiRuntimeFactory.ts +44 -0
- package/src/dev/DevApiRuntimeHost.ts +130 -0
- package/src/dev/DevApiRuntimeServer.ts +107 -0
- package/src/dev/DevApiRuntimeTypes.ts +24 -0
- package/src/dev/DevAuthSettingsLoader.ts +9 -3
- package/src/dev/DevBootstrapSummaryFetcher.ts +1 -1
- package/src/dev/DevHttpProbe.ts +2 -2
- package/src/dev/DevNextHostEnvironmentBuilder.ts +35 -5
- package/src/dev/DevRebuildQueue.ts +54 -0
- package/src/dev/DevRebuildQueueFactory.ts +7 -0
- package/src/dev/DevSessionPortsResolver.ts +2 -2
- package/src/dev/DevSessionServices.ts +0 -4
- package/src/dev/DevSourceChangeClassifier.ts +33 -13
- package/src/dev/WatchRootsResolver.ts +6 -4
- package/src/runtime/NextHostConsumerServerCommandFactory.ts +11 -2
- package/src/runtime/TypeScriptRuntimeConfigurator.ts +7 -0
- package/src/user/CliDatabaseUrlDescriptor.ts +2 -2
- package/src/user/UserAdminCliBootstrap.ts +9 -21
- package/codemation-cli-0.0.3.tgz +0 -0
- package/src/dev/DevSourceRestartCoordinator.ts +0 -48
- package/src/dev/DevelopmentGatewayNotifier.ts +0 -35
- package/src/dev/RuntimeToolEntrypointResolver.ts +0 -47
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { CodemationHonoApiApp, ServerLoggerFactory, logLevelPolicyFactory } from "@codemation/host/next/server";
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
|
|
5
|
+
import type { DevApiRuntimeContext } from "./DevApiRuntimeTypes";
|
|
6
|
+
import type { DevApiRuntimeHost } from "./DevApiRuntimeHost";
|
|
7
|
+
|
|
8
|
+
type ClosableServer = Readonly<{
|
|
9
|
+
close(callback?: (error?: Error) => void): unknown;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export class DevApiRuntimeServer {
|
|
13
|
+
private readonly bootstrapLogger = new ServerLoggerFactory(logLevelPolicyFactory).create(
|
|
14
|
+
"codemation-cli.dev-runtime",
|
|
15
|
+
);
|
|
16
|
+
private server: ClosableServer | null = null;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly httpPort: number,
|
|
20
|
+
private readonly workflowWebSocketPort: number,
|
|
21
|
+
private readonly host: DevApiRuntimeHost,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
async start(): Promise<DevApiRuntimeContext> {
|
|
25
|
+
const root = new Hono();
|
|
26
|
+
root.get("/health", (c) => c.json({ ok: true }));
|
|
27
|
+
root.all("*", async (c) => {
|
|
28
|
+
const context = await this.host.prepare();
|
|
29
|
+
return context.container.resolve(CodemationHonoApiApp).fetch(c.req.raw);
|
|
30
|
+
});
|
|
31
|
+
await this.listen(root);
|
|
32
|
+
try {
|
|
33
|
+
return await this.host.prepare();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
await this.stop();
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async stop(): Promise<void> {
|
|
41
|
+
const server = this.server;
|
|
42
|
+
this.server = null;
|
|
43
|
+
const failures: Error[] = [];
|
|
44
|
+
if (server) {
|
|
45
|
+
try {
|
|
46
|
+
await this.closeServer(server);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
failures.push(this.normalizeError(error));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await this.host.stop();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
failures.push(this.normalizeError(error));
|
|
55
|
+
}
|
|
56
|
+
if (failures.length > 0) {
|
|
57
|
+
throw failures[0];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async listen(root: Hono): Promise<void> {
|
|
62
|
+
if (this.server) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await new Promise<void>((resolve, reject) => {
|
|
66
|
+
let resolved = false;
|
|
67
|
+
const server = serve(
|
|
68
|
+
{
|
|
69
|
+
fetch: root.fetch,
|
|
70
|
+
port: this.httpPort,
|
|
71
|
+
hostname: "127.0.0.1",
|
|
72
|
+
},
|
|
73
|
+
() => {
|
|
74
|
+
resolved = true;
|
|
75
|
+
this.server = server;
|
|
76
|
+
resolve();
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
server.on("error", (error) => {
|
|
80
|
+
if (resolved) {
|
|
81
|
+
this.bootstrapLogger.error("runtime HTTP server error", this.normalizeError(error));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
reject(error);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
this.bootstrapLogger.debug(
|
|
88
|
+
`runtime listening httpPort=${this.httpPort} workflowWebSocketPort=${this.workflowWebSocketPort}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private closeServer(server: ClosableServer): Promise<void> {
|
|
93
|
+
return new Promise<void>((resolve, reject) => {
|
|
94
|
+
server.close((error) => {
|
|
95
|
+
if (error) {
|
|
96
|
+
reject(error);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private normalizeError(error: unknown): Error {
|
|
105
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Container } from "@codemation/core";
|
|
2
|
+
|
|
3
|
+
export type DevApiRuntimeContext = Readonly<{
|
|
4
|
+
buildVersion: string;
|
|
5
|
+
container: Container;
|
|
6
|
+
consumerRoot: string;
|
|
7
|
+
repoRoot: string;
|
|
8
|
+
workflowIds: ReadonlyArray<string>;
|
|
9
|
+
workflowSources: ReadonlyArray<string>;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export type DevApiRuntimeFactoryArgs = Readonly<{
|
|
13
|
+
consumerRoot: string;
|
|
14
|
+
env: NodeJS.ProcessEnv;
|
|
15
|
+
runtimeWorkingDirectory: string;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type DevApiRuntimeServerHandle = Readonly<{
|
|
19
|
+
buildVersion: string;
|
|
20
|
+
httpPort: number;
|
|
21
|
+
stop: () => Promise<void>;
|
|
22
|
+
workflowIds: ReadonlyArray<string>;
|
|
23
|
+
workflowWebSocketPort: number;
|
|
24
|
+
}>;
|
|
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { CodemationConsumerConfigLoader } from "@codemation/host/server";
|
|
4
4
|
|
|
5
|
+
import type { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
|
|
6
|
+
|
|
5
7
|
export type DevResolvedAuthSettings = Readonly<{
|
|
6
8
|
authConfigJson: string;
|
|
7
9
|
authSecret: string;
|
|
@@ -11,7 +13,10 @@ export type DevResolvedAuthSettings = Readonly<{
|
|
|
11
13
|
export class DevAuthSettingsLoader {
|
|
12
14
|
static readonly defaultDevelopmentAuthSecret = "codemation-dev-auth-secret-not-for-production";
|
|
13
15
|
|
|
14
|
-
constructor(
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly configLoader: CodemationConsumerConfigLoader,
|
|
18
|
+
private readonly consumerEnvLoader: ConsumerEnvLoader,
|
|
19
|
+
) {}
|
|
15
20
|
|
|
16
21
|
resolveDevelopmentServerToken(rawToken: string | undefined): string {
|
|
17
22
|
if (rawToken && rawToken.trim().length > 0) {
|
|
@@ -22,15 +27,16 @@ export class DevAuthSettingsLoader {
|
|
|
22
27
|
|
|
23
28
|
async loadForConsumer(consumerRoot: string): Promise<DevResolvedAuthSettings> {
|
|
24
29
|
const resolution = await this.configLoader.load({ consumerRoot });
|
|
30
|
+
const envForAuthSecret = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(consumerRoot, process.env);
|
|
25
31
|
return {
|
|
26
32
|
authConfigJson: JSON.stringify(resolution.config.auth ?? null),
|
|
27
|
-
authSecret: this.resolveDevelopmentAuthSecret(
|
|
33
|
+
authSecret: this.resolveDevelopmentAuthSecret(envForAuthSecret),
|
|
28
34
|
skipUiAuth: resolution.config.auth?.allowUnauthenticatedInDevelopment === true,
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
resolveDevelopmentAuthSecret(env: NodeJS.ProcessEnv): string {
|
|
33
|
-
const configuredSecret = env.AUTH_SECRET
|
|
39
|
+
const configuredSecret = env.AUTH_SECRET;
|
|
34
40
|
if (configuredSecret && configuredSecret.trim().length > 0) {
|
|
35
41
|
return configuredSecret;
|
|
36
42
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DevBootstrapSummaryJson } from "@codemation/host/next/server";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Fetches {@link DevBootstrapSummaryJson} from the
|
|
4
|
+
* Fetches {@link DevBootstrapSummaryJson} from the stable CLI-owned dev endpoint.
|
|
5
5
|
*/
|
|
6
6
|
export class DevBootstrapSummaryFetcher {
|
|
7
7
|
async fetch(gatewayBaseUrl: string): Promise<DevBootstrapSummaryJson | null> {
|
package/src/dev/DevHttpProbe.ts
CHANGED
|
@@ -29,11 +29,11 @@ export class DevHttpProbe {
|
|
|
29
29
|
}
|
|
30
30
|
await delay(50);
|
|
31
31
|
}
|
|
32
|
-
throw new Error("Timed out waiting for dev
|
|
32
|
+
throw new Error("Timed out waiting for the stable dev HTTP health check.");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Polls until the runtime
|
|
36
|
+
* Polls until the active disposable runtime serves bootstrap summary through the stable CLI dev endpoint.
|
|
37
37
|
*/
|
|
38
38
|
async waitUntilBootstrapSummaryReady(gatewayBaseUrl: string): Promise<void> {
|
|
39
39
|
const normalizedBase = gatewayBaseUrl.replace(/\/$/, "");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import process from "node:process";
|
|
3
|
+
import type { CodemationAuthConfig } from "@codemation/host";
|
|
4
|
+
import { CodemationFrontendAuthSnapshotFactory, FrontendAppConfigJsonCodec } from "@codemation/host";
|
|
3
5
|
|
|
4
6
|
import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
|
|
5
7
|
import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
|
|
@@ -8,6 +10,8 @@ export class DevNextHostEnvironmentBuilder {
|
|
|
8
10
|
constructor(
|
|
9
11
|
private readonly consumerEnvLoader: ConsumerEnvLoader,
|
|
10
12
|
private readonly sourceMapNodeOptions: SourceMapNodeOptions,
|
|
13
|
+
private readonly frontendAuthSnapshotFactory: CodemationFrontendAuthSnapshotFactory = new CodemationFrontendAuthSnapshotFactory(),
|
|
14
|
+
private readonly frontendAppConfigJsonCodec: FrontendAppConfigJsonCodec = new FrontendAppConfigJsonCodec(),
|
|
11
15
|
) {}
|
|
12
16
|
|
|
13
17
|
buildConsumerUiProxy(
|
|
@@ -27,6 +31,7 @@ export class DevNextHostEnvironmentBuilder {
|
|
|
27
31
|
return {
|
|
28
32
|
...this.build({
|
|
29
33
|
authConfigJson: args.authConfigJson,
|
|
34
|
+
authSecret: args.authSecret,
|
|
30
35
|
consumerRoot: args.consumerRoot,
|
|
31
36
|
developmentServerToken: args.developmentServerToken,
|
|
32
37
|
nextPort: args.nextPort,
|
|
@@ -35,16 +40,18 @@ export class DevNextHostEnvironmentBuilder {
|
|
|
35
40
|
websocketPort: args.websocketPort,
|
|
36
41
|
consumerOutputManifestPath: args.consumerOutputManifestPath,
|
|
37
42
|
}),
|
|
43
|
+
// Standalone `server.js` uses `process.env.HOSTNAME || '0.0.0.0'` for bind; Docker sets HOSTNAME to the
|
|
44
|
+
// container id, which breaks loopback health checks — force IPv4 loopback for the UI child only.
|
|
45
|
+
HOSTNAME: "127.0.0.1",
|
|
38
46
|
AUTH_SECRET: args.authSecret,
|
|
39
47
|
AUTH_URL: args.publicBaseUrl,
|
|
40
|
-
NEXTAUTH_SECRET: args.authSecret,
|
|
41
|
-
NEXTAUTH_URL: args.publicBaseUrl,
|
|
42
48
|
};
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
build(
|
|
46
52
|
args: Readonly<{
|
|
47
53
|
authConfigJson: string;
|
|
54
|
+
authSecret?: string;
|
|
48
55
|
consumerRoot: string;
|
|
49
56
|
developmentServerToken: string;
|
|
50
57
|
nextPort: number;
|
|
@@ -58,14 +65,29 @@ export class DevNextHostEnvironmentBuilder {
|
|
|
58
65
|
const merged = this.consumerEnvLoader.mergeConsumerRootIntoProcessEnvironment(args.consumerRoot, process.env);
|
|
59
66
|
const manifestPath =
|
|
60
67
|
args.consumerOutputManifestPath ?? path.resolve(args.consumerRoot, ".codemation", "output", "current.json");
|
|
68
|
+
const authSecret = args.authSecret ?? merged.AUTH_SECRET;
|
|
69
|
+
const authSnapshot = this.frontendAuthSnapshotFactory.createFromResolvedInputs({
|
|
70
|
+
authConfig: this.parseAuthConfig(args.authConfigJson),
|
|
71
|
+
env: {
|
|
72
|
+
...merged,
|
|
73
|
+
...(typeof authSecret === "string" && authSecret.trim().length > 0
|
|
74
|
+
? {
|
|
75
|
+
AUTH_SECRET: authSecret,
|
|
76
|
+
}
|
|
77
|
+
: {}),
|
|
78
|
+
},
|
|
79
|
+
uiAuthEnabled: !args.skipUiAuth,
|
|
80
|
+
});
|
|
61
81
|
return {
|
|
62
82
|
...merged,
|
|
63
83
|
PORT: String(args.nextPort),
|
|
64
|
-
CODEMATION_AUTH_CONFIG_JSON: args.authConfigJson,
|
|
65
84
|
CODEMATION_CONSUMER_ROOT: args.consumerRoot,
|
|
66
85
|
CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifestPath,
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
CODEMATION_FRONTEND_APP_CONFIG_JSON: this.frontendAppConfigJsonCodec.serialize({
|
|
87
|
+
auth: authSnapshot,
|
|
88
|
+
productName: "Codemation",
|
|
89
|
+
logoUrl: null,
|
|
90
|
+
}),
|
|
69
91
|
CODEMATION_WS_PORT: String(args.websocketPort),
|
|
70
92
|
NEXT_PUBLIC_CODEMATION_WS_PORT: String(args.websocketPort),
|
|
71
93
|
CODEMATION_DEV_SERVER_TOKEN: args.developmentServerToken,
|
|
@@ -78,4 +100,12 @@ export class DevNextHostEnvironmentBuilder {
|
|
|
78
100
|
: {}),
|
|
79
101
|
};
|
|
80
102
|
}
|
|
103
|
+
|
|
104
|
+
private parseAuthConfig(authConfigJson: string): CodemationAuthConfig | undefined {
|
|
105
|
+
if (authConfigJson.trim().length === 0) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const parsed = JSON.parse(authConfigJson) as CodemationAuthConfig | null;
|
|
109
|
+
return parsed ?? undefined;
|
|
110
|
+
}
|
|
81
111
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type DevRebuildRequest = Readonly<{
|
|
2
|
+
changedPaths: ReadonlyArray<string>;
|
|
3
|
+
shouldRepublishConsumerOutput: boolean;
|
|
4
|
+
shouldRestartUi: boolean;
|
|
5
|
+
}>;
|
|
6
|
+
|
|
7
|
+
export interface DevRebuildHandler {
|
|
8
|
+
run(request: DevRebuildRequest): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class DevRebuildQueue {
|
|
12
|
+
private pendingRequest: DevRebuildRequest | null = null;
|
|
13
|
+
private drainPromise: Promise<void> | null = null;
|
|
14
|
+
|
|
15
|
+
constructor(private readonly handler: DevRebuildHandler) {}
|
|
16
|
+
|
|
17
|
+
async enqueue(request: DevRebuildRequest): Promise<void> {
|
|
18
|
+
this.pendingRequest = this.mergePendingRequest(this.pendingRequest, request);
|
|
19
|
+
if (!this.drainPromise) {
|
|
20
|
+
this.drainPromise = this.drain();
|
|
21
|
+
}
|
|
22
|
+
return await this.drainPromise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async drain(): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
while (this.pendingRequest) {
|
|
28
|
+
const nextRequest = this.pendingRequest;
|
|
29
|
+
this.pendingRequest = null;
|
|
30
|
+
await this.handler.run(nextRequest);
|
|
31
|
+
}
|
|
32
|
+
} finally {
|
|
33
|
+
this.drainPromise = null;
|
|
34
|
+
if (this.pendingRequest) {
|
|
35
|
+
this.drainPromise = this.drain();
|
|
36
|
+
await this.drainPromise;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private mergePendingRequest(current: DevRebuildRequest | null, next: DevRebuildRequest): DevRebuildRequest {
|
|
42
|
+
if (!current) {
|
|
43
|
+
return {
|
|
44
|
+
...next,
|
|
45
|
+
changedPaths: [...next.changedPaths],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
changedPaths: [...new Set([...current.changedPaths, ...next.changedPaths])],
|
|
50
|
+
shouldRepublishConsumerOutput: current.shouldRepublishConsumerOutput || next.shouldRepublishConsumerOutput,
|
|
51
|
+
shouldRestartUi: current.shouldRestartUi || next.shouldRestartUi,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -9,7 +9,7 @@ export class DevSessionPortsResolver {
|
|
|
9
9
|
|
|
10
10
|
async resolve(
|
|
11
11
|
args: Readonly<{
|
|
12
|
-
devMode: "
|
|
12
|
+
devMode: "packaged-ui" | "watch-framework";
|
|
13
13
|
portEnv: string | undefined;
|
|
14
14
|
gatewayPortEnv: string | undefined;
|
|
15
15
|
}>,
|
|
@@ -17,7 +17,7 @@ export class DevSessionPortsResolver {
|
|
|
17
17
|
const nextPort = this.listenPorts.resolvePrimaryApplicationPort(args.portEnv);
|
|
18
18
|
const gatewayPort =
|
|
19
19
|
this.listenPorts.parsePositiveInteger(args.gatewayPortEnv) ??
|
|
20
|
-
(args.devMode === "
|
|
20
|
+
(args.devMode === "packaged-ui" ? nextPort : await this.loopbackPorts.allocate());
|
|
21
21
|
return { nextPort, gatewayPort };
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -6,9 +6,7 @@ import { DevHttpProbe } from "./DevHttpProbe";
|
|
|
6
6
|
import { DevNextHostEnvironmentBuilder } from "./DevNextHostEnvironmentBuilder";
|
|
7
7
|
import { DevSessionPortsResolver } from "./DevSessionPortsResolver";
|
|
8
8
|
import { DevSourceChangeClassifier } from "./DevSourceChangeClassifier";
|
|
9
|
-
import { DevSourceRestartCoordinator } from "./DevSourceRestartCoordinator";
|
|
10
9
|
import { LoopbackPortAllocator } from "./LoopbackPortAllocator";
|
|
11
|
-
import { RuntimeToolEntrypointResolver } from "./RuntimeToolEntrypointResolver";
|
|
12
10
|
import { WatchRootsResolver } from "./WatchRootsResolver";
|
|
13
11
|
|
|
14
12
|
/**
|
|
@@ -21,11 +19,9 @@ export class DevSessionServices {
|
|
|
21
19
|
readonly sessionPorts: DevSessionPortsResolver,
|
|
22
20
|
readonly loopbackPortAllocator: LoopbackPortAllocator,
|
|
23
21
|
readonly devHttpProbe: DevHttpProbe,
|
|
24
|
-
readonly runtimeEntrypointResolver: RuntimeToolEntrypointResolver,
|
|
25
22
|
readonly devAuthLoader: DevAuthSettingsLoader,
|
|
26
23
|
readonly nextHostEnvBuilder: DevNextHostEnvironmentBuilder,
|
|
27
24
|
readonly watchRootsResolver: WatchRootsResolver,
|
|
28
25
|
readonly sourceChangeClassifier: DevSourceChangeClassifier,
|
|
29
|
-
readonly sourceRestartCoordinator: DevSourceRestartCoordinator,
|
|
30
26
|
) {}
|
|
31
27
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
export class DevSourceChangeClassifier {
|
|
4
|
+
private static readonly configFileNames = new Set([
|
|
5
|
+
"codemation.config.ts",
|
|
6
|
+
"codemation.config.js",
|
|
7
|
+
"codemation.config.mjs",
|
|
8
|
+
]);
|
|
9
|
+
|
|
4
10
|
shouldRepublishConsumerOutput(
|
|
5
11
|
args: Readonly<{
|
|
6
12
|
changedPaths: ReadonlyArray<string>;
|
|
@@ -11,24 +17,16 @@ export class DevSourceChangeClassifier {
|
|
|
11
17
|
return args.changedPaths.some((changedPath) => this.isPathInsideDirectory(changedPath, resolvedConsumerRoot));
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
requiresUiRestart(
|
|
15
21
|
args: Readonly<{
|
|
16
22
|
changedPaths: ReadonlyArray<string>;
|
|
17
23
|
consumerRoot: string;
|
|
18
24
|
}>,
|
|
19
25
|
): boolean {
|
|
20
|
-
const
|
|
21
|
-
return args.changedPaths.some((changedPath) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
private resolveConfigPaths(consumerRoot: string): ReadonlyArray<string> {
|
|
25
|
-
const resolvedConsumerRoot = path.resolve(consumerRoot);
|
|
26
|
-
return [
|
|
27
|
-
path.resolve(resolvedConsumerRoot, "codemation.config.ts"),
|
|
28
|
-
path.resolve(resolvedConsumerRoot, "codemation.config.js"),
|
|
29
|
-
path.resolve(resolvedConsumerRoot, "src", "codemation.config.ts"),
|
|
30
|
-
path.resolve(resolvedConsumerRoot, "src", "codemation.config.js"),
|
|
31
|
-
];
|
|
26
|
+
const resolvedConsumerRoot = path.resolve(args.consumerRoot);
|
|
27
|
+
return args.changedPaths.some((changedPath) =>
|
|
28
|
+
this.pathRequiresUiRestart(path.resolve(changedPath), resolvedConsumerRoot),
|
|
29
|
+
);
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
private isPathInsideDirectory(filePath: string, directoryPath: string): boolean {
|
|
@@ -36,4 +34,26 @@ export class DevSourceChangeClassifier {
|
|
|
36
34
|
const relativePath = path.relative(directoryPath, resolvedFilePath);
|
|
37
35
|
return relativePath.length === 0 || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
38
36
|
}
|
|
37
|
+
|
|
38
|
+
private pathRequiresUiRestart(resolvedPath: string, consumerRoot: string): boolean {
|
|
39
|
+
if (!this.isPathInsideDirectory(resolvedPath, consumerRoot)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const relativePath = path.relative(consumerRoot, resolvedPath);
|
|
43
|
+
if (DevSourceChangeClassifier.configFileNames.has(path.basename(relativePath))) {
|
|
44
|
+
// Config changes affect auth and branding projections consumed by the packaged Next host.
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (relativePath.startsWith(path.join("src", "workflows"))) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (relativePath.startsWith(path.join("src", "plugins"))) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (relativePath.includes("credential")) {
|
|
54
|
+
// Credential type edits still require a packaged UI restart until the credentials screen live-refreshes.
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
39
59
|
}
|
|
@@ -4,23 +4,25 @@ export class WatchRootsResolver {
|
|
|
4
4
|
resolve(
|
|
5
5
|
args: Readonly<{
|
|
6
6
|
consumerRoot: string;
|
|
7
|
-
devMode: "
|
|
7
|
+
devMode: "packaged-ui" | "watch-framework";
|
|
8
8
|
repoRoot: string;
|
|
9
9
|
}>,
|
|
10
10
|
): ReadonlyArray<string> {
|
|
11
|
-
if (args.devMode === "
|
|
11
|
+
if (args.devMode === "packaged-ui") {
|
|
12
|
+
// Packaged UI mode watches only the app itself. Framework packages are consumed from their built output.
|
|
12
13
|
return [args.consumerRoot];
|
|
13
14
|
}
|
|
15
|
+
// Watch-framework mode is framework-author development: watch the app plus the workspace packages that
|
|
16
|
+
// feed the consumer output and packaged Next host.
|
|
14
17
|
return [
|
|
15
18
|
args.consumerRoot,
|
|
19
|
+
path.resolve(args.repoRoot, "packages", "cli"),
|
|
16
20
|
path.resolve(args.repoRoot, "packages", "core"),
|
|
17
21
|
path.resolve(args.repoRoot, "packages", "core-nodes"),
|
|
18
22
|
path.resolve(args.repoRoot, "packages", "core-nodes-gmail"),
|
|
19
23
|
path.resolve(args.repoRoot, "packages", "eventbus-redis"),
|
|
20
24
|
path.resolve(args.repoRoot, "packages", "host"),
|
|
21
25
|
path.resolve(args.repoRoot, "packages", "node-example"),
|
|
22
|
-
path.resolve(args.repoRoot, "packages", "queue-bullmq"),
|
|
23
|
-
path.resolve(args.repoRoot, "packages", "runtime-dev"),
|
|
24
26
|
];
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -10,7 +10,14 @@ export type NextHostConsumerServerCommand = Readonly<{
|
|
|
10
10
|
|
|
11
11
|
export class NextHostConsumerServerCommandFactory {
|
|
12
12
|
async create(args: Readonly<{ nextHostRoot: string }>): Promise<NextHostConsumerServerCommand> {
|
|
13
|
-
const standaloneServerPath = path.resolve(
|
|
13
|
+
const standaloneServerPath = path.resolve(
|
|
14
|
+
args.nextHostRoot,
|
|
15
|
+
".next",
|
|
16
|
+
"standalone",
|
|
17
|
+
"packages",
|
|
18
|
+
"next-host",
|
|
19
|
+
"server.js",
|
|
20
|
+
);
|
|
14
21
|
if (await this.exists(standaloneServerPath)) {
|
|
15
22
|
return {
|
|
16
23
|
command: process.execPath,
|
|
@@ -20,7 +27,9 @@ export class NextHostConsumerServerCommandFactory {
|
|
|
20
27
|
}
|
|
21
28
|
return {
|
|
22
29
|
command: "pnpm",
|
|
23
|
-
|
|
30
|
+
// Bind loopback so `codemation dev` health probes (`http://127.0.0.1:<port>`) work in Docker,
|
|
31
|
+
// where `process.env.HOSTNAME` is the container id and would otherwise be used by `next start`.
|
|
32
|
+
args: ["exec", "next", "start", "-H", "127.0.0.1"],
|
|
24
33
|
cwd: args.nextHostRoot,
|
|
25
34
|
};
|
|
26
35
|
}
|
|
@@ -3,6 +3,13 @@ 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
|
+
}
|
|
6
9
|
process.env.CODEMATION_TSCONFIG_PATH = path.resolve(repoRoot, "tsconfig.base.json");
|
|
7
10
|
}
|
|
11
|
+
|
|
12
|
+
private hasExplicitOverride(): boolean {
|
|
13
|
+
return typeof process.env.CODEMATION_TSCONFIG_PATH === "string" && process.env.CODEMATION_TSCONFIG_PATH.length > 0;
|
|
14
|
+
}
|
|
8
15
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AppPersistenceConfig } from "@codemation/host/persistence";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Formats a database URL for CLI messages without exposing credentials (no user/password).
|
|
5
5
|
*/
|
|
6
6
|
export class CliDatabaseUrlDescriptor {
|
|
7
|
-
describePersistence(persistence:
|
|
7
|
+
describePersistence(persistence: AppPersistenceConfig): string {
|
|
8
8
|
if (persistence.kind === "none") {
|
|
9
9
|
return "none";
|
|
10
10
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { DatabasePersistenceResolver } from "@codemation/host/persistence";
|
|
3
|
-
import { CodemationConsumerConfigLoader } from "@codemation/host/server";
|
|
1
|
+
import { AppConfigLoader } from "@codemation/host/server";
|
|
4
2
|
|
|
5
3
|
import { CodemationCliApplicationSession } from "../bootstrap/CodemationCliApplicationSession";
|
|
6
4
|
import type { ConsumerCliTsconfigPreparation } from "../consumer/ConsumerCliTsconfigPreparation";
|
|
@@ -17,11 +15,10 @@ export type UserAdminCliOptions = Readonly<{
|
|
|
17
15
|
*/
|
|
18
16
|
export class UserAdminCliBootstrap {
|
|
19
17
|
constructor(
|
|
20
|
-
private readonly
|
|
18
|
+
private readonly appConfigLoader: AppConfigLoader,
|
|
21
19
|
private readonly pathResolver: CliPathResolver,
|
|
22
20
|
private readonly consumerDotenvLoader: UserAdminConsumerDotenvLoader,
|
|
23
21
|
private readonly tsconfigPreparation: ConsumerCliTsconfigPreparation,
|
|
24
|
-
private readonly databasePersistenceResolver: DatabasePersistenceResolver,
|
|
25
22
|
) {}
|
|
26
23
|
|
|
27
24
|
async withSession<T>(
|
|
@@ -31,32 +28,23 @@ export class UserAdminCliBootstrap {
|
|
|
31
28
|
const consumerRoot = options.consumerRoot ?? process.cwd();
|
|
32
29
|
this.consumerDotenvLoader.load(consumerRoot);
|
|
33
30
|
this.tsconfigPreparation.applyWorkspaceTsconfigForTsxIfPresent(consumerRoot);
|
|
34
|
-
const
|
|
31
|
+
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
32
|
+
const loadResult = await this.appConfigLoader.load({
|
|
35
33
|
consumerRoot,
|
|
34
|
+
repoRoot: paths.repoRoot,
|
|
35
|
+
env: process.env,
|
|
36
36
|
configPathOverride: options.configPath,
|
|
37
37
|
});
|
|
38
|
-
if (
|
|
38
|
+
if (loadResult.appConfig.auth?.kind !== "local") {
|
|
39
39
|
throw new Error('Codemation user commands require CodemationConfig.auth.kind to be "local".');
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
runtimeConfig: resolution.config.runtime ?? {},
|
|
43
|
-
env: process.env,
|
|
44
|
-
consumerRoot,
|
|
45
|
-
});
|
|
46
|
-
if (persistence.kind === "none") {
|
|
41
|
+
if (loadResult.appConfig.persistence.kind === "none") {
|
|
47
42
|
throw new Error(
|
|
48
43
|
"Database persistence is not configured. Set CodemationConfig.runtime.database (postgresql URL or PGlite).",
|
|
49
44
|
);
|
|
50
45
|
}
|
|
51
|
-
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
52
46
|
const session = await CodemationCliApplicationSession.open({
|
|
53
|
-
|
|
54
|
-
bootstrap: new CodemationBootstrapRequest({
|
|
55
|
-
repoRoot: paths.repoRoot,
|
|
56
|
-
consumerRoot,
|
|
57
|
-
env: process.env,
|
|
58
|
-
workflowSources: resolution.workflowSources,
|
|
59
|
-
}),
|
|
47
|
+
appConfig: loadResult.appConfig,
|
|
60
48
|
});
|
|
61
49
|
try {
|
|
62
50
|
return await fn(session);
|
package/codemation-cli-0.0.3.tgz
DELETED
|
Binary file
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { Logger } from "@codemation/host/next/server";
|
|
2
|
-
import process from "node:process";
|
|
3
|
-
|
|
4
|
-
import { DevelopmentGatewayNotifier } from "./DevelopmentGatewayNotifier";
|
|
5
|
-
|
|
6
|
-
export class DevSourceRestartCoordinator {
|
|
7
|
-
constructor(
|
|
8
|
-
private readonly gatewayNotifier: DevelopmentGatewayNotifier,
|
|
9
|
-
private readonly performanceDiagnosticsLogger: Logger,
|
|
10
|
-
private readonly cliLogger: Logger,
|
|
11
|
-
) {}
|
|
12
|
-
|
|
13
|
-
async runHandshakeAfterSourceChange(gatewayBaseUrl: string, developmentServerToken: string): Promise<void> {
|
|
14
|
-
const restartStarted = performance.now();
|
|
15
|
-
try {
|
|
16
|
-
await this.gatewayNotifier.notify({
|
|
17
|
-
gatewayBaseUrl,
|
|
18
|
-
developmentServerToken,
|
|
19
|
-
payload: {
|
|
20
|
-
kind: "buildStarted",
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
await this.gatewayNotifier.notify({
|
|
24
|
-
gatewayBaseUrl,
|
|
25
|
-
developmentServerToken,
|
|
26
|
-
payload: {
|
|
27
|
-
kind: "buildCompleted",
|
|
28
|
-
buildVersion: `${Date.now()}-${process.pid}`,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
const totalMs = performance.now() - restartStarted;
|
|
32
|
-
this.performanceDiagnosticsLogger.info(
|
|
33
|
-
`triggered source-based runtime restart timingMs={total:${totalMs.toFixed(1)}}`,
|
|
34
|
-
);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
const exception = error instanceof Error ? error : new Error(String(error));
|
|
37
|
-
await this.gatewayNotifier.notify({
|
|
38
|
-
gatewayBaseUrl,
|
|
39
|
-
developmentServerToken,
|
|
40
|
-
payload: {
|
|
41
|
-
kind: "buildFailed",
|
|
42
|
-
message: exception.message,
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
this.cliLogger.error("source-based runtime restart request failed", exception);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|