@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
8 lines (7 loc) • 27.7 kB
JavaScript
import{spawnSync as N,spawn as oe}from"node:child_process";import{existsSync as C,watch as Te,readFileSync as M,mkdirSync as _e,openSync as ie,closeSync as se,statSync as Ie,readSync as Le}from"node:fs";import{join as k,dirname as Re}from"node:path";import{runCodegen as ae}from"@lunora/codegen";import{runPostCodegenHook as Ae,detectAgentRules as ce,updateDevServerState as x,readDevServerState as R,claimDevServerState as de,DEV_HANDOFF_ENV as T,clearDevServerState as y,readLiveDevServerState as A,isDevServerReady as B,detectFramework as le,readProjectDependencyNames as Oe,DEV_LOG_FILE_ENV as ue,DEV_DAEMON_ENV as F,detectPackageManager as fe,execArgsFor as $,isRecordedProcessCurrent as De,DEV_LOG_FILE as $e,CODEGEN_ENV as ge,isCodegenDisabled as Ne,detectAiAgent as Ce,readProjectRemotePreference as Me,inferLunoraBindings as xe,packageNamesFromBindings as Fe,ensureDevVarsExample as je,ensureDevVariables as Ge,isInteractive as Ue,DEV_VARS_FILE as _,DEV_VARS_EXAMPLE_FILE as Ve,fillDevSecrets as We,DEV_BINDINGS_FILE as Be,DEV_STATE_FILE as ve,claimAgentRulesHint as He,AGENT_RULES_HINT as Ke,resolveDeployDriver as Ye,resolveProjectTarget as Je,runScriptCommand as Xe,discoverContainerInfo as ze,streamContainerLogs as Qe,parseDevVariableEntries as Ze,formatLunoraEvent as qe}from"@lunora/config";import{resolveRemoteEnabled as er,findWranglerFile as j,readWranglerJsonc as G,materializeRemoteWranglerConfig as rr}from"@lunora/config/cloudflare";import{p as tr,a as nr}from"../packem_shared/deploy-target-DCWwiuAe.mjs";import{w as or}from"../packem_shared/binding-manifest-file-CQ1eQYy1.mjs";import{a as ir}from"../packem_shared/codegen-error-AmH54ofi.mjs";import{a as sr}from"../packem_shared/platform-diagnostics-Cn2g6Jh-.mjs";import{d as ar}from"../packem_shared/command-Mmxzxyr5.mjs";import{EXIT_CODE as U}from"../packem_shared/EXIT_CODE-08cwt3MK.mjs";import{createServer as cr,connect as dr}from"node:net";import{forceJsonLogging as lr}from"../packem_shared/createLogger-Cl18I8AX.mjs";import{networkInterfaces as ur}from"node:os";import{spawnShellCompat as me}from"../packem_shared/createRecordingSpawner-sS7LEN7x.mjs";import{createServer as fr,request as gr}from"node:http";import{pipeline as vr}from"node:stream";import{loadStudioAssets as H,studioAssetsStamp as K,renderStudioHtml as mr,resolveAdminToken as pr,transportRejectionReason as Y,SCHEMA_EDIT_ENDPOINT as wr,POLICY_SCAFFOLD_ENDPOINT as hr,SEED_ENDPOINT as Sr,sendStudioDocument as yr,serveJsonHandler as Er,isStandaloneModulePath as kr,readStandaloneAsset as br,assetContentType as Pr,handleSchemaEditRequest as Tr,handlePolicyScaffoldRequest as _r,handleSeedRequest as Ir,applyStudioAssetCache as Lr}from"@lunora/config/studio-host";import{c as Rr}from"../packem_shared/tui-prompts-B3YwUhGw.mjs";import{setTimeout as I}from"node:timers/promises";import{provisionBindings as Ar}from"./runDeployCommand.mjs";import{a as E}from"../packem_shared/render-lunora-error--4tmM6mt.mjs";const Or=100,Dr=300,$r=/[/\\]/u,Nr=(e,r,n)=>{const{apiSpec:t,logger:o,projectRoot:i,target:s}=e;try{const a=ae({apiSpec:t,lunoraDirectory:r,projectRoot:i,target:s});o.success(`codegen: wrote ${r}/_generated (${n})`),sr(a.platformDiagnostics,o)}catch(a){return o.error(ir(a,n)),!1}return!0},Cr=e=>{const r=e.lunoraDirectory??"lunora",n=k(e.projectRoot,r),t=e.debounceMs??Or;let o=!1,i=!1,s=!1,a,d=0,u=Promise.resolve(),g;const m=new Promise(v=>{g=v}),p=v=>{o||(a=v,!i&&(i=!0,u=(async()=>{try{for(;a!==void 0;){const h=a;if(a=void 0,!!Nr(e,r,h)){s=!0;try{(await Ae({cwd:e.projectRoot,logger:e.logger,spawner:e.spawner,stdoutToStderr:e.jsonLogs})).ran&&(d=Date.now())}finally{s=!1}}}}catch(h){e.logger.error(`codegen watch: ${h instanceof Error?h.message:String(h)}`)}finally{i=!1,g()}})()))};p("startup");const l=v=>(e.logger.warn(`codegen watch unavailable (${v}) — schema edits will NOT auto-regenerate. Run \`lunora codegen\` manually after each edit.`),{close:()=>(o=!0,a=void 0,u),ready:m,watchAvailable:!1});let c,f;if(!C(n))return l(`no such directory: ${n}`);try{f=Te(n,{recursive:!0},(v,h)=>{if(!(typeof h=="string"&&h.split($r).includes("_generated"))){if(s||Date.now()-d<Dr){c&&(clearTimeout(c),c=void 0);return}c&&clearTimeout(c),c=setTimeout(()=>{p(`change: ${h??"?"}`)},t)}})}catch(v){return l(v instanceof Error?v.message:String(v))}const w=f;return{close:()=>(o=!0,a=void 0,c&&clearTimeout(c),w.close(),u),ready:m,watchAvailable:!0}},Mr=64,pe="127.0.0.1",xr=65535,Fr=(e,r=pe)=>new Promise(n=>{const t=cr();t.once("error",()=>{n(!1)}),t.once("listening",()=>{t.close(()=>{n(!0)})}),t.listen(e,r)}),jr=async(e,r=pe,n=Mr)=>{for(let t=e;t<e+n&&t<=xr;t+=1)if(await Fr(t,r))return t;return e},J=(e=ur)=>Object.values(e()).some(r=>r?.some(n=>n.internal&&n.address==="::1")),X="/_lunora",z=e=>{const r=e.indexOf("?");return r===-1?e:e.slice(0,r)},Q=3e4,Z=(e,r,n)=>{const t=gr({headers:{...e.headers,host:n.host},hostname:n.hostname,method:e.method,path:e.url,port:n.port},o=>{r.writeHead(o.statusCode??502,o.headers),vr(o,r,()=>{})});t.setTimeout(Q,()=>{t.destroy(new Error(`no response within ${String(Q)}ms`))}),t.on("error",o=>{if(r.headersSent){r.destroy();return}r.statusCode=o.message.startsWith("no response within")?504:502,r.end(`lunora dev: worker unreachable (${o.message})`)}),r.on("close",()=>{r.writableFinished||t.destroy()}),e.pipe(t)},Gr=5e3,Ur=(e,r,n,t)=>{const o=dr({host:t.hostname,port:Number(t.port)},()=>{o.setTimeout(0);const i=[`${e.method??"GET"} ${e.url??"/"} HTTP/1.1`];for(const[s,a]of Object.entries(e.headers))if(s!=="host")for(const d of Array.isArray(a)?a:[a])d!==void 0&&i.push(`${s}: ${d}`);i.push(`host: ${t.host}`,"",""),o.write(i.join(`\r
`)),n.length>0&&o.write(n),o.pipe(r),r.pipe(o)});o.setTimeout(Gr,()=>o.destroy()),o.on("error",()=>r.destroy()),r.on("error",()=>o.destroy())},Vr=async e=>{const r=e.host??"127.0.0.1",n=r==="127.0.0.1"||r==="localhost"||r==="::1",t=new URL(e.workerOrigin);let o=H(e.logger,import.meta.url),i=K(import.meta.url);const s=mr({adminToken:n?pr(e.cwd):void 0,basePath:"/",dataEditable:n,rulesInstalled:ce(e.cwd).installed,runAsIdentity:n,schemaEditable:n,scriptSrc:"/studio.js",styleHref:"/styles.css"}),a=(l,c,f,w,v)=>{Lr(l,c,v,i)||(c.statusCode=200,c.setHeader("Content-Type",w),c.end(f))},d=(l,c,f)=>{l.statusCode=c,l.setHeader("Content-Type","text/plain"),l.end(f)},u=(l,c,f,w)=>{if(n){Er(l,c,f,e.cwd,{apiSpec:e.apiSpec});return}d(c,403,w)},g=(l,c,f)=>{const w=l==="/styles.css",v=kr(l);if(!w&&!v)return!1;const h=K(import.meta.url);if(h!==i&&(o=H(e.logger,import.meta.url),i=h),o===void 0)return d(f,501,"Lunora studio assets not found — install and build @lunora/studio."),!0;if(w)return a(c,f,o.styles,"text/css; charset=utf-8","styles.css"),!0;const b=l.slice(l.lastIndexOf("/")+1),W=br(b,import.meta.url);return W===void 0?(d(f,404,"Not found"),!0):(a(c,f,W,Pr(b),b),!0)},m=Buffer.from(s),p=fr((l,c)=>{const f=z(l.url??"/");if(n){const v=Y(l,e.logger);if(v!==void 0){d(c,403,v);return}}if(f.startsWith(X)){if(!n){d(c,403,"Lunora studio: the worker admin proxy is only available on a loopback bind.");return}Z(l,c,t);return}if(f===wr){u(l,c,Tr,"Lunora schema editing is only available on loopback hosts in dev.");return}if(f===hr){u(l,c,_r,"Lunora policy scaffolding is only available on loopback hosts in dev.");return}if(f===Sr){u(l,c,Ir,"Lunora data seeding is only available on loopback hosts in dev.");return}if(g(f,l,c))return;const w=l.headers["sec-fetch-mode"];if(n&&typeof w=="string"&&w!=="navigate"){Z(l,c,t);return}yr(c,m)});return p.on("upgrade",(l,c,f)=>{if(!n||Y(l,e.logger)!==void 0){c.destroy();return}z(l.url??"").startsWith(X)?Ur(l,c,f,t):c.destroy()}),await new Promise((l,c)=>{p.once("error",f=>{f.code==="EADDRINUSE"?c(new Error(`studio port ${String(e.port)} is already in use — pass a different port with --port, or stop the process using it`,{cause:f})):c(f)}),p.listen(e.port,r,()=>{l()})}),{close:()=>new Promise(l=>{p.close(()=>{l()})}),url:`http://${r==="0.0.0.0"||r==="::"?"localhost":r}:${String(e.port)}`}},Wr=12e4,V=250,Br=1e3,Hr="LUNORA_DEV_READY_TIMEOUT_MS",we=e=>{if(e!==void 0)return e;const r=Number(process.env[Hr]??"");return Number.isFinite(r)&&r>0?r:Wr},he=async(e,r)=>{try{const n=AbortSignal.timeout(Br);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}},Kr=async e=>{const{cwd:r,logger:n,origin:t,signal:o}=e,i=e.probe??he,s=we(e.readyTimeoutMs),a=Date.now()+s,d=()=>o.aborted;for(;!d()&&Date.now()<a;){let u=!1;try{u=await i(t,o)}catch{}if(d()||Date.now()>=a)break;if(u)return x(r,{readyAt:new Date().toISOString()},{expectedPid:process.pid}),R(r)?.readyAt!==void 0;try{await I(V,void 0,{signal:o})}catch{}}return d()||n.warn(`server did not answer on ${t} within ${String(Math.round(s/1e3))}s — .lunora/dev.json will have no readyAt, so anything polling for readiness will keep waiting. The server itself is unaffected; raise LUNORA_DEV_READY_TIMEOUT_MS if it is just slow to boot.`),!1},Yr=1e4,Jr=100,Xr=40,P=256*1024,zr=new Set(["nuxt","sveltekit"]),O=e=>zr.has(le(e).framework)?"framework-worker":Oe(e).has("@lunora/vite")?"vite":"wrangler",Se=e=>{const r=fe(e);let n;try{const t=M(k(e,"package.json"),"utf8");n=JSON.parse(t).scripts?.dev}catch{}if(n===void 0||n.trim()===""||n.includes("lunora")){const t=$(r,"vite",["dev"]);return{args:t.args,command:t.command}}return{args:["run","dev"],command:r}},Qr=e=>{_e(Re(e.logPath),{recursive:!0});const r=ie(e.logPath,"w",384);let n;const t=me(e.command,e.args);try{n=oe(t.command,t.args,{cwd:e.cwd,detached:!0,env:e.env,shell:t.shell,stdio:["ignore",r,r],windowsHide:!0})}finally{se(r)}return n.unref(),{exited:new Promise(o=>{n.on("error",()=>{o(1)}),n.on("exit",(i,s)=>{o(i??(s?1:0))})}),pid:n.pid}},Zr=e=>{try{const{size:r}=Ie(e);if(r<=P)return M(e,"utf8");const n=ie(e,"r");try{const t=Buffer.alloc(P),o=Le(n,t,0,P,r-P),i=t.toString("utf8",0,o);return i.slice(i.indexOf(`
`)+1)}finally{se(n)}}catch{return}},ye=(e,r)=>{const n=Zr(e);if(n===void 0)return[];const t=n.split(`
`);return t.at(-1)===""&&t.pop(),r>0?t.slice(-r):t},Ee=(e,r)=>r?.logFile??k(e,$e),ke=e=>{e.info(" Stop: lunora dev stop"),e.info(" Status: lunora dev status"),e.info(" Logs: lunora dev logs")},L=(e,r)=>{e.warn(`Dev server already running at ${r.url} (pid ${String(r.pid)})`),ke(e)},qr=(e,r)=>{e.success(`Dev server running at ${r.url} (pid ${String(r.pid)})`),r.studioUrl!==void 0&&e.info(` Studio: ${r.studioUrl}`),ke(e)},et=async e=>{const{child:r,cwd:n,deadline:t,pollIntervalMs:o,probe:i}=e,s=r.exited.then(a=>({exitCode:a}));for(;Date.now()<t;){const a=A(n);if(a!==void 0&&a.pid!==process.pid&&await i(a.url))return{state:a,status:"ready"};const d=await Promise.race([s,I(o)]);if(d!==void 0)return{...d,status:"exited"}}return{status:"timeout"}},rt=async e=>{const{cwd:r,logger:n}=e,t=Ee(r,void 0),o=we(e.readyTimeoutMs),i=(e.spawnDetached??Qr)({args:e.command.args,command:e.command.command,cwd:r,env:{...process.env,...e.env,[F]:"1",[ue]:t,...process.env.FORCE_COLOR===void 0?{NO_COLOR:"1"}:{},...e.json?{LUNORA_LOG_JSON:"1"}:{}},logPath:t}),s=await et({child:i,cwd:r,deadline:Date.now()+o,pollIntervalMs:e.pollIntervalMs??V,probe:e.probe??he});if(s.status==="ready")return qr(n,s.state),{code:0};if(s.status==="exited"){n.error(`dev server exited before becoming ready (exit code ${String(s.exitCode)}). Last output:`);for(const d of ye(t,Xr))n.error(` ${d}`);return y(r,i.pid??process.pid),{code:s.exitCode===0?1:s.exitCode}}i.pid!==void 0&&R(r)?.pid===process.pid&&x(r,{logFile:t,pid:i.pid});const a=i.pid===void 0?"":` (pid ${String(i.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:U.UNAVAILABLE}},be=e=>e.codegen!==!1&&!Ne(process.env[ge]),Pe=e=>{const r={...e.remote===!0?{LUNORA_REMOTE:"1"}:{},...e.codegen===!1?{[ge]:"0"}:{}};return Object.keys(r).length>0?{env:r}:{}},tt=(e,r)=>{const n=["dev"];return 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),r&&n.push("--remote"),n},nt=async e=>{const{cwd:r,jsonLogs:n,logger:t,options:o,remote:i}=e,s=e.run??rt,a=O(r),d=a==="wrangler"?`http://localhost:${String(o.workerPort??8787)}`:"http://localhost:5173",u=de(r,{background:!0,mode:"cli",pid:process.pid,startedAt:new Date().toISOString(),url:d});if(!u.ok)return u.existing!==void 0&&L(t,u.existing),{code:0};const g={[T]:String(process.pid)};try{return a==="vite"?await s({command:Se(r),cwd:r,env:{...Pe({...o,remote:i}).env,...g},json:n,logger:t}):await s({command:{args:[process.argv[1]??"lunora",...tt(o,i)],command:process.execPath},cwd:r,env:g,json:n,logger:t})}finally{y(r,process.pid)}},ot=e=>{try{const r=N("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}},it=(e,r,n=process.platform,t=N)=>{if(n==="win32"){try{t("taskkill",["/pid",String(e.pid),"/T","/F"],{stdio:"ignore"})}catch{}return}if(e.background===!0)try{r(-(ot(e.pid)??e.pid),"SIGKILL");return}catch{}try{r(e.pid,"SIGKILL")}catch{}},st=(e,r)=>(r.json?E({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}),at=async e=>{const{cwd:r,logger:n}=e,t=R(r),o=e.alive??(m=>m===t?.pid&&De(t)),i=e.signal??((m,p)=>{process.kill(m,p)}),s=e.pollIntervalMs??V,a=e.stopGraceMs??Yr,d=e.platform??process.platform,u=e.spawnSyncImpl??N;if(t===void 0||!o(t.pid))return t!==void 0&&y(r,t.pid),e.json?E({running:!1,stopped:!1}):n.info("No dev server running."),{code:0};try{i(t.pid,"SIGTERM")}catch{}const g=Date.now()+a;for(;o(t.pid)&&Date.now()<g;)await I(s);return o(t.pid)&&(it(t,i,d,u),await I(s),o(t.pid))?st(t,e):(y(r,t.pid),e.json?E({pid:t.pid,stopped:!0}):n.success(`Stopped dev server (pid ${String(t.pid)}).`),{code:0})},ct=e=>{const{cwd:r,logger:n}=e,t=A(r);if(t===void 0)return e.json?E({running:!1}):n.info("No dev server running."),{code:0};const o=e.now??Date.now,i=t.startedAt===void 0?Number.NaN:Date.parse(t.startedAt),s=Number.isFinite(i)?Math.max(0,Math.round((o()-i)/1e3)):void 0;if(e.json)return E({background:t.background===!0,logFile:t.logFile,mode:t.mode,pid:t.pid,ready:B(t),readyAt:t.readyAt,running:!0,startedAt:t.startedAt,studioUrl:t.studioUrl,uptimeSeconds:s,url:t.url}),{code:0};const a=[`pid ${String(t.pid)}`,...s===void 0?[]:[`uptime ${String(s)}s`],t.background===!0?"background":"foreground",B(t)?"ready":"starting"];return n.success(`Dev server running at ${t.url} (${a.join(", ")})`),t.studioUrl!==void 0&&n.info(` Studio: ${t.studioUrl}`),t.logFile!==void 0&&n.info(` Logs: lunora dev logs (${t.logFile})`),{code:0}},dt=e=>{const{cwd:r,logger:n}=e,t=R(r),o=Ee(r,t);if(!C(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 i=ye(o,e.lines??Jr);return i.length>0&&process.stdout.write(`${i.join(`
`)}
`),{code:0}},lt=e=>{const{cwd:r,json:n,lines:t,logger:o,subcommand:i}=e;if(i==="stop")return at({cwd:r,json:n,logger:o});if(i==="status")return ct({cwd:r,json:n,logger:o});if(i==="logs")return dt({cwd:r,lines:t,logger:o});if(i!==void 0)return o.error(`dev: unknown subcommand "${i}" — expected stop | status | logs (or no subcommand to start the dev server)`),{code:U.USAGE}},D="wrangler.dev.jsonc",q=6173,S=8787,ee=5173,ut=5e3,ft=(e,r)=>{const n=()=>{};if(!e.remote)return{args:[],plan:{bindings:[],cleanup:n,enabled:!1}};const o=(e.materializeRemote??rr)({enabled:!0,projectRoot:r}),i=o.remoteBindings.map(a=>`${a.binding} (${a.kind})`),{cleanup:s}=o;return o.configPath===void 0?{args:[],plan:{bindings:i,cleanup:s,enabled:!0,reason:o.reason}}:{args:["--config",o.configPath],plan:{bindings:i,cleanup:s,enabled:!0}}},re=e=>G(e).parsed?.dev?.ip,te=(e,r,n)=>{if(n!==void 0){const o=k(e,n);if(C(o)&&re(o)!==void 0)return[]}const t=j(e);return t!==void 0&&re(t)!==void 0?[]:r()?[]:["--ip","127.0.0.1"]},gt=new Set(["127.0.0.1","::1","[::1]","localhost"]),vt=(e,r)=>{let n;try{n=M(k(e,_),"utf8")}catch{return[]}const t=[];for(const o of Ze(n)){let i;try{i=new URL(o.value)}catch{continue}gt.has(i.hostname)&&i.port===String(r)&&t.push(o.key)}return t},mt=async(e,r)=>{if(e.workerPort!==void 0)return e.workerPort;const n=j(r);if(n!==void 0){const{parsed:i}=G(n);if(typeof i?.dev?.port=="number")return i.dev.port}const t=await(e.findFreePort??jr)(S);if(t===S)return t;const o=vt(r,S);if(o.length>0)throw new Error(`port ${String(S)} is in use, but ${_} pins the worker origin to it (${o.join(", ")}). Serving on ${String(t)} would leave those URLs pointing at nothing. Stop whatever holds ${String(S)}, or run \`lunora dev --worker-port ${String(t)}\` and update those values to match.`);return t},pt=(e,r)=>{if(e.inspectorPort!==void 0)return e.inspectorPort;const n=j(r);if(n===void 0)return;const{parsed:t}=G(n),o=t?.dev?.inspector_port;return typeof o=="number"?o:void 0},wt=e=>{const r=e.cwd??process.cwd(),n=fe(r),t=e.flavor??O(r);if(t==="vite"||t==="framework-worker"){const m=Se(r);let p;if(t==="framework-worker"){const l=te(r,e.hasIpv6Loopback??J,D),c=Ye(Je(r)).toolchain?.dev({configPath:D,extraArgs:[...l,"--var","WORKER_ENV:development"]}),f=$(n,c?.tool??"wrangler",c?.args??[]);p={args:f.args,command:f.command,cwd:r,tag:"worker"}}return e.worker===!1&&e.logger.warn(`--no-worker does not apply to the ${t} flavor: Vite owns the worker, codegen and studio in-process. Run your framework's dev script instead.`),e.inspectorPort!==void 0&&e.logger.warn(t==="framework-worker"?`--inspector-port does not apply to the ${t} flavor: pin \`dev.inspector_port\` in ${D} — that is the config the worker sidecar runs.`:`--inspector-port does not apply to the ${t} flavor: Vite owns the worker. Pin it in vite.config — \`lunora({ cloudflare: { inspectorPort: ${String(e.inspectorPort)} } })\`.`),{runsCodegenWatch:!1,flavor:t,ipv4LoopbackForced:!1,remote:{bindings:[],cleanup:()=>{},enabled:e.remote===!0},...p?{sidecar:p}:{},studioEnabled:!1,studioPort:e.port??q,workerEnabled:!0,workerOrigin:`http://localhost:${String(ee)}`,workerPort:ee,wrangler:{args:m.args,command:m.command,cwd:r,...Pe(e),tag:"vite"}}}const o=le(r),i=o.framework==="none"?void 0:`this project uses ${o.framework} — the worker runs inside Vite there. run \`${Xe(n,"dev")}\` for the full app (frontend + HMR); \`lunora dev\` starts only the worker.`,s=e.workerPort??S,a=ft(e,r),d=te(r,e.hasIpv6Loopback??J),u=e.inspectorPort===void 0?[]:["--inspector-port",String(e.inspectorPort)],g=$(n,"wrangler",["dev","--port",String(s),...u,...d,"--var","WORKER_ENV:development",...a.args]);return{runsCodegenWatch:be(e),flavor:t,frameworkHint:i,ipv4LoopbackForced:d.length>0,remote:a.plan,studioEnabled:e.studio!==!1,studioPort:e.port??q,workerEnabled:e.worker!==!1,workerOrigin:`http://localhost:${String(s)}`,workerPort:s,wrangler:{args:g.args,command:g.command,cwd:r,tag:"wrangler"}}},ne=(e,r,n,t)=>{if(e.length===0)return;const o=qe(e);if(o){t[o.level](`[lunora] ${o.text}`);return}const i=`[${r}] ${e}`;n==="stderr"?t.warn(i):t.info(i)},ht=(e,r,n)=>{const t=(o,i)=>{if(!o)return;let s="";o.on("data",a=>{s+=a.toString("utf8");const d=s.split(`
`);s=d.pop()??"";for(const u of d)ne(u.trimEnd(),r,i,n)}),o.on("end",()=>{ne(s.trimEnd(),r,i,n),s=""})};t(e.stdout,"stdout"),t(e.stderr,"stderr")},St=(e,r)=>{const n=me(e.command,e.args),t=oe(n.command,n.args,{cwd:e.cwd??process.cwd(),env:e.env?{...process.env,...e.env}:process.env,shell:n.shell,stdio:["inherit","pipe","pipe"]});return ht(t,e.tag,r),{exited:new Promise(o=>{t.on("error",i=>{r.error(`[${e.tag}] failed to start: ${i.message}`),o(1)}),t.on("exit",(i,s)=>{o(i??(s?1:0))})}),kill:o=>{try{t.kill(o)}catch{}}}},yt=(e,r,n,t)=>{e.info(""),e.success("Lunora dev"),e.info(` ➜ Worker: ${r.workerOrigin}`),n!==void 0&&e.info(` ➜ Studio: ${n}`),r.runsCodegenWatch&&e.info(" ➜ Codegen: watching lunora/"),t!==void 0&&e.info(` ➜ Supervisor: ${t} (needs) · ${ve} (status)`),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("")},Et=(e,r)=>{ce(r).installed||!He()||(e.info(` ⓘ ${Ke}`),e.info(""))},kt=(e,r)=>{if(process.env.LUNORA_CONTAINER_LOGS==="0")return;const t=ze(e,"lunora").containers.map(o=>({className:o.className,exportName:o.exportName}));if(t.length!==0)return Qe({containers:t,onLine:o=>{const i=`[container:${o.name}] ${o.text}`;o.level==="error"?r.warn(i):r.info(i)},onUnavailable:o=>{r.warn(`[container] Docker engine unreachable — container logs unavailable (${o})`)}})},bt=async e=>{e.readyProbe?.abort(),await e.codegen?.close().catch(()=>{}),e.containerLogs?.close(),await e.studio?.close().catch(()=>{});try{e.remoteCleanup?.()}catch{}},Pt=async(e,r)=>{try{const t=await xe({projectRoot:r}),o=Fe(t),i=(e.ensureExample??je)(r,o);i.length>0&&e.logger.info(`Updated .dev.vars.example with secrets for: ${o.join(", ")} (${i.join(", ")})`)}catch{}(await(e.ensureEnv??Ge)({confirm:Rr(),cwd:r,info:t=>{e.logger.info(t)}})).status==="declined"&&!Ue()&&e.logger.info(`hint: ${_} was not scaffolded (non-interactive run). Copy ${Ve} → ${_} and fill in secrets, or run \`lunora dev\` in an interactive terminal to scaffold automatically.`);try{(e.fillSecrets??We)({cwd:r,info:t=>{e.logger.info(t)}})}catch{}},Tt=(e,r,n,t,o)=>{if(e.flavor!=="wrangler")return;t!==void 0&&x(r,{studioUrl:t});let i;try{i=kt(r,n)}catch{}return yt(n,e,t,o),i},_t=(e,r)=>{const n=Number(process.env[T]),t=de(r,{background:process.env[F]==="1",logFile:process.env[ue],mode:"cli",pid:process.pid,startedAt:new Date().toISOString(),url:e.workerOrigin},Number.isInteger(n)&&n>0?{supersedePid:n}:void 0);return t.ok?void 0:t.existing},It=e=>{const{cwd:r,destination:n,logger:t,plan:o}=e,i=n!==void 0,s=n??Be,a=or({destination:s,dev:{...o.flavor==="wrangler"?{origin:o.workerOrigin}:{},statusFile:ve},logger:i?t:{...t,success:()=>{},warn:()=>{}},projectRoot:r});return a.error!==void 0?i?a:(t.debug?.(`skipped the default binding manifest: ${a.error}`),{}):{written:s}},Lt=async e=>{const r=e.cwd??process.cwd(),n=e.flavor??O(r),t=n==="wrangler"?await mt(e,r):e.workerPort,o=n==="wrangler"?pt(e,r):e.inspectorPort;return wt({...e,cwd:r,flavor:n,inspectorPort:o,workerPort:t})},Rt=async e=>await new Promise(r=>{const n={},t=s=>{e.info(`received ${s} — shutting down`),n.sigint&&process.off("SIGINT",n.sigint),n.sigterm&&process.off("SIGTERM",n.sigterm),r(0)},o=()=>{t("SIGINT")},i=()=>{t("SIGTERM")};n.sigint=o,n.sigterm=i,process.on("SIGINT",o),process.on("SIGTERM",i)}),At=async(e,r,n)=>{let t=0,o;const i=u=>{e.kill(u),r?.kill(u)},s=()=>{t+=1,t===1?(n.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)"),i("SIGTERM"),o=setTimeout(()=>{i("SIGKILL")},ut),o.unref()):i("SIGKILL")},a=()=>{i("SIGTERM")};process.on("SIGINT",s),process.on("SIGTERM",a);const d=await Promise.race([e.exited.then(u=>({code:u,who:"worker"})),...r===void 0?[]:[r.exited.then(u=>({code:u,who:"sidecar"}))]]);return r!==void 0&&(d.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",s),process.off("SIGTERM",a),d.code},Ot=async(e,r,n,t)=>{if(r.studioEnabled)try{return await(e.startStudio??Vr)({apiSpec:e.apiSpec,cwd:n,logger:{warnOnce:o=>{t.warn(o)}},port:r.studioPort,workerOrigin:r.workerOrigin})}catch(o){t.warn(`studio server failed to start (${o instanceof Error?o.message:String(o)}) — continuing without it`);return}},Dt=e=>{const r=[e.runsCodegenWatch?"codegen watch":void 0,e.studioEnabled?"studio":void 0].filter(t=>t!==void 0);return`--no-worker: not starting wrangler. ${r.length>0?`${r.join(" + ")} running`:"nothing else to run"}; your task runner owns the worker on ${e.workerOrigin}.`},$t=(e,r,n,t,o)=>{if(!(e.sidecar===void 0||!be(r)))try{ae({apiSpec:r.apiSpec,lunoraDirectory:"lunora",projectRoot:n,target:o})}catch(i){t.warn(`codegen (pre-sidecar) failed: ${i instanceof Error?i.message:String(i)} — the framework dev server will retry`)}},Nt=async e=>{const{logger:r}=e,n=e.cwd??process.cwd(),t=e.flavor??O(n),o=nr(n,e.target);if(o.target===void 0)throw new Error(o.error??"unknown deploy target");const{target:i}=o;t==="wrangler"&&await Ar(n,r,void 0,i,void 0);const s=await Lt({...e,flavor:t}),a={remoteCleanup:s.remote.cleanup};try{const d=Number(process.env[T]),u=A(n);if(u!==void 0&&u.pid!==process.pid&&u.pid!==d)return L(r,u),{code:0,plan:s};const g=_t(s,n);if(g!==void 0)return L(r,g),{code:0,plan:s};(s.flavor==="vite"||s.flavor==="framework-worker")&&(s.wrangler.env={...s.wrangler.env,[T]:String(process.pid)}),await Pt(e,n),r.info(s.flavor==="vite"?"starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)":"starting wrangler dev + studio"),s.ipv4LoopbackForced&&r.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."),s.runsCodegenWatch&&(a.codegen=(e.startCodegen??Cr)({apiSpec:e.apiSpec,jsonLogs:e.jsonLogs,logger:r,projectRoot:n,target:i})),a.studio=await Ot(e,s,n,r);const m=It({cwd:n,destination:e.emitBindings,logger:r,plan:s});if(m.error!==void 0)return r.error(m.error),{code:U.USAGE,plan:s};await a.codegen?.ready;const p=a.studio?.url;s.frameworkHint!==void 0&&r.warn(s.frameworkHint);const l=()=>{s.flavor==="wrangler"&&(a.readyProbe=new AbortController,Kr({cwd:n,logger:r,origin:s.workerOrigin,probe:e.probeReady,signal:a.readyProbe.signal}))};if(!s.workerEnabled)return l(),r.info(Dt(s)),{code:await(e.waitForInterrupt??Rt)(r),plan:s};$t(s,e,n,r,i);const c=e.startWorker??St,f=c(s.wrangler,r),w=s.sidecar===void 0?void 0:c(s.sidecar,r);return l(),a.containerLogs=Tt(s,n,r,p,m.written),Et(r,n),{code:await At(f,w,r),plan:s}}finally{a.readyProbe?.abort(),y(n,process.pid),await bt(a)}},Ct=e=>({codegen:e.codegen===!1?!1:void 0,studio:e.studio===!1?!1:void 0,worker:e.worker===!1?!1:void 0}),sn=ar(async({argument:e,cwd:r,logger:n,options:t})=>{const o=t.json===!0,i=lt({cwd:r,json:o,lines:t.lines,logger:n,subcommand:e[0]});if(i!==void 0)return i;const s=process.env[F]==="1",a=s?void 0:Ce(),d=o||a!==void 0;d&&lr(),a!==void 0&&t.background!==!0&&n.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=er({configPreference:Me(r),envValue:process.env.LUNORA_REMOTE,flag:t.remote});if(!s&&(t.background===!0||a!==void 0)){const g=A(r);return g!==void 0?(L(n,g),{code:0}):nt({cwd:r,jsonLogs:d,logger:n,options:t,remote:u})}return Nt({apiSpec:tr(t.apiSpec),cwd:r,emitBindings:t.emitBindings,inspectorPort:t.inspectorPort,jsonLogs:d,logger:n,port:t.port,remote:u,target:t.target,workerPort:t.workerPort,...Ct(t)})});export{St as defaultWorkerSpawner,O as detectDevFlavor,sn as execute,Ct as negatableDevFlags,wt as planDevCommand,pt as resolveInspectorPort,mt as resolveWorkerPort,Nt as runDevCommand};