@shogo-ai/worker 1.13.8 → 1.13.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +29 -28
- package/package.json +2 -1
- package/src/lib/resource-limits.ts +340 -0
- package/src/lib/runtime-manager.ts +134 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
import{createRequire as
|
|
1
|
+
import{createRequire as U$}from"node:module";var w$=Object.defineProperty;var L$=($)=>$;function C$($,X){this[$]=L$.bind(null,X)}var A$=($,X)=>{for(var Y in X)w$($,Y,{get:X[Y],enumerable:!0,configurable:!0,set:C$.bind(X,Y)})};var N$=($,X)=>()=>($&&(X=$($=0)),X);var T0=U$(import.meta.url);var C2={};A$(C2,{runGit:()=>_,isGitRepo:()=>X0,gitIsAvailable:()=>E0,gitFetchUnshallow:()=>k0,gitFetchAndReset:()=>$0,commitAndPush:()=>s$,cloneProject:()=>b0,buildGitUrl:()=>t});import{spawn as a$,execFile as l$}from"node:child_process";import{existsSync as R0}from"node:fs";import{join as P0}from"node:path";import{promisify as r$}from"node:util";async function E0($=!1){if(s!==null&&!$)return s;try{await e$("git",["--version"],{timeout:3000}),s=!0}catch{s=!1}return s}function t($,X){return`${$.replace(/\/+$/,"")}/api/projects/${X}/git`}function _($,X={}){let{cwd:Y,env:Z,timeoutMs:z=300000,logger:K}=X;return new Promise((J,V)=>{let Q=a$("git",$,{cwd:Y,env:{...process.env,...Z},stdio:["ignore","pipe","pipe"]}),q="",H="",W=[],O=[];Q.stdout.setEncoding("utf-8"),Q.stderr.setEncoding("utf-8"),Q.stdout.on("data",(C)=>{if(W.push(C),K)K.log(C.trimEnd())}),Q.stderr.on("data",(C)=>{if(O.push(C),K)K.warn(C.trimEnd())});let k=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}V(Error(`git ${$[0]} timed out after ${z}ms`))},z);Q.on("error",(C)=>{clearTimeout(k),V(C)}),Q.on("close",(C)=>{clearTimeout(k),q=W.join(""),H=O.join("");let N=C??-1;if(N===0)J({stdout:q,stderr:H,exitCode:N});else{let G=Error(`git ${$[0]} exited with code ${N}: ${H.slice(0,500)}`);G.exitCode=N,G.stdout=q,G.stderr=H,V(G)}})})}async function b0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,shallow:K=!0,logger:J,timeoutMs:V}=$;if(R0(P0(z,".git")))throw Error(`cloneProject: ${z}/.git already exists; use gitFetchAndReset instead`);let Q=t(X,Z),q=["-c",`http.extraHeader=Authorization: Bearer ${Y}`,"clone"];if(K)q.push("--depth=1");return q.push(Q,z),await _(q,{logger:J,timeoutMs:V}),{commitSha:(await _(["rev-parse","HEAD"],{cwd:z,logger:J,timeoutMs:1e4})).stdout.trim()}}async function $0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,branch:K="HEAD",logger:J,timeoutMs:V}=$,Q=t(X,Z),q=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];return await _([...q,"fetch",Q,K],{cwd:z,logger:J,timeoutMs:V}),await _(["reset","--hard","FETCH_HEAD"],{cwd:z,logger:J,timeoutMs:V}),{commitSha:(await _(["rev-parse","HEAD"],{cwd:z,logger:J,timeoutMs:1e4})).stdout.trim()}}async function k0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,logger:K,timeoutMs:J}=$,V=t(X,Z),Q=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];if(R0(P0(z,".git","shallow")))await _([...Q,"fetch","--unshallow",V],{cwd:z,logger:K,timeoutMs:J})}async function s$($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,message:K,branch:J="HEAD",authorEmail:V,authorName:Q,logger:q,timeoutMs:H}=$,W=t(X,Z),O={};if(V)O.GIT_AUTHOR_EMAIL=V,O.GIT_COMMITTER_EMAIL=V;if(Q)O.GIT_AUTHOR_NAME=Q,O.GIT_COMMITTER_NAME=Q;await _(["add","-A"],{cwd:z,env:O,logger:q,timeoutMs:H});let k=!1;try{await _(["diff","--cached","--quiet"],{cwd:z,env:O,logger:q,timeoutMs:H})}catch{k=!0}if(!k)return{committed:!1};await _(["commit","-m",K,"--no-verify"],{cwd:z,env:O,logger:q,timeoutMs:H});let N=(await _(["rev-parse","HEAD"],{cwd:z,env:O,logger:q,timeoutMs:1e4})).stdout.trim(),G=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];return await _([...G,"push",W,J],{cwd:z,env:O,logger:q,timeoutMs:H}),{committed:!0,commitSha:N}}function X0($){return R0(P0($,".git"))}var e$,s=null;var w0=N$(()=>{e$=r$(l$)});import{Command as m1}from"commander";import G$ from"picocolors";import w from"picocolors";import{existsSync as x6}from"node:fs";import{readFileSync as F$,writeFileSync as T$,existsSync as _$,chmodSync as y$}from"node:fs";import{hostname as D$}from"node:os";import{homedir as B$}from"node:os";import{join as D}from"node:path";import{mkdirSync as V0}from"node:fs";var h=D(B$(),".shogo"),j=D(h,"config.json"),i1=D(h,"credentials.json"),o=D(h,"device-id"),a=D(h,"worker.pid"),_0=D(h,"logs"),q0=D(_0,"worker.log"),Q0=D(_0,"worker.err.log"),l=D(h,"runtime"),R=D(l,process.platform==="win32"?"agent-runtime.exe":"agent-runtime"),r=D(l,"version.json"),H0=D(h,"projects");function g($,X=H0){return D(X,$)}function m(){V0(h,{recursive:!0,mode:448}),V0(_0,{recursive:!0,mode:448})}function e0(){m(),V0(l,{recursive:!0,mode:448})}function s0($=H0){m(),V0($,{recursive:!0,mode:448})}var y0={cloudUrl:"https://studio.shogo.ai",port:8002,projectsDir:H0};function I(){if(!_$(j))return{};try{return JSON.parse(F$(j,"utf-8"))}catch($){throw Error(`Corrupt config at ${j}: ${$.message}`)}}function e($){m(),T$(j,JSON.stringify($,null,2),{mode:384}),y$(j,384)}function t0($,X){return{...$,...Object.fromEntries(Object.entries(X).filter(([,Y])=>Y!==void 0))}}function P($={}){let X=I(),Y={apiKey:process.env.SHOGO_API_KEY,cloudUrl:process.env.SHOGO_CLOUD_URL,name:process.env.SHOGO_INSTANCE_NAME,workerDir:process.env.SHOGO_WORKER_DIR,port:process.env.PORT?parseInt(process.env.PORT,10):void 0,projectsDir:process.env.SHOGO_PROJECTS_DIR},Z=t0(t0(X,Y),$);if(!Z.apiKey)throw Error("No API key. Run `shogo login` or pass --api-key / set SHOGO_API_KEY.");return{apiKey:Z.apiKey,cloudUrl:Z.cloudUrl??y0.cloudUrl,name:Z.name??D$(),workerDir:Z.workerDir??process.cwd(),port:Z.port??y0.port,projectsDir:Z.projectsDir??y0.projectsDir}}import{spawn as M$}from"node:child_process";import{existsSync as x$,openSync as $2,readFileSync as S$,unlinkSync as R$,writeFileSync as P$}from"node:fs";function W0(){if(!x$(a))return null;let $=S$(a,"utf-8").trim(),X=parseInt($,10);return Number.isFinite(X)?X:null}function O0($){try{return process.kill($,0),!0}catch{return!1}}function D0(){try{R$(a)}catch{}}function X2($){m();let X=W0();if(X&&O0(X))throw Error(`Worker already running (pid=${X}). Run \`shogo worker stop\` first.`);if(X)D0();let Y=$.inheritStdio?["ignore","inherit","inherit"]:["ignore",$2(q0,"a"),$2(Q0,"a")],Z=M$($.runner,[$.entry],{cwd:$.cwd,env:$.env,detached:!!$.detach,stdio:Y});if(!Z.pid)throw Error("Failed to spawn worker process.");if(P$(a,String(Z.pid),{mode:384}),$.detach)Z.unref();return{pid:Z.pid,child:Z}}function Y2($="SIGTERM"){let X=W0();if(!X)return{killedPid:null};if(!O0(X))return D0(),{killedPid:null};try{process.kill(X,$)}catch{}return D0(),{killedPid:X}}import M from"picocolors";import{existsSync as b$}from"node:fs";import{request as E$}from"node:http";function z2($,X=process.env){let Y=(K)=>K&&K.trim().length>0?K.trim():void 0,Z=Y($);if(Z)return{url:Z2(Z),source:"flag"};let z=[["HTTPS_PROXY",X.HTTPS_PROXY],["https_proxy",X.https_proxy],["HTTP_PROXY",X.HTTP_PROXY],["http_proxy",X.http_proxy]];for(let[K,J]of z){let V=Y(J);if(V)return{url:Z2(V),source:K}}return null}function Z2($){if(/^https?:\/\//i.test($))return $;return`http://${$}`}function M0($,X){if(!X)return $;return{...$,HTTPS_PROXY:$.HTTPS_PROXY??X.url,https_proxy:$.https_proxy??X.url,HTTP_PROXY:$.HTTP_PROXY??X.url,http_proxy:$.http_proxy??X.url}}async function K2($,X="api.shogo.ai",Y=5000){return new Promise((Z)=>{let z=!1,K=(J)=>{if(z)return;z=!0,Z(J)};try{let J=new URL($.url),V=J.hostname,Q=J.port?parseInt(J.port,10):J.protocol==="https:"?443:80,q=E$({method:"CONNECT",host:V,port:Q,path:`${X}:443`,timeout:Y});q.on("connect",(H)=>{q.destroy();let W=H.statusCode??0;if(W===200)K({ok:!0,detail:`CONNECT ${X}:443 → 200 (via ${V}:${Q})`});else if(W===407)K({ok:!1,detail:`407 Proxy Authentication Required — check credentials in ${$.source}`});else K({ok:!1,detail:`CONNECT → HTTP ${W}`})}),q.on("timeout",()=>{q.destroy(),K({ok:!1,detail:`timeout after ${Y}ms`})}),q.on("error",(H)=>{let W=H.code?` (${H.code})`:"";K({ok:!1,detail:`${H.message}${W}`})}),q.end()}catch(J){K({ok:!1,detail:J?.message??"unknown error"})}})}function J2($){let X;try{X=new URL($)}catch{return[]}let Y=X.host,Z=X.protocol.replace(":","")||"https",z=X.hostname.split("."),K=z.length>=2?z.slice(-2).join("."):X.hostname;return[{url:`${Z}://${Y}`,host:Y,purpose:"control",criticality:"fatal"},{url:`${Z}://api-direct.${K}`,host:`api-direct.${K}`,purpose:"tunnel-direct",criticality:"graceful"},{url:`${Z}://artifacts.${K}`,host:`artifacts.${K}`,purpose:"artifacts",criticality:"graceful"}]}var v$=5000;async function h$($,X){try{let Y=new AbortController,Z=setTimeout(()=>Y.abort(),X),z=await fetch(`${$.replace(/\/$/,"")}/health`,{signal:Y.signal}).catch(()=>null);if(clearTimeout(Z),!z)return{ok:!1,detail:"no response (firewall? DNS?)"};return{ok:!0,detail:`HTTP ${z.status}`}}catch(Y){return{ok:!1,detail:Y?.message??"unknown error"}}}var V2=($)=>{let X=[{name:"Runtime (node >= 20)",criticality:"fatal",async run(){let[Z]=process.versions.node.split(".").map(Number);return(Z??0)>=20?{ok:!0,detail:`node v${process.versions.node}`}:{ok:!1,detail:`node v${process.versions.node} — need >=20`}}},{name:"Worker directory exists",criticality:"fatal",async run(){return b$($.workerDir)?{ok:!0,detail:$.workerDir}:{ok:!1,detail:`${$.workerDir} does not exist`}}}];if($.proxy)X.push({name:`Proxy reachable (${j$($.proxy.url)})`,criticality:"fatal",async run(){return K2($.proxy)}});let Y=J2($.cloudUrl);for(let Z of Y)X.push({name:`Reach ${Z.host}${Z.purpose!=="control"?M.dim(` (${Z.purpose})`):""}`,criticality:Z.criticality,run:()=>h$(Z.url,v$)});return X.push({name:"API key valid",criticality:"fatal",async run(){try{let Z=await fetch(`${$.cloudUrl}/api/instances/heartbeat`,{method:"POST",headers:{"content-type":"application/json","x-api-key":$.apiKey},body:JSON.stringify({hostname:"preflight",name:"preflight",os:"preflight",arch:"preflight",metadata:{preflight:!0}})});if(Z.status===401||Z.status===403)return{ok:!1,detail:`HTTP ${Z.status} — key rejected`};return Z.ok?{ok:!0,detail:`HTTP ${Z.status}`}:{ok:!1,detail:`HTTP ${Z.status}`}}catch(Z){return{ok:!1,detail:Z?.message??"unknown error"}}}}),X};function j$($){try{return new URL($).host}catch{return $}}async function q2($){console.log(M.bold(`
|
|
2
2
|
Shogo Worker — Preflight
|
|
3
|
-
`));let X=!1,Y=0;for(let Z of $){process.stdout.write(` ${
|
|
3
|
+
`));let X=!1,Y=0;for(let Z of $){process.stdout.write(` ${M.dim("...")} ${Z.name}`);let z=await Z.run();if(process.stdout.write("\r"),z.ok)console.log(` ${M.green("✓")} ${Z.name}${z.detail?M.dim(` — ${z.detail}`):""}`);else if(Z.criticality==="graceful")console.log(` ${M.yellow("◦")} ${Z.name}${z.detail?M.dim(` — ${z.detail}`):""}${M.yellow(" (optional — worker will still start)")}`),Y++;else console.log(` ${M.red("✗")} ${Z.name}${z.detail?M.dim(` — ${z.detail}`):""}`),X=!0}if(!X&&Y===0)console.log(M.green(`
|
|
4
4
|
All checks passed.
|
|
5
|
-
`));else if(!X)console.log(
|
|
5
|
+
`));else if(!X)console.log(M.yellow(`
|
|
6
6
|
Starting with ${Y} optional host(s) blocked. See docs/my-machines-networking.md.
|
|
7
|
-
`));else console.log(
|
|
7
|
+
`));else console.log(M.red(`
|
|
8
8
|
Preflight failed — fix blocking issues before starting.
|
|
9
|
-
`));return!X}import{accessSync as
|
|
10
|
-
`)}import{spawn as
|
|
11
|
-
`)}class
|
|
12
|
-
`))if(Q)this.log.log(
|
|
13
|
-
`))if(Q)this.log.error(`${w} ${Q}`)});try{return await this.waitForHealth($,J6),$.status="running",$.lastUsedAt=Date.now(),this.armIdleTimer($),this.armGraceTimer($),$}catch(q){$.status="error",$.lastError=q?.message??String(q),this.killProcessGroup($,"SIGTERM");try{V.kill("SIGTERM")}catch{}throw this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,q}}killProcessGroup($,X){if(!$.pid)return;if(process.platform==="win32"){try{o$("taskkill",["/F","/T","/PID",String($.pid)],{stdio:"ignore",windowsHide:!0})}catch{}return}try{process.kill(-$.pid,X)}catch{}}armGraceTimer($){if($.graceTimer)clearTimeout($.graceTimer),$.graceTimer=null;$.graceTimer=setTimeout(()=>{if($.graceTimer=null,$.consecutiveFailures>0)$.consecutiveFailures=0},z6);try{$.graceTimer.unref?.()}catch{}}buildEnv($,X){let Y=$.spawnConfig,Z={...this.opts.env??process.env,PROJECT_ID:$.projectId,PORT:String($.agentPort),API_SERVER_PORT:String($.apiServerPort),SKILL_SERVER_PORT:String($.apiServerPort),WORKSPACE_API_PORT_BASE:String($.agentPort+t$),NODE_ENV:"production",SHOGO_CLOUD_URL:Y.cloudUrl,SHOGO_API_URL:Y.cloudUrl,SHOGO_API_KEY:Y.apiKey,RUNTIME_AUTH_SECRET:k2($.projectId),WEBHOOK_TOKEN:Q6($.projectId)};if(Y.projectDir)Z.PROJECT_DIR=Y.projectDir,Z.WORKSPACE_DIR=Y.projectDir;if(this.shouldDeferToCloudSync($.projectId))Z.SHOGO_CLOUD_SYNC="1";if(Y.aiProxyUrl)Z.AI_PROXY_URL=Y.aiProxyUrl;if(Y.aiProxyToken)Z.AI_PROXY_TOKEN=Y.aiProxyToken;if(Y.techStackId)Z.TECH_STACK_ID=Y.techStackId;if(Y.name)Z.AGENT_NAME=Y.name;if(Y.workspaceId)Z.WORKSPACE_ID=Y.workspaceId;if(!Z.TREE_SITTER_WASM_DIR)Z.TREE_SITTER_WASM_DIR=Y0(s$(X),"tree-sitter-wasm");if(Y.extraEnv)Object.assign(Z,Y.extraEnv);return Z}resolveCwd($){let X=$.spawnConfig;if(X.projectDir&&D0(X.projectDir))return X.projectDir;let Y=this.opts.runtimeWorkDir??Y0(e$(),"shogo-runtime",$.projectId);return q2(Y,{recursive:!0}),Y}handleExit($,X,Y){let Z=Y===null&&X===0;if(this.log.log(`[WorkerRuntimeManager] runtime ${$.projectId} exited (code=${X}, signal=${Y})`),$.proc=null,$.graceTimer)clearTimeout($.graceTimer),$.graceTimer=null;if($.status==="stopping"||this.stopped){$.status="stopped",$.pid=null,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0;return}if(Z){$.status="stopped",$.pid=null,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,this.runtimes.delete($.projectId);return}this.killProcessGroup($,"SIGKILL"),$.pid=null;let z=Date.now(),K=z-$.lastFailureAt<=H2;if($.consecutiveFailures=K?$.consecutiveFailures+1:1,$.lastFailureAt=z,$.restarts+=1,$.lastError=`exited code=${X} signal=${Y}`,$.consecutiveFailures>=Q2){if($.status="failed",$.lastError=`Circuit breaker tripped: ${$.consecutiveFailures} consecutive non-clean exits within ${Math.round(H2/1000)}s (last: code=${X} signal=${Y}). Most recent on macOS is jetsam OOM (signal=SIGKILL with code=null); the previous incarnation's vite/tsserver/preview-manager children were reaped to prevent further RSS growth. Stop, fix the workspace, and call resetFailure(projectId) (or stop(projectId)) to allow another spawn attempt.`,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,$.restartTimer)clearTimeout($.restartTimer),$.restartTimer=null;if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;this.log.error(`[WorkerRuntimeManager] ${$.lastError}`);return}let J=this.restartBackoffMs($.restarts);if($.status="restarting",this.log.warn(`[WorkerRuntimeManager] restarting ${$.projectId} in ${Math.round(J/1000)}s (restart #${$.restarts}, consecutive failures ${$.consecutiveFailures}/${Q2})`),$.restartTimer)clearTimeout($.restartTimer);$.restartTimer=setTimeout(()=>{if($.restartTimer=null,$.status==="failed"||this.stopped)return;$.startPromise=this.doStart($).then((V)=>{return $.startPromise=null,V}).catch((V)=>{return $.startPromise=null,this.log.error(`[WorkerRuntimeManager] restart of ${$.projectId} failed: ${V?.message??V}`),$})},J);try{$.restartTimer.unref?.()}catch{}}restartBackoffMs($){let X=Math.min(Y6*Math.pow(2,Math.max(0,$-1)),Z6),Y=X*0.2*Math.random();return X+Y}armIdleTimer($){if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;let X=this.opts.idleMs??$6;if(!Number.isFinite(X)||X<=0)return;$.idleTimer=setTimeout(()=>{let Y=Date.now()-$.lastUsedAt;if(Y<X){this.armIdleTimer($);return}this.log.log(`[WorkerRuntimeManager] idle-evicting ${$.projectId} after ${Math.round(Y/1000)}s`),this.stop($.projectId).catch((Z)=>{this.log.warn(`[WorkerRuntimeManager] idle stop failed: ${Z?.message??Z}`)})},X)}async allocatePort(){let $=b0-P0,X=Math.min($,50);for(let Y=0;Y<X;Y++){let Z=P0+Math.floor(Math.random()*$);if(Z+O0-1>b0)continue;let z=!0;for(let V=0;V<O0;V++)if(this.usedPorts.has(Z+V)){z=!1;break}if(!z)continue;let K=await this.isPortListening(Z),J=await this.isPortListening(Z+w2);if(K||J)continue;for(let V=0;V<O0;V++)this.usedPorts.add(Z+V);return Z}throw Error(`Cannot allocate port in range ${P0}-${b0} after ${X} attempts`)}releasePort($){if(!$)return;for(let X=0;X<O0;X++)this.usedPorts.delete($+X)}async isPortListening($){let X=new AbortController,Y=setTimeout(()=>X.abort(),250);try{return await fetch(`http://127.0.0.1:${$}/`,{method:"HEAD",signal:X.signal}),clearTimeout(Y),!0}catch{return clearTimeout(Y),!1}}tcpProbe($){return new Promise((X)=>{let Y=!1,Z=(K)=>{if(Y)return;Y=!0;try{z.destroy()}catch{}X(K)},z=r$({host:"127.0.0.1",port:$});z.setTimeout(q6),z.once("connect",()=>Z(!0)),z.once("error",()=>Z(!1)),z.once("timeout",()=>Z(!1))})}async waitForHealth($,X){let{agentPort:Y,proc:Z}=$;if(!Z)throw Error(`waitForHealth: slot ${$.projectId} has no spawned process`);let z=Date.now(),K=z+X,J=null,V=!1,w=0,q=0,Q=0,H=0,W=z;while(Date.now()<K){if(H++,Z.exitCode!==null||Z.signalCode!=null||Z.killed)throw Error(`agent-runtime exited (code=${Z.exitCode}, signal=${Z.signalCode}) before becoming healthy on port ${Y}`);q++;let G=new AbortController,L=setTimeout(()=>G.abort(),1500);try{let k=await fetch(`http://127.0.0.1:${Y}/health`,{method:"GET",signal:G.signal});if(clearTimeout(L),k.ok){this.log.log(`[WorkerRuntimeManager] /health ready for ${$.projectId} on port ${Y} (HTTP ${k.status} after ${Date.now()-z}ms, ${H} iter, ${q} http)`);return}J=`HTTP /health returned ${k.status}`}catch(k){clearTimeout(L);let _=k?.name??"Error",p=k?.code??k?.cause?.code;J=`HTTP /health failed: ${_}${p?`(${p})`:""}: ${k?.message??k}`}if(Q++,V=await this.tcpProbe(Y),V){w=Date.now();let k=Date.now()-$.lastStdoutAt;if(k>=W2)throw Error(`agent-runtime wedged on port ${Y}: TCP listening but stdout silent for ${k}ms (> ${W2}ms window); ${q} /health attempts, last error: ${J??"n/a"}`)}let A=Date.now();if(A-W>=V6){let k=A-z,_=A-$.lastStdoutAt,p=w>0?A-w:null;this.log.log(`[WorkerRuntimeManager] still waiting for /health on ${$.projectId} port ${Y} (${(k/1000).toFixed(1)}s elapsed, tcpListening=${V}${p!=null?`(${p}ms ago)`:""}, lastStdout=${_}ms ago, ${q} http, ${Q} tcp, lastError=${J??"n/a"})`),W=A}await new Promise((k)=>setTimeout(k,K6))}if(Z.exitCode!==null||Z.signalCode!=null||Z.killed)throw Error(`agent-runtime exited (code=${Z.exitCode}, signal=${Z.signalCode}) before becoming healthy on port ${Y}`);throw Error(`Timeout waiting for agent-runtime /health on port ${Y} after ${H} iter (httpAttempts=${q}, tcpAttempts=${Q}, tcpListening=${V}, lastError=${J??"n/a"})`)}async waitForExit($,X){if($.exitCode!==null||$.signalCode!=null||$.killed)return;await new Promise((Y)=>{let Z=setTimeout(()=>{try{$.kill("SIGKILL")}catch{}Y()},X);$.once("exit",()=>{clearTimeout(Z),Y()})})}snapshot($){return{projectId:$.projectId,status:$.status,agentPort:$.agentPort||void 0,apiServerPort:$.apiServerPort||void 0,pid:$.proc?.pid,startedAt:$.startedAt||void 0,lastUsedAt:$.lastUsedAt,restarts:$.restarts,lastError:$.lastError}}}import{hostname as h0,platform as j0,arch as I0}from"node:os";function d(){if(typeof __SHOGO_WORKER_VERSION__==="string"&&__SHOGO_WORKER_VERSION__.length>0)return`shogo-cli/${__SHOGO_WORKER_VERSION__}`;return"shogo-cli/unknown"}class N2 extends Error{code="TUNNEL_WS_HEADERS_UNSUPPORTED";constructor(){super("Tunnel WebSocket auth requires a runtime with WebSocket header support (Bun or Node >= 21). Current runtime does not support WebSocket constructor headers.");this.name="TunnelWebSocketHeaderSupportError"}}var Z0=60,G0=300,u0=3,H6=u0,W6=1800000,k6=25000,B2=1000,A2=60000,U2=3;class f0{opts;log;pollTimer=null;ws=null;wsIdleTimer=null;heartbeatTimer=null;stopped=!1;currentPollInterval=Z0;wsReconnectAttempt=0;lastHeartbeatError=null;consecutiveAuthFailures=0;consecutiveAuthSuccesses=0;serverPublishedWsUrl=null;activeAbortControllers=new Map;constructor($){this.opts=$,this.log=$.logger??console}start(){if(!this.opts.apiKey){this.log.log("[WorkerTunnel] No API key set, skipping tunnel");return}this.stopped=!1,this.wsReconnectAttempt=0,this.currentPollInterval=Z0,this.log.log("[WorkerTunnel] Starting heartbeat polling to Shogo Cloud..."),this.heartbeatLoop()}stop(){if(this.stopped=!0,this.pollTimer)clearTimeout(this.pollTimer),this.pollTimer=null;this.cleanupWs(),this.log.log("[WorkerTunnel] Tunnel stopped")}isConnected(){if(this.ws!==null&&this.ws.readyState===WebSocket.OPEN)return!0;return!this.stopped&&!!this.opts.apiKey&&this.lastHeartbeatError===null&&this.pollTimer!==null}getCloudUrl(){return this.opts.cloudUrl.replace(/\/$/,"")}getWsBaseUrl(){let $=(this.opts.wsUrlOverride||process.env.SHOGO_TUNNEL_WS_URL||"").trim();if($)return $.replace(/\/$/,"");if(this.serverPublishedWsUrl)return this.serverPublishedWsUrl.replace(/\/$/,"");return this.getCloudUrl().replace(/^http/,"ws")}buildWsUrl(){return`${this.getWsBaseUrl()}/api/instances/ws`}supportsWebSocketConstructorHeaders($=globalThis){if(typeof $.Bun<"u"||typeof $.process?.versions?.bun==="string")return!0;let X=$.process?.versions?.node;if(X){if(parseInt(X.split(".")[0]??"0",10)>=21)return!0}return!1}createTunnelWebSocket($,X,Y=globalThis){if(!this.supportsWebSocketConstructorHeaders(Y))throw new N2;return new WebSocket($,X)}getReconnectDelayMs(){let $=Math.min(B2*Math.pow(2,this.wsReconnectAttempt),A2),X=$*0.2*Math.random();return $+X}async collectMetadata(){let $={hostname:h0(),os:j0(),arch:I0(),uptime:process.uptime(),protocolVersion:U2,tunnelStatus:this.ws?.readyState===WebSocket.OPEN?"connected":"polling",kind:this.opts.kind??"cli-worker"};try{let X=this.opts.resolver.getActiveProjects();$.activeProjects=X.length,$.projects=X.map((Y)=>{let Z=this.opts.resolver.status(Y);return{projectId:Y,status:Z?.status??"unknown",agentPort:Z?.agentPort}})}catch{$.activeProjects=0}return $}async sendHeartbeat(){let $=this.getCloudUrl(),X=await this.collectMetadata(),Y=h0(),Z=this.opts.name??Y,z=await fetch(`${$}/api/instances/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":d(),"x-api-key":this.opts.apiKey,"x-shogo-kind":this.opts.kind??"cli-worker"},body:JSON.stringify({hostname:Y,name:Z,os:j0(),arch:I0(),kind:this.opts.kind??"cli-worker",metadata:X})});if(!z.ok)throw Error(`Heartbeat failed: HTTP ${z.status}`);let K=await z.json();if(typeof K.wsUrl==="string"&&K.wsUrl.length>0){if(K.wsUrl!==this.serverPublishedWsUrl)this.log.log(`[WorkerTunnel] Cloud advertised tunnel WS URL: ${K.wsUrl}`);this.serverPublishedWsUrl=K.wsUrl}return K}scheduleNextPoll($){if(this.stopped)return;if(this.pollTimer)clearTimeout(this.pollTimer);let X=($??this.currentPollInterval)*1000;this.pollTimer=setTimeout(()=>void this.heartbeatLoop(),X)}async heartbeatLoop(){if(this.stopped)return;if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.scheduleNextPoll(this.currentPollInterval);return}try{let $=await this.sendHeartbeat(),X=$.nextPollIn||Z0;if(this.consecutiveAuthFailures>=u0){if(this.consecutiveAuthSuccesses++,this.consecutiveAuthSuccesses<H6){this.currentPollInterval=G0,this.scheduleNextPoll();return}}if(this.currentPollInterval=X,this.lastHeartbeatError)this.log.log("[WorkerTunnel] Heartbeat recovered"),this.lastHeartbeatError=null;if(this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0,$.wsRequested&&!this.ws){this.log.log("[WorkerTunnel] Cloud requested WebSocket — connecting..."),this.connectWs();return}}catch($){let X=$?.message??String($);if(/HTTP 40[13]\b/.test(X))this.consecutiveAuthFailures++,this.consecutiveAuthSuccesses=0;else this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0;if(X!==this.lastHeartbeatError)this.log.error(`[WorkerTunnel] Heartbeat error: ${X}`),this.lastHeartbeatError=X;if(this.consecutiveAuthFailures>=u0){let Z=`tunnel saw ${this.consecutiveAuthFailures} consecutive auth failures from Shogo Cloud`;try{this.opts.onAuthRevoked?.(Z)}catch(z){this.log.warn(`[WorkerTunnel] onAuthRevoked threw: ${z?.message??z}`)}if(this.currentPollInterval!==G0)this.log.warn(`[WorkerTunnel] ${this.consecutiveAuthFailures} consecutive auth failures — `+`backing off to ${G0}s. Re-authenticate with \`shogo login\`.`);this.currentPollInterval=G0}else this.currentPollInterval=Z0}this.scheduleNextPoll()}async handleRequest($){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;this.resetWsIdleTimer();let X=new AbortController;this.activeAbortControllers.set($.requestId,X);try{let Y=await this.resolveLocalUrl($.path,$.projectId);if(!Y){let J=this.opts.resolver.describeRejection?this.opts.resolver.describeRejection($.path,$.projectId):{code:"NO_LOCAL_RUNTIME",message:`no local runtime available for path: ${$.path}`};this.sendFrame({type:"response",requestId:$.requestId,status:502,headers:{"content-type":"application/json"},body:JSON.stringify({code:J.code,message:J.message,path:$.path})});return}let Z={...$.headers??{}};if($.projectId&&($.path.startsWith("/agent/")||$.path==="/agent")){let J=this.opts.resolver.deriveRuntimeToken($.projectId);if(J)Z["x-runtime-token"]=J}let z={method:$.method,headers:Z,signal:X.signal};if($.body&&$.method!=="GET"&&$.method!=="HEAD")z.body=$.body;let K=await fetch(Y,z);if($.stream){let J=K.body?.getReader();if(!J){this.sendFrame({type:"stream-error",requestId:$.requestId,error:"No response body for stream"});return}let V=new TextDecoder;try{while(!0){let{done:w,value:q}=await J.read();if(w)break;if(this.ws?.readyState!==WebSocket.OPEN)break;this.sendFrame({type:"stream-chunk",requestId:$.requestId,data:V.decode(q,{stream:!0})})}if(this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-end",requestId:$.requestId})}catch(w){if(w?.name!=="AbortError"&&this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-error",requestId:$.requestId,error:w?.message??String(w)})}}else{let J=await K.text(),V={};K.headers.forEach((w,q)=>{V[q]=w}),this.sendFrame({type:"response",requestId:$.requestId,status:K.status,headers:V,body:J})}}catch(Y){if(Y?.name==="AbortError")return;if(this.ws?.readyState!==WebSocket.OPEN)return;let Z=$.stream?{type:"stream-error",requestId:$.requestId,error:Y?.message??String(Y)}:{type:"response",requestId:$.requestId,status:502,body:JSON.stringify({error:Y?.message??String(Y)})};this.sendFrame(Z)}finally{this.activeAbortControllers.delete($.requestId)}}async resolveLocalUrl($,X){return this.opts.resolver.resolveLocalUrl($,X)}sendFrame($){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{this.ws.send(JSON.stringify($))}catch(X){this.log.warn(`[WorkerTunnel] Frame send failed: ${X?.message??X}`)}}resetWsIdleTimer(){if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer);this.wsIdleTimer=setTimeout(()=>{if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.log.log("[WorkerTunnel] WebSocket idle timeout — closing, returning to polling");try{this.ws.close(1000,"Idle timeout")}catch{}}},W6)}startWsHeartbeat(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.heartbeatTimer=setInterval(async()=>{if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{let $=await this.collectMetadata();this.sendFrame({type:"heartbeat",metadata:$})}catch{}},k6)}connectWs(){if(this.stopped||this.ws)return;let $=this.buildWsUrl(),X=h0(),Y=j0(),Z=I0(),z=this.opts.name??X;this.log.log(`[WorkerTunnel] Opening WebSocket to ${$} (hostname=${X})`);let K={headers:{Authorization:`Bearer ${this.opts.apiKey}`,"User-Agent":d(),"x-shogo-hostname":X,"x-shogo-name":z,"x-shogo-os":Y,"x-shogo-arch":Z,"x-shogo-kind":this.opts.kind??"cli-worker"}},J;try{J=this.createTunnelWebSocket($,K)}catch(V){this.log.error(`[WorkerTunnel] WebSocket creation failed: ${V?.message??V}`),this.ws=null,this.scheduleNextPoll(5);return}this.ws=J,J.onopen=()=>{this.log.log("[WorkerTunnel] WebSocket connected — session active"),this.wsReconnectAttempt=0,this.startWsHeartbeat(),this.resetWsIdleTimer()},J.onmessage=(V)=>{let w;try{let q=typeof V.data==="string"?V.data:V.data.toString();w=JSON.parse(q)}catch{return}if(w.type==="ping"){this.sendFrame({type:"pong"}),this.resetWsIdleTimer();return}if(w.type==="cancel"){let q=this.activeAbortControllers.get(w.requestId);if(q)q.abort();return}if(w.type==="request"){this.handleRequest(w).catch((q)=>{this.log.error(`[WorkerTunnel] Error handling request: ${q?.message??q}`)});return}},J.onclose=(V)=>{if(this.log.log(`[WorkerTunnel] WebSocket closed: code=${V.code} reason=${V.reason||"none"}`),this.cleanupWs(),this.stopped)return;if(V.code===1000||V.code===4000)this.scheduleNextPoll(this.currentPollInterval);else{this.wsReconnectAttempt++;let w=this.getReconnectDelayMs();this.log.log(`[WorkerTunnel] Reconnecting in ${Math.round(w/1000)}s (attempt ${this.wsReconnectAttempt})`),this.scheduleNextPoll(Math.ceil(w/1000))}},J.onerror=(V)=>{this.log.error("[WorkerTunnel] WebSocket error:",V?.message??"unknown")}}cleanupWs(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer),this.wsIdleTimer=null;for(let[,$]of this.activeAbortControllers)try{$.abort()}catch{}this.activeAbortControllers.clear(),this.ws=null}_testing(){let $=this;return{sendHeartbeat:()=>$.sendHeartbeat(),heartbeatLoop:()=>$.heartbeatLoop(),connectWs:()=>$.connectWs(),cleanupWs:()=>$.cleanupWs(),handleRequest:(X)=>$.handleRequest(X),installFakeWs:(X)=>{$.ws=X},getCloudUrl:()=>$.getCloudUrl(),getWsBaseUrl:()=>$.getWsBaseUrl(),buildWsUrl:()=>$.buildWsUrl(),getReconnectDelayMs:()=>$.getReconnectDelayMs(),supportsWebSocketConstructorHeaders:(X)=>$.supportsWebSocketConstructorHeaders(X),createTunnelWebSocket:(X,Y,Z)=>$.createTunnelWebSocket(X,Y,Z),DEFAULT_POLL_INTERVAL_S:Z0,BACKOFF_BASE_MS:B2,BACKOFF_MAX_MS:A2,TUNNEL_PROTOCOL_VERSION:U2,get currentPollInterval(){return $.currentPollInterval},set currentPollInterval(X){$.currentPollInterval=X},get wsReconnectAttempt(){return $.wsReconnectAttempt},set wsReconnectAttempt(X){$.wsReconnectAttempt=X},get ws(){return $.ws},get stopped(){return $.stopped},get serverPublishedWsUrl(){return $.serverPublishedWsUrl},set serverPublishedWsUrl(X){$.serverPublishedWsUrl=X}}}}async function F2($){let X=P({name:$.name,workerDir:$.workerDir,apiKey:$.apiKey,cloudUrl:$.cloudUrl,port:$.port?parseInt($.port,10):void 0,projectsDir:$.projectsDir}),Y=t0($.proxy);if(!$.foreground)return G6({cfg:X,proxy:Y,flags:$});let Z=u({flag:$.runtimeBin});if(!Z)console.error(O.red(n({flag:$.runtimeBin}))),process.exit(1);if(y0(process.env,Y),$.debug){if(!await Z2(Y2({cloudUrl:X.cloudUrl,apiKey:X.apiKey,workerDir:X.workerDir,proxy:Y})))process.exit(1)}let z=!$.noAutoPull,K=!$.noGit;if(console.log(O.bold(`
|
|
14
|
-
Shogo Worker — Starting`)),console.log(O.dim(" name ")+X.name),console.log(O.dim(" worker-dir ")+X.workerDir),console.log(O.dim(" cloud ")+X.cloudUrl),console.log(O.dim(" runtime ")+`${Z.path} ${O.dim(`(via ${Z.source})`)}`),console.log(O.dim(" auto-pull ")+(z?O.green("on")+O.dim(` → ${X.projectsDir}`):O.yellow("off"))),console.log(O.dim(" sync mode ")+(K?O.green("git")+O.dim(" (falls back to file transport if git is missing)"):O.yellow("files-only"))),$.project)console.log(O.dim(" project ")+$.project);if(Y)console.log(O.dim(" proxy ")+`${Y.url} ${O.dim(`(from ${Y.source})`)}`);console.log("");let J=`${X.cloudUrl.replace(/\/+$/,"")}/api/ai/v1`,V={cloudUrl:X.cloudUrl,apiKey:X.apiKey,aiProxyUrl:J,aiProxyToken:X.apiKey},w=new m0({runtimeBin:$.runtimeBin,defaultSpawnConfig:V,autoPull:{enabled:z,projectsDir:X.projectsDir,watch:!0,useGit:K}});if(!w.resolveBinary())console.error(O.red(n({flag:$.runtimeBin}))),process.exit(1);let q=new f0({apiKey:X.apiKey,cloudUrl:X.cloudUrl,name:X.name,kind:"cli-worker",resolver:w,onAuthRevoked:(W)=>{console.error(O.red(`✗ Cloud auth revoked: ${W}`)),console.error(O.dim(" Run `shogo login` to re-authenticate; this worker will keep polling at the auth-failure backoff until then."))}}),Q=!1,H=async(W)=>{if(Q)return;Q=!0,console.log(O.dim(`
|
|
15
|
-
|
|
16
|
-
Shogo Worker — Started`)),console.log(O.dim(" pid: ")+V),console.log(O.dim(" name: ")+$.name),console.log(O.dim(" logs: ")+"~/.shogo/logs/worker.log"),console.log(O.dim(" stop: ")+"shogo worker stop")}function C6($){let X=["worker","start","--foreground"];if($.name)X.push("--name",$.name);if($.workerDir)X.push("--worker-dir",$.workerDir);if($.apiKey)X.push("--api-key",$.apiKey);if($.cloudUrl)X.push("--cloud-url",$.cloudUrl);if($.port)X.push("--port",$.port);if($.proxy)X.push("--proxy",$.proxy);if($.project)X.push("--project",$.project);if($.runtimeBin)X.push("--runtime-bin",$.runtimeBin);if($.debug)X.push("--debug");if($.noAutoPull)X.push("--no-auto-pull");if($.projectsDir)X.push("--projects-dir",$.projectsDir);if($.noGit)X.push("--no-git");return X}function L6(){let $=process.execPath,X=/\bbun(?:-[^/\\]*)?$/.test($)||typeof globalThis.Bun<"u",Y=process.argv[1];if(Y&&O6(Y)){let z=/\.ts$/.test(Y);return{entry:Y,runner:X?"bun":z?"tsx":"node"}}return{entry:new URL("../../bin/shogo.mjs",import.meta.url).pathname,runner:X?"bun":"node"}}import _2 from"picocolors";async function y2(){let{killedPid:$}=e0("SIGTERM");if($===null){console.log(_2.dim("No worker running."));return}console.log(_2.green(`✓ Worker stopped (pid=${$}).`))}import v from"picocolors";async function M2(){let $=Q0();if(!$){console.log(v.yellow("● stopped")+v.dim(" (no pid file)"));return}if(!H0($)){console.log(v.red("● dead")+v.dim(` (stale pid ${$})`));return}let X=I();if(console.log(v.green("● running")+v.dim(` (pid ${$})`)),X.name)console.log(v.dim(" name: ")+X.name);if(X.cloudUrl)console.log(v.dim(" cloud: ")+X.cloudUrl);if(X.workerDir)console.log(v.dim(" dir: ")+X.workerDir)}import{spawn as B6}from"node:child_process";import{existsSync as A6,statSync as U6}from"node:fs";import T2 from"picocolors";async function S2($){let X=$.err?q0:V0;if(!A6(X)){console.log(T2.dim("No logs yet."));return}let Y=$.follow?["-F","-n","200",X]:["-n","200",X];if(B6("tail",Y,{stdio:"inherit"}).on("exit",(z)=>process.exit(z??0)),!$.follow){if(U6(X).size===0)console.log(T2.dim("(log file is empty)"))}}import M from"picocolors";import{spawn as N6}from"node:child_process";import{hostname as F6,platform as _6,arch as y6}from"node:os";import f from"picocolors";class R extends Error{kind;constructor($,X){super($);this.kind=X;this.name="CloudLoginError"}}async function R2($){let X=$.cloudUrl.replace(/\/$/,""),Y=$.client??"cli",Z=$.deviceName??F6(),z=$.devicePlatform??`${_6()}-${y6()}`,K=$.appVersion??S6(),J=$.log??((k)=>console.log(k)),V=$.fetchImpl??fetch,w=await V(`${X}/api/cli/login/start`,{method:"POST",headers:{"content-type":"application/json","user-agent":d()},body:JSON.stringify({deviceId:$.deviceId,deviceName:Z,devicePlatform:z,deviceAppVersion:K,workspaceId:$.workspaceId,client:Y}),signal:AbortSignal.timeout(1e4)}).catch((k)=>{throw new R(`Cannot reach Shogo Cloud at ${X}: ${k?.message??k}`,"transport")});if(!w.ok){let k=await w.text().catch(()=>"");throw new R(`Cloud rejected /api/cli/login/start (HTTP ${w.status}): ${k||"no body"}`,"transport")}let q=await w.json().catch(()=>({}));if(!q?.ok||!q.state||!q.authUrl)throw new R(`Cloud returned a malformed start response: ${q?.error??JSON.stringify(q)}`,"transport");let Q=M6($.pollIntervalMs??q.pollIntervalMs),H=$.timeoutMs??q.expiresInMs,W=Date.now()+H;if(J(""),J(f.bold("Sign in to Shogo Cloud")),J(f.dim(" cloud: ")+X),J(f.dim(" device: ")+`${Z} (${z})`),J(f.dim(" user code: ")+f.cyan(q.userCode)),J(""),J(" Open this URL in your browser to approve:"),J(" "+f.cyan(q.authUrl)),J(""),$.openBrowser!==!1)if(typeof $.openBrowser==="function")try{await $.openBrowser(q.authUrl)}catch{}else R6(q.authUrl).catch(()=>{});J(f.dim("Waiting for approval..."));let G=$.installSignalHandlers??!0,L=()=>{A.abort()},A=new AbortController;if($.abortSignal)if($.abortSignal.aborted)A.abort();else $.abortSignal.addEventListener("abort",()=>A.abort(),{once:!0});if(G)process.once("SIGINT",L),process.once("SIGTERM",L);try{while(!0){if(A.signal.aborted)throw new R("Aborted by caller.","cancelled");if(Date.now()>=W)throw new R(`Timed out after ${Math.round(H/1000)}s waiting for approval.`,"timeout");let k=await V(`${X}/api/cli/login/poll?state=${encodeURIComponent(q.state)}`,{method:"GET",headers:{accept:"application/json","user-agent":d()},signal:AbortSignal.timeout(1e4)}).catch((_)=>{return J(f.dim(` (poll error: ${_?.message??_} — retrying)`)),null});if(k&&k.ok){let _=await k.json().catch(()=>({}));if(_?.status==="approved"&&_.key)return{key:_.key,email:_.email??null,workspace:_.workspace??null,deviceId:_.deviceId??$.deviceId};if(_?.status==="denied")throw new R("Sign-in was denied in the browser.","denied");if(_?.status==="expired")throw new R("Sign-in request expired before it was approved.","expired")}await T6(Q,A.signal)}}finally{if(G)process.removeListener("SIGINT",L),process.removeListener("SIGTERM",L)}}function M6($){if(!Number.isFinite($)||$<=0)return 2000;return Math.min(Math.max($,1000),1e4)}function T6($,X){return new Promise((Y,Z)=>{if(X?.aborted)return Z(new R("Aborted by caller.","cancelled"));let z=setTimeout(()=>{X?.removeEventListener("abort",K),Y()},$),K=()=>{clearTimeout(z),Z(new R("Aborted by caller.","cancelled"))};X?.addEventListener("abort",K,{once:!0})})}function S6(){return d()}function R6($){return new Promise((X)=>{let Y=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",Z=process.platform==="win32"?["/c","start",'""',$]:[$];try{let z=N6(Y,Z,{stdio:"ignore",detached:!0});z.on("error",()=>X()),z.unref(),X()}catch{X()}})}import{readFileSync as D6,writeFileSync as x6,existsSync as P6,chmodSync as b6}from"node:fs";import{randomUUID as E6}from"node:crypto";function D2(){if(P6(c)){let X=D6(c,"utf-8").trim();if(X)return X}j();let $=E6();return x6(c,$,{mode:384}),b6(c,384),$}var v6="https://studio.shogo.ai";async function x2($){let X=I(),Y=($.cloudUrl||X.cloudUrl||v6).replace(/\/$/,""),Z=$.apiKey||process.env.SHOGO_API_KEY;if(Z){await m6({key:Z,cloudUrl:Y,cfg:X,name:$.name});return}let z=D2(),K;try{K=await R2({cloudUrl:Y,deviceId:z,deviceName:$.name,workspaceId:$.workspace,openBrowser:!$.noBrowser})}catch(J){if(J instanceof R){if(console.error(M.red(`✗ ${J.message}`)),J.kind==="transport")console.error(M.dim(" If your network blocks browsers, run with --api-key <key> instead."));process.exit(1)}throw J}if(X.apiKey=K.key,X.cloudUrl=Y,$.name)X.name=$.name;if(r(X),console.log(""),console.log(M.green("✓ Signed in to Shogo Cloud")),K.workspace)console.log(M.dim(" workspace: ")+K.workspace);if(K.email)console.log(M.dim(" email: ")+K.email);console.log(M.dim(" saved to: ")+"~/.shogo/config.json"),console.log(""),console.log(M.dim(" next: ")+"shogo worker start")}async function m6({key:$,cloudUrl:X,cfg:Y,name:Z}){if(!/^shogo_sk_/.test($))console.error(M.red('✗ API key should start with "shogo_sk_". Copy it verbatim from the API Keys page.')),process.exit(1);let z;try{let K=await fetch(`${X}/api/api-keys/validate`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({key:$}),signal:AbortSignal.timeout(1e4)});if(z=await K.json().catch(()=>({})),!K.ok||!z?.valid)console.error(M.red(`✗ ${z?.error||`Cloud rejected the key (HTTP ${K.status}).`}`)),process.exit(1)}catch(K){console.error(M.red(`✗ Cannot reach Shogo Cloud at ${X}: ${K?.message??K}`)),process.exit(1)}if(Y.apiKey=$,Y.cloudUrl=X,Z)Y.name=Z;if(r(Y),console.log(M.green("✓ API key saved to ~/.shogo/config.json")),z.workspace?.name)console.log(M.dim(" workspace: ")+z.workspace.name);if(z.user?.email)console.log(M.dim(" email: ")+z.user.email);console.log(M.dim(" next: ")+"shogo worker start")}import z0 from"picocolors";async function P2(){let $=I();if(Object.keys($).length===0){console.log(z0.dim("(no config — run `shogo login` or `shogo config set`)"));return}let X={...$,apiKey:$.apiKey?`***${$.apiKey.slice(-4)}`:void 0};console.log(z0.dim(`file: ${h}`)),console.log(JSON.stringify(X,null,2))}async function b2($,X){let Y=["apiKey","cloudUrl","name","workerDir","port"];if(!Y.includes($))console.error(z0.red(`Unknown key: ${$}`)),console.error(z0.dim(`Allowed: ${Y.join(", ")}`)),process.exit(1);let Z=I();Z[$]=$==="port"?parseInt(X,10):X,r(Z),console.log(z0.green(`✓ ${$} set`))}import U from"picocolors";import{spawn as h6}from"node:child_process";import{createHash as j6}from"node:crypto";import{createWriteStream as I6,existsSync as K0,mkdirSync as n0,readFileSync as m2,renameSync as E2,rmSync as d0,writeFileSync as u6}from"node:fs";import{tmpdir as f6}from"node:os";import{dirname as n6,join as C0}from"node:path";import{Readable as d6}from"node:stream";import{pipeline as i6}from"node:stream/promises";var g6="https://github.com/shogo-labs/shogo-ai/releases/download";function h2(){let{platform:$,arch:X}=process,Y;if($==="darwin")Y="darwin";else if($==="linux")Y="linux";else if($==="win32")Y="windows";else throw Error(`Unsupported platform: ${$}`);let Z;if(X==="arm64")Z="arm64";else if(X==="x64")Z="x64";else throw Error(`Unsupported arch: ${X} (need arm64 or x64)`);return`${Y}-${Z}`}function L0(){if(!K0(l))return null;try{let $=m2(l,"utf-8");return JSON.parse($)}catch{return null}}async function p6($,X){let Y=X.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/releases\/download/);if(!Y)throw Error(`Cannot auto-resolve latest version from non-GitHub baseUrl '${X}'. Pass --version explicitly.`);let Z=Y[1],z=Y[2];if($==="stable"){let Q=`https://api.github.com/repos/${Z}/${z}/releases/latest`,H=await fetch(Q,{headers:{Accept:"application/vnd.github+json"}});if(!H.ok)throw Error(`GitHub API ${H.status} for ${Q}`);let W=await H.json();if(!W.tag_name)throw Error("GitHub API did not return tag_name");return v2(W.tag_name)}let K=`https://api.github.com/repos/${Z}/${z}/releases?per_page=30`,J=await fetch(K,{headers:{Accept:"application/vnd.github+json"}});if(!J.ok)throw Error(`GitHub API ${J.status} for ${K}`);let V=await J.json(),w=$==="beta"?"-beta":"-nightly",q=V.find((Q)=>Q.prerelease&&/^v\d+\.\d+\.\d+-/.test(Q.tag_name)&&Q.tag_name.includes(w));if(!q)throw Error(`No ${$} runtime release found`);return v2(q.tag_name)}function v2($){if(!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test($))throw Error(`Unexpected app tag '${$}' (expected vX.Y.Z[-prerelease])`);return $.slice(1)}function c6($,X,Y){let Z=`shogo-agent-runtime-${X}.tar.gz`,z=`v${$}`,K=`${Y.replace(/\/$/,"")}/${z}/${Z}`;return{tarball:K,sha256:`${K}.sha256`,assetName:Z}}async function o6($,X){let Y=await fetch($,{redirect:"follow"});if(!Y.ok)throw Error(`Download failed: HTTP ${Y.status} for ${$}`);if(!Y.body)throw Error(`Download failed: empty body for ${$}`);await i6(d6.fromWeb(Y.body),I6(X))}async function a6($){let X=await fetch($,{redirect:"follow"});if(!X.ok)throw Error(`SHA256 sidecar fetch failed: HTTP ${X.status} for ${$}`);let Z=(await X.text()).trim().split(/\s+/)[0];if(!/^[0-9a-f]{64}$/i.test(Z))throw Error(`SHA256 sidecar at ${$} did not contain a 64-char hex digest`);return Z.toLowerCase()}function l6($){let X=m2($);return j6("sha256").update(X).digest("hex")}async function r6($,X){await new Promise((Y,Z)=>{let z=h6("tar",["-xzf",$,"-C",X],{stdio:["ignore","pipe","pipe"]}),K="";z.stderr.on("data",(J)=>{K+=J.toString()}),z.on("error",Z),z.on("exit",(J)=>{if(J===0)Y();else Z(Error(`tar exited ${J}: ${K.trim()}`))})})}function e6($,X){c0();let Y=n6(X);if(!K0(Y))n0(Y,{recursive:!0,mode:448});let Z=`${X}.next`;if(K0(Z))d0(Z,{force:!0});E2($,Z);try{if(process.platform!=="win32"){let{chmodSync:z}=Q$("node:fs");z(Z,493)}}catch{}if(K0(X))d0(X,{force:!0});E2(Z,X)}async function j2($={}){let X=$.logger??console,Y=$.channel??"stable",Z=$.baseUrl??process.env.SHOGO_RUNTIME_RELEASES_URL??g6,z=$.target??h2(),K=$.version;if(!K)X.log(`[runtime install] Resolving latest ${Y} version...`),K=await p6(Y,Z),X.log(`[runtime install] Latest ${Y} = ${K}`);let J=L0();if(J&&J.version===K&&J.target===z&&!$.force)return X.log(`[runtime install] ${K} (${z}) already installed at ${x} — pass --force to reinstall`),{version:K,target:z,binPath:x,source:J.source,sha256:J.sha256,channel:Y};let V=c6(K,z,Z);X.log(`[runtime install] Downloading ${V.tarball}`);let w=C0(f6(),`shogo-runtime-install-${process.pid}-${Date.now()}`);n0(w,{recursive:!0});try{let q=C0(w,V.assetName);await o6(V.tarball,q),X.log("[runtime install] Verifying SHA-256...");let Q=await a6(V.sha256),H=l6(q);if(H!==Q)throw Error(`SHA-256 mismatch for ${V.assetName}
|
|
17
|
-
expected: ${Q}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
${
|
|
24
|
-
|
|
25
|
-
${
|
|
26
|
-
|
|
9
|
+
`));return!X}import{accessSync as m$,constants as I$,existsSync as u$}from"node:fs";import{delimiter as f$,join as n$}from"node:path";function u($={}){let X=$.env??process.env,Y=d$($);for(let K of Y)if(H2(K.path))return K;let Z=$.systemBinName??Q2(),z=g$(Z,X.PATH);if(z)return{path:z,source:"path"};return null}function d$($){let X=$.env??process.env,Y=[];if($.flag&&$.flag.trim())Y.push({path:$.flag.trim(),source:"flag"});let Z=X.SHOGO_AGENT_RUNTIME_BIN?.trim();if(Z)Y.push({path:Z,source:"env"});return Y.push({path:R,source:"home"}),Y}function Q2(){return process.platform==="win32"?"shogo-agent-runtime.exe":"shogo-agent-runtime"}function H2($){if(!u$($))return!1;try{if(process.platform==="win32")return!0;return m$($,I$.X_OK),!0}catch{return!1}}function g$($,X){if(!X)return null;for(let Y of X.split(f$)){if(!Y)continue;let Z=n$(Y,$);if(H2(Z))return Z}return null}function n($={}){let X=$.env??process.env,Y=[];if(Y.push("Error: agent-runtime binary not found."),Y.push(""),Y.push("Looked in (priority order):"),$.flag)Y.push(` --runtime-bin ${$.flag}`);if(X.SHOGO_AGENT_RUNTIME_BIN)Y.push(` $SHOGO_AGENT_RUNTIME_BIN = ${X.SHOGO_AGENT_RUNTIME_BIN}`);return Y.push(` ${R} (default install location)`),Y.push(` ${Q2()} on $PATH`),Y.push(""),Y.push("Fix:"),Y.push(" - Run `shogo runtime install` to download the latest binary, or"),Y.push(" - Pass `--runtime-bin <path>` to point at an existing build."),Y.join(`
|
|
10
|
+
`)}import{spawn as J6,spawnSync as V6}from"node:child_process";import{createHmac as M2,randomBytes as q6}from"node:crypto";import{existsSync as v0,mkdirSync as N2,readdirSync as Q6}from"node:fs";import{createConnection as H6}from"node:net";import{tmpdir as W6}from"node:os";import{dirname as O6,join as Z0}from"node:path";import{spawnSync as x0}from"node:child_process";import{existsSync as i$,mkdirSync as p$,writeFileSync as G0}from"node:fs";import{join as i}from"node:path";var W2=1e5;function O2($){let X=parseInt($.RUNTIME_MEMORY_MB||"",10);if(!Number.isFinite(X)||X<=0)return null;let Y=parseInt($.RUNTIME_CPU_PERCENT||"0",10),Z=Number.isFinite(Y)&&Y>0?Y:0,z=parseInt($.RUNTIME_TASKS_MAX||"",10),K=Number.isFinite(z)&&z>0?z:4096;return{memoryMB:X,cpuPercent:Z,tasksMax:K}}function G2($,X){let Z=`--max-old-space-size=${Math.max(256,Math.floor(X*0.75))}`,z=$.NODE_OPTIONS?.trim();if(z){if(/--max-old-space-size=/.test(z))return;$.NODE_OPTIONS=`${z} ${Z}`}else $.NODE_OPTIONS=Z}function c$(){if(process.platform!=="linux")return!1;try{return x0("systemd-run",["--version"],{stdio:"ignore",timeout:3000}).status===0}catch{return!1}}function k2($){let{command:X,args:Y,limits:Z,scopeName:z,logger:K}=$;if(process.platform!=="linux"||!c$())return{command:X,args:Y,wrapped:!1};let J=["-p",`MemoryMax=${Z.memoryMB}M`,"-p",`MemoryHigh=${Math.max(128,Math.floor(Z.memoryMB*0.9))}M`,"-p",`TasksMax=${Z.tasksMax}`];if(Z.cpuPercent>0)J.push("-p",`CPUQuota=${Z.cpuPercent}%`);let V=["--user","--scope","--quiet","--collect",`--unit=${z}`,...J,"--",X,...Y];return K?.log(`[resource-limits] wrapping spawn in systemd scope ${z} (MemoryMax=${Z.memoryMB}M)`),{command:"systemd-run",args:V,wrapped:!0}}function w2($){let{pid:X,limits:Y,logger:Z}=$;if(process.platform!=="linux")return!1;let z="/sys/fs/cgroup";if(!i$(i(z,"cgroup.controllers")))return!1;let K=i(z,"shogo",`rt-${X}`);try{if(p$(K,{recursive:!0}),G0(i(K,"memory.max"),String(Y.memoryMB*1024*1024)),Y.cpuPercent>0){let J=Math.floor(Y.cpuPercent/100*W2);G0(i(K,"cpu.max"),`${J} ${W2}`)}try{G0(i(K,"pids.max"),String(Y.tasksMax))}catch{}return G0(i(K,"cgroup.procs"),String(X)),Z?.log(`[resource-limits] attached pid ${X} to cgroup ${K} (memory.max=${Y.memoryMB}M)`),!0}catch(J){return Z?.warn(`[resource-limits] direct cgroup attach failed for pid ${X}: ${J?.message??J}`),!1}}function L2($){let{pid:X,limits:Y,logger:Z}=$;if(process.platform!=="win32")return!1;let z=process.env.SHOGO_WINJOB_HELPER||"@shogo/winjob";try{let K=T0(z);if(typeof K.limitProcess!=="function")return!1;let J=K.limitProcess(X,{memoryBytes:Y.memoryMB*1024*1024,killOnClose:!0});if(J)Z?.log(`[resource-limits] attached pid ${X} to Job Object (${Y.memoryMB}M)`);return J}catch{return!1}}function o$($,X=process.platform){try{if(X==="win32"){let K=`$ErrorActionPreference='SilentlyContinue';$root=${$};$all=Get-CimInstance Win32_Process;$seen=@{};$stack=New-Object System.Collections.Stack;$stack.Push($root);$sum=0;while($stack.Count -gt 0){$id=$stack.Pop();if($seen[$id]){continue};$seen[$id]=$true;foreach($p in $all){if($p.ProcessId -eq $id){$sum+=$p.WorkingSetSize};if($p.ParentProcessId -eq $id){$stack.Push($p.ProcessId)}}};[math]::Floor($sum/1024)`,J=x0("powershell.exe",["-NoProfile","-NonInteractive","-Command",K],{encoding:"utf-8",timeout:1e4,windowsHide:!0});if(J.status!==0||!J.stdout)return null;let V=parseInt(J.stdout.trim(),10);return Number.isFinite(V)?V:null}let Y=x0("ps",["-o","rss=","-g",String($)],{encoding:"utf-8",timeout:5000});if(Y.status!==0||!Y.stdout)return null;let Z=0,z=!1;for(let K of Y.stdout.split(`
|
|
11
|
+
`)){let J=parseInt(K.trim(),10);if(Number.isFinite(J))Z+=J,z=!0}return z?Z:null}catch{return null}}class S0{timer=null;consecutive=0;fired=!1;pid;ceilingMB;platform;intervalMs;strikes;logger;onBreach;constructor($){this.pid=$.pid,this.ceilingMB=$.ceilingMB,this.platform=$.platform??process.platform,this.intervalMs=$.intervalMs??parseInt(process.env.RUNTIME_RSS_WATCHDOG_MS||"15000",10),this.strikes=$.strikes??parseInt(process.env.RUNTIME_RSS_WATCHDOG_STRIKES||"2",10),this.logger=$.logger,this.onBreach=$.onBreach}start(){if(this.timer)return;this.timer=setInterval(()=>this.sample(),Math.max(2000,this.intervalMs));try{this.timer.unref?.()}catch{}}stop(){if(this.timer)clearInterval(this.timer),this.timer=null}sample(){if(this.fired)return;let $=o$(this.pid,this.platform);if($==null){this.consecutive=0;return}let X=Math.floor($/1024);if(X<=this.ceilingMB){this.consecutive=0;return}if(this.consecutive+=1,this.logger?.warn(`[resource-limits] pid ${this.pid} RSS ${X}MB over ceiling ${this.ceilingMB}MB (strike ${this.consecutive}/${this.strikes})`),this.consecutive>=this.strikes)this.fired=!0,this.stop(),this.onBreach(X)}}import{CloudFileTransport as h0}from"@shogo-ai/sdk/cloud-file-transport";import{watch as A2,statSync as t$}from"node:fs";import{relative as BX,sep as $6,posix as X6}from"node:path";var Y6=1500,Z6=new Set(["node_modules",".git","dist","build",".vite",".cache"]),z6=[".shogo/"];function K6($){if(!$)return!0;return $.split("/").some((X)=>Z6.has(X))}class Y0{rootDir;transport;debounceMs;logger;onFlush;mode;git;commitAndPush;watcher=null;pending=new Set;timer=null;flushing=!1;stopped=!1;constructor($){if(this.rootDir=$.rootDir,this.transport=$.transport,this.debounceMs=$.debounceMs??Y6,this.logger=$.logger??console,this.onFlush=$.onFlush,this.mode=$.mode??"files",this.mode==="git"&&!$.git)throw Error('CloudSyncWatcher: mode: "git" requires the `git` option block');this.git=$.git,this.commitAndPush=$.commitAndPush??(async(X)=>{return(await Promise.resolve().then(() => (w0(),C2))).commitAndPush(X)})}start(){if(this.watcher)return;if(this.stopped)throw Error("CloudSyncWatcher: already stopped, build a fresh instance");try{this.watcher=A2(this.rootDir,{recursive:!0},($,X)=>{if(!X)return;this.handleEvent(String(X))})}catch($){this.logger.warn(`[CloudSyncWatcher] recursive watch failed (${$?.message??$}); falling back to root-only watch.`),this.watcher=A2(this.rootDir,(X,Y)=>{if(!Y)return;this.handleEvent(String(Y))})}}async stop(){if(this.stopped)return;if(this.stopped=!0,this.timer)clearTimeout(this.timer),this.timer=null;if(this.watcher){try{this.watcher.close()}catch{}this.watcher=null}if(this.pending.size>0)await this.flush()}handleEvent($){if(this.stopped)return;let X=$.split($6).join(X6.sep);if(K6(X))return;try{let Y=`${this.rootDir}/${X}`;if(t$(Y).isDirectory())return}catch{return}this.pending.add(X),this.scheduleFlush()}scheduleFlush(){if(this.timer)return;this.timer=setTimeout(()=>{this.timer=null,this.flush()},this.debounceMs)}async flush(){if(this.flushing){this.scheduleFlush();return}if(this.pending.size===0)return;this.flushing=!0;let $=Array.from(this.pending);this.pending.clear();try{if(this.mode==="git")await this.flushGit($);else await this.flushFiles($)}catch(X){this.logger.error(`[CloudSyncWatcher] flush failed: ${X?.message??X}`);for(let Y of $)this.pending.add(Y);this.scheduleFlush()}finally{this.flushing=!1}}async flushFiles($){if($.length===0)return;let X=await this.transport.uploadFiles($);if(this.onFlush?.({uploaded:$,errors:X.errors.length}),X.errors.length>0)for(let Y of X.errors)this.logger.warn(`[CloudSyncWatcher] upload ${Y.path}: ${Y.message}`)}async flushGit($){if(!this.git)throw Error("CloudSyncWatcher: missing git options");let X=[],Y=[];for(let K of $)if(z6.some((J)=>K===J.slice(0,-1)||K.startsWith(J)))X.push(K);else Y.push(K);if(X.length>0){let K=await this.transport.uploadFiles(X);if(K.errors.length>0)for(let J of K.errors)this.logger.warn(`[CloudSyncWatcher] upload ${J.path}: ${J.message}`)}let Z=!1,z;if(Y.length>0){let K=`auto: ${new Date().toISOString()}`;try{let J=await this.commitAndPush({apiUrl:this.git.apiUrl,apiKey:this.git.apiKey,projectId:this.git.projectId,localDir:this.rootDir,message:K,branch:this.git.branch,authorEmail:this.git.authorEmail,authorName:this.git.authorName,logger:this.logger});Z=J.committed,z=J.commitSha}catch(J){throw J}}this.onFlush?.({uploaded:$,errors:0,committed:Z,commitSha:z})}}w0();var j0=37100,m0=37900,U2=1,G6=2,L0=16,k6=900000,w6=30000,L6=1000,C6=60000,B2=8,F2=300000,A6=60000,N6=500,U6=30000,T2=25000,B6=5000,F6=500,T6=($)=>({command:$,args:[]}),I0=null;function x2(){if(!I0)I0=q6(32).toString("hex");return I0}function _2($){return M2("sha256",x2()).update(`runtime:${$}`).digest("hex")}function _6($){return M2("sha256",x2()).update(`webhook:${$}`).digest("hex")}function y2($){let X=$.indexOf("?");if(X===-1)return{pathname:$,search:""};return{pathname:$.slice(0,X),search:$.slice(X)}}function D2($){try{return Q6($).length===0}catch{return!0}}function u0($,X,Y){let Z=[];switch(Z.push(`Cannot spawn agent-runtime for project ${$}: no workspace directory available.`),Z.push(""),X){case"no-auto-pull-config":Z.push(" Reason: WorkerRuntimeManager was constructed without an `autoPull` config and no caller-provided `projectDir` was found on disk."),Z.push(" This usually means a programmatic embedder forgot to wire up enrichSpawnConfig "+"or autoPull. CLI users should not see this — please file a bug.");break;case"no-projects-dir":Z.push(" Reason: auto-pull is configured but `projectsDir` is empty. The worker needs a persistent root directory under which it can store cloned project workspaces.");break;case"auto-pull-disabled":Z.push(` Reason: auto-pull was disabled (--no-auto-pull) and the expected pre-pulled workspace at ${Y} is missing or empty.`);break}return Z.push(""),Z.push(" How to fix (pick one):"),Z.push(" 1. Re-enable auto-pull (default). Drop the --no-auto-pull flag and restart"),Z.push(" the worker. The first inbound request for this project will clone its"),Z.push(" workspace from Shogo Cloud into <projectsDir>/<projectId>/."),Z.push(""),Z.push(" 2. Pre-pull manually with `shogo project pull <projectId>` before starting"),Z.push(" the worker. Use this when you want full control over when the clone runs"),Z.push(" (slow links, scheduled maintenance windows, etc.)."),Z.push(""),Z.push(" 3. Point the worker at an existing workspace by setting either:"),Z.push(" --projects-dir <path> (per-invocation flag)"),Z.push(" SHOGO_PROJECTS_DIR=<path> (env var, persists across restarts)"),Z.push(" shogo config set projectsDir <path>"),Z.push(" Whichever path you pick must contain a subdirectory named after the "),Z.push(` project id (e.g. <path>/${$}/) populated with the project's source.`),Z.push(""),Z.push(" Docs: https://shogo.ai/docs/self-hosted-worker#workspace-seeding"),Z.join(`
|
|
12
|
+
`)}class f0{opts;log;runtimes=new Map;usedPorts=new Set;spawnCommand;resolved=null;stopped=!1;resolvedLimits=void 0;watchers=new Map;pulledProjects=new Set;syncModes=new Map;constructor($={}){this.opts=$,this.log=$.logger??console,this.spawnCommand=$.spawnCommand??T6}resolveBinary(){if(!this.resolved)this.resolved=this.opts.resolveBin?this.opts.resolveBin():u({flag:this.opts.runtimeBin,env:this.opts.env});return this.resolved}async resolveLocalUrl($,X){let{pathname:Y,search:Z}=y2($);if(!(Y.startsWith("/agent/")||Y==="/agent"))return null;if(!X){let J=this.getActiveProjects();if(J.length!==1)return null;X=J[0]}let z=await this.spawnConfigFor(X);if(!z)return this.log.warn(`[WorkerRuntimeManager] No spawn config for ${X} — set defaultSpawnConfig or enrichSpawnConfig`),null;let K=await this.ensureRunning(X,z);if(!K.agentPort)return null;return this.touch(X),`http://127.0.0.1:${K.agentPort}${Y}${Z}`}deriveRuntimeToken($){return _2($)}describeRejection($,X){let{pathname:Y}=y2($);if(!(Y.startsWith("/agent/")||Y==="/agent"))return{code:"CLI_WORKER_HAS_NO_DATA_API",message:`cli-worker only serves /agent/* paths; tried: ${Y}`};return{code:"CLI_WORKER_NO_PROJECT_FOR_PATH",message:`cli-worker received an /agent path without a single active project; projectId=${X??"none"}, path=${Y}`}}async spawnConfigFor($){let X=this.opts.defaultSpawnConfig;if(!X)return null;if(this.opts.enrichSpawnConfig)try{return await this.opts.enrichSpawnConfig($,X)}catch(Y){this.log.warn(`[WorkerRuntimeManager] enrichSpawnConfig failed for ${$}: ${Y?.message??Y}`)}return X}async ensureRunning($,X){if(this.stopped)throw Error("WorkerRuntimeManager is stopped");let Y=this.runtimes.get($);if(Y?.status==="failed")throw Error(`[WorkerRuntimeManager] cannot ensureRunning(${$}): ${Y.lastError??"runtime is in failed state"}. Call resetFailure(${$}) or stop(${$}) before retrying.`);X=await this.maybeAutoPull($,X);let Z=this.runtimes.get($);if(Z?.status==="running")return this.touch($),this.snapshot(Z);if(Z?.startPromise){let K=await Z.startPromise;return this.snapshot(K)}let z=Z??this.makeSlot($,X);if(!Z)this.runtimes.set($,z);z.spawnConfig=X,z.startPromise=this.doStart(z);try{let K=await z.startPromise;return this.enforceMaxRuntimes($),this.snapshot(K)}finally{z.startPromise=null}}enforceMaxRuntimes($){let X=this.opts.maxRuntimes;if(X==null||!Number.isFinite(X)||X<=0)return;let Y=Date.now(),Z=Array.from(this.runtimes.values()).filter((J)=>J.status==="running");if(Z.length<=X)return;let z=Z.filter((J)=>J.projectId!==$&&Y-J.lastUsedAt>=w6).sort((J,V)=>J.lastUsedAt-V.lastUsedAt),K=Z.length-X;for(let J of z){if(K<=0)break;let V=Y-J.lastUsedAt;this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} exceeded (${Z.length} running) — `+`LRU-evicting ${J.projectId} (idle ${Math.round(V/1000)}s)`),this.stop(J.projectId).catch((Q)=>{this.log.warn(`[WorkerRuntimeManager] maxRuntimes eviction of ${J.projectId} failed: ${Q?.message??Q}`)}),K--}if(K>0)this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} still exceeded by ${K} after eviction pass — `+"remaining over-cap slots are mid-stream; will retry on next spawn / idle reap")}async ensurePulled($,X){return this.maybeAutoPull($,X)}async maybeAutoPull($,X){let Y=this.opts.autoPull;if(X.projectDir&&v0(X.projectDir))return X;if(!Y)throw Error(u0($,"no-auto-pull-config",null));if(!Y.projectsDir)throw Error(u0($,"no-projects-dir",null));if(!Y.enabled){let J=Z0(Y.projectsDir,$);if(v0(J)&&!D2(J))return{...X,projectDir:J};throw Error(u0($,"auto-pull-disabled",J))}if(this.pulledProjects.has($))return{...X,projectDir:Z0(Y.projectsDir,$)};let Z=Z0(Y.projectsDir,$),z=Y.logger??this.log,K={cloneProject:Y.gitOps?.cloneProject??b0,gitIsAvailable:Y.gitOps?.gitIsAvailable??E0,isGitRepo:Y.gitOps?.isGitRepo??X0};this.pulledProjects.add($);try{N2(Z,{recursive:!0});let J=D2(Z),V=K.isGitRepo(Z),H=(Y.useGit!==!1?await K.gitIsAvailable():!1)&&(J||V)?"git":"files";if(this.syncModes.set($,H),H==="git"){if(J){z.log(`[WorkerRuntimeManager] auto-pull: git clone project ${$} into ${Z}`);try{let W=await K.cloneProject({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z,shallow:!0,logger:z});z.log(`[WorkerRuntimeManager] auto-pull: ${$} cloned at ${W.commitSha.slice(0,8)}`)}catch(W){z.warn(`[WorkerRuntimeManager] auto-pull: git clone failed for ${$} (${W?.message??W}); falling back to CloudFileTransport.downloadAll`),this.syncModes.set($,"files"),await this.fileTransportClone($,Z,X,z)}}else if(V)z.log(`[WorkerRuntimeManager] auto-pull: ${$} already has .git/; skipping clone`);if(this.syncModes.get($)==="git")await this.topUpShogoState($,Z,X,z)}else if(J)await this.fileTransportClone($,Z,X,z);else z.log(`[WorkerRuntimeManager] auto-pull: ${$} workspace already populated; skipping clone`);if(Y.watch!==!1&&!this.watchers.has($))try{let W=new h0({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z}),O=this.syncModes.get($)??"files",k=new Y0({rootDir:Z,transport:W,logger:z,mode:O,git:O==="git"?{apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$}:void 0});k.start(),this.watchers.set($,k)}catch(W){z.warn(`[WorkerRuntimeManager] auto-pull: watcher start failed for ${$}: ${W?.message??W}`)}}catch(J){z.warn(`[WorkerRuntimeManager] auto-pull: failed for ${$} — runtime will fall back to template defaults. `+`(${J?.message??J})`)}return{...X,projectDir:Z}}async fileTransportClone($,X,Y,Z){Z.log(`[WorkerRuntimeManager] auto-pull: file-transport clone of ${$} into ${X}`);let K=await new h0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}).downloadAll();Z.log(`[WorkerRuntimeManager] auto-pull: ${$} downloaded ${K.downloaded} files (${K.errors.length} errors)`)}async topUpShogoState($,X,Y,Z){try{let z=new h0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}),J=(await z.listManifest()).filter((Q)=>Q.path===".shogo"||Q.path.startsWith(".shogo/"));if(J.length===0)return;let V=await z.downloadFiles(J);Z.log(`[WorkerRuntimeManager] auto-pull: ${$} .shogo/ top-up downloaded ${V.downloaded} files (${V.errors.length} errors)`)}catch(z){Z.warn(`[WorkerRuntimeManager] auto-pull: .shogo top-up failed for ${$}: ${z?.message??z}`)}}shouldDeferToCloudSync($){return!!this.opts.autoPull?.enabled&&this.pulledProjects.has($)}status($){let X=this.runtimes.get($);return X?this.snapshot(X):null}getActiveProjects(){return Array.from(this.runtimes.keys()).filter(($)=>{let X=this.runtimes.get($);return X&&(X.status==="running"||X.status==="starting"||X.status==="restarting")})}touch($){let X=this.runtimes.get($);if(!X)return;X.lastUsedAt=Date.now(),this.armIdleTimer(X)}async stop($,X="SIGTERM"){let Y=this.runtimes.get($);if(!Y)return;if(Y.status="stopping",Y.restartTimer)clearTimeout(Y.restartTimer),Y.restartTimer=null;if(Y.idleTimer)clearTimeout(Y.idleTimer),Y.idleTimer=null;if(Y.graceTimer)clearTimeout(Y.graceTimer),Y.graceTimer=null;if(Y.limitWatchdog)Y.limitWatchdog.stop(),Y.limitWatchdog=null;if(Y.proc){this.killProcessGroup(Y,X);try{Y.proc.kill(X)}catch{}await this.waitForExit(Y.proc,5000),this.killProcessGroup(Y,"SIGKILL")}Y.pid=null,this.releasePort(Y.agentPort),this.runtimes.delete($)}resetFailure($){let X=this.runtimes.get($);if(!X||X.status!=="failed")return!1;if(X.restartTimer)clearTimeout(X.restartTimer),X.restartTimer=null;if(X.idleTimer)clearTimeout(X.idleTimer),X.idleTimer=null;if(X.graceTimer)clearTimeout(X.graceTimer),X.graceTimer=null;if(X.limitWatchdog)X.limitWatchdog.stop(),X.limitWatchdog=null;return this.runtimes.delete($),this.log.log(`[WorkerRuntimeManager] resetFailure: ${$} cleared, next ensureRunning will respawn`),!0}async stopAll($="SIGTERM"){this.stopped=!0;let X=Array.from(this.watchers.keys());await Promise.all(X.map(async(Z)=>{let z=this.watchers.get(Z);if(this.watchers.delete(Z),z)try{await z.stop()}catch(K){this.log.warn(`[WorkerRuntimeManager] watcher stop ${Z}: ${K?.message??K}`)}}));let Y=Array.from(this.runtimes.keys());await Promise.all(Y.map((Z)=>this.stop(Z,$).catch((z)=>{this.log.error(`[WorkerRuntimeManager] Failed to stop ${Z}: ${z?.message??z}`)})))}makeSlot($,X){return{projectId:$,agentPort:0,apiServerPort:0,status:"starting",proc:null,pid:null,startedAt:0,lastStdoutAt:0,lastUsedAt:Date.now(),restarts:0,consecutiveFailures:0,lastFailureAt:0,graceTimer:null,restartTimer:null,idleTimer:null,limitWatchdog:null,spawnConfig:X,startPromise:null}}async doStart($){let X=this.resolveBinary();if(!X)throw $.status="error",$.lastError="agent-runtime binary not found (run `shogo runtime install`)",Error($.lastError);if(!$.agentPort)$.agentPort=await this.allocatePort(),$.apiServerPort=$.agentPort+U2;let Y=this.buildEnv($,X.path),Z=this.resolveCwd($),z=this.spawnCommand(X.path),K=this.resolveLimits(),J=z.command,V=z.args,Q=!1;if(K){let O=k2({command:z.command,args:z.args,limits:K,scopeName:`shogo-rt-${$.projectId.slice(0,8)}-${Date.now().toString(36)}`,logger:this.log});J=O.command,V=O.args,Q=O.wrapped}this.log.log(`[WorkerRuntimeManager] Spawning agent-runtime for ${$.projectId} via ${J} ${V.join(" ")} (port=${$.agentPort}, source=${X.source}${K?`, memCeil=${K.memoryMB}M`:""})`);let q=process.platform!=="win32",H=J6(J,V,{cwd:Z,env:Y,detached:q,stdio:["ignore","pipe","pipe"]});if(q)try{H.unref()}catch{}if($.proc=H,$.pid=H.pid??null,$.status="starting",$.startedAt=Date.now(),K)try{this.applyPostSpawnLimits($,K,Q)}catch(O){this.log.warn(`[WorkerRuntimeManager] failed to apply resource limits for ${$.projectId}: ${O?.message??O}`)}$.lastStdoutAt=$.startedAt,H.on("error",(O)=>{$.lastError=O?.message??String(O),this.log.error(`[WorkerRuntimeManager] spawn error for ${$.projectId}: ${$.lastError}`)}),H.on("exit",(O,k)=>{this.handleExit($,O,k)});let W=`[runtime:${$.projectId.slice(0,8)}]`;H.stdout?.on("data",(O)=>{$.lastStdoutAt=Date.now();for(let k of O.toString().trimEnd().split(`
|
|
13
|
+
`))if(k)this.log.log(`${W} ${k}`)}),H.stderr?.on("data",(O)=>{$.lastStdoutAt=Date.now();for(let k of O.toString().trimEnd().split(`
|
|
14
|
+
`))if(k)this.log.error(`${W} ${k}`)});try{return await this.waitForHealth($,U6),$.status="running",$.lastUsedAt=Date.now(),this.armIdleTimer($),this.armGraceTimer($),$}catch(O){$.status="error",$.lastError=O?.message??String(O),this.killProcessGroup($,"SIGTERM");try{H.kill("SIGTERM")}catch{}throw this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,O}}resolveLimits(){if(this.resolvedLimits===void 0)this.resolvedLimits=O2(this.opts.env??process.env);return this.resolvedLimits}applyPostSpawnLimits($,X,Y){let Z=$.pid;if(!Z)return;if(process.platform==="linux"){if(Y)return;if(w2({pid:Z,limits:X,logger:this.log}))return}else if(process.platform==="win32"){if(L2({pid:Z,limits:X,logger:this.log}))return}$.limitWatchdog=new S0({pid:Z,ceilingMB:X.memoryMB,logger:this.log,onBreach:(z)=>this.handleLimitBreach($,z,X.memoryMB)}),$.limitWatchdog.start()}handleLimitBreach($,X,Y){if(this.log.warn(`[WorkerRuntimeManager] runtime ${$.projectId} exceeded memory ceiling (${X}MB > ${Y}MB) — restarting`),$.lastError=`memory ceiling exceeded (${X}MB > ${Y}MB)`,$.limitWatchdog)$.limitWatchdog.stop(),$.limitWatchdog=null;this.killProcessGroup($,"SIGTERM");try{$.proc?.kill("SIGTERM")}catch{}let Z=setTimeout(()=>this.killProcessGroup($,"SIGKILL"),5000);try{Z.unref?.()}catch{}}killProcessGroup($,X){if(!$.pid)return;if(process.platform==="win32"){try{V6("taskkill",["/F","/T","/PID",String($.pid)],{stdio:"ignore",windowsHide:!0})}catch{}return}try{process.kill(-$.pid,X)}catch{}}armGraceTimer($){if($.graceTimer)clearTimeout($.graceTimer),$.graceTimer=null;$.graceTimer=setTimeout(()=>{if($.graceTimer=null,$.consecutiveFailures>0)$.consecutiveFailures=0},A6);try{$.graceTimer.unref?.()}catch{}}buildEnv($,X){let Y=$.spawnConfig,Z={...this.opts.env??process.env,PROJECT_ID:$.projectId,PORT:String($.agentPort),API_SERVER_PORT:String($.apiServerPort),SKILL_SERVER_PORT:String($.apiServerPort),WORKSPACE_API_PORT_BASE:String($.agentPort+G6),NODE_ENV:"production",SHOGO_CLOUD_URL:Y.cloudUrl,SHOGO_API_URL:Y.cloudUrl,SHOGO_API_KEY:Y.apiKey,RUNTIME_AUTH_SECRET:_2($.projectId),WEBHOOK_TOKEN:_6($.projectId)};if(Y.projectDir)Z.PROJECT_DIR=Y.projectDir,Z.WORKSPACE_DIR=Y.projectDir;if(this.shouldDeferToCloudSync($.projectId))Z.SHOGO_CLOUD_SYNC="1";if(Y.aiProxyUrl)Z.AI_PROXY_URL=Y.aiProxyUrl;if(Y.aiProxyToken)Z.AI_PROXY_TOKEN=Y.aiProxyToken;if(Y.techStackId)Z.TECH_STACK_ID=Y.techStackId;if(Y.name)Z.AGENT_NAME=Y.name;if(Y.workspaceId)Z.WORKSPACE_ID=Y.workspaceId;if(!Z.TREE_SITTER_WASM_DIR)Z.TREE_SITTER_WASM_DIR=Z0(O6(X),"tree-sitter-wasm");if(Y.extraEnv)Object.assign(Z,Y.extraEnv);let z=this.resolveLimits();if(z)G2(Z,z.memoryMB);return Z}resolveCwd($){let X=$.spawnConfig;if(X.projectDir&&v0(X.projectDir))return X.projectDir;let Y=this.opts.runtimeWorkDir??Z0(W6(),"shogo-runtime",$.projectId);return N2(Y,{recursive:!0}),Y}handleExit($,X,Y){let Z=Y===null&&X===0;if(this.log.log(`[WorkerRuntimeManager] runtime ${$.projectId} exited (code=${X}, signal=${Y})`),$.proc=null,$.graceTimer)clearTimeout($.graceTimer),$.graceTimer=null;if($.limitWatchdog)$.limitWatchdog.stop(),$.limitWatchdog=null;if($.status==="stopping"||this.stopped){$.status="stopped",$.pid=null,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0;return}if(Z){$.status="stopped",$.pid=null,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,this.runtimes.delete($.projectId);return}this.killProcessGroup($,"SIGKILL"),$.pid=null;let z=Date.now(),K=z-$.lastFailureAt<=F2;if($.consecutiveFailures=K?$.consecutiveFailures+1:1,$.lastFailureAt=z,$.restarts+=1,$.lastError=`exited code=${X} signal=${Y}`,$.consecutiveFailures>=B2){if($.status="failed",$.lastError=`Circuit breaker tripped: ${$.consecutiveFailures} consecutive non-clean exits within ${Math.round(F2/1000)}s (last: code=${X} signal=${Y}). Most recent on macOS is jetsam OOM (signal=SIGKILL with code=null); the previous incarnation's vite/tsserver/preview-manager children were reaped to prevent further RSS growth. Stop, fix the workspace, and call resetFailure(projectId) (or stop(projectId)) to allow another spawn attempt.`,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,$.restartTimer)clearTimeout($.restartTimer),$.restartTimer=null;if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;this.log.error(`[WorkerRuntimeManager] ${$.lastError}`);return}let J=this.restartBackoffMs($.restarts);if($.status="restarting",this.log.warn(`[WorkerRuntimeManager] restarting ${$.projectId} in ${Math.round(J/1000)}s (restart #${$.restarts}, consecutive failures ${$.consecutiveFailures}/${B2})`),$.restartTimer)clearTimeout($.restartTimer);$.restartTimer=setTimeout(()=>{if($.restartTimer=null,$.status==="failed"||this.stopped)return;$.startPromise=this.doStart($).then((V)=>{return $.startPromise=null,V}).catch((V)=>{return $.startPromise=null,this.log.error(`[WorkerRuntimeManager] restart of ${$.projectId} failed: ${V?.message??V}`),$})},J);try{$.restartTimer.unref?.()}catch{}}restartBackoffMs($){let X=Math.min(L6*Math.pow(2,Math.max(0,$-1)),C6),Y=X*0.2*Math.random();return X+Y}armIdleTimer($){if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;let X=this.opts.idleMs??k6;if(!Number.isFinite(X)||X<=0)return;$.idleTimer=setTimeout(()=>{let Y=Date.now()-$.lastUsedAt;if(Y<X){this.armIdleTimer($);return}this.log.log(`[WorkerRuntimeManager] idle-evicting ${$.projectId} after ${Math.round(Y/1000)}s`),this.stop($.projectId).catch((Z)=>{this.log.warn(`[WorkerRuntimeManager] idle stop failed: ${Z?.message??Z}`)})},X)}async allocatePort(){let $=m0-j0,X=Math.min($,50);for(let Y=0;Y<X;Y++){let Z=j0+Math.floor(Math.random()*$);if(Z+L0-1>m0)continue;let z=!0;for(let V=0;V<L0;V++)if(this.usedPorts.has(Z+V)){z=!1;break}if(!z)continue;let K=await this.isPortListening(Z),J=await this.isPortListening(Z+U2);if(K||J)continue;for(let V=0;V<L0;V++)this.usedPorts.add(Z+V);return Z}throw Error(`Cannot allocate port in range ${j0}-${m0} after ${X} attempts`)}releasePort($){if(!$)return;for(let X=0;X<L0;X++)this.usedPorts.delete($+X)}async isPortListening($){let X=new AbortController,Y=setTimeout(()=>X.abort(),250);try{return await fetch(`http://127.0.0.1:${$}/`,{method:"HEAD",signal:X.signal}),clearTimeout(Y),!0}catch{return clearTimeout(Y),!1}}tcpProbe($){return new Promise((X)=>{let Y=!1,Z=(K)=>{if(Y)return;Y=!0;try{z.destroy()}catch{}X(K)},z=H6({host:"127.0.0.1",port:$});z.setTimeout(F6),z.once("connect",()=>Z(!0)),z.once("error",()=>Z(!1)),z.once("timeout",()=>Z(!1))})}async waitForHealth($,X){let{agentPort:Y,proc:Z}=$;if(!Z)throw Error(`waitForHealth: slot ${$.projectId} has no spawned process`);let z=Date.now(),K=z+X,J=null,V=!1,Q=0,q=0,H=0,W=0,O=z;while(Date.now()<K){if(W++,Z.exitCode!==null||Z.signalCode!=null||Z.killed)throw Error(`agent-runtime exited (code=${Z.exitCode}, signal=${Z.signalCode}) before becoming healthy on port ${Y}`);q++;let k=new AbortController,C=setTimeout(()=>k.abort(),1500);try{let G=await fetch(`http://127.0.0.1:${Y}/health`,{method:"GET",signal:k.signal});if(clearTimeout(C),G.ok){this.log.log(`[WorkerRuntimeManager] /health ready for ${$.projectId} on port ${Y} (HTTP ${G.status} after ${Date.now()-z}ms, ${W} iter, ${q} http)`);return}J=`HTTP /health returned ${G.status}`}catch(G){clearTimeout(C);let T=G?.name??"Error",c=G?.code??G?.cause?.code;J=`HTTP /health failed: ${T}${c?`(${c})`:""}: ${G?.message??G}`}if(H++,V=await this.tcpProbe(Y),V){Q=Date.now();let G=Date.now()-$.lastStdoutAt;if(G>=T2)throw Error(`agent-runtime wedged on port ${Y}: TCP listening but stdout silent for ${G}ms (> ${T2}ms window); ${q} /health attempts, last error: ${J??"n/a"}`)}let N=Date.now();if(N-O>=B6){let G=N-z,T=N-$.lastStdoutAt,c=Q>0?N-Q:null;this.log.log(`[WorkerRuntimeManager] still waiting for /health on ${$.projectId} port ${Y} (${(G/1000).toFixed(1)}s elapsed, tcpListening=${V}${c!=null?`(${c}ms ago)`:""}, lastStdout=${T}ms ago, ${q} http, ${H} tcp, lastError=${J??"n/a"})`),O=N}await new Promise((G)=>setTimeout(G,N6))}if(Z.exitCode!==null||Z.signalCode!=null||Z.killed)throw Error(`agent-runtime exited (code=${Z.exitCode}, signal=${Z.signalCode}) before becoming healthy on port ${Y}`);throw Error(`Timeout waiting for agent-runtime /health on port ${Y} after ${W} iter (httpAttempts=${q}, tcpAttempts=${H}, tcpListening=${V}, lastError=${J??"n/a"})`)}async waitForExit($,X){if($.exitCode!==null||$.signalCode!=null||$.killed)return;await new Promise((Y)=>{let Z=setTimeout(()=>{try{$.kill("SIGKILL")}catch{}Y()},X);$.once("exit",()=>{clearTimeout(Z),Y()})})}snapshot($){return{projectId:$.projectId,status:$.status,agentPort:$.agentPort||void 0,apiServerPort:$.apiServerPort||void 0,pid:$.proc?.pid,startedAt:$.startedAt||void 0,lastUsedAt:$.lastUsedAt,restarts:$.restarts,lastError:$.lastError}}}import{hostname as n0,platform as d0,arch as g0}from"node:os";function d(){if(typeof __SHOGO_WORKER_VERSION__==="string"&&__SHOGO_WORKER_VERSION__.length>0)return`shogo-cli/${__SHOGO_WORKER_VERSION__}`;return"shogo-cli/unknown"}class E2 extends Error{code="TUNNEL_WS_HEADERS_UNSUPPORTED";constructor(){super("Tunnel WebSocket auth requires a runtime with WebSocket header support (Bun or Node >= 21). Current runtime does not support WebSocket constructor headers.");this.name="TunnelWebSocketHeaderSupportError"}}var z0=60,C0=300,i0=3,y6=i0,D6=1800000,M6=25000,S2=1000,R2=60000,P2=3;class p0{opts;log;pollTimer=null;ws=null;wsIdleTimer=null;heartbeatTimer=null;stopped=!1;currentPollInterval=z0;wsReconnectAttempt=0;lastHeartbeatError=null;consecutiveAuthFailures=0;consecutiveAuthSuccesses=0;serverPublishedWsUrl=null;activeAbortControllers=new Map;constructor($){this.opts=$,this.log=$.logger??console}start(){if(!this.opts.apiKey){this.log.log("[WorkerTunnel] No API key set, skipping tunnel");return}this.stopped=!1,this.wsReconnectAttempt=0,this.currentPollInterval=z0,this.log.log("[WorkerTunnel] Starting heartbeat polling to Shogo Cloud..."),this.heartbeatLoop()}stop(){if(this.stopped=!0,this.pollTimer)clearTimeout(this.pollTimer),this.pollTimer=null;this.cleanupWs(),this.log.log("[WorkerTunnel] Tunnel stopped")}isConnected(){if(this.ws!==null&&this.ws.readyState===WebSocket.OPEN)return!0;return!this.stopped&&!!this.opts.apiKey&&this.lastHeartbeatError===null&&this.pollTimer!==null}getCloudUrl(){return this.opts.cloudUrl.replace(/\/$/,"")}getWsBaseUrl(){let $=(this.opts.wsUrlOverride||process.env.SHOGO_TUNNEL_WS_URL||"").trim();if($)return $.replace(/\/$/,"");if(this.serverPublishedWsUrl)return this.serverPublishedWsUrl.replace(/\/$/,"");return this.getCloudUrl().replace(/^http/,"ws")}buildWsUrl(){return`${this.getWsBaseUrl()}/api/instances/ws`}supportsWebSocketConstructorHeaders($=globalThis){if(typeof $.Bun<"u"||typeof $.process?.versions?.bun==="string")return!0;let X=$.process?.versions?.node;if(X){if(parseInt(X.split(".")[0]??"0",10)>=21)return!0}return!1}createTunnelWebSocket($,X,Y=globalThis){if(!this.supportsWebSocketConstructorHeaders(Y))throw new E2;return new WebSocket($,X)}getReconnectDelayMs(){let $=Math.min(S2*Math.pow(2,this.wsReconnectAttempt),R2),X=$*0.2*Math.random();return $+X}async collectMetadata(){let $={hostname:n0(),os:d0(),arch:g0(),uptime:process.uptime(),protocolVersion:P2,tunnelStatus:this.ws?.readyState===WebSocket.OPEN?"connected":"polling",kind:this.opts.kind??"cli-worker"};try{let X=this.opts.resolver.getActiveProjects();$.activeProjects=X.length,$.projects=X.map((Y)=>{let Z=this.opts.resolver.status(Y);return{projectId:Y,status:Z?.status??"unknown",agentPort:Z?.agentPort}})}catch{$.activeProjects=0}return $}async sendHeartbeat(){let $=this.getCloudUrl(),X=await this.collectMetadata(),Y=n0(),Z=this.opts.name??Y,z=await fetch(`${$}/api/instances/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":d(),"x-api-key":this.opts.apiKey,"x-shogo-kind":this.opts.kind??"cli-worker"},body:JSON.stringify({hostname:Y,name:Z,os:d0(),arch:g0(),kind:this.opts.kind??"cli-worker",metadata:X})});if(!z.ok)throw Error(`Heartbeat failed: HTTP ${z.status}`);let K=await z.json();if(typeof K.wsUrl==="string"&&K.wsUrl.length>0){if(K.wsUrl!==this.serverPublishedWsUrl)this.log.log(`[WorkerTunnel] Cloud advertised tunnel WS URL: ${K.wsUrl}`);this.serverPublishedWsUrl=K.wsUrl}return K}scheduleNextPoll($){if(this.stopped)return;if(this.pollTimer)clearTimeout(this.pollTimer);let X=($??this.currentPollInterval)*1000;this.pollTimer=setTimeout(()=>void this.heartbeatLoop(),X)}async heartbeatLoop(){if(this.stopped)return;if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.scheduleNextPoll(this.currentPollInterval);return}try{let $=await this.sendHeartbeat(),X=$.nextPollIn||z0;if(this.consecutiveAuthFailures>=i0){if(this.consecutiveAuthSuccesses++,this.consecutiveAuthSuccesses<y6){this.currentPollInterval=C0,this.scheduleNextPoll();return}}if(this.currentPollInterval=X,this.lastHeartbeatError)this.log.log("[WorkerTunnel] Heartbeat recovered"),this.lastHeartbeatError=null;if(this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0,$.wsRequested&&!this.ws){this.log.log("[WorkerTunnel] Cloud requested WebSocket — connecting..."),this.connectWs();return}}catch($){let X=$?.message??String($);if(/HTTP 40[13]\b/.test(X))this.consecutiveAuthFailures++,this.consecutiveAuthSuccesses=0;else this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0;if(X!==this.lastHeartbeatError)this.log.error(`[WorkerTunnel] Heartbeat error: ${X}`),this.lastHeartbeatError=X;if(this.consecutiveAuthFailures>=i0){let Z=`tunnel saw ${this.consecutiveAuthFailures} consecutive auth failures from Shogo Cloud`;try{this.opts.onAuthRevoked?.(Z)}catch(z){this.log.warn(`[WorkerTunnel] onAuthRevoked threw: ${z?.message??z}`)}if(this.currentPollInterval!==C0)this.log.warn(`[WorkerTunnel] ${this.consecutiveAuthFailures} consecutive auth failures — `+`backing off to ${C0}s. Re-authenticate with \`shogo login\`.`);this.currentPollInterval=C0}else this.currentPollInterval=z0}this.scheduleNextPoll()}async handleRequest($){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;this.resetWsIdleTimer();let X=new AbortController;this.activeAbortControllers.set($.requestId,X);try{let Y=await this.resolveLocalUrl($.path,$.projectId);if(!Y){let J=this.opts.resolver.describeRejection?this.opts.resolver.describeRejection($.path,$.projectId):{code:"NO_LOCAL_RUNTIME",message:`no local runtime available for path: ${$.path}`};this.sendFrame({type:"response",requestId:$.requestId,status:502,headers:{"content-type":"application/json"},body:JSON.stringify({code:J.code,message:J.message,path:$.path})});return}let Z={...$.headers??{}};if($.projectId&&($.path.startsWith("/agent/")||$.path==="/agent")){let J=this.opts.resolver.deriveRuntimeToken($.projectId);if(J)Z["x-runtime-token"]=J}let z={method:$.method,headers:Z,signal:X.signal};if($.body&&$.method!=="GET"&&$.method!=="HEAD")z.body=$.body;let K=await fetch(Y,z);if($.stream){let J=K.body?.getReader();if(!J){this.sendFrame({type:"stream-error",requestId:$.requestId,error:"No response body for stream"});return}let V=new TextDecoder;try{while(!0){let{done:Q,value:q}=await J.read();if(Q)break;if(this.ws?.readyState!==WebSocket.OPEN)break;this.sendFrame({type:"stream-chunk",requestId:$.requestId,data:V.decode(q,{stream:!0})})}if(this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-end",requestId:$.requestId})}catch(Q){if(Q?.name!=="AbortError"&&this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-error",requestId:$.requestId,error:Q?.message??String(Q)})}}else{let J=await K.text(),V={};K.headers.forEach((Q,q)=>{V[q]=Q}),this.sendFrame({type:"response",requestId:$.requestId,status:K.status,headers:V,body:J})}}catch(Y){if(Y?.name==="AbortError")return;if(this.ws?.readyState!==WebSocket.OPEN)return;let Z=$.stream?{type:"stream-error",requestId:$.requestId,error:Y?.message??String(Y)}:{type:"response",requestId:$.requestId,status:502,body:JSON.stringify({error:Y?.message??String(Y)})};this.sendFrame(Z)}finally{this.activeAbortControllers.delete($.requestId)}}async resolveLocalUrl($,X){return this.opts.resolver.resolveLocalUrl($,X)}sendFrame($){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{this.ws.send(JSON.stringify($))}catch(X){this.log.warn(`[WorkerTunnel] Frame send failed: ${X?.message??X}`)}}resetWsIdleTimer(){if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer);this.wsIdleTimer=setTimeout(()=>{if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.log.log("[WorkerTunnel] WebSocket idle timeout — closing, returning to polling");try{this.ws.close(1000,"Idle timeout")}catch{}}},D6)}startWsHeartbeat(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.heartbeatTimer=setInterval(async()=>{if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{let $=await this.collectMetadata();this.sendFrame({type:"heartbeat",metadata:$})}catch{}},M6)}connectWs(){if(this.stopped||this.ws)return;let $=this.buildWsUrl(),X=n0(),Y=d0(),Z=g0(),z=this.opts.name??X;this.log.log(`[WorkerTunnel] Opening WebSocket to ${$} (hostname=${X})`);let K={headers:{Authorization:`Bearer ${this.opts.apiKey}`,"User-Agent":d(),"x-shogo-hostname":X,"x-shogo-name":z,"x-shogo-os":Y,"x-shogo-arch":Z,"x-shogo-kind":this.opts.kind??"cli-worker"}},J;try{J=this.createTunnelWebSocket($,K)}catch(V){this.log.error(`[WorkerTunnel] WebSocket creation failed: ${V?.message??V}`),this.ws=null,this.scheduleNextPoll(5);return}this.ws=J,J.onopen=()=>{this.log.log("[WorkerTunnel] WebSocket connected — session active"),this.wsReconnectAttempt=0,this.startWsHeartbeat(),this.resetWsIdleTimer()},J.onmessage=(V)=>{let Q;try{let q=typeof V.data==="string"?V.data:V.data.toString();Q=JSON.parse(q)}catch{return}if(Q.type==="ping"){this.sendFrame({type:"pong"}),this.resetWsIdleTimer();return}if(Q.type==="cancel"){let q=this.activeAbortControllers.get(Q.requestId);if(q)q.abort();return}if(Q.type==="request"){this.handleRequest(Q).catch((q)=>{this.log.error(`[WorkerTunnel] Error handling request: ${q?.message??q}`)});return}},J.onclose=(V)=>{if(this.log.log(`[WorkerTunnel] WebSocket closed: code=${V.code} reason=${V.reason||"none"}`),this.cleanupWs(),this.stopped)return;if(V.code===1000||V.code===4000)this.scheduleNextPoll(this.currentPollInterval);else{this.wsReconnectAttempt++;let Q=this.getReconnectDelayMs();this.log.log(`[WorkerTunnel] Reconnecting in ${Math.round(Q/1000)}s (attempt ${this.wsReconnectAttempt})`),this.scheduleNextPoll(Math.ceil(Q/1000))}},J.onerror=(V)=>{this.log.error("[WorkerTunnel] WebSocket error:",V?.message??"unknown")}}cleanupWs(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer),this.wsIdleTimer=null;for(let[,$]of this.activeAbortControllers)try{$.abort()}catch{}this.activeAbortControllers.clear(),this.ws=null}_testing(){let $=this;return{sendHeartbeat:()=>$.sendHeartbeat(),heartbeatLoop:()=>$.heartbeatLoop(),connectWs:()=>$.connectWs(),cleanupWs:()=>$.cleanupWs(),handleRequest:(X)=>$.handleRequest(X),installFakeWs:(X)=>{$.ws=X},getCloudUrl:()=>$.getCloudUrl(),getWsBaseUrl:()=>$.getWsBaseUrl(),buildWsUrl:()=>$.buildWsUrl(),getReconnectDelayMs:()=>$.getReconnectDelayMs(),supportsWebSocketConstructorHeaders:(X)=>$.supportsWebSocketConstructorHeaders(X),createTunnelWebSocket:(X,Y,Z)=>$.createTunnelWebSocket(X,Y,Z),DEFAULT_POLL_INTERVAL_S:z0,BACKOFF_BASE_MS:S2,BACKOFF_MAX_MS:R2,TUNNEL_PROTOCOL_VERSION:P2,get currentPollInterval(){return $.currentPollInterval},set currentPollInterval(X){$.currentPollInterval=X},get wsReconnectAttempt(){return $.wsReconnectAttempt},set wsReconnectAttempt(X){$.wsReconnectAttempt=X},get ws(){return $.ws},get stopped(){return $.stopped},get serverPublishedWsUrl(){return $.serverPublishedWsUrl},set serverPublishedWsUrl(X){$.serverPublishedWsUrl=X}}}}async function b2($){let X=P({name:$.name,workerDir:$.workerDir,apiKey:$.apiKey,cloudUrl:$.cloudUrl,port:$.port?parseInt($.port,10):void 0,projectsDir:$.projectsDir}),Y=z2($.proxy);if(!$.foreground)return S6({cfg:X,proxy:Y,flags:$});let Z=u({flag:$.runtimeBin});if(!Z)console.error(w.red(n({flag:$.runtimeBin}))),process.exit(1);if(M0(process.env,Y),$.debug){if(!await q2(V2({cloudUrl:X.cloudUrl,apiKey:X.apiKey,workerDir:X.workerDir,proxy:Y})))process.exit(1)}let z=!$.noAutoPull,K=!$.noGit;if(console.log(w.bold(`
|
|
15
|
+
Shogo Worker — Starting`)),console.log(w.dim(" name ")+X.name),console.log(w.dim(" worker-dir ")+X.workerDir),console.log(w.dim(" cloud ")+X.cloudUrl),console.log(w.dim(" runtime ")+`${Z.path} ${w.dim(`(via ${Z.source})`)}`),console.log(w.dim(" auto-pull ")+(z?w.green("on")+w.dim(` → ${X.projectsDir}`):w.yellow("off"))),console.log(w.dim(" sync mode ")+(K?w.green("git")+w.dim(" (falls back to file transport if git is missing)"):w.yellow("files-only"))),$.project)console.log(w.dim(" project ")+$.project);if(Y)console.log(w.dim(" proxy ")+`${Y.url} ${w.dim(`(from ${Y.source})`)}`);console.log("");let J=`${X.cloudUrl.replace(/\/+$/,"")}/api/ai/v1`,V={cloudUrl:X.cloudUrl,apiKey:X.apiKey,aiProxyUrl:J,aiProxyToken:X.apiKey},Q=new f0({runtimeBin:$.runtimeBin,defaultSpawnConfig:V,autoPull:{enabled:z,projectsDir:X.projectsDir,watch:!0,useGit:K}});if(!Q.resolveBinary())console.error(w.red(n({flag:$.runtimeBin}))),process.exit(1);let q=new p0({apiKey:X.apiKey,cloudUrl:X.cloudUrl,name:X.name,kind:"cli-worker",resolver:Q,onAuthRevoked:(O)=>{console.error(w.red(`✗ Cloud auth revoked: ${O}`)),console.error(w.dim(" Run `shogo login` to re-authenticate; this worker will keep polling at the auth-failure backoff until then."))}}),H=!1,W=async(O)=>{if(H)return;H=!0,console.log(w.dim(`
|
|
16
|
+
Received ${O} — shutting down...`));try{q.stop()}catch{}try{await Q.stopAll()}catch(k){console.warn(w.yellow(`stopAll: ${k?.message??k}`))}process.exit(0)};process.once("SIGINT",()=>void W("SIGINT")),process.once("SIGTERM",()=>void W("SIGTERM")),process.once("SIGHUP",()=>void W("SIGHUP")),q.start(),console.log(w.green("✓ Worker running. Ctrl-C to stop.")),await new Promise(()=>{})}function S6({cfg:$,proxy:X,flags:Y}){let{entry:Z,runner:z}=P6(),K=M0({...process.env,SHOGO_API_KEY:$.apiKey,SHOGO_CLOUD_URL:$.cloudUrl,SHOGO_INSTANCE_NAME:$.name,SHOGO_WORKER_DIR:$.workerDir,SHOGO_LOCAL_MODE:"true",PORT:String($.port)},X),J=R6(Y),{pid:V}=X2({entry:Z,runner:z,env:{...K,SHOGO_DETACHED_ARGS:J.join(" ")},cwd:$.workerDir,detach:!0,inheritStdio:!1});console.log(w.bold(`
|
|
17
|
+
Shogo Worker — Started`)),console.log(w.dim(" pid: ")+V),console.log(w.dim(" name: ")+$.name),console.log(w.dim(" logs: ")+"~/.shogo/logs/worker.log"),console.log(w.dim(" stop: ")+"shogo worker stop")}function R6($){let X=["worker","start","--foreground"];if($.name)X.push("--name",$.name);if($.workerDir)X.push("--worker-dir",$.workerDir);if($.apiKey)X.push("--api-key",$.apiKey);if($.cloudUrl)X.push("--cloud-url",$.cloudUrl);if($.port)X.push("--port",$.port);if($.proxy)X.push("--proxy",$.proxy);if($.project)X.push("--project",$.project);if($.runtimeBin)X.push("--runtime-bin",$.runtimeBin);if($.debug)X.push("--debug");if($.noAutoPull)X.push("--no-auto-pull");if($.projectsDir)X.push("--projects-dir",$.projectsDir);if($.noGit)X.push("--no-git");return X}function P6(){let $=process.execPath,X=/\bbun(?:-[^/\\]*)?$/.test($)||typeof globalThis.Bun<"u",Y=process.argv[1];if(Y&&x6(Y)){let z=/\.ts$/.test(Y);return{entry:Y,runner:X?"bun":z?"tsx":"node"}}return{entry:new URL("../../bin/shogo.mjs",import.meta.url).pathname,runner:X?"bun":"node"}}import v2 from"picocolors";async function h2(){let{killedPid:$}=Y2("SIGTERM");if($===null){console.log(v2.dim("No worker running."));return}console.log(v2.green(`✓ Worker stopped (pid=${$}).`))}import v from"picocolors";async function j2(){let $=W0();if(!$){console.log(v.yellow("● stopped")+v.dim(" (no pid file)"));return}if(!O0($)){console.log(v.red("● dead")+v.dim(` (stale pid ${$})`));return}let X=I();if(console.log(v.green("● running")+v.dim(` (pid ${$})`)),X.name)console.log(v.dim(" name: ")+X.name);if(X.cloudUrl)console.log(v.dim(" cloud: ")+X.cloudUrl);if(X.workerDir)console.log(v.dim(" dir: ")+X.workerDir)}import{spawn as E6}from"node:child_process";import{existsSync as b6,statSync as v6}from"node:fs";import m2 from"picocolors";async function I2($){let X=$.err?Q0:q0;if(!b6(X)){console.log(m2.dim("No logs yet."));return}let Y=$.follow?["-F","-n","200",X]:["-n","200",X];if(E6("tail",Y,{stdio:"inherit"}).on("exit",(z)=>process.exit(z??0)),!$.follow){if(v6(X).size===0)console.log(m2.dim("(log file is empty)"))}}import y from"picocolors";import{spawn as h6}from"node:child_process";import{hostname as j6,platform as m6,arch as I6}from"node:os";import f from"picocolors";class x extends Error{kind;constructor($,X){super($);this.kind=X;this.name="CloudLoginError"}}async function u2($){let X=$.cloudUrl.replace(/\/$/,""),Y=$.client??"cli",Z=$.deviceName??j6(),z=$.devicePlatform??`${m6()}-${I6()}`,K=$.appVersion??n6(),J=$.log??((G)=>console.log(G)),V=$.fetchImpl??fetch,Q=await V(`${X}/api/cli/login/start`,{method:"POST",headers:{"content-type":"application/json","user-agent":d()},body:JSON.stringify({deviceId:$.deviceId,deviceName:Z,devicePlatform:z,deviceAppVersion:K,workspaceId:$.workspaceId,client:Y}),signal:AbortSignal.timeout(1e4)}).catch((G)=>{throw new x(`Cannot reach Shogo Cloud at ${X}: ${G?.message??G}`,"transport")});if(!Q.ok){let G=await Q.text().catch(()=>"");throw new x(`Cloud rejected /api/cli/login/start (HTTP ${Q.status}): ${G||"no body"}`,"transport")}let q=await Q.json().catch(()=>({}));if(!q?.ok||!q.state||!q.authUrl)throw new x(`Cloud returned a malformed start response: ${q?.error??JSON.stringify(q)}`,"transport");let H=u6($.pollIntervalMs??q.pollIntervalMs),W=$.timeoutMs??q.expiresInMs,O=Date.now()+W;if(J(""),J(f.bold("Sign in to Shogo Cloud")),J(f.dim(" cloud: ")+X),J(f.dim(" device: ")+`${Z} (${z})`),J(f.dim(" user code: ")+f.cyan(q.userCode)),J(""),J(" Open this URL in your browser to approve:"),J(" "+f.cyan(q.authUrl)),J(""),$.openBrowser!==!1)if(typeof $.openBrowser==="function")try{await $.openBrowser(q.authUrl)}catch{}else d6(q.authUrl).catch(()=>{});J(f.dim("Waiting for approval..."));let k=$.installSignalHandlers??!0,C=()=>{N.abort()},N=new AbortController;if($.abortSignal)if($.abortSignal.aborted)N.abort();else $.abortSignal.addEventListener("abort",()=>N.abort(),{once:!0});if(k)process.once("SIGINT",C),process.once("SIGTERM",C);try{while(!0){if(N.signal.aborted)throw new x("Aborted by caller.","cancelled");if(Date.now()>=O)throw new x(`Timed out after ${Math.round(W/1000)}s waiting for approval.`,"timeout");let G=await V(`${X}/api/cli/login/poll?state=${encodeURIComponent(q.state)}`,{method:"GET",headers:{accept:"application/json","user-agent":d()},signal:AbortSignal.timeout(1e4)}).catch((T)=>{return J(f.dim(` (poll error: ${T?.message??T} — retrying)`)),null});if(G&&G.ok){let T=await G.json().catch(()=>({}));if(T?.status==="approved"&&T.key)return{key:T.key,email:T.email??null,workspace:T.workspace??null,deviceId:T.deviceId??$.deviceId};if(T?.status==="denied")throw new x("Sign-in was denied in the browser.","denied");if(T?.status==="expired")throw new x("Sign-in request expired before it was approved.","expired")}await f6(H,N.signal)}}finally{if(k)process.removeListener("SIGINT",C),process.removeListener("SIGTERM",C)}}function u6($){if(!Number.isFinite($)||$<=0)return 2000;return Math.min(Math.max($,1000),1e4)}function f6($,X){return new Promise((Y,Z)=>{if(X?.aborted)return Z(new x("Aborted by caller.","cancelled"));let z=setTimeout(()=>{X?.removeEventListener("abort",K),Y()},$),K=()=>{clearTimeout(z),Z(new x("Aborted by caller.","cancelled"))};X?.addEventListener("abort",K,{once:!0})})}function n6(){return d()}function d6($){return new Promise((X)=>{let Y=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",Z=process.platform==="win32"?["/c","start",'""',$]:[$];try{let z=h6(Y,Z,{stdio:"ignore",detached:!0});z.on("error",()=>X()),z.unref(),X()}catch{X()}})}import{readFileSync as g6,writeFileSync as i6,existsSync as p6,chmodSync as c6}from"node:fs";import{randomUUID as o6}from"node:crypto";function f2(){if(p6(o)){let X=g6(o,"utf-8").trim();if(X)return X}m();let $=o6();return i6(o,$,{mode:384}),c6(o,384),$}var a6="https://studio.shogo.ai";async function n2($){let X=I(),Y=($.cloudUrl||X.cloudUrl||a6).replace(/\/$/,""),Z=$.apiKey||process.env.SHOGO_API_KEY;if(Z){await l6({key:Z,cloudUrl:Y,cfg:X,name:$.name});return}let z=f2(),K;try{K=await u2({cloudUrl:Y,deviceId:z,deviceName:$.name,workspaceId:$.workspace,openBrowser:!$.noBrowser})}catch(J){if(J instanceof x){if(console.error(y.red(`✗ ${J.message}`)),J.kind==="transport")console.error(y.dim(" If your network blocks browsers, run with --api-key <key> instead."));process.exit(1)}throw J}if(X.apiKey=K.key,X.cloudUrl=Y,$.name)X.name=$.name;if(e(X),console.log(""),console.log(y.green("✓ Signed in to Shogo Cloud")),K.workspace)console.log(y.dim(" workspace: ")+K.workspace);if(K.email)console.log(y.dim(" email: ")+K.email);console.log(y.dim(" saved to: ")+"~/.shogo/config.json"),console.log(""),console.log(y.dim(" next: ")+"shogo worker start")}async function l6({key:$,cloudUrl:X,cfg:Y,name:Z}){if(!/^shogo_sk_/.test($))console.error(y.red('✗ API key should start with "shogo_sk_". Copy it verbatim from the API Keys page.')),process.exit(1);let z;try{let K=await fetch(`${X}/api/api-keys/validate`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({key:$}),signal:AbortSignal.timeout(1e4)});if(z=await K.json().catch(()=>({})),!K.ok||!z?.valid)console.error(y.red(`✗ ${z?.error||`Cloud rejected the key (HTTP ${K.status}).`}`)),process.exit(1)}catch(K){console.error(y.red(`✗ Cannot reach Shogo Cloud at ${X}: ${K?.message??K}`)),process.exit(1)}if(Y.apiKey=$,Y.cloudUrl=X,Z)Y.name=Z;if(e(Y),console.log(y.green("✓ API key saved to ~/.shogo/config.json")),z.workspace?.name)console.log(y.dim(" workspace: ")+z.workspace.name);if(z.user?.email)console.log(y.dim(" email: ")+z.user.email);console.log(y.dim(" next: ")+"shogo worker start")}import K0 from"picocolors";async function d2(){let $=I();if(Object.keys($).length===0){console.log(K0.dim("(no config — run `shogo login` or `shogo config set`)"));return}let X={...$,apiKey:$.apiKey?`***${$.apiKey.slice(-4)}`:void 0};console.log(K0.dim(`file: ${j}`)),console.log(JSON.stringify(X,null,2))}async function g2($,X){let Y=["apiKey","cloudUrl","name","workerDir","port"];if(!Y.includes($))console.error(K0.red(`Unknown key: ${$}`)),console.error(K0.dim(`Allowed: ${Y.join(", ")}`)),process.exit(1);let Z=I();Z[$]=$==="port"?parseInt(X,10):X,e(Z),console.log(K0.green(`✓ ${$} set`))}import U from"picocolors";import{spawn as r6}from"node:child_process";import{createHash as e6}from"node:crypto";import{createWriteStream as s6,existsSync as J0,mkdirSync as c0,readFileSync as c2,renameSync as i2,rmSync as o0,writeFileSync as t6}from"node:fs";import{tmpdir as $1}from"node:os";import{dirname as X1,join as A0}from"node:path";import{Readable as Y1}from"node:stream";import{pipeline as Z1}from"node:stream/promises";var z1="https://github.com/shogo-labs/shogo-ai/releases/download";function o2(){let{platform:$,arch:X}=process,Y;if($==="darwin")Y="darwin";else if($==="linux")Y="linux";else if($==="win32")Y="windows";else throw Error(`Unsupported platform: ${$}`);let Z;if(X==="arm64")Z="arm64";else if(X==="x64")Z="x64";else throw Error(`Unsupported arch: ${X} (need arm64 or x64)`);return`${Y}-${Z}`}function N0(){if(!J0(r))return null;try{let $=c2(r,"utf-8");return JSON.parse($)}catch{return null}}async function K1($,X){let Y=X.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/releases\/download/);if(!Y)throw Error(`Cannot auto-resolve latest version from non-GitHub baseUrl '${X}'. Pass --version explicitly.`);let Z=Y[1],z=Y[2];if($==="stable"){let H=`https://api.github.com/repos/${Z}/${z}/releases/latest`,W=await fetch(H,{headers:{Accept:"application/vnd.github+json"}});if(!W.ok)throw Error(`GitHub API ${W.status} for ${H}`);let O=await W.json();if(!O.tag_name)throw Error("GitHub API did not return tag_name");return p2(O.tag_name)}let K=`https://api.github.com/repos/${Z}/${z}/releases?per_page=30`,J=await fetch(K,{headers:{Accept:"application/vnd.github+json"}});if(!J.ok)throw Error(`GitHub API ${J.status} for ${K}`);let V=await J.json(),Q=$==="beta"?"-beta":"-nightly",q=V.find((H)=>H.prerelease&&/^v\d+\.\d+\.\d+-/.test(H.tag_name)&&H.tag_name.includes(Q));if(!q)throw Error(`No ${$} runtime release found`);return p2(q.tag_name)}function p2($){if(!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test($))throw Error(`Unexpected app tag '${$}' (expected vX.Y.Z[-prerelease])`);return $.slice(1)}function J1($,X,Y){let Z=`shogo-agent-runtime-${X}.tar.gz`,z=`v${$}`,K=`${Y.replace(/\/$/,"")}/${z}/${Z}`;return{tarball:K,sha256:`${K}.sha256`,assetName:Z}}async function V1($,X){let Y=await fetch($,{redirect:"follow"});if(!Y.ok)throw Error(`Download failed: HTTP ${Y.status} for ${$}`);if(!Y.body)throw Error(`Download failed: empty body for ${$}`);await Z1(Y1.fromWeb(Y.body),s6(X))}async function q1($){let X=await fetch($,{redirect:"follow"});if(!X.ok)throw Error(`SHA256 sidecar fetch failed: HTTP ${X.status} for ${$}`);let Z=(await X.text()).trim().split(/\s+/)[0];if(!/^[0-9a-f]{64}$/i.test(Z))throw Error(`SHA256 sidecar at ${$} did not contain a 64-char hex digest`);return Z.toLowerCase()}function Q1($){let X=c2($);return e6("sha256").update(X).digest("hex")}async function H1($,X){await new Promise((Y,Z)=>{let z=r6("tar",["-xzf",$,"-C",X],{stdio:["ignore","pipe","pipe"]}),K="";z.stderr.on("data",(J)=>{K+=J.toString()}),z.on("error",Z),z.on("exit",(J)=>{if(J===0)Y();else Z(Error(`tar exited ${J}: ${K.trim()}`))})})}function W1($,X){e0();let Y=X1(X);if(!J0(Y))c0(Y,{recursive:!0,mode:448});let Z=`${X}.next`;if(J0(Z))o0(Z,{force:!0});i2($,Z);try{if(process.platform!=="win32"){let{chmodSync:z}=T0("node:fs");z(Z,493)}}catch{}if(J0(X))o0(X,{force:!0});i2(Z,X)}async function a2($={}){let X=$.logger??console,Y=$.channel??"stable",Z=$.baseUrl??process.env.SHOGO_RUNTIME_RELEASES_URL??z1,z=$.target??o2(),K=$.version;if(!K)X.log(`[runtime install] Resolving latest ${Y} version...`),K=await K1(Y,Z),X.log(`[runtime install] Latest ${Y} = ${K}`);let J=N0();if(J&&J.version===K&&J.target===z&&!$.force)return X.log(`[runtime install] ${K} (${z}) already installed at ${R} — pass --force to reinstall`),{version:K,target:z,binPath:R,source:J.source,sha256:J.sha256,channel:Y};let V=J1(K,z,Z);X.log(`[runtime install] Downloading ${V.tarball}`);let Q=A0($1(),`shogo-runtime-install-${process.pid}-${Date.now()}`);c0(Q,{recursive:!0});try{let q=A0(Q,V.assetName);await V1(V.tarball,q),X.log("[runtime install] Verifying SHA-256...");let H=await q1(V.sha256),W=Q1(q);if(W!==H)throw Error(`SHA-256 mismatch for ${V.assetName}
|
|
18
|
+
expected: ${H}
|
|
19
|
+
actual: ${W}`);let O=A0(Q,"extract");c0(O,{recursive:!0}),await H1(q,O);let k=A0(O,"agent-runtime");if(!J0(k))throw Error(`Tarball ${V.assetName} did not contain ./agent-runtime`);W1(k,R);let C={version:K,target:z,installedAt:new Date().toISOString(),channel:Y,source:V.tarball,sha256:W};return t6(r,JSON.stringify(C,null,2)+`
|
|
20
|
+
`,{mode:384}),X.log(`[runtime install] Installed agent-runtime ${K} (${z}) to ${R}`),{version:K,target:z,binPath:R,source:V.tarball,sha256:W,channel:Y}}finally{try{o0(Q,{recursive:!0,force:!0})}catch{}}}function l2(){return{runtimeDir:l,runtimeBin:R,versionFile:r}}async function a0($={}){let X=await a2({channel:$.channel,version:$.version,baseUrl:$.baseUrl,force:$.force});console.log(),console.log(U.green("✓"),`agent-runtime ${U.bold(X.version)} installed (${X.target})`),console.log(` ${U.dim("path: ")} ${X.binPath}`),console.log(` ${U.dim("source: ")} ${X.source}`),console.log(` ${U.dim("sha256: ")} ${X.sha256}`),console.log(` ${U.dim("channel: ")} ${X.channel}`)}function r2(){let $=N0();if(!$){console.log(U.yellow("No agent-runtime installed.")),console.log(`Run ${U.cyan("shogo runtime install")} to download the latest.`),process.exitCode=1;return}console.log(`${U.bold("agent-runtime")} ${$.version}`),console.log(` ${U.dim("target: ")} ${$.target}`),console.log(` ${U.dim("channel: ")} ${$.channel}`),console.log(` ${U.dim("installed at:")} ${$.installedAt}`),console.log(` ${U.dim("source: ")} ${$.source}`)}function e2(){let $=u(),X=l2();if(!$){console.log(U.yellow("agent-runtime binary not found on this machine.")),console.log(),console.log(U.dim("Default install path:"),X.runtimeBin),console.log(),console.log(n()),process.exitCode=1;return}if(console.log($.path),process.env.SHOGO_DEBUG||process.env.VERBOSE)console.log(U.dim(` (resolved via: ${$.source})`))}async function s2($={}){let X=N0(),Y=$.channel??X?.channel??"stable";if(X)console.log(`${U.dim("current:")} ${X.version} (${X.target}, ${X.channel})`);else console.log(U.dim("No existing install — installing fresh..."));await a0({channel:Y,baseUrl:$.baseUrl,force:!0})}import{existsSync as O1,mkdirSync as G1}from"node:fs";import{resolve as k1}from"node:path";import L from"picocolors";import{CloudFileTransport as w1}from"@shogo-ai/sdk/cloud-file-transport";async function t2($,X){if(!$)throw Error("projectId is required");let Y=P({apiKey:X.apiKey,cloudUrl:X.cloudUrl});s0(Y.projectsDir);let Z=k1(X.into??g($,Y.projectsDir));if(!O1(Z))G1(Z,{recursive:!0});let z=X.include?.split(",").map((V)=>V.trim()).filter(Boolean);if(console.log(L.bold(`
|
|
21
|
+
shogo project pull ${L.cyan($)}`)),console.log(L.dim(" cloud ")+Y.cloudUrl),console.log(L.dim(" into ")+Z),z?.length)console.log(L.dim(" include ")+z.join(", "));console.log("");let K=new w1({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z,include:z,onProgress:L1()}),J=await K.downloadAll();if(C1("Pull",J),X.watch){console.log(L.bold(`
|
|
22
|
+
Watching for local changes...`));let V=new Y0({rootDir:Z,transport:K,onFlush:({uploaded:H,errors:W})=>{let O=H.length===1?H[0]:`${H.length} files`,k=W>0?L.red(` (${W} errors)`):"";console.log(L.dim(` ↑ ${O}${k}`))}});V.start();let Q=!1,q=async(H)=>{if(Q)return;Q=!0,console.log(L.dim(`
|
|
23
|
+
Received ${H} — flushing pending uploads...`));try{await V.stop()}catch(W){console.warn(L.yellow(`watcher.stop: ${W?.message??W}`))}process.exit(0)};process.once("SIGINT",()=>void q("SIGINT")),process.once("SIGTERM",()=>void q("SIGTERM")),process.once("SIGHUP",()=>void q("SIGHUP")),await new Promise(()=>{})}}function L1(){return($)=>{let X=$.kind==="download"?"↓":$.kind==="upload"?"↑":$.kind==="delete"?"✗":"·",Y=$.total>0?` ${$.index+1}/${$.total}`:"",Z=$.bytes!=null?L.dim(` (${A1($.bytes)})`):"";console.log(L.dim(` ${X}${Y} ${$.path}${Z}`))}}function C1($,X){let Y=X.errors.length===0,Z=Y?L.green(`✓ ${$} complete`):L.red(`✗ ${$} completed with errors`);if(console.log(`
|
|
24
|
+
${Z}`),console.log(L.dim(" downloaded: ")+X.downloaded),X.uploaded)console.log(L.dim(" uploaded: ")+X.uploaded);if(X.deleted)console.log(L.dim(" deleted: ")+X.deleted);if(X.skipped)console.log(L.dim(" skipped: ")+X.skipped);if(!Y){console.log(L.dim(" errors: ")+X.errors.length);for(let z of X.errors.slice(0,5))console.log(L.dim(" ")+L.red(`${z.path}: ${z.message}`));if(X.errors.length>5)console.log(L.dim(` ... and ${X.errors.length-5} more`))}}function A1($){if($<1024)return`${$}B`;if($<1048576)return`${($/1024).toFixed(1)}KB`;return`${($/1024/1024).toFixed(2)}MB`}import{existsSync as N1}from"node:fs";import{resolve as U1}from"node:path";import B from"picocolors";import{CloudFileTransport as B1}from"@shogo-ai/sdk/cloud-file-transport";async function $$($,X){if(!$)throw Error("projectId is required");let Y=P({apiKey:X.apiKey,cloudUrl:X.cloudUrl}),Z=U1(X.from??g($,Y.projectsDir));if(!N1(Z))throw Error(`Source directory does not exist: ${Z}`);let z=X.include?.split(",").map((V)=>V.trim()).filter(Boolean);if(console.log(B.bold(`
|
|
25
|
+
shogo project push ${B.cyan($)}`)),console.log(B.dim(" cloud ")+Y.cloudUrl),console.log(B.dim(" from ")+Z),z?.length)console.log(B.dim(" include ")+z.join(", "));if(X.deleteRemote)console.log(B.yellow(" --delete-remote: remote files not present locally will be DELETED"));console.log("");let J=await new B1({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z,include:z,onProgress:(V)=>{let Q=V.kind==="upload"?"↑":V.kind==="delete"?"✗":"·",q=V.total>0?` ${V.index+1}/${V.total}`:"",H=V.bytes!=null?B.dim(` (${T1(V.bytes)})`):"";console.log(B.dim(` ${Q}${q} ${V.path}${H}`))}}).uploadAll({deleteRemote:X.deleteRemote});F1("Push",J)}function F1($,X){let Y=X.errors.length===0,Z=Y?B.green(`✓ ${$} complete`):B.red(`✗ ${$} completed with errors`);if(console.log(`
|
|
26
|
+
${Z}`),console.log(B.dim(" uploaded: ")+X.uploaded),X.deleted)console.log(B.dim(" deleted: ")+X.deleted);if(!Y){console.log(B.dim(" errors: ")+X.errors.length);for(let z of X.errors.slice(0,5))console.log(B.dim(" ")+B.red(`${z.path}: ${z.message}`));if(X.errors.length>5)console.log(B.dim(` ... and ${X.errors.length-5} more`))}}function T1($){if($<1024)return`${$}B`;if($<1048576)return`${($/1024).toFixed(1)}KB`;return`${($/1024/1024).toFixed(2)}MB`}import{existsSync as _1}from"node:fs";import{resolve as y1}from"node:path";import E from"picocolors";w0();async function X$($,X){if(!$)throw Error("projectId is required");let Y=P({apiKey:X.apiKey,cloudUrl:X.cloudUrl}),Z=y1(X.into??g($,Y.projectsDir));if(!_1(Z))throw Error(`Local project dir does not exist: ${Z} — run \`shogo project pull\` first`);if(!X0(Z))throw Error(`${Z} is not a git repo. \`shogo project checkout\` requires the git sync path; run \`shogo project pull\` after installing git, or use \`shogo project pull --include\` for file-only restore.`);if(console.log(E.bold(`
|
|
27
|
+
shogo project checkout ${E.cyan($)}`)),console.log(E.dim(" cloud ")+Y.cloudUrl),console.log(E.dim(" local ")+Z),X.at)console.log(E.dim(" at ")+X.at);if(console.log(""),X.unshallow)console.log(E.dim("Unshallowing repo (this may take a moment)...")),await k0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z});if(!X.at){let K=await $0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z});console.log(E.green(`✓ Reset to ${K.commitSha.slice(0,8)} (remote HEAD)`));return}let z;try{z=(await _(["rev-parse","--verify",`${X.at}^{commit}`],{cwd:Z})).stdout.trim()}catch{z=await D1(Y.cloudUrl,Y.apiKey,$,X.at),console.log(E.dim(` resolved checkpoint "${X.at}" → ${z.slice(0,8)}`))}try{await $0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z,branch:z})}catch{if(!X.unshallow)console.log(E.yellow(" fetch failed; unshallowing and retrying...")),await k0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z}),await $0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z,branch:z});else throw Error(`Cannot reach commit ${z} in local clone`)}await _(["reset","--hard",z],{cwd:Z}),console.log(E.green(`✓ Checked out ${z.slice(0,8)}`))}async function D1($,X,Y,Z){let z=`${$.replace(/\/+$/,"")}/api/projects/${encodeURIComponent(Y)}/checkpoints?limit=100`,K=await fetch(z,{headers:{Authorization:`Bearer ${X}`}});if(!K.ok)throw Error(`Failed to list checkpoints: HTTP ${K.status}`);let J=await K.json(),V=Z.toLowerCase(),Q=J.checkpoints.find((q)=>{if(q.name&&q.name.toLowerCase()===V)return!0;if(q.commitSha.startsWith(Z))return!0;if(q.commitMessage&&q.commitMessage.toLowerCase().includes(V))return!0;return!1});if(!Q)throw Error(`No checkpoint matches "${Z}" (searched ${J.checkpoints.length} most recent)`);return Q.commitSha}import{createInterface as P1}from"node:readline";import A from"picocolors";import{execFileSync as Z$}from"node:child_process";import{existsSync as p,copyFileSync as Y$}from"node:fs";import S from"node:path";import{homedir as z$,platform as K$}from"node:os";function J$($,X,Y){let z=Z$($,["-e",Y],{env:{...process.env,DBP:X},encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:5000}).trim();if(!z)throw Error(`bun script returned empty output for ${X}`);return JSON.parse(z)}function U0($,X){if(!p(X))return[];return J$($,X,`
|
|
27
28
|
import { Database } from 'bun:sqlite';
|
|
28
29
|
try {
|
|
29
30
|
const db = new Database(process.env.DBP, { readonly: true });
|
|
@@ -37,7 +38,7 @@ shogo project checkout ${b.cyan($)}`)),console.log(b.dim(" cloud ")+Y.cloudUrl
|
|
|
37
38
|
console.error(String(e?.stack || e));
|
|
38
39
|
process.exit(2);
|
|
39
40
|
}
|
|
40
|
-
`)}function
|
|
41
|
+
`)}function M1($){if(!p($))throw Error(`Database does not exist at ${$} — refusing to back up nothing`);let X=S.dirname($),Y=S.basename($),Z=new Date().toISOString().replace(/[:.]/g,"-"),z=S.join(X,`${Y}.bak-${Z}`);Y$($,z);for(let K of["-wal","-shm"]){let J=`${$}${K}`;if(p(J))Y$(J,`${z}${K}`)}return z}function x1($,X,Y){if(Y.length===0)return 0;if(!p(X))throw Error(`Cannot repair: database does not exist at ${X}`);let z=`
|
|
41
42
|
import { Database } from 'bun:sqlite';
|
|
42
43
|
const names = ${JSON.stringify(Y)};
|
|
43
44
|
if (!Array.isArray(names) || names.some(n => typeof n !== 'string')) {
|
|
@@ -56,8 +57,8 @@ shogo project checkout ${b.cyan($)}`)),console.log(b.dim(" cloud ")+Y.cloudUrl
|
|
|
56
57
|
console.error(String(e?.stack || e));
|
|
57
58
|
process.exit(3);
|
|
58
59
|
}
|
|
59
|
-
`;return
|
|
60
|
-
`)[0]?.trim();if(console.log(` ${
|
|
61
|
-
`+" (e.g. /Applications/Shogo.app/Contents/Resources/bun/bun on macOS).");console.log(
|
|
62
|
-
→ Relaunch the Shogo app to finish applying migrations.`))}else{if(console.log(
|
|
63
|
-
(use --help for usage)`));
|
|
60
|
+
`;return J$($,X,z).deleted}function V$($){let X=$.log??(()=>{}),{bunPath:Y,dbPath:Z}=$;if(!p(Z))return{status:"no-database",detected:[],cleared:[],remaining:[],message:`No database found at ${Z}. Nothing to repair — launch Shogo once to create it.`};let z=U0(Y,Z);if(z.length===0)return{status:"healthy",detected:[],cleared:[],remaining:[],message:"No failed migrations detected — the local database looks healthy."};X(`Found ${z.length} failed migration(s): ${z.map((W)=>W.name).join(", ")}`);let K;if(!$.skipBackup)K=M1(Z),X(`Backed up database to ${K}`);let J=z.map((W)=>W.name),V=x1(Y,Z,J);X(`Cleared ${V} failed migration row(s).`);let Q=U0(Y,Z),q=Q.length===0?"repaired":"failed",H=q==="repaired"?"Cleared the failed migration record. Relaunch Shogo to re-apply migrations cleanly.":`Repair incomplete — ${Q.length} migration(s) still failed: ${Q.map((W)=>W.name).join(", ")}.`;return{status:q,detected:z,backupPath:K,cleared:J.slice(0,V),remaining:Q,message:H}}function S1(){let $=z$(),X=K$(),Y;if(X==="darwin")Y=S.join($,"Library","Application Support");else if(X==="win32")Y=process.env.APPDATA??S.join($,"AppData","Roaming");else Y=process.env.XDG_CONFIG_HOME??S.join($,".config");return S.join(Y,"Shogo","data")}function q$(){return S.join(S1(),"shogo.db")}function R1(){let $=K$(),X=$==="win32"?"bun.exe":"bun",Y=z$(),Z=[];if($==="darwin")Z.push(S.join("/Applications","Shogo.app","Contents","Resources","bun",X),S.join(Y,"Applications","Shogo.app","Contents","Resources","bun",X));else if($==="linux")Z.push(S.join("/opt","Shogo","resources","bun",X),S.join("/usr","lib","shogo","resources","bun",X));return Z}function l0($){try{return Z$($,["--version"],{stdio:"ignore",timeout:5000}),!0}catch{return!1}}function Q$($){if($)return l0($)?$:null;if(process.versions.bun&&process.execPath)return process.execPath;for(let X of R1())if(p(X)&&l0(X))return X;if(l0("bun"))return"bun";return null}function E1($){for(let X of $){let Y=Number.isFinite(X.startedAt)?new Date(X.startedAt).toISOString():"unknown time",Z=(X.errorExcerpt??"").split(`
|
|
61
|
+
`)[0]?.trim();if(console.log(` ${A.yellow("•")} ${A.bold(X.name)} ${A.dim(`(attempted ${Y})`)}`),Z)console.log(` ${A.dim(Z)}`)}}async function b1($){if(!process.stdin.isTTY)return console.log(A.dim("(stdin is not a TTY — re-run with --yes to repair non-interactively)")),!1;let X=P1({input:process.stdin,output:process.stdout});try{let Y=await new Promise((Z)=>X.question($,Z));return/^y(es)?$/i.test(Y.trim())}finally{X.close()}}async function H$($={}){let X=$.db??q$(),Y=Q$($.bun);if(!Y)throw Error("Could not find a usable `bun` binary (needed to inspect the SQLite database).\n"+" • Install Bun (https://bun.sh) so `bun` is on your PATH, or\n"+` • pass --bun <path> pointing at the bun shipped inside the Shogo app
|
|
62
|
+
`+" (e.g. /Applications/Shogo.app/Contents/Resources/bun/bun on macOS).");console.log(A.dim(`Database: ${X}`)),console.log(A.dim(`Bun: ${Y}`)),console.log();let Z=U0(Y,X);if(Z.length===0){console.log(A.green("✓ No failed migrations detected — your local database looks healthy."));return}if(console.log(A.red(`✗ Found ${Z.length} failed migration(s):`)),E1(Z),console.log(),$.check){console.log(A.dim("Run `shogo doctor` (without --check) to back up the database and clear these records.")),process.exitCode=1;return}if(!$.yes){let K=$.backup===!1?A.red("This will NOT create a backup (--no-backup)."):"Your database will be backed up to a .bak-<timestamp> file first.";if(console.log(K),!await b1(A.bold("Clear these failed migration records and repair? [y/N] "))){console.log(A.dim("Aborted — no changes made.")),process.exitCode=1;return}console.log()}let z=V$({bunPath:Y,dbPath:X,skipBackup:$.backup===!1,log:(K)=>console.log(A.dim(` ${K}`))});if(console.log(),z.status==="repaired"){if(console.log(A.green(`✓ ${z.message}`)),z.backupPath)console.log(A.dim(` Backup: ${z.backupPath}`));console.log(A.bold(`
|
|
63
|
+
→ Relaunch the Shogo app to finish applying migrations.`))}else{if(console.log(A.red(`✗ ${z.message}`)),z.backupPath)console.log(A.dim(` Backup: ${z.backupPath}`));process.exitCode=1}}import{spawn as v1}from"node:child_process";import{resolve as h1}from"node:path";import W$ from"picocolors";function j1($){let{flags:X,config:Y,runtime:Z}=$,z=X.cwd?h1(X.cwd):process.cwd(),K=["interactive"];if(X.print!==void 0)K.push("-p",X.print);if(X.noTui)K.push("--no-tui");let J=Y.cloudUrl.replace(/\/$/,""),V={...$.baseEnv??process.env,SHOGO_INTERACTIVE:"1",SHOGO_INTERACTIVE_CWD:z,PROJECT_DIR:z,WORKSPACE_DIR:z,SHOGO_API_URL:J,SHOGO_CLOUD_URL:J,SHOGO_API_KEY:Y.apiKey,AI_PROXY_URL:`${J}/api/ai/v1`,AI_PROXY_TOKEN:Y.apiKey,NODE_ENV:"production"};if(X.model)V.SHOGO_MODEL=X.model;if(X.print!==void 0)V.SHOGO_PRINT_PROMPT=X.print;return{bin:Z.path,args:K,env:V,cwd:z}}async function O$($={}){let X=P({apiKey:$.apiKey,cloudUrl:$.cloudUrl}),Y=u({flag:$.runtimeBin});if(!Y)console.error(W$.red(n({flag:$.runtimeBin}))),process.exit(1);let Z=j1({flags:$,config:{apiKey:X.apiKey,cloudUrl:X.cloudUrl},runtime:Y}),z=await new Promise((K)=>{let J=v1(Z.bin,Z.args,{cwd:Z.cwd,env:Z.env,stdio:"inherit"});J.on("error",(V)=>{console.error(W$.red(`✗ Failed to launch agent-runtime: ${V.message}`)),K(1)}),J.on("exit",(V)=>K(V??0))});process.exit(z)}var I1="0.1.0",b=new m1;b.name("shogo").description("Shogo Cloud Agent Worker — run Shogo agents on your own machine.").version(I1);b.command("login").description("Pair this machine with Shogo Cloud (browser device flow, or --api-key for CI)").option("--api-key <key>","CI escape hatch — skip the browser flow and use a key directly").option("--cloud-url <url>","Shogo Cloud URL (default: https://studio.shogo.ai)").option("--name <name>","Device label shown in the dashboard (default: hostname)").option("--workspace <id>","Pre-select a workspace on the bridge picker").option("--no-browser","Do not auto-open the browser; print the URL instead").action(($)=>F(()=>n2($)));var B0=b.command("worker").description("Manage the local worker process");B0.command("start").description("Start the worker and pair with Shogo Cloud").option("--name <name>","Instance name shown in the dashboard (default: hostname)").option("--worker-dir <path>","Working directory for the worker (default: $PWD)").option("--api-key <key>","API key (overrides config/env)").option("--cloud-url <url>","Shogo Cloud URL").option("--port <port>","Local HTTP port for the embedded API").option("--proxy <url>","HTTPS proxy (overrides HTTPS_PROXY env)").option("--project <id>","Pin to a single project (default: multi-project on demand)").option("--runtime-bin <path>","Override the agent-runtime binary path").option("--debug","Run preflight checks before starting").option("--foreground","Run in foreground (don't detach)").option("--no-auto-pull","Disable auto-clone of project workspaces on first request").option("--projects-dir <path>","Root directory for cloned project workspaces (default: ~/.shogo/projects)").option("--no-git","Force the file-transport sync path even when git is available").action(($)=>F(()=>b2($)));B0.command("stop").description("Stop the running worker").action(()=>F(h2));B0.command("status").description("Show worker status").action(()=>F(j2));B0.command("logs").description("Tail worker logs from ~/.shogo/logs/worker.log").option("-f, --follow","Follow the log (tail -F)").option("--err","Show stderr log instead").action(($)=>F(()=>I2($)));var F0=b.command("runtime").description("Install / inspect the local agent-runtime binary");F0.command("install").description("Download + verify the agent-runtime tarball into ~/.shogo/runtime/").option("--channel <channel>","stable | beta | nightly (default: stable)").option("--version <version>","install a specific version (e.g. 0.1.0)").option("--base-url <url>","override release base URL (default: GitHub Releases)").option("--force","reinstall even if the same version is already on disk").action(($)=>F(()=>a0($)));F0.command("version").description("Print the installed agent-runtime version").action(()=>F(()=>r2()));F0.command("where").description("Print the resolved agent-runtime binary path").action(()=>F(()=>e2()));F0.command("update").description("Update agent-runtime to the latest in its channel").option("--channel <channel>","override channel (defaults to whatever is installed)").option("--base-url <url>","override release base URL").action(($)=>F(()=>s2($)));var k$=b.command("config").description("Inspect or modify ~/.shogo/config.json");k$.command("show").description("Print current config (API key masked)").action(()=>F(d2));k$.command("set <key> <value>").description("Set apiKey / cloudUrl / name / workerDir / port / projectsDir").action(($,X)=>F(()=>g2($,X)));var r0=b.command("project").description("Clone/sync project workspaces between Shogo Cloud and this machine");r0.command("pull <projectId>").description("Clone a project from Shogo Cloud into ~/.shogo/projects/<projectId>/").option("--into <dir>","Destination directory (default: ~/.shogo/projects/<projectId>)").option("--watch","After pull, watch the local dir and push edits back to cloud").option("--include <patterns>",'Comma-separated glob patterns (e.g. "src/**,*.md")').option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action(($,X)=>F(()=>t2($,X)));r0.command("push <projectId>").description("Upload local workspace edits back to Shogo Cloud").option("--from <dir>","Source directory (default: ~/.shogo/projects/<projectId>)").option("--delete-remote","Mirror local deletions to cloud (DESTRUCTIVE)").option("--include <patterns>","Comma-separated glob patterns").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action(($,X)=>F(()=>$$($,X)));r0.command("checkout <projectId>").description("Roll the local workspace to a specific git checkpoint (SHA or named checkpoint)").option("--at <ref>","Target SHA or checkpoint name (default: remote HEAD)").option("--unshallow","Fetch full history before checking out (needed for old SHAs)").option("--into <dir>","Local dir override (default: ~/.shogo/projects/<projectId>)").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action(($,X)=>F(()=>X$($,X)));b.command("doctor").description("Diagnose & repair a wedged local Shogo database (clears failed migrations so the app can reboot)").option("--check","Detect only — never modify the database").option("--yes","Repair without the confirmation prompt").option("--db <path>","Path to shogo.db (default: the desktop app's local database)").option("--bun <path>","Path to a bun binary (default: bundled/PATH bun)").option("--no-backup","Skip the pre-repair database backup (discouraged)").action(($)=>F(()=>H$($)));b.command("chat",{isDefault:!0}).description("Start the interactive coding agent in the current directory").argument("[prompt...]","Optional one-shot prompt (runs non-interactively, like -p)").option("-p, --print <prompt>","Headless one-shot: run a single turn and print the result").option("--model <model>","Model id to use for new turns").option("--cwd <dir>","Working directory to operate in (default: $PWD)").option("--runtime-bin <path>","Override the agent-runtime binary path").option("--no-tui","Disable the rich TUI; use a plain readline renderer").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action(($,X)=>F(()=>O$({print:X.print??($.length?$.join(" "):void 0),model:X.model,cwd:X.cwd,runtimeBin:X.runtimeBin,noTui:X.tui===!1,apiKey:X.apiKey,cloudUrl:X.cloudUrl})));b.showHelpAfterError(G$.dim(`
|
|
64
|
+
(use --help for usage)`));b.parseAsync(process.argv);async function F($){try{await $()}catch(X){if(console.error(G$.red(`✗ ${X?.message??X}`)),process.env.SHOGO_DEBUG)console.error(X?.stack);process.exit(1)}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shogo-ai/worker",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.10",
|
|
4
4
|
"description": "Shogo Cloud Agent Worker — run Shogo agents on your own machine (laptop, devbox, CI).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Shogo Technologies, Inc.",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
".": "./src/cli.ts",
|
|
20
20
|
"./tunnel": "./src/lib/tunnel.ts",
|
|
21
21
|
"./runtime-manager": "./src/lib/runtime-manager.ts",
|
|
22
|
+
"./resource-limits": "./src/lib/resource-limits.ts",
|
|
22
23
|
"./cloud-sync-watcher": "./src/lib/cloud-sync-watcher.ts",
|
|
23
24
|
"./git-cloner": "./src/lib/git-cloner.ts",
|
|
24
25
|
"./runtime-resolver": "./src/lib/runtime-resolver.ts",
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (C) 2026 Shogo Technologies, Inc.
|
|
3
|
+
/**
|
|
4
|
+
* Cross-platform resource limits for host-spawned agent-runtime process groups.
|
|
5
|
+
*
|
|
6
|
+
* A single project runtime is a tree: `bun run agent-runtime` -> vite build
|
|
7
|
+
* --watch, `bun run server.tsx`, tsserver, pyright, expo, etc. Left unbounded
|
|
8
|
+
* its RSS climbs until the OS OOM-kills it (macOS jetsam SIGKILL is the
|
|
9
|
+
* canonical case), taking the whole app's memory with it. We cap each tree,
|
|
10
|
+
* using the strongest primitive each platform offers:
|
|
11
|
+
*
|
|
12
|
+
* - Linux: cgroup v2. Prefer a transient `systemd-run --scope` (no root,
|
|
13
|
+
* hard `memory.max`/`cpu.max`/`pids.max`), fall back to writing a
|
|
14
|
+
* cgroup directly, fall back to the RSS watchdog.
|
|
15
|
+
* - Windows: a Job Object with ProcessMemoryLimit + KILL_ON_JOB_CLOSE via an
|
|
16
|
+
* optional native helper; fall back to the RSS watchdog.
|
|
17
|
+
* - macOS: no cgroups and RLIMIT_AS is unreliable for JIT/mmap-heavy Bun,
|
|
18
|
+
* so an RSS watchdog samples the process group and asks the
|
|
19
|
+
* manager to restart on breach.
|
|
20
|
+
*
|
|
21
|
+
* The JS-heap env cap (see {@link applyHeapEnvCap}) is a secondary hint layered
|
|
22
|
+
* on top everywhere — OS-level limits are the real enforcement.
|
|
23
|
+
*/
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
export interface RuntimeResourceLimits {
|
|
29
|
+
/** Per-project RAM ceiling in MB. Always > 0 when limits are active. */
|
|
30
|
+
memoryMB: number;
|
|
31
|
+
/** CPU ceiling as percent of ONE core (100 = one full core). 0 = no cap. */
|
|
32
|
+
cpuPercent: number;
|
|
33
|
+
/** Max tasks/pids in the group (cgroup pids.max). */
|
|
34
|
+
tasksMax: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Logger = Pick<Console, 'log' | 'warn' | 'error'>;
|
|
38
|
+
|
|
39
|
+
/** cgroup CPU period in microseconds (100ms), the systemd/cgroup default. */
|
|
40
|
+
const CPU_PERIOD_US = 100_000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the active resource limits from the environment. Returns `null` when
|
|
44
|
+
* no ceiling is configured (`RUNTIME_MEMORY_MB` unset or <= 0), which disables
|
|
45
|
+
* all enforcement for backwards compatibility.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveResourceLimits(env: NodeJS.ProcessEnv): RuntimeResourceLimits | null {
|
|
48
|
+
const mb = parseInt(env.RUNTIME_MEMORY_MB || '', 10);
|
|
49
|
+
if (!Number.isFinite(mb) || mb <= 0) return null;
|
|
50
|
+
|
|
51
|
+
const cpuRaw = parseInt(env.RUNTIME_CPU_PERCENT || '0', 10);
|
|
52
|
+
const cpuPercent = Number.isFinite(cpuRaw) && cpuRaw > 0 ? cpuRaw : 0;
|
|
53
|
+
|
|
54
|
+
const tasksRaw = parseInt(env.RUNTIME_TASKS_MAX || '', 10);
|
|
55
|
+
const tasksMax = Number.isFinite(tasksRaw) && tasksRaw > 0 ? tasksRaw : 4096;
|
|
56
|
+
|
|
57
|
+
return { memoryMB: mb, cpuPercent, tasksMax };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Append a JS-heap ceiling to `NODE_OPTIONS` (mutates `env` in place).
|
|
62
|
+
*
|
|
63
|
+
* This bounds the V8/JSC old-space of Node children (tsserver, vite, esbuild)
|
|
64
|
+
* and modern Bun, which honours `--max-old-space-size`. It does NOT bound
|
|
65
|
+
* native RSS, so it is only a hint — the OS-level limit is the real cap. We
|
|
66
|
+
* size it below the RSS ceiling (~75%) so the heap limit trips (recoverable
|
|
67
|
+
* JS OOM) before the native RSS watchdog / cgroup kill does.
|
|
68
|
+
*/
|
|
69
|
+
export function applyHeapEnvCap(env: NodeJS.ProcessEnv, memoryMB: number): void {
|
|
70
|
+
const heapMB = Math.max(256, Math.floor(memoryMB * 0.75));
|
|
71
|
+
const flag = `--max-old-space-size=${heapMB}`;
|
|
72
|
+
const existing = env.NODE_OPTIONS?.trim();
|
|
73
|
+
if (existing) {
|
|
74
|
+
if (/--max-old-space-size=/.test(existing)) return; // caller already set one
|
|
75
|
+
env.NODE_OPTIONS = `${existing} ${flag}`;
|
|
76
|
+
} else {
|
|
77
|
+
env.NODE_OPTIONS = flag;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Linux: cgroup v2 ────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/** True if `systemd-run` is usable for a rootless `--user --scope` unit. */
|
|
84
|
+
export function hasSystemdRun(): boolean {
|
|
85
|
+
if (process.platform !== 'linux') return false;
|
|
86
|
+
try {
|
|
87
|
+
const r = spawnSync('systemd-run', ['--version'], { stdio: 'ignore', timeout: 3000 });
|
|
88
|
+
return r.status === 0;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Wrap a spawn command in a transient rootless cgroup v2 scope via
|
|
96
|
+
* `systemd-run --user --scope`. The child (and everything it forks) runs inside
|
|
97
|
+
* a scope unit with hard `MemoryMax`/`CPUQuota`/`TasksMax`. Returns the original
|
|
98
|
+
* command untouched (`wrapped: false`) on non-Linux or when systemd-run is
|
|
99
|
+
* absent — the caller then falls back to {@link attachDirectCgroup} or the
|
|
100
|
+
* watchdog.
|
|
101
|
+
*/
|
|
102
|
+
export function wrapSpawnForCgroup(opts: {
|
|
103
|
+
command: string;
|
|
104
|
+
args: string[];
|
|
105
|
+
limits: RuntimeResourceLimits;
|
|
106
|
+
scopeName: string;
|
|
107
|
+
logger?: Logger;
|
|
108
|
+
}): { command: string; args: string[]; wrapped: boolean } {
|
|
109
|
+
const { command, args, limits, scopeName, logger } = opts;
|
|
110
|
+
if (process.platform !== 'linux' || !hasSystemdRun()) {
|
|
111
|
+
return { command, args, wrapped: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const props = [
|
|
115
|
+
'-p', `MemoryMax=${limits.memoryMB}M`,
|
|
116
|
+
// MemoryHigh throttles + reclaims before the hard OOM at MemoryMax, so a
|
|
117
|
+
// spiky build is slowed rather than instantly killed.
|
|
118
|
+
'-p', `MemoryHigh=${Math.max(128, Math.floor(limits.memoryMB * 0.9))}M`,
|
|
119
|
+
'-p', `TasksMax=${limits.tasksMax}`,
|
|
120
|
+
];
|
|
121
|
+
if (limits.cpuPercent > 0) props.push('-p', `CPUQuota=${limits.cpuPercent}%`);
|
|
122
|
+
|
|
123
|
+
const wrappedArgs = [
|
|
124
|
+
'--user',
|
|
125
|
+
'--scope',
|
|
126
|
+
'--quiet',
|
|
127
|
+
'--collect', // GC the transient unit as soon as it exits
|
|
128
|
+
`--unit=${scopeName}`,
|
|
129
|
+
...props,
|
|
130
|
+
'--',
|
|
131
|
+
command,
|
|
132
|
+
...args,
|
|
133
|
+
];
|
|
134
|
+
logger?.log(`[resource-limits] wrapping spawn in systemd scope ${scopeName} (MemoryMax=${limits.memoryMB}M)`);
|
|
135
|
+
return { command: 'systemd-run', args: wrappedArgs, wrapped: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fallback for Linux hosts without systemd: write a cgroup v2 directly and move
|
|
140
|
+
* `pid` (plus its future children) into it. Best-effort — returns `false` when
|
|
141
|
+
* cgroup v2 is unavailable or not writable (the common non-root, non-delegated
|
|
142
|
+
* case), so the caller can start the watchdog instead.
|
|
143
|
+
*/
|
|
144
|
+
export function attachDirectCgroup(opts: {
|
|
145
|
+
pid: number;
|
|
146
|
+
limits: RuntimeResourceLimits;
|
|
147
|
+
logger?: Logger;
|
|
148
|
+
}): boolean {
|
|
149
|
+
const { pid, limits, logger } = opts;
|
|
150
|
+
if (process.platform !== 'linux') return false;
|
|
151
|
+
|
|
152
|
+
const root = '/sys/fs/cgroup';
|
|
153
|
+
// cgroup v2 unified hierarchy exposes cgroup.controllers at the root.
|
|
154
|
+
if (!existsSync(join(root, 'cgroup.controllers'))) return false;
|
|
155
|
+
|
|
156
|
+
const dir = join(root, 'shogo', `rt-${pid}`);
|
|
157
|
+
try {
|
|
158
|
+
mkdirSync(dir, { recursive: true });
|
|
159
|
+
writeFileSync(join(dir, 'memory.max'), String(limits.memoryMB * 1024 * 1024));
|
|
160
|
+
if (limits.cpuPercent > 0) {
|
|
161
|
+
const quota = Math.floor((limits.cpuPercent / 100) * CPU_PERIOD_US);
|
|
162
|
+
writeFileSync(join(dir, 'cpu.max'), `${quota} ${CPU_PERIOD_US}`);
|
|
163
|
+
}
|
|
164
|
+
try { writeFileSync(join(dir, 'pids.max'), String(limits.tasksMax)); } catch { /* controller may be absent */ }
|
|
165
|
+
// Moving the pid in also captures children forked after this point.
|
|
166
|
+
writeFileSync(join(dir, 'cgroup.procs'), String(pid));
|
|
167
|
+
logger?.log(`[resource-limits] attached pid ${pid} to cgroup ${dir} (memory.max=${limits.memoryMB}M)`);
|
|
168
|
+
return true;
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
logger?.warn(`[resource-limits] direct cgroup attach failed for pid ${pid}: ${err?.message ?? err}`);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Windows: Job Object (optional native helper) ────────────────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Assign `pid` to a Windows Job Object with a hard process-memory limit and
|
|
179
|
+
* KILL_ON_JOB_CLOSE, using an optional native helper. Node has no built-in Job
|
|
180
|
+
* Object API, so we load a helper lazily:
|
|
181
|
+
*
|
|
182
|
+
* - a module path in `SHOGO_WINJOB_HELPER`, or
|
|
183
|
+
* - a `@shogo/winjob` package if installed,
|
|
184
|
+
*
|
|
185
|
+
* exposing `limitProcess(pid, { memoryBytes, killOnClose }): boolean`.
|
|
186
|
+
*
|
|
187
|
+
* Returns `false` when no helper is available so the caller falls back to the
|
|
188
|
+
* RSS watchdog (which also enforces the ceiling on Windows, just via sampling).
|
|
189
|
+
*/
|
|
190
|
+
export function tryAttachJobObject(opts: {
|
|
191
|
+
pid: number;
|
|
192
|
+
limits: RuntimeResourceLimits;
|
|
193
|
+
logger?: Logger;
|
|
194
|
+
}): boolean {
|
|
195
|
+
const { pid, limits, logger } = opts;
|
|
196
|
+
if (process.platform !== 'win32') return false;
|
|
197
|
+
|
|
198
|
+
const helperPath = process.env.SHOGO_WINJOB_HELPER || '@shogo/winjob';
|
|
199
|
+
try {
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
201
|
+
const helper = require(helperPath) as {
|
|
202
|
+
limitProcess?: (pid: number, o: { memoryBytes: number; killOnClose: boolean }) => boolean;
|
|
203
|
+
};
|
|
204
|
+
if (typeof helper.limitProcess !== 'function') return false;
|
|
205
|
+
const ok = helper.limitProcess(pid, {
|
|
206
|
+
memoryBytes: limits.memoryMB * 1024 * 1024,
|
|
207
|
+
killOnClose: true,
|
|
208
|
+
});
|
|
209
|
+
if (ok) {
|
|
210
|
+
logger?.log(`[resource-limits] attached pid ${pid} to Job Object (${limits.memoryMB}M)`);
|
|
211
|
+
}
|
|
212
|
+
return ok;
|
|
213
|
+
} catch {
|
|
214
|
+
// Helper not installed — expected on stock installs. Watchdog covers us.
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── RSS watchdog (macOS always; Windows / Linux fallback) ───────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Sum the resident-set size (KB) of the process group led by `pid`.
|
|
223
|
+
*
|
|
224
|
+
* - POSIX: `ps -o rss= -g <pgid>` (pid is the group leader because the
|
|
225
|
+
* runtime is spawned `detached`), summed across the group.
|
|
226
|
+
* - Windows: a PowerShell pass that sums WorkingSet64 of `pid` and all of its
|
|
227
|
+
* descendants (walking Win32_Process.ParentProcessId).
|
|
228
|
+
*
|
|
229
|
+
* Returns `null` when sampling fails (process gone, tool missing) so the
|
|
230
|
+
* watchdog treats it as "no reading" rather than "0".
|
|
231
|
+
*/
|
|
232
|
+
export function samplePgidRssKb(pid: number, platform: NodeJS.Platform = process.platform): number | null {
|
|
233
|
+
try {
|
|
234
|
+
if (platform === 'win32') {
|
|
235
|
+
const script =
|
|
236
|
+
`$ErrorActionPreference='SilentlyContinue';` +
|
|
237
|
+
`$root=${pid};` +
|
|
238
|
+
`$all=Get-CimInstance Win32_Process;` +
|
|
239
|
+
`$seen=@{};$stack=New-Object System.Collections.Stack;$stack.Push($root);$sum=0;` +
|
|
240
|
+
`while($stack.Count -gt 0){$id=$stack.Pop();if($seen[$id]){continue};$seen[$id]=$true;` +
|
|
241
|
+
`foreach($p in $all){if($p.ProcessId -eq $id){$sum+=$p.WorkingSetSize};` +
|
|
242
|
+
`if($p.ParentProcessId -eq $id){$stack.Push($p.ProcessId)}}};` +
|
|
243
|
+
`[math]::Floor($sum/1024)`;
|
|
244
|
+
const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
245
|
+
encoding: 'utf-8',
|
|
246
|
+
timeout: 10_000,
|
|
247
|
+
windowsHide: true,
|
|
248
|
+
});
|
|
249
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
250
|
+
const kb = parseInt(r.stdout.trim(), 10);
|
|
251
|
+
return Number.isFinite(kb) ? kb : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// POSIX: sum RSS (KB) of every process in the group.
|
|
255
|
+
const r = spawnSync('ps', ['-o', 'rss=', '-g', String(pid)], {
|
|
256
|
+
encoding: 'utf-8',
|
|
257
|
+
timeout: 5_000,
|
|
258
|
+
});
|
|
259
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
260
|
+
let total = 0;
|
|
261
|
+
let sawRow = false;
|
|
262
|
+
for (const line of r.stdout.split('\n')) {
|
|
263
|
+
const v = parseInt(line.trim(), 10);
|
|
264
|
+
if (Number.isFinite(v)) { total += v; sawRow = true; }
|
|
265
|
+
}
|
|
266
|
+
return sawRow ? total : null;
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Periodically samples a process group's RSS and invokes `onBreach` once the
|
|
274
|
+
* ceiling has been exceeded for {@link RssWatchdogOptions.strikes} consecutive
|
|
275
|
+
* samples (so a single transient spike doesn't trigger a restart). Used where
|
|
276
|
+
* no kernel-level cap is available.
|
|
277
|
+
*/
|
|
278
|
+
export interface RssWatchdogOptions {
|
|
279
|
+
pid: number;
|
|
280
|
+
ceilingMB: number;
|
|
281
|
+
platform?: NodeJS.Platform;
|
|
282
|
+
intervalMs?: number;
|
|
283
|
+
/** Consecutive over-ceiling samples required before onBreach fires. */
|
|
284
|
+
strikes?: number;
|
|
285
|
+
logger?: Logger;
|
|
286
|
+
/** Called with the observed RSS (MB) when the ceiling is breached. */
|
|
287
|
+
onBreach: (rssMB: number) => void;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export class RssWatchdog {
|
|
291
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
292
|
+
private consecutive = 0;
|
|
293
|
+
private fired = false;
|
|
294
|
+
private readonly pid: number;
|
|
295
|
+
private readonly ceilingMB: number;
|
|
296
|
+
private readonly platform: NodeJS.Platform;
|
|
297
|
+
private readonly intervalMs: number;
|
|
298
|
+
private readonly strikes: number;
|
|
299
|
+
private readonly logger?: Logger;
|
|
300
|
+
private readonly onBreach: (rssMB: number) => void;
|
|
301
|
+
|
|
302
|
+
constructor(opts: RssWatchdogOptions) {
|
|
303
|
+
this.pid = opts.pid;
|
|
304
|
+
this.ceilingMB = opts.ceilingMB;
|
|
305
|
+
this.platform = opts.platform ?? process.platform;
|
|
306
|
+
this.intervalMs = opts.intervalMs ?? parseInt(process.env.RUNTIME_RSS_WATCHDOG_MS || '15000', 10);
|
|
307
|
+
this.strikes = opts.strikes ?? parseInt(process.env.RUNTIME_RSS_WATCHDOG_STRIKES || '2', 10);
|
|
308
|
+
this.logger = opts.logger;
|
|
309
|
+
this.onBreach = opts.onBreach;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
start(): void {
|
|
313
|
+
if (this.timer) return;
|
|
314
|
+
this.timer = setInterval(() => this.sample(), Math.max(2000, this.intervalMs));
|
|
315
|
+
try { this.timer.unref?.(); } catch { /* unref is best-effort */ }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
stop(): void {
|
|
319
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private sample(): void {
|
|
323
|
+
if (this.fired) return;
|
|
324
|
+
const kb = samplePgidRssKb(this.pid, this.platform);
|
|
325
|
+
if (kb == null) { this.consecutive = 0; return; }
|
|
326
|
+
const rssMB = Math.floor(kb / 1024);
|
|
327
|
+
if (rssMB <= this.ceilingMB) { this.consecutive = 0; return; }
|
|
328
|
+
|
|
329
|
+
this.consecutive += 1;
|
|
330
|
+
this.logger?.warn(
|
|
331
|
+
`[resource-limits] pid ${this.pid} RSS ${rssMB}MB over ceiling ${this.ceilingMB}MB ` +
|
|
332
|
+
`(strike ${this.consecutive}/${this.strikes})`,
|
|
333
|
+
);
|
|
334
|
+
if (this.consecutive >= this.strikes) {
|
|
335
|
+
this.fired = true;
|
|
336
|
+
this.stop();
|
|
337
|
+
this.onBreach(rssMB);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -38,6 +38,15 @@ import { createConnection } from 'node:net';
|
|
|
38
38
|
import { tmpdir } from 'node:os';
|
|
39
39
|
import { dirname, join } from 'node:path';
|
|
40
40
|
import { resolveRuntime, type ResolvedRuntime } from './runtime-resolver.ts';
|
|
41
|
+
import {
|
|
42
|
+
RssWatchdog,
|
|
43
|
+
applyHeapEnvCap,
|
|
44
|
+
attachDirectCgroup,
|
|
45
|
+
resolveResourceLimits,
|
|
46
|
+
tryAttachJobObject,
|
|
47
|
+
wrapSpawnForCgroup,
|
|
48
|
+
type RuntimeResourceLimits,
|
|
49
|
+
} from './resource-limits.ts';
|
|
41
50
|
import type { ResolveRejection, RuntimeResolver } from './tunnel.ts';
|
|
42
51
|
import { CloudFileTransport } from '@shogo-ai/sdk/cloud-file-transport';
|
|
43
52
|
import { CloudSyncWatcher } from './cloud-sync-watcher.ts';
|
|
@@ -389,6 +398,13 @@ interface InternalRuntime {
|
|
|
389
398
|
graceTimer: ReturnType<typeof setTimeout> | null;
|
|
390
399
|
restartTimer: ReturnType<typeof setTimeout> | null;
|
|
391
400
|
idleTimer: ReturnType<typeof setTimeout> | null;
|
|
401
|
+
/**
|
|
402
|
+
* RSS watchdog enforcing the per-project memory ceiling where no kernel-level
|
|
403
|
+
* cap is available (macOS always; Windows without the Job Object helper;
|
|
404
|
+
* Linux without cgroup access). Null when a cgroup/Job Object handles the cap
|
|
405
|
+
* or when no limit is configured. Stopped on exit/stop.
|
|
406
|
+
*/
|
|
407
|
+
limitWatchdog: RssWatchdog | null;
|
|
392
408
|
lastError?: string;
|
|
393
409
|
spawnConfig: ProjectSpawnConfig;
|
|
394
410
|
/**
|
|
@@ -512,6 +528,11 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
512
528
|
private readonly spawnCommand: SpawnCommandFactory;
|
|
513
529
|
private resolved: ResolvedRuntime | null = null;
|
|
514
530
|
private stopped = false;
|
|
531
|
+
/**
|
|
532
|
+
* Per-project resource limits (memory/CPU ceiling) read once from the
|
|
533
|
+
* environment. `undefined` = not yet resolved; `null` = no limit configured.
|
|
534
|
+
*/
|
|
535
|
+
private resolvedLimits: RuntimeResourceLimits | null | undefined = undefined;
|
|
515
536
|
|
|
516
537
|
/** Active watchers per projectId, keyed by projectId. Stopped in stopAll. */
|
|
517
538
|
private readonly watchers = new Map<string, CloudSyncWatcher>();
|
|
@@ -1017,6 +1038,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1017
1038
|
if (r.restartTimer) { clearTimeout(r.restartTimer); r.restartTimer = null; }
|
|
1018
1039
|
if (r.idleTimer) { clearTimeout(r.idleTimer); r.idleTimer = null; }
|
|
1019
1040
|
if (r.graceTimer) { clearTimeout(r.graceTimer); r.graceTimer = null; }
|
|
1041
|
+
if (r.limitWatchdog) { r.limitWatchdog.stop(); r.limitWatchdog = null; }
|
|
1020
1042
|
if (r.proc) {
|
|
1021
1043
|
// Send the requested signal to the whole process group first so
|
|
1022
1044
|
// children (vite, preview-manager's API server, LSPs) start their
|
|
@@ -1051,6 +1073,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1051
1073
|
if (r.restartTimer) { clearTimeout(r.restartTimer); r.restartTimer = null; }
|
|
1052
1074
|
if (r.idleTimer) { clearTimeout(r.idleTimer); r.idleTimer = null; }
|
|
1053
1075
|
if (r.graceTimer) { clearTimeout(r.graceTimer); r.graceTimer = null; }
|
|
1076
|
+
if (r.limitWatchdog) { r.limitWatchdog.stop(); r.limitWatchdog = null; }
|
|
1054
1077
|
this.runtimes.delete(projectId);
|
|
1055
1078
|
this.log.log(`[WorkerRuntimeManager] resetFailure: ${projectId} cleared, next ensureRunning will respawn`);
|
|
1056
1079
|
return true;
|
|
@@ -1098,6 +1121,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1098
1121
|
graceTimer: null,
|
|
1099
1122
|
restartTimer: null,
|
|
1100
1123
|
idleTimer: null,
|
|
1124
|
+
limitWatchdog: null,
|
|
1101
1125
|
spawnConfig: config,
|
|
1102
1126
|
startPromise: null,
|
|
1103
1127
|
};
|
|
@@ -1118,11 +1142,32 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1118
1142
|
|
|
1119
1143
|
const env = this.buildEnv(slot, resolved.path);
|
|
1120
1144
|
const cwd = this.resolveCwd(slot);
|
|
1121
|
-
const
|
|
1145
|
+
const baseSpawn = this.spawnCommand(resolved.path);
|
|
1146
|
+
|
|
1147
|
+
// Resource limits: on Linux, prefer wrapping the spawn in a rootless
|
|
1148
|
+
// systemd cgroup scope (hard memory.max/cpu.max). The other platforms and
|
|
1149
|
+
// the systemd-absent fallback are handled post-spawn in applyPostSpawnLimits.
|
|
1150
|
+
const limits = this.resolveLimits();
|
|
1151
|
+
let command = baseSpawn.command;
|
|
1152
|
+
let args = baseSpawn.args;
|
|
1153
|
+
let cgroupWrapped = false;
|
|
1154
|
+
if (limits) {
|
|
1155
|
+
const wrapped = wrapSpawnForCgroup({
|
|
1156
|
+
command: baseSpawn.command,
|
|
1157
|
+
args: baseSpawn.args,
|
|
1158
|
+
limits,
|
|
1159
|
+
scopeName: `shogo-rt-${slot.projectId.slice(0, 8)}-${Date.now().toString(36)}`,
|
|
1160
|
+
logger: this.log,
|
|
1161
|
+
});
|
|
1162
|
+
command = wrapped.command;
|
|
1163
|
+
args = wrapped.args;
|
|
1164
|
+
cgroupWrapped = wrapped.wrapped;
|
|
1165
|
+
}
|
|
1122
1166
|
|
|
1123
1167
|
this.log.log(
|
|
1124
1168
|
`[WorkerRuntimeManager] Spawning agent-runtime for ${slot.projectId} ` +
|
|
1125
|
-
`via ${command} ${args.join(' ')} (port=${slot.agentPort}, source=${resolved.source}
|
|
1169
|
+
`via ${command} ${args.join(' ')} (port=${slot.agentPort}, source=${resolved.source}` +
|
|
1170
|
+
`${limits ? `, memCeil=${limits.memoryMB}M` : ''})`,
|
|
1126
1171
|
);
|
|
1127
1172
|
|
|
1128
1173
|
// Spawn the runtime as its own process group leader (posix only —
|
|
@@ -1156,6 +1201,19 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1156
1201
|
slot.pid = proc.pid ?? null;
|
|
1157
1202
|
slot.status = 'starting';
|
|
1158
1203
|
slot.startedAt = Date.now();
|
|
1204
|
+
|
|
1205
|
+
// Enforce the per-project memory/CPU ceiling on the freshly-spawned group
|
|
1206
|
+
// (cgroup direct-write / Windows Job Object / RSS watchdog fallback). No-op
|
|
1207
|
+
// when the systemd scope above already caps it or no limit is configured.
|
|
1208
|
+
if (limits) {
|
|
1209
|
+
try {
|
|
1210
|
+
this.applyPostSpawnLimits(slot, limits, cgroupWrapped);
|
|
1211
|
+
} catch (err: any) {
|
|
1212
|
+
this.log.warn(
|
|
1213
|
+
`[WorkerRuntimeManager] failed to apply resource limits for ${slot.projectId}: ${err?.message ?? err}`,
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1159
1217
|
// Seed the progress timestamp at spawn so a child that emits its
|
|
1160
1218
|
// first line within `STDOUT_PROGRESS_WINDOW_MS` is treated as
|
|
1161
1219
|
// "making progress since spawn" without the readiness check having
|
|
@@ -1232,6 +1290,70 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1232
1290
|
* that wedge the next spawn's event loop). We use `taskkill /F /T`
|
|
1233
1291
|
* to walk the process tree by parent PID instead.
|
|
1234
1292
|
*/
|
|
1293
|
+
/** Memoized per-project resource limits from the environment. */
|
|
1294
|
+
private resolveLimits(): RuntimeResourceLimits | null {
|
|
1295
|
+
if (this.resolvedLimits === undefined) {
|
|
1296
|
+
this.resolvedLimits = resolveResourceLimits(this.opts.env ?? process.env);
|
|
1297
|
+
}
|
|
1298
|
+
return this.resolvedLimits;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Apply the strongest available OS-level cap to a freshly-spawned runtime,
|
|
1303
|
+
* or start the RSS watchdog as a fallback. Called right after spawn so most
|
|
1304
|
+
* of the runtime's children inherit the cgroup/Job Object.
|
|
1305
|
+
*
|
|
1306
|
+
* @param cgroupWrapped true when the spawn was already wrapped in a
|
|
1307
|
+
* `systemd-run` scope (Linux), in which case the cap is already enforced.
|
|
1308
|
+
*/
|
|
1309
|
+
private applyPostSpawnLimits(
|
|
1310
|
+
slot: InternalRuntime,
|
|
1311
|
+
limits: RuntimeResourceLimits,
|
|
1312
|
+
cgroupWrapped: boolean,
|
|
1313
|
+
): void {
|
|
1314
|
+
const pid = slot.pid;
|
|
1315
|
+
if (!pid) return;
|
|
1316
|
+
|
|
1317
|
+
if (process.platform === 'linux') {
|
|
1318
|
+
if (cgroupWrapped) return; // systemd scope already enforces the cap
|
|
1319
|
+
if (attachDirectCgroup({ pid, limits, logger: this.log })) return;
|
|
1320
|
+
// fall through to watchdog
|
|
1321
|
+
} else if (process.platform === 'win32') {
|
|
1322
|
+
if (tryAttachJobObject({ pid, limits, logger: this.log })) return;
|
|
1323
|
+
// fall through to watchdog
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// macOS always, or a fallback from the branches above: sample RSS and
|
|
1327
|
+
// restart the group on sustained breach.
|
|
1328
|
+
slot.limitWatchdog = new RssWatchdog({
|
|
1329
|
+
pid,
|
|
1330
|
+
ceilingMB: limits.memoryMB,
|
|
1331
|
+
logger: this.log,
|
|
1332
|
+
onBreach: (rssMB) => this.handleLimitBreach(slot, rssMB, limits.memoryMB),
|
|
1333
|
+
});
|
|
1334
|
+
slot.limitWatchdog.start();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* React to a memory-ceiling breach reported by the RSS watchdog: tear the
|
|
1339
|
+
* process group down gracefully. The existing `handleExit` path then treats
|
|
1340
|
+
* this as a non-clean exit and restarts with backoff (a project that keeps
|
|
1341
|
+
* ballooning will trip the circuit breaker, exactly as with an OOM loop).
|
|
1342
|
+
*/
|
|
1343
|
+
private handleLimitBreach(slot: InternalRuntime, rssMB: number, ceilingMB: number): void {
|
|
1344
|
+
this.log.warn(
|
|
1345
|
+
`[WorkerRuntimeManager] runtime ${slot.projectId} exceeded memory ceiling ` +
|
|
1346
|
+
`(${rssMB}MB > ${ceilingMB}MB) — restarting`,
|
|
1347
|
+
);
|
|
1348
|
+
slot.lastError = `memory ceiling exceeded (${rssMB}MB > ${ceilingMB}MB)`;
|
|
1349
|
+
if (slot.limitWatchdog) { slot.limitWatchdog.stop(); slot.limitWatchdog = null; }
|
|
1350
|
+
this.killProcessGroup(slot, 'SIGTERM');
|
|
1351
|
+
try { slot.proc?.kill('SIGTERM'); } catch { /* already gone */ }
|
|
1352
|
+
// Escalate to SIGKILL if the group ignores SIGTERM within the grace window.
|
|
1353
|
+
const killTimer = setTimeout(() => this.killProcessGroup(slot, 'SIGKILL'), 5000);
|
|
1354
|
+
try { killTimer.unref?.(); } catch { /* unref is best-effort */ }
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1235
1357
|
private killProcessGroup(slot: InternalRuntime, signal: NodeJS.Signals): void {
|
|
1236
1358
|
if (!slot.pid) return;
|
|
1237
1359
|
if (process.platform === 'win32') {
|
|
@@ -1348,6 +1470,13 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1348
1470
|
}
|
|
1349
1471
|
|
|
1350
1472
|
if (cfg.extraEnv) Object.assign(env, cfg.extraEnv);
|
|
1473
|
+
|
|
1474
|
+
// Secondary hint: cap the JS heap of Node children (tsserver, vite/esbuild)
|
|
1475
|
+
// and Bun below the RSS ceiling so a JS OOM (recoverable) trips before the
|
|
1476
|
+
// OS-level cap/watchdog kills the whole group. OS limits are the real cap.
|
|
1477
|
+
const limits = this.resolveLimits();
|
|
1478
|
+
if (limits) applyHeapEnvCap(env, limits.memoryMB);
|
|
1479
|
+
|
|
1351
1480
|
return env;
|
|
1352
1481
|
}
|
|
1353
1482
|
|
|
@@ -1369,6 +1498,9 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1369
1498
|
clearTimeout(slot.graceTimer);
|
|
1370
1499
|
slot.graceTimer = null;
|
|
1371
1500
|
}
|
|
1501
|
+
// The process (and its group) is gone — stop sampling its RSS. A fresh
|
|
1502
|
+
// watchdog is armed by the next doStart() if the runtime restarts.
|
|
1503
|
+
if (slot.limitWatchdog) { slot.limitWatchdog.stop(); slot.limitWatchdog = null; }
|
|
1372
1504
|
|
|
1373
1505
|
if (slot.status === 'stopping' || this.stopped) {
|
|
1374
1506
|
// We initiated the stop; the orphan reap was already done by
|