@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
6 lines (5 loc) • 24.5 kB
JavaScript
import{existsSync as q,readFileSync as N,mkdirSync as Le,openSync as X,closeSync as Q,statSync as Te,readSync as Pe}from"node:fs";import{join as L,dirname as be}from"node:path";import{readServiceBindings as _e}from"@lunora/codegen";import{readLiveDevServerState as O,discoverContainerInfo as Re,streamContainerLogs as $e,formatLunoraEvent as Ce,updateDevServerState as _,targetRunsOwnDevServer as D,resolveProjectTarget as Z,claimDevServerState as Ne,DEV_HANDOFF_ENV as Oe,clearDevServerState as y,readDevServerState as A,isDevServerReady as V,detectFramework as ee,readProjectDependencyNames as De,DEV_LOG_FILE_ENV as Ae,DEV_DAEMON_ENV as xe,detectPackageManager as x,execArgsFor as re,isRecordedProcessCurrent as Me,DEV_LOG_FILE as Ue,CODEGEN_ENV as te,isCodegenDisabled as Ge,planToolchainInvocation as Fe,toolchainExecArgs as ne,startCelldDevSession as je,resolveDeployDriver as oe,DEV_VARS_FILE as se,runScriptCommand as Ve,parseDevVariableEntries as We}from"@lunora/config";import{warnCloudflareCliConfigOnce as Be,findWranglerFile as T,readWranglerJsonc as M,materializeRemoteWranglerConfig as Ke,materializeServiceDevConfigs as He}from"@lunora/config/cloudflare";import{createServer as ze}from"node:net";import{networkInterfaces as Ye}from"node:os";import{spawn as ie,spawnSync as P}from"node:child_process";import{setTimeout as w}from"node:timers/promises";import{EXIT_CODE as ae}from"./EXIT_CODE-D_XCixv8.mjs";import{a as h}from"./output-format-B4eQHi5y.mjs";import{spawnShellCompat as ce,defaultSpawner as Je}from"./createRecordingSpawner-CYhdPBD5.mjs";import{isLunoraError as qe}from"@lunora/errors";import{s as W}from"./index-oHZBGBwB.mjs";import{createInterface as Xe}from"node:readline";const Qe=12e4,R=250,Ze=1e3,er="LUNORA_DEV_READY_TIMEOUT_MS",rr=e=>{if(e!==void 0)return e;const r=Number(process.env[er]??"");return Number.isFinite(r)&&r>0?r:Qe},tr=async(e,r)=>{try{const n=AbortSignal.timeout(Ze);return await(await fetch(new URL("/_lunora/status",e),{redirect:"manual",signal:r===void 0?n:AbortSignal.any([r,n])})).body?.cancel(),!0}catch{return!1}},le=async(e,r,n)=>{const t=Date.now()+n.timeoutMs;for(;n.signal?.aborted!==!0;){const o=r(O(e));if(o!==void 0)return o;if(Date.now()>=t)return;try{await w(n.intervalMs??500,void 0,{signal:n.signal})}catch{return}}},B=(e,r)=>{Xe({crlfDelay:Number.POSITIVE_INFINITY,input:e}).on("line",r)},nr=5e3,de=(e,r,n,t)=>{if(e.length===0)return;const o=Ce(e);if(o){t[o.level](`[lunora] ${o.text}`);return}const s=`[${r}] ${e}`;n==="stderr"?t.warn(s):t.info(s)},or=(e,r,n=process.platform,t=P)=>{try{if(r==="SIGKILL"&&n==="win32"&&e.pid!==void 0){t("taskkill",["/pid",String(e.pid),"/T","/F"],{stdio:"ignore"});return}e.kill(r)}catch{}},ue=(e,r,n)=>{const t=e.direct===!0?{args:[...e.args],command:e.command,shell:!1}:ce(e.command,e.args),o=ie(t.command,t.args,{cwd:e.cwd??process.cwd(),env:e.env?{...process.env,...e.env}:process.env,shell:t.shell,stdio:[e.direct===!0?"ignore":"inherit","pipe","pipe"]});return B(o.stdout,s=>{r(s,"stdout")}),B(o.stderr,s=>{r(s,"stderr")}),{exited:new Promise(s=>{o.on("error",i=>{n?.(i),s(1)}),o.on("exit",(i,a)=>{s(i??(a?1:0))})}),kill:s=>{or(o,s)}}},vt=(e,r)=>ue(e,(n,t)=>{de(n.trimEnd(),e.tag,t,r)},n=>{r.error(`[${e.tag}] failed to start: ${n.message}`)}),gt=(e,r)=>{if(process.env.LUNORA_CONTAINER_LOGS==="0")return;const t=Re(e,"lunora").containers.map(o=>({className:o.className,exportName:o.exportName}));if(t.length!==0)return $e({containers:t,onLine:o=>{const s=`[container:${o.name}] ${o.text}`;o.level==="error"?r.warn(s):r.info(s)},onUnavailable:o=>{r.warn(`[container] Docker engine unreachable — container logs unavailable (${o})`)}})},pt=async e=>{e.readyProbe?.abort(),await e.tunnel?.close(),await e.codegen?.close().catch(()=>{}),e.containerLogs?.close(),await e.studio?.close().catch(()=>{});try{e.remoteCleanup?.(),e.serviceConfigCleanup?.()}catch{}},mt=async e=>await new Promise(r=>{const n={},t=i=>{e.info(`received ${i} — shutting down`),n.sigint&&process.off("SIGINT",n.sigint),n.sigterm&&process.off("SIGTERM",n.sigterm),r(0)},o=()=>{t("SIGINT")},s=()=>{t("SIGTERM")};n.sigint=o,n.sigterm=s,process.on("SIGINT",o),process.on("SIGTERM",s)}),wt=async(e,r,n)=>{let t=0,o;const s=l=>{e.kill(l),r?.kill(l)},i=()=>{t+=1,t===1?(n.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)"),s("SIGTERM"),o=setTimeout(()=>{s("SIGKILL")},nr),o.unref()):s("SIGKILL")},a=()=>{s("SIGTERM")};process.on("SIGINT",i),process.on("SIGTERM",a);const c=await Promise.race([e.exited.then(l=>({code:l,who:"worker"})),...r===void 0?[]:[r.exited.then(l=>({code:l,who:"sidecar"}))]]);return r!==void 0&&(c.who==="sidecar"?(n.warn("[worker] the Lunora sidecar (wrangler dev) exited — shutting down the framework dev server"),e.kill("SIGTERM")):r.kill("SIGTERM"),await Promise.allSettled([e.exited,r.exited])),o&&clearTimeout(o),process.off("SIGINT",i),process.off("SIGTERM",a),c.code},E="2026.9.3",sr="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/",ir=/version (\S+)/u,K=/https:\/\/[\da-z-]+\.trycloudflare\.com/u,ar="1s",cr=5e3,H=async(e,r)=>await Promise.race([e.then(()=>!0),w(r,!1,{ref:!1})]),lr=6e4,dr='Vite answers 403 for hosts it does not know — add `server: { allowedHosts: [".trycloudflare.com"] }` to vite.config if the URL is blocked.',ur=async e=>{const r=`Install or update it: ${sr} (macOS: \`brew install cloudflared\`).`;let n;try{const o=await e({args:["--version"],captureStdoutSilently:!0,command:"cloudflared"});if(o.code!==0)return{message:`\`cloudflared --version\` exited ${String(o.code)}. ${r}`,ok:!1};n=o.stdout??""}catch(o){return qe(o)&&o.code==="LOCAL_DEPENDENCY_MISSING"?{message:`--tunnel needs \`cloudflared\` ${E} or newer, and it is not on your PATH. ${r}`,ok:!1}:{message:`\`cloudflared --version\` could not run: ${o instanceof Error?o.message:String(o)}. ${r}`,ok:!1}}const t=W.coerce(ir.exec(n)?.[1])?.version;return t===void 0?{ok:!0,version:void 0}:W.gte(t,E)?{ok:!0,version:t}:{message:`--tunnel needs \`cloudflared\` ${E} or newer (protected quick tunnels); found ${t}. ${r}`,ok:!1}},fr=e=>{const r=[],n=[];for(const t of e??[]){const o=t.split(",").map(s=>s.trim()).filter(s=>s.length>0);o.length>0&&(n.push(...o.filter(s=>!s.includes("@"))),r.push(o.join(",")))}return{entries:r,invalid:n}},vr=(e,r)=>["tunnel","--url",e,"--output","json","--grace-period",ar,...r.flatMap(n=>["--allowed-mail",n])],gr=e=>{let r;try{r=JSON.parse(e)}catch{r=void 0}if(typeof r=="object"&&r!==null){const n=r,t=typeof n.message=="string"?n.message:"";return{level:typeof n.level=="string"?n.level:void 0,text:t,url:K.exec(t)?.[0]}}return{level:void 0,text:e,url:K.exec(e)?.[0]}},pr=e=>{e.warn(" ⚠ PUBLIC TUNNEL: anyone with the tunnel URL can reach this dev server — there is no sign-in in front of it."),e.warn(" Protect it with a one-time-PIN email allow-list: lunora dev --tunnel --allow-mail you@example.com"),e.warn(" (repeat --allow-mail, pass a comma-separated list, or allow a whole domain with '*@example.com')")},mr=async(e,r,n,{intervalMs:t,timeoutMs:o=lr}={})=>{const s=await le(e,i=>i?.mode==="vite"?{origin:i.url,ownerPid:i.pid}:void 0,{intervalMs:t,signal:n,timeoutMs:o});return s===void 0&&!n.aborted&&r.warn(`--tunnel: the Vite dev server did not report its URL within ${String(o/1e3)}s — continuing without a tunnel.`),s},wr=e=>{const{allowMail:r,cwd:n,logger:t}=e,o=r.length===0,s=new AbortController,{signal:i}=s,a=()=>i.aborted,c=e.closeTimeoutMs??cr;let l;const d=()=>{s.abort()};process.once("SIGINT",d),process.once("SIGTERM",d);const v=(async()=>{const u=await ur(e.spawner??Je);if(!u.ok){t.error(u.message),t.warn("continuing without a tunnel.");return}u.version===void 0&&t.warn(`could not read the cloudflared version — --tunnel needs ${E} or newer; trying anyway.`);const g=e.origin.kind==="fixed"?{origin:e.origin.origin,ownerPid:process.pid}:await mr(n,t,i);if(g===void 0||i.aborted)return;o&&pr(t),t.info(`starting a Cloudflare quick tunnel to ${g.origin}${o?"":" (email one-time-PIN protected)"}…`);let S;const G={failedToStart:!1},ye=k=>{if(k.length===0)return;const p=gr(k);if(p.url!==void 0&&S===void 0){S=p.url,t.success(` ➜ Tunnel: ${p.url}${o?" (PUBLIC)":""}`),o||t.info(` Allowed: ${r.join(", ")} — visitors confirm with a one-time PIN; a session lasts up to 4 hours`),e.origin.kind==="vite"&&t.info(` ${dr}`),_(n,{tunnelUrl:p.url},{expectedPid:g.ownerPid});return}p.level==="error"||p.level==="fatal"?t.warn(`[cloudflared] ${p.text}`):t.debug?.(`[cloudflared] ${p.text}`)},b=(e.startChild??ue)({args:vr(g.origin,r),command:"cloudflared",direct:!0},ye,k=>{G.failedToStart=!0,t.error(`could not start cloudflared (${k.message}) — continuing without a tunnel.`)});l=b;const F=()=>{b.kill("SIGTERM")};i.addEventListener("abort",F,{once:!0});const j=await b.exited;i.removeEventListener("abort",F),l=void 0,S!==void 0&&_(n,{tunnelUrl:void 0},{expectedPid:g.ownerPid}),!(a()||G.failedToStart)&&t.warn(S===void 0?`cloudflared exited (${String(j)}) before a tunnel URL was assigned — continuing without a tunnel.`:`cloudflared exited (${String(j)}) — the tunnel is closed; the dev server keeps running.`)})().catch(u=>{t.error(`--tunnel failed (${u instanceof Error?u.message:String(u)}) — continuing without a tunnel.`)});return{close:async()=>{if(process.off("SIGINT",d),process.off("SIGTERM",d),s.abort(),await H(v,c))return;const u=l;u!==void 0&&(t.warn(`cloudflared did not exit within ${String(c/1e3)}s of SIGTERM — force-killing it.`),u.kill("SIGKILL"),!await H(u.exited,c)&&t.error(`could not confirm that cloudflared stopped — ${o?"the PUBLIC tunnel":"the tunnel"} may still be reachable. Stop the \`cloudflared\` process manually.`))}}},ht=e=>{const{cwd:r,logger:n,plan:t,tunnel:o}=e;if(o!==void 0)return wr({allowMail:o.allowMail,cwd:r,logger:n,origin:t.flavor==="wrangler"?{kind:"fixed",origin:t.workerOrigin}:{kind:"vite"},spawner:o.spawner,startChild:o.startChild})},hr=(e,r)=>{const n=["dev"];if(e.apiSpec!==void 0&&n.push("--api-spec",e.apiSpec),e.port!==void 0&&n.push("--port",String(e.port)),e.workerPort!==void 0&&n.push("--worker-port",String(e.workerPort)),e.inspectorPort!==void 0&&n.push("--inspector-port",String(e.inspectorPort)),e.emitBindings!==void 0&&n.push("--emit-bindings",e.emitBindings),e.codegen===!1&&n.push("--no-codegen"),e.studio===!1&&n.push("--no-studio"),e.worker===!1&&n.push("--no-worker"),e.target!==void 0&&n.push("--target",e.target),e.tunnel===!0){n.push("--tunnel");for(const t of fr(e.allowMail).entries)n.push("--allow-mail",t)}return r&&n.push("--remote"),e.local===!0&&n.push("--local"),n},Sr=1e4,kr=100,Ir=40,I=256*1024,Er=new Set(["nuxt","sveltekit"]),yr=["@lunora/rspack","@lunora/vite"],U=e=>{if(Er.has(ee(e).framework))return"framework-worker";const r=De(e);return yr.some(n=>r.has(n))?"vite":"wrangler"},fe=e=>{const r=x(e);let n;try{const t=N(L(e,"package.json"),"utf8");n=JSON.parse(t).scripts?.dev}catch{}if(n===void 0||n.trim()===""||n.includes("lunora")){const t=re(r,"vite",["dev"]);return{args:t.args,command:t.command}}return{args:["run","dev"],command:r}},Lr=e=>{Le(be(e.logPath),{recursive:!0});const r=X(e.logPath,"w",384);let n;const t=ce(e.command,e.args);try{n=ie(t.command,t.args,{cwd:e.cwd,detached:!0,env:e.env,shell:t.shell,stdio:["ignore",r,r],windowsHide:!0})}finally{Q(r)}return n.unref(),{exited:new Promise(o=>{n.on("error",()=>{o(1)}),n.on("exit",(s,i)=>{o(s??(i?1:0))})}),pid:n.pid}},Tr=e=>{try{const{size:r}=Te(e);if(r<=I)return N(e,"utf8");const n=X(e,"r");try{const t=Buffer.alloc(I),o=Pe(n,t,0,I,r-I),s=t.toString("utf8",0,o);return s.slice(s.indexOf(`
`)+1)}finally{Q(n)}}catch{return}},ve=(e,r)=>{const n=Tr(e);if(n===void 0)return[];const t=n.split(`
`);return t.at(-1)===""&&t.pop(),r>0?t.slice(-r):t},ge=(e,r)=>r?.logFile??L(e,Ue),pe=e=>{e.info(" Stop: lunora dev stop"),e.info(" Status: lunora dev status"),e.info(" Logs: lunora dev logs")},Pr=(e,r)=>{Be(r,n=>{e.warn(n)})},br=(e,r)=>{e.warn(`Dev server already running at ${r.url} (pid ${String(r.pid)})`),pe(e)},_r=(e,r)=>{e.success(`Dev server running at ${r.url} (pid ${String(r.pid)})`),r.studioUrl!==void 0&&e.info(` Studio: ${r.studioUrl}`),r.tunnelUrl!==void 0&&e.info(` Tunnel: ${r.tunnelUrl}`),pe(e)},Rr=3e4,$r=async(e,r,n,t)=>{const o=await le(e,s=>s?.tunnelUrl,{intervalMs:n,timeoutMs:t});o===void 0?r.warn(" Tunnel: no URL yet — `lunora dev status` shows it once cloudflared reports one; `lunora dev logs` says why if it does not."):r.success(` Tunnel: ${o}`)},Cr=async e=>{const{child:r,cwd:n,deadline:t,pollIntervalMs:o,probe:s}=e,i=r.exited.then(a=>({exitCode:a}));for(;Date.now()<t;){const a=O(n);if(a!==void 0&&a.pid!==process.pid&&await s(a.url))return{state:a,status:"ready"};const c=await Promise.race([i,w(o)]);if(c!==void 0)return{...c,status:"exited"}}return{status:"timeout"}},Nr=async e=>{const{cwd:r,logger:n}=e,t=ge(r,void 0),o=rr(e.readyTimeoutMs),s=(e.spawnDetached??Lr)({args:e.command.args,command:e.command.command,cwd:r,env:{...process.env,...e.env,[xe]:"1",[Ae]:t,...process.env.FORCE_COLOR===void 0?{NO_COLOR:"1"}:{},...e.json?{LUNORA_LOG_JSON:"1"}:{}},logPath:t}),i=await Cr({child:s,cwd:r,deadline:Date.now()+o,pollIntervalMs:e.pollIntervalMs??R,probe:e.probe??tr});if(i.status==="ready")return _r(n,i.state),e.tunnel===!0&&i.state.tunnelUrl===void 0&&await $r(r,n,e.pollIntervalMs??R,e.tunnelUrlTimeoutMs??Rr),{code:0};if(i.status==="exited"){n.error(`dev server exited before becoming ready (exit code ${String(i.exitCode)}). Last output:`);for(const c of ve(t,Ir))n.error(` ${c}`);return y(r,s.pid??process.pid),{code:i.exitCode===0?1:i.exitCode}}s.pid!==void 0&&A(r)?.pid===process.pid&&_(r,{logFile:t,pid:s.pid});const a=s.pid===void 0?"":` (pid ${String(s.pid)})`;return n.warn(`dev server did not confirm ready within ${String(Math.round(o/1e3))}s — it may still be compiling${a}. Check \`lunora dev status\` and \`lunora dev logs\`; \`lunora dev stop\` shuts it down.`),{code:ae.UNAVAILABLE}},me=e=>e.codegen!==!1&&!Ge(process.env[te]),we=e=>{const r={...e.remote===!0?{LUNORA_REMOTE:"1"}:{},...e.codegen===!1?{[te]:"0"}:{}};return Object.keys(r).length>0?{env:r}:{}},St=async e=>{const{cwd:r,jsonLogs:n,logger:t,options:o,remote:s}=e,i=e.run??Nr,a=U(r),c=a==="vite"&&D(o.target??Z(r))?"wrangler":a,l=c==="wrangler"?`http://localhost:${String(o.workerPort??8787)}`:"http://localhost:5173",d=Ne(r,{background:!0,mode:"cli",pid:process.pid,startedAt:new Date().toISOString(),url:l});if(!d.ok)return d.existing!==void 0&&br(t,d.existing),{code:0};const f={[Oe]:String(process.pid)};Pr(t,r);try{return c==="vite"&&o.tunnel!==!0?await i({command:fe(r),cwd:r,env:{...we({...o,remote:s}).env,...f},json:n,logger:t}):await i({command:{args:[process.argv[1]??"lunora",...hr(o,s)],command:process.execPath},cwd:r,env:f,json:n,logger:t,tunnel:o.tunnel===!0})}finally{y(r,process.pid)}},Or=e=>{try{const r=P("ps",["-o","pgid=","-p",String(e)],{encoding:"utf8"}),n=Number.parseInt(r.stdout.trim(),10);return Number.isInteger(n)&&n>0?n:void 0}catch{return}},Dr=(e,r,n=process.platform,t=P)=>{if(n==="win32"){try{t("taskkill",["/pid",String(e.pid),"/T","/F"],{stdio:"ignore"})}catch{}return}if(e.background===!0)try{r(-(Or(e.pid)??e.pid),"SIGKILL");return}catch{}try{r(e.pid,"SIGKILL")}catch{}},Ar=(e,r)=>(r.json?h({pid:e.pid,stopped:!1}):r.logger.error(`dev server (pid ${String(e.pid)}) survived the force-kill — record kept; retry \`lunora dev stop\` or kill the process manually.`),{code:1}),xr=async e=>{const{cwd:r,logger:n}=e,t=A(r),o=e.alive??(f=>f===t?.pid&&Me(t)),s=e.signal??((f,v)=>{process.kill(f,v)}),i=e.pollIntervalMs??R,a=e.stopGraceMs??Sr,c=e.platform??process.platform,l=e.spawnSyncImpl??P;if(t===void 0||!o(t.pid))return t!==void 0&&y(r,t.pid),e.json?h({running:!1,stopped:!1}):n.info("No dev server running."),{code:0};try{s(t.pid,"SIGTERM")}catch{}const d=Date.now()+a;for(;o(t.pid)&&Date.now()<d;)await w(i);return o(t.pid)&&(Dr(t,s,c,l),await w(i),o(t.pid))?Ar(t,e):(y(r,t.pid),e.json?h({pid:t.pid,stopped:!0}):n.success(`Stopped dev server (pid ${String(t.pid)}).`),{code:0})},Mr=e=>{const{cwd:r,logger:n}=e,t=O(r);if(t===void 0)return e.json?h({running:!1}):n.info("No dev server running."),{code:0};const o=e.now??Date.now,s=t.startedAt===void 0?Number.NaN:Date.parse(t.startedAt),i=Number.isFinite(s)?Math.max(0,Math.round((o()-s)/1e3)):void 0;if(e.json)return h({background:t.background===!0,logFile:t.logFile,mode:t.mode,pid:t.pid,ready:V(t),readyAt:t.readyAt,running:!0,startedAt:t.startedAt,studioUrl:t.studioUrl,tunnelUrl:t.tunnelUrl,uptimeSeconds:i,url:t.url}),{code:0};const a=[`pid ${String(t.pid)}`,...i===void 0?[]:[`uptime ${String(i)}s`],t.background===!0?"background":"foreground",V(t)?"ready":"starting"];return n.success(`Dev server running at ${t.url} (${a.join(", ")})`),t.studioUrl!==void 0&&n.info(` Studio: ${t.studioUrl}`),t.tunnelUrl!==void 0&&n.info(` Tunnel: ${t.tunnelUrl}`),t.logFile!==void 0&&n.info(` Logs: lunora dev logs (${t.logFile})`),{code:0}},Ur=e=>{const{cwd:r,logger:n}=e,t=A(r),o=ge(r,t);if(!q(o))return n.info("No dev server logs found — output is captured when the server runs in background mode (`lunora dev --background`)."),{code:0};const s=ve(o,e.lines??kr);return s.length>0&&process.stdout.write(`${s.join(`
`)}
`),{code:0}},kt=e=>{const{cwd:r,json:n,lines:t,logger:o,subcommand:s}=e;if(s==="stop")return xr({cwd:r,json:n,logger:o});if(s==="status")return Mr({cwd:r,json:n,logger:o});if(s==="logs")return Ur({cwd:r,lines:t,logger:o});if(s!==void 0)return o.error(`dev: unknown subcommand "${s}" — expected stop | status | logs (or no subcommand to start the dev server)`),{code:ae.USAGE}},It=(e,r,n)=>D(e)?(r!=="wrangler"&&n.info(`target ${e} runs its own dev server, so lunora dev serves the worker on it — start the frontend's dev server separately`),"wrangler"):r,Et=async e=>{const{logger:r}=e,n=await(e.start??je)({log:(s,{stream:i,tag:a})=>{de(s,a,i,r)},port:e.port,projectRoot:e.projectRoot});let t=()=>{};const o=new Promise(s=>{t=s});return{exited:Promise.race([o,n.exited]),kill:()=>{n.stop().then(()=>0,()=>1).then(t).catch(()=>{})}}},Gr=e=>{const{cwd:r,driver:n,options:t,studioPort:o,workerPort:s}=e,{projectConfig:i,toolchain:a}=n;if(i===void 0||a===void 0)throw new Error(`deploy target "${n.id}" runs its own dev server but ships no config projection or toolchain for it`);if(t.remote===!0)throw new Error(`--remote proxies bindings to Cloudflare; ${n.name} has no remote bindings to proxy to`);t.inspectorPort!==void 0&&t.logger.warn(`--inspector-port is a wrangler dev flag; ${n.name} dev has no inspector to pin`);const{command:c,commit:l,projection:d}=Fe(n,r,"dev",v=>a.dev({configPath:v,extraArgs:["--port",String(s)]}));l(),d!==void 0&&d.dropped.length>0&&t.logger.info(`${n.name} ignores these wrangler keys, so its dev server runs without them: ${d.dropped.join(", ")}`);const f=ne(x(r),c);return{flavor:"wrangler",ipv4LoopbackForced:!1,remote:{bindings:[],cleanup:()=>{},enabled:!1},runsCodegenWatch:me(t),celldSession:!0,studioEnabled:t.studio!==!1,studioPort:o,workerEnabled:t.worker!==!1,workerOrigin:`http://localhost:${String(s)}`,workerPort:s,wrangler:{args:f.args,command:f.command,cwd:r,tag:c.tool}}},Fr=64,he="127.0.0.1",jr=65535,Vr=(e,r=he)=>new Promise(n=>{const t=ze();t.once("error",()=>{n(!1)}),t.once("listening",()=>{t.close(()=>{n(!0)})}),t.listen(e,r)}),Wr=async(e,r=he,n=Fr)=>{for(let t=e;t<e+n&&t<=jr;t+=1)if(await Vr(t,r))return t;return e},Se=(e=Ye)=>Object.values(e()).some(r=>r?.some(n=>n.internal&&n.address==="::1")),$="wrangler.dev.jsonc",C=6173,m=8787,z=5173,Br=(e,r)=>{const n=()=>{};if(!e.remote)return{args:[],plan:{bindings:[],cleanup:n,enabled:!1}};const o=(e.materializeRemote??Ke)({enabled:!0,projectRoot:r}),s=o.remoteBindings.map(a=>`${a.binding} (${a.kind})`),{cleanup:i}=o;return o.configPath===void 0?{args:[],plan:{bindings:s,cleanup:i,enabled:!0,reason:o.reason}}:{args:["--config",o.configPath],plan:{bindings:s,cleanup:i,enabled:!0}}},ke=(e,r)=>{const n=[...new Set(_e(r).services.map(s=>s.wranglerPath))],{cleanup:t,configPaths:o}=(e.materializeServiceConfigs??He)(n);return{args:o.flatMap(s=>["--config",s]),cleanup:t}},Kr=(e,r,n)=>{const t=T(e);return r.length===0||t===void 0?[]:[...n.length>0?[]:["--config",t],...r]},Ie=e=>e.local===!0?["--local"]:[],Y=e=>M(e).parsed?.dev?.ip,Ee=(e,r,n)=>{if(n!==void 0){const o=L(e,n);if(q(o)&&Y(o)!==void 0)return[]}const t=T(e);return t!==void 0&&Y(t)!==void 0?[]:r()?[]:["--ip","127.0.0.1"]},Hr=new Set(["127.0.0.1","::1","[::1]","localhost"]),zr=(e,r)=>{let n;try{n=N(L(e,se),"utf8")}catch{return[]}const t=[];for(const o of We(n)){let s;try{s=new URL(o.value)}catch{continue}Hr.has(s.hostname)&&s.port===String(r)&&t.push(o.key)}return t},J=async(e,r)=>{if(e.workerPort!==void 0)return e.workerPort;const n=T(r);if(n!==void 0){const{parsed:s}=M(n);if(typeof s?.dev?.port=="number")return s.dev.port}const t=await(e.findFreePort??Wr)(m);if(t===m)return t;const o=zr(r,m);if(o.length>0)throw new Error(`port ${String(m)} is in use, but ${se} pins the worker origin to it (${o.join(", ")}). Serving on ${String(t)} would leave those URLs pointing at nothing. Stop whatever holds ${String(m)}, or run \`lunora dev --worker-port ${String(t)}\` and update those values to match.`);return t},Yr=(e,r)=>{if(e.inspectorPort!==void 0)return e.inspectorPort;const n=T(r);if(n===void 0)return;const{parsed:t}=M(n),o=t?.dev?.inspector_port;return typeof o=="number"?o:void 0},Jr=(e,r,n)=>{const t=Ee(r,e.hasIpv6Loopback??Se,$),o=oe(e.target??Z(r));if(o.toolchain===void 0)throw new Error(`deploy target "${o.id}" has no dev server to run the worker sidecar`);const s=ke(e,r),i=o.toolchain.dev({configPath:$,extraArgs:[...t,...Ie(e),"--var","WORKER_ENV:development",...s.args]}),a=ne(n,i);return{cleanup:s.cleanup,sidecar:{args:a.args,command:a.command,cwd:r,tag:"worker"}}},qr=(e,r)=>{e.worker===!1&&e.logger.warn(`--no-worker does not apply to the ${r} flavor: Vite owns the worker, codegen and studio in-process. Run your framework's dev script instead.`),e.inspectorPort!==void 0&&e.logger.warn(r==="framework-worker"?`--inspector-port does not apply to the ${r} flavor: pin \`dev.inspector_port\` in ${$} — that is the config the worker sidecar runs.`:`--inspector-port does not apply to the ${r} flavor: Vite owns the worker. Pin it in vite.config — \`lunora({ cloudflare: { inspectorPort: ${String(e.inspectorPort)} } })\`.`),e.local===!0&&r==="vite"&&e.logger.warn("--local does not apply to the vite flavor: Vite owns the worker. Turn remote bindings off in vite.config — `lunora({ cloudflare: { remoteBindings: false } })`.")},Xr=e=>{const r=e.cwd??process.cwd(),n=x(r),t=e.flavor??U(r);if(t==="vite"||t==="framework-worker"){const v=fe(r),u=t==="framework-worker"?Jr(e,r,n):void 0,g=u?.sidecar;return qr(e,t),{runsCodegenWatch:!1,flavor:t,ipv4LoopbackForced:!1,remote:{bindings:[],cleanup:()=>{},enabled:e.remote===!0},...u?{serviceConfigCleanup:u.cleanup}:{},...g?{sidecar:g}:{},studioEnabled:!1,studioPort:e.port??C,workerEnabled:!0,workerOrigin:`http://localhost:${String(z)}`,workerPort:z,wrangler:{args:v.args,command:v.command,cwd:r,...we(e),tag:"vite"}}}const o=ee(r),s=o.framework==="none"?void 0:`this project uses ${o.framework} — the worker runs inside Vite there. run \`${Ve(n,"dev")}\` for the full app (frontend + HMR); \`lunora dev\` starts only the worker.`,i=e.workerPort??m,a=Br(e,r),c=Ee(r,e.hasIpv6Loopback??Se),l=e.inspectorPort===void 0?[]:["--inspector-port",String(e.inspectorPort)],d=ke(e,r),f=re(n,"wrangler",["dev","--port",String(i),...l,...c,...Ie(e),"--var","WORKER_ENV:development",...a.args,...Kr(r,d.args,a.args)]);return{runsCodegenWatch:me(e),flavor:t,frameworkHint:s,ipv4LoopbackForced:c.length>0,remote:a.plan,serviceConfigCleanup:d.cleanup,studioEnabled:e.studio!==!1,studioPort:e.port??C,workerEnabled:e.worker!==!1,workerOrigin:`http://localhost:${String(i)}`,workerPort:i,wrangler:{args:f.args,command:f.command,cwd:r,tag:"wrangler"}}},yt=async e=>{const r=e.cwd??process.cwd();if(e.target!==void 0&&D(e.target))return Gr({cwd:r,driver:oe(e.target),options:e,studioPort:e.port??C,workerPort:await J(e,r)});const n=e.flavor??U(r),t=n==="wrangler"?await J(e,r):e.workerPort,o=n==="wrangler"?Yr(e,r):e.inspectorPort;return Xr({...e,cwd:r,flavor:n,inspectorPort:o,workerPort:t})};export{R as P,kt as a,br as b,pr as c,tr as d,U as e,It as f,yt as g,Pr as h,ht as i,vt as j,wt as k,me as l,Et as m,fr as n,gt as o,Xr as p,Yr as q,rr as r,St as s,pt as t,J as u,mt as w};