upgrading/extension/upgrades/0.11-to-0.12/regenerate-extension-public-baseline.ts
upgrading/extension/upgrades/0.11-to-0.12/regenerate-extension-public-baseline.tsBrowse 76 files
1,849 tokens
7,282 bytes
Token encoding: o200k_base
Snapshot fac8604
← Back to SKILL.md
1/**2 * Re-emits a Postgres extension pack's contract-space and regenerates its3 * install migration baseline after the 0.12 public-by-default flip4 * (`__unbound__`/`postgres-unbound-schema` → `public`/`postgres-schema`).5 *6 * The migration ops are unchanged — only the contract hash envelope moves.7 * This script:8 * 1. Finds extension package roots (nearest `package.json` with a9 * `build:contract-space` script) whose `src/contract.json` still10 * carries `postgres-unbound-schema`.11 * 2. Runs `pnpm build:contract-space`.12 * 3. Patches each baseline `migrations/<dir>/migration.ts` `describe().to`13 * hash to match the new `storageHash`.14 * 4. Self-emits each migration (`pnpm exec tsx <migration.ts>`).15 * 5. Updates `migrations/refs/head.json` `hash` to the new storage hash16 * (preserves `invariants`).17 *18 * Flags:19 * --check dry-run; lists extension roots that still need regeneration20 * and exits 1 if any remain.21 */22import { execFile } from 'node:child_process';23import { access, copyFile, readdir, readFile, writeFile } from 'node:fs/promises';24import { join } from 'node:path';25import { promisify } from 'node:util';26 27const execFileAsync = promisify(execFile);28 29const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);30 31const dryRun = process.argv.includes('--check');32const projectRoot = process.cwd();33 34async function pathExists(path: string): Promise<boolean> {35 try {36 await access(path);37 return true;38 } catch {39 return false;40 }41}42 43function isJsonObject(value: unknown): value is Record<string, unknown> {44 return typeof value === 'object' && value !== null && !Array.isArray(value);45}46 47async function findPackageJsonFiles(root: string): Promise<string[]> {48 const out: string[] = [];49 50 async function walk(dir: string): Promise<void> {51 let entries: Awaited<ReturnType<typeof readdir>>;52 try {53 entries = await readdir(dir, { withFileTypes: true });54 } catch {55 return;56 }57 for (const entry of entries) {58 if (entry.isDirectory()) {59 if (SKIP_DIRS.has(entry.name)) continue;60 await walk(join(dir, entry.name));61 } else if (entry.isFile() && entry.name === 'package.json') {62 out.push(join(dir, entry.name));63 }64 }65 }66 67 await walk(root);68 return out.sort();69}70 71function contractNeedsPublicDefaultMigration(raw: string): boolean {72 return (73 raw.includes('"kind": "postgres-unbound-schema"') ||74 raw.includes('"kind":"postgres-unbound-schema"')75 );76}77 78async function packageHasBuildContractSpace(pkgPath: string): Promise<boolean> {79 const raw = await readFile(pkgPath, 'utf-8');80 try {81 const parsed: unknown = JSON.parse(raw);82 if (!isJsonObject(parsed)) return false;83 const scripts = parsed['scripts'];84 if (!isJsonObject(scripts)) return false;85 return (86 typeof scripts['build:contract-space'] === 'string' &&87 scripts['build:contract-space'].length > 088 );89 } catch {90 return false;91 }92}93 94async function findMigrationDirs(migrationsDir: string): Promise<string[]> {95 const out: string[] = [];96 let entries: Awaited<ReturnType<typeof readdir>>;97 try {98 entries = await readdir(migrationsDir, { withFileTypes: true });99 } catch {100 return out;101 }102 for (const entry of entries) {103 if (!entry.isDirectory()) continue;104 const migrationDir = join(migrationsDir, entry.name);105 if (await pathExists(join(migrationDir, 'migration.ts'))) out.push(migrationDir);106 }107 return out.sort();108}109 110async function readStorageHash(contractPath: string): Promise<string | null> {111 const raw = await readFile(contractPath, 'utf-8');112 try {113 const parsed: unknown = JSON.parse(raw);114 if (!isJsonObject(parsed)) return null;115 const storage = parsed['storage'];116 if (!isJsonObject(storage)) return null;117 const storageHash = storage['storageHash'];118 return typeof storageHash === 'string' ? storageHash : null;119 } catch {120 return null;121 }122}123 124async function patchMigrationToHash(125 migrationTsPath: string,126 storageHash: string,127): Promise<boolean> {128 const raw = await readFile(migrationTsPath, 'utf-8');129 const patched = raw.replace(/(\bto:\s*['"])sha256:[0-9a-f]{64}(['"])/, `$1${storageHash}$2`);130 if (patched === raw) return false;131 await writeFile(migrationTsPath, patched);132 return true;133}134 135async function patchHeadRef(headPath: string, storageHash: string): Promise<boolean> {136 const raw = await readFile(headPath, 'utf-8');137 let parsed: unknown;138 try {139 parsed = JSON.parse(raw);140 } catch {141 return false;142 }143 if (!isJsonObject(parsed)) return false;144 if (parsed['hash'] === storageHash) return false;145 parsed['hash'] = storageHash;146 await writeFile(headPath, `${JSON.stringify(parsed, null, 2)}\n`);147 return true;148}149 150interface ExtensionRoot {151 readonly dir: string;152 readonly contractPath: string;153}154 155const extensionRoots: ExtensionRoot[] = [];156 157for (const pkgPath of await findPackageJsonFiles(projectRoot)) {158 if (!(await packageHasBuildContractSpace(pkgPath))) continue;159 const dir = join(pkgPath, '..');160 const contractPath = join(dir, 'src', 'contract.json');161 if (!(await pathExists(contractPath))) continue;162 const raw = await readFile(contractPath, 'utf-8');163 if (!contractNeedsPublicDefaultMigration(raw)) continue;164 extensionRoots.push({ dir, contractPath });165}166 167if (extensionRoots.length === 0) {168 console.error(`No extension public-default migration candidates under ${projectRoot}.`);169 process.exit(dryRun ? 0 : 1);170}171 172let needsFix = 0;173let alreadyClean = 0;174 175for (const { dir, contractPath } of extensionRoots) {176 const rel = dir.slice(projectRoot.length + 1) || '.';177 const raw = await readFile(contractPath, 'utf-8');178 if (!contractNeedsPublicDefaultMigration(raw)) {179 alreadyClean += 1;180 console.log(`OK ${rel}`);181 continue;182 }183 184 needsFix += 1;185 if (dryRun) {186 console.log(`WOULD REGENERATE ${rel}`);187 continue;188 }189 190 console.log(`REGENERATE ${rel}`);191 await execFileAsync('pnpm', ['build:contract-space'], { cwd: dir, env: process.env });192 193 const storageHash = await readStorageHash(contractPath);194 if (storageHash === null) {195 throw new Error(`Could not read storageHash from ${contractPath}`);196 }197 198 const migrationsDir = join(dir, 'migrations');199 const srcContractJson = join(dir, 'src', 'contract.json');200 const srcContractDts = join(dir, 'src', 'contract.d.ts');201 202 for (const migrationDir of await findMigrationDirs(migrationsDir)) {203 await copyFile(srcContractJson, join(migrationDir, 'end-contract.json'));204 if (await pathExists(srcContractDts)) {205 await copyFile(srcContractDts, join(migrationDir, 'end-contract.d.ts'));206 }207 const migrationTs = join(migrationDir, 'migration.ts');208 await patchMigrationToHash(migrationTs, storageHash);209 await execFileAsync('pnpm', ['exec', 'tsx', migrationTs], { cwd: dir, env: process.env });210 }211 212 const headPath = join(migrationsDir, 'refs', 'head.json');213 if (await pathExists(headPath)) {214 await patchHeadRef(headPath, storageHash);215 }216}217 218console.log();219console.log(220 `${extensionRoots.length} extension pack(s): ${needsFix} ${dryRun ? 'needing regeneration' : 'regenerated'}, ${alreadyClean} already on public default.`,221);222 223if (dryRun && needsFix > 0) process.exit(1);224