@codemation/cli 0.0.1
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 +148 -0
- package/bin/codemation.js +24 -0
- package/bin/codemation.ts +5 -0
- package/dist/CliBin-vjSSUDWE.js +2304 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/index.d.ts +23456 -0
- package/dist/index.js +4 -0
- package/package.json +56 -0
- package/src/CliBin.ts +17 -0
- package/src/CliProgramFactory.ts +118 -0
- package/src/Program.ts +157 -0
- package/src/bin.ts +6 -0
- package/src/bootstrap/CodemationCliApplicationSession.ts +60 -0
- package/src/build/ConsumerBuildArtifactsPublisher.ts +77 -0
- package/src/build/ConsumerBuildOptionsParser.ts +26 -0
- package/src/commands/BuildCommand.ts +31 -0
- package/src/commands/DbMigrateCommand.ts +19 -0
- package/src/commands/DevCommand.ts +391 -0
- package/src/commands/ServeWebCommand.ts +72 -0
- package/src/commands/ServeWorkerCommand.ts +40 -0
- package/src/commands/UserCreateCommand.ts +25 -0
- package/src/commands/UserListCommand.ts +59 -0
- package/src/commands/devCommandLifecycle.types.ts +32 -0
- package/src/consumer/ConsumerCliTsconfigPreparation.ts +26 -0
- package/src/consumer/ConsumerEnvLoader.ts +47 -0
- package/src/consumer/ConsumerOutputBuilder.ts +898 -0
- package/src/consumer/Loader.ts +8 -0
- package/src/consumer/consumerBuildOptions.types.ts +12 -0
- package/src/database/ConsumerDatabaseConnectionResolver.ts +18 -0
- package/src/database/DatabaseMigrationsApplyService.ts +76 -0
- package/src/database/HostPackageRootResolver.ts +26 -0
- package/src/database/PrismaMigrateDeployInvoker.ts +24 -0
- package/src/dev/Builder.ts +45 -0
- package/src/dev/ConsumerEnvDotenvFilePredicate.ts +12 -0
- package/src/dev/DevAuthSettingsLoader.ts +27 -0
- package/src/dev/DevBootstrapSummaryFetcher.ts +15 -0
- package/src/dev/DevCliBannerRenderer.ts +106 -0
- package/src/dev/DevConsumerPublishBootstrap.ts +30 -0
- package/src/dev/DevHttpProbe.ts +54 -0
- package/src/dev/DevLock.ts +98 -0
- package/src/dev/DevNextHostEnvironmentBuilder.ts +49 -0
- package/src/dev/DevSessionPortsResolver.ts +23 -0
- package/src/dev/DevSessionServices.ts +29 -0
- package/src/dev/DevSourceRestartCoordinator.ts +48 -0
- package/src/dev/DevSourceWatcher.ts +102 -0
- package/src/dev/DevTrackedProcessTreeKiller.ts +107 -0
- package/src/dev/DevelopmentGatewayNotifier.ts +35 -0
- package/src/dev/Factory.ts +7 -0
- package/src/dev/LoopbackPortAllocator.ts +20 -0
- package/src/dev/Runner.ts +7 -0
- package/src/dev/RuntimeToolEntrypointResolver.ts +47 -0
- package/src/dev/WatchRootsResolver.ts +26 -0
- package/src/index.ts +12 -0
- package/src/path/CliPathResolver.ts +41 -0
- package/src/runtime/ListenPortResolver.ts +35 -0
- package/src/runtime/SourceMapNodeOptions.ts +12 -0
- package/src/runtime/TypeScriptRuntimeConfigurator.ts +8 -0
- package/src/user/CliDatabaseUrlDescriptor.ts +33 -0
- package/src/user/LocalUserCreator.ts +29 -0
- package/src/user/UserAdminCliBootstrap.ts +67 -0
- package/src/user/UserAdminCliOptionsParser.ts +24 -0
- package/src/user/UserAdminConsumerDotenvLoader.ts +24 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import type { Logger } from "@codemation/host/next/server";
|
|
2
|
+
import { CodemationPluginDiscovery } from "@codemation/host/server";
|
|
3
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
|
|
8
|
+
import type { DatabaseMigrationsApplyService } from "../database/DatabaseMigrationsApplyService";
|
|
9
|
+
import type { DevBootstrapSummaryFetcher } from "../dev/DevBootstrapSummaryFetcher";
|
|
10
|
+
import type { DevCliBannerRenderer } from "../dev/DevCliBannerRenderer";
|
|
11
|
+
import type { DevConsumerPublishBootstrap } from "../dev/DevConsumerPublishBootstrap";
|
|
12
|
+
import { ConsumerEnvDotenvFilePredicate } from "../dev/ConsumerEnvDotenvFilePredicate";
|
|
13
|
+
import type { DevSourceWatcher } from "../dev/DevSourceWatcher";
|
|
14
|
+
import { DevSessionServices } from "../dev/DevSessionServices";
|
|
15
|
+
import { DevLockFactory } from "../dev/Factory";
|
|
16
|
+
import { DevTrackedProcessTreeKiller } from "../dev/DevTrackedProcessTreeKiller";
|
|
17
|
+
import { DevSourceWatcherFactory } from "../dev/Runner";
|
|
18
|
+
import { CliPathResolver, type CliPaths } from "../path/CliPathResolver";
|
|
19
|
+
import { TypeScriptRuntimeConfigurator } from "../runtime/TypeScriptRuntimeConfigurator";
|
|
20
|
+
|
|
21
|
+
import type { DevResolvedAuthSettings } from "../dev/DevAuthSettingsLoader";
|
|
22
|
+
|
|
23
|
+
import type { DevMode, DevMutableProcessState, DevPreparedRuntime } from "./devCommandLifecycle.types";
|
|
24
|
+
|
|
25
|
+
export class DevCommand {
|
|
26
|
+
private readonly require = createRequire(import.meta.url);
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly pathResolver: CliPathResolver,
|
|
30
|
+
private readonly pluginDiscovery: CodemationPluginDiscovery,
|
|
31
|
+
private readonly tsRuntime: TypeScriptRuntimeConfigurator,
|
|
32
|
+
private readonly devLockFactory: DevLockFactory,
|
|
33
|
+
private readonly devSourceWatcherFactory: DevSourceWatcherFactory,
|
|
34
|
+
private readonly cliLogger: Logger,
|
|
35
|
+
private readonly session: DevSessionServices,
|
|
36
|
+
private readonly databaseMigrationsApplyService: DatabaseMigrationsApplyService,
|
|
37
|
+
private readonly devBootstrapSummaryFetcher: DevBootstrapSummaryFetcher,
|
|
38
|
+
private readonly devCliBannerRenderer: DevCliBannerRenderer,
|
|
39
|
+
private readonly devConsumerPublishBootstrap: DevConsumerPublishBootstrap,
|
|
40
|
+
private readonly consumerEnvDotenvFilePredicate: ConsumerEnvDotenvFilePredicate,
|
|
41
|
+
private readonly devTrackedProcessTreeKiller: DevTrackedProcessTreeKiller,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
async execute(consumerRoot: string): Promise<void> {
|
|
45
|
+
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
46
|
+
this.devCliBannerRenderer.renderBrandHeader();
|
|
47
|
+
this.tsRuntime.configure(paths.repoRoot);
|
|
48
|
+
await this.databaseMigrationsApplyService.applyForConsumer(paths.consumerRoot);
|
|
49
|
+
await this.devConsumerPublishBootstrap.ensurePublished(paths);
|
|
50
|
+
const devMode = this.resolveDevModeFromEnv();
|
|
51
|
+
const { nextPort, gatewayPort } = await this.session.sessionPorts.resolve({
|
|
52
|
+
devMode,
|
|
53
|
+
portEnv: process.env.PORT,
|
|
54
|
+
gatewayPortEnv: process.env.CODEMATION_DEV_GATEWAY_HTTP_PORT,
|
|
55
|
+
});
|
|
56
|
+
const devLock = this.devLockFactory.create();
|
|
57
|
+
await devLock.acquire({
|
|
58
|
+
consumerRoot: paths.consumerRoot,
|
|
59
|
+
nextPort: devMode === "framework" ? nextPort : gatewayPort,
|
|
60
|
+
});
|
|
61
|
+
const authSettings = await this.session.devAuthLoader.loadForConsumer(paths.consumerRoot);
|
|
62
|
+
const watcher = this.devSourceWatcherFactory.create();
|
|
63
|
+
try {
|
|
64
|
+
const prepared = await this.prepareDevRuntime(paths, devMode, nextPort, gatewayPort, authSettings);
|
|
65
|
+
const processState = this.createInitialProcessState();
|
|
66
|
+
const stopPromise = this.wireStopPromise(processState);
|
|
67
|
+
const uiProxyBase = await this.startConsumerUiProxyWhenNeeded(prepared, processState);
|
|
68
|
+
const gatewayBaseUrl = this.gatewayBaseHttpUrl(gatewayPort);
|
|
69
|
+
await this.spawnGatewayChildAndWaitForHealth(prepared, processState, gatewayBaseUrl, uiProxyBase);
|
|
70
|
+
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
71
|
+
const initialSummary = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
72
|
+
if (initialSummary) {
|
|
73
|
+
this.devCliBannerRenderer.renderRuntimeSummary(initialSummary);
|
|
74
|
+
}
|
|
75
|
+
this.bindShutdownSignalsToChildProcesses(processState);
|
|
76
|
+
this.spawnFrameworkNextHostWhenNeeded(prepared, processState, gatewayBaseUrl);
|
|
77
|
+
await this.startWatcherForSourceRestart(prepared, watcher, devMode, gatewayBaseUrl);
|
|
78
|
+
this.logConsumerDevHintWhenNeeded(devMode, gatewayPort);
|
|
79
|
+
await stopPromise;
|
|
80
|
+
} finally {
|
|
81
|
+
await watcher.stop();
|
|
82
|
+
await devLock.release();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private resolveDevModeFromEnv(): DevMode {
|
|
87
|
+
return process.env.CODEMATION_DEV_MODE === "framework" ? "framework" : "consumer";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async prepareDevRuntime(
|
|
91
|
+
paths: CliPaths,
|
|
92
|
+
devMode: DevMode,
|
|
93
|
+
nextPort: number,
|
|
94
|
+
gatewayPort: number,
|
|
95
|
+
authSettings: DevResolvedAuthSettings,
|
|
96
|
+
): Promise<DevPreparedRuntime> {
|
|
97
|
+
const developmentServerToken = this.session.devAuthLoader.resolveDevelopmentServerToken(
|
|
98
|
+
process.env.CODEMATION_DEV_SERVER_TOKEN,
|
|
99
|
+
);
|
|
100
|
+
const gatewayEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
|
|
101
|
+
packageName: "@codemation/dev-gateway",
|
|
102
|
+
repoRoot: paths.repoRoot,
|
|
103
|
+
sourceEntrypoint: "packages/dev-gateway/src/bin.ts",
|
|
104
|
+
});
|
|
105
|
+
const runtimeEntrypoint = await this.session.runtimeEntrypointResolver.resolve({
|
|
106
|
+
packageName: "@codemation/runtime-dev",
|
|
107
|
+
repoRoot: paths.repoRoot,
|
|
108
|
+
sourceEntrypoint: "packages/runtime-dev/src/bin.ts",
|
|
109
|
+
});
|
|
110
|
+
const runtimeWorkingDirectory = paths.repoRoot ?? paths.consumerRoot;
|
|
111
|
+
const consumerEnv = this.session.consumerEnvLoader.load(paths.consumerRoot);
|
|
112
|
+
const discoveredPluginPackagesJson = JSON.stringify(await this.pluginDiscovery.discover(paths.consumerRoot));
|
|
113
|
+
return {
|
|
114
|
+
paths,
|
|
115
|
+
devMode,
|
|
116
|
+
nextPort,
|
|
117
|
+
gatewayPort,
|
|
118
|
+
authSettings,
|
|
119
|
+
developmentServerToken,
|
|
120
|
+
gatewayEntrypoint,
|
|
121
|
+
runtimeEntrypoint,
|
|
122
|
+
runtimeWorkingDirectory,
|
|
123
|
+
discoveredPluginPackagesJson,
|
|
124
|
+
consumerEnv,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private createInitialProcessState(): DevMutableProcessState {
|
|
129
|
+
return {
|
|
130
|
+
currentGateway: null,
|
|
131
|
+
currentNextHost: null,
|
|
132
|
+
currentUiNext: null,
|
|
133
|
+
stopRequested: false,
|
|
134
|
+
stopResolve: null,
|
|
135
|
+
stopReject: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private wireStopPromise(state: DevMutableProcessState): Promise<void> {
|
|
140
|
+
return new Promise<void>((resolve, reject) => {
|
|
141
|
+
state.stopResolve = resolve;
|
|
142
|
+
state.stopReject = reject;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private gatewayBaseHttpUrl(gatewayPort: number): string {
|
|
147
|
+
return `http://127.0.0.1:${gatewayPort}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Consumer mode: run `next start` for the host UI and wait until it responds, so the gateway can proxy to it.
|
|
152
|
+
* Framework mode: no separate UI child (Next runs in dev later).
|
|
153
|
+
*/
|
|
154
|
+
private async startConsumerUiProxyWhenNeeded(
|
|
155
|
+
prepared: DevPreparedRuntime,
|
|
156
|
+
state: DevMutableProcessState,
|
|
157
|
+
): Promise<string> {
|
|
158
|
+
if (prepared.devMode !== "consumer") {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
const websocketPort = prepared.gatewayPort;
|
|
162
|
+
const uiPort = await this.session.loopbackPortAllocator.allocate();
|
|
163
|
+
const uiProxyBase = `http://127.0.0.1:${uiPort}`;
|
|
164
|
+
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
165
|
+
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
166
|
+
const consumerOutputManifestPath = path.resolve(
|
|
167
|
+
prepared.paths.consumerRoot,
|
|
168
|
+
".codemation",
|
|
169
|
+
"output",
|
|
170
|
+
"current.json",
|
|
171
|
+
);
|
|
172
|
+
state.currentUiNext = spawn("pnpm", ["exec", "next", "start"], {
|
|
173
|
+
cwd: nextHostRoot,
|
|
174
|
+
...this.devDetachedChildSpawnOptions(),
|
|
175
|
+
env: {
|
|
176
|
+
...process.env,
|
|
177
|
+
...prepared.consumerEnv,
|
|
178
|
+
PORT: String(uiPort),
|
|
179
|
+
CODEMATION_AUTH_CONFIG_JSON: prepared.authSettings.authConfigJson,
|
|
180
|
+
CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
|
|
181
|
+
CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: consumerOutputManifestPath,
|
|
182
|
+
CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
|
|
183
|
+
NEXT_PUBLIC_CODEMATION_SKIP_UI_AUTH: prepared.authSettings.skipUiAuth ? "true" : "false",
|
|
184
|
+
CODEMATION_WS_PORT: String(websocketPort),
|
|
185
|
+
NEXT_PUBLIC_CODEMATION_WS_PORT: String(websocketPort),
|
|
186
|
+
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
187
|
+
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
188
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
189
|
+
WS_NO_UTF_8_VALIDATE: "1",
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
state.currentUiNext.on("error", (error) => {
|
|
193
|
+
if (!state.stopRequested) {
|
|
194
|
+
state.stopRequested = true;
|
|
195
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
state.currentUiNext.on("exit", (code) => {
|
|
199
|
+
if (state.stopRequested) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
state.stopRequested = true;
|
|
203
|
+
if (state.currentGateway?.exitCode === null) {
|
|
204
|
+
void this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
205
|
+
}
|
|
206
|
+
state.stopReject?.(new Error(`next start (consumer UI) exited unexpectedly with code ${code ?? 0}.`));
|
|
207
|
+
});
|
|
208
|
+
await this.session.devHttpProbe.waitUntilUrlRespondsOk(`${uiProxyBase}/`);
|
|
209
|
+
return uiProxyBase;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async spawnGatewayChildAndWaitForHealth(
|
|
213
|
+
prepared: DevPreparedRuntime,
|
|
214
|
+
state: DevMutableProcessState,
|
|
215
|
+
gatewayBaseUrl: string,
|
|
216
|
+
uiProxyBase: string,
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
const gatewayProcessEnv = this.session.consumerEnvLoader.mergeIntoProcessEnvironment(
|
|
219
|
+
process.env,
|
|
220
|
+
prepared.consumerEnv,
|
|
221
|
+
);
|
|
222
|
+
state.currentGateway = spawn(prepared.gatewayEntrypoint.command, prepared.gatewayEntrypoint.args, {
|
|
223
|
+
cwd: prepared.runtimeWorkingDirectory,
|
|
224
|
+
...this.devDetachedChildSpawnOptions(),
|
|
225
|
+
env: {
|
|
226
|
+
...gatewayProcessEnv,
|
|
227
|
+
...prepared.gatewayEntrypoint.env,
|
|
228
|
+
CODEMATION_DEV_GATEWAY_HTTP_PORT: String(prepared.gatewayPort),
|
|
229
|
+
CODEMATION_RUNTIME_CHILD_BIN: prepared.runtimeEntrypoint.command,
|
|
230
|
+
CODEMATION_RUNTIME_CHILD_ARGS_JSON: JSON.stringify(prepared.runtimeEntrypoint.args),
|
|
231
|
+
CODEMATION_RUNTIME_CHILD_ENV_JSON: JSON.stringify(prepared.runtimeEntrypoint.env),
|
|
232
|
+
CODEMATION_RUNTIME_CHILD_CWD: prepared.runtimeWorkingDirectory,
|
|
233
|
+
CODEMATION_CONSUMER_ROOT: prepared.paths.consumerRoot,
|
|
234
|
+
CODEMATION_DISCOVERED_PLUGIN_PACKAGES_JSON: prepared.discoveredPluginPackagesJson,
|
|
235
|
+
CODEMATION_PREFER_PLUGIN_SOURCE_ENTRY: "true",
|
|
236
|
+
CODEMATION_DEV_SERVER_TOKEN: prepared.developmentServerToken,
|
|
237
|
+
CODEMATION_SKIP_STARTUP_MIGRATIONS: "true",
|
|
238
|
+
NODE_OPTIONS: this.session.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
239
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
240
|
+
WS_NO_UTF_8_VALIDATE: "1",
|
|
241
|
+
...(uiProxyBase.length > 0 ? { CODEMATION_DEV_UI_PROXY_TARGET: uiProxyBase } : {}),
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
state.currentGateway.on("error", (error) => {
|
|
245
|
+
if (!state.stopRequested) {
|
|
246
|
+
state.stopRequested = true;
|
|
247
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
state.currentGateway.on("exit", (code) => {
|
|
251
|
+
if (state.stopRequested) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
state.stopRequested = true;
|
|
255
|
+
state.stopReject?.(new Error(`codemation dev-gateway exited unexpectedly with code ${code ?? 0}.`));
|
|
256
|
+
});
|
|
257
|
+
await this.session.devHttpProbe.waitUntilGatewayHealthy(gatewayBaseUrl);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private devDetachedChildSpawnOptions(): Readonly<{
|
|
261
|
+
stdio: "inherit";
|
|
262
|
+
detached: boolean;
|
|
263
|
+
windowsHide?: boolean;
|
|
264
|
+
}> {
|
|
265
|
+
return process.platform === "win32"
|
|
266
|
+
? { stdio: "inherit", detached: true, windowsHide: true }
|
|
267
|
+
: { stdio: "inherit", detached: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private bindShutdownSignalsToChildProcesses(state: DevMutableProcessState): void {
|
|
271
|
+
let shutdownInProgress = false;
|
|
272
|
+
const runShutdown = async (): Promise<void> => {
|
|
273
|
+
if (shutdownInProgress) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
shutdownInProgress = true;
|
|
277
|
+
state.stopRequested = true;
|
|
278
|
+
process.stdout.write("\n[codemation] Stopping..\n");
|
|
279
|
+
const children: ChildProcess[] = [];
|
|
280
|
+
for (const child of [state.currentUiNext, state.currentNextHost, state.currentGateway]) {
|
|
281
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
282
|
+
children.push(child);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
await Promise.all(children.map((child) => this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(child)));
|
|
286
|
+
process.stdout.write("[codemation] Stopped.\n");
|
|
287
|
+
state.stopResolve?.();
|
|
288
|
+
};
|
|
289
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGQUIT"] as const) {
|
|
290
|
+
process.on(signal, () => {
|
|
291
|
+
void runShutdown();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Framework mode: run `next dev` for the Next host with HMR, pointed at the dev gateway runtime URL.
|
|
298
|
+
*/
|
|
299
|
+
private spawnFrameworkNextHostWhenNeeded(
|
|
300
|
+
prepared: DevPreparedRuntime,
|
|
301
|
+
state: DevMutableProcessState,
|
|
302
|
+
gatewayBaseUrl: string,
|
|
303
|
+
): void {
|
|
304
|
+
if (prepared.devMode !== "framework") {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const websocketPort = prepared.gatewayPort;
|
|
308
|
+
const nextHostPackageJsonPath = this.require.resolve("@codemation/next-host/package.json");
|
|
309
|
+
const nextHostRoot = path.dirname(nextHostPackageJsonPath);
|
|
310
|
+
const nextHostEnvironment = this.session.nextHostEnvBuilder.build({
|
|
311
|
+
authConfigJson: prepared.authSettings.authConfigJson,
|
|
312
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
313
|
+
developmentServerToken: prepared.developmentServerToken,
|
|
314
|
+
nextPort: prepared.nextPort,
|
|
315
|
+
skipUiAuth: prepared.authSettings.skipUiAuth,
|
|
316
|
+
websocketPort,
|
|
317
|
+
runtimeDevUrl: gatewayBaseUrl,
|
|
318
|
+
});
|
|
319
|
+
state.currentNextHost = spawn("pnpm", ["exec", "next", "dev"], {
|
|
320
|
+
cwd: nextHostRoot,
|
|
321
|
+
...this.devDetachedChildSpawnOptions(),
|
|
322
|
+
env: nextHostEnvironment,
|
|
323
|
+
});
|
|
324
|
+
state.currentNextHost.on("exit", (code) => {
|
|
325
|
+
const normalizedCode = code ?? 0;
|
|
326
|
+
if (state.stopRequested) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (normalizedCode === 0) {
|
|
330
|
+
state.stopRequested = true;
|
|
331
|
+
if (state.currentGateway?.exitCode === null) {
|
|
332
|
+
void this.devTrackedProcessTreeKiller.killProcessTreeRootedAt(state.currentGateway);
|
|
333
|
+
}
|
|
334
|
+
state.stopResolve?.();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
state.stopRequested = true;
|
|
338
|
+
state.stopReject?.(new Error(`next host exited with code ${normalizedCode}.`));
|
|
339
|
+
});
|
|
340
|
+
state.currentNextHost.on("error", (error) => {
|
|
341
|
+
if (!state.stopRequested) {
|
|
342
|
+
state.stopRequested = true;
|
|
343
|
+
state.stopReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private async startWatcherForSourceRestart(
|
|
349
|
+
prepared: DevPreparedRuntime,
|
|
350
|
+
watcher: DevSourceWatcher,
|
|
351
|
+
devMode: DevMode,
|
|
352
|
+
gatewayBaseUrl: string,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
await watcher.start({
|
|
355
|
+
roots: this.session.watchRootsResolver.resolve({
|
|
356
|
+
consumerRoot: prepared.paths.consumerRoot,
|
|
357
|
+
devMode,
|
|
358
|
+
repoRoot: prepared.paths.repoRoot,
|
|
359
|
+
}),
|
|
360
|
+
onChange: async ({ changedPaths }) => {
|
|
361
|
+
if (changedPaths.length > 0 && changedPaths.every((p) => this.consumerEnvDotenvFilePredicate.matches(p))) {
|
|
362
|
+
process.stdout.write(
|
|
363
|
+
"\n[codemation] Consumer environment file changed (e.g. .env). Restart the `codemation dev` process so the gateway and runtime pick up updated variables (host `process.env` does not hot-reload).\n",
|
|
364
|
+
);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
process.stdout.write("\n[codemation] Source change detected — rebuilding consumer…\n");
|
|
368
|
+
await this.session.sourceRestartCoordinator.runHandshakeAfterSourceChange(
|
|
369
|
+
gatewayBaseUrl,
|
|
370
|
+
prepared.developmentServerToken,
|
|
371
|
+
);
|
|
372
|
+
process.stdout.write("[codemation] Waiting for runtime to accept traffic…\n");
|
|
373
|
+
await this.session.devHttpProbe.waitUntilBootstrapSummaryReady(gatewayBaseUrl);
|
|
374
|
+
const json = await this.devBootstrapSummaryFetcher.fetch(gatewayBaseUrl);
|
|
375
|
+
if (json) {
|
|
376
|
+
this.devCliBannerRenderer.renderCompact(json);
|
|
377
|
+
}
|
|
378
|
+
process.stdout.write("[codemation] Runtime ready.\n");
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private logConsumerDevHintWhenNeeded(devMode: DevMode, gatewayPort: number): void {
|
|
384
|
+
if (devMode !== "consumer") {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.cliLogger.info(
|
|
388
|
+
`codemation dev (consumer): open http://127.0.0.1:${gatewayPort} — requires a built @codemation/next-host (next build). For Next HMR use CODEMATION_DEV_MODE=framework.`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CodemationPluginDiscovery } from "@codemation/host/server";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
import { ConsumerBuildArtifactsPublisher } from "../build/ConsumerBuildArtifactsPublisher";
|
|
8
|
+
import { ConsumerEnvLoader } from "../consumer/ConsumerEnvLoader";
|
|
9
|
+
import type { ConsumerBuildOptions } from "../consumer/consumerBuildOptions.types";
|
|
10
|
+
import { ConsumerOutputBuilderLoader } from "../consumer/Loader";
|
|
11
|
+
import { CliPathResolver } from "../path/CliPathResolver";
|
|
12
|
+
import { ListenPortResolver } from "../runtime/ListenPortResolver";
|
|
13
|
+
import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
|
|
14
|
+
import { TypeScriptRuntimeConfigurator } from "../runtime/TypeScriptRuntimeConfigurator";
|
|
15
|
+
|
|
16
|
+
export class ServeWebCommand {
|
|
17
|
+
private readonly require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly pathResolver: CliPathResolver,
|
|
21
|
+
private readonly pluginDiscovery: CodemationPluginDiscovery,
|
|
22
|
+
private readonly artifactsPublisher: ConsumerBuildArtifactsPublisher,
|
|
23
|
+
private readonly tsRuntime: TypeScriptRuntimeConfigurator,
|
|
24
|
+
private readonly sourceMapNodeOptions: SourceMapNodeOptions,
|
|
25
|
+
private readonly outputBuilderLoader: ConsumerOutputBuilderLoader,
|
|
26
|
+
private readonly envLoader: ConsumerEnvLoader,
|
|
27
|
+
private readonly listenPortResolver: ListenPortResolver,
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
async execute(consumerRoot: string, buildOptions: ConsumerBuildOptions): Promise<void> {
|
|
31
|
+
const paths = await this.pathResolver.resolve(consumerRoot);
|
|
32
|
+
this.tsRuntime.configure(paths.repoRoot);
|
|
33
|
+
const builder = this.outputBuilderLoader.create(paths.consumerRoot, buildOptions);
|
|
34
|
+
const snapshot = await builder.ensureBuilt();
|
|
35
|
+
const discoveredPlugins = await this.pluginDiscovery.discover(paths.consumerRoot);
|
|
36
|
+
const manifest = await this.artifactsPublisher.publish(snapshot, discoveredPlugins);
|
|
37
|
+
const nextHostRoot = path.dirname(this.require.resolve("@codemation/next-host/package.json"));
|
|
38
|
+
const consumerEnv = this.envLoader.load(paths.consumerRoot);
|
|
39
|
+
const nextPort = this.listenPortResolver.resolvePrimaryApplicationPort(process.env.PORT);
|
|
40
|
+
const websocketPort = this.listenPortResolver.resolveWebsocketPortRelativeToHttp({
|
|
41
|
+
nextPort,
|
|
42
|
+
publicWebsocketPort: process.env.NEXT_PUBLIC_CODEMATION_WS_PORT,
|
|
43
|
+
websocketPort: process.env.CODEMATION_WS_PORT,
|
|
44
|
+
});
|
|
45
|
+
const child = spawn("pnpm", ["exec", "next", "start"], {
|
|
46
|
+
cwd: nextHostRoot,
|
|
47
|
+
stdio: "inherit",
|
|
48
|
+
env: {
|
|
49
|
+
...process.env,
|
|
50
|
+
...consumerEnv,
|
|
51
|
+
PORT: String(nextPort),
|
|
52
|
+
CODEMATION_CONSUMER_OUTPUT_MANIFEST_PATH: manifest.manifestPath,
|
|
53
|
+
CODEMATION_CONSUMER_ROOT: paths.consumerRoot,
|
|
54
|
+
CODEMATION_WS_PORT: String(websocketPort),
|
|
55
|
+
NEXT_PUBLIC_CODEMATION_WS_PORT: String(websocketPort),
|
|
56
|
+
NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
57
|
+
WS_NO_BUFFER_UTIL: "1",
|
|
58
|
+
WS_NO_UTF_8_VALIDATE: "1",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
await new Promise<void>((resolve, reject) => {
|
|
62
|
+
child.on("exit", (code) => {
|
|
63
|
+
if ((code ?? 0) === 0) {
|
|
64
|
+
resolve();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
reject(new Error(`next start exited with code ${code ?? 0}.`));
|
|
68
|
+
});
|
|
69
|
+
child.on("error", reject);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
import { SourceMapNodeOptions } from "../runtime/SourceMapNodeOptions";
|
|
7
|
+
|
|
8
|
+
export class ServeWorkerCommand {
|
|
9
|
+
private readonly require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
constructor(private readonly sourceMapNodeOptions: SourceMapNodeOptions) {}
|
|
12
|
+
|
|
13
|
+
async execute(consumerRoot: string, configPathOverride?: string): Promise<void> {
|
|
14
|
+
const workerPackageRoot = path.dirname(this.require.resolve("@codemation/worker-cli/package.json"));
|
|
15
|
+
const workerBin = path.join(workerPackageRoot, "bin", "codemation-worker.js");
|
|
16
|
+
const args = [workerBin];
|
|
17
|
+
if (configPathOverride !== undefined && configPathOverride.trim().length > 0) {
|
|
18
|
+
args.push("--config", path.resolve(process.cwd(), configPathOverride.trim()));
|
|
19
|
+
}
|
|
20
|
+
args.push("--consumer-root", consumerRoot);
|
|
21
|
+
const child = spawn(process.execPath, args, {
|
|
22
|
+
cwd: consumerRoot,
|
|
23
|
+
stdio: "inherit",
|
|
24
|
+
env: {
|
|
25
|
+
...process.env,
|
|
26
|
+
NODE_OPTIONS: this.sourceMapNodeOptions.appendToNodeOptions(process.env.NODE_OPTIONS),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
await new Promise<void>((resolve, reject) => {
|
|
30
|
+
child.on("exit", (code) => {
|
|
31
|
+
if ((code ?? 0) === 0) {
|
|
32
|
+
resolve();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
reject(new Error(`codemation-worker exited with code ${code ?? 0}.`));
|
|
36
|
+
});
|
|
37
|
+
child.on("error", reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LocalUserCreator, type LocalUserCreateOptions } from "../user/LocalUserCreator";
|
|
2
|
+
import type { UserAdminCliOptionsParser } from "../user/UserAdminCliOptionsParser";
|
|
3
|
+
|
|
4
|
+
export class UserCreateCommand {
|
|
5
|
+
constructor(
|
|
6
|
+
private readonly localUserCreator: LocalUserCreator,
|
|
7
|
+
private readonly userAdminCliOptionsParser: UserAdminCliOptionsParser,
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
async execute(
|
|
11
|
+
opts: Readonly<{
|
|
12
|
+
email: string;
|
|
13
|
+
password: string;
|
|
14
|
+
consumerRoot?: string;
|
|
15
|
+
config?: string;
|
|
16
|
+
}>,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const options: LocalUserCreateOptions = {
|
|
19
|
+
...this.userAdminCliOptionsParser.parse(opts),
|
|
20
|
+
email: opts.email,
|
|
21
|
+
password: opts.password,
|
|
22
|
+
};
|
|
23
|
+
await this.localUserCreator.run(options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ListUserAccountsQuery } from "@codemation/host";
|
|
2
|
+
import type { Logger } from "@codemation/host/next/server";
|
|
3
|
+
import type { CliDatabaseUrlDescriptor } from "../user/CliDatabaseUrlDescriptor";
|
|
4
|
+
import type { UserAdminCliBootstrap } from "../user/UserAdminCliBootstrap";
|
|
5
|
+
import type { UserAdminCliCommandOptionsRaw, UserAdminCliOptionsParser } from "../user/UserAdminCliOptionsParser";
|
|
6
|
+
|
|
7
|
+
type UserRowForTable = Readonly<{
|
|
8
|
+
email: string;
|
|
9
|
+
status: string;
|
|
10
|
+
id: string;
|
|
11
|
+
loginMethods: ReadonlyArray<string>;
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export class UserListCommand {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly cliLogger: Logger,
|
|
17
|
+
private readonly userAdminBootstrap: UserAdminCliBootstrap,
|
|
18
|
+
private readonly databaseUrlDescriptor: CliDatabaseUrlDescriptor,
|
|
19
|
+
private readonly userAdminCliOptionsParser: UserAdminCliOptionsParser,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
async execute(opts: UserAdminCliCommandOptionsRaw): Promise<void> {
|
|
23
|
+
await this.userAdminBootstrap.withSession(this.userAdminCliOptionsParser.parse(opts), async (session) => {
|
|
24
|
+
const where = this.databaseUrlDescriptor.describeForDisplay(process.env.DATABASE_URL);
|
|
25
|
+
const users = await session.getQueryBus().execute(new ListUserAccountsQuery());
|
|
26
|
+
if (users.length === 0) {
|
|
27
|
+
this.cliLogger.info(
|
|
28
|
+
`No users found (${where}). If this is the wrong database, fix DATABASE_URL or CodemationConfig.runtime.database.url.`,
|
|
29
|
+
);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.cliLogger.info(`${where}\n${this.formatUserAccountsTable(users)}`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private formatUserAccountsTable(users: ReadonlyArray<UserRowForTable>): string {
|
|
37
|
+
const headers = ["Email", "Status", "Id", "Login methods"] as const;
|
|
38
|
+
const rows: ReadonlyArray<ReadonlyArray<string>> = users.map((user) => [
|
|
39
|
+
user.email,
|
|
40
|
+
user.status,
|
|
41
|
+
user.id,
|
|
42
|
+
user.loginMethods.length > 0 ? user.loginMethods.join(", ") : "—",
|
|
43
|
+
]);
|
|
44
|
+
const columnCount = headers.length;
|
|
45
|
+
const widths: number[] = [];
|
|
46
|
+
for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
|
47
|
+
const headerWidth = headers[columnIndex].length;
|
|
48
|
+
const cellWidths = rows.map((row) => row[columnIndex].length);
|
|
49
|
+
widths.push(Math.max(headerWidth, ...cellWidths, 3));
|
|
50
|
+
}
|
|
51
|
+
const padCell = (text: string, columnIndex: number): string => text.padEnd(widths[columnIndex] ?? text.length);
|
|
52
|
+
const horizontal = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
|
|
53
|
+
const formatRow = (cells: ReadonlyArray<string>): string =>
|
|
54
|
+
`| ${cells.map((cell, index) => padCell(cell, index)).join(" | ")} |`;
|
|
55
|
+
const headerLine = formatRow([...headers]);
|
|
56
|
+
const bodyLines = rows.map((row) => formatRow([...row]));
|
|
57
|
+
return [horizontal, headerLine, horizontal, ...bodyLines, horizontal].join("\n");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import type { DevResolvedAuthSettings } from "../dev/DevAuthSettingsLoader";
|
|
4
|
+
import type { ResolvedRuntimeToolEntrypoint } from "../dev/RuntimeToolEntrypointResolver";
|
|
5
|
+
import type { CliPaths } from "../path/CliPathResolver";
|
|
6
|
+
|
|
7
|
+
export type DevMode = "consumer" | "framework";
|
|
8
|
+
|
|
9
|
+
/** Mutable child process handles and stop coordination (shared across dev session helpers). */
|
|
10
|
+
export type DevMutableProcessState = {
|
|
11
|
+
currentGateway: ChildProcess | null;
|
|
12
|
+
currentNextHost: ChildProcess | null;
|
|
13
|
+
currentUiNext: ChildProcess | null;
|
|
14
|
+
stopRequested: boolean;
|
|
15
|
+
stopResolve: (() => void) | null;
|
|
16
|
+
stopReject: ((error: Error) => void) | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Immutable inputs resolved before any child processes are spawned. */
|
|
20
|
+
export type DevPreparedRuntime = Readonly<{
|
|
21
|
+
paths: CliPaths;
|
|
22
|
+
devMode: DevMode;
|
|
23
|
+
nextPort: number;
|
|
24
|
+
gatewayPort: number;
|
|
25
|
+
authSettings: DevResolvedAuthSettings;
|
|
26
|
+
developmentServerToken: string;
|
|
27
|
+
gatewayEntrypoint: ResolvedRuntimeToolEntrypoint;
|
|
28
|
+
runtimeEntrypoint: ResolvedRuntimeToolEntrypoint;
|
|
29
|
+
runtimeWorkingDirectory: string;
|
|
30
|
+
discoveredPluginPackagesJson: string;
|
|
31
|
+
consumerEnv: Readonly<Record<string, string>>;
|
|
32
|
+
}>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensures `CODEMATION_TSCONFIG_PATH` points at the repo's `tsconfig.codemation-tsx.json` when present,
|
|
6
|
+
* so tsx can load consumer `codemation.config.ts` files that import decorator-using workspace packages.
|
|
7
|
+
*/
|
|
8
|
+
export class ConsumerCliTsconfigPreparation {
|
|
9
|
+
applyWorkspaceTsconfigForTsxIfPresent(consumerRoot: string): void {
|
|
10
|
+
if (process.env.CODEMATION_TSCONFIG_PATH && process.env.CODEMATION_TSCONFIG_PATH.trim().length > 0) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const resolvedRoot = path.resolve(consumerRoot);
|
|
14
|
+
const candidates = [
|
|
15
|
+
path.resolve(resolvedRoot, "tsconfig.codemation-tsx.json"),
|
|
16
|
+
path.resolve(resolvedRoot, "..", "tsconfig.codemation-tsx.json"),
|
|
17
|
+
path.resolve(resolvedRoot, "..", "..", "tsconfig.codemation-tsx.json"),
|
|
18
|
+
];
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
if (existsSync(candidate)) {
|
|
21
|
+
process.env.CODEMATION_TSCONFIG_PATH = candidate;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|