grix-connector
Version:
Connect local AI coding agents (Claude, Codex, Gemini, Qwen, DeepSeek, Cursor, OpenCode, Pi, OpenHuman, Reasonix) to the Grix scheduling platform. Also serves as an OpenClaw plugin for Grix channel transport.
9 lines (8 loc) • 11 kB
JavaScript
import{execFile as k,spawn as D}from"node:child_process";import{existsSync as P}from"node:fs";import{join as T}from"node:path";import{log as h}from"../log/logger.js";import{resolveCliPath as v,getCliVersion as S}from"../util/cli-probe.js";import{getInstallCommand as y,getCliBinary as A,isKnownAgent as O,detectPlatformOS as U}from"./registry.js";import{checkPrerequisites as x,getMissingPrerequisites as L}from"./preflight.js";import{installMissingPrerequisites as V}from"./prereq-installer.js";import{detectEnvironment as C,formatEnvironmentInfo as q,isEnvironmentSupported as F}from"./env-detect.js";import{generateManualGuide as b}from"./manual-guide.js";import{npmInstallWithMirror as G}from"./npm-registry.js";import{getAllAgentInstallInfo as B}from"./registry.js";class a extends Error{code;constructor(n,t){super(t),this.name="InstallerError",this.code=n}}const w=64*1024,H=20,N=2,W=3e3;class se{os;activeInstalls=new Map;constructor(){this.os=U()}listInstallable(){return{platform:this.os,agents:B(this.os)}}getProgress(n){return this.activeInstalls.get(n)}isInProgress(n){return this.activeInstalls.has(n)}async install(n){const{agentType:t}=n,e=Date.now();if(this.activeInstalls.has(t))return this.fail(t,"preflight",e,new a("INSTALL_IN_PROGRESS",`${t} is already being installed`));try{return await this._doInstall(n,e)}catch(i){this.activeInstalls.delete(t);const o=i instanceof Error?i.message:String(i);return h.error("installer",`${t} install unexpected error: ${o}`),{agentType:t,ok:!1,phase:"failed",error:{code:"INTERNAL",message:`Unexpected error: ${o}`},durationMs:Date.now()-e,output:""}}}async _doInstall(n,t){const{agentType:e}=n;let i;try{i=await C()}catch(l){const c=l instanceof Error?l.message:String(l);return this.fail(e,"preflight",t,new a("INTERNAL",`Environment detection failed: ${c}`))}h.info("installer",`Install request: ${e}
${q(i)}`);const o=F(i);if(!o.supported)return this.fail(e,"preflight",t,new a("ENVIRONMENT_UNSUPPORTED",`Current environment is not supported for automatic installation: ${o.reason}. Please install ${e} manually.`),i);if(this.setProgress(e,"preflight",t),!O(e))return this.fail(e,"preflight",t,new a("UNKNOWN_AGENT",`Unknown agent type: ${e}`),i);const s=y(e,this.os);if(!s){const l=this.getManualHint(e,this.os),c=l?`Installation of ${e} is not supported on ${this.os}. ${l}`:`Installation of ${e} is not supported on ${this.os}`;return this.fail(e,"preflight",t,new a("UNSUPPORTED_OS",c),i)}const u=A(e),m=await v(u),p=m?(await S(m)).version:null;if(m&&!n.force){this.activeInstalls.delete(e);const l=Date.now()-t;return h.info("installer",`${e} already installed at ${m}${p?` (v${p})`:""}`),{agentType:e,ok:!0,phase:"completed",installedPath:m,installedVersion:p,durationMs:l,output:"",environment:i}}const f=s.prerequisites??[];let r=[];if(f.length>0){h.info("installer",`Checking prerequisites for ${e}: ${f.join(", ")}`),r=await x(f,this.os),h.info("installer",`Prerequisites: ${r.map(c=>`${c.label}=${c.met?c.version:"missing"}`).join(", ")}`);const l=L(r);if(l.length>0){const c=l.map(d=>`${d.label}${d.minVersion?` >= ${d.minVersion}`:""}`).join(", ");if(!n.dryRun){if(n.skipPrereqInstall)return this.fail(e,"preflight",t,new a("PREREQ_MISSING",`Missing prerequisites: ${c}. Install them first or retry without skipPrereqInstall.`),i,r);const d=l.map(I=>`${I.label}${I.minVersion?` >= ${I.minVersion}`:""}`);h.info("installer",`Will auto-install prerequisites: ${d.join(", ")}`),this.setProgress(e,"installing_prereq",t,l[0].label,d);const $=await V(l,this.os);if(!$.allOk){const I=$.results.find(_=>!_.ok),R=I?`Failed to install prerequisite ${I.prereq.label}: ${I.output}`:"Prerequisite installation failed";return this.fail(e,"installing_prereq",t,new a("PREREQ_INSTALL_FAILED",R),i,r)}h.info("installer",`All prerequisites installed for ${e}`),r=await x(f,this.os)}}}if(n.dryRun){this.activeInstalls.delete(e);const l=L(r),c=this.getManualHint(e,this.os),d={agentType:e,environment:i,canInstall:!0,alreadyInstalled:!!m,installedPath:m,installedVersion:p,installCommand:s.command,installMode:s.mode,prerequisites:r,missingPrerequisites:l,fallbackCommand:s.fallback?.command??null,manualHint:c},$=b({agentType:e,os:this.os,env:i,missingPrereqs:l});return{agentType:e,ok:!0,phase:"completed",durationMs:Date.now()-t,output:"dry-run: no commands executed",environment:i,dryRun:d,manualGuide:$}}this.setProgress(e,"installing",t),h.info("installer",`Installing ${e}: ${s.command}`);let g;try{g=await this.executeWithRetry(s,e,t,n.timeoutMs)}catch(l){if(s.fallback&&l instanceof a&&(l.code==="INSTALL_FAILED"||l.code==="INSTALL_TIMEOUT")){h.info("installer",`Primary install failed after retries, trying fallback: ${s.fallback.command}`),this.setProgress(e,"installing",t);try{g=await this.executeWithRetry(s.fallback,e,t,n.timeoutMs),g=`[primary failed, fallback succeeded]
${g}`}catch(c){const d=l.message,$=c instanceof a?c.message:String(c);return this.fail(e,"installing",t,new a("FALLBACK_EXHAUSTED",`Both primary and fallback install methods failed.
Primary: ${d}
Fallback: ${$}`),i,r)}}else return l instanceof a?this.fail(e,"installing",t,new a(l.code,l.message),i,r):this.fail(e,"installing",t,new a("INTERNAL",l instanceof Error?l.message:String(l)),i,r)}if(!n.skipVerify&&!s.skipVerification){this.setProgress(e,"verifying",t),h.info("installer",`Verifying ${e} installation...`);let l=await v(u);if(l||(l=await this.resolveViaNpmBin(u)),!l)return this.fail(e,"verifying",t,new a("VERIFICATION_FAILED",`${u} not found on PATH after installation. You may need to open a new terminal or run: source ~/.zshrc (or ~/.bashrc)`),i,r);const{version:c}=await S(l),d=Date.now()-t;return this.activeInstalls.delete(e),h.info("installer",`${e} installed successfully at ${l} (v${c??"unknown"}, ${d}ms)`),{agentType:e,ok:!0,phase:"completed",installedPath:l,installedVersion:c,durationMs:d,output:g,prerequisites:r.length>0?r:void 0,environment:i}}const E=Date.now()-t;return this.activeInstalls.delete(e),h.info("installer",`${e} install command completed (${E}ms, verification skipped)`),{agentType:e,ok:!0,phase:"completed",installedPath:null,installedVersion:null,durationMs:E,output:g,prerequisites:r.length>0?r:void 0,environment:i}}getManualHint(n,t){const i=y(n,t)?.fallback,s={claude:"https://docs.anthropic.com/en/docs/claude-code/overview",codex:"https://github.com/openai/codex",gemini:"https://github.com/google-gemini/gemini-cli",qwen:"https://github.com/QwenLM/qwen-code",cursor:"https://cursor.com/docs/cli/installation",copilot:"https://docs.github.com/en/copilot/managing-copilot/configure-personal-settings/installing-github-copilot-in-the-cli",kiro:"https://kiro.dev/docs/cli/",openclaw:"https://github.com/openclaw/openclaw",reasonix:"https://github.com/esengine/DeepSeek-Reasonix",openhuman:"https://github.com/tinyhumansai/openhuman/issues/128"}[n],u=[];return s&&u.push(`Docs: ${s}`),i&&u.push(`Alternative: ${i.command}`),u.length>0?u.join(" | "):null}setProgress(n,t,e,i,o){this.activeInstalls.set(n,{agentType:n,phase:t,startedAt:e,elapsedMs:Date.now()-e,...i?{currentPrereq:i}:{},...o?{pendingPrereqs:o}:{}})}fail(n,t,e,i,o,s,u){const m=u??this.activeInstalls.get(n)?.outputTail??"";this.activeInstalls.delete(n),h.error("installer",`${n} install failed at ${t}: ${i.message}`);const p=s?L(s):[],f=b({agentType:n,os:this.os,env:o??{platform:this.os,osVersion:"unknown",arch:process.arch,shell:process.env.SHELL??null,nodeVersion:null,npmVersion:null,isDocker:!1,isCI:!1},missingPrereqs:p,primaryFailed:t==="installing",fallbackFailed:i.code==="FALLBACK_EXHAUSTED",error:i.message});return{agentType:n,ok:!1,phase:"failed",error:{code:i.code,message:i.message},durationMs:Date.now()-e,output:m,environment:o,prerequisites:s,manualGuide:f}}async executeWithRetry(n,t,e,i){let o=null;for(let s=0;s<=N;s++)try{return s>0&&(h.info("installer",`Retry ${s}/${N} for ${t}...`),await this.sleep(W)),await this.executeCommand(n,t,e,i)}catch(u){if(o=u instanceof a?u:new a("INTERNAL",String(u)),!(o.code==="INSTALL_TIMEOUT")||s>=N)throw o;h.info("installer",`Attempt ${s+1} failed (retryable): ${o.message}`)}throw o??new a("INTERNAL","Unexpected retry loop exit")}sleep(n){return new Promise(t=>setTimeout(t,n))}executeCommand(n,t,e,i){const o=i??n.timeoutMs;switch(n.mode){case"npm":return this.executeNpm(n.npmPackage,o,t,e);case"shell":return this.executeShell(n.command,o,t,e);case"exec":return this.executeExec(n.command,n.execArgs??[],o,t,e);default:return Promise.reject(new a("INTERNAL",`Unknown install mode: ${n.mode}`))}}async executeNpm(n,t,e,i){try{const{output:o,registry:s}=await G(n,t,w);return h.info("installer",`npm install ${n} succeeded via ${s}`),o}catch(o){const s=o instanceof Error?o.message:String(o);throw s.includes("timed out")||s.includes("ETIMEDOUT")?new a("INSTALL_TIMEOUT",`npm install timed out (tried all mirrors): ${s}`):new a("INSTALL_FAILED",`npm install failed (tried all mirrors): ${s}`)}}executeShell(n,t,e,i){return new Promise((o,s)=>{const u=process.platform==="win32",m=u?"cmd.exe":"sh",p=u?["/d","/s","/c",n]:["-c",n];h.info("installer",`exec: ${m} ${p.join(" ")}`);const f=D(m,p,{timeout:t,stdio:["ignore","pipe","pipe"],env:{...process.env,NONINTERACTIVE:"1",DEBIAN_FRONTEND:"noninteractive"}});let r="",g=!1;const E=setTimeout(()=>{g=!0;try{f.kill("SIGTERM")}catch{}setTimeout(()=>{try{f.kill("SIGKILL")}catch{}},5e3).unref()},t);f.stdout?.on("data",l=>{const c=l.toString("utf-8");r+=c,r.length>w&&(r=r.slice(-w)),this.updateOutputTail(e,i,r)}),f.stderr?.on("data",l=>{const c=l.toString("utf-8");r+=c,r.length>w&&(r=r.slice(-w)),this.updateOutputTail(e,i,r)}),f.on("error",l=>{clearTimeout(E),s(new a("INSTALL_FAILED",`Spawn error: ${l.message}`))}),f.on("close",l=>{if(clearTimeout(E),g){s(new a("INSTALL_TIMEOUT",`Install timed out after ${t/1e3}s`));return}if(l!==0){const c=r.slice(-1024);s(new a("INSTALL_FAILED",`Process exited with code ${l}: ${c}`));return}o(r.trim())})})}executeExec(n,t,e,i,o){return new Promise((s,u)=>{h.info("installer",`exec: ${n} ${t.join(" ")}`);const m=k(n,t,{timeout:e,maxBuffer:w},(p,f,r)=>{if(p){if(p.killed)u(new a("INSTALL_TIMEOUT",`${n} timed out after ${e/1e3}s`));else{const g=r?.trim()||p.message;u(new a("INSTALL_FAILED",`${n} failed: ${g}`))}return}s(`${f??""}
${r??""}`.trim())});this.trackOutput(m,i,o)})}trackOutput(n,t,e){n.on("close",()=>{const i=this.activeInstalls.get(t);i&&(i.elapsedMs=Date.now()-e)})}async resolveViaNpmBin(n){return new Promise(t=>{const e=process.platform==="win32";k(e?"cmd.exe":"npm",e?["/c","npm","prefix","-g"]:["prefix","-g"],{timeout:5e3,encoding:"utf-8"},(s,u)=>{if(s){t(null);return}const m=u.trim();if(!m){t(null);return}const p=this.os==="windows"?T(m,`${n}.cmd`):T(m,"bin",n);if(P(p)){t(p);return}const f=T(m,n);if(P(f)){t(f);return}t(null)})})}updateOutputTail(n,t,e){const o=e.split(`
`).slice(-H).join(`
`),s=this.activeInstalls.get(n);s&&(s.outputTail=o,s.elapsedMs=Date.now()-t)}}export{se as AgentInstaller,a as InstallerError};