first commit

This commit is contained in:
2026-07-12 15:55:18 +05:30
commit ee98026cc5
404 changed files with 44522 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
/**
* `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 (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 { existsSync, readFileSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.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 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;
}
/**
* 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");
},
},
];
/** Bump every `@wrnexus/*` range to `^target`. Returns the human-readable changes. */
function bumpDeps(pkg: Record<string, unknown>, target: string): string[] {
const changed: string[] = [];
const next = `^${target}`;
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
const deps = pkg[field] as Record<string, string> | 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, unknown>): 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 {
// llms.txt is a generated reference — always safe to overwrite.
const llms = join(appRoot, "llms.txt");
if (!existsSync(llms) || readFileSync(llms, "utf8") !== AI_GUIDE) {
log("~ llms.txt refreshed");
if (!dryRun) 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). */
function updateApp(appRoot: string, target: string, dryRun: boolean): boolean {
const pkgPath = join(appRoot, "package.json");
if (!existsSync(pkgPath)) {
console.log(`${appRoot}: no package.json — skipped`);
return false;
}
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
const from = appVersion(appRoot, pkg);
const log = (m: string) => console.log(` ${m}`);
console.log(`${pkg.name ?? 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 });
}
}
// Record the applied version so the next update knows where it started.
pkg.wrnexus = { ...(pkg.wrnexus as object), version: target };
if (!dryRun) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
return true;
}
/** Load a `wrnexus.workspace.ts`, returning its app dirs (or null if not a workspace). */
async function workspaceApps(root: string): Promise<string[] | null> {
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<void> {
const root = resolve(dir);
const dryRun = args.includes("--dry-run");
const versionArg = args.find((a) => a.startsWith("--version="))?.split("=")[1];
const target =
versionArg ?? (args.includes("--latest") ? latestPublished() : null) ?? cliVersion();
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];
let updated = 0;
for (const t of targets) if (updateApp(t, target, dryRun)) updated++;
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);
}
console.log(`\n ✓ Updated ${updated} package(s) to ${target}.`);
console.log(` Review the changes, then rebuild/redeploy (wrnexus build).\n`);
}