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