/** * `wrnexus update [dir] [--version=x.y.z | --latest] [--dry-run]` * * Upgrade an app (or every app in a workspace) to a WrNexus release: * 1. Bump every `@wrnexus/*` dependency to the target version. * 2. `bun install`. * 3. Refresh framework-owned reference files (public/llms.txt) and apply any * versioned, idempotent migrations that newer releases introduce. * 4. Record the applied version in package.json (`"wrnexus": { version }`). * * Target version resolution: `--version=x.y.z` > `--latest` (queries npm) > * the running CLI's own version (the default — pair with `bunx @wrnexus/cli@latest * update` to jump to the newest release with no network guesswork). * * Migrations are CONSERVATIVE: they only add/refresh framework-owned things and * never clobber your own code or edited CLAUDE.md. Add new ones to `MIGRATIONS` * as the framework evolves — that is how "new things" reach existing apps. */ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; import { dirname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts"; import { inspectProject } from "./doctor.ts"; /** The version of the CLI currently running (its own package.json). */ function cliVersion(): string { try { return JSON.parse(readFileSync(join(import.meta.dir, "..", "package.json"), "utf8")).version; } catch { return "0.0.0"; } } /** Query the registry for the latest published `@wrnexus/cli` version. */ function latestPublished(): string | null { try { const npm = process.platform === "win32" ? "npm.cmd" : "npm"; const out = spawnSync(npm, ["view", "@wrnexus/cli", "version"], { encoding: "utf8" }); const v = (out.stdout ?? "").trim(); return /^\d+\.\d+\.\d+/.test(v) ? v : null; } catch { return null; } } /** Numeric compare of `x.y.z` (pre-release/build tags ignored). */ function cmp(a: string, b: string): number { const pa = a.split("-")[0]!.split(".").map(Number); const pb = b.split("-")[0]!.split(".").map(Number); for (let i = 0; i < 3; i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d) return d > 0 ? 1 : -1; } return 0; } interface MigrationCtx { appRoot: string; from: string; to: string; dryRun: boolean; log: (msg: string) => void; } interface Migration { /** Framework version that introduced this change. Runs when `from < version <= to`. */ version: string; id: string; description: string; apply: (ctx: MigrationCtx) => void; } const VSCODE_EXTENSIONS = `{ "recommendations": ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] } `; const LEGACY_VSCODE_EXTENSIONS = JSON.stringify( { recommendations: ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"], }, null, 2, ) + "\n"; /** * Versioned, idempotent upgrade steps. Each MUST be safe to re-run and MUST NOT * overwrite user code. Append new entries with the version that ships them. */ const MIGRATIONS: Migration[] = [ { version: "0.2.8", id: "gitignore-artifacts", description: "Ensure .gitignore covers framework build artifacts", apply(ctx) { const file = join(ctx.appRoot, ".gitignore"); const want = ["node_modules/", "dist/", ".wrnexus/", "*.db"]; const current = existsSync(file) ? readFileSync(file, "utf8") : ""; const have = new Set(current.split(/\r?\n/).map((l) => l.trim())); const missing = want.filter((w) => !have.has(w)); if (missing.length === 0) return; ctx.log(`+ .gitignore: ${missing.join(", ")}`); if (ctx.dryRun) return; const next = current.replace(/\s*$/, "") + "\n" + missing.join("\n") + "\n"; writeFileSync(file, next.replace(/^\n+/, ""), "utf8"); }, }, { version: "0.2.14", id: "complete-gitignore", description: "Add the complete WRNexusJS generated-project ignore set", apply(ctx) { const file = join(ctx.appRoot, ".gitignore"); const want = [ "node_modules/", "dist/", ".wrnexus/", "**/.wrnexus/", "coverage/", ".env", ".env.*", "!.env.example", "*.log", "*.db", "*.db-shm", "*.db-wal", "*.sqlite", "*.sqlite3", "mobile/android/", "mobile/ios/", "mobile/.expo/", ".idea/", ".vscode/", ".DS_Store", "Thumbs.db", "*.tsbuildinfo", ".eslintcache", ]; const current = existsSync(file) ? readFileSync(file, "utf8") : ""; const have = new Set(current.split(/\r?\n/).map((line) => line.trim())); const missing = want.filter((entry) => !have.has(entry)); if (!missing.length) return; ctx.log(`+ .gitignore: ${missing.join(", ")}`); if (!ctx.dryRun) { const next = `${current.replace(/\s*$/, "")}\n${missing.join("\n")}\n`.replace(/^\n+/, ""); writeFileSync(file, next, "utf8"); } }, }, { version: "0.2.14", id: "production-scripts", description: "Add standard production build and start scripts", apply(ctx) { // Workspace roots are manifests, not runnable applications. if (!existsSync(join(ctx.appRoot, "app", "pages"))) return; const file = join(ctx.appRoot, "package.json"); const pkg = JSON.parse(readFileSync(file, "utf8")) as Record; const scripts = (pkg.scripts ??= {}); const additions: Record = {}; if (!scripts.build) additions.build = "wrnexus build ."; if (!scripts.start) additions.start = "bun dist/server.js"; if (!scripts.production) additions.production = "bun run build && bun run start"; if (!Object.keys(additions).length) return; Object.assign(scripts, additions); ctx.log(`+ package scripts: ${Object.keys(additions).join(", ")}`); if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); }, }, { version: "0.2.18", id: "add-helpers-package", description: "Add @wrnexus/helpers to existing runnable applications", apply(ctx) { // Workspace roots do not consume application Context helpers directly. if (!existsSync(join(ctx.appRoot, "app", "pages"))) return; const file = join(ctx.appRoot, "package.json"); const pkg = JSON.parse(readFileSync(file, "utf8")) as Record; const dependencies = (pkg.dependencies ??= {} as Record); if (dependencies["@wrnexus/helpers"]) return; dependencies["@wrnexus/helpers"] = `^${ctx.to}`; ctx.log(`+ @wrnexus/helpers ^${ctx.to}`); if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); }, }, { version: "0.2.19", id: "vscode-formatting", description: "Add safe VS Code format-on-save defaults and extension recommendations", apply(ctx) { const vscode = join(ctx.appRoot, ".vscode"); const settings = join(vscode, "settings.json"); const extensions = join(vscode, "extensions.json"); const prettierIgnore = join(ctx.appRoot, ".prettierignore"); const gitignore = join(ctx.appRoot, ".gitignore"); const files: Array<[string, string, string]> = [ [ settings, JSON.stringify( { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "prettier.requireConfig": true, "[wrn]": { "editor.defaultFormatter": "wrnexus.wrnexus", "editor.formatOnSave": true, }, }, null, 2, ) + "\n", ".vscode/settings.json", ], [extensions, VSCODE_EXTENSIONS, ".vscode/extensions.json"], ]; for (const [file, contents, label] of files) { if (existsSync(file)) continue; ctx.log(`+ ${label}`); if (!ctx.dryRun) { mkdirSync(vscode, { recursive: true }); writeFileSync(file, contents, "utf8"); } } const prettier = existsSync(prettierIgnore) ? readFileSync(prettierIgnore, "utf8") : ""; if (!new Set(prettier.split(/\r?\n/).map((line) => line.trim())).has("CLAUDE.md")) { ctx.log("+ .prettierignore: CLAUDE.md"); if (!ctx.dryRun) { writeFileSync(prettierIgnore, `${prettier.replace(/\s*$/, "")}\nCLAUDE.md\n`, "utf8"); } } if (existsSync(gitignore)) { const current = readFileSync(gitignore, "utf8"); const lines = current.split(/\r?\n/); const vscodeIgnore = lines.findIndex((line) => line.trim() === ".vscode/"); const needed = [".vscode/*", "!.vscode/settings.json", "!.vscode/extensions.json"]; if (vscodeIgnore >= 0 || needed.some((entry) => !lines.includes(entry))) { ctx.log("~ .gitignore: track shared VS Code configuration"); if (!ctx.dryRun) { const filtered = lines.filter((line) => line.trim() !== ".vscode/"); const have = new Set(filtered.map((line) => line.trim())); for (const entry of needed) if (!have.has(entry)) filtered.push(entry); writeFileSync(gitignore, filtered.join("\n").replace(/\n*$/, "\n"), "utf8"); } } } }, }, { version: "0.2.20", id: "normalize-vscode-extensions", description: "Normalize the framework-generated VS Code recommendations file", apply(ctx) { const file = join(ctx.appRoot, ".vscode", "extensions.json"); if (!existsSync(file) || readFileSync(file, "utf8") !== LEGACY_VSCODE_EXTENSIONS) return; ctx.log("~ .vscode/extensions.json formatting normalized"); if (!ctx.dryRun) writeFileSync(file, VSCODE_EXTENSIONS, "utf8"); }, }, { version: "0.2.21", id: "in-process-hmr-runtime", description: "No project-file changes; the in-process HMR behavior is runtime-only", apply() { // Explicit no-op: updating @wrnexus/dev-server is sufficient. }, }, { version: "0.2.22", id: "nested-forward-auth-origin", description: "No project-file changes; nested forward-auth headers are fixed at runtime", apply() { // Explicit no-op: updating @wrnexus/dev-server and helpers is sufficient. }, }, { version: "0.2.23", id: "custom-required-validation-message", description: "No project-file changes; custom required messages are provided at runtime", apply() { // Explicit no-op: updating @wrnexus/validation is sufficient. }, }, { version: "0.2.24", id: "package-usage-documentation", description: "No project-file changes; package usage documentation is expanded", apply() { // Explicit no-op: this release updates package and portal documentation only. }, }, { version: "0.2.25", id: "reactive-state-attributes", description: "No project-file changes; state expressions in attributes are reactive at runtime", apply() { // Explicit no-op: updating @wrnexus/compiler and @wrnexus/csr is sufficient. }, }, { version: "0.2.26", id: "class-improvements-attributes", description: "Adds conditional class directives, route state parameters, formatter improvements, and HMR fixes.", apply() { // No project file migration is required for this framework release. }, }, { version: "0.2.27", id: "component-improvements-tag", description: "Adds Component renders system allowed 2 ways.", apply() { // No project file migration is required for this framework release. }, }, { version: "0.2.28", id: "layout-component-improvements", description: "Added new block layout for component and improved component rendering.", apply() { // No project file migration is required for this framework release. }, }, { version: "0.2.29", id: "workspace-config-improvements", description: "Adds environment-aware workspace domains, app URL helpers, component tag syntax, layouts, and editor improvements.", apply() { // No generated project files require automatic changes. // Existing applications remain compatible with this release. }, }, { version: "0.2.30", id: "workspace-config-auth-improvements", description: "Adds dynamic workspace origins, workspace forward-auth resolution, layout support, component tag syntax, and editor improvements.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.31", id: "lifecycle-improvements", description: "Adds component functions, lifecycle hooks, state watchers, optimized behavior cleanup, and CSR navigation lifecycle support.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.32", id: "fix-runtime-bug", description: "Fixes safe encoding and browser parsing of multiline component behavior metadata.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.33", id: "fix-runtime-bug", description: "Fixes safe encoding and browser parsing of multiline component behavior metadata.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.34", id: "reactive-bug", description: "Fixes component behavior encoding, lifecycle and watcher metadata parsing, CSR cleanup, and reactive form control property updates.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.35", id: "class-bug", description: "Fixes component behavior encoding, lifecycle and watcher metadata parsing, CSR cleanup, and reactive form control property updates.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.36", id: "attribute-bug", description: "Adds support for Tailwind arbitrary-value class directives and improves reactive class bindings.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.37", id: "loop-condition-fix", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.38", id: "loop-condition-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.39", id: "server-function-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.40", id: "conditions-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.41", id: "conditions-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.42", id: "conditions-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.43", id: "conditions-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.44", id: "conditions-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.45", id: "conditions-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.46", id: "conditions-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.47", id: "conditions-bug", description: "Adds server-rendered component each/if blocks and fixes dynamic component rendering.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.48", id: "components-update", description: "Adds reactive hydration support for server-rendered each loop locals.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.49", id: "components-update", description: "Adds reactive hydration support for server-rendered each loop locals.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.50", id: "components-update", description: "Adds reactive hydration support for server-rendered each loop locals.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.51", id: "ui-package-repair", description: "Repairs bundled UI component syntax and app-local stylesheet processing.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.52", id: "shared-marketing-components", description: "Adds reusable FAQ, announcement, and back-to-top UI components.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.53", id: "faq-accordion-tailwind-safelist", description: "Ensures conditional FAQ accordion layout utilities are generated by Tailwind.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.54", id: "essential-ui-component-system", description: "Adds the PDF-defined minimum and essential shared UI component system.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.55", id: "expanded-ui-foundation-catalog", description: "Adds the expanded typography, layout, action, surface, card, and indicator catalog.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.56", id: "ui-motion-system", description: "Adds consistent, accessible motion and modern interactions across the shared UI catalog.", apply() { // No generated application files require automatic migration. }, }, { version: "0.2.57", id: "semantic-theme-palettes", description: "Adds eight semantic UI palettes and validated complete custom palette configuration.", apply() { // Existing theme configuration remains compatible; blue is the default palette. }, }, { version: "0.2.58", id: "complete-ui-component-catalog", description: "Completes the PDF-aligned UI catalog and ships generated props, slots, and events metadata.", apply() { // UI components remain auto-discovered; no application files require migration. }, }, { version: "0.2.59", id: "typed-wrn-declarations", description: "Adds explicit prop and state types, local type declarations, and typed component runtime validation.", apply() { // Existing inferred declarations remain compatible; typed syntax is opt-in. }, }, { version: "0.2.60", id: "page-owned-shell-backgrounds", description: "Lets pages own their backgrounds by keeping shared page shells transparent.", apply() { // The UI stylesheet update applies automatically. }, }, { version: "0.2.61", id: "spacing-neutral-component-boundaries", description: "Removes default outer component spacing and standardizes root class customization.", apply() { // The UI component and stylesheet updates apply automatically. }, }, { version: "0.2.62", id: "application-theme-contract-and-public-ui", description: "Exposes semantic theme helpers to application UI and promotes the public-site components into @wrnexus/ui.", apply() { // Package auto-discovery and the root-level theme aliases apply automatically. }, }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ export function updateMigrationVersions(): string[] { return [...new Set(MIGRATIONS.map((migration) => migration.version))]; } /** Bump every `@wrnexus/*` range to `^target`. Returns the human-readable changes. */ function bumpDeps(pkg: Record, target: string): string[] { const changed: string[] = []; const next = `^${target}`; for (const field of ["dependencies", "devDependencies", "peerDependencies"]) { const deps = pkg[field] as Record | undefined; if (!deps) continue; for (const name of Object.keys(deps)) { if (name.startsWith("@wrnexus/") && deps[name] !== next) { changed.push(`${name} ${deps[name]} → ${next}`); deps[name] = next; } } } return changed; } /** The framework version an app was last updated to (or its installed CLI version). */ function appVersion(appRoot: string, pkg: Record): string { const marker = (pkg.wrnexus as { version?: string } | undefined)?.version; if (marker) return marker; try { const p = join(appRoot, "node_modules", "@wrnexus", "cli", "package.json"); if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8")).version; } catch { /* ignore */ } return "0.0.0"; } /** Refresh pure framework-owned reference files. Never touches user-edited CLAUDE.md. */ function refreshFrameworkFiles(appRoot: string, dryRun: boolean, log: (m: string) => void): void { // Public llms.txt is generated and web-visible, so it is safe to refresh. const llms = join(appRoot, "public", "llms.txt"); if (!existsSync(llms) || readFileSync(llms, "utf8") !== AI_GUIDE) { log("~ public/llms.txt refreshed"); if (!dryRun) { mkdirSync(join(appRoot, "public"), { recursive: true }); writeFileSync(llms, AI_GUIDE, "utf8"); } } // CLAUDE.md is often user-edited — only create it when absent. const claude = join(appRoot, "CLAUDE.md"); if (!existsSync(claude)) { log("+ CLAUDE.md created"); if (!dryRun) writeFileSync(claude, CLAUDE_MD, "utf8"); } } /** Update a single app dir (bump its package.json, refresh files, run migrations). */ interface UpdatedApp { root: string; packagePath: string; pkg: Record; } function backupProjectFiles(appRoot: string, from: string, target: string): string { const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const backup = join(appRoot, ".wrnexus", "update-backups", `${stamp}-${from}-to-${target}`); mkdirSync(backup, { recursive: true }); for (const name of [ "package.json", "wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs", "tsconfig.json", ".gitignore", ".prettierignore", ".vscode/settings.json", ".vscode/extensions.json", "CLAUDE.md", "public/llms.txt", ]) { const source = join(appRoot, name); if (existsSync(source)) { const destination = join(backup, name); mkdirSync(dirname(destination), { recursive: true }); cpSync(source, destination); } } return backup; } /** Update one app without marking success until install and verification pass. */ export function updateApp(appRoot: string, target: string, dryRun: boolean): UpdatedApp | null { const pkgPath = join(appRoot, "package.json"); if (!existsSync(pkgPath)) { console.log(` ⚠ ${appRoot}: no package.json — skipped`); return null; } const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record; const from = appVersion(appRoot, pkg); const log = (m: string) => console.log(` ${m}`); console.log(` ▸ ${pkg.name ?? appRoot} (${from} → ${target})`); if (!dryRun) log(`backup: ${backupProjectFiles(appRoot, from, target)}`); const changes = bumpDeps(pkg, target); changes.forEach((c) => log(c)); if (!changes.length) log("dependencies already current"); refreshFrameworkFiles(appRoot, dryRun, log); for (const m of MIGRATIONS) { if (cmp(m.version, from) > 0 && cmp(m.version, target) <= 0) { m.apply({ appRoot, from, to: target, dryRun, log }); } } // A migration may have conservatively edited package.json. Merge those // changes back before writing the dependency bump so neither side is lost. if (!dryRun) { Object.assign(pkg, JSON.parse(readFileSync(pkgPath, "utf8")) as Record); bumpDeps(pkg, target); } if (!dryRun) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8"); return { root: appRoot, packagePath: pkgPath, pkg }; } function verifyApp(app: UpdatedApp): boolean { const health = inspectProject(app.root); const failed = health.filter((check) => !check.ok && check.name !== "configuration"); if (failed.length) { for (const check of failed) console.error(` ✗ ${check.name}: ${check.detail}`); return false; } const scripts = (app.pkg.scripts ?? {}) as Record; const commands: string[][] = []; if (scripts.check) commands.push(["run", "check"]); if (scripts.build) commands.push(["run", "build"]); for (const args of commands) { console.log(`\n Verifying ${app.pkg.name ?? app.root}: bun ${args.join(" ")}…`); const result = spawnSync(process.execPath, args, { cwd: app.root, stdio: "inherit" }); if (result.status !== 0) return false; } return true; } /** Load a `wrnexus.workspace.ts`, returning its app dirs (or null if not a workspace). */ async function workspaceApps(root: string): Promise { for (const f of ["wrnexus.workspace.ts", "wrnexus.workspace.js", "wrnexus.workspace.mjs"]) { const path = join(root, f); if (!existsSync(path)) continue; const mod = (await import(pathToFileURL(path).href)) as { default?: { apps?: { dir: string }[] }; }; return (mod.default?.apps ?? []).map((a) => resolve(root, a.dir)); } return null; } export async function runUpdate(dir: string, args: string[]): Promise { const root = resolve(dir); const dryRun = args.includes("--dry-run"); const verify = !args.includes("--no-verify"); const versionArg = args.find((a) => a.startsWith("--version="))?.split("=")[1]; const target = versionArg ?? (args.includes("--latest") ? latestPublished() : null) ?? cliVersion(); // Always let the target CLI apply its own migrations. Without this handoff, // an older installed CLI could bump dependency versions but would not know // about syntax/config migrations shipped by the newer release. if (!args.includes("--delegated") && cmp(target, cliVersion()) > 0) { console.log(`\n Fetching WRNexusJS CLI ${target} to run its project migrations…`); const forwarded = args.filter( (arg) => !arg.startsWith("--version=") && arg !== "--latest" && arg !== "--delegated", ); const result = spawnSync( process.execPath, [ "x", `@wrnexus/cli@${target}`, "update", dir, `--version=${target}`, "--delegated", ...forwarded, ], { cwd: root, stdio: "inherit" }, ); if (result.status !== 0) process.exitCode = result.status ?? 1; return; } console.log(`\n ⚡ wrnexus update → ${target}${dryRun ? " (dry run)" : ""}\n`); // Workspace → update the root manifest + every app; else just this app. const apps = await workspaceApps(root); const targets = apps ? [root, ...apps] : [root]; const updated = targets.map((t) => updateApp(t, target, dryRun)).filter(Boolean) as UpdatedApp[]; if (dryRun) { console.log(`\n Dry run — no files written. Re-run without --dry-run to apply.\n`); return; } // One install at the top (Bun workspaces hoist), using the bun that's running us. console.log(`\n Installing…`); const res = spawnSync(process.execPath, ["install"], { cwd: root, stdio: "inherit" }); if (res.status !== 0) { console.error(`\n ⚠ bun install exited with ${res.status}. Fix the error and re-run.`); process.exit(res.status ?? 1); } if (verify) { const appsToVerify = apps ? updated.filter((app) => app.root !== root) : updated; for (const app of appsToVerify) { if (!verifyApp(app)) { console.error( `\n ✗ Update files were applied, but verification failed. Fix the reported error and re-run wrnexus update.`, ); process.exitCode = 1; return; } } } for (const app of updated) { app.pkg.wrnexus = { ...(app.pkg.wrnexus as object), version: target }; writeFileSync(app.packagePath, JSON.stringify(app.pkg, null, 2) + "\n", "utf8"); } console.log(`\n ✓ Updated and verified ${updated.length} package(s) at ${target}.`); console.log(` Backups are under .wrnexus/update-backups/. Review and redeploy.\n`); }