/** Local release preparation only. Never authenticates to or publishes to a registry. */ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, } from "node:fs"; import { basename, dirname, join, resolve, sep } from "node:path"; import { tmpdir } from "node:os"; import { spawnSync } from "node:child_process"; import { validateAndHashStage } from "./lib/package-integrity.ts"; const root = resolve(import.meta.dir, ".."); const output = join(root, "output", "design-system-phase-7"); const stage = join(root, ".publish-design"); const mode = process.argv[2] ?? "plan"; const all = new Map( readdirSync(join(root, "packages")).flatMap((name) => { const file = join(root, "packages", name, "package.json"); if (!existsSync(file)) return []; const manifest = JSON.parse(readFileSync(file, "utf8")); return [[manifest.name, { directory: name, ...manifest }]]; }), ); const names = new Set(); function visit(name: string) { if (names.has(name)) return; const pkg = all.get(name); if (!pkg) throw Error("Missing workspace package " + name); names.add(name); for (const dependency of Object.keys({ ...pkg.dependencies, ...pkg.optionalDependencies, ...pkg.peerDependencies, })) if (dependency.startsWith("@wrnexus/")) visit(dependency); } for (const name of ["@wrnexus/design-system", "@wrnexus/design-studio", "@wrnexus/mcp"]) visit(name); mkdirSync(output, { recursive: true }); const plan = { formatVersion: 1, stageDirectory: stage, packages: [...names].map((name) => ({ name, version: all.get(name).version })), schemaVersion: 1, contractVersion: 1, designPackageVersion: 1, registryAvailability: "not-checked", published: false, notes: [ "Source versions remain unchanged. Confirm unused release versions before publication.", "Use the existing version/release process for production; this command only stages and packs locally.", "Production-wide API/visual/type gates must pass separately before publication.", ], }; writeFileSync(join(output, "release-plan.json"), JSON.stringify(plan, null, 2)); function run(command: string, args: string[], cwd = root) { const result = spawnSync(command, args, { cwd, encoding: "utf8", maxBuffer: 20 * 1024 * 1024, env: { ...process.env, WRNEXUS_DESIGN_REPORT: join(output, "consumer-report.json") }, }); if (result.status !== 0) throw Error( `${basename(command)} failed: ${result.error?.message ?? result.stderr ?? result.stdout}`, ); return result.stdout; } if (mode === "plan") { console.log(JSON.stringify(plan, null, 2)); } else if (mode === "stage") { console.log( run(process.execPath, [ "run", "scripts/publish-packages.ts", "design-system", "design-studio", "mcp", "--with-dependencies", "--stage-root=.publish-design", ]), ); } else if (mode === "check") { writeFileSync( join(output, "consumer-report.json"), JSON.stringify({ packagingVerified: false, published: false, status: "running" }, null, 2), ); const integrity = JSON.parse(readFileSync(join(stage, "PACKAGE-INTEGRITY.json"), "utf8")); if ( integrity.packages.length !== names.size || integrity.packages.some( (pkg: { name: string; version: string }) => !names.has(pkg.name) || pkg.version !== all.get(pkg.name)?.version, ) ) throw Error("Staged closure differs from release plan. Restage all design packages."); const node = Bun.which("node"); if (!node) throw Error("Node.js with npm is required to verify tarballs."); const npm = join(dirname(node), "node_modules", "npm", "bin", "npm-cli.js"); if (!existsSync(npm)) throw Error("Unable to locate npm CLI."); const tarballs = join(output, "tarballs"); mkdirSync(tarballs, { recursive: true }); const dependencies: Record = {}; const packed = []; for (const pkg of integrity.packages) { const folder = join(stage, pkg.name.slice("@wrnexus/".length)); const manifest = JSON.parse(readFileSync(join(folder, "package.json"), "utf8")); const actual = validateAndHashStage(folder, manifest); if (JSON.stringify(actual) !== JSON.stringify(pkg)) throw Error("Staged integrity mismatch for " + pkg.name); const files = JSON.parse( run(node, [ npm, "pack", folder, "--pack-destination", tarballs, "--json", "--ignore-scripts", ]), ); dependencies[pkg.name] = "file:" + join(tarballs, files[0].filename).replace(/\\/g, "/"); packed.push({ name: pkg.name, version: manifest.version, filename: files[0].filename, integrity: files[0].integrity, }); } writeFileSync(join(output, "tarballs.json"), JSON.stringify(packed, null, 2)); const consumer = mkdtempSync(join(tmpdir(), "wrnexus-design-release-")); try { writeFileSync( join(consumer, "package.json"), JSON.stringify({ name: "design-release-consumer", private: true, type: "module", dependencies, }), ); console.log( run(node, [npm, "install", "--ignore-scripts", "--no-audit", "--no-fund"], consumer), ); writeFileSync( join(consumer, "probe.ts"), readFileSync(join(root, "scripts", "design-release-consumer.ts"), "utf8"), ); console.log(run(process.execPath, ["probe.ts"], consumer)); writeFileSync( join(consumer, "types.ts"), 'import {CORE_DESIGN_SYSTEMS} from "@wrnexus/design-system"; import {createDesignPackage} from "@wrnexus/design-system/package"; import {StudioSession} from "@wrnexus/design-studio"; import {planDesignUpgrade} from "@wrnexus/design-studio/upgrades"; import {createFrameworkMcpServer} from "@wrnexus/mcp"; const session = new StudioSession(CORE_DESIGN_SYSTEMS.square); const artifact = await createDesignPackage(session.valid); await planDesignUpgrade(session, JSON.stringify(artifact)); createFrameworkMcpServer(".", {design:true,designWrite:true}); const recovery: string | undefined = session.recoverySource; console.log(recovery);', ); writeFileSync( join(consumer, "tsconfig.json"), JSON.stringify({ compilerOptions: { noEmit: true, module: "ESNext", moduleResolution: "bundler", target: "ESNext", skipLibCheck: true, types: [], lib: ["ESNext", "DOM"], }, files: ["types.ts"], }), ); run( node, [join(root, "node_modules", "typescript", "bin", "tsc"), "--project", "tsconfig.json"], consumer, ); console.log("Installed package declarations passed consumer type checking."); } finally { // The path comes exclusively from mkdtemp; verify its workspace-independent boundary before removal. if ( !resolve(consumer).startsWith(resolve(tmpdir()) + sep) || !basename(consumer).startsWith("wrnexus-design-release-") ) console.error("Skipped cleanup: invalid consumer boundary."); else if (process.argv.includes("--keep-consumer")) { writeFileSync( join(output, "consumer-path.json"), JSON.stringify({ directory: consumer }, null, 2), ); console.log("Retained installed consumer for local QA: " + consumer); } else rmSync(consumer, { recursive: true, force: true }); } const solo = mkdtempSync(join(tmpdir(), "wrnexus-design-release-")); try { writeFileSync( join(solo, "package.json"), JSON.stringify({ name: "mcp-only-release-consumer", private: true, type: "module", dependencies: { "@wrnexus/mcp": dependencies["@wrnexus/mcp"] }, }), ); run(node, [npm, "install", "--ignore-scripts", "--no-audit", "--no-fund"], solo); writeFileSync( join(solo, "probe.ts"), 'import assert from "node:assert/strict"; import {createFrameworkMcpServer} from "@wrnexus/mcp"; import {existsSync} from "node:fs"; assert.equal(existsSync("node_modules/@wrnexus/design-studio"),false); assert.equal(createFrameworkMcpServer(".").tools().length,8); await assert.rejects(createFrameworkMcpServer(".",{design:true}).call("wrnexus_design_context"),/WRN-MCP-DESIGN-INSTALL/); console.log("MCP-only install keeps default APIs without requiring Studio.");', ); console.log(run(process.execPath, ["probe.ts"], solo)); } finally { if ( resolve(solo).startsWith(resolve(tmpdir()) + sep) && basename(solo).startsWith("wrnexus-design-release-") ) rmSync(solo, { recursive: true, force: true }); } const reportPath = join(output, "consumer-report.json"); const report = JSON.parse(readFileSync(reportPath, "utf8")); report.packagingVerified = true; report.checkedAt = new Date().toISOString(); report.checks.push( "installed declaration consumer type check", "MCP-only installation without optional Studio peer", ); writeFileSync(reportPath, JSON.stringify(report, null, 2)); console.log("Local tarballs verified. No registry publication performed."); } else throw Error("Usage: bun scripts/prepare-design-release.ts plan | stage | check");