UNPKG

@lunora/cli

Version:

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

175 lines (172 loc) 6.7 kB
import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { findWranglerFile, readWranglerJsonc, validateWranglerConfig, isPlaceholderValue, DEV_VARS_FILE, parseDevVariableEntries, inferLunoraBindings } from '@lunora/config'; import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs'; const SECRET_KEY_PATTERN = /(?:KEY|PASSWORD|SECRET|TOKEN)$/u; const isD1PlaceholderId = (databaseId) => { const value = databaseId.trim(); if (value === "") { return true; } const lower = value.toLowerCase(); return lower.includes("replace") || lower.startsWith("<") && lower.endsWith(">"); }; const readWrangler = (cwd) => { const path = findWranglerFile(cwd); if (path === void 0) { return { parsed: void 0, path: void 0 }; } const { parsed } = readWranglerJsonc(path); return { parsed, path }; }; const checkWrangler = (parsed, path, findings) => { if (path === void 0) { findings.push({ fix: "Run `lunora init` (or `lunora dev`) to scaffold and reconcile wrangler.jsonc.", level: "fail", message: "wrangler.jsonc not found." }); return; } if (parsed === void 0) { findings.push({ fix: `Check ${path} is valid JSONC.`, level: "fail", message: `Could not parse ${path}.` }); return; } const report = validateWranglerConfig(parsed); const shardError = report.errors.find((error) => error.includes("SHARD")); if (shardError === void 0) { findings.push({ level: "pass", message: "wrangler.jsonc present with a SHARD durable-object binding." }); } else { findings.push({ fix: "Run `lunora dev` to auto-reconcile, or add the binding manually.", level: "fail", message: shardError }); } }; const checkD1Placeholders = (parsed, findings) => { if (parsed === void 0) { return; } const databases = (parsed.d1_databases ?? []).filter(Boolean); for (const database of databases) { const databaseId = typeof database.database_id === "string" ? database.database_id : ""; if (isD1PlaceholderId(databaseId)) { const label = typeof database.binding === "string" && database.binding.length > 0 ? database.binding : "<unnamed>"; findings.push({ fix: "Run `wrangler d1 create <name>` and paste the returned database_id into wrangler.jsonc.", level: "fail", message: `D1 binding "${label}" has a placeholder database_id ("${databaseId || "<empty>"}").` }); } } }; const checkEmailDestination = (parsed, findings) => { if (parsed === void 0) { return; } const bindings = (parsed.send_email ?? []).filter(Boolean); for (const binding of bindings) { const destination = typeof binding.destination_address === "string" ? binding.destination_address : ""; if (destination !== "" && isPlaceholderValue(destination)) { const label = typeof binding.name === "string" && binding.name.length > 0 ? binding.name : "send_email"; findings.push({ fix: "Set destination_address to a verified Cloudflare Email Routing address.", level: "warn", message: `send_email binding "${label}" has a placeholder destination_address ("${destination}").` }); } } }; const checkDevVariables = (cwd, findings) => { const devVariablesPath = join(cwd, DEV_VARS_FILE); if (!existsSync(devVariablesPath)) { return; } let content; try { content = readFileSync(devVariablesPath, "utf8"); } catch { return; } const unfilled = parseDevVariableEntries(content).filter((entry) => SECRET_KEY_PATTERN.test(entry.key) && isPlaceholderValue(entry.value)).map((entry) => entry.key); if (unfilled.length > 0) { findings.push({ fix: "Run `lunora dev` to auto-generate secrets, or fill them in by hand.", level: "warn", message: `${DEV_VARS_FILE} has unfilled secret value(s): ${unfilled.join(", ")}.` }); } }; const checkAdminToken = (findings) => { const token = process.env.LUNORA_ADMIN_TOKEN; if (token === void 0 || token.trim() === "") { findings.push({ fix: "Set LUNORA_ADMIN_TOKEN (env or `.dev.vars`) to enable admin RPCs / studio.", level: "info", message: "LUNORA_ADMIN_TOKEN is not set." }); } else { findings.push({ level: "pass", message: "LUNORA_ADMIN_TOKEN is set." }); } }; const checkContainers = async (cwd, findings) => { let containers; try { ({ containers } = await inferLunoraBindings({ projectRoot: cwd })); } catch { return; } for (const container of containers) { if (container.exported) { findings.push({ level: "pass", message: `container "${container.exportName}" is exported by the worker entry.` }); } else { findings.push({ fix: 'Add `export * from "./lunora/_generated/containers"` to your worker entry (or re-run `vis generate lunora-container`).', level: "fail", message: `container "${container.exportName}" is declared but ${container.className} is not exported by the worker entry.` }); } } }; const runDoctor = async (options) => { const cwd = options.cwd ?? process.cwd(); const findings = []; const { parsed, path } = readWrangler(cwd); checkWrangler(parsed, path, findings); checkD1Placeholders(parsed, findings); checkEmailDestination(parsed, findings); checkDevVariables(cwd, findings); checkAdminToken(findings); await checkContainers(cwd, findings); const code = findings.some((finding) => finding.level === "fail") ? 1 : 0; return { code, findings }; }; const LEVEL_LABEL = { fail: "FAIL", info: "INFO", pass: "PASS", warn: "WARN" }; const renderReport = (result, logger) => { logger.info("lunora doctor — project preflight"); for (const finding of result.findings) { const line = `[${LEVEL_LABEL[finding.level]}] ${finding.message}`; if (finding.level === "fail") { logger.error(line); } else if (finding.level === "warn") { logger.warn(line); } else { logger.info(line); } if (finding.fix !== void 0 && finding.level !== "pass") { logger.info(` fix: ${finding.fix}`); } } const fails = result.findings.filter((finding) => finding.level === "fail").length; const warns = result.findings.filter((finding) => finding.level === "warn").length; if (fails > 0) { logger.error(`${String(fails)} failure(s), ${String(warns)} warning(s).`); } else if (warns > 0) { logger.warn(`0 failures, ${String(warns)} warning(s).`); } else { logger.success("all checks passed."); } }; const execute = defineHandler(async ({ cwd, logger }) => { const result = await runDoctor({ cwd}); renderReport(result, logger); return { code: result.code }; }); export { execute, runDoctor };