/** * `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: source rewrites are syntax-aware, idempotent, and * protected by a complete pre-update backup. User-owned files are never replaced wholesale. Add new ones to `MIGRATIONS` * as the framework evolves — that is how "new things" reach existing apps. */ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { spawnSync } from "node:child_process"; import { basename, dirname, join, relative, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { formatWrn, parse } from "@wrnexus/syntax"; import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts"; import { inspectProject, type DoctorCheck } 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; explicitImports: boolean; report: MigrationReport; log: (msg: string) => void; } export interface MigrationReport { changedAutomatically: string[]; needsReview: string[]; unresolvedImports: string[]; ambiguousFunctions: string[]; legacyOutputPayloads: string[]; parseFailures: string[]; } 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"; function walkProjectFiles(dir: string, extension: string): string[] { if (!existsSync(dir)) return []; const out: string[] = []; for (const entry of readdirSync(dir)) { if (["node_modules", "dist", ".git", ".wrnexus"].includes(entry)) continue; const path = join(dir, entry); const stat = statSync(path); if (stat.isDirectory()) out.push(...walkProjectFiles(path, extension)); else if (stat.isFile() && path.endsWith(extension)) out.push(path); } return out; } function formatInlineProps(source: string): string { return source.replace( /(^[ \t]*)props\s*\{([^{}\n]*)\}/gm, (whole, indent: string, body: string) => { const declaration = /([A-Za-z_$][\w$]*)(\s*:\s*[^=]+?)?\s*=\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\[\]|\{\}|true|false|null|undefined|-?\d+(?:\.\d+)?)/gy; const values: string[] = []; let offset = 0; while (offset < body.length) { while (/\s/.test(body[offset] ?? "")) offset++; if (offset >= body.length) break; declaration.lastIndex = offset; const match = declaration.exec(body); if (!match || match.index !== offset) return whole; values.push(`${match[1]}${match[2] ?? ""} = ${match[3]}`); offset = declaration.lastIndex; } if (values.length < 2) return whole; return `${indent}props {\n${values.map((value) => `${indent} ${value}`).join("\n")}\n${indent}}`; }, ); } function quoteLegacyDynamicAttributes(source: string): string { let output = ""; let index = 0; while (index < source.length) { const start = source.indexOf("<", index); if (start < 0) return output + source.slice(index); output += source.slice(index, start); if (source.startsWith("", start + 4); if (end < 0) return output + source.slice(start); output += source.slice(start, end + 3); index = end + 3; continue; } let end = start + 1; let quote = ""; let braceDepth = 0; for (; end < source.length; end++) { const char = source[end]!; if (quote) { if (char === "\\") end++; else if (char === quote) quote = ""; continue; } if (char === '"' || char === "'") quote = char; else if (char === "{") braceDepth++; else if (char === "}") braceDepth = Math.max(0, braceDepth - 1); else if (char === ">" && braceDepth === 0) break; } if (end >= source.length) return output + source.slice(start); let tag = source.slice(start, end + 1); if (!/^<\/?[A-Za-z]/.test(tag) || /^<\//.test(tag)) { output += tag; index = end + 1; continue; } tag = tag.replace( /(\s[@:#A-Za-z_$][\w$:.-]*)\s*=\s*\{([^{}']+)\}/g, (_all, name: string, expr: string) => `${name}='{${expr.trim()}}'`, ); output += tag; index = end + 1; } return output; } export function migrateWrnSource(source: string): string { return formatInlineProps(quoteLegacyDynamicAttributes(source)) .replace(/[ \t]+$/gm, "") .replace(/\r\n?/g, "\n") .replace(/\n{3,}$/g, "\n\n") .replace(/\s*$/, "\n"); } export function formatCurrentWrnSource(source: string): string { return formatWrn(migrateWrnSource(source), { insertSpaces: true, tabSize: 2, printWidth: 100, multilineAttributes: true, }); } function findMatching(source: string, open: number, openChar = "{", closeChar = "}"): number { let depth = 0; let quote = ""; for (let index = open; index < source.length; index++) { const char = source[index]!; if (quote) { if (char === "\\") index++; else if (char === quote) quote = ""; continue; } if (char === '"' || char === "'" || char === "`") quote = char; else if (char === openChar) depth++; else if (char === closeChar && --depth === 0) return index; } return -1; } function convertLegacyEmit(source: string, report: MigrationReport, file: string): string { let output = ""; let cursor = 0; while (true) { const start = source.indexOf("$emit(", cursor); if (start < 0) return output + source.slice(cursor); output += source.slice(cursor, start); const end = findMatching(source, start + 5, "(", ")"); if (end < 0) { report.needsReview.push(`${file}: unterminated $emit call`); return output + source.slice(start); } const args = source.slice(start + 6, end); const match = args.match(/^\s*(["'])([A-Za-z_$][\w$:-]*)\1\s*(?:,\s*([\s\S]*))?$/); if (!match) { report.needsReview.push(`${file}: dynamic $emit requires manual review`); output += source.slice(start, end + 1); } else { const payload = (match[3] ?? "").trim(); output += payload ? `output.${match[2]}(${payload})` : `output.${match[2]}()`; } cursor = end + 1; } } function collectLegacyEvents(source: string): { source: string; outputs: string[] } { const outputs: string[] = []; const next = source.replace( /(^|[\s;])@event\s+([A-Za-z_$][\w$]*)\s*=\s*function\b[ \t]*(?:\r?\n)?/gm, (_whole, prefix: string, name: string) => { outputs.push(name); return prefix; }, ); return { source: next, outputs }; } function ensureOutputsBlock( source: string, names: string[], report: MigrationReport, file: string, ): string { if (!names.length) return source; const unique = [...new Set(names)]; const blockMatch = /\boutputs\s*\{/.exec(source); if (blockMatch) { const open = source.indexOf("{", blockMatch.index); const close = findMatching(source, open); if (close < 0) return source; const body = source.slice(open + 1, close); const missing = unique.filter((name) => !new RegExp(`\\b${name}\\s*\\(`).test(body)); if (!missing.length) return source; const indent = source.slice(source.lastIndexOf("\n", blockMatch.index) + 1, blockMatch.index); const additions = missing.map((name) => `${indent} ${name}(payload: unknown)`).join("\n"); for (const name of missing) report.legacyOutputPayloads.push(`${file}: ${name}(payload: unknown)`); return source.slice(0, close) + `\n${additions}\n${indent}` + source.slice(close); } const rootOpen = source.indexOf("{"); if (rootOpen < 0) return source; const lineStart = source.lastIndexOf("\n", rootOpen) + 1; const nonWhitespace = source.slice(lineStart).search(/\S/); const rootIndent = nonWhitespace < 0 ? "" : source.slice(lineStart, lineStart + nonWhitespace); const indent = `${rootIndent} `; const block = `\n${indent}outputs {\n${unique.map((name) => `${indent} ${name}(payload: unknown)`).join("\n")}\n${indent}}\n`; for (const name of unique) report.legacyOutputPayloads.push(`${file}: ${name}(payload: unknown)`); return source.slice(0, rootOpen + 1) + block + source.slice(rootOpen + 1); } function classifyLegacyFunctions(source: string, report: MigrationReport, file: string): string { const functionsMatch = /\bfunctions\s*\{/.exec(source); if (!functionsMatch) return source; const open = source.indexOf("{", functionsMatch.index); const close = findMatching(source, open); if (close < 0) return source; const body = source.slice(open + 1, close); let output = ""; let cursor = 0; const header = /(^|\n)([ \t]*)(?!client\s|server\s|shared\s)(async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)(?:\s*:\s*[^{\n]+)?\s*\{/g; for (let match; (match = header.exec(body));) { const bodyOpen = body.indexOf("{", match.index + match[0].length - 1); const bodyClose = findMatching(body, bodyOpen); if (bodyClose < 0) break; const functionBody = body.slice(bodyOpen + 1, bodyClose); const client = /\b(window|document|localStorage|sessionStorage|navigator|HTMLElement|MouseEvent|KeyboardEvent|output\.|refs\.)\b/.test( functionBody, ); const server = /\b(ctx|Bun|process|database|db\.|serverService|authService|request|response)\b/.test( functionBody, ); let runtime = ""; if (client && !server) runtime = "client "; else if (server && !client) runtime = "server "; else if (!client && !server && !/\b(fetch|Date\.now|Math\.random)\b/.test(functionBody)) runtime = "shared "; else report.ambiguousFunctions.push(`${file}: ${match[4]}`); output += body.slice(cursor, match.index) + match[1] + match[2] + runtime + (match[3] ?? "") + `function ${match[4]}`; const consumedHeader = match[0].lastIndexOf(`function ${match[4]}`) + `function ${match[4]}`.length; output += match[0].slice(consumedHeader); cursor = match.index + match[0].length; header.lastIndex = bodyClose + 1; } output += body.slice(cursor); return source.slice(0, open + 1) + output + source.slice(close); } export function migrateV060WrnSource( source: string, report: MigrationReport, file = "", ): string { const legacy = collectLegacyEvents(source); let next = ensureOutputsBlock(legacy.source, legacy.outputs, report, file); next = convertLegacyEmit(next, report, file); next = next.replace(/\bevent\.detail\b/g, "payload"); next = classifyLegacyFunctions(next, report, file); return migrateWrnSource(next); } function updateV060Config(ctx: MigrationCtx): void { const candidates = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"]; const file = candidates.map((name) => join(ctx.appRoot, name)).find(existsSync); if (!file) return; const current = readFileSync(file, "utf8"); if (/\bimports\s*:/.test(current) && /\bcompatibility\s*:/.test(current)) return; const insertion = `\n imports: { mode: "compatible", autoImport: true, aliases: { "@": "./app" } },\n types: { strict: false, noImplicitAny: false, strictNullChecks: true, checkTemplates: true, checkComponentProps: true, generateDeclarations: true, globalTypes: "./app/types/global.d.ts" },\n functions: { legacyDefaultRuntime: "current" },\n stores: { strictMutations: true, persistence: true },\n compatibility: { legacyEmit: true, legacyEventProps: true, legacyComponentDiscovery: true, stringLayouts: true },`; const index = current.lastIndexOf("}"); if (index < 0) return; const next = current.slice(0, index).replace(/,?\s*$/, "") + "," + insertion + "\n" + current.slice(index); ctx.log(`~ ${file.slice(ctx.appRoot.length + 1)}: v0.6 compatibility configuration`); if (!ctx.dryRun) writeFileSync(file, next, "utf8"); } interface V060SymbolIndex { components: Map; layouts: Map; ui: Set; } function declarationName(file: string): string { const source = readFileSync(file, "utf8"); return ( /\b(?:component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/.exec(source)?.[1] ?? basename(file, ".wrn") ); } function addSymbol(index: Map, name: string, file: string): void { const current = index.get(name) ?? []; current.push(file); index.set(name, current); } function loadUiComponentNames(appRoot: string): Set { const candidates = [ join(appRoot, "node_modules", "@wrnexus", "ui", "component-reference.json"), join(appRoot, "packages", "ui", "component-reference.json"), ]; for (const candidate of candidates) { if (!existsSync(candidate)) continue; try { const parsed = JSON.parse(readFileSync(candidate, "utf8")) as { components?: Array<{ name?: string }>; }; return new Set( (parsed.components ?? []) .map((entry) => entry.name) .filter((name): name is string => Boolean(name)), ); } catch { // Continue to an empty package index. The migration reports unresolved tags. } } return new Set(); } function buildV060SymbolIndex(appRoot: string): V060SymbolIndex { const components = new Map(); const layouts = new Map(); for (const file of walkProjectFiles(join(appRoot, "app", "components"), ".wrn")) { addSymbol(components, declarationName(file), file); } for (const file of walkProjectFiles(join(appRoot, "app", "layouts"), ".wrn")) { const name = declarationName(file); addSymbol(layouts, name, file); addSymbol(layouts, basename(file, ".wrn"), file); } return { components, layouts, ui: loadUiComponentNames(appRoot) }; } function importedLocalNames(source: string): Set { const names = new Set(); for (const statement of source.matchAll( /^import\s+(?:type\s+)?([\s\S]*?)\s+from\s+["'][^"']+["']\s*;?/gm, )) { const clause = statement[1]!.trim(); if (!clause.startsWith("{") && !clause.startsWith("*")) names.add(clause.split(",")[0]!.trim()); for (const named of clause.matchAll( /(?:\{|,)\s*(?:type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?/g, )) { names.add(named[2] ?? named[1]!); } } return names; } function importPathFor(appRoot: string, file: string): string { return `@/${relative(join(appRoot, "app"), file).replace(/\\/g, "/")}`; } function insertImports(source: string, statements: string[]): string { if (!statements.length) return source; const unique = [...new Set(statements)].filter((statement) => !source.includes(statement)); if (!unique.length) return source; const imports = Array.from( source.matchAll( /^import\s+[\s\S]*?(?:;\s*|\n(?=import|\s*(?:page|component|layout|global\s+store|page\s+store)\b))/gm, ), ); const insertion = imports.length ? imports.at(-1)!.index! + imports.at(-1)![0].length : 0; const prefix = insertion ? "\n" : ""; return ( source.slice(0, insertion) + prefix + unique.join("\n") + "\n\n" + source.slice(insertion).replace(/^\s*/, "") ); } function addExplicitImportsToSource( source: string, file: string, appRoot: string, index: V060SymbolIndex, report: MigrationReport, ): string { const imported = importedLocalNames(source); const rootName = /\b(?:page|component|layout|global\s+store|page\s+store)\s+([A-Za-z_$][\w$]*)/.exec( source, )?.[1]; const applicationImports: string[] = []; const uiImports = new Set(); const tags = new Set( Array.from(source.matchAll(/<([A-Z][A-Za-z0-9_$]*)\b/g), (match) => match[1]!), ); for (const name of tags) { if (name === rootName || imported.has(name)) continue; const candidates = index.components.get(name) ?? []; if (candidates.length === 1) { applicationImports.push( `import ${name} from ${JSON.stringify(importPathFor(appRoot, candidates[0]!))}`, ); imported.add(name); } else if (candidates.length > 1) { report.unresolvedImports.push( `${file}: component '${name}' has ${candidates.length} application matches`, ); } else if (index.ui.has(name)) { uiImports.add(name); imported.add(name); } else { report.unresolvedImports.push(`${file}: component '${name}' could not be resolved`); } } if (uiImports.size) { const existingUi = /import\s*\{([\s\S]*?)\}\s*from\s*["']@wrnexus\/ui["']\s*;?/.exec(source); if (existingUi) { const names = new Set([ ...Array.from( existingUi[1]!.matchAll(/(?:^|,)\s*([A-Za-z_$][\w$]*)/g), (match) => match[1]!, ), ...uiImports, ]); source = source.replace( existingUi[0], `import {\n${[...names] .sort() .map((name) => ` ${name},`) .join("\n")}\n} from "@wrnexus/ui"`, ); } else { applicationImports.push( `import {\n${[...uiImports] .sort() .map((name) => ` ${name},`) .join("\n")}\n} from "@wrnexus/ui"`, ); } } return insertImports(source, applicationImports); } function migrateImportedLayout( source: string, file: string, appRoot: string, index: V060SymbolIndex, report: MigrationReport, ): string { const match = /\blayout\s*=\s*["']([^"']+)["']/.exec(source); if (!match || match[1] === "none") return source; const candidates = index.layouts.get(match[1]) ?? []; const unique = [...new Set(candidates)]; if (unique.length !== 1) { report.unresolvedImports.push( `${file}: layout '${match[1]}' ${unique.length ? "is ambiguous" : "was not found"}`, ); return source; } const symbol = declarationName(unique[0]!); const statement = `import ${symbol} from ${JSON.stringify(importPathFor(appRoot, unique[0]!))}`; return insertImports(source.replace(match[0], `layout = ${symbol}`), [statement]); } function writeMigrationReport(ctx: MigrationCtx): void { const file = join(ctx.appRoot, ".wrnexus", "migrations", "0.6.0-report.json"); ctx.log(`+ .wrnexus/migrations/0.6.0-report.json`); if (ctx.dryRun) return; mkdirSync(dirname(file), { recursive: true }); writeFileSync( file, JSON.stringify( { version: "0.6.0", from: ctx.from, to: ctx.to, appliedAt: new Date().toISOString(), ...ctx.report, }, null, 2, ) + "\n", "utf8", ); } /** * Versioned, idempotent upgrade steps. Each MUST be safe to re-run. Source * migrations must preserve semantics and are protected by update backups. */ function updateV070Config(ctx: MigrationCtx): void { const candidates = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"]; const file = candidates.map((name) => join(ctx.appRoot, name)).find(existsSync); if (!file) return; const current = readFileSync(file, "utf8"); const additions: string[] = []; if (!/\bnavigation\s*:/.test(current)) { additions.push(` navigation: { mode: "auto", }`); } if (!/\bsecurity\s*:/.test(current)) { additions.push(` security: { headers: true, requestLimits: { maxUrlLength: 8192, maxHeaderCount: 100, maxHeaderBytes: 32768, maxQueryParameters: 100, maxBodyBytes: 10485760, timeoutMs: 30000, maxConcurrent: 1000, fetchMetadata: true, }, contentSecurityPolicy: { enabled: true, useDefaults: true }, trustedTypes: { enabled: true, requireForScript: true }, hsts: { enabled: true, maxAge: 63072000, includeSubDomains: true, preload: true }, }`); } if (!/\bperformance\s*:/.test(current)) { additions.push(` performance: { enforcement: "warn", analyze: true, budgets: { routeJsBytes: 51200, routeCssBytes: 25600, htmlBytes: 204800, hydrationMs: 200, serverRenderMs: 500, lcpMs: 2500, inpMs: 200, cls: 0.1, ttfbMs: 800, }, }`); } if (!/\bobservability\s*:/.test(current)) { additions.push(` observability: { enabled: true, serverTiming: true, sampleRate: 0.1, exporter: "none", webVitals: true, }`); } if (!additions.length) return; const index = current.lastIndexOf("}"); if (index < 0) return; const before = current.slice(0, index).replace(/,?\s*$/, ""); const next = `${before},\n${additions.join(",\n")}\n${current.slice(index)}`; ctx.log(`~ ${file.slice(ctx.appRoot.length + 1)}: security/performance production defaults`); if (!ctx.dryRun) writeFileSync(file, next, "utf8"); } 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-0-2-32", 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-0-2-33", 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-0-2-40", 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-0-2-41", 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-0-2-42", 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-0-2-43", 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-0-2-44", 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-0-2-45", 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-0-2-46", 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-0-2-47", 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-0-2-48", 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-0-2-49", 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-0-2-50", 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. }, }, { version: "0.2.63", id: "polished-linked-catalog-cards", description: "Fixes visually hidden full-card links and improves channel, integration, and campaign card hierarchy and interaction.", apply() { // 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. }, }, { version: "0.2.65", id: "private-production-app-listeners", description: "Restricts workspace-managed production app servers to loopback so external clients must use the gateway.", apply() { // The production gateway applies the private bind address automatically. }, }, { version: "0.2.66", id: "named-workspace-environments-and-reusable-public-chrome", description: "Adds top-level named workspace environment commands and data-driven public announcement, header, and footer components.", apply() { // Existing workspaces gain the CLI command aliases and optional UI props automatically. }, }, { version: "0.2.67", id: "configurable-workspace-environment-runtime", description: "Adds per-environment runtime, HMR, build, migration, profile, hostname, and port controls to workspace commands.", apply() { // Existing workspace configurations remain compatible; new controls are opt-in. }, }, { version: "0.2.68", id: "production-request-error-logging", description: "Logs production request exceptions, forward-auth verifier failures, proxy failures, and unexpected app exits with actionable context.", apply() { // Runtime diagnostics improve automatically after updating and redeploying. }, }, { version: "0.2.69", id: "centralized-production-error-diagnostics", description: "Relays bounded internal app error diagnostics to gateway logs and strips them from public responses.", apply() { // Runtime diagnostics improve automatically after updating and rebuilding. }, }, { version: "0.2.70", id: "direct-production-runtime-dependency", description: "Declares @wrnexus/dev-server directly in runnable apps so production bundles resolve the matching runtime release.", apply(ctx) { 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/dev-server"] === `^${ctx.to}`) return; dependencies["@wrnexus/dev-server"] = `^${ctx.to}`; ctx.log(`+ @wrnexus/dev-server ^${ctx.to}`); if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); }, }, { version: "0.2.71", id: "public-forward-auth-redirect-origins", description: "Builds SSO redirects and returnTo URLs from configured public app origins instead of internal gateway listeners.", apply() { // Public URL handling improves automatically after updating and rebuilding. }, }, { version: "0.2.72", id: "fresh-linked-ui-compilation", description: "Refreshes the shared UI catalog and makes WRN compilation caches follow source content across package updates and linked dependencies.", apply() { // Component and cache behavior improves automatically after updating. }, }, { version: "0.2.73", id: "polished-auth-component-visuals", description: "Updates shared authentication actions and notices with reliable built-in icons, semantic color treatments, and polished sizing.", apply() { // Shared UI visuals improve automatically after updating and rebuilding. }, }, { version: "0.2.74", id: "wrn-imports-and-dynamic-public-shell", description: "Adds top-level WRN imports and data-driven public header and footer navigation, actions, links, and labels.", apply() { // Language and shared UI behavior improve automatically after updating. }, }, { version: "0.2.75", id: "dev-toolbar", description: "Dev toolbar for optimization and developer.", apply() { // Language and shared UI behavior improve automatically after updating. }, }, { version: "0.2.76", id: "dev-toolbar-fix", description: "Dev toolbar for optimization and developer.", apply() { // Language and shared UI behavior improve automatically after updating. }, }, { version: "0.2.77", id: "ui-components-fix", description: "Dev toolbar for optimization and developer.", apply() { // Language and shared UI behavior improve automatically after updating. }, }, { version: "0.2.78", id: "ui-components-fix-2", description: "Dev toolbar for optimization and developer.", apply() { // Language and shared UI behavior improve automatically after updating. }, }, { version: "0.2.79", id: "ui-components-fix-3", description: "Dev toolbar for optimization and developer.", apply() { // Language and shared UI behavior improve automatically after updating. }, }, { version: "0.3.0", id: "language-foundation-and-compatible-platform-upgrade", description: "Adds the shared syntax/AST package, stable diagnostics, plugins, partial hydration, advanced routing, build analysis, tracing, typed data APIs, and safe WRN source normalization.", apply(ctx) { const runnable = existsSync(join(ctx.appRoot, "app", "pages")); const packageFile = join(ctx.appRoot, "package.json"); const pkg = JSON.parse(readFileSync(packageFile, "utf8")) as Record; const scripts = (pkg.scripts ??= {}); const dependencies = (pkg.dependencies ??= {}); const addedScripts: string[] = []; const addedDependencies: string[] = []; if (runnable) { for (const [name, command] of Object.entries({ doctor: "wrnexus doctor .", "config:explain": "wrnexus config . --explain", analyze: "wrnexus analyze .", "update:preview": "wrnexus update . --dry-run", })) { if (!scripts[name]) { scripts[name] = command; addedScripts.push(name); } } for (const name of ["@wrnexus/syntax", "@wrnexus/plugin"]) { if (!dependencies[name]) { dependencies[name] = `^${ctx.to}`; addedDependencies.push(name); } } } if (addedScripts.length) ctx.log(`+ package scripts: ${addedScripts.join(", ")}`); if (addedDependencies.length) ctx.log(`+ dependencies: ${addedDependencies.join(", ")}`); if (!ctx.dryRun && (addedScripts.length || addedDependencies.length)) { writeFileSync(packageFile, JSON.stringify(pkg, null, 2) + "\n", "utf8"); } const changedFiles: string[] = []; for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) { const before = readFileSync(file, "utf8"); const after = migrateWrnSource(before); if (after === before) continue; changedFiles.push(file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/")); ctx.log(`~ normalized ${changedFiles[changedFiles.length - 1]}`); if (!ctx.dryRun) writeFileSync(file, after, "utf8"); } const reportFile = join(ctx.appRoot, ".wrnexus", "migrations", "0.3.0.json"); ctx.log(`+ .wrnexus/migrations/0.3.0.json (${changedFiles.length} source files normalized)`); if (!ctx.dryRun) { mkdirSync(dirname(reportFile), { recursive: true }); writeFileSync( reportFile, JSON.stringify( { version: "0.3.0", from: ctx.from, appliedAt: new Date().toISOString(), changedFiles, compatibility: { legacyDataFor: true, legacyEachBlocks: true, legacyComponentMounts: true, quotedDynamicAttributes: true, }, }, null, 2, ) + "\n", "utf8", ); } }, }, { version: "0.3.1", id: "pending-update-marker-verification-fix", description: "Allows warning-level doctor checks during pre-commit update verification so successfully verified migrations can commit their new version marker.", apply() { // CLI-only transaction fix. No generated application files require migration. }, }, { version: "0.3.2", id: "ssr-page-state-control-scope", description: "Makes page state variables available to server-rendered if and each expressions.", apply() { // Compiler-only fix. Existing WRN source files require no migration. }, }, { version: "0.3.3", id: "component-fix-0-3-3", description: "UI component fixes.", apply() { // Compiler-only fix. Existing WRN source files require no migration. }, }, { version: "0.3.4", id: "component-fix-0-3-4", description: "UI component fixes.", apply() { // Compiler-only fix. Existing WRN source files require no migration. }, }, { version: "0.3.5", id: "new-component-added-0-3-5", description: "UI component Added.", apply() { // Compiler-only fix. Existing WRN source files require no migration. }, }, { version: "0.3.6", id: "new-component-added-0-3-6", description: "UI component Added.", apply() { // Compiler-only fix. Existing WRN source files require no migration. }, }, { version: "0.4.0", id: "package-runtime-and-asset-platform", description: "Enables automatic package discovery, package components/routes/migrations, and page-scoped client runtime injection without manually copied JavaScript assets.", apply(ctx) { const packageFile = join(ctx.appRoot, "package.json"); const pkg = JSON.parse(readFileSync(packageFile, "utf8")) as Record; const scripts = (pkg.scripts ??= {}); const additions: Record = { "inspect:plugins": "wrnexus inspect plugins .", "inspect:runtimes": "wrnexus inspect runtimes .", "inspect:assets": "wrnexus inspect assets .", "inspect:routes": "wrnexus inspect routes .", }; const addedScripts: string[] = []; for (const [name, command] of Object.entries(additions)) { if (!scripts[name]) { scripts[name] = command; addedScripts.push(name); } } if (addedScripts.length) ctx.log(`+ package scripts: ${addedScripts.join(", ")}`); if (!ctx.dryRun && addedScripts.length) { writeFileSync(packageFile, JSON.stringify(pkg, null, 2) + "\n", "utf8"); } const changedFiles: string[] = []; const legacyCaptchaScript = /\s*]*\bsrc\s*=\s*["']\/(?:__wrnexus\/captcha|assets\/wrnexus\/captcha)\.js(?:\?[^"']*)?["'][^>]*>(?:\s*<\/script>)?\s*/gi; for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) { const before = readFileSync(file, "utf8"); const after = before.replace(legacyCaptchaScript, "\n"); if (after === before) continue; const relativeFile = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"); changedFiles.push(relativeFile); ctx.log(`~ removed legacy CAPTCHA script tag from ${relativeFile}`); if (!ctx.dryRun) writeFileSync(file, after, "utf8"); } const movedAssets: string[] = []; for (const relativeFile of [ "public/assets/wrnexus/captcha.js", "public/__wrnexus/captcha.js", ]) { const source = join(ctx.appRoot, relativeFile); if (!existsSync(source)) continue; const destination = join(ctx.appRoot, ".wrnexus", "legacy-assets", "0.4.0", relativeFile); movedAssets.push(relativeFile); ctx.log(`~ archived legacy ${relativeFile}`); if (!ctx.dryRun) { mkdirSync(dirname(destination), { recursive: true }); cpSync(source, destination); rmSync(source, { force: true }); } } const reportFile = join(ctx.appRoot, ".wrnexus", "migrations", "0.4.0.json"); ctx.log( `+ .wrnexus/migrations/0.4.0.json (${changedFiles.length} source files, ${movedAssets.length} legacy assets)`, ); if (!ctx.dryRun) { mkdirSync(dirname(reportFile), { recursive: true }); writeFileSync( reportFile, JSON.stringify( { version: "0.4.0", from: ctx.from, appliedAt: new Date().toISOString(), changedFiles, movedAssets, packageRuntimeDiscovery: true, manualCaptchaRuntimeRequired: false, }, null, 2, ) + "\n", "utf8", ); } }, }, { version: "0.5.0", id: "custom-ui-components-and-auth-platform", description: "Adds the authentication package, custom form controls, expanded UI component events, and reliable development hot reloading.", apply() { // Package, component, and development-runtime improvements apply automatically. }, }, { version: "0.5.1", id: "native-wrn-json-expressions", description: "Adds native array and object state initializers plus JSX-style expression props in WRN component markup.", apply() { // Syntax and compiler support are package-managed; existing applications remain compatible. }, }, { version: "0.5.2", id: "multiline-wrn-json-and-stable-gateway-runtime", description: "Supports formatted multiline array and object state initializers and keeps gateway children on the matching framework runtime.", apply() { // Syntax, compiler, and gateway fixes are package-managed; no application rewrite is required. }, }, { version: "0.5.3", id: "public-workspace-origins-and-schema-backed-auth-forms", description: "Keeps cross-app redirects on configured public origins and enables schema-backed auth forms with active navigation states.", apply() { // Gateway, helper, validation, and UI improvements are package-managed. }, }, { version: "0.5.10", id: "latest-release-after-upgrade", description: "Latest New Packages and release ready packages", apply() { // Gateway, helper, validation, and UI improvements are package-managed. }, }, { version: "0.5.11", id: "latest-release-after-upgrade", description: "Latest New Packages and release ready packages", apply() { // Gateway, helper, validation, and UI improvements are package-managed. }, }, { version: "0.5.11", id: "latest-release-after-upgrade", description: "Latest New Packages and release ready packages", apply() { // Gateway, helper, validation, and UI improvements are package-managed. }, }, { version: "0.5.12", id: "latest-release-after-upgrade", description: "Latest New Packages and release ready packages", apply() { // Gateway, helper, validation, and UI improvements are package-managed. }, }, { version: "0.5.13", id: "latest-release-after-upgrade", description: "Latest New Packages and release ready packages", apply() { // Gateway, helper, validation, and UI improvements are package-managed. }, }, { version: "0.5.13", id: "preserve-theme-accent-during-hmr", description: "Preserves the user's active theme and accent palette during HMR morph updates.", apply() { // Runtime-only framework fix. No project files require migration. }, }, { version: "0.5.14", id: "preserve-theme-accent-during-hmr", description: "Preserves the user's active theme and accent palette during HMR morph updates.", apply() { // Runtime-only framework fix. No project files require migration. }, }, { version: "0.5.15", id: "wrn-style-block-head-pipeline", description: "Promotes page, layout, and component style blocks into the document head after global CSS with CSP, HMR, and CSR navigation support.", apply() { // Compiler, SSR, dev-server, and CSR runtime changes apply automatically. }, }, { version: "0.6.0", id: "0.6.0-01-parser-and-language", description: "Enables the v0.6 parser, AST, typed state, imports, stores, outputs, and runtime function grammar.", apply() {}, }, { version: "0.6.0", id: "0.6.0-02-function-runtime", description: "Classifies unambiguous legacy functions as client, server, or shared while preserving ambiguous behavior.", apply(ctx) { for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) { const before = readFileSync(file, "utf8"); const relative = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"); const after = classifyLegacyFunctions(before, ctx.report, relative); if (after === before) continue; ctx.log(`~ ${relative}: runtime function modifiers`); ctx.report.changedAutomatically.push(relative); if (!ctx.dryRun) writeFileSync(file, after, "utf8"); } }, }, { version: "0.6.0", id: "0.6.0-03-typed-outputs", description: "Converts legacy @event declarations to typed outputs with safe unknown payload fallbacks.", apply(ctx) { for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) { const before = readFileSync(file, "utf8"); const relative = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"); const legacy = collectLegacyEvents(before); const after = ensureOutputsBlock(legacy.source, legacy.outputs, ctx.report, relative); if (after === before) continue; ctx.log(`~ ${relative}: typed outputs`); ctx.report.changedAutomatically.push(relative); if (!ctx.dryRun) writeFileSync(file, after, "utf8"); } }, }, { version: "0.6.0", id: "0.6.0-04-remove-emit", description: "Migrates static $emit calls and event.detail consumers to output.name(payload) and payload.", apply(ctx) { for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) { const before = readFileSync(file, "utf8"); const relative = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"); const after = convertLegacyEmit(before, ctx.report, relative).replace( /\bevent\.detail\b/g, "payload", ); if (after === before) continue; ctx.log(`~ ${relative}: callable outputs`); ctx.report.changedAutomatically.push(relative); if (!ctx.dryRun) writeFileSync(file, after, "utf8"); } }, }, { version: "0.6.0", id: "0.6.0-05-explicit-imports", description: "Enables compatible explicit imports and records unresolved symbols without guessing.", apply(ctx) { if (!ctx.explicitImports) return; const index = buildV060SymbolIndex(ctx.appRoot); for (const path of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) { const relativeFile = path.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"); const before = readFileSync(path, "utf8"); const after = addExplicitImportsToSource( before, relativeFile, ctx.appRoot, index, ctx.report, ); if (after === before) continue; ctx.log(`~ ${relativeFile}: explicit imports`); if (!ctx.report.changedAutomatically.includes(relativeFile)) ctx.report.changedAutomatically.push(relativeFile); if (!ctx.dryRun) writeFileSync(path, after, "utf8"); } }, }, { version: "0.6.0", id: "0.6.0-06-layout-imports", description: "Converts safely resolvable string layouts to imported symbols while retaining compatibility.", apply(ctx) { if (!ctx.explicitImports) return; const index = buildV060SymbolIndex(ctx.appRoot); for (const path of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) { const relativeFile = path.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"); const before = readFileSync(path, "utf8"); const after = migrateImportedLayout(before, relativeFile, ctx.appRoot, index, ctx.report); if (after === before) continue; ctx.log(`~ ${relativeFile}: imported layout`); if (!ctx.report.changedAutomatically.includes(relativeFile)) ctx.report.changedAutomatically.push(relativeFile); if (!ctx.dryRun) writeFileSync(path, after, "utf8"); } }, }, { version: "0.6.0", id: "0.6.0-07-component-contracts", description: "Enables generated component, output, function, and declaration contracts.", apply() {}, }, { version: "0.6.0", id: "0.6.0-08-store-support", description: "Adds @wrnexus/store and @wrnexus/typecheck to runnable applications.", apply(ctx) { 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 ??= {}); const added: string[] = []; for (const name of ["@wrnexus/store", "@wrnexus/typecheck"]) { if (dependencies[name] === `^${ctx.to}`) continue; dependencies[name] = `^${ctx.to}`; added.push(name); } if (added.length) ctx.log(`+ dependencies: ${added.join(", ")}`); if (!ctx.dryRun && added.length) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); }, }, { version: "0.6.0", id: "0.6.0-09-type-config", description: "Adds compatible imports, type checking, stores, and legacy behavior configuration.", apply(ctx) { updateV060Config(ctx); const types = join(ctx.appRoot, "app", "types"); if (!existsSync(types)) { ctx.log("+ app/types/global.d.ts"); if (!ctx.dryRun) { mkdirSync(types, { recursive: true }); writeFileSync( join(types, "global.d.ts"), "// Application-wide ambient WRNexusJS types.\nexport {};\n", "utf8", ); } } }, }, { version: "0.6.0", id: "0.6.0-10-showcase-and-ui", description: "Finalizes v0.6 source normalization and writes the detailed migration report.", apply(ctx) { for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) { const before = readFileSync(file, "utf8"); const relative = file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"); const after = migrateWrnSource(before); if (after === before) continue; ctx.log(`~ ${relative}: final v0.6 normalization`); if (!ctx.report.changedAutomatically.includes(relative)) ctx.report.changedAutomatically.push(relative); if (!ctx.dryRun) writeFileSync(file, after, "utf8"); } writeMigrationReport(ctx); }, }, { version: "0.7.0", id: "0.7.0-01-security-performance-foundation", description: "Adds secure-by-default request limits, caching, image optimization, observability, benchmarks, and production hardening packages.", apply(ctx) { const file = join(ctx.appRoot, "package.json"); if (existsSync(file)) { const pkg = JSON.parse(readFileSync(file, "utf8")) as Record; const dependencies = (pkg.dependencies ??= {}); const devDependencies = (pkg.devDependencies ??= {}); const added: string[] = []; for (const name of [ "@wrnexus/security", "@wrnexus/cache", "@wrnexus/image", "@wrnexus/observability", ]) { if (dependencies[name] === `^${ctx.to}`) continue; dependencies[name] = `^${ctx.to}`; added.push(name); } if (devDependencies["@wrnexus/benchmark"] !== `^${ctx.to}`) { devDependencies["@wrnexus/benchmark"] = `^${ctx.to}`; added.push("@wrnexus/benchmark (dev)"); } if (added.length) { ctx.log(`+ security/performance dependencies: ${added.join(", ")}`); if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); } } updateV070Config(ctx); const gitignore = join(ctx.appRoot, ".gitignore"); const currentIgnore = existsSync(gitignore) ? readFileSync(gitignore, "utf8") : ""; const ignoreEntries = [".env", ".env.*", "!.env.example", "!.env.*.example"]; const knownIgnore = new Set(currentIgnore.split(/\r?\n/).map((line) => line.trim())); const missingIgnore = ignoreEntries.filter((entry) => !knownIgnore.has(entry)); if (missingIgnore.length) { ctx.log(`+ .gitignore secure environment templates: ${missingIgnore.join(", ")}`); if (!ctx.dryRun) { writeFileSync( gitignore, `${currentIgnore.replace(/\s*$/, "")}\n${missingIgnore.join("\n")}\n`.replace( /^\n+/, "", ), "utf8", ); } } const review = "Review WRNexusJS 0.7 security policy, CSP allowlists, tenant authorization, upload scanners, cache adapters, and production performance budgets."; if (!ctx.report.needsReview.includes(review)) ctx.report.needsReview.push(review); }, }, { version: "0.8.0", id: "0.8.0-01-package-kits", description: "Adds package-owned helper kits, reusable UI blocks, standalone realtime rooms, repaired i18n, image helpers, JWT utilities, and encrypted HTTP envelopes.", apply(ctx) { const file = join(ctx.appRoot, "package.json"); if (!existsSync(file)) return; const pkg = JSON.parse(readFileSync(file, "utf8")) as Record; const dependencies = (pkg.dependencies ??= {}); const additions = ["@wrnexus/realtime", "@wrnexus/csr"]; const added: string[] = []; for (const name of additions) { if (dependencies[name] === `^${ctx.to}`) continue; dependencies[name] = `^${ctx.to}`; added.push(name); } if (added.length) { ctx.log(`+ package-kit dependencies: ${added.join(", ")}`); if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); } const review = "Review package-owned components and helpers, i18n locale layout, encrypted HTTP trust boundaries, and realtime room authorization before enabling them in production."; if (!ctx.report.needsReview.includes(review)) ctx.report.needsReview.push(review); }, }, { version: "0.8.0", id: "0.8.0-02-current-wrn-source", description: "Upgrades every application WRN source to current syntax, adds resolvable imports, normalizes formatting, and records unresolved work.", apply(ctx) { const appDirectory = join(ctx.appRoot, "app"); if (!existsSync(appDirectory)) return; const index = buildV060SymbolIndex(ctx.appRoot); const changedFiles: string[] = []; for (const file of walkProjectFiles(appDirectory, ".wrn")) { const relativeFile = relative(ctx.appRoot, file).replace(/\\/g, "/"); const before = readFileSync(file, "utf8"); let after = migrateV060WrnSource(before, ctx.report, relativeFile); after = migrateImportedLayout(after, relativeFile, ctx.appRoot, index, ctx.report); after = addExplicitImportsToSource(after, relativeFile, ctx.appRoot, index, ctx.report); after = formatCurrentWrnSource(after); if (after === before) continue; try { parse(after); } catch (error) { const message = error instanceof Error ? error.message.split("\n", 1)[0] : String(error); ctx.report.parseFailures.push(`${relativeFile}: ${message}`); ctx.report.needsReview.push( `${relativeFile}: automatic modernization was skipped because the migrated source did not parse`, ); ctx.log(`! ${relativeFile}: left unchanged because migrated source did not parse`); continue; } changedFiles.push(relativeFile); if (!ctx.report.changedAutomatically.includes(relativeFile)) { ctx.report.changedAutomatically.push(relativeFile); } ctx.log(`~ ${relativeFile}: current WRN syntax, imports, and formatting`); if (!ctx.dryRun) writeFileSync(file, after, "utf8"); } const reportFile = join( ctx.appRoot, ".wrnexus", "migrations", "0.8.0-source-modernization.json", ); ctx.log( `+ .wrnexus/migrations/0.8.0-source-modernization.json (${changedFiles.length} WRN files updated)`, ); if (!ctx.dryRun) { mkdirSync(dirname(reportFile), { recursive: true }); writeFileSync( reportFile, JSON.stringify( { version: "0.8.0", from: ctx.from, to: ctx.to, appliedAt: new Date().toISOString(), changedFiles, unresolvedImports: ctx.report.unresolvedImports, ambiguousFunctions: ctx.report.ambiguousFunctions, legacyOutputPayloads: ctx.report.legacyOutputPayloads, parseFailures: ctx.report.parseFailures, needsReview: ctx.report.needsReview, }, null, 2, ) + "\n", "utf8", ); } }, }, { version: "0.8.1", id: "0.8.1-production-performance", description: "Applies production build, CSS delivery, service-worker lifecycle, asset caching, compression, and server request-path optimizations.", apply() { // Package-managed production optimizations require no application source rewrite. }, }, { version: "0.8.2", id: "0.8.2-navigation-and-vitals-stability", description: "Stops scroll-driven route prefetch storms, batches browser vitals, and supports telemetry behind trusted reverse proxies.", apply() { // Runtime stability fixes require no application source rewrite. }, }, { version: "0.8.3", id: "0.8.3-client-ssr-realtime-stability", description: "Fixes browser function bundling, SSR computed values, Async aliases, typed loop props, realtime identity isolation, loader invalidation, and strict package imports.", apply() { // Framework runtime and compiler fixes require no application source rewrite. // The compile-cache generation changes automatically invalidate old artifacts. }, }, { version: "0.8.4", id: "0.8.4-editor-language-support-stability", description: "Fixes WRNexus Language Server crashes, WRN formatting stability, false closing-tag diagnostics, typed event inference, dynamic component prop validation, and VS Code extension reliability.", apply() { // Compiler, diagnostics, formatter, language-server, and VS Code // extension fixes require no application source migration. // // Existing projects receive the framework package changes through the // dependency update. Developers must separately update/reinstall the // WRNexus Language Support VS Code extension. }, }, { version: "0.8.5", id: "0.8.5-datatable-toaster-overlay-dialogs", description: "Adds the DataTable and Toaster components, removes the Table scaffold, and gives modal dialogs focus management, a Tab trap and a scroll lock.", apply() { // Applications using must move to . The two are not // prop-compatible -- Table took `columns`/`rows` and rendered them as // plain text, while DataTable owns sorting, filtering, paging and // selection -- so this is a source change no codemod can make safely. // // The `@wrnexus/ui` main entry no longer re-exports the filesystem // helpers; import them from `@wrnexus/ui/registry` instead. Everything // else arrives through the dependency update. }, }, { version: "0.8.6", id: "0.8.6-navigation-and-layout-groups", description: "Rebuilds the navigation and layout component groups, moves them off Tailwind utilities onto wire-* classes, defines theme tokens that components referenced but nothing declared, and makes component outputs actually reach parent bindings.", apply() { // Source changes no codemod can make safely, so they are listed rather // than attempted. // // OUTPUTS NOW ARRIVE. Two faults kept declared outputs from reaching a // parent @binding, and both are fixed. Expect handlers that never ran // before to start running -- this is the intended repair, but it is a // behaviour change in code you may have written around. // // 1. Every camelCase output was undeliverable. HTML lowercases // attribute names, so @sizeChange registered as "sizechange" while // the component emitted "sizeChange" and the lookup missed. That // covered all 17 camelCase outputs, including DataTable.pageChange // and .rowClick, Map.markerClick, ChatBubble.messageClick and // LayoutSplitter.sizeChange. The runtime now matches case // insensitively. // // 2. Eighteen components dispatched hand-built CustomEvents instead of // calling output.*, which never reaches a binding. Card, Footer, // Breadcrumb, Accordion, alert, Badge, AnnouncementBar, AvatarGroup, // ToggleCount and InputNumber now emit properly. // // If you worked around the old silence by listening for the raw DOM // event on the element, that listener still fires for cases where no // binding is registered, but the supported route is the @binding. // // Marquee, Map, Timeline, List and SearchBox now DECLARE the outputs // they were already firing: pause/resume, markerClick/select/zoom, // select, select and search/clear respectively. // // Tabs replaced its raw CustomEvents with declared outputs. Code // listening for the old change and select events on the element must // move to the @change and @select bindings. // // Sidebar renamed its classes to the BEM form used everywhere else: // wire-sidebar-shell, -items, -group, -toggle, -backdrop, -panel and // -layout became wire-sidebar__*. Nesting via children still works. // // The layout, page and section components moved from Tailwind utility // classes to wire-* classes with their own styles. Application CSS // selecting on the utility classes they used to render -- max-w-7xl, // gap-5, sm:grid-cols-2 and the rest -- no longer matches. Variants are // data attributes now, so target [data-variant] and friends instead. // // LayoutSplitter and CustomScrollbar previously declared outputs they // never emitted. LayoutSplitter now resizes and emits sizeChange rather // than resizeStart, resize and resizeEnd, and its props are size, // minSize, step and orientation rather than columns, gap and maxWidth. // CustomScrollbar dropped its scroll output; listen for the plain scroll // event on the element. Its props are axis, thickness, maxHeight and // radius. // // ui.css lost several application-pattern class families that nothing // referenced: wire-catalog-*, wire-page-shell, wire-product-card, // wire-legal-toc, wire-sdk-tabs, wire-cookie-* and wire-analytics-preview. // Anything hand-written against those needs its own styles. // // Themes gain tokens that were referenced but never defined, including // --wire-color-focus, --wire-color-surface-soft, --wire-color-on-danger // and the input-* family. A custom theme that declared these itself // keeps winning; one that did not will see focus rings and soft surfaces // start painting where they previously rendered as nothing. }, }, { version: "0.8.7", id: "0.8.7-ui-component-depth", description: "Removes superseded UI scaffolds and moves the remaining utility-styled components to self-contained wire-* BEM styles.", apply() { // AdvancedDatePicker -> DatePicker; AdvancedRangeSlider -> RangeSlider; // FileUpload -> FileInput + FileUploadProgress (or @wrnexus/uploader); // Toast and ToastNotifications -> Toaster. // // List, InputNumber, Marquee, TextLink, Map, SearchBox and Timeline no // longer render Tailwind utility class names. Application CSS selecting // those internal utilities must move to the component's wire-* BEM // classes or its data-size/data-variant attributes. List now emits // select for non-link items as well as navigation items. }, }, ]; /** 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: (message: string) => void, ): void { const publicDir = join(appRoot, "public"); const llms = join(publicDir, "llms.txt"); if (!existsSync(llms)) { log("+ public/llms.txt created"); if (!dryRun) { mkdirSync(publicDir, { recursive: true, }); writeFileSync(llms, AI_GUIDE, "utf8"); } } 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", "app", ]) { const source = join(appRoot, name); if (existsSync(source)) { const destination = join(backup, name); mkdirSync(dirname(destination), { recursive: true }); cpSync(source, destination, { recursive: statSync(source).isDirectory() }); } } return backup; } /** Update one app without marking success until install and verification pass. */ export function updateApp( appRoot: string, target: string, dryRun: boolean, options: { explicitImports?: boolean; report?: MigrationReport } = {}, ): 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}`); const report = options.report ?? ({ changedAutomatically: [], needsReview: [], unresolvedImports: [], ambiguousFunctions: [], legacyOutputPayloads: [], parseFailures: [], } satisfies MigrationReport); 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, explicitImports: options.explicitImports ?? false, report, 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 }; } export function verificationCommands(scripts: Record): string[][] { const commands: string[][] = []; // Update migrations can rewrite TypeScript, JSON, and framework configuration // files. Normalize those edits with the project's own formatter before running // the read-only format check. This prevents a successful migration from failing // only because wrnexus.config.ts or another touched file needs Prettier output. if (scripts.format) commands.push(["run", "format"]); if (scripts.check) commands.push(["run", "check"]); if (scripts.build) commands.push(["run", "build"]); return commands; } export function blockingVerificationChecks(checks: DoctorCheck[]): DoctorCheck[] { return checks.filter((check) => !check.ok && check.level !== "warning"); } function verifyApp(app: UpdatedApp): boolean { const health = inspectProject(app.root); const failed = blockingVerificationChecks(health); if (failed.length) { for (const check of failed) { console.error(` ✗ ${check.name}: ${check.detail}`); } return false; } const scripts = (app.pkg.scripts ?? {}) as Record; for (const args of verificationCommands(scripts)) { const action = args[1] === "format" ? "Formatting" : "Verifying"; console.log(`\n ${action} ${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 explicitImports = args.includes("--explicit-imports"); const reportRequested = args.includes("--report"); 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 reports = new Map(); const updated = targets .map((targetRoot) => { const report: MigrationReport = { changedAutomatically: [], needsReview: [], unresolvedImports: [], ambiguousFunctions: [], legacyOutputPayloads: [], parseFailures: [], }; reports.set(targetRoot, report); return updateApp(targetRoot, target, dryRun, { explicitImports, report }); }) .filter(Boolean) as UpdatedApp[]; if (reportRequested || dryRun) { for (const [targetRoot, report] of reports) { console.log(` Migration report: ${targetRoot}`); console.log(` Changed automatically: ${new Set(report.changedAutomatically).size}`); console.log(` Needs review: ${report.needsReview.length}`); console.log(` Unresolved imports: ${report.unresolvedImports.length}`); console.log(` Ambiguous function runtime: ${report.ambiguousFunctions.length}`); console.log( ` Legacy output payloads using unknown: ${report.legacyOutputPayloads.length}`, ); console.log(` Parse failures: ${report.parseFailures.length}`); } } 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`); }