From 5ce45b4973fe11d3762e7406ec33812ade4c0482 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sun, 19 Jul 2026 21:57:55 +0530 Subject: [PATCH] feat(cli): add production workspace command --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/README.md | 12 ++++ .publish/cli/package.json | 22 +++---- .publish/compiler/package.json | 2 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 26 ++++---- .publish/encryption/package.json | 2 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 4 +- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +- .publish/ssr/package.json | 4 +- .publish/styles/package.json | 4 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 4 +- .publish/validation/package.json | 2 +- packages/ai/package.json | 2 +- packages/authz/package.json | 2 +- packages/cli/README.md | 12 ++++ packages/cli/package.json | 2 +- packages/cli/src/index.ts | 7 +++ packages/cli/src/update.ts | 9 +++ packages/cli/src/workspace.ts | 93 +++++++++++++++++++++++++++-- packages/cli/test/workspace.test.ts | 24 +++++++- packages/compiler/package.json | 2 +- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/encryption/package.json | 2 +- packages/helpers/package.json | 2 +- packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- 58 files changed, 235 insertions(+), 90 deletions(-) diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 5675a469..74ed7b3b 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 0fced98f..699e73ea 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/README.md b/.publish/cli/README.md index 640b6200..9b5cc3f0 100644 --- a/.publish/cli/README.md +++ b/.publish/cli/README.md @@ -36,6 +36,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r | `wrnexus workspace ` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | | `wrnexus workspace add ` | Add and register an app in 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 every workspace app in production. | | `wrnexus generate ` | Scaffold a `page` \| `component` \| `api` \| `schema`. | | `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | | `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. | @@ -168,6 +169,17 @@ wrnexus workspace acme wrnexus gateway --port=3000 ``` +For a complete production start, use the first-class workspace orchestrator: + +```bash +wrnexus production --host=0.0.0.0 --port=3000 +``` + +It builds every registered app, applies default and named-database SQL migrations +when present, and starts the production gateway only after preparation succeeds. +Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are +managed separately; `--environment=` selects another workspace environment. + From a workspace root, add and register another app in one command: ```bash diff --git a/.publish/cli/package.json b/.publish/cli/package.json index baf52689..758cde64 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.63", - "@wrnexus/router": "^0.2.63", - "@wrnexus/csr": "^0.2.63", - "@wrnexus/compiler": "^0.2.63", - "@wrnexus/styles": "^0.2.63", - "@wrnexus/dev-server": "^0.2.63", - "@wrnexus/ui": "^0.2.63", - "@wrnexus/validation": "^0.2.63", - "@wrnexus/i18n": "^0.2.63", - "@wrnexus/db": "^0.2.63" + "@wrnexus/core": "^0.2.64", + "@wrnexus/router": "^0.2.64", + "@wrnexus/csr": "^0.2.64", + "@wrnexus/compiler": "^0.2.64", + "@wrnexus/styles": "^0.2.64", + "@wrnexus/dev-server": "^0.2.64", + "@wrnexus/ui": "^0.2.64", + "@wrnexus/validation": "^0.2.64", + "@wrnexus/i18n": "^0.2.64", + "@wrnexus/db": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 420febf1..47b83535 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index bd7256b5..58ed18cc 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index cabf4ceb..ce64a7ed 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.63" + "@wrnexus/core": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 2725a8c8..ae3e29a0 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 72778121..5223a415 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.63", - "@wrnexus/router": "^0.2.63", - "@wrnexus/ssr": "^0.2.63", - "@wrnexus/csr": "^0.2.63", - "@wrnexus/compiler": "^0.2.63", - "@wrnexus/styles": "^0.2.63", - "@wrnexus/ui": "^0.2.63", - "@wrnexus/validation": "^0.2.63", - "@wrnexus/i18n": "^0.2.63", - "@wrnexus/db": "^0.2.63", - "@wrnexus/pubsub": "^0.2.63", - "@wrnexus/uploader": "^0.2.63" + "@wrnexus/core": "^0.2.64", + "@wrnexus/router": "^0.2.64", + "@wrnexus/ssr": "^0.2.64", + "@wrnexus/csr": "^0.2.64", + "@wrnexus/compiler": "^0.2.64", + "@wrnexus/styles": "^0.2.64", + "@wrnexus/ui": "^0.2.64", + "@wrnexus/validation": "^0.2.64", + "@wrnexus/i18n": "^0.2.64", + "@wrnexus/db": "^0.2.64", + "@wrnexus/pubsub": "^0.2.64", + "@wrnexus/uploader": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 76cf5fd6..0d88ea66 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index ca14cbbe..d5707835 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.63" + "@wrnexus/core": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 5548efdb..e6349d3f 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.63" + "@wrnexus/core": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index 46e6a653..7c712fd8 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index 3e80487a..298a2f27 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.63" + "@wrnexus/native": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index 5ff7d955..b97e4a0f 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 79dbbc2e..27e1b60c 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index fe0cc4df..72657177 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 3dcf5c6a..f545e1df 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index 37eaa221..628226ad 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 3a87b756..cbbc002d 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.63", - "@wrnexus/core": "^0.2.63" + "@wrnexus/compiler": "^0.2.64", + "@wrnexus/core": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 016d2287..f0924e84 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.63" + "@wrnexus/core": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index f29bfcec..d81f2e96 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.63" + "@wrnexus/uploader": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index c1fff68c..79228f37 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 9cbfb26e..e15532b1 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index 71daffd8..ec96f696 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -25,7 +25,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.63" + "@wrnexus/core": "^0.2.64" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 95062406..4525bd08 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.63" + "@wrnexus/core": "^0.2.64" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index 10a652ec..161e75b2 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/packages/ai/package.json b/packages/ai/package.json index 35a562ea..c92be37e 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", diff --git a/packages/authz/package.json b/packages/authz/package.json index f800d18e..94bd334a 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/cli/README.md b/packages/cli/README.md index 640b6200..9b5cc3f0 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -36,6 +36,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r | `wrnexus workspace ` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | | `wrnexus workspace add ` | Add and register an app in 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 every workspace app in production. | | `wrnexus generate ` | Scaffold a `page` \| `component` \| `api` \| `schema`. | | `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | | `wrnexus generate docker` | Scaffold `Dockerfile`, `.dockerignore`, and `docker-compose.yml`. | @@ -168,6 +169,17 @@ wrnexus workspace acme wrnexus gateway --port=3000 ``` +For a complete production start, use the first-class workspace orchestrator: + +```bash +wrnexus production --host=0.0.0.0 --port=3000 +``` + +It builds every registered app, applies default and named-database SQL migrations +when present, and starts the production gateway only after preparation succeeds. +Use `--prepare-only`, `--no-build`, or `--no-migrate` when deployment stages are +managed separately; `--environment=` selects another workspace environment. + From a workspace root, add and register another app in one command: ```bash diff --git a/packages/cli/package.json b/packages/cli/package.json index 1fd54117..bbe37111 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 11074134..eb791a64 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -46,6 +46,7 @@ Usage: 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 generate Scaffold a page | component | api | schema wrnexus generate routes | docker | mobile Generate routes or scaffold deployment targets @@ -108,6 +109,12 @@ async function main(): Promise { 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") { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index d42a5d51..aa4e66bb 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -637,6 +637,15 @@ const MIGRATIONS: Migration[] = [ // The shared UI stylesheet update applies automatically. }, }, + { + version: "0.2.64", + id: "production-workspace-orchestrator", + description: + "Adds a first-class production command that builds, migrates, and serves every registered workspace app.", + apply() { + // Existing workspaces can invoke `wrnexus production` without generated-file changes. + }, + }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ diff --git a/packages/cli/src/workspace.ts b/packages/cli/src/workspace.ts index e07d3e3f..5af6a0a8 100644 --- a/packages/cli/src/workspace.ts +++ b/packages/cli/src/workspace.ts @@ -10,7 +10,7 @@ * cross-process). `wrnexus.workspace.ts` maps each app to the domains it serves. */ -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { scaffoldApp } from "./create.ts"; @@ -68,7 +68,8 @@ export const workspaceFiles = (name: string): Record => ({ "workspaces": ["apps/*", "packages/*"], "scripts": { "dev": "wrnexus gateway", - "gateway": "wrnexus gateway" + "gateway": "wrnexus gateway", + "production": "wrnexus production" }, "devDependencies": { "@wrnexus/cli": "${frameworkVersion}" @@ -394,8 +395,7 @@ export async function runGateway(root: string, args: string[]): Promise { profileArg?.split("=")[1] || process.env.WRNEXUS_ENV || process.env.WRNEXUS_PROFILE || - config.defaultEnvironment || - (args.includes("--prod") ? "production" : "development"); + (args.includes("--prod") ? "production" : config.defaultEnvironment || "development"); const resolved = resolveWorkspaceConfig(config, environment); @@ -426,6 +426,91 @@ export async function runGateway(root: string, args: string[]): Promise { }); } +/** Return default/named database targets that contain SQL migrations. */ +export function workspaceMigrationTargets(appRoot: string): Array { + const dbRoot = join(resolve(appRoot), "app", "db"); + if (!existsSync(dbRoot)) return []; + const targets: Array = []; + const hasSql = (dir: string) => + existsSync(dir) && + readdirSync(dir, { withFileTypes: true }).some( + (entry) => entry.isFile() && entry.name.endsWith(".sql"), + ); + if (hasSql(join(dbRoot, "migrations"))) targets.push(null); + for (const entry of readdirSync(dbRoot, { withFileTypes: true })) { + if ( + entry.isDirectory() && + entry.name !== "migrations" && + hasSql(join(dbRoot, entry.name, "migrations")) + ) { + targets.push(entry.name); + } + } + return targets; +} + +/** Build, migrate, then serve every registered workspace app in production mode. */ +export async function runProduction(root: string, args: string[]): Promise { + const workspaceRoot = resolve(root); + const config = await loadWorkspaceConfig(workspaceRoot); + const shouldBuild = !args.includes("--no-build"); + const shouldMigrate = !args.includes("--no-migrate"); + const prepareOnly = args.includes("--prepare-only"); + const environmentArg = args.find((arg) => arg.startsWith("--environment=")); + const profileArg = args.find((arg) => arg.startsWith("--profile=")); + const environment = environmentArg?.split("=")[1] || profileArg?.split("=")[1] || "production"; + + process.env.NODE_ENV = "production"; + process.env.WRNEXUS_ENV = environment; + process.env.WRNEXUS_PROFILE = environment; + console.log(`\n ⚡ Preparing WRNexus workspace (${environment})\n`); + + if (shouldBuild) { + const { runBuild } = await import("./build.ts"); + for (const app of config.apps) { + const appRoot = resolve(workspaceRoot, app.dir); + if (!existsSync(join(appRoot, "package.json"))) { + throw new Error(`Workspace app '${app.name}' has no package.json at ${appRoot}.`); + } + console.log(`\n ▸ Building ${app.name}`); + await runBuild(appRoot); + } + } else { + console.log(" = builds skipped (--no-build)"); + } + + if (shouldMigrate) { + const { runDbCommand } = await import("./db.ts"); + for (const app of config.apps) { + const appRoot = resolve(workspaceRoot, app.dir); + const targets = workspaceMigrationTargets(appRoot); + if (!targets.length) { + console.log(` = ${app.name}: no SQL migrations`); + continue; + } + for (const target of targets) { + console.log(`\n ▸ Migrating ${app.name}${target ? ` (${target})` : ""}`); + await runDbCommand(appRoot, "migrate", target ? [`--db=${target}`] : []); + } + } + } else { + console.log(" = migrations skipped (--no-migrate)"); + } + + console.log("\n ✓ Workspace builds and migrations are ready.\n"); + if (prepareOnly) return; + + const gatewayArgs = args.filter( + (arg) => arg !== "--prepare-only" && arg !== "--no-build" && arg !== "--no-migrate", + ); + if (!gatewayArgs.some((arg) => arg.startsWith("--environment="))) { + gatewayArgs.push(`--environment=${environment}`); + } + if (!gatewayArgs.some((arg) => arg.startsWith("--host="))) gatewayArgs.push("--host=0.0.0.0"); + if (!gatewayArgs.some((arg) => arg.startsWith("--port="))) gatewayArgs.push("--port=3000"); + await runGateway(workspaceRoot, gatewayArgs); +} + function environmentVariableName(appName: string): string { return `WRNEXUS_APP_${appName.replace(/[^A-Za-z0-9]/g, "_").toUpperCase()}_URL`; } diff --git a/packages/cli/test/workspace.test.ts b/packages/cli/test/workspace.test.ts index 9cdb6d33..d547430c 100644 --- a/packages/cli/test/workspace.test.ts +++ b/packages/cli/test/workspace.test.ts @@ -1,9 +1,14 @@ import { expect, test } from "bun:test"; -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { currentCliVersion } from "../src/update-notifier.ts"; -import { addWorkspaceApp, insertWorkspaceApp, workspaceFiles } from "../src/workspace.ts"; +import { + addWorkspaceApp, + insertWorkspaceApp, + workspaceFiles, + workspaceMigrationTargets, +} from "../src/workspace.ts"; test("workspace templates pin the running framework release", () => { const files = workspaceFiles("acme"); @@ -15,6 +20,21 @@ test("workspace templates pin the running framework release", () => { expect(sharedPackage.dependencies["@wrnexus/pubsub"]).toBe(version); expect(files["README.md"]).toContain("http://127.0.0.1:3000"); expect(files["README.md"]).toContain("internal gateway targets"); + expect(JSON.parse(files["package.json"]!).scripts.production).toBe("wrnexus production"); +}); + +test("production workspace detects default and named SQL migrations", () => { + const root = mkdtempSync(join(tmpdir(), "wrnexus-production-migrations-")); + try { + mkdirSync(join(root, "app", "db", "migrations"), { recursive: true }); + mkdirSync(join(root, "app", "db", "analytics", "migrations"), { recursive: true }); + mkdirSync(join(root, "app", "db", "empty", "migrations"), { recursive: true }); + writeFileSync(join(root, "app", "db", "migrations", "0001_init.sql"), "select 1;"); + writeFileSync(join(root, "app", "db", "analytics", "migrations", "0001_init.sql"), "select 1;"); + expect(workspaceMigrationTargets(root)).toEqual([null, "analytics"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } }); test("insertWorkspaceApp handles nested arrays and comments", () => { diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 27d8739d..2a4486c7 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/core/package.json b/packages/core/package.json index 561ed378..a48150b8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index c6fa3ffd..822c78c2 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index ce644be7..3ff3933d 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 5270407f..f32051a3 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 73ae7b93..aba82f1a 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 976e5f31..c94ef104 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 64aa934b..f31cf8bd 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index f9f55d89..f030fae1 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 34122d05..705d89ef 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index cbd40b34..c821a3ba 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index a8a51562..f317f9a2 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index f65135b8..159d90f1 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index ba228a18..ea9fad27 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index e486b905..284f739b 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index 91825497..545e3222 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index ea4e234f..cae93cec 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index 79a0dd5f..1968c6c4 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.63", + "version": "0.2.64", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index 57a108a6..cd060a3d 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index ebd64e5c..1c1ed4fd 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index e4e305df..31efe175 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 46f37ad2..5962d38c 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index b081cab5..e7b04f73 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.63", + "version": "0.2.64", "private": true, "type": "module", "main": "src/index.ts",