#!/usr/bin/env tsx /** * One-shot fast-deploy migration: populate Cloudflare KV from a site's * bundled decofile so the worker can serve content KV-first. * * Reads `.deco/blocks/*.json` (the same merge the block generator does), * writes `decofile:` + `index:revision:` (keyed by deployment id) to * the site's KV namespace via the REST API, then reads both keys back to * verify. Run ONCE per site to seed the first deployment's content before * flipping it to KV-first (i.e. before adding the `DECO_KV` binding + * deploying the fast-deploy framework version). * * Usage (from the site root): * # dry-run (default): reads blocks, prints what would be written, no writes * CF_ACCOUNT_ID=... CF_KV_NAMESPACE_ID=... CF_API_TOKEN=... \ * npx -p @decocms/start deco-migrate-blocks-to-kv --deployment-id "$COMMIT_SHA" * # apply: * ... npx -p @decocms/start deco-migrate-blocks-to-kv --deployment-id "$COMMIT_SHA" --write * * Options: * --deployment-id Deployment id (git commit sha) to key the write under (required to write) * --blocks-dir Input blocks dir (default: .deco/blocks) * --write Perform the KV writes (otherwise dry-run, exit 0) * --help, -h Show this help * * Env: * CF_ACCOUNT_ID, CF_KV_NAMESPACE_ID, CF_API_TOKEN (required with --write) * * Exit codes: 0 ok / dry-run; 2 error (bad dir, missing env/args, verify failed) */ import * as path from "node:path"; import { createKvRestClient, kvConfigFromEnv } from "./lib/cf-kv-rest"; import { buildSnapshot, verifySnapshotInKv, writeSnapshotToKv } from "./lib/kv-snapshot"; import { readDecofileFromDir } from "./lib/read-decofile"; function parseArgs(argv: string[]) { const has = (f: string) => argv.includes(f); const val = (f: string, d: string) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; }; return { help: has("--help") || has("-h"), write: has("--write"), deploymentId: val("--deployment-id", ""), blocksDir: val("--blocks-dir", ".deco/blocks"), }; } async function main() { const opts = parseArgs(process.argv.slice(2)); if (opts.help) { console.log( "Usage: deco-migrate-blocks-to-kv --deployment-id [--blocks-dir .deco/blocks] [--write]\n" + "Env: CF_ACCOUNT_ID, CF_KV_NAMESPACE_ID, CF_API_TOKEN", ); process.exit(0); } if (!opts.deploymentId && opts.write) { console.error("error: --deployment-id is required to write to KV."); process.exit(2); } const blocksDir = path.resolve(process.cwd(), opts.blocksDir); let blocks: Record; try { const result = readDecofileFromDir(blocksDir); blocks = result.blocks; if (result.collisions.length) { console.warn(`warning: ${result.collisions.length} filename collision(s) resolved by tie-break`); } } catch (e) { console.error(`error: ${e instanceof Error ? e.message : String(e)}`); process.exit(2); } const snap = buildSnapshot(blocks); console.log(`decofile: ${snap.count} blocks, revision ${snap.revision}, ${snap.snapshot.length} bytes`); if (!opts.write) { console.log("\nDry-run only. Re-run with --write to populate KV."); process.exit(0); } let client: ReturnType; try { client = createKvRestClient(kvConfigFromEnv(process.env, { wranglerDir: process.cwd() })); } catch (e) { console.error(`error: ${e instanceof Error ? e.message : String(e)}`); process.exit(2); } try { await writeSnapshotToKv(client, snap, opts.deploymentId); const verify = await verifySnapshotInKv(client, snap.revision, opts.deploymentId); if (!verify.ok) { console.error(`error: KV verify failed — ${verify.reason}`); process.exit(2); } } catch (e) { console.error(`error: ${e instanceof Error ? e.message : String(e)}`); process.exit(2); } console.log(`\nwrote + verified decofile:${opts.deploymentId} (rev ${snap.revision}) → KV.`); console.log("Next: add the DECO_KV binding in wrangler.toml and deploy the fast-deploy build."); } main();