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.
12 lines (11 loc) • 11.8 kB
JavaScript
import{execFile as N,spawn as D}from"node:child_process";import{existsSync as y}from"node:fs";import{delimiter as k,dirname as O,join as L}from"node:path";import{log as m}from"../log/logger.js";import{resolveCliPath as S,getCliVersion as x,resolveWindowsInstalledCli as V,invalidateWindowsRegistryPathCache as C}from"../util/cli-probe.js";import{getInstallCommand as R,getCliBinary as U,isKnownAgent as F,detectPlatformOS as q,formatInstallCommand as H}from"./registry.js";import{checkPrerequisites as b,getMissingPrerequisites as P}from"./preflight.js";import{installMissingPrerequisites as W}from"./prereq-installer.js";import{detectEnvironment as j,formatEnvironmentInfo as G,isEnvironmentSupported as B}from"./env-detect.js";import{generateManualGuide as M}from"./manual-guide.js";import{npmInstallWithMirror as K,isTransientInstallError as X}from"./npm-registry.js";import{getAllAgentInstallInfo as Y}from"./registry.js";class c extends Error{code;constructor(n,t){super(t),this.name="InstallerError",this.code=n}}const w=64*1024,Q=20,v=2,z=3e3;class ue{os;activeInstalls=new Map;constructor(){this.os=q()}listInstallable(){return{platform:this.os,agents:Y(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 c("INSTALL_IN_PROGRESS",`${t} is already being installed`));try{return await this._doInstall(n,e)}catch(s){this.activeInstalls.delete(t);const o=s instanceof Error?s.message:String(s);return m.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 s;try{s=await j()}catch(i){const u=i instanceof Error?i.message:String(i);return this.fail(e,"preflight",t,new c("INTERNAL",`Environment detection failed: ${u}`))}m.info("installer",`Install request: ${e}
${G(s)}`);const o=B(s);if(!o.supported)return this.fail(e,"preflight",t,new c("ENVIRONMENT_UNSUPPORTED",`Current environment is not supported for automatic installation: ${o.reason}. Please install ${e} manually.`),s);if(this.setProgress(e,"preflight",t),!F(e))return this.fail(e,"preflight",t,new c("UNKNOWN_AGENT",`Unknown agent type: ${e}`),s);const l=R(e,this.os);if(!l){const i=this.getManualHint(e,this.os),u=i?`Installation of ${e} is not supported on ${this.os}. ${i}`:`Installation of ${e} is not supported on ${this.os}`;return this.fail(e,"preflight",t,new c("UNSUPPORTED_OS",u),s)}const a=U(e),f=await S(a),p=f?(await x(f)).version:null;if(f&&!n.force&&!n.dryRun){this.activeInstalls.delete(e);const i=Date.now()-t;return m.info("installer",`${e} already installed at ${f}${p?` (v${p})`:""}`),{agentType:e,ok:!0,phase:"completed",installedPath:f,installedVersion:p,durationMs:i,output:"",environment:s}}const h=l.prerequisites??[];let r=[];if(h.length>0){m.info("installer",`Checking prerequisites for ${e}: ${h.join(", ")}`),r=await b(h,this.os),m.info("installer",`Prerequisites: ${r.map(u=>`${u.label}=${u.met?u.version:"missing"}`).join(", ")}`);const i=P(r);if(i.length>0){const u=i.map(d=>`${d.label}${d.minVersion?` >= ${d.minVersion}`:""}`).join(", ");if(!n.dryRun){if(n.skipPrereqInstall)return this.fail(e,"preflight",t,new c("PREREQ_MISSING",`Missing prerequisites: ${u}. Install them first or retry without skipPrereqInstall.`),s,r);const d=i.map($=>`${$.label}${$.minVersion?` >= ${$.minVersion}`:""}`);m.info("installer",`Will auto-install prerequisites: ${d.join(", ")}`),this.setProgress(e,"installing_prereq",t,i[0].label,d);const I=await W(i,this.os);if(!I.allOk){const $=I.results.find(A=>!A.ok),T=$?`Failed to install prerequisite ${$.prereq.label}: ${$.output}`:"Prerequisite installation failed";return this.fail(e,"installing_prereq",t,new c("PREREQ_INSTALL_FAILED",T),s,r)}m.info("installer",`All prerequisites installed for ${e}`),r=await b(h,this.os)}}}if(n.dryRun){this.activeInstalls.delete(e);const i=P(r),u=this.getManualHint(e,this.os),d={agentType:e,environment:s,canInstall:!0,alreadyInstalled:!!f,installedPath:f,installedVersion:p,installCommand:H(l),installMode:l.mode,prerequisites:r,missingPrerequisites:i,fallbackCommand:l.fallback?.command??null,manualHint:u},I=M({agentType:e,os:this.os,env:s,missingPrereqs:i});return{agentType:e,ok:!0,phase:"completed",durationMs:Date.now()-t,output:"dry-run: no commands executed",environment:s,dryRun:d,manualGuide:I}}this.setProgress(e,"installing",t),m.info("installer",`Installing ${e}: ${l.command}`);let g;try{g=await this.executeWithRetry(l,e,t,n.timeoutMs)}catch(i){if(l.fallback&&i instanceof c&&(i.code==="INSTALL_FAILED"||i.code==="INSTALL_TIMEOUT")){m.info("installer",`Primary install failed after retries, trying fallback: ${l.fallback.command}`),this.setProgress(e,"installing",t);try{g=await this.executeWithRetry(l.fallback,e,t,n.timeoutMs),g=`[primary failed, fallback succeeded]
${g}`}catch(u){const d=i.message,I=u instanceof c?u.message:String(u);return this.fail(e,"installing",t,new c("FALLBACK_EXHAUSTED",`Both primary and fallback install methods failed.
Primary: ${d}
Fallback: ${I}`),s,r)}}else return i instanceof c?this.fail(e,"installing",t,new c(i.code,i.message),s,r):this.fail(e,"installing",t,new c("INTERNAL",i instanceof Error?i.message:String(i)),s,r)}{const i=(g??"").trim().split(`
`).slice(-12).join(`
`);m.info("installer",`${e} \u5B89\u88C5\u547D\u4EE4\u8F93\u51FA(\u5C3E\u90E8):
${i||"<\u7A7A>"}`)}if(C(),!n.skipVerify&&!l.skipVerification){this.setProgress(e,"verifying",t),m.info("installer",`Verifying ${e} installation...`);let i=await S(a);if(i||(i=await this.resolveViaNpmBin(a)),!i&&(i=await this.resolveViaRegistryPath(a),i)){const I=O(i);(process.env.PATH??"").split(k).some(T=>T.toLowerCase()===I.toLowerCase())||(process.env.PATH=`${I}${k}${process.env.PATH??""}`,m.info("installer",`Injected ${I} into process PATH (picked up from registry)`))}if(!i)return this.fail(e,"verifying",t,new c("VERIFICATION_FAILED",`${a} not found on PATH after installation. You may need to open a new terminal or run: source ~/.zshrc (or ~/.bashrc)`),s,r);const{version:u}=await x(i),d=Date.now()-t;return this.activeInstalls.delete(e),m.info("installer",`${e} installed successfully at ${i} (v${u??"unknown"}, ${d}ms)`),{agentType:e,ok:!0,phase:"completed",installedPath:i,installedVersion:u,durationMs:d,output:g,prerequisites:r.length>0?r:void 0,environment:s}}const E=Date.now()-t;return this.activeInstalls.delete(e),m.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:s}}getManualHint(n,t){const s=R(n,t)?.fallback,l={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],a=[];return l&&a.push(`Docs: ${l}`),s&&a.push(`Alternative: ${s.command}`),a.length>0?a.join(" | "):null}setProgress(n,t,e,s,o){this.activeInstalls.set(n,{agentType:n,phase:t,startedAt:e,elapsedMs:Date.now()-e,...s?{currentPrereq:s}:{},...o?{pendingPrereqs:o}:{}})}fail(n,t,e,s,o,l,a){const f=a??this.activeInstalls.get(n)?.outputTail??"";this.activeInstalls.delete(n),m.error("installer",`${n} install failed at ${t}: ${s.message}`);const p=l?P(l):[],h=M({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:s.code==="FALLBACK_EXHAUSTED",error:s.message});return{agentType:n,ok:!1,phase:"failed",error:{code:s.code,message:s.message},durationMs:Date.now()-e,output:f,environment:o,prerequisites:l,manualGuide:h}}async executeWithRetry(n,t,e,s){let o=null;for(let l=0;l<=v;l++)try{return l>0&&(m.info("installer",`Retry ${l}/${v} for ${t}...`),await this.sleep(z)),await this.executeCommand(n,t,e,s)}catch(a){if(o=a instanceof c?a:new c("INTERNAL",String(a)),!(o.code==="INSTALL_TIMEOUT"||o.code==="INSTALL_FAILED"&&X(o.message))||l>=v)throw o;m.info("installer",`Attempt ${l+1} failed (retryable): ${o.message}`)}throw o??new c("INTERNAL","Unexpected retry loop exit")}sleep(n){return new Promise(t=>setTimeout(t,n))}executeCommand(n,t,e,s){const o=s??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 c("INTERNAL",`Unknown install mode: ${n.mode}`))}}async executeNpm(n,t,e,s){try{const{output:o,registry:l}=await K(n,t,w);return m.info("installer",`npm install ${n} succeeded via ${l}`),o}catch(o){const l=o instanceof Error?o.message:String(o);throw l.includes("timed out")||l.includes("ETIMEDOUT")?new c("INSTALL_TIMEOUT",`npm install timed out (tried all mirrors): ${l}`):new c("INSTALL_FAILED",`npm install failed (tried all mirrors): ${l}`)}}executeShell(n,t,e,s){return new Promise((o,l)=>{const a=process.platform==="win32",f=a?"cmd.exe":"sh",p=a?["/d","/c",n]:["-c",n];m.info("installer",`exec: ${f} ${p.join(" ")}`);const h=D(f,p,{timeout:t,stdio:["ignore","pipe","pipe"],...a?{windowsVerbatimArguments:!0}:{},env:{...process.env,NONINTERACTIVE:"1",DEBIAN_FRONTEND:"noninteractive"}});let r="",g=!1;const E=setTimeout(()=>{g=!0;try{h.kill("SIGTERM")}catch{}setTimeout(()=>{try{h.kill("SIGKILL")}catch{}},5e3).unref()},t);h.stdout?.on("data",i=>{const u=i.toString("utf-8");r+=u,r.length>w&&(r=r.slice(-w)),this.updateOutputTail(e,s,r)}),h.stderr?.on("data",i=>{const u=i.toString("utf-8");r+=u,r.length>w&&(r=r.slice(-w)),this.updateOutputTail(e,s,r)}),h.on("error",i=>{clearTimeout(E),l(new c("INSTALL_FAILED",`Spawn error: ${i.message}`))}),h.on("close",i=>{if(clearTimeout(E),g){l(new c("INSTALL_TIMEOUT",`Install timed out after ${t/1e3}s`));return}if(i!==0){const u=r.slice(-1024);l(new c("INSTALL_FAILED",`Process exited with code ${i}: ${u}`));return}o(r.trim())})})}executeExec(n,t,e,s,o){return new Promise((l,a)=>{m.info("installer",`exec: ${n} ${t.join(" ")}`);const f=N(n,t,{timeout:e,maxBuffer:w},(p,h,r)=>{if(p){if(p.killed)a(new c("INSTALL_TIMEOUT",`${n} timed out after ${e/1e3}s`));else{const g=r?.trim()||p.message;a(new c("INSTALL_FAILED",`${n} failed: ${g}`))}return}l(`${h??""}
${r??""}`.trim())});this.trackOutput(f,s,o)})}trackOutput(n,t,e){n.on("close",()=>{const s=this.activeInstalls.get(t);s&&(s.elapsedMs=Date.now()-e)})}async resolveViaNpmBin(n){return new Promise(t=>{const e=process.platform==="win32";N(e?"cmd.exe":"npm",e?["/c","npm","prefix","-g"]:["prefix","-g"],{timeout:5e3,encoding:"utf-8"},(l,a)=>{if(l){t(null);return}const f=a.trim();if(!f){t(null);return}const p=this.os==="windows"?L(f,`${n}.cmd`):L(f,"bin",n);if(y(p)){t(p);return}const h=L(f,n);if(y(h)){t(h);return}t(null)})})}async resolveViaRegistryPath(n){const t=await V(n);return m.info("installer",`\u5E38\u89C1\u76EE\u5F55/\u6CE8\u518C\u8868\u515C\u5E95\u67E5\u627E ${n}: \u7ED3\u679C=${t??"\u672A\u627E\u5230"}`),t}updateOutputTail(n,t,e){const o=e.split(`
`).slice(-Q).join(`
`),l=this.activeInstalls.get(n);l&&(l.outputTail=o,l.elapsedMs=Date.now()-t)}}export{ue as AgentInstaller,c as InstallerError};