@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
2 lines (1 loc) • 22.1 kB
JavaScript
import{existsSync as b,readFileSync as L,realpathSync as A}from"node:fs";import{join as h}from"node:path";import{readProjectTarget as u,DEFAULT_TARGET as f,readServiceBindings as B}from"@lunora/codegen";import{DEV_VARS_FILE as m,parseDevVariableEntries as C,isPlaceholderValue as O,readProjectDependencies as M,discoverSchemaInfo as G,inferLunoraBindings as V,resolveSchemaDirectory as K,probeCelldToolchain as q,D1_PLACEHOLDER_ID as W}from"@lunora/config";import{GLOBAL_FETCH_STRICTLY_PUBLIC_FLAG as g,resolveObservabilitySampling as y,findWranglerFile as T,readWranglerJsonc as D,buildBindingManifest as H,validateWranglerProject as z,UNEXPORTED_CLASS_MARKER as x,detectCloudflareCliConfig as J,collectExportGaps as Y}from"@lunora/config/cloudflare";import{a as Q,i as X,m as Z,c as ee,b as te,d as ne,e as oe,f as re}from"../packem_shared/vectorize-metadata-BVSF8z2h.mjs";import{r as se,d as ae}from"../packem_shared/admin-token-VdUnvnKW.mjs";import{r as ie,n as w,c as ce}from"../packem_shared/cloudflare-credentials-BVmZnDZk.mjs";import{d as le}from"../packem_shared/command-B6vu47iR.mjs";import{i as de}from"../packem_shared/path-containment-CgxYZggb.mjs";import{A as $,a as R,b as k}from"../packem_shared/variables-CPYyJtsL.mjs";const ue=(o,t)=>{const e=new Set,n=s=>{for(const[a,c]of Object.entries(s??{}))typeof c=="string"&&c.trim().length>0&&e.add(a)};n(o.vars);for(const s of Object.values(o.env??{}))n(s.vars);const r=h(t,m);if(b(r))try{n(Object.fromEntries(C(L(r,"utf8")).map(s=>[s.key,s.value])))}catch{}return e},fe=(o,t,e,n)=>{if(o===void 0||!e)return;const r=ue(o,t),s=r.has($),a=o.ai?.binding;!s&&(typeof a!="string"||a.length===0)&&n.push({code:"ai-binding-missing",fix:`Run \`lunora dev\` (it adds the binding), add \`"ai": { "binding": "AI" }\` to wrangler.jsonc, or set ${$} to an OpenAI-compatible proxy.`,level:"warn",message:"the project uses ctx.ai (@lunora/ai / env.AI) but wrangler.jsonc declares no `ai` binding — model calls will fail at runtime."}),r.has(R)&&n.push({code:"ai-gateway-token-unused",fix:"Make the gateway unauthenticated for ctx.ai, or remove the token. A bring-your-own AI SDK provider built with `resolveAiGateway` does send it.",level:"warn",message:`${R} is set, but ctx.ai's Workers AI binding cannot send a gateway token — it is ignored on that path.`}),!s&&!r.has(k)&&n.push({code:"ai-gateway-default",fix:`Run \`lunora cloudflare ai-gateway\` to create a gateway for this app and write ${k} into wrangler vars.`,level:"info",message:`no ${k} in vars or ${m} — \`<provider>/<model>\` slugs route through the account's \`default\` AI Gateway.`})},pe=(o,t,e)=>{if(o===void 0||!t)return;const n=[];(o.compatibility_flags??[]).includes(g)||n.push("the top level");for(const[r,s]of Object.entries(o.env??{}))s.compatibility_flags!==void 0&&!s.compatibility_flags.includes(g)&&n.push(`env.${r}`);n.length>0&&e.push({code:"cimd-fetch-not-strictly-public",fix:`Add "${g}" to "compatibility_flags" in wrangler.jsonc.`,level:"warn",message:`@lunora/auth/cimd/workers is imported, but ${n.join(", ")} of wrangler.jsonc lacks the ${g} compatibility flag — workersCimdFetch() refuses to build without it.`})},N=5e3,he="https://api.cloudflare.com/client/v4/user/tokens/verify",U=o=>{if(typeof o!="object"||o===null||!("result"in o)||"success"in o&&o.success===!1)return;const{result:t}=o;return typeof t=="object"&&t!==null&&"status"in t&&typeof t.status=="string"?t.status:void 0},ge=async(o,t)=>{const e=await t(he,{headers:{Authorization:`Bearer ${o}`},signal:AbortSignal.timeout(N)}),n=await e.json().catch(()=>{});return{reported:e.ok?U(n):void 0,status:e.status}},me=async(o,t,e)=>{const n=await ce({accountId:o,apiToken:t,fetch:e,init:{signal:AbortSignal.timeout(N)},path:"/tokens/verify"});return n.ok?{reported:U(n.body),status:200}:{reported:void 0,status:n.status}},ve=async(o,t,e)=>{const n=await me(o,t,e);return n.reported!==void 0?n.reported==="active"?{verdict:"active"}:{verdict:"rejected"}:n.status===401||n.status===403?{verdict:"rejected"}:{reason:`HTTP ${String(n.status)}`,verdict:"unchecked"}},be=async(o,t,e)=>{const n=await ge(o,e);if(n.reported!==void 0)return n.reported==="active"?{verdict:"active"}:{verdict:"rejected"};if(n.status!==401&&n.status!==403)return{reason:`HTTP ${String(n.status)}`,verdict:"unchecked"};if(t.length===0)return{reason:"the user endpoint refused it, and no account id is set to check an account-owned token",verdict:"unchecked"};const r=await Promise.all(t.map(async s=>await ve(s,o,e).catch(a=>({reason:a instanceof Error?a.message:String(a),verdict:"unchecked"}))));return r.find(s=>s.verdict==="active")??r.find(s=>s.verdict==="unchecked")??{verdict:"rejected"}},we=o=>{const t=[o?.account_id,...Object.values(o?.env??{}).map(e=>e?.account_id)];return[...new Set(t.map(e=>w(e)).filter(e=>e!==void 0))]},ke=async(o,t,e)=>{const n=t.environment??process.env,r=we(o),{accountId:s,token:a}=ie(n,r[0]),c=w(n.CLOUDFLARE_ACCOUNT_ID)===void 0?r:[s].filter(d=>d!==void 0);if(a===void 0){e.push({code:"cloudflare-token-unchecked",fix:"Set CLOUDFLARE_API_TOKEN to verify it.",level:"info",message:"the Cloudflare API token was not verified: CLOUDFLARE_API_TOKEN is not set."});return}const i=t.fetch??globalThis.fetch.bind(globalThis);let l;try{l=await be(a,c,i)}catch(d){l={reason:d instanceof Error?d.message:String(d),verdict:"unchecked"}}l.verdict==="active"?e.push({code:"cloudflare-token-ok",level:"pass",message:"the Cloudflare API token is active."}):l.verdict==="rejected"?e.push({code:"cloudflare-token-rejected",fix:"Create a token at https://dash.cloudflare.com/profile/api-tokens with the Workers Scripts Edit permission, then set CLOUDFLARE_API_TOKEN to it.",level:"fail",message:"the Cloudflare API token is not active, so deploys with it will fail."}):e.push({code:"cloudflare-token-unchecked",level:"info",message:`the Cloudflare API token could not be verified (${l.reason}).`})},S=o=>{const{logs:t,traces:e}=y(o);return Object.entries({logs:t,traces:e}).filter(([,n])=>n.enabled&&n.headSamplingRate>=1).map(([n])=>n)},ye=(o,t)=>{if(o===void 0)return;const e=[],n=S(o.observability);n.length>0&&e.push(`${n.join(" + ")} (top level)`);for(const[r,s]of Object.entries(o.env??{})){const a=s?.observability;if(a===void 0)continue;const c=S(a);c.length>0&&e.push(`${c.join(" + ")} (env.${r})`)}e.length!==0&&t.push({code:"observability-full-sampling",fix:"For a high-traffic Worker, lower head_sampling_rate in wrangler.jsonc: under observability for logs, under observability.traces for traces — e.g. 0.1 keeps 10% of requests. Lunora never changes a rate you set.",level:"info",message:`observability samples every request (head_sampling_rate 1) for ${e.join(", ")}. From 2026-12-01 Workers Observability is billed: Free caps ingestion at 0.5 GB/day; Paid includes 50 GB per billing cycle, then $0.25/GB.`})},v=o=>o.enabled&&o.headSamplingRate>0,j=o=>{if(!o||o.redact_query_string===!0)return!1;const{logs:t,traces:e}=y(o);return v(t)||v(e)},E=o=>{const{logs:t,traces:e}=y(o);return[v(t)&&"request logs",v(e)&&"traces"].filter(Boolean).join(" and ")},_e=(o,t)=>{if(o===void 0)return;const e=[],n=new Set;j(o.observability)&&(e.push("top level"),n.add(E(o.observability)));for(const[r,s]of Object.entries(o.env??{})){const a=s?.observability;j(a)&&(e.push(`env.${r}`),n.add(E(a)))}e.length!==0&&t.push({code:"observability-query-unredacted",fix:'Set "redact_query_string": true in each observability block listed above in wrangler.jsonc. Lunora never changes a value you set.',level:"warn",message:`${[...n].join(" / ")} keep query strings (redact_query_string is not true) for ${e.join(", ")}. Emailed auth links carry their token in the query, so that token is stored with the request URL.`})},P={ai:"Workers AI",browser:"Browser Rendering",images:"Cloudflare Images (Image Transformations)"},I=["Argo Smart Routing","Log Explorer"],Ae=o=>{const t=[["top level",o],...Object.entries(o.env??{}).flatMap(([n,r])=>r?[[`env.${n}`,r]]:[])],e=new Map;for(const[n,r]of t)for(const s of H(r).bindings)P[s.type]!==void 0&&e.set(s.type,[...e.get(s.type)??[],n]);return e},xe=(o,t)=>{if((u(o)??f)!=="cloudflare")return;const e=T(o),n=e===void 0?void 0:D(e).parsed;if(n!==void 0)for(const[r,s]of Ae(n)){const a=P[r]??r;t.push({code:"paid-product-bound",fix:"Create an account budget alert in the dashboard: Manage Account > Billing > Billable Usage > Create budget alert. Lunora cannot create or read one.",level:"info",message:`${a} is bound (${s.join(", ")}) and billed per use. No documented usage-alert product covers it, and lunora cannot read budget alerts, so an account budget alert is the only cap.`})}t.push({code:"paid-product-unknown",level:"info",message:`enablement of ${I.join(" and ")} cannot be read by lunora (dashboard settings); check the dashboard if your account uses ${I.length>1?"them":"it"}.`})},$e=(o,t)=>{for(const e of B(o).services)for(const n of e.publicScopes){const r=n===""?"":` (env.${n})`;t.push({code:"service-workers-dev",fix:`If ${e.name} has no public route (custom domain or route, including ones set outside ${e.wranglerPath}), set \`"workers_dev": false\`${r}. Keep its internal auth (HMAC) either way.`,level:"warn",message:`service ${e.name} (${e.worker}) has no route in its wrangler config and still serves on workers.dev${r}; a custom domain set elsewhere would also be public.`})}},F=o=>{const t=T(o);if(t===void 0)return{parsed:void 0,path:void 0};const{parsed:e}=D(t);return{parsed:e,path:t}},Re=(o,t,e,n)=>{if(e===void 0){n.push({code:"wrangler-missing",fix:"Run `lunora init` (or `lunora dev`) to scaffold and reconcile wrangler.jsonc.",level:"fail",message:"wrangler.jsonc not found."});return}if(t===void 0){n.push({code:"wrangler-unparseable",fix:`Check ${e} is valid JSONC.`,level:"fail",message:`Could not parse ${e}.`});return}const{report:r}=z({projectRoot:o});for(const c of r.errors.filter(i=>i.includes(x)))n.push({code:"wrangler-class-unexported",level:"fail",message:c});for(const c of r.warnings.filter(i=>i.includes("LUNORA_ORIGIN_URL")))n.push({code:"scheduler-origin-missing",fix:"Set LUNORA_ORIGIN_URL in wrangler vars to the worker's public URL (or `wrangler secret put LUNORA_ORIGIN_URL`).",level:"warn",message:c});const s=c=>c.includes(x)||c.includes("SHARD");for(const c of r.errors.filter(i=>!s(i)))n.push({code:"wrangler-invalid",fix:"Fix the reported wrangler.jsonc problem; `lunora verify` reports the same set.",level:"fail",message:c});for(const c of r.warnings.filter(i=>!i.includes("LUNORA_ORIGIN_URL")))n.push({code:"wrangler-advisory",level:"warn",message:c});const a=r.errors.find(c=>c.includes("SHARD"));a===void 0?n.push({code:"wrangler-shard-binding-ok",level:"pass",message:"wrangler.jsonc present with a SHARD durable-object binding."}):n.push({code:"wrangler-shard-binding-missing",fix:"Run `lunora dev` to auto-reconcile, or add the binding manually.",level:"fail",message:a})},Se=(o,t)=>{b(h(o,"lunora.json"))&&t.push({code:"stale-lunora-json",fix:'Move `target` / `remote` into `lunora.config.ts` (`export default { target: "…" }`), then delete lunora.json.',level:"warn",message:"lunora.json is present but no longer read — lunora.config.* replaced it, so any `target` or `remote` in it is being ignored."})},je=(o,t)=>{const e=J(o);e!==void 0&&t.push({code:"cf-config-present",fix:e.fix,level:"warn",message:e.message})},Ee=(o,t)=>{o===void 0||o.limits?.cpu_ms!==void 0||t.push({code:"cpu-limit-missing",fix:'Add `"limits": { "cpu_ms": 30000 }` to wrangler.jsonc, tuned to your slowest legitimate handler.',level:"info",message:"no limits.cpu_ms is set — a runaway handler can burn CPU up to the platform default before anything stops it."})},Ie=(o,t)=>{const e=(o?.r2_buckets??[]).map(n=>n?.bucket_name).filter(n=>typeof n=="string"&&n.length>0);e.length!==0&&t.push({code:"r2-lifecycle-unset",fix:"wrangler r2 bucket lifecycle list <bucket> — add an abort-incomplete-multipart rule if none exists.",level:"info",message:`R2 bucket(s) ${e.join(", ")}: verify a lifecycle rule aborts incomplete multipart uploads — orphaned parts are billed but invisible in a listing.`})},Le=(o,t)=>{const{error:e,info:n}=G(o,"lunora");if(e!==void 0){t.push({code:"schema-unreadable",fix:"Fix the parse error, then re-run `lunora doctor`. `lunora codegen` reports the same failure with the offending source.",level:"fail",message:`lunora/schema.ts could not be parsed, so every schema-derived check was skipped: ${e}`});return}for(const r of n?.vectorMetadata??[]){const s=Z(r.kind);if(s===void 0){t.push({code:"vector-metadata-unfilterable",fix:"Filter on a string, number or boolean column, or drop it from `metadata`.",level:"warn",message:`vector index "${r.index}" declares metadata "${r.property}", whose type Vectorize cannot filter on.`});continue}t.push({code:"vector-metadata-index-required",fix:`wrangler ${ee({index:r.index,property:r.property,type:s}).join(" ")}`,level:"info",message:`vector index "${r.index}" filters on metadata "${r.property}" — that needs a Vectorize metadata index (\`lunora deploy\` creates it).`})}},Ce=o=>{const t=o?.d1_databases;return Array.isArray(t)?t.filter(Boolean):[]},Oe=(o,t,e)=>{const n=Ce(o),r=[];for(const s of n){const a=typeof s.database_id=="string"?s.database_id:"";if(e?oe(a)&&a!==W:re(a)){const i=typeof s.binding=="string"&&s.binding.length>0?s.binding:"<unnamed>";r.push({code:"d1-placeholder-id",fix:e?"Set database_id to the database celld should use.":"Run `lunora doctor --fix` to create the database and write its id into wrangler.jsonc, or run `wrangler d1 create <name>` and paste the returned database_id in yourself.",level:"fail",message:`D1 binding "${i}"${t} has a placeholder database_id ("${a||"<empty>"}").`})}}return r},Te=(o,t,e)=>{if(t===void 0)return;const n=(u(o)??f)==="celld",r=[{config:t,suffix:""},...Object.entries(n?{}:t.env??{}).map(([s,a])=>({config:a,suffix:` in env.${s}`}))];for(const{config:s,suffix:a}of r)e.push(...Oe(s,a,n))},De=(o,t)=>{if(o===void 0)return;const e=(o.send_email??[]).filter(Boolean);for(const n of e){const r=typeof n.destination_address=="string"?n.destination_address:"";if(r!==""&&O(r)){const s=typeof n.name=="string"&&n.name.length>0?n.name:"send_email";t.push({code:"email-destination-placeholder",fix:"Set destination_address to a verified Cloudflare Email Routing address.",level:"warn",message:`send_email binding "${s}" has a placeholder destination_address ("${r}").`})}}},Ne=(o,t)=>{const e=h(o,m);if(!b(e))return;let n;try{n=L(e,"utf8")}catch{return}const r=C(n).filter(s=>X(s.key)&&O(s.value)).map(s=>s.key);r.length>0&&t.push({code:"dev-vars-missing-secret",fix:"Run `lunora dev` to auto-generate secrets, or fill them in by hand.",level:"warn",message:`${m} has unfilled secret value(s): ${r.join(", ")}.`})},Ue=(o,t)=>{const{source:e}=se({cwd:o});e===void 0?t.push({code:"admin-token-missing",fix:"Set LUNORA_ADMIN_TOKEN (env or `.dev.vars`) to enable admin RPCs / studio.",level:"info",message:"LUNORA_ADMIN_TOKEN is not set."}):t.push({code:"admin-token-set",level:"pass",message:`LUNORA_ADMIN_TOKEN is set (${ae(e)}).`})},Pe=async(o,t)=>{try{return await V({projectRoot:o,schemaDir:K(o)})}catch(e){t.push({code:"declared-export-unchecked",fix:"Make sure the worker entry resolves (see `lunora codegen`), then re-run.",level:"warn",message:`could not check whether declared containers/workflows/agents are re-exported by the worker entry: ${e instanceof Error?e.message:String(e)}`});return}},Fe=(o,t)=>{const e=Y(o),n=[...o.containers.map(r=>({...r,kind:"container"})),...o.workflows.map(r=>({...r,kind:"workflow"})),...o.agents.map(r=>({...r,kind:"agent"}))];for(const r of n.filter(s=>s.exported))t.push({code:"declared-export-ok",level:"pass",message:`${r.kind} "${r.exportName}" is exported by the worker entry.`});for(const r of e)t.push({code:"declared-export-missing",fix:`Add \`export * from "./lunora/_generated/${r.module}"\` to your worker entry (or re-run \`vis generate lunora-${r.kind}\`).`,level:"fail",message:`${r.kind} "${r.exportName}" is declared but ${r.className} is not exported by the worker entry.`})},Be=/^[\^~><= ]+/u,Me=/^(?<core>\d+\.\d+\.\d+)(?:-(?<channel>[a-z]+)\.(?<counter>\d+))?$/u,Ge=o=>{const t=o.replace(Be,"").trim(),e=Me.exec(t);if(!e?.groups)return;const{channel:n,core:r,counter:s}=e.groups;return{channel:n??"",core:r??"",counter:s===void 0?void 0:Number.parseInt(s,10),raw:t}},Ve=(o,t)=>{const e=M(o),n=[];for(const[i,l]of Object.entries(e)){if(i!=="lunorash"&&!i.startsWith("@lunora/"))continue;const d=Ge(l);d&&n.push({name:i,version:d})}if(n.length<2)return;const r=i=>i.map(l=>`${l.name}@${l.version.raw}`).join(", "),s=new Set(n.map(i=>i.version.core)),a=new Set(n.map(i=>i.version.channel));if(s.size>1){t.push({code:"version-skew-cores",fix:'Run `pnpm update "@lunora/*" lunorash` (or `npm`/`yarn` equivalent) so every Lunora package moves to the same release.',level:"warn",message:`Lunora packages span ${String(s.size)} different versions: ${r(n)}.`});return}if(a.size>1){t.push({code:"version-skew-channels",fix:"Pick one channel (all stable, or all alpha/beta) and update the odd package out.",level:"warn",message:`Lunora packages mix release channels (${[...a].map(i=>i===""?"stable":i).join(" + ")}): ${r(n)}.`});return}const c=n.map(i=>i.version.counter).filter(i=>i!==void 0);if(c.length>1){const i=Math.min(...c),l=Math.max(...c);t.push({code:"version-counter-spread",level:"info",message:c.length===n.length&&i===l?`Lunora packages are all at ${[...a][0]??""}.${String(l)}.`:`Lunora pre-release counters span ${String(i)}–${String(l)}: ${r(n)}.`})}},Ke=[h("@lunora","cli"),"lunorash"],qe=(o,t,e)=>{if(t===void 0)return;const n=[];for(const s of Ke){const a=h(o,"node_modules",s);if(b(a))try{n.push(A(a))}catch{}}if(n.length===0)return;let r;try{r=A(t)}catch{return}n.some(s=>de(s,r))||e.push({code:"cli-shadowed",fix:"Run the project's own CLI: `pnpm exec lunora …` (or `npx lunora …`).",level:"warn",message:`the running lunora (${r}) is not the project's own install (${n.join(", ")}).`})},We={full:{code:"do-class-budget-full",level:"warn"},near:{code:"do-class-budget-near",level:"warn"},ok:{code:"do-class-budget-ok",level:"pass"},unchecked:{code:"do-class-budget-unchecked",level:"info"}},He=o=>{const t=new Map,e=(n,r)=>{const s=w(n);s!==void 0&&t.set(s,[...t.get(s)??[],r])};e(o?.account_id,"top level");for(const[n,r]of Object.entries(o?.env??{}))e(r?.account_id,`env.${n}`);return t},ze=async(o,t,e,n)=>{if((u(o)??f)!=="cloudflare")return;const s=w((e.environment??process.env).CLOUDFLARE_ACCOUNT_ID)!==void 0?[]:[...He(t)],a=s.length===0?[[void 0,[]]]:s,c=await Promise.all(a.map(async([i,l])=>({accountId:i,blocks:l,budget:await te({accountId:i,environment:e.environment,fetch:e.fetch})})));for(const{accountId:i,blocks:l,budget:d}of c)n.push({...We[d.verdict],message:c.length>1?`account ${String(i)} (${l.join(", ")}): ${d.message}`:d.message,...d.fix===void 0?{}:{fix:d.fix}})},Je="CELLD_BUCKET is not set — a live `lunora deploy` needs it to name the fleet bucket; `lunora dev` and a dry run do not.",Ye=async(o,t,e)=>{if((u(o)??f)!=="celld")return;const n={...process.env,...t.environment},r=await q({dryRun:!0,env:n,purpose:"deploy"}),s=(n.CELLD_BUCKET??"").trim()===""?[...r.warnings,Je]:r.warnings;for(const a of r.errors)e.push({code:"celld-toolchain-missing",level:"fail",message:a});for(const a of s)e.push({code:"celld-toolchain-warn",level:"warn",message:a});r.errors.length===0&&s.length===0&&e.push({code:"celld-toolchain-ok",level:"pass",message:"celld, esbuild and CELLD_BUCKET are in place for a deploy."})},Qe={mismatch:{code:"artifacts-jurisdiction-mismatch",level:"fail"},missing:{code:"artifacts-namespace-missing",level:"fail"},ok:{code:"artifacts-jurisdiction-ok",level:"pass"},unchecked:{code:"artifacts-jurisdiction-unchecked",level:"warn"}},Xe=async(o,t,e,n,r)=>{if(e===void 0||t===void 0||(u(o)??f)!=="cloudflare")return;const s=[["",t],...Object.entries(t.env??{}).map(([i,l])=>[`env.${i}: `,l])],a=new Map;for(const[i,l]of s){const d=l.account_id??t.account_id;for(const p of l.artifacts??[]){const _=`${String(d)}/${String(p?.namespace)}`;typeof p?.binding=="string"&&typeof p.namespace=="string"&&!a.has(_)&&a.set(_,{accountId:d,binding:p.binding,label:i,namespace:p.namespace})}}const c=await Promise.all([...a.values()].map(async({label:i,...l})=>({check:await ne({...l,environment:n.environment,fetch:n.fetch,jurisdiction:e,missingOk:!0}),label:i})));for(const{check:i,label:l}of c)r.push({...Qe[i.verdict],message:`${l}${i.message}`,...i.fix===void 0?{}:{fix:i.fix}})},Ze=async o=>{const t=o.cwd??process.cwd(),e=[],{parsed:n,path:r}=F(t);Re(t,n,r,e),je(t,e),Te(t,n,e),De(n,e),Ee(n,e),Ie(n,e),ye(n,e),_e(n,e),Ne(t,e),Ue(t,e),Ve(t,e),Le(t,e),Se(t,e),qe(t,o.executablePath??process.argv[1],e),$e(t,e),xe(t,e);const s=await Pe(t,e);s!==void 0&&Fe(s,e),fe(n,t,s?.usesAi===!0,e),await Ye(t,o,e),await ze(t,n,o,e),(u(t)??f)==="cloudflare"&&await ke(n,o,e),await Xe(t,n,s?.jurisdiction,o,e),pe(n,s?.usesCimdWorkers===!0,e);const a={fail:0,info:0,pass:0,warn:0};for(const i of e)a[i.level]+=1;const c=a.fail>0?1:0;return{code:c,findings:e,ok:c===0,summary:a}},et={fail:"FAIL",info:"INFO",pass:"PASS",warn:"WARN"},tt=(o,t)=>{t.info("lunora doctor — project preflight");for(const r of o.findings){const s=`[${et[r.level]}] ${r.message}`;r.level==="fail"?t.error(s):r.level==="warn"?t.warn(s):t.info(s),r.fix!==void 0&&r.level!=="pass"&&t.info(` fix: ${r.fix}`)}const{fail:e,warn:n}=o.summary;e>0?t.error(`${String(e)} failure(s), ${String(n)} warning(s).`):n>0?t.warn(`0 failures, ${String(n)} warning(s).`):t.success("all checks passed.")},nt=async o=>{const{logger:t}=o,e=await Ze({...o});return tt(e,t),e},ht=le(async({cwd:o,format:t,logger:e,options:n})=>{if(n.fix===!0){const{path:s}=F(o),a=await Q({configPath:s,cwd:o,dryRun:n.dryRun===!0,logger:e});for(const c of a.applied)e.info(`fix: ${c}`);for(const c of a.skipped)e.warn(`not fixed: ${c}`)}const r=await nt({cwd:o,format:t,logger:e});return{code:r.code,data:{findings:r.findings,summary:r.summary}}});export{ht as execute,Ze as runDoctor,nt as runDoctorCommand};