@archlast/forge-cli 0.1.0 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archlast/forge-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The archlast-forge command line: dev, up, build, data and config commands.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,9 +25,9 @@
25
25
  "archlast-forge": "./src/index.ts"
26
26
  },
27
27
  "dependencies": {
28
- "@archlast/forge-dashboard": "^0.1.0",
29
- "@archlast/forge-runtime": "^0.1.0",
30
- "create-archlast-forge": "^0.1.0"
28
+ "@archlast/forge-dashboard": "^0.2.0",
29
+ "@archlast/forge-runtime": "^0.2.0",
30
+ "create-archlast-forge": "^0.2.0"
31
31
  },
32
32
  "engines": {
33
33
  "bun": ">=1.4.3"
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { ops } from "@archlast/forge-runtime/server";
3
3
  import { watch } from "node:fs";
4
4
  import { basename, join, resolve } from "node:path";
5
- import { parseArgs } from "node:util";
5
+ import { parseArgs, type ParseArgsConfig } from "node:util";
6
6
  import { buildFramework, loadFramework } from "./app";
7
7
  import { devSecrets, fail, findForgeDir, generate, trustProxyOrigins, VERSION } from "./common";
8
8
  import { ensureDeps, findUp } from "./deps";
@@ -59,7 +59,16 @@ ${c.bold("Engine")}
59
59
 
60
60
  Options: --dir <forge dir> --port <n> --no-docker`;
61
61
 
62
- const { positionals, values } = parseArgs({
62
+ /** A mistyped option is a one-line message, not a stack trace. */
63
+ function parseCommandLine<T extends ParseArgsConfig>(config: T) {
64
+ try {
65
+ return parseArgs(config);
66
+ } catch (e) {
67
+ return fail(`${(e as Error).message.replace(/\.$/, "")}. Run \`archlast-forge --help\` to see the options.`);
68
+ }
69
+ }
70
+
71
+ const { positionals, values } = parseCommandLine({
63
72
  args: Bun.argv.slice(2),
64
73
  allowPositionals: true,
65
74
  options: {
@@ -81,8 +90,13 @@ const { positionals, values } = parseArgs({
81
90
  rebuild: { type: "boolean" },
82
91
  dev: { type: "boolean" },
83
92
  help: { type: "boolean", short: "h" },
93
+ version: { type: "boolean", short: "v" },
84
94
  },
85
95
  });
96
+ if (values.version) {
97
+ console.log(VERSION);
98
+ process.exit(0);
99
+ }
86
100
  const [command, ...rest] = positionals;
87
101
  const port = Number(values.port ?? process.env.PORT ?? 3000);
88
102
  const inContainer = process.env.ARCHLAST_ENGINE === "1";
@@ -101,6 +115,7 @@ function devEnv(dir: string) {
101
115
  env.DATA_DIR ??= join(dir, "data");
102
116
  // The app's own address is trusted, and a proxy in front of it (code-server's port forwarding) is worked out from that one.
103
117
  env.TRUSTED_ORIGINS ||= `http://localhost:${port}`;
118
+ env.BETTER_AUTH_URL = `http://localhost:${port}`; // a project's .env (made for the stack) must not point sessions at another address
104
119
  trustProxyOrigins(env);
105
120
  return env;
106
121
  }
@@ -136,7 +151,6 @@ async function devFramework(dir: string, cfg: ProjectConfig) {
136
151
  const began = performance.now();
137
152
  const root = projectRoot(dir);
138
153
  const env = devEnv(dir);
139
- env.BETTER_AUTH_URL = `http://localhost:${port}`; // a stray .env must not point sessions at another port
140
154
  await ensureDeps(dir);
141
155
  ok(`codegen: ${(await generate(dir)).length} modules`);
142
156
 
@@ -153,6 +167,7 @@ async function devFramework(dir: string, cfg: ProjectConfig) {
153
167
  process.on("exit", dockerEngine.stopStartedServices); // Ctrl-C ends the session, so it ends its services too
154
168
  }
155
169
 
170
+ companion(); // --run "<cmd>"
156
171
  const { createApp } = await loadFramework(root, "server");
157
172
  const app = await createApp({ root, dev: true });
158
173
  const server = await serve({ forgeDir: dir, dev: true, env: { ...env, PORT: String(port) }, app, watchDir: dir, onChange: forgeChanged(dir) });
@@ -231,7 +246,8 @@ function watchSource(dir: string, onChange: (files: string[]) => Promise<void>)
231
246
  /** Where data and config commands run: inside the running engine container, or in this process. */
232
247
  function engineTarget(dir: string) {
233
248
  if (!useDocker()) return null;
234
- return dockerEngine.runningContainer(dir) ?? null;
249
+ // The app of a compose stack, then a single engine container.
250
+ return stack.appContainer(projectRoot(dir)) ?? dockerEngine.runningContainer(dir) ?? null;
235
251
  }
236
252
 
237
253
  /**
@@ -350,7 +366,8 @@ const commands: Record<string, () => Promise<unknown>> = {
350
366
  const root = projectRoot(dir);
351
367
  app = await (await loadFramework(root, "server")).createApp({ root, dev: false }).catch((e: Error) => fail(e.message.includes("No production build") ? `${e.message}` : e.message));
352
368
  }
353
- const server = await serve({ forgeDir: dir, dev: false, env: { ...process.env, PORT: String(port) }, app });
369
+ // Next to forge/, where `dev` keeps it, unless DATA_DIR says otherwise.
370
+ const server = await serve({ forgeDir: dir, dev: false, env: { ...process.env, DATA_DIR: process.env.DATA_DIR ?? join(dir, "data"), PORT: String(port) }, app });
354
371
  ok(`archlast-forge on ${server.url}${app ? " (app and API)" : ""}`);
355
372
  },
356
373
  async stop() {
package/src/stack.ts CHANGED
@@ -37,7 +37,16 @@ export function ensureEnv(root: string, services: StackService[]): boolean {
37
37
  return true;
38
38
  }
39
39
 
40
- const envValue = (root: string, key: string) => new RegExp(`^${key}=(.*)$`, "m").exec(existsSync(join(root, ".env")) ? readFileSync(join(root, ".env"), "utf8") : "")?.[1]?.trim();
40
+ const rawEnvValue = (root: string, key: string) => new RegExp(`^${key}=(.*)$`, "m").exec(existsSync(join(root, ".env")) ? readFileSync(join(root, ".env"), "utf8") : "")?.[1]?.trim();
41
+ /** A value from .env, with `${OTHER}` filled in from the same file the way compose does (BETTER_AUTH_URL=https://${DOMAIN}). */
42
+ const envValue = (root: string, key: string) => rawEnvValue(root, key)?.replace(/\$\{(\w+)\}/g, (_, other) => rawEnvValue(root, other) ?? "");
43
+
44
+ /** The running app container of the project's compose stack, for commands that have to run inside it. */
45
+ export function appContainer(root: string): string | undefined {
46
+ if (!existsSync(composeFile(root)) || !dockerAvailable()) return undefined;
47
+ const p = Bun.spawnSync([DOCKER, "compose", "--project-directory", root, "-f", composeFile(root), "ps", "-q", "app"], { stdout: "pipe", stderr: "ignore", stdin: "ignore", env: { ...process.env, ENGINE_IMAGE: process.env.ENGINE_IMAGE ?? engineImage() } });
48
+ return p.exitCode === 0 ? p.stdout.toString().trim().split("\n")[0] || undefined : undefined;
49
+ }
41
50
 
42
51
  /** Builds and starts the stack, and waits until the app reports healthy. */
43
52
  export async function up(root: string, services: StackService[], opts: { rebuild?: boolean } = {}) {