UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands

209 lines (206 loc) 8.14 kB
import { existsSync } from 'node:fs'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { LunoraError } from '@lunora/errors'; import { r as resolveAdminBaseUrl } from '../packem_shared/admin-url-4UzT-CI4.mjs'; import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs'; import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs'; import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs'; const DEFAULT_BACKUP_DIR = ".lunora-backups"; const MANIFEST_FILE = "manifest.json"; const PITR_ENDPOINT_PATH = "/_lunora/admin/pitr"; const GET_PITR_BOOKMARK_OP = "__lunora_admin__:getPitrBookmark"; const PITR_RESTORE_OP = "__lunora_admin__:pitrRestore"; const isManifestEntry = (value) => typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.file === "string"; const readManifest = async (directory) => { const path = join(directory, MANIFEST_FILE); if (!existsSync(path)) { return []; } let parsed; try { parsed = JSON.parse(await readFile(path, "utf8")); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new LunoraError("INTERNAL", `backup: ${path} exists but is not valid JSON (${message}) — refusing to overwrite it; fix or remove it manually`, { cause: error }); } if (!Array.isArray(parsed)) { throw new TypeError(`backup: ${path} exists but is not a JSON array — refusing to overwrite it; fix or remove it manually`); } return parsed.filter(isManifestEntry); }; const writeManifest = async (directory, entries) => { await writeFile(join(directory, MANIFEST_FILE), `${JSON.stringify(entries, void 0, 2)} `, "utf8"); }; const runBackupCreate = async (options, directory) => { await mkdir(directory, { recursive: true }); const timestamp = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(); const file = `lunora-backup-${timestamp.replaceAll(/[.:]/gu, "-")}.ndjson`; const result = await runExportCommand({ fetchImpl: options.fetchImpl, logger: options.logger, out: join(directory, file), prod: options.prod, tables: options.tables, token: options.token, url: options.url }); if (result.code !== 0) { return { code: result.code }; } const entry = { bytes: result.bytes, createdAt: timestamp, file, id: timestamp, rows: result.rows, tables: options.tables }; const manifest = await readManifest(directory); manifest.push(entry); await writeManifest(directory, manifest); options.logger.success(`backup created: ${file} (${result.rows.toString()} rows, ${result.bytes.toString()} bytes)`); return { code: 0, entry }; }; const runBackupList = async (options, directory) => { const manifest = await readManifest(directory); if (manifest.length === 0) { options.logger.info(`no backups found in ${directory}`); return { code: 0 }; } for (const entry of manifest) { options.logger.info(`${entry.id} ${entry.rows.toString()} rows ${entry.bytes.toString()} bytes ${entry.file}`); } return { code: 0 }; }; const runBackupRestore = async (options, directory) => { const { target } = options; if (target === void 0 || target.length === 0) { options.logger.error("restore requires a backup id or file path. Usage: lunora backup restore <id|file>"); return { code: 1 }; } const manifest = await readManifest(directory); const matched = manifest.find((entry) => entry.id === target); const file = matched ? join(directory, matched.file) : target; if (!existsSync(file)) { options.logger.error(`backup not found: ${target}`); return { code: 1 }; } const result = await runImportCommand({ fetchImpl: options.fetchImpl, file, logger: options.logger, prod: options.prod, token: options.token, url: options.url, yes: options.yes }); return { code: result.code }; }; const resolvePitrRequest = (options) => { const token = options.token ?? process.env.LUNORA_ADMIN_TOKEN; if (!token) { options.logger.error("admin token required — pass --token or set LUNORA_ADMIN_TOKEN"); return void 0; } if (options.prod && options.url === void 0) { options.logger.error("--prod requires an explicit --url (refusing to target the implicit localhost worker)"); return void 0; } if (options.restore === true && options.at === void 0 && options.bookmark === void 0) { options.logger.error("pitr --restore requires --at <time> or --bookmark <bookmark>"); return void 0; } if (options.restore === true && options.prod === true && options.yes !== true) { options.logger.error("pitr --restore --prod restores production data in place. Re-run with --yes to confirm."); return void 0; } const baseUrl = resolveAdminBaseUrl(options.url, options.logger); if (baseUrl === void 0) { return void 0; } const fetchImpl = options.pitrFetch ?? globalThis.fetch; if (typeof fetchImpl !== "function") { throw new TypeError("no fetch implementation available — pass pitrFetch or run on Node >= 18"); } return { fetchImpl, requestUrl: `${baseUrl}${PITR_ENDPOINT_PATH}`, token }; }; const buildPitrArgs = (options, isRestore) => { const args = {}; if (options.at !== void 0) { args.time = options.at; } if (isRestore && options.bookmark !== void 0) { args.bookmark = options.bookmark; } if (isRestore && options.restart === true) { args.restart = true; } return args; }; const runBackupPitr = async (options) => { const request = resolvePitrRequest(options); if (request === void 0) { return { code: 1 }; } const isRestore = options.restore === true; const functionPath = isRestore ? PITR_RESTORE_OP : GET_PITR_BOOKMARK_OP; const args = buildPitrArgs(options, isRestore); const action = isRestore ? "restore" : "bookmark"; options.logger.info(`POST ${request.requestUrl} -> pitr ${action}${options.shard === void 0 ? "" : ` (shard "${options.shard}")`}`); const response = await request.fetchImpl(request.requestUrl, { body: JSON.stringify({ args, functionPath, shardKey: options.shard }), headers: { authorization: `Bearer ${request.token}`, "content-type": "application/json" }, method: "POST" }); const text = await response.text(); let body; try { body = JSON.parse(text); } catch { body = text; } options.logger.info(JSON.stringify(body, void 0, 2)); return { code: response.ok ? 0 : 1 }; }; const runBackupCommand = async (options) => { const cwd = options.cwd ?? process.cwd(); const directory = join(cwd, options.dir ?? DEFAULT_BACKUP_DIR); try { if (options.subcommand === "create") { return await runBackupCreate(options, directory); } if (options.subcommand === "list") { return await runBackupList(options, directory); } if (options.subcommand === "pitr") { return await runBackupPitr(options); } return await runBackupRestore(options, directory); } catch (error) { options.logger.error(error instanceof Error ? error.message : String(error)); return { code: 1 }; } }; const isBackupSubcommand = (value) => value === "create" || value === "list" || value === "pitr" || value === "restore"; const execute = defineHandler(({ argument, cwd, logger, options }) => { const sub = argument[0]; if (!isBackupSubcommand(sub)) { logger.error(`backup: unknown subcommand "${sub ?? ""}" — expected create | list | restore | pitr`); return { code: 1 }; } return runBackupCommand({ at: options.at, bookmark: options.bookmark, cwd, dir: options.dir, logger, prod: options.prod === true, restart: options.restart === true, restore: options.restore === true, shard: options.shard, subcommand: sub, tables: options.tables, target: argument[1], token: options.token, url: resolveProductionWorkerUrl({ cwd, prod: options.prod === true, url: options.url }), yes: options.yes === true }); }); export { execute, runBackupCommand };