#!/usr/bin/env bun /** * @wrnexus/cli — the `wrnexus` command line. * * wrnexus dev [app-dir] [--port=3000] start the dev server (live reload) * wrnexus build [app-dir] build a production server + assets * wrnexus create scaffold a new app * wrnexus eject copy a Wire UI component into your app * wrnexus db database migrations */ import { join, resolve } from "node:path"; import { resolveProfile, loadEnv } from "@wrnexus/styles"; import { runDev } from "./dev.ts"; import { createApp } from "./create.ts"; import { currentCliVersion, notifyIfUpdateAvailable } from "./update-notifier.ts"; /** * Resolve the active profile from `--profile=` (or WRNEXUS_PROFILE / mode), * publish it as WRNEXUS_PROFILE (so config loaders + the dev child pick it up), * and load its `.env` cascade into process.env. Returns the profile name. */ function bootstrapProfile( appRoot: string, mode: "development" | "production", args: string[], ): string { const flag = args.find((a) => a.startsWith("--profile=")); const profile = resolveProfile({ explicit: flag?.split("=")[1], mode }); process.env.WRNEXUS_PROFILE = profile; const loaded = loadEnv(resolve(appRoot), profile); const count = Object.keys(loaded).length; console.log(` ▸ profile: ${profile}${count ? ` (${count} env vars loaded)` : ""}`); return profile; } function help(): void { console.log(`wrnexus — WrNexus CLI Usage: wrnexus dev [app-dir] [--port=3000] [--host=::] Start the development server (live reload) wrnexus build [app-dir] Build a production server bundle + assets wrnexus create Scaffold a new app wrnexus workspace Scaffold a monorepo (apps/* + shared packages/*) wrnexus workspace add [--domain=name.localhost] Add an app to the current workspace wrnexus gateway [--port=3000] Serve every workspace app behind one port, routed by domain wrnexus production [workspace-dir] Build, migrate, and serve the complete production workspace wrnexus [workspace-dir] Run a configured workspace environment (development/staging/custom) wrnexus generate Scaffold a page | component | api | schema wrnexus generate routes | docker | mobile Generate routes or scaffold deployment targets wrnexus mobile add Install Capacitor or Expo native packages wrnexus mobile compile Compile .wrn pages into native Expo routes wrnexus native list List cross-platform native capabilities wrnexus native add Install capability packages for the configured mobile mode wrnexus eject Copy a Wire UI component into app/components wrnexus update [dir] [--latest] Upgrade deps, migrate project files, and verify the app wrnexus db Migrations: migrate | rollback | status | seed | generate | new wrnexus test [app-dir] [--watch] Run the app's tests (bun test, 'test' profile) wrnexus profiles [app-dir] List config profiles (dev/prod/uat/…) and their env files wrnexus doctor [app-dir] Check project structure, runtime, mobile config, and next fixes Profiles: pass --profile= to dev/build/db (or set WRNEXUS_PROFILE) to load that profile's .env cascade and config overrides. e.g. wrnexus dev --profile=uat Update options: --dry-run previews changes; --no-verify skips post-update check/build. `); } async function main(): Promise { const [command, ...rest] = process.argv.slice(2); notifyIfUpdateAvailable(currentCliVersion(), command); switch (command) { case "dev": { const appRoot = rest.find((a) => !a.startsWith("--")) ?? "."; const portArg = rest.find((a) => a.startsWith("--port=")); const hostArg = rest.find((a) => a.startsWith("--host=")); const port = portArg ? Number(portArg.split("=")[1]) : 3000; const host = hostArg?.split("=")[1] || "::"; bootstrapProfile(appRoot, "development", rest); runDev(appRoot, port, host); break; } case "build": { const appRoot = rest.find((a) => !a.startsWith("--")) ?? "."; bootstrapProfile(appRoot, "production", rest); const { runBuild } = await import("./build.ts"); await runBuild(appRoot); break; } case "create": createApp(rest[0] ?? ""); break; case "workspace": { const { addWorkspaceApp, createWorkspace } = await import("./workspace.ts"); if (rest[0] === "add") { await addWorkspaceApp(".", rest[1] ?? "", rest.slice(2)); } else { createWorkspace(rest.find((a) => !a.startsWith("--")) ?? ""); } break; } case "gateway": { const appRoot = rest.find((a) => !a.startsWith("--")) ?? "."; const { runGateway } = await import("./workspace.ts"); await runGateway(appRoot, rest); break; } case "production": { const workspaceRoot = rest.find((a) => !a.startsWith("--")) ?? "."; const { runProduction } = await import("./workspace.ts"); await runProduction(workspaceRoot, rest); break; } case "generate": case "g": { if (rest[0] === "routes") { const { regenerateRoutes } = await import("./routes.ts"); const n = regenerateRoutes(join(process.cwd(), "app")); console.log(`✓ Generated app/routes.gen.ts (${n} routes)`); break; } if (rest[0] === "docker") { const { generateDocker } = await import("./docker.ts"); generateDocker(process.cwd()); break; } if (rest[0] === "mobile") { const { generateMobile, mobileOptions } = await import("./mobile.ts"); await generateMobile(process.cwd(), mobileOptions(rest.slice(1))); break; } const { runGenerate } = await import("./generate.ts"); runGenerate(".", rest[0], rest[1]); break; } case "eject": { const { runEject } = await import("./eject.ts"); const args = rest.filter((a) => !a.startsWith("--")); // First arg may be an app dir; treat known component names as names. runEject(".", args); break; } case "mobile": { const { runMobileCommand } = await import("./mobile-command.ts"); await runMobileCommand(".", rest[0], rest.slice(1)); break; } case "native": { const { runNativeCommand } = await import("./native-command.ts"); await runNativeCommand(".", rest[0], rest.slice(1)); break; } case "update": case "upgrade": { const dir = rest.find((a) => !a.startsWith("--")) ?? "."; const { runUpdate } = await import("./update.ts"); await runUpdate(dir, rest); break; } case "db": { bootstrapProfile(".", "development", rest); const { runDbCommand } = await import("./db.ts"); const [sub, ...dbArgs] = rest.filter((a) => !a.startsWith("--profile=")); await runDbCommand(".", sub, dbArgs); break; } case "profiles": { const { listProfiles } = await import("./profiles.ts"); await listProfiles(rest.find((a) => !a.startsWith("--")) ?? "."); break; } case "doctor": { const { runDoctor } = await import("./doctor.ts"); const healthy = runDoctor(rest.find((a) => !a.startsWith("--")) ?? "."); if (!healthy) process.exitCode = 1; break; } case "test": { const appRoot = rest.find((a) => !a.startsWith("--")) ?? "."; const flag = rest.find((a) => a.startsWith("--profile=")); // Tests default to the `test` profile (config + .env.test), unless overridden. process.env.WRNEXUS_PROFILE = resolveProfile({ explicit: flag?.split("=")[1] ?? "test" }); loadEnv(resolve(appRoot), process.env.WRNEXUS_PROFILE); const { runTests } = await import("./test.ts"); runTests(appRoot, rest); break; } case undefined: case "help": case "--help": case "-h": help(); break; default: { const { loadWorkspaceConfig, runGateway, runProduction } = await import("./workspace.ts"); const workspaceRoot = rest.find((a) => !a.startsWith("--")) ?? "."; try { const workspace = await loadWorkspaceConfig(resolve(workspaceRoot)); const knownEnvironments = new Set([ "development", "production", workspace.defaultEnvironment, ...Object.keys(workspace.environments ?? {}), ]); if (!knownEnvironments.has(command)) throw new Error("unknown-environment-command"); const environmentArgs = rest.filter((arg) => arg !== workspaceRoot); environmentArgs.push(`--environment=${command}`); if (command === "development") { await runGateway(workspaceRoot, environmentArgs); } else { await runProduction(workspaceRoot, environmentArgs); } } catch (error) { if (error instanceof Error && error.message !== "unknown-environment-command") throw error; console.error(`Unknown command or workspace environment: ${command}\n`); help(); process.exit(1); } } } } main().catch((err) => { console.error(err); process.exit(1); });