@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands
171 lines (168 loc) • 6.51 kB
JavaScript
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 };