UNPKG

@lunora/cli

Version:

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

8 lines (7 loc) 22 kB
import{spawnSync as N,spawn as J}from"node:child_process";import{existsSync as L,watch as me,readFileSync as K,statSync as fe,openSync as z,readSync as ve,closeSync as Y,mkdirSync as we}from"node:fs";import{join as k,dirname as he}from"node:path";import{runCodegen as X}from"@lunora/codegen";import{detectAgentRules as Z,claimDevServerState as Q,clearDevServerState as f,readDevServerState as R,readLiveDevServerState as y,detectFramework as ee,readProjectDependencyNames as Se,DEV_HANDOFF_ENV as h,isRecordedProcessCurrent as ke,DEV_LOG_FILE as ye,DEV_LOG_FILE_ENV as re,DEV_DAEMON_ENV as I,updateDevServerState as oe,detectAiAgent as be,resolveRemoteEnabled as $e,readProjectRemotePreference as Ee,inferLunoraBindings as Ne,packageNamesFromBindings as Le,ensureDevVarsExample as Re,ensureDevVariables as Ie,isInteractive as Oe,DEV_VARS_FILE as T,DEV_VARS_EXAMPLE_FILE as Pe,fillDevSecrets as xe,claimAgentRulesHint as De,AGENT_RULES_HINT as Te,findWranglerFile as te,readWranglerJsonc as ne,discoverContainerInfo as Ce,streamContainerLogs as _e,materializeRemoteWranglerConfig as Ae,formatLunoraEvent as je}from"@lunora/config";import{r as Fe}from"../packem_shared/api-spec-BENwiyUa.mjs";import{l as Me}from"../packem_shared/codegen-error-DJN6pTH5.mjs";import{i as Ge}from"../packem_shared/command-Bjv4VmYA.mjs";import{P as se,M as $,k as He}from"../packem_shared/detect-package-manager-B2eIuAiP.mjs";import{createServer as Ue,connect as Ve}from"node:net";import{forceJsonLogging as We}from"../packem_shared/createLogger--C3x1FNE.mjs";import{networkInterfaces as qe}from"node:os";import{spawnShellCompat as ie}from"../packem_shared/createRecordingSpawner-C8_7nffs.mjs";import{createServer as Be,request as Je}from"node:http";import{loadStudioAssets as C,studioAssetsStamp as _,renderStudioHtml as Ke,resolveAdminToken as ze,SCHEMA_EDIT_ENDPOINT as Ye,POLICY_SCAFFOLD_ENDPOINT as Xe,SEED_ENDPOINT as Ze,serveJsonHandler as Qe,isStandaloneModulePath as er,readStandaloneAsset as rr,assetContentType as or,handleSchemaEditRequest as tr,handlePolicyScaffoldRequest as nr,handleSeedRequest as sr}from"@lunora/config/studio-host";import{$ as ir}from"../packem_shared/tui-prompts-CCt3jm1k.mjs";import{d as v}from"../packem_shared/output-format-D7cKB-O5.mjs";const ar=100,dr=/[/\\]/u,A=(e,r,t,o,n)=>{try{X({apiSpec:t,lunoraDirectory:r,projectRoot:e}),o.success(`codegen: wrote ${r}/_generated (${n})`)}catch(s){o.error(Me(s,n))}},cr=e=>{const r=e.lunoraDirectory??"lunora",t=k(e.projectRoot,r),o=e.debounceMs??ar,{apiSpec:n}=e;A(e.projectRoot,r,n,e.logger,"startup");let s,i;if(!L(t))return e.logger.warn(`codegen watch unavailable (no such directory: ${t}) — schema edits will NOT auto-regenerate. Run \`lunora codegen\` manually after each edit.`),{close:()=>{},watchAvailable:!1};try{i=me(t,{recursive:!0},(c,u)=>{typeof u=="string"&&u.split(dr).includes("_generated")||(s&&clearTimeout(s),s=setTimeout(A,o,e.projectRoot,r,n,e.logger,`change: ${u??"?"}`))})}catch(c){return e.logger.warn(`codegen watch unavailable (${c instanceof Error?c.message:String(c)}) — schema edits will NOT auto-regenerate. Run \`lunora codegen\` manually after each edit.`),{close:()=>{},watchAvailable:!1}}const a=i;return{close:()=>{s&&clearTimeout(s),a.close()},watchAvailable:!0}},lr=64,ae="127.0.0.1",ur=65535,pr=(e,r=ae)=>new Promise(t=>{const o=Ue();o.once("error",()=>{t(!1)}),o.once("listening",()=>{o.close(()=>{t(!0)})}),o.listen(e,r)}),gr=async(e,r=ae,t=lr)=>{for(let o=e;o<e+t&&o<=ur;o+=1)if(await pr(o,r))return o;return e},j=(e=qe)=>Object.values(e()).some(r=>r?.some(t=>t.internal&&t.address==="::1")),F="/_lunora",M=e=>{const r=e.indexOf("?");return r===-1?e:e.slice(0,r)},mr=new Set(["127.0.0.1","::1","[::1]","localhost"]),G=e=>{if(e===void 0||e==="")return!1;const r=e.startsWith("[")?e.slice(0,e.indexOf("]")+1):e.split(":")[0]??e;return mr.has(r)},fr=(e,r,t)=>{const o=Je({headers:{...e.headers,host:t.host},hostname:t.hostname,method:e.method,path:e.url,port:t.port},n=>{r.writeHead(n.statusCode??502,n.headers),n.pipe(r)});o.on("error",n=>{r.statusCode=502,r.end(`lunora dev: worker unreachable (${n.message})`)}),e.pipe(o)},vr=5e3,wr=(e,r,t,o)=>{const n=Ve({host:o.hostname,port:Number(o.port)},()=>{n.setTimeout(0);const s=[`${e.method??"GET"} ${e.url??"/"} HTTP/1.1`];for(const[i,a]of Object.entries(e.headers))if(i!=="host")for(const c of Array.isArray(a)?a:[a])c!==void 0&&s.push(`${i}: ${c}`);s.push(`host: ${o.host}`,"",""),n.write(s.join(`\r `)),t.length>0&&n.write(t),n.pipe(r),r.pipe(n)});n.setTimeout(vr,()=>n.destroy()),n.on("error",()=>r.destroy()),r.on("error",()=>n.destroy())},hr=async e=>{const r=e.host??"127.0.0.1",t=r==="127.0.0.1"||r==="localhost"||r==="::1",o=new URL(e.workerOrigin);let n=C(e.logger,import.meta.url),s=_(import.meta.url);const i=Ke({adminToken:t?ze(e.cwd):void 0,basePath:"/",dataEditable:t,rulesInstalled:Z(e.cwd).installed,runAsIdentity:t,schemaEditable:t,scriptSrc:"/studio.js",styleHref:"/styles.css"}),a=(l,d,g)=>{l.statusCode=200,l.setHeader("Content-Type",g),l.end(d)},c=(l,d,g,b)=>{if(t){Qe(l,d,g,e.cwd);return}d.statusCode=403,d.setHeader("Content-Type","text/plain"),d.end(b)},u=(l,d)=>{const g=l==="/styles.css",b=er(l);if(!g&&!b)return!1;const P=_(import.meta.url);if(P!==s&&(n=C(e.logger,import.meta.url),s=P),n===void 0)return d.statusCode=501,d.setHeader("Content-Type","text/plain"),d.end("Lunora studio assets not found — install and build @lunora/studio."),!0;if(g)return a(d,n.styles,"text/css; charset=utf-8"),!0;const x=l.slice(l.lastIndexOf("/")+1),D=rr(x,import.meta.url);return D===void 0?(d.statusCode=404,d.setHeader("Content-Type","text/plain"),d.end("Not found"),!0):(a(d,D,or(x)),!0)},p=Buffer.from(i),m=Be((l,d)=>{const g=M(l.url??"/");if(t&&!G(l.headers.host)){d.statusCode=403,d.setHeader("Content-Type","text/plain"),d.end("Lunora studio: refusing a request whose Host is not a loopback literal (DNS-rebinding guard).");return}if(g.startsWith(F)){if(!t){d.statusCode=403,d.setHeader("Content-Type","text/plain"),d.end("Lunora studio: the worker admin proxy is only available on a loopback bind.");return}fr(l,d,o);return}if(g===Ye){c(l,d,tr,"Lunora schema editing is only available on loopback hosts in dev.");return}if(g===Xe){c(l,d,nr,"Lunora policy scaffolding is only available on loopback hosts in dev.");return}if(g===Ze){c(l,d,sr,"Lunora data seeding is only available on loopback hosts in dev.");return}u(g,d)||a(d,p,"text/html; charset=utf-8")});return m.on("upgrade",(l,d,g)=>{if(!t||!G(l.headers.host)){d.destroy();return}M(l.url??"").startsWith(F)?wr(l,d,g,o):d.destroy()}),await new Promise((l,d)=>{m.once("error",g=>{g.code==="EADDRINUSE"?d(new Error(`studio port ${String(e.port)} is already in use — pass a different port with --port, or stop the process using it`,{cause:g})):d(g)}),m.listen(e.port,r,()=>{l()})}),{close:()=>new Promise(l=>{m.close(()=>{l()})}),url:`http://${r==="0.0.0.0"||r==="::"?"localhost":r}:${String(e.port)}`}},Sr=12e4,de=250,kr=1e4,yr=100,br=40,w=256*1024,$r="LUNORA_DEV_READY_TIMEOUT_MS",Er=new Set(["nuxt","sveltekit"]),O=e=>Er.has(ee(e).framework)?"framework-worker":Se(e).has("@lunora/vite")?"vite":"wrangler",ce=e=>{const r=se(e);let t;try{const o=K(k(e,"package.json"),"utf8");t=JSON.parse(o).scripts?.dev}catch{}if(t===void 0||t.trim()===""||t.includes("lunora")){const o=$(r,"vite",["dev"]);return{args:o.args,command:o.command}}return{args:["run","dev"],command:r}},E=e=>new Promise(r=>{setTimeout(r,e)}),Nr=async e=>{try{return await fetch(new URL("/_lunora/status",e),{signal:AbortSignal.timeout(1e3)}),!0}catch{return!1}},Lr=e=>{we(he(e.logPath),{recursive:!0});const r=z(e.logPath,"w",384);let t;const o=ie(e.command,e.args);try{t=J(o.command,o.args,{cwd:e.cwd,detached:!0,env:e.env,shell:o.shell,stdio:["ignore",r,r],windowsHide:!0})}finally{Y(r)}return t.unref(),{exited:new Promise(n=>{t.on("error",()=>{n(1)}),t.on("exit",(s,i)=>{n(s??(i?1:0))})}),pid:t.pid}},Rr=e=>{try{const{size:r}=fe(e);if(r<=w)return K(e,"utf8");const t=z(e,"r");try{const o=Buffer.alloc(w),n=ve(t,o,0,w,r-w),s=o.toString("utf8",0,n);return s.slice(s.indexOf(` `)+1)}finally{Y(t)}}catch{return}},le=(e,r)=>{const t=Rr(e);if(t===void 0)return[];const o=t.split(` `);return o.at(-1)===""&&o.pop(),r>0?o.slice(-r):o},ue=(e,r)=>r?.logFile??k(e,ye),pe=e=>{e.info(" Stop: lunora dev stop"),e.info(" Status: lunora dev status"),e.info(" Logs: lunora dev logs")},S=(e,r)=>{e.warn(`Dev server already running at ${r.url} (pid ${String(r.pid)})`),pe(e)},Ir=(e,r)=>{e.success(`Dev server running at ${r.url} (pid ${String(r.pid)})`),r.studioUrl!==void 0&&e.info(` Studio: ${r.studioUrl}`),pe(e)},Or=async e=>{const{child:r,cwd:t,deadline:o,pollIntervalMs:n,probe:s}=e,i=r.exited.then(a=>({exitCode:a}));for(;Date.now()<o;){const a=y(t);if(a!==void 0&&a.pid!==process.pid&&await s(a.url))return{state:a,status:"ready"};const c=await Promise.race([i,E(n)]);if(c!==void 0)return{...c,status:"exited"}}return{status:"timeout"}},Pr=async e=>{const{cwd:r,logger:t}=e,o=ue(r,void 0),n=Number(process.env[$r]??""),s=e.readyTimeoutMs??(Number.isFinite(n)&&n>0?n:Sr),i=(e.spawnDetached??Lr)({args:e.command.args,command:e.command.command,cwd:r,env:{...process.env,...e.env,[I]:"1",[re]:o,...process.env.FORCE_COLOR===void 0?{NO_COLOR:"1"}:{},...e.json?{LUNORA_LOG_JSON:"1"}:{}},logPath:o}),a=await Or({child:i,cwd:r,deadline:Date.now()+s,pollIntervalMs:e.pollIntervalMs??de,probe:e.probe??Nr});if(a.status==="ready")return Ir(t,a.state),{code:0};if(a.status==="exited"){t.error(`dev server exited before becoming ready (exit code ${String(a.exitCode)}). Last output:`);for(const u of le(o,br))t.error(` ${u}`);return f(r,i.pid??process.pid),{code:a.exitCode===0?1:a.exitCode}}i.pid!==void 0&&R(r)?.pid===process.pid&&oe(r,{logFile:o,pid:i.pid});const c=i.pid===void 0?"":` (pid ${String(i.pid)})`;return t.warn(`dev server did not confirm ready within ${String(Math.round(s/1e3))}s — it may still be compiling${c}. Check \`lunora dev status\` and \`lunora dev logs\`; \`lunora dev stop\` shuts it down.`),{code:1}},xr=(e,r)=>{const t=["dev"];return e.apiSpec!==void 0&&t.push("--api-spec",e.apiSpec),e.port!==void 0&&t.push("--port",String(e.port)),e.workerPort!==void 0&&t.push("--worker-port",String(e.workerPort)),e.codegen===!1&&t.push("--no-codegen"),e.studio===!1&&t.push("--no-studio"),r&&t.push("--remote"),t},Dr=async e=>{const{cwd:r,jsonLogs:t,logger:o,options:n,remote:s}=e,i=e.run??Pr,a=O(r),c=a==="wrangler"?`http://localhost:${String(n.workerPort??8787)}`:"http://localhost:5173",u=Q(r,{background:!0,mode:"cli",pid:process.pid,startedAt:new Date().toISOString(),url:c});if(!u.ok)return u.existing!==void 0&&S(o,u.existing),{code:0};const p={[h]:String(process.pid)};try{return a==="vite"?await i({command:ce(r),cwd:r,env:{...s?{LUNORA_REMOTE:"1"}:{},...p},json:t,logger:o}):await i({command:{args:[process.argv[1]??"lunora",...xr(n,s)],command:process.execPath},cwd:r,env:p,json:t,logger:o})}finally{f(r,process.pid)}},Tr=e=>{try{const r=N("ps",["-o","pgid=","-p",String(e)],{encoding:"utf8"}),t=Number.parseInt(r.stdout.trim(),10);return Number.isInteger(t)&&t>0?t:void 0}catch{return}},Cr=(e,r,t=process.platform,o=N)=>{if(t==="win32"){try{o("taskkill",["/pid",String(e.pid),"/T","/F"],{stdio:"ignore"})}catch{}return}if(e.background===!0)try{r(-(Tr(e.pid)??e.pid),"SIGKILL");return}catch{}try{r(e.pid,"SIGKILL")}catch{}},_r=(e,r)=>(r.json?v({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}),Ar=async e=>{const{cwd:r,logger:t}=e,o=R(r),n=e.alive??(m=>m===o?.pid&&ke(o)),s=e.signal??((m,l)=>{process.kill(m,l)}),i=e.pollIntervalMs??de,a=e.stopGraceMs??kr,c=e.platform??process.platform,u=e.spawnSyncImpl??N;if(o===void 0||!n(o.pid))return o!==void 0&&f(r,o.pid),e.json?v({running:!1,stopped:!1}):t.info("No dev server running."),{code:0};try{s(o.pid,"SIGTERM")}catch{}const p=Date.now()+a;for(;n(o.pid)&&Date.now()<p;)await E(i);return n(o.pid)&&(Cr(o,s,c,u),await E(i),n(o.pid))?_r(o,e):(f(r,o.pid),e.json?v({pid:o.pid,stopped:!0}):t.success(`Stopped dev server (pid ${String(o.pid)}).`),{code:0})},jr=e=>{const{cwd:r,logger:t}=e,o=y(r);if(o===void 0)return e.json?v({running:!1}):t.info("No dev server running."),{code:0};const n=e.now??Date.now,s=o.startedAt===void 0?Number.NaN:Date.parse(o.startedAt),i=Number.isFinite(s)?Math.max(0,Math.round((n()-s)/1e3)):void 0;if(e.json)return v({background:o.background===!0,logFile:o.logFile,mode:o.mode,pid:o.pid,running:!0,startedAt:o.startedAt,studioUrl:o.studioUrl,uptimeSeconds:i,url:o.url}),{code:0};const a=[`pid ${String(o.pid)}`,...i===void 0?[]:[`uptime ${String(i)}s`],o.background===!0?"background":"foreground"];return t.success(`Dev server running at ${o.url} (${a.join(", ")})`),o.studioUrl!==void 0&&t.info(` Studio: ${o.studioUrl}`),o.logFile!==void 0&&t.info(` Logs: lunora dev logs (${o.logFile})`),{code:0}},Fr=e=>{const{cwd:r,logger:t}=e,o=R(r),n=ue(r,o);if(!L(n))return t.info("No dev server logs found — output is captured when the server runs in background mode (`lunora dev --background`)."),{code:0};const s=le(n,e.lines??yr);return s.length>0&&process.stdout.write(`${s.join(` `)} `),{code:0}},Mr=e=>{const{cwd:r,json:t,lines:o,logger:n,subcommand:s}=e;if(s==="stop")return Ar({cwd:r,json:t,logger:n});if(s==="status")return jr({cwd:r,json:t,logger:n});if(s==="logs")return Fr({cwd:r,lines:o,logger:n});if(s!==void 0)return n.error(`dev: unknown subcommand "${s}" — expected stop | status | logs (or no subcommand to start the dev server)`),{code:1}},H="wrangler.dev.jsonc",U=6173,ge=8787,V=5173,Gr=5e3,Hr=(e,r)=>{const t=()=>{};if(!e.remote)return{args:[],plan:{bindings:[],cleanup:t,enabled:!1}};const o=(e.materializeRemote??Ae)({enabled:!0,projectRoot:r}),n=o.remoteBindings.map(i=>`${i.binding} (${i.kind})`),{cleanup:s}=o;return o.configPath===void 0?{args:[],plan:{bindings:n,cleanup:s,enabled:!0,reason:o.reason}}:{args:["--config",o.configPath],plan:{bindings:n,cleanup:s,enabled:!0}}},W=e=>ne(e).parsed?.dev?.ip,q=(e,r,t)=>{if(t!==void 0){const n=k(e,t);if(L(n)&&W(n)!==void 0)return[]}const o=te(e);return o!==void 0&&W(o)!==void 0?[]:r()?[]:["--ip","127.0.0.1"]},Ur=async(e,r)=>{if(e.workerPort!==void 0)return e.workerPort;const t=te(r);if(t!==void 0){const{parsed:o}=ne(t);if(typeof o?.dev?.port=="number")return o.dev.port}return(e.findFreePort??gr)(ge)},Vr=e=>{const r=e.cwd??process.cwd(),t=se(r),o=e.flavor??O(r);if(o==="vite"||o==="framework-worker"){const p=ce(r);let m;if(o==="framework-worker"){const l=q(r,e.hasIpv6Loopback??j,H),d=$(t,"wrangler",["dev","--config",H,...l,"--var","WORKER_ENV:development"]);m={args:d.args,command:d.command,cwd:r,tag:"worker"}}return{codegenEnabled:!1,flavor:o,ipv4LoopbackForced:!1,remote:{bindings:[],cleanup:()=>{},enabled:e.remote===!0},...m?{sidecar:m}:{},studioEnabled:!1,studioPort:e.port??U,workerOrigin:`http://localhost:${String(V)}`,workerPort:V,wrangler:{args:p.args,command:p.command,cwd:r,...e.remote===!0?{env:{LUNORA_REMOTE:"1"}}:{},tag:"vite"}}}const n=ee(r),s=n.framework==="none"?void 0:`this project uses ${n.framework} — the worker runs inside Vite there. run \`${He(t,"dev")}\` for the full app (frontend + HMR); \`lunora dev\` starts only the worker.`,i=e.workerPort??ge,a=Hr(e,r),c=q(r,e.hasIpv6Loopback??j),u=$(t,"wrangler",["dev","--port",String(i),...c,"--var","WORKER_ENV:development",...a.args]);return{codegenEnabled:e.codegen!==!1,flavor:o,frameworkHint:s,ipv4LoopbackForced:c.length>0,remote:a.plan,studioEnabled:e.studio!==!1,studioPort:e.port??U,workerOrigin:`http://localhost:${String(i)}`,workerPort:i,wrangler:{args:u.args,command:u.command,cwd:r,tag:"wrangler"}}},B=(e,r,t,o)=>{if(e.length===0)return;const n=je(e);if(n){o[n.level](`[lunora] ${n.text}`);return}const s=`[${r}] ${e}`;t==="stderr"?o.warn(s):o.info(s)},Wr=(e,r,t)=>{const o=(n,s)=>{if(!n)return;let i="";n.on("data",a=>{i+=a.toString("utf8");const c=i.split(` `);i=c.pop()??"";for(const u of c)B(u.trimEnd(),r,s,t)}),n.on("end",()=>{B(i.trimEnd(),r,s,t),i=""})};o(e.stdout,"stdout"),o(e.stderr,"stderr")},qr=(e,r)=>{const t=ie(e.command,e.args),o=J(t.command,t.args,{cwd:e.cwd??process.cwd(),env:e.env?{...process.env,...e.env}:process.env,shell:t.shell,stdio:["inherit","pipe","pipe"]});return Wr(o,e.tag,r),{exited:new Promise(n=>{o.on("error",s=>{r.error(`[${e.tag}] failed to start: ${s.message}`),n(1)}),o.on("exit",s=>{n(s??0)})}),kill:n=>{try{o.kill(n)}catch{}}}},Br=(e,r,t)=>{e.info(""),e.success("Lunora dev"),e.info(` ➜ Worker: ${r.workerOrigin}`),t!==void 0&&e.info(` ➜ Studio: ${t}`),r.codegenEnabled&&e.info(" ➜ Codegen: watching lunora/"),r.remote.enabled&&(r.remote.bindings.length>0?e.info(` ➜ Remote: ${r.remote.bindings.join(", ")} → deployed worker`):e.warn(` ➜ Remote: requested but inactive (${r.remote.reason??"no eligible bindings"}) — running fully local`)),e.info("")},Jr=(e,r)=>{Z(r).installed||!De()||(e.info(` ⓘ ${Te}`),e.info(""))},Kr=(e,r)=>{if(process.env.LUNORA_CONTAINER_LOGS==="0")return;const t=Ce(e,"lunora").containers.map(o=>({className:o.className,exportName:o.exportName}));if(t.length!==0)return _e({containers:t,onLine:o=>{const n=`[container:${o.name}] ${o.text}`;o.level==="error"?r.warn(n):r.info(n)},onUnavailable:o=>{r.warn(`[container] Docker engine unreachable — container logs unavailable (${o})`)}})},zr=async e=>{e.codegen?.close(),e.containerLogs?.close(),await e.studio?.close().catch(()=>{});try{e.remoteCleanup?.()}catch{}},Yr=async(e,r)=>{try{const t=await Ne({projectRoot:r}),o=Le(t),n=(e.ensureExample??Re)(r,o);n.length>0&&e.logger.info(`Updated .dev.vars.example with secrets for: ${o.join(", ")} (${n.join(", ")})`)}catch{}(await(e.ensureEnv??Ie)({confirm:ir(),cwd:r,info:t=>{e.logger.info(t)}})).status==="declined"&&!Oe()&&e.logger.info(`hint: ${T} was not scaffolded (non-interactive run). Copy ${Pe}${T} and fill in secrets, or run \`lunora dev\` in an interactive terminal to scaffold automatically.`);try{(e.fillSecrets??xe)({cwd:r,info:t=>{e.logger.info(t)}})}catch{}},Xr=(e,r,t,o)=>{if(e.flavor!=="wrangler")return;o!==void 0&&oe(r,{studioUrl:o});let n;try{n=Kr(r,t)}catch{}return Br(t,e,o),n},Zr=(e,r)=>{const t=Number(process.env[h]),o=Q(r,{background:process.env[I]==="1",logFile:process.env[re],mode:"cli",pid:process.pid,startedAt:new Date().toISOString(),url:e.workerOrigin},Number.isInteger(t)&&t>0?{supersedePid:t}:void 0);return o.ok?void 0:o.existing},Qr=async e=>{const r=e.cwd??process.cwd(),t=e.flavor??O(r),o=t==="wrangler"?await Ur(e,r):e.workerPort;return Vr({...e,cwd:r,flavor:t,workerPort:o})},eo=async(e,r,t)=>{let o=0,n;const s=p=>{e.kill(p),r?.kill(p)},i=()=>{o+=1,o===1?(t.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)"),s("SIGTERM"),n=setTimeout(()=>{s("SIGKILL")},Gr),n.unref()):s("SIGKILL")},a=()=>{s("SIGTERM")};process.on("SIGINT",i),process.on("SIGTERM",a);const c=[e.exited.then(p=>({code:p,who:"worker"}))];r!==void 0&&c.push(r.exited.then(p=>({code:p,who:"sidecar"})));const u=await Promise.race(c);return r!==void 0&&(u.who==="sidecar"?(t.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])),n&&clearTimeout(n),process.off("SIGINT",i),process.off("SIGTERM",a),u.code},ro=async(e,r,t,o)=>{if(r.studioEnabled)try{return await(e.startStudio??hr)({cwd:t,logger:{warnOnce:n=>{o.warn(n)}},port:r.studioPort,workerOrigin:r.workerOrigin})}catch(n){o.warn(`studio server failed to start (${n instanceof Error?n.message:String(n)}) — continuing without it`);return}},oo=(e,r,t,o)=>{if(e.sidecar!==void 0)try{X({apiSpec:r.apiSpec,lunoraDirectory:"lunora",projectRoot:t})}catch(n){o.warn(`codegen (pre-sidecar) failed: ${n instanceof Error?n.message:String(n)} — the framework dev server will retry`)}},to=async e=>{const r=await Qr(e),{logger:t}=e,o=r.wrangler.cwd??process.cwd(),n={remoteCleanup:r.remote.cleanup};try{const s=Number(process.env[h]),i=y(o);if(i!==void 0&&i.pid!==process.pid&&i.pid!==s)return S(t,i),{code:0,plan:r};const a=Zr(r,o);if(a!==void 0)return S(t,a),{code:0,plan:r};(r.flavor==="vite"||r.flavor==="framework-worker")&&(r.wrangler.env={...r.wrangler.env,[h]:String(process.pid)}),await Yr(e,o),t.info(r.flavor==="vite"?"starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)":"starting wrangler dev + studio"),r.ipv4LoopbackForced&&t.info("no IPv6 loopback (::1) on this host — binding the worker to 127.0.0.1 (--ip) so wrangler dev doesn't crash. Pin `dev.ip` in wrangler.jsonc to override."),r.codegenEnabled&&(n.codegen=(e.startCodegen??cr)({apiSpec:e.apiSpec,logger:t,projectRoot:o})),n.studio=await ro(e,r,o,t);const c=n.studio?.url;r.frameworkHint!==void 0&&t.warn(r.frameworkHint),oo(r,e,o,t);const u=e.startWorker??qr,p=u(r.wrangler,t),m=r.sidecar===void 0?void 0:u(r.sidecar,t);return n.containerLogs=Xr(r,o,t,c),Jr(t,o),{code:await eo(p,m,t),plan:r}}finally{f(o,process.pid),await zr(n)}},bo=Ge(async({argument:e,cwd:r,logger:t,options:o})=>{const n=o.json===!0,s=Mr({cwd:r,json:n,lines:o.lines,logger:t,subcommand:e[0]});if(s!==void 0)return s;const i=process.env[I]==="1",a=i?void 0:be(),c=n||a!==void 0;c&&We(),a!==void 0&&o.background!==!0&&t.info(`AI agent detected (${a.name} via ${a.variable}) — starting the dev server in background mode with JSON logs. Set LUNORA_AGENT_MODE=0 to opt out.`);const u=$e({configPreference:Ee(r),envValue:process.env.LUNORA_REMOTE,flag:o.remote});if(!i&&(o.background===!0||a!==void 0)){const p=y(r);return p!==void 0?(S(t,p),{code:0}):Dr({cwd:r,jsonLogs:c,logger:t,options:o,remote:u})}return to({apiSpec:Fe(o.apiSpec),codegen:o.codegen===!1?!1:void 0,cwd:r,logger:t,port:o.port,remote:u,studio:o.studio===!1?!1:void 0,workerPort:o.workerPort})});export{O as detectDevFlavor,bo as execute,Vr as planDevCommand,Hr as resolveRemotePlan,Ur as resolveWorkerPort,to as runDevCommand};