@shogo-ai/worker 2.0.6 → 2.0.8
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
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import{createRequire as F$}from"node:module";var L$=Object.defineProperty;var U$=($)=>$;function N$($,X){this[$]=U$.bind(null,X)}var A$=($,X)=>{for(var Y in X)L$($,Y,{get:X[Y],enumerable:!0,configurable:!0,set:N$.bind(X,Y)})};var B$=($,X)=>()=>($&&(X=$($=0)),X);var y0=F$(import.meta.url);var U2={};A$(U2,{runGit:()=>y,isGitRepo:()=>X0,gitIsAvailable:()=>E0,gitFetchUnshallow:()=>O0,gitFetchAndReset:()=>$0,commitAndPush:()=>$6,cloneProject:()=>v0,buildGitUrl:()=>t});import{spawn as r$,execFile as e$}from"node:child_process";import{existsSync as R0}from"node:fs";import{join as b0}from"node:path";import{promisify as s$}from"node:util";async function E0($=!1){if(s!==null&&!$)return s;try{await t$("git",["--version"],{timeout:3000}),s=!0}catch{s=!1}return s}function t($,X){return`${$.replace(/\/+$/,"")}/api/projects/${X}/git`}function y($,X={}){let{cwd:Y,env:Z,timeoutMs:z=300000,logger:J}=X;return new Promise((K,V)=>{let H=r$("git",$,{cwd:Y,env:{...process.env,...Z},stdio:["ignore","pipe","pipe"]}),q="",Q="",G=[],W=[];H.stdout.setEncoding("utf-8"),H.stderr.setEncoding("utf-8"),H.stdout.on("data",(k)=>{if(G.push(k),J)J.log(k.trimEnd())}),H.stderr.on("data",(k)=>{if(W.push(k),J)J.warn(k.trimEnd())});let C=setTimeout(()=>{try{H.kill("SIGKILL")}catch{}V(Error(`git ${$[0]} timed out after ${z}ms`))},z);H.on("error",(k)=>{clearTimeout(C),V(k)}),H.on("close",(k)=>{clearTimeout(C),q=G.join(""),Q=W.join("");let L=k??-1;if(L===0)K({stdout:q,stderr:Q,exitCode:L});else{let O=Error(`git ${$[0]} exited with code ${L}: ${Q.slice(0,500)}`);O.exitCode=L,O.stdout=q,O.stderr=Q,V(O)}})})}async function v0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,shallow:J=!0,logger:K,timeoutMs:V}=$;if(R0(b0(z,".git")))throw Error(`cloneProject: ${z}/.git already exists; use gitFetchAndReset instead`);let H=t(X,Z),q=["-c",`http.extraHeader=Authorization: Bearer ${Y}`,"clone"];if(J)q.push("--depth=1");return q.push(H,z),await y(q,{logger:K,timeoutMs:V}),{commitSha:(await y(["rev-parse","HEAD"],{cwd:z,logger:K,timeoutMs:1e4})).stdout.trim()}}async function $0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,branch:J="HEAD",logger:K,timeoutMs:V}=$,H=t(X,Z),q=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];return await y([...q,"fetch",H,J],{cwd:z,logger:K,timeoutMs:V}),await y(["reset","--hard","FETCH_HEAD"],{cwd:z,logger:K,timeoutMs:V}),{commitSha:(await y(["rev-parse","HEAD"],{cwd:z,logger:K,timeoutMs:1e4})).stdout.trim()}}async function O0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,logger:J,timeoutMs:K}=$,V=t(X,Z),H=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];if(R0(b0(z,".git","shallow")))await y([...H,"fetch","--unshallow",V],{cwd:z,logger:J,timeoutMs:K})}async function $6($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,message:J,branch:K="HEAD",authorEmail:V,authorName:H,logger:q,timeoutMs:Q}=$,G=t(X,Z),W={};if(V)W.GIT_AUTHOR_EMAIL=V,W.GIT_COMMITTER_EMAIL=V;if(H)W.GIT_AUTHOR_NAME=H,W.GIT_COMMITTER_NAME=H;await y(["add","-A"],{cwd:z,env:W,logger:q,timeoutMs:Q});let C=!1;try{await y(["diff","--cached","--quiet"],{cwd:z,env:W,logger:q,timeoutMs:Q})}catch{C=!0}if(!C)return{committed:!1};await y(["commit","-m",J,"--no-verify"],{cwd:z,env:W,logger:q,timeoutMs:Q});let L=(await y(["rev-parse","HEAD"],{cwd:z,env:W,logger:q,timeoutMs:1e4})).stdout.trim(),O=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];return await y([...O,"push",G,K],{cwd:z,env:W,logger:q,timeoutMs:Q}),{committed:!0,commitSha:L}}function X0($){return R0(b0($,".git"))}var t$,s=null;var w0=B$(()=>{t$=s$(e$)});import{Command as g1}from"commander";import w$ from"picocolors";import w from"picocolors";import{existsSync as v6}from"node:fs";import{readFileSync as y$,writeFileSync as _$,existsSync as D$,chmodSync as x$}from"node:fs";import{hostname as M$}from"node:os";import{homedir as T$}from"node:os";import{join as D}from"node:path";import{mkdirSync as V0}from"node:fs";var h=D(T$(),".shogo"),j=D(h,"config.json"),r1=D(h,"credentials.json"),o=D(h,"device-id"),a=D(h,"worker.pid"),_0=D(h,"logs"),q0=D(_0,"worker.log"),H0=D(_0,"worker.err.log"),l=D(h,"runtime"),S=D(l,process.platform==="win32"?"agent-runtime.exe":"agent-runtime"),r=D(l,"version.json"),Q0=D(h,"projects");function g($,X=Q0){return D(X,$)}function m(){V0(h,{recursive:!0,mode:448}),V0(_0,{recursive:!0,mode:448})}function s0(){m(),V0(l,{recursive:!0,mode:448})}function t0($=Q0){m(),V0($,{recursive:!0,mode:448})}var D0={cloudUrl:"https://studio.shogo.ai",port:8002,projectsDir:Q0};function I(){if(!D$(j))return{};try{return JSON.parse(y$(j,"utf-8"))}catch($){throw Error(`Corrupt config at ${j}: ${$.message}`)}}function e($){m(),_$(j,JSON.stringify($,null,2),{mode:384}),x$(j,384)}function $2($,X){return{...$,...Object.fromEntries(Object.entries(X).filter(([,Y])=>Y!==void 0))}}function R($={}){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=$2($2(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??D0.cloudUrl,name:Z.name??M$(),workerDir:Z.workerDir??process.cwd(),port:Z.port??D0.port,projectsDir:Z.projectsDir??D0.projectsDir}}import{spawn as P$}from"node:child_process";import{existsSync as S$,openSync as X2,readFileSync as R$,unlinkSync as b$,writeFileSync as E$}from"node:fs";function G0(){if(!S$(a))return null;let $=R$(a,"utf-8").trim(),X=parseInt($,10);return Number.isFinite(X)?X:null}function k0($){try{return process.kill($,0),!0}catch{return!1}}function x0(){try{b$(a)}catch{}}function Y2($){m();let X=G0();if(X&&k0(X))throw Error(`Worker already running (pid=${X}). Run \`shogo worker stop\` first.`);if(X)x0();let Y=$.inheritStdio?["ignore","inherit","inherit"]:["ignore",X2(q0,"a"),X2(H0,"a")],Z=P$($.runner,[$.entry],{cwd:$.cwd,env:$.env,detached:!!$.detach,stdio:Y});if(!Z.pid)throw Error("Failed to spawn worker process.");if(E$(a,String(Z.pid),{mode:384}),$.detach)Z.unref();return{pid:Z.pid,child:Z}}function Z2($="SIGTERM"){let X=G0();if(!X)return{killedPid:null};if(!k0(X))return x0(),{killedPid:null};try{process.kill(X,$)}catch{}return x0(),{killedPid:X}}import x from"picocolors";import{existsSync as h$}from"node:fs";import{request as v$}from"node:http";function J2($,X=process.env){let Y=(J)=>J&&J.trim().length>0?J.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[J,K]of z){let V=Y(K);if(V)return{url:z2(V),source:J}}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,J=(K)=>{if(z)return;z=!0,Z(K)};try{let K=new URL($.url),V=K.hostname,H=K.port?parseInt(K.port,10):K.protocol==="https:"?443:80,q=v$({method:"CONNECT",host:V,port:H,path:`${X}:443`,timeout:Y});q.on("connect",(Q)=>{q.destroy();let G=Q.statusCode??0;if(G===200)J({ok:!0,detail:`CONNECT ${X}:443 → 200 (via ${V}:${H})`});else if(G===407)J({ok:!1,detail:`407 Proxy Authentication Required — check credentials in ${$.source}`});else J({ok:!1,detail:`CONNECT → HTTP ${G}`})}),q.on("timeout",()=>{q.destroy(),J({ok:!1,detail:`timeout after ${Y}ms`})}),q.on("error",(Q)=>{let G=Q.code?` (${Q.code})`:"";J({ok:!1,detail:`${Q.message}${G}`})}),q.end()}catch(K){J({ok:!1,detail:K?.message??"unknown error"})}})}function V2($){let X;try{X=new URL($)}catch{return[]}let Y=X.host,Z=X.protocol.replace(":","")||"https",z=X.hostname.split("."),J=z.length>=2?z.slice(-2).join("."):X.hostname;return[{url:`${Z}://${Y}`,host:Y,purpose:"control",criticality:"fatal"},{url:`${Z}://api-direct.${J}`,host:`api-direct.${J}`,purpose:"tunnel-direct",criticality:"graceful"},{url:`${Z}://artifacts.${J}`,host:`artifacts.${J}`,purpose:"artifacts",criticality:"graceful"}]}var j$=5000;async function m$($,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 q2=($)=>{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 h$($.workerDir)?{ok:!0,detail:$.workerDir}:{ok:!1,detail:`${$.workerDir} does not exist`}}}];if($.proxy)X.push({name:`Proxy reachable (${I$($.proxy.url)})`,criticality:"fatal",async run(){return K2($.proxy)}});let Y=V2($.cloudUrl);for(let Z of Y)X.push({name:`Reach ${Z.host}${Z.purpose!=="control"?x.dim(` (${Z.purpose})`):""}`,criticality:Z.criticality,run:()=>m$(Z.url,j$)});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 I$($){try{return new URL($).host}catch{return $}}async function H2($){console.log(x.bold(`
|
|
1
|
+
import{createRequire as y$}from"node:module";var A$=Object.defineProperty;var N$=($)=>$;function B$($,X){this[$]=N$.bind(null,X)}var F$=($,X)=>{for(var Y in X)A$($,Y,{get:X[Y],enumerable:!0,configurable:!0,set:B$.bind(X,Y)})};var T$=($,X)=>()=>($&&(X=$($=0)),X);var y0=y$(import.meta.url);var U2={};F$(U2,{runGit:()=>y,isGitRepo:()=>Y0,gitIsAvailable:()=>E0,gitFetchUnshallow:()=>O0,gitFetchAndReset:()=>X0,commitAndPush:()=>Y6,cloneProject:()=>v0,buildGitUrl:()=>$0});import{spawn as s$,execFile as t$}from"node:child_process";import{existsSync as R0}from"node:fs";import{join as b0}from"node:path";import{promisify as $6}from"node:util";async function E0($=!1){if(t!==null&&!$)return t;try{await X6("git",["--version"],{timeout:3000}),t=!0}catch{t=!1}return t}function $0($,X){return`${$.replace(/\/+$/,"")}/api/projects/${X}/git`}function y($,X={}){let{cwd:Y,env:Z,timeoutMs:z=300000,logger:J}=X;return new Promise((K,V)=>{let H=s$("git",$,{cwd:Y,env:{...process.env,...Z},stdio:["ignore","pipe","pipe"]}),q="",Q="",G=[],k=[];H.stdout.setEncoding("utf-8"),H.stderr.setEncoding("utf-8"),H.stdout.on("data",(W)=>{if(G.push(W),J)J.log(W.trimEnd())}),H.stderr.on("data",(W)=>{if(k.push(W),J)J.warn(W.trimEnd())});let C=setTimeout(()=>{try{H.kill("SIGKILL")}catch{}V(Error(`git ${$[0]} timed out after ${z}ms`))},z);H.on("error",(W)=>{clearTimeout(C),V(W)}),H.on("close",(W)=>{clearTimeout(C),q=G.join(""),Q=k.join("");let L=W??-1;if(L===0)K({stdout:q,stderr:Q,exitCode:L});else{let O=Error(`git ${$[0]} exited with code ${L}: ${Q.slice(0,500)}`);O.exitCode=L,O.stdout=q,O.stderr=Q,V(O)}})})}async function v0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,shallow:J=!0,logger:K,timeoutMs:V}=$;if(R0(b0(z,".git")))throw Error(`cloneProject: ${z}/.git already exists; use gitFetchAndReset instead`);let H=$0(X,Z),q=["-c",`http.extraHeader=Authorization: Bearer ${Y}`,"clone"];if(J)q.push("--depth=1");return q.push(H,z),await y(q,{logger:K,timeoutMs:V}),{commitSha:(await y(["rev-parse","HEAD"],{cwd:z,logger:K,timeoutMs:1e4})).stdout.trim()}}async function X0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,branch:J="HEAD",logger:K,timeoutMs:V}=$,H=$0(X,Z),q=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];return await y([...q,"fetch",H,J],{cwd:z,logger:K,timeoutMs:V}),await y(["reset","--hard","FETCH_HEAD"],{cwd:z,logger:K,timeoutMs:V}),{commitSha:(await y(["rev-parse","HEAD"],{cwd:z,logger:K,timeoutMs:1e4})).stdout.trim()}}async function O0($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,logger:J,timeoutMs:K}=$,V=$0(X,Z),H=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];if(R0(b0(z,".git","shallow")))await y([...H,"fetch","--unshallow",V],{cwd:z,logger:J,timeoutMs:K})}async function Y6($){let{apiUrl:X,apiKey:Y,projectId:Z,localDir:z,message:J,branch:K="HEAD",authorEmail:V,authorName:H,logger:q,timeoutMs:Q}=$,G=$0(X,Z),k={};if(V)k.GIT_AUTHOR_EMAIL=V,k.GIT_COMMITTER_EMAIL=V;if(H)k.GIT_AUTHOR_NAME=H,k.GIT_COMMITTER_NAME=H;await y(["add","-A"],{cwd:z,env:k,logger:q,timeoutMs:Q});let C=!1;try{await y(["diff","--cached","--quiet"],{cwd:z,env:k,logger:q,timeoutMs:Q})}catch{C=!0}if(!C)return{committed:!1};await y(["commit","-m",J,"--no-verify","--trailer",`Co-authored-by: ${H||process.env.SHOGO_AGENT_GIT_NAME||"Shogo Agent"} <${V||process.env.SHOGO_AGENT_GIT_EMAIL||"agent@shogo.ai"}>`],{cwd:z,env:k,logger:q,timeoutMs:Q});let L=(await y(["rev-parse","HEAD"],{cwd:z,env:k,logger:q,timeoutMs:1e4})).stdout.trim(),O=["-c",`http.extraHeader=Authorization: Bearer ${Y}`];return await y([...O,"push",G,K],{cwd:z,env:k,logger:q,timeoutMs:Q}),{committed:!0,commitSha:L}}function Y0($){return R0(b0($,".git"))}var X6,t=null;var w0=T$(()=>{X6=$6(t$)});import{Command as i1}from"commander";import L$ from"picocolors";import w from"picocolors";import{existsSync as h6}from"node:fs";import{readFileSync as x$,writeFileSync as M$,existsSync as D$,chmodSync as P$}from"node:fs";import{hostname as S$}from"node:os";import{homedir as _$}from"node:os";import{join as x}from"node:path";import{mkdirSync as V0}from"node:fs";var h=x(_$(),".shogo"),j=x(h,"config.json"),e1=x(h,"credentials.json"),o=x(h,"device-id"),l=x(h,"worker.pid"),_0=x(h,"logs"),q0=x(_0,"worker.log"),H0=x(_0,"worker.err.log"),r=x(h,"runtime"),S=x(r,process.platform==="win32"?"agent-runtime.exe":"agent-runtime"),e=x(r,"version.json"),Q0=x(h,"projects");function g($,X=Q0){return x(X,$)}function m(){V0(h,{recursive:!0,mode:448}),V0(_0,{recursive:!0,mode:448})}function s0(){m(),V0(r,{recursive:!0,mode:448})}function t0($=Q0){m(),V0($,{recursive:!0,mode:448})}var x0={cloudUrl:"https://studio.shogo.ai",port:8002,projectsDir:Q0};function I(){if(!D$(j))return{};try{return JSON.parse(x$(j,"utf-8"))}catch($){throw Error(`Corrupt config at ${j}: ${$.message}`)}}function s($){m(),M$(j,JSON.stringify($,null,2),{mode:384}),P$(j,384)}function $2($,X){return{...$,...Object.fromEntries(Object.entries(X).filter(([,Y])=>Y!==void 0))}}function R($={}){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=$2($2(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??x0.cloudUrl,name:Z.name??S$(),workerDir:Z.workerDir??process.cwd(),port:Z.port??x0.port,projectsDir:Z.projectsDir??x0.projectsDir}}import{spawn as R$}from"node:child_process";import{existsSync as b$,openSync as X2,readFileSync as E$,unlinkSync as v$,writeFileSync as h$}from"node:fs";function G0(){if(!b$(l))return null;let $=E$(l,"utf-8").trim(),X=parseInt($,10);return Number.isFinite(X)?X:null}function W0($){try{return process.kill($,0),!0}catch{return!1}}function M0(){try{v$(l)}catch{}}function Y2($){m();let X=G0();if(X&&W0(X))throw Error(`Worker already running (pid=${X}). Run \`shogo worker stop\` first.`);if(X)M0();let Y=$.inheritStdio?["ignore","inherit","inherit"]:["ignore",X2(q0,"a"),X2(H0,"a")],Z=R$($.runner,[$.entry],{cwd:$.cwd,env:$.env,detached:!!$.detach,stdio:Y});if(!Z.pid)throw Error("Failed to spawn worker process.");if(h$(l,String(Z.pid),{mode:384}),$.detach)Z.unref();return{pid:Z.pid,child:Z}}function Z2($="SIGTERM"){let X=G0();if(!X)return{killedPid:null};if(!W0(X))return M0(),{killedPid:null};try{process.kill(X,$)}catch{}return M0(),{killedPid:X}}import M from"picocolors";import{existsSync as m$}from"node:fs";import{request as j$}from"node:http";function J2($,X=process.env){let Y=(J)=>J&&J.trim().length>0?J.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[J,K]of z){let V=Y(K);if(V)return{url:z2(V),source:J}}return null}function z2($){if(/^https?:\/\//i.test($))return $;return`http://${$}`}function D0($,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,J=(K)=>{if(z)return;z=!0,Z(K)};try{let K=new URL($.url),V=K.hostname,H=K.port?parseInt(K.port,10):K.protocol==="https:"?443:80,q=j$({method:"CONNECT",host:V,port:H,path:`${X}:443`,timeout:Y});q.on("connect",(Q)=>{q.destroy();let G=Q.statusCode??0;if(G===200)J({ok:!0,detail:`CONNECT ${X}:443 → 200 (via ${V}:${H})`});else if(G===407)J({ok:!1,detail:`407 Proxy Authentication Required — check credentials in ${$.source}`});else J({ok:!1,detail:`CONNECT → HTTP ${G}`})}),q.on("timeout",()=>{q.destroy(),J({ok:!1,detail:`timeout after ${Y}ms`})}),q.on("error",(Q)=>{let G=Q.code?` (${Q.code})`:"";J({ok:!1,detail:`${Q.message}${G}`})}),q.end()}catch(K){J({ok:!1,detail:K?.message??"unknown error"})}})}function V2($){let X;try{X=new URL($)}catch{return[]}let Y=X.host,Z=X.protocol.replace(":","")||"https",z=X.hostname.split("."),J=z.length>=2?z.slice(-2).join("."):X.hostname;return[{url:`${Z}://${Y}`,host:Y,purpose:"control",criticality:"fatal"},{url:`${Z}://api-direct.${J}`,host:`api-direct.${J}`,purpose:"tunnel-direct",criticality:"graceful"},{url:`${Z}://artifacts.${J}`,host:`artifacts.${J}`,purpose:"artifacts",criticality:"graceful"}]}var I$=5000;async function u$($,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 q2=($)=>{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 m$($.workerDir)?{ok:!0,detail:$.workerDir}:{ok:!1,detail:`${$.workerDir} does not exist`}}}];if($.proxy)X.push({name:`Proxy reachable (${f$($.proxy.url)})`,criticality:"fatal",async run(){return K2($.proxy)}});let Y=V2($.cloudUrl);for(let Z of Y)X.push({name:`Reach ${Z.host}${Z.purpose!=="control"?M.dim(` (${Z.purpose})`):""}`,criticality:Z.criticality,run:()=>u$(Z.url,I$)});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 f$($){try{return new URL($).host}catch{return $}}async function H2($){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
|
-
`)){let K=parseInt(J.trim(),10);if(Number.isFinite(K))Z+=K,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 $=
|
|
12
|
-
`)}class n0{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??P6}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}=x2($);if(!(Y.startsWith("/agent/")||Y==="/agent"))return null;if(!X){let K=this.getActiveProjects();if(K.length!==1)return null;X=K[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 J=await this.ensureRunning(X,z);if(!J.agentPort)return null;return this.touch(X),`http://127.0.0.1:${J.agentPort}${Y}${Z}`}deriveRuntimeToken($){return D2($)}describeRejection($,X){let{pathname:Y}=x2($);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"){let J=this.breakerCooldownMs(Y.breakerTrips),K=Date.now()-Y.lastFailureAt;if(K>=J)this.log.log(`[WorkerRuntimeManager] auto-heal: ${$} cooldown elapsed (${Math.round(K/1000)}s since trip #${Y.breakerTrips}) — `+"clearing circuit breaker and attempting a fresh spawn"),this.healBreaker(Y);else{let V=J-K;throw Error(`[WorkerRuntimeManager] cannot ensureRunning(${$}): ${Y.lastError??"runtime is in failed state"} Will auto-retry in ~${Math.ceil(V/1000)}s; call resetFailure(${$}) to retry immediately, or stop(${$}) to give up.`)}}X=await this.maybeAutoPull($,X);let Z=this.runtimes.get($);if(Z?.status==="running")return this.touch($),this.snapshot(Z);if(Z?.startPromise){let J=await Z.startPromise;return this.snapshot(J)}let z=Z??this.makeSlot($,X);if(!Z)this.runtimes.set($,z);z.spawnConfig=X,z.startPromise=this.doStart(z);try{let J=await z.startPromise;return this.enforceMaxRuntimes($),this.snapshot(J)}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((K)=>K.status==="running");if(Z.length<=X)return;let z=Z.filter((K)=>K.projectId!==$&&Y-K.lastUsedAt>=L6).sort((K,V)=>K.lastUsedAt-V.lastUsedAt),J=Z.length-X;for(let K of z){if(J<=0)break;let V=Y-K.lastUsedAt;this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} exceeded (${Z.length} running) — `+`LRU-evicting ${K.projectId} (idle ${Math.round(V/1000)}s)`),this.stop(K.projectId).catch((H)=>{this.log.warn(`[WorkerRuntimeManager] maxRuntimes eviction of ${K.projectId} failed: ${H?.message??H}`)}),J--}if(J>0)this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} still exceeded by ${J} 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&&h0(X.projectDir))return X;if(!Y)throw Error(f0($,"no-auto-pull-config",null));if(!Y.projectsDir)throw Error(f0($,"no-projects-dir",null));if(!Y.enabled){let K=Z0(Y.projectsDir,$);if(h0(K)&&!M2(K))return{...X,projectDir:K};throw Error(f0($,"auto-pull-disabled",K))}if(this.pulledProjects.has($))return{...X,projectDir:Z0(Y.projectsDir,$)};let Z=Z0(Y.projectsDir,$),z=Y.logger??this.log,J={cloneProject:Y.gitOps?.cloneProject??v0,gitIsAvailable:Y.gitOps?.gitIsAvailable??E0,isGitRepo:Y.gitOps?.isGitRepo??X0};this.pulledProjects.add($);try{A2(Z,{recursive:!0});let K=M2(Z),V=J.isGitRepo(Z),Q=(Y.useGit!==!1?await J.gitIsAvailable():!1)&&(K||V)?"git":"files";if(this.syncModes.set($,Q),Q==="git"){if(K){z.log(`[WorkerRuntimeManager] auto-pull: git clone project ${$} into ${Z}`);try{let G=await J.cloneProject({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z,shallow:!0,logger:z});z.log(`[WorkerRuntimeManager] auto-pull: ${$} cloned at ${G.commitSha.slice(0,8)}`)}catch(G){z.warn(`[WorkerRuntimeManager] auto-pull: git clone failed for ${$} (${G?.message??G}); 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(K)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 G=new j0({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z}),W=this.syncModes.get($)??"files",C=new Y0({rootDir:Z,transport:G,logger:z,mode:W,git:W==="git"?{apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$}:void 0});C.start(),this.watchers.set($,C)}catch(G){z.warn(`[WorkerRuntimeManager] auto-pull: watcher start failed for ${$}: ${G?.message??G}`)}}catch(K){z.warn(`[WorkerRuntimeManager] auto-pull: failed for ${$} — runtime will fall back to template defaults. `+`(${K?.message??K})`)}return{...X,projectDir:Z}}async fileTransportClone($,X,Y,Z){Z.log(`[WorkerRuntimeManager] auto-pull: file-transport clone of ${$} into ${X}`);let J=await new j0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}).downloadAll();Z.log(`[WorkerRuntimeManager] auto-pull: ${$} downloaded ${J.downloaded} files (${J.errors.length} errors)`)}async topUpShogoState($,X,Y,Z){try{let z=new j0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}),K=(await z.listManifest()).filter((H)=>H.path===".shogo"||H.path.startsWith(".shogo/"));if(K.length===0)return;let V=await z.downloadFiles(K);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}breakerCooldownMs($){let X=Math.max(0,$-1),Y=B6*Math.pow(2,X);return Math.min(Y,F6)}healBreaker($){$.status="stopped",$.consecutiveFailures=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(J){this.log.warn(`[WorkerRuntimeManager] watcher stop ${Z}: ${J?.message??J}`)}}));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,breakerTrips:0,restartTimestamps:[],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+B2;let Y=this.buildEnv($,X.path),Z=this.resolveCwd($),z=this.spawnCommand(X.path),J=this.resolveLimits(),K=z.command,V=z.args,H=!1;if(J){let k=w2({command:z.command,args:z.args,limits:J,scopeName:`shogo-rt-${$.projectId.slice(0,8)}-${Date.now().toString(36)}`,logger:this.log});K=k.command,V=k.args,H=k.wrapped}this.log.log(`[WorkerRuntimeManager] Spawning agent-runtime for ${$.projectId} via ${K} ${V.join(" ")} (port=${$.agentPort}, source=${X.source}${J?`, memCeil=${J.memoryMB}M`:""})`);let q=process.platform!=="win32",Q=q6(K,V,{cwd:Z,env:Y,detached:q,stdio:["ignore","pipe","pipe"]});if(q)try{Q.unref()}catch{}$.proc=Q,$.pid=Q.pid??null,$.status="starting",$.startedAt=Date.now();let G=$.spawnConfig.extraEnv?.SHOGO_OPEN_ID;if(L0("agent-spawned",$.projectId,G,{pid:$.pid,command:K,args:V,source:X.source}),J)try{this.applyPostSpawnLimits($,J,H)}catch(k){this.log.warn(`[WorkerRuntimeManager] failed to apply resource limits for ${$.projectId}: ${k?.message??k}`)}$.lastStdoutAt=$.startedAt,Q.on("error",(k)=>{$.lastError=k?.message??String(k),this.log.error(`[WorkerRuntimeManager] spawn error for ${$.projectId}: ${$.lastError}`)}),Q.on("exit",(k,L)=>{this.handleExit($,k,L)});let W=`[runtime:${$.projectId.slice(0,8)}]`,C=!0;Q.stdout?.on("data",(k)=>{if($.lastStdoutAt=Date.now(),C)C=!1,L0("agent-first-output",$.projectId,G,{elapsedMs:Date.now()-($.startedAt??Date.now())});for(let L of k.toString().trimEnd().split(`
|
|
13
|
-
`))if(L)this.log.log(`${
|
|
14
|
-
`))if(L)this.log.error(`${W} ${L}`)});try{return await this.waitForHealth($,_6),L0("agent-health-ready",$.projectId,G,{elapsedMs:Date.now()-($.startedAt??Date.now()),agentPort:$.agentPort}),$.status="running",$.lastUsedAt=Date.now(),this.armIdleTimer($),this.armGraceTimer($),$}catch(k){$.status="error",$.lastError=k?.message??String(k),this.killProcessGroup($,"SIGTERM");try{Q.kill("SIGTERM")}catch{}throw this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,k}}resolveLimits(){if(this.resolvedLimits===void 0)this.resolvedLimits=W2(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(C2({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{H6("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;$.breakerTrips=0,$.restartTimestamps=[]},T6);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+w6),NODE_ENV:"production",STARTUP_TIME:String(Date.now()),SHOGO_CLOUD_URL:Y.cloudUrl,SHOGO_API_URL:Y.cloudUrl,SHOGO_API_KEY:Y.apiKey,RUNTIME_AUTH_SECRET:D2($.projectId),WEBHOOK_TOKEN:S6($.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)O2(Z,z.memoryMB);return Z}resolveCwd($){let X=$.spawnConfig;if(X.projectDir&&h0(X.projectDir))return X.projectDir;let Y=this.opts.runtimeWorkDir??Z0(W6(),"shogo-runtime",$.projectId);return A2(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(),J=z-$.lastFailureAt<=T2;$.consecutiveFailures=J?$.consecutiveFailures+1:1,$.lastFailureAt=z,$.restarts+=1,$.lastError=`exited code=${X} signal=${Y}`,$.restartTimestamps.push(z),$.restartTimestamps=$.restartTimestamps.filter((q)=>z-q<=y2);let K=$.consecutiveFailures>=F2,V=$.restartTimestamps.length>=A6;if(K||V){$.status="failed",$.breakerTrips+=1;let q=this.breakerCooldownMs($.breakerTrips),Q=K?`${$.consecutiveFailures} consecutive non-clean exits within ${Math.round(T2/1000)}s`:`${$.restartTimestamps.length} non-clean exits within the last ${Math.round(y2/60000)}m (slow-bleed loop — gaps between crashes kept clearing the tight-loop window)`;if($.lastError=`Circuit breaker tripped (trip #${$.breakerTrips}): ${Q} (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. Will auto-retry in ~${Math.round(q/1000)}s; call resetFailure(${$.projectId}) to retry immediately, or stop(${$.projectId}) to give up.`,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 H=this.restartBackoffMs($.restarts);if($.status="restarting",this.log.warn(`[WorkerRuntimeManager] restarting ${$.projectId} in ${Math.round(H/1000)}s (restart #${$.restarts}, consecutive failures ${$.consecutiveFailures}/${F2})`),$.restartTimer)clearTimeout($.restartTimer);$.restartTimer=setTimeout(()=>{if($.restartTimer=null,$.status==="failed"||this.stopped)return;$.startPromise=this.doStart($).then((q)=>{return $.startPromise=null,q}).catch((q)=>{return $.startPromise=null,this.log.error(`[WorkerRuntimeManager] restart of ${$.projectId} failed: ${q?.message??q}`),$})},H);try{$.restartTimer.unref?.()}catch{}}restartBackoffMs($){let X=Math.min(U6*Math.pow(2,Math.max(0,$-1)),N6),Y=X*0.2*Math.random();return X+Y}armIdleTimer($){if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;let X=this.opts.idleMs??C6;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 $=I0-m0,X=Math.min($,50);for(let Y=0;Y<X;Y++){let Z=m0+Math.floor(Math.random()*$);if(Z+C0-1>I0)continue;let z=!0;for(let V=0;V<C0;V++)if(this.usedPorts.has(Z+V)){z=!1;break}if(!z)continue;let J=await this.isPortListening(Z),K=await this.isPortListening(Z+B2);if(J||K)continue;for(let V=0;V<C0;V++)this.usedPorts.add(Z+V);return Z}throw Error(`Cannot allocate port in range ${m0}-${I0} after ${X} attempts`)}releasePort($){if(!$)return;for(let X=0;X<C0;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=(J)=>{if(Y)return;Y=!0;try{z.destroy()}catch{}X(J)},z=k6({host:"127.0.0.1",port:$});z.setTimeout(x6),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(),J=z+X,K=null,V=!1,H=0,q=0,Q=0,G=0,W=z;while(Date.now()<J){if(G++,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 C=new AbortController,k=setTimeout(()=>C.abort(),1500);try{let O=await fetch(`http://127.0.0.1:${Y}/health`,{method:"GET",signal:C.signal});if(clearTimeout(k),O.ok){this.log.log(`[WorkerRuntimeManager] /health ready for ${$.projectId} on port ${Y} (HTTP ${O.status} after ${Date.now()-z}ms, ${G} iter, ${q} http)`);return}K=`HTTP /health returned ${O.status}`}catch(O){clearTimeout(k);let T=O?.name??"Error",c=O?.code??O?.cause?.code;K=`HTTP /health failed: ${T}${c?`(${c})`:""}: ${O?.message??O}`}if(Q++,V=await this.tcpProbe(Y),V){H=Date.now();let O=Date.now()-$.lastStdoutAt;if(O>=_2)throw Error(`agent-runtime wedged on port ${Y}: TCP listening but stdout silent for ${O}ms (> ${_2}ms window); ${q} /health attempts, last error: ${K??"n/a"}`)}let L=Date.now();if(L-W>=D6){let O=L-z,T=L-$.lastStdoutAt,c=H>0?L-H:null;this.log.log(`[WorkerRuntimeManager] still waiting for /health on ${$.projectId} port ${Y} (${(O/1000).toFixed(1)}s elapsed, tcpListening=${V}${c!=null?`(${c}ms ago)`:""}, lastStdout=${T}ms ago, ${q} http, ${Q} tcp, lastError=${K??"n/a"})`),W=L}await new Promise((O)=>setTimeout(O,y6))}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 ${G} iter (httpAttempts=${q}, tcpAttempts=${Q}, tcpListening=${V}, lastError=${K??"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 d0,platform as g0,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 v2 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,U0=300,p0=3,R6=p0,b6=1800000,E6=25000,R2=1000,b2=60000,E2=3;class c0{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 v2;return new WebSocket($,X)}getReconnectDelayMs(){let $=Math.min(R2*Math.pow(2,this.wsReconnectAttempt),b2),X=$*0.2*Math.random();return $+X}async collectMetadata(){let $={hostname:d0(),os:g0(),arch:i0(),uptime:process.uptime(),protocolVersion:E2,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=d0(),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:g0(),arch:i0(),kind:this.opts.kind??"cli-worker",metadata:X})});if(!z.ok)throw Error(`Heartbeat failed: HTTP ${z.status}`);let J=await z.json();if(typeof J.wsUrl==="string"&&J.wsUrl.length>0){if(J.wsUrl!==this.serverPublishedWsUrl)this.log.log(`[WorkerTunnel] Cloud advertised tunnel WS URL: ${J.wsUrl}`);this.serverPublishedWsUrl=J.wsUrl}return J}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>=p0){if(this.consecutiveAuthSuccesses++,this.consecutiveAuthSuccesses<R6){this.currentPollInterval=U0,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>=p0){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!==U0)this.log.warn(`[WorkerTunnel] ${this.consecutiveAuthFailures} consecutive auth failures — `+`backing off to ${U0}s. Re-authenticate with \`shogo login\`.`);this.currentPollInterval=U0}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 K=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:K.code,message:K.message,path:$.path})});return}let Z={...$.headers??{}};if($.projectId&&($.path.startsWith("/agent/")||$.path==="/agent")){let K=this.opts.resolver.deriveRuntimeToken($.projectId);if(K)Z["x-runtime-token"]=K}let z={method:$.method,headers:Z,signal:X.signal};if($.body&&$.method!=="GET"&&$.method!=="HEAD")z.body=$.body;let J=await fetch(Y,z);if($.stream){let K=J.body?.getReader();if(!K){this.sendFrame({type:"stream-error",requestId:$.requestId,error:"No response body for stream"});return}let V=new TextDecoder;try{while(!0){let{done:H,value:q}=await K.read();if(H)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(H){if(H?.name!=="AbortError"&&this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-error",requestId:$.requestId,error:H?.message??String(H)})}}else{let K=await J.text(),V={};J.headers.forEach((H,q)=>{V[q]=H}),this.sendFrame({type:"response",requestId:$.requestId,status:J.status,headers:V,body:K})}}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{}}},b6)}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{}},E6)}connectWs(){if(this.stopped||this.ws)return;let $=this.buildWsUrl(),X=d0(),Y=g0(),Z=i0(),z=this.opts.name??X;this.log.log(`[WorkerTunnel] Opening WebSocket to ${$} (hostname=${X})`);let J={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"}},K;try{K=this.createTunnelWebSocket($,J)}catch(V){this.log.error(`[WorkerTunnel] WebSocket creation failed: ${V?.message??V}`),this.ws=null,this.scheduleNextPoll(5);return}this.ws=K,K.onopen=()=>{this.log.log("[WorkerTunnel] WebSocket connected — session active"),this.wsReconnectAttempt=0,this.startWsHeartbeat(),this.resetWsIdleTimer()},K.onmessage=(V)=>{let H;try{let q=typeof V.data==="string"?V.data:V.data.toString();H=JSON.parse(q)}catch{return}if(H.type==="ping"){this.sendFrame({type:"pong"}),this.resetWsIdleTimer();return}if(H.type==="cancel"){let q=this.activeAbortControllers.get(H.requestId);if(q)q.abort();return}if(H.type==="request"){this.handleRequest(H).catch((q)=>{this.log.error(`[WorkerTunnel] Error handling request: ${q?.message??q}`)});return}},K.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 H=this.getReconnectDelayMs();this.log.log(`[WorkerTunnel] Reconnecting in ${Math.round(H/1000)}s (attempt ${this.wsReconnectAttempt})`),this.scheduleNextPoll(Math.ceil(H/1000))}},K.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:R2,BACKOFF_MAX_MS:b2,TUNNEL_PROTOCOL_VERSION:E2,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 h2($){let X=R({name:$.name,workerDir:$.workerDir,apiKey:$.apiKey,cloudUrl:$.cloudUrl,port:$.port?parseInt($.port,10):void 0,projectsDir:$.projectsDir}),Y=J2($.proxy);if(!$.foreground)return h6({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 H2(q2({cloudUrl:X.cloudUrl,apiKey:X.apiKey,workerDir:X.workerDir,proxy:Y})))process.exit(1)}let z=!$.noAutoPull,J=!$.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 ")+(J?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 K=`${X.cloudUrl.replace(/\/+$/,"")}/api/ai/v1`,V={cloudUrl:X.cloudUrl,apiKey:X.apiKey,aiProxyUrl:K,aiProxyToken:X.apiKey},H=new n0({runtimeBin:$.runtimeBin,defaultSpawnConfig:V,autoPull:{enabled:z,projectsDir:X.projectsDir,watch:!0,useGit:J}});if(!H.resolveBinary())console.error(w.red(n({flag:$.runtimeBin}))),process.exit(1);let q=new c0({apiKey:X.apiKey,cloudUrl:X.cloudUrl,name:X.name,kind:"cli-worker",resolver:H,onAuthRevoked:(
|
|
16
|
-
Received ${
|
|
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 j6($){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 m6(){let $=process.execPath,X=/\bbun(?:-[^/\\]*)?$/.test($)||typeof globalThis.Bun<"u",Y=process.argv[1];if(Y&&v6(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 j2 from"picocolors";async function m2(){let{killedPid:$}=Z2("SIGTERM");if($===null){console.log(j2.dim("No worker running."));return}console.log(j2.green(`✓ Worker stopped (pid=${$}).`))}import v from"picocolors";async function I2(){let $=G0();if(!$){console.log(v.yellow("● stopped")+v.dim(" (no pid file)"));return}if(!k0($)){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 I6}from"node:child_process";import{existsSync as u6,statSync as f6}from"node:fs";import u2 from"picocolors";async function f2($){let X=$.err?H0:q0;if(!u6(X)){console.log(u2.dim("No logs yet."));return}let Y=$.follow?["-F","-n","200",X]:["-n","200",X];if(I6("tail",Y,{stdio:"inherit"}).on("exit",(z)=>process.exit(z??0)),!$.follow){if(f6(X).size===0)console.log(u2.dim("(log file is empty)"))}}import _ from"picocolors";import{spawn as n6}from"node:child_process";import{hostname as d6,platform as g6,arch as i6}from"node:os";import f from"picocolors";class M extends Error{kind;constructor($,X){super($);this.kind=X;this.name="CloudLoginError"}}async function n2($){let X=$.cloudUrl.replace(/\/$/,""),Y=$.client??"cli",Z=$.deviceName??d6(),z=$.devicePlatform??`${g6()}-${i6()}`,J=$.appVersion??o6(),K=$.log??((O)=>console.log(O)),V=$.fetchImpl??fetch,H=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:J,workspaceId:$.workspaceId,client:Y}),signal:AbortSignal.timeout(1e4)}).catch((O)=>{throw new M(`Cannot reach Shogo Cloud at ${X}: ${O?.message??O}`,"transport")});if(!H.ok){let O=await H.text().catch(()=>"");throw new M(`Cloud rejected /api/cli/login/start (HTTP ${H.status}): ${O||"no body"}`,"transport")}let q=await H.json().catch(()=>({}));if(!q?.ok||!q.state||!q.authUrl)throw new M(`Cloud returned a malformed start response: ${q?.error??JSON.stringify(q)}`,"transport");let Q=p6($.pollIntervalMs??q.pollIntervalMs),G=$.timeoutMs??q.expiresInMs,W=Date.now()+G;if(K(""),K(f.bold("Sign in to Shogo Cloud")),K(f.dim(" cloud: ")+X),K(f.dim(" device: ")+`${Z} (${z})`),K(f.dim(" user code: ")+f.cyan(q.userCode)),K(""),K(" Open this URL in your browser to approve:"),K(" "+f.cyan(q.authUrl)),K(""),$.openBrowser!==!1)if(typeof $.openBrowser==="function")try{await $.openBrowser(q.authUrl)}catch{}else a6(q.authUrl).catch(()=>{});K(f.dim("Waiting for approval..."));let C=$.installSignalHandlers??!0,k=()=>{L.abort()},L=new AbortController;if($.abortSignal)if($.abortSignal.aborted)L.abort();else $.abortSignal.addEventListener("abort",()=>L.abort(),{once:!0});if(C)process.once("SIGINT",k),process.once("SIGTERM",k);try{while(!0){if(L.signal.aborted)throw new M("Aborted by caller.","cancelled");if(Date.now()>=W)throw new M(`Timed out after ${Math.round(G/1000)}s waiting for approval.`,"timeout");let O=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 K(f.dim(` (poll error: ${T?.message??T} — retrying)`)),null});if(O&&O.ok){let T=await O.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 M("Sign-in was denied in the browser.","denied");if(T?.status==="expired")throw new M("Sign-in request expired before it was approved.","expired")}await c6(Q,L.signal)}}finally{if(C)process.removeListener("SIGINT",k),process.removeListener("SIGTERM",k)}}function p6($){if(!Number.isFinite($)||$<=0)return 2000;return Math.min(Math.max($,1000),1e4)}function c6($,X){return new Promise((Y,Z)=>{if(X?.aborted)return Z(new M("Aborted by caller.","cancelled"));let z=setTimeout(()=>{X?.removeEventListener("abort",J),Y()},$),J=()=>{clearTimeout(z),Z(new M("Aborted by caller.","cancelled"))};X?.addEventListener("abort",J,{once:!0})})}function o6(){return d()}function a6($){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 l6,writeFileSync as r6,existsSync as e6,chmodSync as s6}from"node:fs";import{randomUUID as t6}from"node:crypto";function d2(){if(e6(o)){let X=l6(o,"utf-8").trim();if(X)return X}m();let $=t6();return r6(o,$,{mode:384}),s6(o,384),$}var $1="https://studio.shogo.ai";async function g2($){let X=I(),Y=($.cloudUrl||X.cloudUrl||$1).replace(/\/$/,""),Z=$.apiKey||process.env.SHOGO_API_KEY;if(Z){await X1({key:Z,cloudUrl:Y,cfg:X,name:$.name});return}let z=d2(),J;try{J=await n2({cloudUrl:Y,deviceId:z,deviceName:$.name,workspaceId:$.workspace,openBrowser:!$.noBrowser})}catch(K){if(K instanceof M){if(console.error(_.red(`✗ ${K.message}`)),K.kind==="transport")console.error(_.dim(" If your network blocks browsers, run with --api-key <key> instead."));process.exit(1)}throw K}if(X.apiKey=J.key,X.cloudUrl=Y,$.name)X.name=$.name;if(e(X),console.log(""),console.log(_.green("✓ Signed in to Shogo Cloud")),J.workspace)console.log(_.dim(" workspace: ")+J.workspace);if(J.email)console.log(_.dim(" email: ")+J.email);console.log(_.dim(" saved to: ")+"~/.shogo/config.json"),console.log(""),console.log(_.dim(" next: ")+"shogo worker start")}async function X1({key:$,cloudUrl:X,cfg:Y,name:Z}){if(!/^shogo_sk_/.test($))console.error(_.red('✗ API key should start with "shogo_sk_". Copy it verbatim from the API Keys page.')),process.exit(1);let z;try{let J=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 J.json().catch(()=>({})),!J.ok||!z?.valid)console.error(_.red(`✗ ${z?.error||`Cloud rejected the key (HTTP ${J.status}).`}`)),process.exit(1)}catch(J){console.error(_.red(`✗ Cannot reach Shogo Cloud at ${X}: ${J?.message??J}`)),process.exit(1)}if(Y.apiKey=$,Y.cloudUrl=X,Z)Y.name=Z;if(e(Y),console.log(_.green("✓ API key saved to ~/.shogo/config.json")),z.workspace?.name)console.log(_.dim(" workspace: ")+z.workspace.name);if(z.user?.email)console.log(_.dim(" email: ")+z.user.email);console.log(_.dim(" next: ")+"shogo worker start")}import J0 from"picocolors";async function i2(){let $=I();if(Object.keys($).length===0){console.log(J0.dim("(no config — run `shogo login` or `shogo config set`)"));return}let X={...$,apiKey:$.apiKey?`***${$.apiKey.slice(-4)}`:void 0};console.log(J0.dim(`file: ${j}`)),console.log(JSON.stringify(X,null,2))}async function p2($,X){let Y=["apiKey","cloudUrl","name","workerDir","port"];if(!Y.includes($))console.error(J0.red(`Unknown key: ${$}`)),console.error(J0.dim(`Allowed: ${Y.join(", ")}`)),process.exit(1);let Z=I();Z[$]=$==="port"?parseInt(X,10):X,e(Z),console.log(J0.green(`✓ ${$} set`))}import A from"picocolors";import{spawn as Y1}from"node:child_process";import{createHash as Z1}from"node:crypto";import{createWriteStream as z1,existsSync as K0,mkdirSync as o0,readFileSync as a2,renameSync as c2,rmSync as a0,writeFileSync as J1}from"node:fs";import{tmpdir as K1}from"node:os";import{dirname as V1,join as N0}from"node:path";import{Readable as q1}from"node:stream";import{pipeline as H1}from"node:stream/promises";var Q1="https://github.com/shogo-labs/shogo-ai/releases/download";function l2(){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 A0(){if(!K0(r))return null;try{let $=a2(r,"utf-8");return JSON.parse($)}catch{return null}}async function G1($,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`,G=await fetch(Q,{headers:{Accept:"application/vnd.github+json"}});if(!G.ok)throw Error(`GitHub API ${G.status} for ${Q}`);let W=await G.json();if(!W.tag_name)throw Error("GitHub API did not return tag_name");return o2(W.tag_name)}let J=`https://api.github.com/repos/${Z}/${z}/releases?per_page=30`,K=await fetch(J,{headers:{Accept:"application/vnd.github+json"}});if(!K.ok)throw Error(`GitHub API ${K.status} for ${J}`);let V=await K.json(),H=$==="beta"?"-beta":"-nightly",q=V.find((Q)=>Q.prerelease&&/^v\d+\.\d+\.\d+-/.test(Q.tag_name)&&Q.tag_name.includes(H));if(!q)throw Error(`No ${$} runtime release found`);return o2(q.tag_name)}function o2($){if(!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test($))throw Error(`Unexpected app tag '${$}' (expected vX.Y.Z[-prerelease])`);return $.slice(1)}function k1($,X,Y){let Z=`shogo-agent-runtime-${X}.tar.gz`,z=`v${$}`,J=`${Y.replace(/\/$/,"")}/${z}/${Z}`;return{tarball:J,sha256:`${J}.sha256`,assetName:Z}}async function W1($,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 H1(q1.fromWeb(Y.body),z1(X))}async function O1($){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 w1($){let X=a2($);return Z1("sha256").update(X).digest("hex")}async function C1($,X){await new Promise((Y,Z)=>{let z=Y1("tar",["-xzf",$,"-C",X],{stdio:["ignore","pipe","pipe"]}),J="";z.stderr.on("data",(K)=>{J+=K.toString()}),z.on("error",Z),z.on("exit",(K)=>{if(K===0)Y();else Z(Error(`tar exited ${K}: ${J.trim()}`))})})}function L1($,X){s0();let Y=V1(X);if(!K0(Y))o0(Y,{recursive:!0,mode:448});let Z=`${X}.next`;if(K0(Z))a0(Z,{force:!0});c2($,Z);try{if(process.platform!=="win32"){let{chmodSync:z}=y0("node:fs");z(Z,493)}}catch{}if(K0(X))a0(X,{force:!0});c2(Z,X)}async function r2($={}){let X=$.logger??console,Y=$.channel??"stable",Z=$.baseUrl??process.env.SHOGO_RUNTIME_RELEASES_URL??Q1,z=$.target??l2(),J=$.version;if(!J)X.log(`[runtime install] Resolving latest ${Y} version...`),J=await G1(Y,Z),X.log(`[runtime install] Latest ${Y} = ${J}`);let K=A0();if(K&&K.version===J&&K.target===z&&!$.force)return X.log(`[runtime install] ${J} (${z}) already installed at ${S} — pass --force to reinstall`),{version:J,target:z,binPath:S,source:K.source,sha256:K.sha256,channel:Y};let V=k1(J,z,Z);X.log(`[runtime install] Downloading ${V.tarball}`);let H=N0(K1(),`shogo-runtime-install-${process.pid}-${Date.now()}`);o0(H,{recursive:!0});try{let q=N0(H,V.assetName);await W1(V.tarball,q),X.log("[runtime install] Verifying SHA-256...");let Q=await O1(V.sha256),G=w1(q);if(G!==Q)throw Error(`SHA-256 mismatch for ${V.assetName}
|
|
9
|
+
`));return!X}import{accessSync as n$,constants as d$,existsSync as g$}from"node:fs";import{delimiter as i$,join as p$}from"node:path";function u($={}){let X=$.env??process.env,Y=c$($);for(let J of Y)if(G2(J.path))return J;let Z=$.systemBinName??Q2(),z=a$(Z,X.PATH);if(z)return{path:z,source:"path"};return null}function c$($){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:S,source:"home"}),Y}function Q2(){return process.platform==="win32"?"shogo-agent-runtime.exe":"shogo-agent-runtime"}function G2($){if(!g$($))return!1;try{if(process.platform==="win32")return!0;return n$($,d$.X_OK),!0}catch{return!1}}function a$($,X){if(!X)return null;for(let Y of X.split(i$)){if(!Y)continue;let Z=p$(Y,$);if(G2(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(` ${S} (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 Q6,spawnSync as G6}from"node:child_process";import{createHmac as S2,randomBytes as W6}from"node:crypto";import{existsSync as h0,mkdirSync as N2,readdirSync as R2}from"node:fs";import{createConnection as k6}from"node:net";import{tmpdir as O6}from"node:os";import{dirname as w6,join as p}from"node:path";import{spawnSync as P0}from"node:child_process";import{existsSync as o$,mkdirSync as l$,writeFileSync as k0}from"node:fs";import{join as i}from"node:path";var W2=1e5;function k2($){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),J=Number.isFinite(z)&&z>0?z:4096;return{memoryMB:X,cpuPercent:Z,tasksMax:J}}function O2($,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 r$(){if(process.platform!=="linux")return!1;try{return P0("systemd-run",["--version"],{stdio:"ignore",timeout:3000}).status===0}catch{return!1}}function w2($){let{command:X,args:Y,limits:Z,scopeName:z,logger:J}=$;if(process.platform!=="linux"||!r$())return{command:X,args:Y,wrapped:!1};let K=["-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)K.push("-p",`CPUQuota=${Z.cpuPercent}%`);let V=["--user","--scope","--quiet","--collect",`--unit=${z}`,...K,"--",X,...Y];return J?.log(`[resource-limits] wrapping spawn in systemd scope ${z} (MemoryMax=${Z.memoryMB}M)`),{command:"systemd-run",args:V,wrapped:!0}}function C2($){let{pid:X,limits:Y,logger:Z}=$;if(process.platform!=="linux")return!1;let z="/sys/fs/cgroup";if(!o$(i(z,"cgroup.controllers")))return!1;let J=i(z,"shogo",`rt-${X}`);try{if(l$(J,{recursive:!0}),k0(i(J,"memory.max"),String(Y.memoryMB*1024*1024)),Y.cpuPercent>0){let K=Math.floor(Y.cpuPercent/100*W2);k0(i(J,"cpu.max"),`${K} ${W2}`)}try{k0(i(J,"pids.max"),String(Y.tasksMax))}catch{}return k0(i(J,"cgroup.procs"),String(X)),Z?.log(`[resource-limits] attached pid ${X} to cgroup ${J} (memory.max=${Y.memoryMB}M)`),!0}catch(K){return Z?.warn(`[resource-limits] direct cgroup attach failed for pid ${X}: ${K?.message??K}`),!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 J=y0(z);if(typeof J.limitProcess!=="function")return!1;let K=J.limitProcess(X,{memoryBytes:Y.memoryMB*1024*1024,killOnClose:!0});if(K)Z?.log(`[resource-limits] attached pid ${X} to Job Object (${Y.memoryMB}M)`);return K}catch{return!1}}function e$($,X=process.platform){try{if(X==="win32"){let J=`$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)`,K=P0("powershell.exe",["-NoProfile","-NonInteractive","-Command",J],{encoding:"utf-8",timeout:1e4,windowsHide:!0});if(K.status!==0||!K.stdout)return null;let V=parseInt(K.stdout.trim(),10);return Number.isFinite(V)?V:null}let Y=P0("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 J of Y.stdout.split(`
|
|
11
|
+
`)){let K=parseInt(J.trim(),10);if(Number.isFinite(K))Z+=K,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 $=e$(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 j0}from"@shogo-ai/sdk/cloud-file-transport";import{killViteWatchFromPidfile as B2}from"@shogo-ai/sdk/vite-watch";import{watch as A2,statSync as Z6}from"node:fs";import{relative as DX,sep as z6,posix as J6}from"node:path";var K6=1500,V6=new Set(["node_modules",".git","dist","build",".vite",".cache"]),q6=[".shogo/"];function H6($){if(!$)return!0;return $.split("/").some((X)=>V6.has(X))}class Z0{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??K6,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(),U2))).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(z6).join(J6.sep);if(H6(X))return;try{let Y=`${this.rootDir}/${X}`;if(Z6(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 J of $)if(q6.some((K)=>J===K.slice(0,-1)||J.startsWith(K)))X.push(J);else Y.push(J);if(X.length>0){let J=await this.transport.uploadFiles(X);if(J.errors.length>0)for(let K of J.errors)this.logger.warn(`[CloudSyncWatcher] upload ${K.path}: ${K.message}`)}let Z=!1,z;if(Y.length>0){let J=`auto: ${new Date().toISOString()}`;try{let K=await this.commitAndPush({apiUrl:this.git.apiUrl,apiKey:this.git.apiKey,projectId:this.git.projectId,localDir:this.rootDir,message:J,branch:this.git.branch,authorEmail:this.git.authorEmail,authorName:this.git.authorName,logger:this.logger});Z=K.committed,z=K.commitSha}catch(K){throw K}}this.onFlush?.({uploaded:$,errors:0,committed:Z,commitSha:z})}}w0();var m0=37100,I0=37900,F2=1,C6=2,C0=16,L6=900000,U6=30000,A6=1000,N6=60000,T2=8,y2=300000,_2=3600000,B6=8,F6=120000,T6=900000,y6=60000,_6=500,x6=30000,x2=25000,M6=5000,D6=500,P6=process.env.SHOGO_PERF_LOG==="1";function L0($,X,Y,Z){if(!P6)return;console.log(`[shogo-perf] ${JSON.stringify({perf:"open",source:"worker",phase:$,projectId:X,openAttemptId:Y,atMs:Date.now(),...Z})}`)}var S6=($)=>({command:$,args:[]}),u0=null;function b2(){if(!u0)u0=W6(32).toString("hex");return u0}function M2($){return S2("sha256",b2()).update(`runtime:${$}`).digest("hex")}function R6($){return S2("sha256",b2()).update(`webhook:${$}`).digest("hex")}function D2($){let X=$.indexOf("?");if(X===-1)return{pathname:$,search:""};return{pathname:$.slice(0,X),search:$.slice(X)}}function P2($){try{return R2($).length===0}catch{return!0}}function f0($,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 n0{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??S6,this.sweepOrphanedViteWatches()}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}=D2($);if(!(Y.startsWith("/agent/")||Y==="/agent"))return null;if(!X){let K=this.getActiveProjects();if(K.length!==1)return null;X=K[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 J=await this.ensureRunning(X,z);if(!J.agentPort)return null;return this.touch(X),`http://127.0.0.1:${J.agentPort}${Y}${Z}`}deriveRuntimeToken($){return M2($)}describeRejection($,X){let{pathname:Y}=D2($);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"){let J=this.breakerCooldownMs(Y.breakerTrips),K=Date.now()-Y.lastFailureAt;if(K>=J)this.log.log(`[WorkerRuntimeManager] auto-heal: ${$} cooldown elapsed (${Math.round(K/1000)}s since trip #${Y.breakerTrips}) — `+"clearing circuit breaker and attempting a fresh spawn"),this.healBreaker(Y);else{let V=J-K;throw Error(`[WorkerRuntimeManager] cannot ensureRunning(${$}): ${Y.lastError??"runtime is in failed state"} Will auto-retry in ~${Math.ceil(V/1000)}s; call resetFailure(${$}) to retry immediately, or stop(${$}) to give up.`)}}X=await this.maybeAutoPull($,X);let Z=this.runtimes.get($);if(Z?.status==="running")return this.touch($),this.snapshot(Z);if(Z?.startPromise){let J=await Z.startPromise;return this.snapshot(J)}let z=Z??this.makeSlot($,X);if(!Z)this.runtimes.set($,z);z.spawnConfig=X,z.startPromise=this.doStart(z);try{let J=await z.startPromise;return this.enforceMaxRuntimes($),this.snapshot(J)}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((K)=>K.status==="running");if(Z.length<=X)return;let z=Z.filter((K)=>K.projectId!==$&&Y-K.lastUsedAt>=U6).sort((K,V)=>K.lastUsedAt-V.lastUsedAt),J=Z.length-X;for(let K of z){if(J<=0)break;let V=Y-K.lastUsedAt;this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} exceeded (${Z.length} running) — `+`LRU-evicting ${K.projectId} (idle ${Math.round(V/1000)}s)`),this.stop(K.projectId,"SIGTERM","lru-evict").catch((H)=>{this.log.warn(`[WorkerRuntimeManager] maxRuntimes eviction of ${K.projectId} failed: ${H?.message??H}`)}),J--}if(J>0)this.log.log(`[WorkerRuntimeManager] maxRuntimes=${X} still exceeded by ${J} 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&&h0(X.projectDir))return X;if(!Y)throw Error(f0($,"no-auto-pull-config",null));if(!Y.projectsDir)throw Error(f0($,"no-projects-dir",null));if(!Y.enabled){let K=p(Y.projectsDir,$);if(h0(K)&&!P2(K))return{...X,projectDir:K};throw Error(f0($,"auto-pull-disabled",K))}if(this.pulledProjects.has($))return{...X,projectDir:p(Y.projectsDir,$)};let Z=p(Y.projectsDir,$),z=Y.logger??this.log,J={cloneProject:Y.gitOps?.cloneProject??v0,gitIsAvailable:Y.gitOps?.gitIsAvailable??E0,isGitRepo:Y.gitOps?.isGitRepo??Y0};this.pulledProjects.add($);try{N2(Z,{recursive:!0});let K=P2(Z),V=J.isGitRepo(Z),Q=(Y.useGit!==!1?await J.gitIsAvailable():!1)&&(K||V)?"git":"files";if(this.syncModes.set($,Q),Q==="git"){if(K){z.log(`[WorkerRuntimeManager] auto-pull: git clone project ${$} into ${Z}`);try{let G=await J.cloneProject({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z,shallow:!0,logger:z});z.log(`[WorkerRuntimeManager] auto-pull: ${$} cloned at ${G.commitSha.slice(0,8)}`)}catch(G){z.warn(`[WorkerRuntimeManager] auto-pull: git clone failed for ${$} (${G?.message??G}); 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(K)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 G=new j0({apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$,localDir:Z}),k=this.syncModes.get($)??"files",C=new Z0({rootDir:Z,transport:G,logger:z,mode:k,git:k==="git"?{apiUrl:X.cloudUrl,apiKey:X.apiKey,projectId:$}:void 0});C.start(),this.watchers.set($,C)}catch(G){z.warn(`[WorkerRuntimeManager] auto-pull: watcher start failed for ${$}: ${G?.message??G}`)}}catch(K){z.warn(`[WorkerRuntimeManager] auto-pull: failed for ${$} — runtime will fall back to template defaults. `+`(${K?.message??K})`)}return{...X,projectDir:Z}}async fileTransportClone($,X,Y,Z){Z.log(`[WorkerRuntimeManager] auto-pull: file-transport clone of ${$} into ${X}`);let J=await new j0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}).downloadAll();Z.log(`[WorkerRuntimeManager] auto-pull: ${$} downloaded ${J.downloaded} files (${J.errors.length} errors)`)}async topUpShogoState($,X,Y,Z){try{let z=new j0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:X}),K=(await z.listManifest()).filter((H)=>H.path===".shogo"||H.path.startsWith(".shogo/"));if(K.length===0)return;let V=await z.downloadFiles(K);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",Y="stop"){let Z=this.runtimes.get($);if(!Z)return;if(Z.status="stopping",Z.restartTimer)clearTimeout(Z.restartTimer),Z.restartTimer=null;if(Z.idleTimer)clearTimeout(Z.idleTimer),Z.idleTimer=null;if(Z.graceTimer)clearTimeout(Z.graceTimer),Z.graceTimer=null;if(Z.limitWatchdog)Z.limitWatchdog.stop(),Z.limitWatchdog=null;if(Z.proc){this.killProcessGroup(Z,X),this.killViteWatch(Z);try{Z.proc.kill(X)}catch{}await this.waitForExit(Z.proc,5000),this.killProcessGroup(Z,"SIGKILL"),this.killViteWatch(Z)}Z.pid=null,this.releasePort(Z.agentPort),this.runtimes.delete($),this.notifyGone($,{reason:Y})}notifyGone($,X){let Y=this.opts.onRuntimeGone;if(!Y)return;try{Y($,X)}catch(Z){this.log.warn(`[WorkerRuntimeManager] onRuntimeGone(${$}) threw: ${Z?.message??Z}`)}}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}breakerCooldownMs($){let X=Math.max(0,$-1),Y=F6*Math.pow(2,X);return Math.min(Y,T6)}healBreaker($){$.status="stopped",$.consecutiveFailures=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(J){this.log.warn(`[WorkerRuntimeManager] watcher stop ${Z}: ${J?.message??J}`)}}));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,breakerTrips:0,restartTimestamps:[],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+F2;let Y=this.buildEnv($,X.path),Z=this.resolveCwd($),z=this.spawnCommand(X.path),J=this.resolveLimits(),K=z.command,V=z.args,H=!1;if(J){let W=w2({command:z.command,args:z.args,limits:J,scopeName:`shogo-rt-${$.projectId.slice(0,8)}-${Date.now().toString(36)}`,logger:this.log});K=W.command,V=W.args,H=W.wrapped}this.log.log(`[WorkerRuntimeManager] Spawning agent-runtime for ${$.projectId} via ${K} ${V.join(" ")} (port=${$.agentPort}, source=${X.source}${J?`, memCeil=${J.memoryMB}M`:""})`);let q=process.platform!=="win32",Q=Q6(K,V,{cwd:Z,env:Y,detached:q,stdio:["ignore","pipe","pipe"]});if(q)try{Q.unref()}catch{}$.proc=Q,$.pid=Q.pid??null,$.status="starting",$.startedAt=Date.now();let G=$.spawnConfig.extraEnv?.SHOGO_OPEN_ID;if(L0("agent-spawned",$.projectId,G,{pid:$.pid,command:K,args:V,source:X.source}),J)try{this.applyPostSpawnLimits($,J,H)}catch(W){this.log.warn(`[WorkerRuntimeManager] failed to apply resource limits for ${$.projectId}: ${W?.message??W}`)}$.lastStdoutAt=$.startedAt,Q.on("error",(W)=>{$.lastError=W?.message??String(W),this.log.error(`[WorkerRuntimeManager] spawn error for ${$.projectId}: ${$.lastError}`)}),Q.on("exit",(W,L)=>{this.handleExit($,W,L)});let k=`[runtime:${$.projectId.slice(0,8)}]`,C=!0;Q.stdout?.on("data",(W)=>{if($.lastStdoutAt=Date.now(),C)C=!1,L0("agent-first-output",$.projectId,G,{elapsedMs:Date.now()-($.startedAt??Date.now())});for(let L of W.toString().trimEnd().split(`
|
|
13
|
+
`))if(L)this.log.log(`${k} ${L}`)}),Q.stderr?.on("data",(W)=>{if($.lastStdoutAt=Date.now(),C)C=!1,L0("agent-first-output",$.projectId,G,{elapsedMs:Date.now()-($.startedAt??Date.now()),stream:"stderr"});for(let L of W.toString().trimEnd().split(`
|
|
14
|
+
`))if(L)this.log.error(`${k} ${L}`)});try{return await this.waitForHealth($,x6),L0("agent-health-ready",$.projectId,G,{elapsedMs:Date.now()-($.startedAt??Date.now()),agentPort:$.agentPort}),$.status="running",$.lastUsedAt=Date.now(),this.armIdleTimer($),this.armGraceTimer($),$}catch(W){$.status="error",$.lastError=W?.message??String(W),this.killProcessGroup($,"SIGTERM"),this.killViteWatch($);try{Q.kill("SIGTERM")}catch{}throw this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,W}}resolveLimits(){if(this.resolvedLimits===void 0)this.resolvedLimits=k2(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(C2({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"),this.killViteWatch($);try{$.proc?.kill("SIGTERM")}catch{}let Z=setTimeout(()=>{this.killProcessGroup($,"SIGKILL"),this.killViteWatch($)},5000);try{Z.unref?.()}catch{}}killProcessGroup($,X){if(!$.pid)return;if(process.platform==="win32"){try{G6("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;$.breakerTrips=0,$.restartTimestamps=[]},y6);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+C6),NODE_ENV:"production",STARTUP_TIME:String(Date.now()),SHOGO_CLOUD_URL:Y.cloudUrl,SHOGO_API_URL:Y.cloudUrl,SHOGO_API_KEY:Y.apiKey,RUNTIME_AUTH_SECRET:M2($.projectId),WEBHOOK_TOKEN:R6($.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=p(w6(X),"tree-sitter-wasm");if(Y.extraEnv)Object.assign(Z,Y.extraEnv);let z=this.resolveLimits();if(z)O2(Z,z.memoryMB);return Z}killViteWatch($){let X=$.spawnConfig.projectDir;if(!X)return;try{B2(X,{logger:this.log})}catch(Y){this.log.warn(`[WorkerRuntimeManager] Vite watch cleanup for ${$.projectId} failed: ${Y?.message??Y}`)}}sweepOrphanedViteWatches(){let $=this.opts.autoPull?.projectsDir;if(!$)return;let X;try{X=R2($,{withFileTypes:!0})}catch{return}for(let Y of X){if(!Y.isDirectory()||this.runtimes.has(Y.name))continue;let Z=p($,Y.name);try{let z=B2(Z,{logger:this.log});if(z)this.log.log(`[WorkerRuntimeManager] Reaped orphaned Vite watch for ${Y.name} (pid=${z.pid})`)}catch(z){this.log.warn(`[WorkerRuntimeManager] Vite watch startup sweep failed for ${Y.name}: ${z?.message??z}`)}}}resolveCwd($){let X=$.spawnConfig;if(X.projectDir&&h0(X.projectDir))return X.projectDir;let Y=this.opts.runtimeWorkDir??p(O6(),"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(this.killViteWatch($),$.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),this.notifyGone($.projectId,{reason:"exited",code:X,signal:Y});return}this.killProcessGroup($,"SIGKILL"),this.killViteWatch($),$.pid=null;let z=Date.now(),J=z-$.lastFailureAt<=y2;$.consecutiveFailures=J?$.consecutiveFailures+1:1,$.lastFailureAt=z,$.restarts+=1,$.lastError=`exited code=${X} signal=${Y}`,$.restartTimestamps.push(z),$.restartTimestamps=$.restartTimestamps.filter((q)=>z-q<=_2);let K=$.consecutiveFailures>=T2,V=$.restartTimestamps.length>=B6;if(K||V){$.status="failed",$.breakerTrips+=1;let q=this.breakerCooldownMs($.breakerTrips),Q=K?`${$.consecutiveFailures} consecutive non-clean exits within ${Math.round(y2/1000)}s`:`${$.restartTimestamps.length} non-clean exits within the last ${Math.round(_2/60000)}m (slow-bleed loop — gaps between crashes kept clearing the tight-loop window)`;if($.lastError=`Circuit breaker tripped (trip #${$.breakerTrips}): ${Q} (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. Will auto-retry in ~${Math.round(q/1000)}s; call resetFailure(${$.projectId}) to retry immediately, or stop(${$.projectId}) to give up.`,this.releasePort($.agentPort),$.agentPort=0,$.apiServerPort=0,$.restartTimer)clearTimeout($.restartTimer),$.restartTimer=null;if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;this.log.error(`[WorkerRuntimeManager] ${$.lastError}`),this.notifyGone($.projectId,{reason:"failed",code:X,signal:Y});return}let H=this.restartBackoffMs($.restarts);if($.status="restarting",this.log.warn(`[WorkerRuntimeManager] restarting ${$.projectId} in ${Math.round(H/1000)}s (restart #${$.restarts}, consecutive failures ${$.consecutiveFailures}/${T2})`),$.restartTimer)clearTimeout($.restartTimer);$.restartTimer=setTimeout(()=>{if($.restartTimer=null,$.status==="failed"||this.stopped)return;$.startPromise=this.doStart($).then((q)=>{return $.startPromise=null,q}).catch((q)=>{return $.startPromise=null,this.log.error(`[WorkerRuntimeManager] restart of ${$.projectId} failed: ${q?.message??q}`),$})},H);try{$.restartTimer.unref?.()}catch{}}restartBackoffMs($){let X=Math.min(A6*Math.pow(2,Math.max(0,$-1)),N6),Y=X*0.2*Math.random();return X+Y}armIdleTimer($){if($.idleTimer)clearTimeout($.idleTimer),$.idleTimer=null;let X=this.opts.idleMs??L6;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,"SIGTERM","idle-evict").catch((Z)=>{this.log.warn(`[WorkerRuntimeManager] idle stop failed: ${Z?.message??Z}`)})},X)}async allocatePort(){let $=I0-m0,X=Math.min($,50);for(let Y=0;Y<X;Y++){let Z=m0+Math.floor(Math.random()*$);if(Z+C0-1>I0)continue;let z=!0;for(let V=0;V<C0;V++)if(this.usedPorts.has(Z+V)){z=!1;break}if(!z)continue;let J=await this.isPortListening(Z),K=await this.isPortListening(Z+F2);if(J||K)continue;for(let V=0;V<C0;V++)this.usedPorts.add(Z+V);return Z}throw Error(`Cannot allocate port in range ${m0}-${I0} after ${X} attempts`)}releasePort($){if(!$)return;for(let X=0;X<C0;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=(J)=>{if(Y)return;Y=!0;try{z.destroy()}catch{}X(J)},z=k6({host:"127.0.0.1",port:$});z.setTimeout(D6),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(),J=z+X,K=null,V=!1,H=0,q=0,Q=0,G=0,k=z;while(Date.now()<J){if(G++,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 C=new AbortController,W=setTimeout(()=>C.abort(),1500);try{let O=await fetch(`http://127.0.0.1:${Y}/health`,{method:"GET",signal:C.signal});if(clearTimeout(W),O.ok){this.log.log(`[WorkerRuntimeManager] /health ready for ${$.projectId} on port ${Y} (HTTP ${O.status} after ${Date.now()-z}ms, ${G} iter, ${q} http)`);return}K=`HTTP /health returned ${O.status}`}catch(O){clearTimeout(W);let T=O?.name??"Error",a=O?.code??O?.cause?.code;K=`HTTP /health failed: ${T}${a?`(${a})`:""}: ${O?.message??O}`}if(Q++,V=await this.tcpProbe(Y),V){H=Date.now();let O=Date.now()-$.lastStdoutAt;if(O>=x2)throw Error(`agent-runtime wedged on port ${Y}: TCP listening but stdout silent for ${O}ms (> ${x2}ms window); ${q} /health attempts, last error: ${K??"n/a"}`)}let L=Date.now();if(L-k>=M6){let O=L-z,T=L-$.lastStdoutAt,a=H>0?L-H:null;this.log.log(`[WorkerRuntimeManager] still waiting for /health on ${$.projectId} port ${Y} (${(O/1000).toFixed(1)}s elapsed, tcpListening=${V}${a!=null?`(${a}ms ago)`:""}, lastStdout=${T}ms ago, ${q} http, ${Q} tcp, lastError=${K??"n/a"})`),k=L}await new Promise((O)=>setTimeout(O,_6))}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 ${G} iter (httpAttempts=${q}, tcpAttempts=${Q}, tcpListening=${V}, lastError=${K??"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 d0,platform as g0,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 j2 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,U0=300,p0=3,b6=p0,E6=1800000,v6=25000,E2=1000,v2=60000,h2=3;class c0{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 j2;return new WebSocket($,X)}getReconnectDelayMs(){let $=Math.min(E2*Math.pow(2,this.wsReconnectAttempt),v2),X=$*0.2*Math.random();return $+X}async collectMetadata(){let $={hostname:d0(),os:g0(),arch:i0(),uptime:process.uptime(),protocolVersion:h2,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=d0(),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:g0(),arch:i0(),kind:this.opts.kind??"cli-worker",metadata:X})});if(!z.ok)throw Error(`Heartbeat failed: HTTP ${z.status}`);let J=await z.json();if(typeof J.wsUrl==="string"&&J.wsUrl.length>0){if(J.wsUrl!==this.serverPublishedWsUrl)this.log.log(`[WorkerTunnel] Cloud advertised tunnel WS URL: ${J.wsUrl}`);this.serverPublishedWsUrl=J.wsUrl}return J}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>=p0){if(this.consecutiveAuthSuccesses++,this.consecutiveAuthSuccesses<b6){this.currentPollInterval=U0,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>=p0){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!==U0)this.log.warn(`[WorkerTunnel] ${this.consecutiveAuthFailures} consecutive auth failures — `+`backing off to ${U0}s. Re-authenticate with \`shogo login\`.`);this.currentPollInterval=U0}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 K=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:K.code,message:K.message,path:$.path})});return}let Z={...$.headers??{}};if($.projectId&&($.path.startsWith("/agent/")||$.path==="/agent")){let K=this.opts.resolver.deriveRuntimeToken($.projectId);if(K)Z["x-runtime-token"]=K}let z={method:$.method,headers:Z,signal:X.signal};if($.body&&$.method!=="GET"&&$.method!=="HEAD")z.body=$.body;let J=await fetch(Y,z);if($.stream){let K=J.body?.getReader();if(!K){this.sendFrame({type:"stream-error",requestId:$.requestId,error:"No response body for stream"});return}let V=new TextDecoder;try{while(!0){let{done:H,value:q}=await K.read();if(H)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(H){if(H?.name!=="AbortError"&&this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-error",requestId:$.requestId,error:H?.message??String(H)})}}else{let K=await J.text(),V={};J.headers.forEach((H,q)=>{V[q]=H}),this.sendFrame({type:"response",requestId:$.requestId,status:J.status,headers:V,body:K})}}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{}}},E6)}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{}},v6)}connectWs(){if(this.stopped||this.ws)return;let $=this.buildWsUrl(),X=d0(),Y=g0(),Z=i0(),z=this.opts.name??X;this.log.log(`[WorkerTunnel] Opening WebSocket to ${$} (hostname=${X})`);let J={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"}},K;try{K=this.createTunnelWebSocket($,J)}catch(V){this.log.error(`[WorkerTunnel] WebSocket creation failed: ${V?.message??V}`),this.ws=null,this.scheduleNextPoll(5);return}this.ws=K,K.onopen=()=>{this.log.log("[WorkerTunnel] WebSocket connected — session active"),this.wsReconnectAttempt=0,this.startWsHeartbeat(),this.resetWsIdleTimer()},K.onmessage=(V)=>{let H;try{let q=typeof V.data==="string"?V.data:V.data.toString();H=JSON.parse(q)}catch{return}if(H.type==="ping"){this.sendFrame({type:"pong"}),this.resetWsIdleTimer();return}if(H.type==="cancel"){let q=this.activeAbortControllers.get(H.requestId);if(q)q.abort();return}if(H.type==="request"){this.handleRequest(H).catch((q)=>{this.log.error(`[WorkerTunnel] Error handling request: ${q?.message??q}`)});return}},K.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 H=this.getReconnectDelayMs();this.log.log(`[WorkerTunnel] Reconnecting in ${Math.round(H/1000)}s (attempt ${this.wsReconnectAttempt})`),this.scheduleNextPoll(Math.ceil(H/1000))}},K.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:E2,BACKOFF_MAX_MS:v2,TUNNEL_PROTOCOL_VERSION:h2,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 m2($){let X=R({name:$.name,workerDir:$.workerDir,apiKey:$.apiKey,cloudUrl:$.cloudUrl,port:$.port?parseInt($.port,10):void 0,projectsDir:$.projectsDir}),Y=J2($.proxy);if(!$.foreground)return j6({cfg:X,proxy:Y,flags:$});let Z=u({flag:$.runtimeBin});if(!Z)console.error(w.red(n({flag:$.runtimeBin}))),process.exit(1);if(D0(process.env,Y),$.debug){if(!await H2(q2({cloudUrl:X.cloudUrl,apiKey:X.apiKey,workerDir:X.workerDir,proxy:Y})))process.exit(1)}let z=!$.noAutoPull,J=!$.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 ")+(J?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 K=`${X.cloudUrl.replace(/\/+$/,"")}/api/ai/v1`,V={cloudUrl:X.cloudUrl,apiKey:X.apiKey,aiProxyUrl:K,aiProxyToken:X.apiKey},H=new n0({runtimeBin:$.runtimeBin,defaultSpawnConfig:V,autoPull:{enabled:z,projectsDir:X.projectsDir,watch:!0,useGit:J}});if(!H.resolveBinary())console.error(w.red(n({flag:$.runtimeBin}))),process.exit(1);let q=new c0({apiKey:X.apiKey,cloudUrl:X.cloudUrl,name:X.name,kind:"cli-worker",resolver:H,onAuthRevoked:(k)=>{console.error(w.red(`✗ Cloud auth revoked: ${k}`)),console.error(w.dim(" Run `shogo login` to re-authenticate; this worker will keep polling at the auth-failure backoff until then."))}}),Q=!1,G=async(k)=>{if(Q)return;Q=!0,console.log(w.dim(`
|
|
16
|
+
Received ${k} — shutting down...`));try{q.stop()}catch{}try{await H.stopAll()}catch(C){console.warn(w.yellow(`stopAll: ${C?.message??C}`))}process.exit(0)};process.once("SIGINT",()=>void G("SIGINT")),process.once("SIGTERM",()=>void G("SIGTERM")),process.once("SIGHUP",()=>void G("SIGHUP")),q.start(),console.log(w.green("✓ Worker running. Ctrl-C to stop.")),await new Promise(()=>{})}function j6({cfg:$,proxy:X,flags:Y}){let{entry:Z,runner:z}=I6(),J=D0({...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),K=m6(Y),{pid:V}=Y2({entry:Z,runner:z,env:{...J,SHOGO_DETACHED_ARGS:K.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 m6($){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 I6(){let $=process.execPath,X=/\bbun(?:-[^/\\]*)?$/.test($)||typeof globalThis.Bun<"u",Y=process.argv[1];if(Y&&h6(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 I2 from"picocolors";async function u2(){let{killedPid:$}=Z2("SIGTERM");if($===null){console.log(I2.dim("No worker running."));return}console.log(I2.green(`✓ Worker stopped (pid=${$}).`))}import v from"picocolors";async function f2(){let $=G0();if(!$){console.log(v.yellow("● stopped")+v.dim(" (no pid file)"));return}if(!W0($)){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 u6}from"node:child_process";import{existsSync as f6,statSync as n6}from"node:fs";import n2 from"picocolors";async function d2($){let X=$.err?H0:q0;if(!f6(X)){console.log(n2.dim("No logs yet."));return}let Y=$.follow?["-F","-n","200",X]:["-n","200",X];if(u6("tail",Y,{stdio:"inherit"}).on("exit",(z)=>process.exit(z??0)),!$.follow){if(n6(X).size===0)console.log(n2.dim("(log file is empty)"))}}import _ from"picocolors";import{spawn as d6}from"node:child_process";import{hostname as g6,platform as i6,arch as p6}from"node:os";import f from"picocolors";class D extends Error{kind;constructor($,X){super($);this.kind=X;this.name="CloudLoginError"}}async function g2($){let X=$.cloudUrl.replace(/\/$/,""),Y=$.client??"cli",Z=$.deviceName??g6(),z=$.devicePlatform??`${i6()}-${p6()}`,J=$.appVersion??o6(),K=$.log??((O)=>console.log(O)),V=$.fetchImpl??fetch,H=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:J,workspaceId:$.workspaceId,client:Y}),signal:AbortSignal.timeout(1e4)}).catch((O)=>{throw new D(`Cannot reach Shogo Cloud at ${X}: ${O?.message??O}`,"transport")});if(!H.ok){let O=await H.text().catch(()=>"");throw new D(`Cloud rejected /api/cli/login/start (HTTP ${H.status}): ${O||"no body"}`,"transport")}let q=await H.json().catch(()=>({}));if(!q?.ok||!q.state||!q.authUrl)throw new D(`Cloud returned a malformed start response: ${q?.error??JSON.stringify(q)}`,"transport");let Q=c6($.pollIntervalMs??q.pollIntervalMs),G=$.timeoutMs??q.expiresInMs,k=Date.now()+G;if(K(""),K(f.bold("Sign in to Shogo Cloud")),K(f.dim(" cloud: ")+X),K(f.dim(" device: ")+`${Z} (${z})`),K(f.dim(" user code: ")+f.cyan(q.userCode)),K(""),K(" Open this URL in your browser to approve:"),K(" "+f.cyan(q.authUrl)),K(""),$.openBrowser!==!1)if(typeof $.openBrowser==="function")try{await $.openBrowser(q.authUrl)}catch{}else l6(q.authUrl).catch(()=>{});K(f.dim("Waiting for approval..."));let C=$.installSignalHandlers??!0,W=()=>{L.abort()},L=new AbortController;if($.abortSignal)if($.abortSignal.aborted)L.abort();else $.abortSignal.addEventListener("abort",()=>L.abort(),{once:!0});if(C)process.once("SIGINT",W),process.once("SIGTERM",W);try{while(!0){if(L.signal.aborted)throw new D("Aborted by caller.","cancelled");if(Date.now()>=k)throw new D(`Timed out after ${Math.round(G/1000)}s waiting for approval.`,"timeout");let O=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 K(f.dim(` (poll error: ${T?.message??T} — retrying)`)),null});if(O&&O.ok){let T=await O.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 D("Sign-in was denied in the browser.","denied");if(T?.status==="expired")throw new D("Sign-in request expired before it was approved.","expired")}await a6(Q,L.signal)}}finally{if(C)process.removeListener("SIGINT",W),process.removeListener("SIGTERM",W)}}function c6($){if(!Number.isFinite($)||$<=0)return 2000;return Math.min(Math.max($,1000),1e4)}function a6($,X){return new Promise((Y,Z)=>{if(X?.aborted)return Z(new D("Aborted by caller.","cancelled"));let z=setTimeout(()=>{X?.removeEventListener("abort",J),Y()},$),J=()=>{clearTimeout(z),Z(new D("Aborted by caller.","cancelled"))};X?.addEventListener("abort",J,{once:!0})})}function o6(){return d()}function l6($){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=d6(Y,Z,{stdio:"ignore",detached:!0});z.on("error",()=>X()),z.unref(),X()}catch{X()}})}import{readFileSync as r6,writeFileSync as e6,existsSync as s6,chmodSync as t6}from"node:fs";import{randomUUID as $1}from"node:crypto";function i2(){if(s6(o)){let X=r6(o,"utf-8").trim();if(X)return X}m();let $=$1();return e6(o,$,{mode:384}),t6(o,384),$}var X1="https://studio.shogo.ai";async function p2($){let X=I(),Y=($.cloudUrl||X.cloudUrl||X1).replace(/\/$/,""),Z=$.apiKey||process.env.SHOGO_API_KEY;if(Z){await Y1({key:Z,cloudUrl:Y,cfg:X,name:$.name});return}let z=i2(),J;try{J=await g2({cloudUrl:Y,deviceId:z,deviceName:$.name,workspaceId:$.workspace,openBrowser:!$.noBrowser})}catch(K){if(K instanceof D){if(console.error(_.red(`✗ ${K.message}`)),K.kind==="transport")console.error(_.dim(" If your network blocks browsers, run with --api-key <key> instead."));process.exit(1)}throw K}if(X.apiKey=J.key,X.cloudUrl=Y,$.name)X.name=$.name;if(s(X),console.log(""),console.log(_.green("✓ Signed in to Shogo Cloud")),J.workspace)console.log(_.dim(" workspace: ")+J.workspace);if(J.email)console.log(_.dim(" email: ")+J.email);console.log(_.dim(" saved to: ")+"~/.shogo/config.json"),console.log(""),console.log(_.dim(" next: ")+"shogo worker start")}async function Y1({key:$,cloudUrl:X,cfg:Y,name:Z}){if(!/^shogo_sk_/.test($))console.error(_.red('✗ API key should start with "shogo_sk_". Copy it verbatim from the API Keys page.')),process.exit(1);let z;try{let J=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 J.json().catch(()=>({})),!J.ok||!z?.valid)console.error(_.red(`✗ ${z?.error||`Cloud rejected the key (HTTP ${J.status}).`}`)),process.exit(1)}catch(J){console.error(_.red(`✗ Cannot reach Shogo Cloud at ${X}: ${J?.message??J}`)),process.exit(1)}if(Y.apiKey=$,Y.cloudUrl=X,Z)Y.name=Z;if(s(Y),console.log(_.green("✓ API key saved to ~/.shogo/config.json")),z.workspace?.name)console.log(_.dim(" workspace: ")+z.workspace.name);if(z.user?.email)console.log(_.dim(" email: ")+z.user.email);console.log(_.dim(" next: ")+"shogo worker start")}import J0 from"picocolors";async function c2(){let $=I();if(Object.keys($).length===0){console.log(J0.dim("(no config — run `shogo login` or `shogo config set`)"));return}let X={...$,apiKey:$.apiKey?`***${$.apiKey.slice(-4)}`:void 0};console.log(J0.dim(`file: ${j}`)),console.log(JSON.stringify(X,null,2))}async function a2($,X){let Y=["apiKey","cloudUrl","name","workerDir","port"];if(!Y.includes($))console.error(J0.red(`Unknown key: ${$}`)),console.error(J0.dim(`Allowed: ${Y.join(", ")}`)),process.exit(1);let Z=I();Z[$]=$==="port"?parseInt(X,10):X,s(Z),console.log(J0.green(`✓ ${$} set`))}import N from"picocolors";import{spawn as Z1}from"node:child_process";import{createHash as z1}from"node:crypto";import{createWriteStream as J1,existsSync as K0,mkdirSync as a0,readFileSync as r2,renameSync as o2,rmSync as o0,writeFileSync as K1}from"node:fs";import{tmpdir as V1}from"node:os";import{dirname as q1,join as A0}from"node:path";import{Readable as H1}from"node:stream";import{pipeline as Q1}from"node:stream/promises";var G1="https://github.com/shogo-labs/shogo-ai/releases/download";function e2(){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(!K0(e))return null;try{let $=r2(e,"utf-8");return JSON.parse($)}catch{return null}}async function W1($,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`,G=await fetch(Q,{headers:{Accept:"application/vnd.github+json"}});if(!G.ok)throw Error(`GitHub API ${G.status} for ${Q}`);let k=await G.json();if(!k.tag_name)throw Error("GitHub API did not return tag_name");return l2(k.tag_name)}let J=`https://api.github.com/repos/${Z}/${z}/releases?per_page=30`,K=await fetch(J,{headers:{Accept:"application/vnd.github+json"}});if(!K.ok)throw Error(`GitHub API ${K.status} for ${J}`);let V=await K.json(),H=$==="beta"?"-beta":"-nightly",q=V.find((Q)=>Q.prerelease&&/^v\d+\.\d+\.\d+-/.test(Q.tag_name)&&Q.tag_name.includes(H));if(!q)throw Error(`No ${$} runtime release found`);return l2(q.tag_name)}function l2($){if(!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test($))throw Error(`Unexpected app tag '${$}' (expected vX.Y.Z[-prerelease])`);return $.slice(1)}function k1($,X,Y){let Z=`shogo-agent-runtime-${X}.tar.gz`,z=`v${$}`,J=`${Y.replace(/\/$/,"")}/${z}/${Z}`;return{tarball:J,sha256:`${J}.sha256`,assetName:Z}}async function O1($,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 Q1(H1.fromWeb(Y.body),J1(X))}async function w1($){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 C1($){let X=r2($);return z1("sha256").update(X).digest("hex")}async function L1($,X){await new Promise((Y,Z)=>{let z=Z1("tar",["-xzf",$,"-C",X],{stdio:["ignore","pipe","pipe"]}),J="";z.stderr.on("data",(K)=>{J+=K.toString()}),z.on("error",Z),z.on("exit",(K)=>{if(K===0)Y();else Z(Error(`tar exited ${K}: ${J.trim()}`))})})}function U1($,X){s0();let Y=q1(X);if(!K0(Y))a0(Y,{recursive:!0,mode:448});let Z=`${X}.next`;if(K0(Z))o0(Z,{force:!0});o2($,Z);try{if(process.platform!=="win32"){let{chmodSync:z}=y0("node:fs");z(Z,493)}}catch{}if(K0(X))o0(X,{force:!0});o2(Z,X)}async function s2($={}){let X=$.logger??console,Y=$.channel??"stable",Z=$.baseUrl??process.env.SHOGO_RUNTIME_RELEASES_URL??G1,z=$.target??e2(),J=$.version;if(!J)X.log(`[runtime install] Resolving latest ${Y} version...`),J=await W1(Y,Z),X.log(`[runtime install] Latest ${Y} = ${J}`);let K=N0();if(K&&K.version===J&&K.target===z&&!$.force)return X.log(`[runtime install] ${J} (${z}) already installed at ${S} — pass --force to reinstall`),{version:J,target:z,binPath:S,source:K.source,sha256:K.sha256,channel:Y};let V=k1(J,z,Z);X.log(`[runtime install] Downloading ${V.tarball}`);let H=A0(V1(),`shogo-runtime-install-${process.pid}-${Date.now()}`);a0(H,{recursive:!0});try{let q=A0(H,V.assetName);await O1(V.tarball,q),X.log("[runtime install] Verifying SHA-256...");let Q=await w1(V.sha256),G=C1(q);if(G!==Q)throw Error(`SHA-256 mismatch for ${V.assetName}
|
|
18
18
|
expected: ${Q}
|
|
19
|
-
actual: ${G}`);let
|
|
20
|
-
`,{mode:384}),X.log(`[runtime install] Installed agent-runtime ${J} (${z}) to ${S}`),{version:J,target:z,binPath:S,source:V.tarball,sha256:G,channel:Y}}finally{try{
|
|
21
|
-
shogo project pull ${U.cyan($)}`)),console.log(U.dim(" cloud ")+Y.cloudUrl),console.log(U.dim(" into ")+Z),z?.length)console.log(U.dim(" include ")+z.join(", "));console.log("");let J=new
|
|
22
|
-
Watching for local changes...`));let V=new
|
|
23
|
-
Received ${Q} — flushing pending uploads...`));try{await V.stop()}catch(G){console.warn(U.yellow(`watcher.stop: ${G?.message??G}`))}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
|
|
24
|
-
${Z}`),console.log(U.dim(" downloaded: ")+X.downloaded),X.uploaded)console.log(U.dim(" uploaded: ")+X.uploaded);if(X.deleted)console.log(U.dim(" deleted: ")+X.deleted);if(X.skipped)console.log(U.dim(" skipped: ")+X.skipped);if(!Y){console.log(U.dim(" errors: ")+X.errors.length);for(let z of X.errors.slice(0,5))console.log(U.dim(" ")+U.red(`${z.path}: ${z.message}`));if(X.errors.length>5)console.log(U.dim(` ... and ${X.errors.length-5} more`))}}function
|
|
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 K=await new
|
|
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
|
|
27
|
-
shogo project checkout ${b.cyan($)}`)),console.log(b.dim(" cloud ")+Y.cloudUrl),console.log(b.dim(" local ")+Z),X.at)console.log(b.dim(" at ")+X.at);if(console.log(""),X.unshallow)console.log(b.dim("Unshallowing repo (this may take a moment)...")),await O0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z});if(!X.at){let J=await
|
|
19
|
+
actual: ${G}`);let k=A0(H,"extract");a0(k,{recursive:!0}),await L1(q,k);let C=A0(k,"agent-runtime");if(!K0(C))throw Error(`Tarball ${V.assetName} did not contain ./agent-runtime`);U1(C,S);let W={version:J,target:z,installedAt:new Date().toISOString(),channel:Y,source:V.tarball,sha256:G};return K1(e,JSON.stringify(W,null,2)+`
|
|
20
|
+
`,{mode:384}),X.log(`[runtime install] Installed agent-runtime ${J} (${z}) to ${S}`),{version:J,target:z,binPath:S,source:V.tarball,sha256:G,channel:Y}}finally{try{o0(H,{recursive:!0,force:!0})}catch{}}}function t2(){return{runtimeDir:r,runtimeBin:S,versionFile:e}}async function l0($={}){let X=await s2({channel:$.channel,version:$.version,baseUrl:$.baseUrl,force:$.force});console.log(),console.log(N.green("✓"),`agent-runtime ${N.bold(X.version)} installed (${X.target})`),console.log(` ${N.dim("path: ")} ${X.binPath}`),console.log(` ${N.dim("source: ")} ${X.source}`),console.log(` ${N.dim("sha256: ")} ${X.sha256}`),console.log(` ${N.dim("channel: ")} ${X.channel}`)}function $$(){let $=N0();if(!$){console.log(N.yellow("No agent-runtime installed.")),console.log(`Run ${N.cyan("shogo runtime install")} to download the latest.`),process.exitCode=1;return}console.log(`${N.bold("agent-runtime")} ${$.version}`),console.log(` ${N.dim("target: ")} ${$.target}`),console.log(` ${N.dim("channel: ")} ${$.channel}`),console.log(` ${N.dim("installed at:")} ${$.installedAt}`),console.log(` ${N.dim("source: ")} ${$.source}`)}function X$(){let $=u(),X=t2();if(!$){console.log(N.yellow("agent-runtime binary not found on this machine.")),console.log(),console.log(N.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(N.dim(` (resolved via: ${$.source})`))}async function Y$($={}){let X=N0(),Y=$.channel??X?.channel??"stable";if(X)console.log(`${N.dim("current:")} ${X.version} (${X.target}, ${X.channel})`);else console.log(N.dim("No existing install — installing fresh..."));await l0({channel:Y,baseUrl:$.baseUrl,force:!0})}import{existsSync as A1,mkdirSync as N1}from"node:fs";import{resolve as B1}from"node:path";import U from"picocolors";import{CloudFileTransport as F1}from"@shogo-ai/sdk/cloud-file-transport";async function Z$($,X){if(!$)throw Error("projectId is required");let Y=R({apiKey:X.apiKey,cloudUrl:X.cloudUrl});t0(Y.projectsDir);let Z=B1(X.into??g($,Y.projectsDir));if(!A1(Z))N1(Z,{recursive:!0});let z=X.include?.split(",").map((V)=>V.trim()).filter(Boolean);if(console.log(U.bold(`
|
|
21
|
+
shogo project pull ${U.cyan($)}`)),console.log(U.dim(" cloud ")+Y.cloudUrl),console.log(U.dim(" into ")+Z),z?.length)console.log(U.dim(" include ")+z.join(", "));console.log("");let J=new F1({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z,include:z,onProgress:T1()}),K=await J.downloadAll();if(y1("Pull",K),X.watch){console.log(U.bold(`
|
|
22
|
+
Watching for local changes...`));let V=new Z0({rootDir:Z,transport:J,onFlush:({uploaded:Q,errors:G})=>{let k=Q.length===1?Q[0]:`${Q.length} files`,C=G>0?U.red(` (${G} errors)`):"";console.log(U.dim(` ↑ ${k}${C}`))}});V.start();let H=!1,q=async(Q)=>{if(H)return;H=!0,console.log(U.dim(`
|
|
23
|
+
Received ${Q} — flushing pending uploads...`));try{await V.stop()}catch(G){console.warn(U.yellow(`watcher.stop: ${G?.message??G}`))}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 T1(){return($)=>{let X=$.kind==="download"?"↓":$.kind==="upload"?"↑":$.kind==="delete"?"✗":"·",Y=$.total>0?` ${$.index+1}/${$.total}`:"",Z=$.bytes!=null?U.dim(` (${_1($.bytes)})`):"";console.log(U.dim(` ${X}${Y} ${$.path}${Z}`))}}function y1($,X){let Y=X.errors.length===0,Z=Y?U.green(`✓ ${$} complete`):U.red(`✗ ${$} completed with errors`);if(console.log(`
|
|
24
|
+
${Z}`),console.log(U.dim(" downloaded: ")+X.downloaded),X.uploaded)console.log(U.dim(" uploaded: ")+X.uploaded);if(X.deleted)console.log(U.dim(" deleted: ")+X.deleted);if(X.skipped)console.log(U.dim(" skipped: ")+X.skipped);if(!Y){console.log(U.dim(" errors: ")+X.errors.length);for(let z of X.errors.slice(0,5))console.log(U.dim(" ")+U.red(`${z.path}: ${z.message}`));if(X.errors.length>5)console.log(U.dim(` ... and ${X.errors.length-5} more`))}}function _1($){if($<1024)return`${$}B`;if($<1048576)return`${($/1024).toFixed(1)}KB`;return`${($/1024/1024).toFixed(2)}MB`}import{existsSync as x1}from"node:fs";import{resolve as M1}from"node:path";import B from"picocolors";import{CloudFileTransport as D1}from"@shogo-ai/sdk/cloud-file-transport";async function z$($,X){if(!$)throw Error("projectId is required");let Y=R({apiKey:X.apiKey,cloudUrl:X.cloudUrl}),Z=M1(X.from??g($,Y.projectsDir));if(!x1(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 K=await new D1({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z,include:z,onProgress:(V)=>{let H=V.kind==="upload"?"↑":V.kind==="delete"?"✗":"·",q=V.total>0?` ${V.index+1}/${V.total}`:"",Q=V.bytes!=null?B.dim(` (${S1(V.bytes)})`):"";console.log(B.dim(` ${H}${q} ${V.path}${Q}`))}}).uploadAll({deleteRemote:X.deleteRemote});P1("Push",K)}function P1($,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 S1($){if($<1024)return`${$}B`;if($<1048576)return`${($/1024).toFixed(1)}KB`;return`${($/1024/1024).toFixed(2)}MB`}import{existsSync as R1}from"node:fs";import{resolve as b1}from"node:path";import b from"picocolors";w0();async function J$($,X){if(!$)throw Error("projectId is required");let Y=R({apiKey:X.apiKey,cloudUrl:X.cloudUrl}),Z=b1(X.into??g($,Y.projectsDir));if(!R1(Z))throw Error(`Local project dir does not exist: ${Z} — run \`shogo project pull\` first`);if(!Y0(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(b.bold(`
|
|
27
|
+
shogo project checkout ${b.cyan($)}`)),console.log(b.dim(" cloud ")+Y.cloudUrl),console.log(b.dim(" local ")+Z),X.at)console.log(b.dim(" at ")+X.at);if(console.log(""),X.unshallow)console.log(b.dim("Unshallowing repo (this may take a moment)...")),await O0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z});if(!X.at){let J=await X0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z});console.log(b.green(`✓ Reset to ${J.commitSha.slice(0,8)} (remote HEAD)`));return}let z;try{z=(await y(["rev-parse","--verify",`${X.at}^{commit}`],{cwd:Z})).stdout.trim()}catch{z=await E1(Y.cloudUrl,Y.apiKey,$,X.at),console.log(b.dim(` resolved checkpoint "${X.at}" → ${z.slice(0,8)}`))}try{await X0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z,branch:z})}catch{if(!X.unshallow)console.log(b.yellow(" fetch failed; unshallowing and retrying...")),await O0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z}),await X0({apiUrl:Y.cloudUrl,apiKey:Y.apiKey,projectId:$,localDir:Z,branch:z});else throw Error(`Cannot reach commit ${z} in local clone`)}await y(["reset","--hard",z],{cwd:Z}),console.log(b.green(`✓ Checked out ${z.slice(0,8)}`))}async function E1($,X,Y,Z){let z=`${$.replace(/\/+$/,"")}/api/projects/${encodeURIComponent(Y)}/checkpoints?limit=100`,J=await fetch(z,{headers:{Authorization:`Bearer ${X}`}});if(!J.ok)throw Error(`Failed to list checkpoints: HTTP ${J.status}`);let K=await J.json(),V=Z.toLowerCase(),H=K.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(!H)throw Error(`No checkpoint matches "${Z}" (searched ${K.checkpoints.length} most recent)`);return H.commitSha}import{createInterface as I1}from"node:readline";import A from"picocolors";import{execFileSync as V$}from"node:child_process";import{existsSync as c,copyFileSync as K$}from"node:fs";import P from"node:path";import{homedir as q$,platform as H$}from"node:os";function Q$($,X,Y){let z=V$($,["-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 B0($,X){if(!c(X))return[];return Q$($,X,`
|
|
28
28
|
import { Database } from 'bun:sqlite';
|
|
29
29
|
try {
|
|
30
30
|
const db = new Database(process.env.DBP, { readonly: true });
|
|
@@ -38,7 +38,7 @@ shogo project checkout ${b.cyan($)}`)),console.log(b.dim(" cloud ")+Y.cloudUrl
|
|
|
38
38
|
console.error(String(e?.stack || e));
|
|
39
39
|
process.exit(2);
|
|
40
40
|
}
|
|
41
|
-
`)}function
|
|
41
|
+
`)}function v1($){if(!c($))throw Error(`Database does not exist at ${$} — refusing to back up nothing`);let X=P.dirname($),Y=P.basename($),Z=new Date().toISOString().replace(/[:.]/g,"-"),z=P.join(X,`${Y}.bak-${Z}`);K$($,z);for(let J of["-wal","-shm"]){let K=`${$}${J}`;if(c(K))K$(K,`${z}${J}`)}return z}function h1($,X,Y){if(Y.length===0)return 0;if(!c(X))throw Error(`Cannot repair: database does not exist at ${X}`);let z=`
|
|
42
42
|
import { Database } from 'bun:sqlite';
|
|
43
43
|
const names = ${JSON.stringify(Y)};
|
|
44
44
|
if (!Array.isArray(names) || names.some(n => typeof n !== 'string')) {
|
|
@@ -57,8 +57,8 @@ shogo project checkout ${b.cyan($)}`)),console.log(b.dim(" cloud ")+Y.cloudUrl
|
|
|
57
57
|
console.error(String(e?.stack || e));
|
|
58
58
|
process.exit(3);
|
|
59
59
|
}
|
|
60
|
-
`;return
|
|
61
|
-
`)[0]?.trim();if(console.log(` ${
|
|
62
|
-
`+" (e.g. /Applications/Shogo.app/Contents/Resources/bun/bun on macOS).");console.log(
|
|
63
|
-
→ Relaunch the Shogo app to finish applying migrations.`))}else{if(console.log(
|
|
64
|
-
(use --help for usage)`));E.parseAsync(process.argv);async function F($){try{await $()}catch(X){if(console.error(
|
|
60
|
+
`;return Q$($,X,z).deleted}function G$($){let X=$.log??(()=>{}),{bunPath:Y,dbPath:Z}=$;if(!c(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=B0(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((G)=>G.name).join(", ")}`);let J;if(!$.skipBackup)J=v1(Z),X(`Backed up database to ${J}`);let K=z.map((G)=>G.name),V=h1(Y,Z,K);X(`Cleared ${V} failed migration row(s).`);let H=B0(Y,Z),q=H.length===0?"repaired":"failed",Q=q==="repaired"?"Cleared the failed migration record. Relaunch Shogo to re-apply migrations cleanly.":`Repair incomplete — ${H.length} migration(s) still failed: ${H.map((G)=>G.name).join(", ")}.`;return{status:q,detected:z,backupPath:J,cleared:K.slice(0,V),remaining:H,message:Q}}function j1(){let $=q$(),X=H$(),Y;if(X==="darwin")Y=P.join($,"Library","Application Support");else if(X==="win32")Y=process.env.APPDATA??P.join($,"AppData","Roaming");else Y=process.env.XDG_CONFIG_HOME??P.join($,".config");return P.join(Y,"Shogo","data")}function W$(){return P.join(j1(),"shogo.db")}function m1(){let $=H$(),X=$==="win32"?"bun.exe":"bun",Y=q$(),Z=[];if($==="darwin")Z.push(P.join("/Applications","Shogo.app","Contents","Resources","bun",X),P.join(Y,"Applications","Shogo.app","Contents","Resources","bun",X));else if($==="linux")Z.push(P.join("/opt","Shogo","resources","bun",X),P.join("/usr","lib","shogo","resources","bun",X));return Z}function r0($){try{return V$($,["--version"],{stdio:"ignore",timeout:5000}),!0}catch{return!1}}function k$($){if($)return r0($)?$:null;if(process.versions.bun&&process.execPath)return process.execPath;for(let X of m1())if(c(X)&&r0(X))return X;if(r0("bun"))return"bun";return null}function u1($){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 f1($){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=I1({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 O$($={}){let X=$.db??W$(),Y=k$($.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=B0(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):`)),u1(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 J=$.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(J),!await f1(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=G$({bunPath:Y,dbPath:X,skipBackup:$.backup===!1,log:(J)=>console.log(A.dim(` ${J}`))});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 n1}from"node:child_process";import{resolve as d1}from"node:path";import w$ from"picocolors";function g1($){let{flags:X,config:Y,runtime:Z}=$,z=X.cwd?d1(X.cwd):process.cwd(),J=["interactive"];if(X.print!==void 0)J.push("-p",X.print);if(X.noTui)J.push("--no-tui");let K=Y.cloudUrl.replace(/\/$/,""),V={...$.baseEnv??process.env,SHOGO_INTERACTIVE:"1",SHOGO_INTERACTIVE_CWD:z,PROJECT_DIR:z,WORKSPACE_DIR:z,SHOGO_API_URL:K,SHOGO_CLOUD_URL:K,SHOGO_API_KEY:Y.apiKey,AI_PROXY_URL:`${K}/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:J,env:V,cwd:z}}async function C$($={}){let X=R({apiKey:$.apiKey,cloudUrl:$.cloudUrl}),Y=u({flag:$.runtimeBin});if(!Y)console.error(w$.red(n({flag:$.runtimeBin}))),process.exit(1);let Z=g1({flags:$,config:{apiKey:X.apiKey,cloudUrl:X.cloudUrl},runtime:Y}),z=await new Promise((J)=>{let K=n1(Z.bin,Z.args,{cwd:Z.cwd,env:Z.env,stdio:"inherit"});K.on("error",(V)=>{console.error(w$.red(`✗ Failed to launch agent-runtime: ${V.message}`)),J(1)}),K.on("exit",(V)=>J(V??0))});process.exit(z)}var p1="0.1.0",E=new i1;E.name("shogo").description("Shogo Cloud Agent Worker — run Shogo agents on your own machine.").version(p1);E.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(()=>p2($)));var F0=E.command("worker").description("Manage the local worker process");F0.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(()=>m2($)));F0.command("stop").description("Stop the running worker").action(()=>F(u2));F0.command("status").description("Show worker status").action(()=>F(f2));F0.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(()=>d2($)));var T0=E.command("runtime").description("Install / inspect the local agent-runtime binary");T0.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(()=>l0($)));T0.command("version").description("Print the installed agent-runtime version").action(()=>F(()=>$$()));T0.command("where").description("Print the resolved agent-runtime binary path").action(()=>F(()=>X$()));T0.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(()=>Y$($)));var U$=E.command("config").description("Inspect or modify ~/.shogo/config.json");U$.command("show").description("Print current config (API key masked)").action(()=>F(c2));U$.command("set <key> <value>").description("Set apiKey / cloudUrl / name / workerDir / port / projectsDir").action(($,X)=>F(()=>a2($,X)));var e0=E.command("project").description("Clone/sync project workspaces between Shogo Cloud and this machine");e0.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(()=>Z$($,X)));e0.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(()=>z$($,X)));e0.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(()=>J$($,X)));E.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(()=>O$($)));E.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(()=>C$({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})));E.showHelpAfterError(L$.dim(`
|
|
64
|
+
(use --help for usage)`));E.parseAsync(process.argv);async function F($){try{await $()}catch(X){if(console.error(L$.red(`✗ ${X?.message??X}`)),process.env.SHOGO_DEBUG)console.error(X?.stack);process.exit(1)}}
|
package/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (C) 2026 Shogo Technologies, Inc.
|
|
3
|
+
/**
|
|
4
|
+
* `onRuntimeGone` contract: hosts that cache the agent port (the desktop
|
|
5
|
+
* API's RuntimeManager) must hear about every terminal teardown — idle/LRU
|
|
6
|
+
* eviction, explicit stop, clean exit, breaker trip — and must NOT hear about
|
|
7
|
+
* a transient crash-restart, which the worker brings back on the same port.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'bun:test';
|
|
10
|
+
import { WorkerRuntimeManager, type RuntimeGoneInfo } from '../runtime-manager.ts';
|
|
11
|
+
|
|
12
|
+
const SILENT = { log: () => {}, warn: () => {}, error: () => {} } as const;
|
|
13
|
+
|
|
14
|
+
function makeFakeProc() {
|
|
15
|
+
const proc = {
|
|
16
|
+
exitCode: 0 as number | null,
|
|
17
|
+
signalCode: null,
|
|
18
|
+
killed: false,
|
|
19
|
+
pid: 99999,
|
|
20
|
+
kill() { proc.killed = true; return true; },
|
|
21
|
+
once() { return proc; },
|
|
22
|
+
on() { return proc; },
|
|
23
|
+
};
|
|
24
|
+
return proc;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function insertSlot(mgr: WorkerRuntimeManager, projectId: string, extra: Record<string, unknown> = {}) {
|
|
28
|
+
const slot = {
|
|
29
|
+
projectId,
|
|
30
|
+
agentPort: 0,
|
|
31
|
+
apiServerPort: 0,
|
|
32
|
+
status: 'running',
|
|
33
|
+
proc: makeFakeProc(),
|
|
34
|
+
pid: null,
|
|
35
|
+
startedAt: Date.now(),
|
|
36
|
+
lastUsedAt: Date.now(),
|
|
37
|
+
restarts: 0,
|
|
38
|
+
consecutiveFailures: 0,
|
|
39
|
+
lastFailureAt: 0,
|
|
40
|
+
restartTimestamps: [] as number[],
|
|
41
|
+
breakerTrips: 0,
|
|
42
|
+
restartTimer: null as ReturnType<typeof setTimeout> | null,
|
|
43
|
+
idleTimer: null,
|
|
44
|
+
graceTimer: null,
|
|
45
|
+
limitWatchdog: null,
|
|
46
|
+
spawnConfig: {} as never,
|
|
47
|
+
startPromise: null,
|
|
48
|
+
...extra,
|
|
49
|
+
};
|
|
50
|
+
(mgr as unknown as { runtimes: Map<string, typeof slot> }).runtimes.set(projectId, slot);
|
|
51
|
+
return slot;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleExit(mgr: WorkerRuntimeManager, slot: unknown, code: number | null, signal: NodeJS.Signals | null) {
|
|
55
|
+
(mgr as unknown as { handleExit: (s: unknown, c: number | null, sig: NodeJS.Signals | null) => void })
|
|
56
|
+
.handleExit(slot, code, signal);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function recordingManager(opts: Record<string, unknown> = {}) {
|
|
60
|
+
const calls: Array<[string, RuntimeGoneInfo]> = [];
|
|
61
|
+
const mgr = new WorkerRuntimeManager({
|
|
62
|
+
logger: SILENT,
|
|
63
|
+
onRuntimeGone: (id, info) => calls.push([id, info]),
|
|
64
|
+
...opts,
|
|
65
|
+
});
|
|
66
|
+
return { mgr, calls };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('WorkerRuntimeManager onRuntimeGone', () => {
|
|
70
|
+
it('fires with reason=idle-evict when the idle reaper stops a runtime', async () => {
|
|
71
|
+
const { mgr, calls } = recordingManager({ idleMs: 30 });
|
|
72
|
+
const slot = insertSlot(mgr, 'proj-idle');
|
|
73
|
+
(mgr as unknown as { armIdleTimer: (s: unknown) => void }).armIdleTimer(slot);
|
|
74
|
+
|
|
75
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
76
|
+
|
|
77
|
+
expect(calls).toEqual([['proj-idle', { reason: 'idle-evict' }]]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('fires with reason=stop on an explicit stop()', async () => {
|
|
81
|
+
const { mgr, calls } = recordingManager();
|
|
82
|
+
insertSlot(mgr, 'proj-stop');
|
|
83
|
+
|
|
84
|
+
await mgr.stop('proj-stop');
|
|
85
|
+
|
|
86
|
+
expect(calls).toEqual([['proj-stop', { reason: 'stop' }]]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not fire for a stop() of an unknown project', async () => {
|
|
90
|
+
const { mgr, calls } = recordingManager();
|
|
91
|
+
await mgr.stop('nope');
|
|
92
|
+
expect(calls).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('fires with reason=exited on a clean exit', () => {
|
|
96
|
+
const { mgr, calls } = recordingManager();
|
|
97
|
+
const slot = insertSlot(mgr, 'proj-clean');
|
|
98
|
+
|
|
99
|
+
handleExit(mgr, slot, 0, null);
|
|
100
|
+
|
|
101
|
+
expect(calls).toEqual([['proj-clean', { reason: 'exited', code: 0, signal: null }]]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does not fire on a transient crash that the worker will restart', () => {
|
|
105
|
+
const { mgr, calls } = recordingManager();
|
|
106
|
+
const slot = insertSlot(mgr, 'proj-crash');
|
|
107
|
+
|
|
108
|
+
handleExit(mgr, slot, null, 'SIGKILL');
|
|
109
|
+
|
|
110
|
+
expect(slot.status).toBe('restarting');
|
|
111
|
+
expect(calls).toEqual([]);
|
|
112
|
+
if (slot.restartTimer) clearTimeout(slot.restartTimer);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('fires with reason=failed when the circuit breaker trips', () => {
|
|
116
|
+
const { mgr, calls } = recordingManager();
|
|
117
|
+
const slot = insertSlot(mgr, 'proj-loop', {
|
|
118
|
+
consecutiveFailures: 7,
|
|
119
|
+
lastFailureAt: Date.now(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
handleExit(mgr, slot, null, 'SIGKILL');
|
|
123
|
+
|
|
124
|
+
expect(slot.status).toBe('failed');
|
|
125
|
+
expect(calls).toEqual([['proj-loop', { reason: 'failed', code: null, signal: 'SIGKILL' }]]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('swallows errors thrown by the callback', async () => {
|
|
129
|
+
const mgr = new WorkerRuntimeManager({
|
|
130
|
+
logger: SILENT,
|
|
131
|
+
onRuntimeGone: () => { throw new Error('boom'); },
|
|
132
|
+
});
|
|
133
|
+
insertSlot(mgr, 'proj-throw');
|
|
134
|
+
|
|
135
|
+
await expect(mgr.stop('proj-throw')).resolves.toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (C) 2026 Shogo Technologies, Inc.
|
|
3
|
+
|
|
4
|
+
import { afterEach, describe, expect, it } from 'bun:test'
|
|
5
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
6
|
+
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import { WorkerRuntimeManager } from '../runtime-manager.ts'
|
|
9
|
+
import { writeViteWatchPidfile } from '@shogo-ai/sdk/vite-watch'
|
|
10
|
+
|
|
11
|
+
const RUN_INTEGRATION = process.env.RUN_INTEGRATION === '1'
|
|
12
|
+
const tempDirs: string[] = []
|
|
13
|
+
let child: ChildProcess | null = null
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (child?.pid) {
|
|
17
|
+
try {
|
|
18
|
+
process.kill(-child.pid, 'SIGKILL')
|
|
19
|
+
} catch {
|
|
20
|
+
// The child already exited during the test.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
child = null
|
|
24
|
+
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe.skipIf(!RUN_INTEGRATION)('WorkerRuntimeManager Vite watch cleanup e2e', () => {
|
|
28
|
+
it('reaps a detached Vite watch left in a project workspace at startup', async () => {
|
|
29
|
+
const projectsDir = mkdtempSync(join('/tmp', 'shogo-worker-vite-e2e-'))
|
|
30
|
+
tempDirs.push(projectsDir)
|
|
31
|
+
const projectDir = join(projectsDir, 'project-e2e')
|
|
32
|
+
const bundlerCwd = join(projectDir, 'project')
|
|
33
|
+
mkdirSync(bundlerCwd, { recursive: true })
|
|
34
|
+
const viteShim = join(bundlerCwd, 'node_modules', '.bin', 'vite')
|
|
35
|
+
|
|
36
|
+
child = spawn(
|
|
37
|
+
process.execPath,
|
|
38
|
+
['-e', 'setInterval(() => {}, 1000)', viteShim, 'build', '--watch'],
|
|
39
|
+
{ detached: true, stdio: 'ignore' },
|
|
40
|
+
)
|
|
41
|
+
expect(child.pid).toBeDefined()
|
|
42
|
+
const pid = child.pid!
|
|
43
|
+
const exited = new Promise<void>((resolve) => child!.once('exit', () => resolve()))
|
|
44
|
+
child.unref()
|
|
45
|
+
|
|
46
|
+
writeViteWatchPidfile(projectDir, {
|
|
47
|
+
pid,
|
|
48
|
+
pgid: pid,
|
|
49
|
+
startedAt: Date.now(),
|
|
50
|
+
bundlerCwd,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
new WorkerRuntimeManager({
|
|
54
|
+
autoPull: { enabled: false, projectsDir },
|
|
55
|
+
logger: { log: () => {}, warn: () => {}, error: () => {} },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
await Promise.race([
|
|
59
|
+
exited,
|
|
60
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('watch process did not exit')), 2_000)),
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
})
|
package/src/lib/git-cloner.ts
CHANGED
|
@@ -332,7 +332,14 @@ export async function commitAndPush(opts: CommitAndPushOptions): Promise<CommitA
|
|
|
332
332
|
if (!hasChanges) return { committed: false };
|
|
333
333
|
|
|
334
334
|
await runGit(
|
|
335
|
-
[
|
|
335
|
+
[
|
|
336
|
+
'commit',
|
|
337
|
+
'-m',
|
|
338
|
+
message,
|
|
339
|
+
'--no-verify',
|
|
340
|
+
'--trailer',
|
|
341
|
+
`Co-authored-by: ${authorName || process.env.SHOGO_AGENT_GIT_NAME || 'Shogo Agent'} <${authorEmail || process.env.SHOGO_AGENT_GIT_EMAIL || 'agent@shogo.ai'}>`,
|
|
342
|
+
],
|
|
336
343
|
{ cwd: localDir, env, logger, timeoutMs },
|
|
337
344
|
);
|
|
338
345
|
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
} from './resource-limits.ts';
|
|
50
50
|
import type { ResolveRejection, RuntimeResolver } from './tunnel.ts';
|
|
51
51
|
import { CloudFileTransport } from '@shogo-ai/sdk/cloud-file-transport';
|
|
52
|
+
import { killViteWatchFromPidfile } from '@shogo-ai/sdk/vite-watch';
|
|
52
53
|
import { CloudSyncWatcher } from './cloud-sync-watcher.ts';
|
|
53
54
|
import { cloneProject, gitIsAvailable, isGitRepo } from './git-cloner.ts';
|
|
54
55
|
|
|
@@ -391,8 +392,26 @@ export interface WorkerRuntimeManagerOptions {
|
|
|
391
392
|
* `shogo project pull` first.
|
|
392
393
|
*/
|
|
393
394
|
autoPull?: AutoPullOptions;
|
|
395
|
+
/**
|
|
396
|
+
* Called when a runtime is no longer serving on its agent port and the
|
|
397
|
+
* manager will NOT bring it back on its own: an explicit or idle/LRU
|
|
398
|
+
* `stop()`, a clean exit, or a circuit-breaker trip into `'failed'`.
|
|
399
|
+
* Transient crash-restarts (same port) do not fire it. Hosts that cache
|
|
400
|
+
* the agent port (the desktop API's RuntimeManager) use this to drop
|
|
401
|
+
* their entry instead of proxying to a dead port. Errors thrown by the
|
|
402
|
+
* callback are swallowed.
|
|
403
|
+
*/
|
|
404
|
+
onRuntimeGone?: (projectId: string, info: RuntimeGoneInfo) => void;
|
|
394
405
|
}
|
|
395
406
|
|
|
407
|
+
export interface RuntimeGoneInfo {
|
|
408
|
+
reason: RuntimeStopReason | 'exited' | 'failed';
|
|
409
|
+
code?: number | null;
|
|
410
|
+
signal?: NodeJS.Signals | null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export type RuntimeStopReason = 'stop' | 'idle-evict' | 'lru-evict';
|
|
414
|
+
|
|
396
415
|
export interface AutoPullOptions {
|
|
397
416
|
/** Master switch. Defaults to false; the `worker start` command flips
|
|
398
417
|
* it on for `cli_worker` instances unless `--no-auto-pull` is passed. */
|
|
@@ -441,10 +460,9 @@ interface InternalRuntime {
|
|
|
441
460
|
* PID of the most recent spawn, retained after `proc` is nulled in
|
|
442
461
|
* {@link WorkerRuntimeManager.handleExit}. On posix, the runtime is
|
|
443
462
|
* spawned as a process group leader (`detached: true`), so this is
|
|
444
|
-
* also the PGID — `process.kill(-pid, ...)` cascades to
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* (jetsam OOM) and accumulate as orphans until app restart.
|
|
463
|
+
* also the PGID — `process.kill(-pid, ...)` cascades to the runtime's
|
|
464
|
+
* descendants such as its API server, tsserver and pyright. Vite's
|
|
465
|
+
* intentionally separate process group is reaped through its pidfile.
|
|
448
466
|
*/
|
|
449
467
|
pid: number | null;
|
|
450
468
|
startedAt: number;
|
|
@@ -638,6 +656,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
638
656
|
this.opts = opts;
|
|
639
657
|
this.log = opts.logger ?? console;
|
|
640
658
|
this.spawnCommand = opts.spawnCommand ?? defaultSpawnCommand;
|
|
659
|
+
this.sweepOrphanedViteWatches();
|
|
641
660
|
}
|
|
642
661
|
|
|
643
662
|
/**
|
|
@@ -853,7 +872,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
853
872
|
`[WorkerRuntimeManager] maxRuntimes=${cap} exceeded (${running.length} running) — ` +
|
|
854
873
|
`LRU-evicting ${victim.projectId} (idle ${Math.round(idleMs / 1000)}s)`,
|
|
855
874
|
);
|
|
856
|
-
void this.stop(victim.projectId).catch((err: any) => {
|
|
875
|
+
void this.stop(victim.projectId, 'SIGTERM', 'lru-evict').catch((err: any) => {
|
|
857
876
|
this.log.warn(
|
|
858
877
|
`[WorkerRuntimeManager] maxRuntimes eviction of ${victim.projectId} failed: ${err?.message ?? err}`,
|
|
859
878
|
);
|
|
@@ -1141,7 +1160,11 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1141
1160
|
this.armIdleTimer(r);
|
|
1142
1161
|
}
|
|
1143
1162
|
|
|
1144
|
-
async stop(
|
|
1163
|
+
async stop(
|
|
1164
|
+
projectId: string,
|
|
1165
|
+
signal: NodeJS.Signals = 'SIGTERM',
|
|
1166
|
+
reason: RuntimeStopReason = 'stop',
|
|
1167
|
+
): Promise<void> {
|
|
1145
1168
|
const r = this.runtimes.get(projectId);
|
|
1146
1169
|
if (!r) return;
|
|
1147
1170
|
r.status = 'stopping';
|
|
@@ -1154,16 +1177,29 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1154
1177
|
// children (vite, preview-manager's API server, LSPs) start their
|
|
1155
1178
|
// own graceful shutdown in parallel with the parent.
|
|
1156
1179
|
this.killProcessGroup(r, signal);
|
|
1180
|
+
this.killViteWatch(r);
|
|
1157
1181
|
try { r.proc.kill(signal); } catch { /* already gone */ }
|
|
1158
1182
|
await this.waitForExit(r.proc, 5000);
|
|
1159
1183
|
// Belt-and-suspenders: if the grace window elapsed without a
|
|
1160
1184
|
// clean exit, `waitForExit` already SIGKILL'd the parent — chase
|
|
1161
1185
|
// the rest of the group too in case any child ignored SIGTERM.
|
|
1162
1186
|
this.killProcessGroup(r, 'SIGKILL');
|
|
1187
|
+
this.killViteWatch(r);
|
|
1163
1188
|
}
|
|
1164
1189
|
r.pid = null;
|
|
1165
1190
|
this.releasePort(r.agentPort);
|
|
1166
1191
|
this.runtimes.delete(projectId);
|
|
1192
|
+
this.notifyGone(projectId, { reason });
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
private notifyGone(projectId: string, info: RuntimeGoneInfo): void {
|
|
1196
|
+
const cb = this.opts.onRuntimeGone;
|
|
1197
|
+
if (!cb) return;
|
|
1198
|
+
try {
|
|
1199
|
+
cb(projectId, info);
|
|
1200
|
+
} catch (err: any) {
|
|
1201
|
+
this.log.warn(`[WorkerRuntimeManager] onRuntimeGone(${projectId}) threw: ${err?.message ?? err}`);
|
|
1202
|
+
}
|
|
1167
1203
|
}
|
|
1168
1204
|
|
|
1169
1205
|
/**
|
|
@@ -1434,6 +1470,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1434
1470
|
// don't leave a half-booted preview-manager + vite running on
|
|
1435
1471
|
// the allocated ports.
|
|
1436
1472
|
this.killProcessGroup(slot, 'SIGTERM');
|
|
1473
|
+
this.killViteWatch(slot);
|
|
1437
1474
|
try { proc.kill('SIGTERM'); } catch { /* already gone */ }
|
|
1438
1475
|
this.releasePort(slot.agentPort);
|
|
1439
1476
|
slot.agentPort = 0;
|
|
@@ -1520,9 +1557,13 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1520
1557
|
slot.lastError = `memory ceiling exceeded (${rssMB}MB > ${ceilingMB}MB)`;
|
|
1521
1558
|
if (slot.limitWatchdog) { slot.limitWatchdog.stop(); slot.limitWatchdog = null; }
|
|
1522
1559
|
this.killProcessGroup(slot, 'SIGTERM');
|
|
1560
|
+
this.killViteWatch(slot);
|
|
1523
1561
|
try { slot.proc?.kill('SIGTERM'); } catch { /* already gone */ }
|
|
1524
1562
|
// Escalate to SIGKILL if the group ignores SIGTERM within the grace window.
|
|
1525
|
-
const killTimer = setTimeout(() =>
|
|
1563
|
+
const killTimer = setTimeout(() => {
|
|
1564
|
+
this.killProcessGroup(slot, 'SIGKILL');
|
|
1565
|
+
this.killViteWatch(slot);
|
|
1566
|
+
}, 5000);
|
|
1526
1567
|
try { killTimer.unref?.(); } catch { /* unref is best-effort */ }
|
|
1527
1568
|
}
|
|
1528
1569
|
|
|
@@ -1661,6 +1702,51 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1661
1702
|
return env;
|
|
1662
1703
|
}
|
|
1663
1704
|
|
|
1705
|
+
private killViteWatch(slot: InternalRuntime): void {
|
|
1706
|
+
const workspaceDir = slot.spawnConfig.projectDir;
|
|
1707
|
+
if (!workspaceDir) return;
|
|
1708
|
+
try {
|
|
1709
|
+
killViteWatchFromPidfile(workspaceDir, { logger: this.log });
|
|
1710
|
+
} catch (err: any) {
|
|
1711
|
+
this.log.warn(
|
|
1712
|
+
`[WorkerRuntimeManager] Vite watch cleanup for ${slot.projectId} failed: ${err?.message ?? err}`,
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Clean pidfiles left by a previous worker process before this manager
|
|
1719
|
+
* starts accepting projects. The command-line verification in
|
|
1720
|
+
* killViteWatchFromPidfile prevents a reused PID from being signalled.
|
|
1721
|
+
*/
|
|
1722
|
+
private sweepOrphanedViteWatches(): void {
|
|
1723
|
+
const projectsDir = this.opts.autoPull?.projectsDir;
|
|
1724
|
+
if (!projectsDir) return;
|
|
1725
|
+
|
|
1726
|
+
let entries: Array<{ name: string; isDirectory: () => boolean }>;
|
|
1727
|
+
try {
|
|
1728
|
+
entries = readdirSync(projectsDir, { withFileTypes: true });
|
|
1729
|
+
} catch {
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
for (const entry of entries) {
|
|
1733
|
+
if (!entry.isDirectory() || this.runtimes.has(entry.name)) continue;
|
|
1734
|
+
const workspaceDir = join(projectsDir, entry.name);
|
|
1735
|
+
try {
|
|
1736
|
+
const reaped = killViteWatchFromPidfile(workspaceDir, { logger: this.log });
|
|
1737
|
+
if (reaped) {
|
|
1738
|
+
this.log.log(
|
|
1739
|
+
`[WorkerRuntimeManager] Reaped orphaned Vite watch for ${entry.name} (pid=${reaped.pid})`,
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
} catch (err: any) {
|
|
1743
|
+
this.log.warn(
|
|
1744
|
+
`[WorkerRuntimeManager] Vite watch startup sweep failed for ${entry.name}: ${err?.message ?? err}`,
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1664
1750
|
private resolveCwd(slot: InternalRuntime): string {
|
|
1665
1751
|
const cfg = slot.spawnConfig;
|
|
1666
1752
|
if (cfg.projectDir && existsSync(cfg.projectDir)) return cfg.projectDir;
|
|
@@ -1682,6 +1768,10 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1682
1768
|
// The process (and its group) is gone — stop sampling its RSS. A fresh
|
|
1683
1769
|
// watchdog is armed by the next doStart() if the runtime restarts.
|
|
1684
1770
|
if (slot.limitWatchdog) { slot.limitWatchdog.stop(); slot.limitWatchdog = null; }
|
|
1771
|
+
// PreviewManager.stop() normally removes this, but a runtime killed by
|
|
1772
|
+
// jetsam or SIGKILL cannot run its JS teardown. Reap the detached Vite
|
|
1773
|
+
// group from the durable pidfile before deciding whether to restart.
|
|
1774
|
+
this.killViteWatch(slot);
|
|
1685
1775
|
|
|
1686
1776
|
if (slot.status === 'stopping' || this.stopped) {
|
|
1687
1777
|
// We initiated the stop; the orphan reap was already done by
|
|
@@ -1701,6 +1791,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1701
1791
|
slot.agentPort = 0;
|
|
1702
1792
|
slot.apiServerPort = 0;
|
|
1703
1793
|
this.runtimes.delete(slot.projectId);
|
|
1794
|
+
this.notifyGone(slot.projectId, { reason: 'exited', code, signal });
|
|
1704
1795
|
return;
|
|
1705
1796
|
}
|
|
1706
1797
|
|
|
@@ -1713,6 +1804,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1713
1804
|
// respawns the full child tree and leaks more RSS. The
|
|
1714
1805
|
// circuit breaker below stops that loop.
|
|
1715
1806
|
this.killProcessGroup(slot, 'SIGKILL');
|
|
1807
|
+
this.killViteWatch(slot);
|
|
1716
1808
|
slot.pid = null;
|
|
1717
1809
|
|
|
1718
1810
|
const now = Date.now();
|
|
@@ -1752,6 +1844,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1752
1844
|
if (slot.restartTimer) { clearTimeout(slot.restartTimer); slot.restartTimer = null; }
|
|
1753
1845
|
if (slot.idleTimer) { clearTimeout(slot.idleTimer); slot.idleTimer = null; }
|
|
1754
1846
|
this.log.error(`[WorkerRuntimeManager] ${slot.lastError}`);
|
|
1847
|
+
this.notifyGone(slot.projectId, { reason: 'failed', code, signal });
|
|
1755
1848
|
return;
|
|
1756
1849
|
}
|
|
1757
1850
|
|
|
@@ -1801,7 +1894,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
|
|
|
1801
1894
|
return;
|
|
1802
1895
|
}
|
|
1803
1896
|
this.log.log(`[WorkerRuntimeManager] idle-evicting ${slot.projectId} after ${Math.round(since / 1000)}s`);
|
|
1804
|
-
void this.stop(slot.projectId).catch((err) => {
|
|
1897
|
+
void this.stop(slot.projectId, 'SIGTERM', 'idle-evict').catch((err) => {
|
|
1805
1898
|
this.log.warn(`[WorkerRuntimeManager] idle stop failed: ${err?.message ?? err}`);
|
|
1806
1899
|
});
|
|
1807
1900
|
}, idleMs);
|