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.
54 lines (51 loc) • 14 kB
JavaScript
import{existsSync as R,readFileSync as j}from"node:fs";import{mkdir as P,readdir as C,readFile as f,rm as w,stat as V,writeFile as h}from"node:fs/promises";import p from"node:os";import u from"node:path";import{isProcessRunning as v,runCommand as d,spawnDetached as W,isWindowsElevated as G,killProcessesByCommandLine as T}from"./process-control.js";import{getServicePrefix as A,parseConfigDirFromPlistXML as Q,parseConfigDirFromSystemdUnit as z,resolveBareDaemonMarkerPath as g,resolveLinuxUserUnitPath as H,resolveMacOSLaunchAgentPath as J}from"./service-paths.js";function y(t){return String(t??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function F(t){return`'${String(t??"").replace(/'/g,"'\\''")}'`}function M(t){return[t.nodePath,t.cliPath,...t.configDir?["--config-dir",t.configDir]:[]]}function X(t){const r=M(t);return`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${y(t.serviceID)}</string>
<key>ProgramArguments</key>
<array>
${r.map(e=>` <string>${y(e)}</string>`).join(`
`)}
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>WorkingDirectory</key>
<string>${y(u.dirname(t.cliPath))}</string>
${t.environmentPath?` <key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>${y(t.environmentPath)}</string>
</dict>
`:""} <key>StandardOutPath</key>
<string>${y(t.stdoutPath)}</string>
<key>StandardErrorPath</key>
<string>${y(t.stderrPath)}</string>
</dict>
</plist>
`}function Z(t){const r=M(t).map(e=>F(e)).join(" ");return`[Unit]
Description=grix-connector daemon (${t.serviceID})
After=network.target
[Service]
Type=simple
ExecStart=${r}
WorkingDirectory=${u.dirname(t.cliPath)}
Restart=always
RestartSec=2
StandardOutput=append:${t.stdoutPath}
StandardError=append:${t.stderrPath}
[Install]
WantedBy=default.target
`}function k(t,r){const e=t.replace(/[^a-zA-Z0-9._-]/g,"_");return u.join(r,".grix",`${e}-wrapper.vbs`)}function B(t,r){const e=t.replace(/[^a-zA-Z0-9._-]/g,"_"),n=u.join(r,"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup");return u.join(n,`${e}.lnk`)}function $(t){return String(t??"").replace(/"/g,'""')}function q(t,r){const e=u.join(u.dirname(t),u.basename(r).replace(/\.lnk$/,"-create.vbs")),n=["Option Explicit","Dim shell, shortcut",'Set shell = CreateObject("WScript.Shell")',`Set shortcut = shell.CreateShortcut("${$(r)}")`,'shortcut.TargetPath = "wscript.exe"',`shortcut.Arguments = "//B //NoLogo \\"${$(t)}\\""`,"shortcut.WindowStyle = 7","shortcut.Save",""].join(`\r
`);return{path:e,content:n}}function K(t){const r=[`"${$(t.nodePath)}"`,`"${$(t.cliPath)}"`,...t.configDir?["--config-dir",`"${$(t.configDir)}"`]:[]].join(" ");return["Option Explicit","Dim shell, command, delayMs, rapidCount",`command = "${$(r)}"`,'Set shell = CreateObject("WScript.Shell")',"delayMs = 5000","rapidCount = 0","Do"," shell.Run command, 0, True"," rapidCount = rapidCount + 1"," If rapidCount >= 10 Then"," WScript.Sleep 300000"," rapidCount = 0"," Else"," WScript.Sleep delayMs"," End If","Loop",""].join(`\r
`)}function b(t){return`gui/${t??0}`}function E(t){const r=String(t?.stdout??"").trim();return String(t?.stderr??"").trim()||r||`exit=${Number(t?.exitCode??-1)}`}function L(t,r){if(Number(t?.exitCode??0)!==0)throw new Error(`${r}: ${E(t)}`)}function Y(){return{platform:"darwin",kind:"launchd",async install({serviceID:t,nodePath:r,cliPath:e,configDir:n,stdoutPath:a,stderrPath:i,environmentPath:s="",homeDir:o=p.homedir()}){const c=J(t,o);return await P(u.dirname(c),{recursive:!0}),await h(c,X({serviceID:t,nodePath:r,cliPath:e,configDir:n,stdoutPath:a,stderrPath:i,environmentPath:s}),{encoding:"utf8",mode:384}),{definitionPath:c}},async start({serviceID:t,definitionPath:r,runCommand:e=d,uid:n=process.getuid?.()??0}){const a=b(n);let i=await e("launchctl",["bootstrap",a,r],{allowFailure:!0});Number(i?.exitCode??0)!==0&&(await e("launchctl",["bootout",`${a}/${t}`],{allowFailure:!0}),i=await e("launchctl",["bootstrap",a,r],{allowFailure:!0}),L(i,`launchctl bootstrap ${a}`));const s=await e("launchctl",["kickstart","-k",`${a}/${t}`],{allowFailure:!0});if(Number(s?.exitCode??0)!==0){const o=Number(i?.exitCode??0)===0?"":`, bootstrap=${E(i)}`;throw new Error(`launchctl start failed for ${a}/${t}: ${E(s)}${o}`)}},async stop({serviceID:t,runCommand:r=d,uid:e=process.getuid?.()??0}){const n=b(e);await r("launchctl",["bootout",`${n}/${t}`],{allowFailure:!0})},async restart({serviceID:t,definitionPath:r,runCommand:e=d,uid:n=process.getuid?.()??0}){const a=b(n);await e("launchctl",["bootout",`${a}/${t}`],{allowFailure:!0});for(let o=0;o<20&&(await e("launchctl",["print",`${a}/${t}`],{allowFailure:!0})).exitCode===0;o+=1)await new Promise(l=>setTimeout(l,250));try{await V(r)}catch{throw new Error(`launchd plist missing: ${r}`)}const i=await e("launchctl",["bootstrap",a,r],{allowFailure:!0});L(i,`launchctl bootstrap ${a}`);const s=await e("launchctl",["kickstart","-k",`${a}/${t}`],{allowFailure:!0});L(s,`launchctl kickstart ${a}/${t}`)},async uninstall({serviceID:t,definitionPath:r,runCommand:e=d,uid:n=process.getuid?.()??0}){const a=b(n);await e("launchctl",["bootout",`${a}/${t}`],{allowFailure:!0}),await w(r,{force:!0})},async discoverServices({homeDir:t=p.homedir()}={}){const r=u.join(t,"Library","LaunchAgents"),e=await C(r).catch(()=>[]),n=A("darwin"),a=[];for(const i of e){if(!i.startsWith(n)||!i.endsWith(".plist"))continue;const s=i.slice(0,-6),o=u.join(r,i);let c=null;try{const l=await f(o,"utf8");c=Q(l)}catch{}a.push({serviceID:s,definitionPath:o,configDir:c})}return a},async isServiceLoaded({serviceID:t,runCommand:r=d,uid:e=process.getuid?.()??0}){const n=b(e),a=await r("launchctl",["print",`${n}/${t}`],{allowFailure:!0});return Number(a?.exitCode??1)===0}}}function I(){return{platform:"win32",kind:"task-scheduler",async install({serviceID:t,nodePath:r,cliPath:e,configDir:n,runCommand:a=d,homeDir:i=p.homedir()}){const s=k(t,i);await P(u.dirname(s),{recursive:!0}),await h(s,K({nodePath:r,cliPath:e,configDir:n}),"utf8");let o=!1;if(G())try{await a("schtasks",["/Create","/TN",t,"/SC","ONCE","/ST","00:00","/SD","2099/12/31","/RL","LIMITED","/F","/TR",`wscript.exe //B //NoLogo "${s}"`]),o=!0}catch{}const c=B(t,i),l=q(s,c);return await h(l.path,l.content,"utf8"),await a("wscript.exe",[l.path,"//B","//NoLogo"]).catch(()=>{}),await w(l.path,{force:!0}),{definitionPath:o?`task:${t}`:`startup:${t}`}},async start({serviceID:t,definitionPath:r,runCommand:e=d,homeDir:n=p.homedir()}){const a=k(t,n);if(r.startsWith("task:"))try{await e("schtasks",["/Run","/TN",t]);return}catch{}W("wscript.exe",["//B","//NoLogo",a])},async stop({serviceID:t,runCommand:r=d,homeDir:e=p.homedir()}){await r("schtasks",["/End","/TN",t],{allowFailure:!0});const n=`${t}-wrapper.vbs`;await T(n,{platform:"win32"})},async restart({serviceID:t,definitionPath:r,runCommand:e=d,homeDir:n=p.homedir()}){await e("schtasks",["/End","/TN",t],{allowFailure:!0});const a=`${t}-wrapper.vbs`;await T(a,{platform:"win32"});const i=k(t,n);if(r?.startsWith("task:"))try{await e("schtasks",["/Run","/TN",t]);return}catch{}W("wscript.exe",["//B","//NoLogo",i])},async uninstall({serviceID:t,runCommand:r=d,homeDir:e=p.homedir()}){await r("schtasks",["/Delete","/TN",t,"/F"],{allowFailure:!0});const n=k(t,e);await w(n,{force:!0});const a=B(t,e);await w(a,{force:!0})},async discoverServices({homeDir:t=p.homedir(),runCommand:r=d}={}){const e=A("win32"),n=await r("schtasks",["/Query","/FO","CSV","/NH"],{allowFailure:!0}),a=[];if(Number(n?.exitCode??-1)===0){const o=String(n.stdout??"").split(/\r?\n/);for(const c of o){const l=c.match(/^"([^"]+)"/);if(!l)continue;const m=l[1];if(!m.startsWith(e))continue;let _=null;try{const x=k(m,t),D=(await f(x,"utf8")).match(/--config-dir\s+""([^""]+)""/);D&&(_=D[1])}catch{}a.push({serviceID:m,definitionPath:`task:${m}`,configDir:_})}}const i=u.join(t,"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup"),s=await C(i).catch(()=>[]);for(const o of s){if(!o.startsWith(e)||!o.endsWith(".lnk"))continue;const c=o.slice(0,-4);if(a.some(m=>m.serviceID===c))continue;let l=null;try{const m=k(c,t),x=(await f(m,"utf8")).match(/--config-dir\s+""([^""]+)""/);x&&(l=x[1])}catch{}a.push({serviceID:c,definitionPath:`startup:${c}`,configDir:l})}return a},async isServiceLoaded({serviceID:t,runCommand:r=d}){const e=await r("schtasks",["/Query","/TN",t,"/NH"],{allowFailure:!0});return Number(e?.exitCode??1)===0}}}function tt(){return{platform:"linux",kind:"systemd-user",async install({serviceID:t,nodePath:r,cliPath:e,configDir:n,stdoutPath:a,stderrPath:i,homeDir:s=p.homedir(),runCommand:o=d}){const c=H(t,s);return await P(u.dirname(c),{recursive:!0}),await h(c,Z({serviceID:t,nodePath:r,cliPath:e,configDir:n,stdoutPath:a,stderrPath:i}),{encoding:"utf8",mode:384}),await o("systemctl",["--user","daemon-reload"]),await o("systemctl",["--user","enable",`${t}.service`]),{definitionPath:c}},async start({serviceID:t,runCommand:r=d}){await r("systemctl",["--user","start",`${t}.service`])},async stop({serviceID:t,runCommand:r=d}){await r("systemctl",["--user","stop",`${t}.service`],{allowFailure:!0})},async restart({serviceID:t,runCommand:r=d}){await r("systemctl",["--user","restart",`${t}.service`])},async uninstall({serviceID:t,definitionPath:r,runCommand:e=d}){await e("systemctl",["--user","stop",`${t}.service`],{allowFailure:!0}),await e("systemctl",["--user","disable",`${t}.service`],{allowFailure:!0}),await w(r,{force:!0}),await e("systemctl",["--user","daemon-reload"])},async discoverServices({homeDir:t=p.homedir()}={}){const r=u.join(t,".config","systemd","user"),e=await C(r).catch(()=>[]),n=A("linux"),a=[];for(const i of e){if(!i.startsWith(n)||!i.endsWith(".service"))continue;const s=i.slice(0,-8),o=u.join(r,i);let c=null;try{const l=await f(o,"utf8");c=z(l)}catch{}a.push({serviceID:s,definitionPath:o,configDir:c})}return a},async isServiceLoaded({serviceID:t,runCommand:r=d}){const e=await r("systemctl",["--user","is-active",`${t}.service`],{allowFailure:!0});return String(e?.stdout??"").trim()==="active"}}}function S(t){try{const r=j(t,"utf8"),e=JSON.parse(r);if(e&&typeof e.service_id=="string"&&typeof e.node_path=="string"&&typeof e.cli_path=="string"&&typeof e.root_dir=="string")return e}catch{}return null}function rt(t){return u.join(t,"daemon.lock.json")}function N(t){const r=rt(t);try{const e=j(r,"utf8"),n=JSON.parse(e);if(typeof n?.pid=="number")return n.pid}catch{}return 0}function O(t){return u.join(t,".profile")}function et(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function nt(t){const r=[F(t.nodePath),F(t.cliPath)];t.configDir&&r.push("--config-dir",F(t.configDir));const e=r.join(" ");return`${e} status >/dev/null 2>&1 || (nohup ${e} start >/dev/null 2>&1 &)`}function at(t,r){return`# >>> grix-connector autostart [${t}] >>>
${r}
# <<< grix-connector autostart [${t}] <<<
`}function U(t){const r=et(t);return new RegExp(`(?:^|\\n)# >>> grix-connector autostart \\[${r}\\] >>>\\n[\\s\\S]*?\\n# <<< grix-connector autostart \\[${r}\\] <<<\\n?`,"m")}async function it(t,r,e){let n="";try{n=await f(t,"utf8")}catch{}const a=at(r,e),i=U(r);let s;if(i.test(n))s=n.replace(i,`
${a}`).replace(/^\n+/,"");else{const o=n.length===0||n.endsWith(`
`)?"":`
`;s=`${n}${o}${a}`}s!==n&&(await P(u.dirname(t),{recursive:!0}),await h(t,s,{encoding:"utf8"}))}async function ot(t,r){let e="";try{e=await f(t,"utf8")}catch{return}const n=U(r);if(!n.test(e))return;const a=e.replace(n,"").replace(/^\n+/,"");await h(t,a,{encoding:"utf8"})}function st(){return{platform:"linux",kind:"bare-daemon",async install({serviceID:t,nodePath:r,cliPath:e,configDir:n,stdoutPath:a,stderrPath:i,homeDir:s=p.homedir()}){const o=g(t,s);await P(u.dirname(o),{recursive:!0});const c=u.resolve(u.dirname(u.dirname(a||i||o))),l={schema_version:1,service_id:t,node_path:r,cli_path:e,config_dir:n??"",root_dir:c,stdout_path:a,stderr_path:i,installed_at:Date.now()};await h(o,`${JSON.stringify(l,null,2)}
`,{encoding:"utf8",mode:384});const m=nt({nodePath:r,cliPath:e,configDir:n??""});return await it(O(s),t,m),{definitionPath:o}},async start({serviceID:t,homeDir:r=p.homedir()}){const e=g(t,r),n=S(e);if(!n)throw new Error(`bare-daemon marker missing: ${e}`);const a=[n.cli_path];n.config_dir&&a.push("--config-dir",n.config_dir),W(n.node_path,a)},async stop({serviceID:t,homeDir:r=p.homedir()}){const e=g(t,r),n=S(e);if(!n)return;const a=N(n.root_dir);if(a>0&&v(a))try{process.kill(a,"SIGTERM")}catch{}},async restart(t){await this.stop(t);const r=Date.now()+5e3,e=g(t.serviceID,t.homeDir??p.homedir()),n=S(e);for(;n&&Date.now()<r;){const a=N(n.root_dir);if(a<=0||!v(a))break;await new Promise(i=>setTimeout(i,100))}await this.start(t)},async uninstall({serviceID:t,homeDir:r=p.homedir()}){const e=g(t,r),n=S(e);if(n){const a=N(n.root_dir);if(a>0&&v(a))try{process.kill(a,"SIGTERM")}catch{}}await w(e,{force:!0}),await ot(O(r),t)},async discoverServices({homeDir:t=p.homedir()}={}){const r=u.join(t,".grix","service"),e=await C(r).catch(()=>[]),n=A("linux"),a=[];for(const i of e){if(!i.startsWith(n)||!i.endsWith(".bare.json"))continue;const s=i.slice(0,-10),o=u.join(r,i),c=S(o);a.push({serviceID:s,definitionPath:o,configDir:c?.config_dir||null})}return a},async isServiceLoaded({serviceID:t,homeDir:r=p.homedir()}){const e=g(t,r),n=S(e);if(!n)return!1;const a=N(n.root_dir);return a>0&&v(a)}}}function ct(t=process.getuid?.()??0){if(process.env.GRIX_FORCE_BARE_DAEMON==="1")return!1;const r=process.env.XDG_RUNTIME_DIR;return!!(r&&R(u.join(r,"bus"))||R(`/run/user/${t}/bus`))}function wt(t=process.platform,r={}){return t==="darwin"?Y():t==="win32"?I():r.systemdUserAvailable??ct()?tt():st()}export{wt as getPlatformServiceAdapter,ct as isSystemdUserBusAvailable,ot as removeProfileAutostart,it as upsertProfileAutostart};