UNPKG

@lunora/cli

Version:

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

171 lines (168 loc) 6.51 kB
import { existsSync } from 'node:fs'; import { mkdtemp, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { discoverSchema, schemaFromIr } from '@lunora/codegen'; import { seedPlan } from '@lunora/seed'; import { join } from '@visulima/path'; import { Project } from 'ts-morph'; import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs'; import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs'; import { b as tuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs'; import { runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs'; import { runResetCommand } from './runResetCommand.mjs'; const isLocalUrl = (url) => { if (url === void 0) { return true; } try { const { hostname } = new URL(url); return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"; } catch { return false; } }; const ndjsonReplacer = (_key, value) => { if (typeof value === "bigint") { return Number(value); } if (value instanceof ArrayBuffer) { return [...new Uint8Array(value)]; } return value; }; const seedFailure = (code) => { return { code, conflicts: 0, generated: 0, inserted: 0, ndjson: "" }; }; const guardSeedTargets = (options, schemaPath) => { if (!existsSync(schemaPath)) { options.logger.error(`schema not found: ${schemaPath} — run \`vis generate lunora-table --name=<name>\` to create one`); return seedFailure(1); } if (options.reset === true && (options.prod === true || !isLocalUrl(options.url))) { options.logger.error("--reset only clears local .wrangler/state and cannot be combined with --prod or a remote --url"); return seedFailure(1); } return void 0; }; const insertSeedRows = async (ndjson, generated, cwd, options) => { const scratchDirectory = await mkdtemp(join(tmpdir(), "lunora-seed-")); const temporaryFile = join(scratchDirectory, "rows.ndjson"); await writeFile(temporaryFile, ndjson, "utf8"); try { const result = await runImportCommand({ batchSize: options.batchSize, fetchImpl: options.fetchImpl, file: temporaryFile, logger: options.logger, prod: options.prod, token: options.token, url: options.url }); const conflicts = result.body?.conflicts ?? 0; if (conflicts > 0) { options.logger.warn( `${String(conflicts)} row(s) skipped — their _id already exists. Seeding is deterministic; re-run with --reset to wipe local state first, or a different --seed for fresh ids.` ); } return { code: result.code, conflicts, generated, inserted: result.inserted, ndjson }; } finally { await rm(scratchDirectory, { force: true, recursive: true }).catch(() => { }); } }; const validateSeedTable = (options, ir) => { if (options.table === void 0 || ir.tables.some((table) => table.name === options.table)) { return void 0; } const available = ir.tables.map((table) => table.name).join(", "); options.logger.error(`unknown table "${options.table}" — schema defines: ${available || "(no tables)"}`); return seedFailure(1); }; const confirmRemoteSeedTarget = async (options, generated) => { const targetsRemote = options.prod === true || !isLocalUrl(options.url); if (!targetsRemote || options.yes === true) { return void 0; } if (!process.stdin.isTTY && options.confirm === void 0) { options.logger.error("seed: refusing to insert into a non-local target without confirmation — re-run with --yes"); return seedFailure(1); } const confirmer = options.confirm ?? tuiConfirm; const confirmed = await confirmer(`This will insert ${String(generated)} generated row(s) into ${options.url ?? "the production worker"}. Continue?`); if (!confirmed) { options.logger.info("seed: aborted"); return seedFailure(1); } return void 0; }; const runSeedCommand = async (options) => { const cwd = options.cwd ?? process.cwd(); const schemaPath = join(cwd, "lunora", "schema.ts"); const guard = guardSeedTargets(options, schemaPath); if (guard !== void 0) { return guard; } const project = new Project({ skipAddingFilesFromTsConfig: true }); const ir = discoverSchema(project, schemaPath); const unknownTable = validateSeedTable(options, ir); if (unknownTable !== void 0) { return unknownTable; } const schema = schemaFromIr(ir); const plan = seedPlan(schema, { defaultCount: options.count ?? 10, only: options.table === void 0 ? void 0 : [options.table], seed: options.seed ?? 0 }); const lines = []; for (const { rows, table } of plan) { for (const row of rows) { lines.push(JSON.stringify({ doc: row, table }, ndjsonReplacer)); } } const ndjson = lines.length > 0 ? `${lines.join("\n")} ` : ""; const generated = lines.length; if (options.dryRun === true) { if (ndjson.length > 0) { process.stdout.write(ndjson); } options.logger.info(`generated ${String(generated)} row(s) across ${String(plan.length)} table(s) — dry run, nothing inserted`); return { code: 0, conflicts: 0, generated, inserted: 0, ndjson }; } if (options.reset === true) { const reset = await runResetCommand({ cwd, logger: options.logger, yes: true }); if (reset.code !== 0) { return { code: reset.code, conflicts: 0, generated, inserted: 0, ndjson }; } } if (generated === 0) { options.logger.warn("no rows generated — nothing to insert"); return { code: 0, conflicts: 0, generated: 0, inserted: 0, ndjson }; } const aborted = await confirmRemoteSeedTarget(options, generated); if (aborted !== void 0) { return aborted; } return insertSeedRows(ndjson, generated, cwd, options); }; const execute = defineHandler(async ({ cwd, logger, options }) => { const result = await runSeedCommand({ batchSize: options.batchSize, count: options.count, cwd, dryRun: options.dryRun === true, logger, prod: options.prod === true, reset: options.reset === true, seed: options.seed, table: options.table, token: options.token, // Resolve the link here (only under --prod) so seed's own remote/confirm // logic and the downstream import both see the same effective target. url: resolveProductionWorkerUrl({ cwd, prod: options.prod === true, url: options.url }), yes: options.yes === true }); return { code: result.code }; }); export { execute, runSeedCommand };