grix-connector 4.3.9 → 4.4.0-beta.2

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.
Files changed (48) hide show
  1. package/dist/assets/dsh-bridge/grix-dsh-bridge-4.4.0-beta.2.tgz +0 -0
  2. package/dist/assets/dsh-bridge/manifest.json +6 -6
  3. package/dist/core/upgrade/npm-upgrader.js +2 -2
  4. package/dist/core/upgrade/upgrade-checker.js +1 -1
  5. package/dist/core/upgrade/version-store.js +2 -0
  6. package/dist/grix.js +7 -7
  7. package/dist/launcher.mjs +188 -0
  8. package/dist/service/platform-adapter.js +19 -15
  9. package/dist/service/service-manager.js +3 -3
  10. package/dist/service/windows-wrapper-migration.js +1 -1
  11. package/openclaw-plugin/index.js +1 -1
  12. package/package.json +3 -2
  13. package/scripts/launcher.mjs +188 -0
  14. package/dist/adapter/claude/claude-bridge-server.js +0 -1
  15. package/dist/adapter/claude/claude-tools.js +0 -1
  16. package/dist/adapter/claude/claude-worker-client.js +0 -1
  17. package/dist/adapter/claude/mcp-http-launcher.js +0 -2
  18. package/dist/adapter/claude/result-timeout.js +0 -1
  19. package/dist/adapter/deepseek/deepseek-adapter.js +0 -6
  20. package/dist/adapter/deepseek/index.js +0 -1
  21. package/dist/adapter/qwen/index.js +0 -1
  22. package/dist/adapter/qwen/qwen-adapter.js +0 -4
  23. package/dist/aibot/client.js +0 -1
  24. package/dist/aibot/index.js +0 -1
  25. package/dist/aibot/types.js +0 -0
  26. package/dist/assets/dsh-bridge/grix-dsh-bridge-4.3.9.tgz +0 -0
  27. package/dist/core/access/allowlist-gate.js +0 -1
  28. package/dist/core/access/allowlist-store.js +0 -1
  29. package/dist/core/access/index.js +0 -1
  30. package/dist/core/file-ops/handler.js +0 -1
  31. package/dist/core/file-ops/list-files.js +0 -1
  32. package/dist/core/file-ops/types.js +0 -0
  33. package/dist/log.js +0 -3
  34. package/dist/main.js +0 -31
  35. package/dist/mcp/stream-http/config.js +0 -1
  36. package/dist/mcp/stream-http/connection-binding.js +0 -1
  37. package/dist/mcp/stream-http/event-tool-executor.js +0 -1
  38. package/dist/mcp/stream-http/gateway.js +0 -1
  39. package/dist/mcp/stream-http/index.js +0 -1
  40. package/dist/mcp/stream-http/security.js +0 -1
  41. package/dist/mcp/stream-http/session-manager.js +0 -1
  42. package/dist/mcp/stream-http/tool-executor.js +0 -1
  43. package/dist/mcp/stream-http/tool-registry.js +0 -1
  44. package/dist/mcp/stream-http/tool-schemas.js +0 -1
  45. package/dist/session/index.js +0 -1
  46. package/dist/session/manager.js +0 -1
  47. package/dist/transport/index.js +0 -1
  48. package/dist/transport/json-rpc.js +0 -3
@@ -1,4 +1,4 @@
1
- import{existsSync as T,readFileSync as M}from"node:fs";import{mkdir as k,readdir as F,readFile as S,rm as y,stat as q,writeFile as $}from"node:fs/promises";import d from"node:os";import c from"node:path";import{GRIX_HOME_ENV as C}from"../core/config/paths.js";import{isProcessRunning as I,runCommand as f,spawnDetached as _,isWindowsElevated as K,killProcessesByCommandLine as U}from"./process-control.js";import{getServicePrefix as D,parseConfigDirFromPlistXML as Z,parseConfigDirFromSystemdUnit as Y,resolveBareDaemonMarkerPath as x,resolveLinuxUserUnitPath as tt,resolveMacOSLaunchAgentPath as et}from"./service-paths.js";function g(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function A(t){return`'${String(t??"").replace(/'/g,"'\\''")}'`}function G(t){return String(t??"").replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r")}function R(t){return`"${G(t).replace(/%/g,"%%")}"`}function nt(t){return`"${G(t).replace(/\$/g,()=>"$$").replace(/%/g,"%%")}"`}function H(t){return[t.nodePath,t.cliPath,...t.configDir?["--config-dir",t.configDir]:[]]}function rt(t){const e=H(t);return`<?xml version="1.0" encoding="UTF-8"?>
1
+ import{existsSync as b,readFileSync as U}from"node:fs";import{mkdir as k,readdir as F,readFile as S,rm as y,stat as Y,writeFile as $}from"node:fs/promises";import d from"node:os";import c from"node:path";import{GRIX_HOME_ENV as C}from"../core/config/paths.js";import{isProcessRunning as I,runCommand as f,spawnDetached as _,isWindowsElevated as tt,killProcessesByCommandLine as G}from"./process-control.js";import{getServicePrefix as D,parseConfigDirFromPlistXML as et,parseConfigDirFromSystemdUnit as nt,resolveBareDaemonMarkerPath as x,resolveLinuxUserUnitPath as rt,resolveMacOSLaunchAgentPath as it}from"./service-paths.js";function g(t){return String(t??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function R(t){return`'${String(t??"").replace(/'/g,"'\\''")}'`}function H(t){return String(t??"").replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r")}function A(t){return`"${H(t).replace(/%/g,"%%")}"`}function at(t){return`"${H(t).replace(/\$/g,()=>"$$").replace(/%/g,"%%")}"`}function j(t=process.env){const e=[];for(const n of["GRIX_HEALTH_PORT","GRIX_ADMIN_PORT"]){const r=String(t[n]??"").trim();/^\d{2,5}$/.test(r)&&e.push([n,r])}return e}function V(t){return[t.nodePath,t.cliPath,...t.configDir?["--config-dir",t.configDir]:[]]}function ot(t){const e=V(t);return`<?xml version="1.0" encoding="UTF-8"?>
2
2
  <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
3
  <plist version="1.0">
4
4
  <dict>
@@ -22,7 +22,9 @@ ${e.map(n=>` <string>${g(n)}</string>`).join(`
22
22
  <dict>
23
23
  <key>${C}</key>
24
24
  <string>${g(t.rootDir)}</string>
25
- ${t.environmentPath?` <key>PATH</key>
25
+ ${j().map(([n,r])=>` <key>${n}</key>
26
+ <string>${r}</string>
27
+ `).join("")}${t.environmentPath?` <key>PATH</key>
26
28
  <string>${g(t.environmentPath)}</string>
27
29
  `:""} </dict>
28
30
  <key>StandardOutPath</key>
@@ -31,24 +33,26 @@ ${t.environmentPath?` <key>PATH</key>
31
33
  <string>${g(t.stderrPath)}</string>
32
34
  </dict>
33
35
  </plist>
34
- `}function it(t){const e=H(t).map(n=>nt(n)).join(" ");return`[Unit]
36
+ `}function st(t){const e=V(t).map(n=>at(n)).join(" ");return`[Unit]
35
37
  Description=grix-connector daemon (${t.serviceID})
36
38
  After=network.target
37
39
 
38
40
  [Service]
39
41
  Type=simple
40
42
  ExecStart=${e}
41
- WorkingDirectory=${R(c.dirname(t.cliPath))}
42
- Environment=${R(`${C}=${t.rootDir}`)}
43
+ WorkingDirectory=${A(c.dirname(t.cliPath))}
44
+ Environment=${A(`${C}=${t.rootDir}`)}
45
+ ${j().map(([n,r])=>`Environment=${n}=${r}
46
+ `).join("")}
43
47
  Restart=always
44
48
  RestartSec=30
45
- StandardOutput=${R(`append:${t.stdoutPath}`)}
46
- StandardError=${R(`append:${t.stderrPath}`)}
49
+ StandardOutput=${A(`append:${t.stdoutPath}`)}
50
+ StandardError=${A(`append:${t.stderrPath}`)}
47
51
 
48
52
  [Install]
49
53
  WantedBy=default.target
50
- `}function P(t,e){const n=t.replace(/[^a-zA-Z0-9._-]/g,"_");return c.join(e,".grix",`${n}-wrapper.vbs`)}function j(t,e){const n=t.replace(/[^a-zA-Z0-9._-]/g,"_"),r=c.join(e,"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup");return c.join(r,`${n}.lnk`)}function h(t){return String(t??"").replace(/"/g,'""')}function N(t){return`'${String(t??"").replace(/'/g,"''")}'`}function at(t,e){const n=c.join(c.dirname(t),c.basename(e).replace(/\.lnk$/,"-create.vbs")),r=["Option Explicit","Dim shell, shortcut",'Set shell = CreateObject("WScript.Shell")',`Set shortcut = shell.CreateShortcut("${h(e)}")`,'shortcut.TargetPath = "wscript.exe"',`shortcut.Arguments = "//B //NoLogo \\"${h(t)}\\""`,"shortcut.WindowStyle = 7","shortcut.Save",""].join(`\r
51
- `);return{path:n,content:r}}async function ot(t){const{wrapperPath:e,shortcutPath:n,runCommand:r}=t,i=at(e,n);for(let a=1;a<=3;a+=1){try{await $(i.path,i.content,"utf8"),await r("wscript.exe",[i.path,"//B","//NoLogo"])}catch{}finally{await y(i.path,{force:!0}).catch(()=>{})}if(T(n))return!0}return console.warn(`Warning: failed to create Windows logon-autostart shortcut at ${n}. The daemon will NOT start automatically after the next reboot/logon. Check Controlled Folder Access / antivirus blocking writes to the Startup folder, or create the shortcut manually.`),!1}async function st(t){try{const e=await t("whoami",["/user","/fo","csv","/nh"],{allowFailure:!0}),n=String(e?.stdout??"").match(/"(S-1-[0-9-]+)"/);if(n)return n[1]}catch{}return null}function ct(t){const e=`//B //NoLogo "${t.wrapperPath}"`;return`<?xml version="1.0" encoding="UTF-16"?>
54
+ `}function P(t,e){const n=t.replace(/[^a-zA-Z0-9._-]/g,"_");return c.join(e,".grix",`${n}-wrapper.vbs`)}function M(t,e){const n=t.replace(/[^a-zA-Z0-9._-]/g,"_"),r=c.join(e,"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup");return c.join(r,`${n}.lnk`)}function p(t){return String(t??"").replace(/"/g,'""')}function N(t){return`'${String(t??"").replace(/'/g,"''")}'`}function ct(t,e){const n=c.join(c.dirname(t),c.basename(e).replace(/\.lnk$/,"-create.vbs")),r=["Option Explicit","Dim shell, shortcut",'Set shell = CreateObject("WScript.Shell")',`Set shortcut = shell.CreateShortcut("${p(e)}")`,'shortcut.TargetPath = "wscript.exe"',`shortcut.Arguments = "//B //NoLogo \\"${p(t)}\\""`,"shortcut.WindowStyle = 7","shortcut.Save",""].join(`\r
55
+ `);return{path:n,content:r}}async function lt(t){const{wrapperPath:e,shortcutPath:n,runCommand:r}=t,i=ct(e,n);for(let a=1;a<=3;a+=1){try{await $(i.path,i.content,"utf8"),await r("wscript.exe",[i.path,"//B","//NoLogo"])}catch{}finally{await y(i.path,{force:!0}).catch(()=>{})}if(b(n))return!0}return console.warn(`Warning: failed to create Windows logon-autostart shortcut at ${n}. The daemon will NOT start automatically after the next reboot/logon. Check Controlled Folder Access / antivirus blocking writes to the Startup folder, or create the shortcut manually.`),!1}async function ut(t){try{const e=await t("whoami",["/user","/fo","csv","/nh"],{allowFailure:!0}),n=String(e?.stdout??"").match(/"(S-1-[0-9-]+)"/);if(n)return n[1]}catch{}return null}function dt(t){const e=`//B //NoLogo "${t.wrapperPath}"`;return`<?xml version="1.0" encoding="UTF-16"?>
52
56
  <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
53
57
  <RegistrationInfo>
54
58
  <Description>grix-connector daemon autostart</Description>
@@ -91,13 +95,13 @@ WantedBy=default.target
91
95
  </Exec>
92
96
  </Actions>
93
97
  </Task>
94
- `}async function lt(t){const{serviceID:e,wrapperPath:n,runCommand:r}=t,i=await st(r)??d.userInfo().username,a=c.join(c.dirname(n),`${e}-task.xml`);try{const o=ct({userId:i,wrapperPath:n});return await $(a,"\uFEFF"+o,"utf16le"),await r("schtasks",["/Create","/TN",e,"/XML",a,"/F"]),!0}catch{return!1}finally{await y(a,{force:!0}).catch(()=>{})}}const ut=3,dt="grix-wrapper-template:";function mt(t){const e=c.join(t.rootDir,"data","upgrade-wrapper-child.json"),n=[`"${t.cliPath}"`,...t.configDir?["--config-dir",`"${t.configDir}"`]:[]],r=["$ErrorActionPreference = 'Stop'",`$recordPath = ${N(e)}`,"$exitCode = 1","try {"," [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($recordPath)) | Out-Null",` $p = Start-Process -FilePath ${N(t.nodePath)} -ArgumentList @(${n.map(N).join(", ")}) -WindowStyle Hidden -PassThru`,` $record = @{ pid = $p.Id; launched_at = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(); boot_time = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - [Environment]::TickCount64; cli_path = ${N(t.cliPath)} }`," $utf8NoBom = New-Object System.Text.UTF8Encoding($false)"," try {"," [IO.File]::WriteAllText($recordPath, ($record | ConvertTo-Json -Compress), $utf8NoBom)"," } catch {",' & "$env:SystemRoot\\System32\\taskkill.exe" /PID $p.Id /T /F 2>$null | Out-Null'," $p.WaitForExit()"," throw"," }"," $p.WaitForExit()"," $exitCode = $p.ExitCode","} finally {"," Remove-Item -LiteralPath $recordPath -Force -ErrorAction SilentlyContinue","}","exit $exitCode"].join(`
95
- `),i=Buffer.from(r,"utf16le").toString("base64"),l=`"${c.join(process.env.SystemRoot||"C:\\Windows","System32","WindowsPowerShell","v1.0","powershell.exe")}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${i}`,o=c.join(t.rootDir,"bin","upgrade-guardian.mjs"),s=c.join(t.rootDir,"bin","upgrade-guardian.fallback.mjs"),u=c.join(t.rootDir,"data","upgrade-wrapper.pause.json"),m=c.join(t.rootDir,"data","upgrade-pending.json"),p=[`"${h(t.nodePath)}"`,`"${h(o)}"`,"activate","--root",`"${h(t.rootDir)}"`].join(" "),w=`${p} --wrapper-handoff`,L=[`"${h(t.nodePath)}"`,`"${h(s)}"`,"activate","--root",`"${h(t.rootDir)}"`].join(" "),b=`${L} --wrapper-handoff`;return[`' ${dt} ${ut}`,"Option Explicit","Dim shell, processEnv, fso, command, guardianCommand, handoffGuardianCommand, guardianFallbackCommand, handoffGuardianFallbackCommand, guardianPath, guardianFallbackPath, maintenanceFile, pendingFile, delayMs, rapidCount, guardianExit",`command = "${h(l)}"`,`guardianCommand = "${h(p)}"`,`handoffGuardianCommand = "${h(w)}"`,`guardianFallbackCommand = "${h(L)}"`,`handoffGuardianFallbackCommand = "${h(b)}"`,`guardianPath = "${h(o)}"`,`guardianFallbackPath = "${h(s)}"`,`maintenanceFile = "${h(u)}"`,`pendingFile = "${h(m)}"`,'Set shell = CreateObject("WScript.Shell")','Set processEnv = shell.Environment("Process")','Set fso = CreateObject("Scripting.FileSystemObject")',`processEnv("${C}") = "${h(t.rootDir)}"`,'processEnv("GRIX_WRAPPER_SUPERVISED") = "1"',"delayMs = 5000","rapidCount = 0","Do"," Do While fso.FileExists(maintenanceFile)"," If fso.FileExists(guardianPath) Then"," guardianExit = shell.Run(guardianCommand, 0, True)"," ElseIf fso.FileExists(guardianFallbackPath) Then"," guardianExit = shell.Run(guardianFallbackCommand, 0, True)"," Else"," On Error Resume Next"," fso.DeleteFile maintenanceFile, True"," On Error GoTo 0"," WScript.Sleep 1000"," End If"," If fso.FileExists(maintenanceFile) Then WScript.Sleep 3000"," Loop"," If fso.FileExists(pendingFile) Then"," If fso.FileExists(guardianPath) Then"," guardianExit = shell.Run(guardianCommand, 0, True)"," ElseIf fso.FileExists(guardianFallbackPath) Then"," guardianExit = shell.Run(guardianFallbackCommand, 0, True)"," End If"," End If"," shell.Run command, 0, True"," If fso.FileExists(pendingFile) Then"," If fso.FileExists(guardianPath) Then"," guardianExit = shell.Run(handoffGuardianCommand, 0, True)"," ElseIf fso.FileExists(guardianFallbackPath) Then"," guardianExit = shell.Run(handoffGuardianFallbackCommand, 0, True)"," End If"," End If"," If fso.FileExists(pendingFile) And Not PendingIsTerminal() Then"," rapidCount = 0"," WScript.Sleep delayMs"," ElseIf fso.FileExists(maintenanceFile) Then"," rapidCount = 0"," Else"," rapidCount = rapidCount + 1"," If rapidCount >= 10 Then"," WScript.Sleep 300000"," rapidCount = 0"," Else"," WScript.Sleep delayMs"," End If"," End If","Loop","","Function PendingIsTerminal()"," Dim stream, text"," PendingIsTerminal = False"," On Error Resume Next"," Set stream = fso.OpenTextFile(pendingFile, 1)"," If Err.Number <> 0 Then"," Err.Clear"," On Error GoTo 0"," Exit Function"," End If"," text = stream.ReadAll"," stream.Close"," If Err.Number <> 0 Then"," Err.Clear"," On Error GoTo 0"," Exit Function"," End If"," On Error GoTo 0",' text = Replace(Replace(Replace(text, " ", ""), vbCr, ""), vbLf, "")',' If InStr(text, """phase"":""failed""") > 0 Then PendingIsTerminal = True',' If InStr(text, """phase"":""finalizing""") > 0 Then PendingIsTerminal = True',"End Function",""].join(`\r
96
- `)}function v(t){return`gui/${t??0}`}function B(t){const e=String(t?.stdout??"").trim();return String(t?.stderr??"").trim()||e||`exit=${Number(t?.exitCode??-1)}`}function W(t,e,n,r){if(t?.trim())return c.resolve(t);const i=e||n;return i?c.resolve(c.dirname(c.dirname(i))):c.join(r,".grix")}async function V(t){try{await q(t)}catch{throw new Error(`launchd plist missing: ${t}`)}}async function Q(t){const{serviceID:e,domain:n,runCommand:r}=t,i=`${n}/${e}`,a=await r("launchctl",["bootout",i],{allowFailure:!0});if(Number(a?.exitCode??-1)!==0){const l=await r("launchctl",["print",i],{allowFailure:!0});if(Number(l?.exitCode??1)===0){await new Promise(s=>setTimeout(s,250));const o=await r("launchctl",["print",i],{allowFailure:!0});if(Number(o?.exitCode??1)===0)throw new Error(`launchctl bootout ${i}: ${B(a)}`)}return}for(let l=0;l<20;l+=1){const o=await r("launchctl",["print",i],{allowFailure:!0});if(Number(o?.exitCode??1)!==0)return;await new Promise(s=>setTimeout(s,250))}throw new Error(`launchctl bootout ${i}: service remains loaded`)}async function X(t){const{serviceID:e,domain:n,definitionPath:r,runCommand:i}=t,a=`${n}/${e}`,l=await i("launchctl",["bootstrap",n,r],{allowFailure:!0});if(Number(l?.exitCode??-1)===0)return;const o=await i("launchctl",["print",a],{allowFailure:!0});Number(o?.exitCode??1)===0&&await Q(t);const s=await i("launchctl",["bootstrap",n,r],{allowFailure:!0});if(Number(s?.exitCode??-1)!==0)throw new Error(`launchctl bootstrap ${n} failed: initial=${B(l)}; retry=${B(s)}`)}function ft(){return{platform:"darwin",kind:"launchd",async install({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:i,stdoutPath:a,stderrPath:l,environmentPath:o="",homeDir:s=d.homedir()}){const u=et(t,s),m=W(i,a,l,s);return await k(c.dirname(u),{recursive:!0}),await $(u,rt({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:m,stdoutPath:a,stderrPath:l,environmentPath:o}),{encoding:"utf8",mode:384}),{definitionPath:u}},async start({serviceID:t,definitionPath:e,runCommand:n=f,uid:r=process.getuid?.()??0}){const i=v(r);await V(e),await X({serviceID:t,domain:i,definitionPath:e,runCommand:n})},async stop({serviceID:t,runCommand:e=f,uid:n=process.getuid?.()??0}){const r=v(n);await e("launchctl",["bootout",`${r}/${t}`],{allowFailure:!0})},async restart({serviceID:t,definitionPath:e,runCommand:n=f,uid:r=process.getuid?.()??0}){const i=v(r),a={serviceID:t,domain:i,definitionPath:e,runCommand:n};await V(e),await Q(a),await X(a)},async uninstall({serviceID:t,definitionPath:e,runCommand:n=f,uid:r=process.getuid?.()??0}){const i=v(r);await n("launchctl",["bootout",`${i}/${t}`],{allowFailure:!0}),await y(e,{force:!0})},async discoverServices({homeDir:t=d.homedir()}={}){const e=c.join(t,"Library","LaunchAgents"),n=await F(e).catch(()=>[]),r=D("darwin"),i=[];for(const a of n){if(!a.startsWith(r)||!a.endsWith(".plist"))continue;const l=a.slice(0,-6),o=c.join(e,a);let s=null;try{const u=await S(o,"utf8");s=Z(u)}catch{}i.push({serviceID:l,definitionPath:o,configDir:s})}return i},async isServiceLoaded({serviceID:t,runCommand:e=f,uid:n=process.getuid?.()??0}){const r=v(n),i=await e("launchctl",["print",`${r}/${t}`],{allowFailure:!0});return Number(i?.exitCode??1)===0}}}function ht(){return{platform:"win32",kind:"task-scheduler",async install({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:i,stdoutPath:a,stderrPath:l,runCommand:o=f,homeDir:s=d.homedir()}){const u=P(t,s),m=W(i,a,l,s);await k(c.dirname(u),{recursive:!0}),await $(u,mt({nodePath:e,cliPath:n,configDir:r,rootDir:m}),"utf8");let p=!1;K()&&(p=await lt({serviceID:t,wrapperPath:u,runCommand:o}));const w=j(t,s);return p?await y(w,{force:!0}).catch(()=>{}):await ot({wrapperPath:u,shortcutPath:w,runCommand:o}),{definitionPath:p?`task:${t}`:`startup:${t}`}},async start({serviceID:t,definitionPath:e,runCommand:n=f,homeDir:r=d.homedir()}){const i=P(t,r);if(e.startsWith("task:"))try{await n("schtasks",["/Run","/TN",t]);return}catch{}_("wscript.exe",["//B","//NoLogo",i])},async stop({serviceID:t,runCommand:e=f,homeDir:n=d.homedir()}){await e("schtasks",["/End","/TN",t],{allowFailure:!0});const r=`${t}-wrapper.vbs`;await U(r,{platform:"win32"})},async restart({serviceID:t,definitionPath:e,runCommand:n=f,homeDir:r=d.homedir()}){await n("schtasks",["/End","/TN",t],{allowFailure:!0});const i=`${t}-wrapper.vbs`;await U(i,{platform:"win32"});const a=P(t,r);if(e?.startsWith("task:"))try{await n("schtasks",["/Run","/TN",t]);return}catch{}_("wscript.exe",["//B","//NoLogo",a])},async uninstall({serviceID:t,runCommand:e=f,homeDir:n=d.homedir()}){await e("schtasks",["/Delete","/TN",t,"/F"],{allowFailure:!0});const r=P(t,n);await y(r,{force:!0});const i=j(t,n);await y(i,{force:!0})},async discoverServices({homeDir:t=d.homedir(),runCommand:e=f}={}){const n=D("win32"),r=await e("schtasks",["/Query","/FO","CSV","/NH"],{allowFailure:!0}),i=[];if(Number(r?.exitCode??-1)===0){const o=String(r.stdout??"").split(/\r?\n/);for(const s of o){const u=s.match(/^"([^"]+)"/);if(!u)continue;const m=u[1];if(!m.startsWith(n))continue;let p=null;try{const w=P(m,t),b=(await S(w,"utf8")).match(/--config-dir\s+""([^""]+)""/);b&&(p=b[1])}catch{}i.push({serviceID:m,definitionPath:`task:${m}`,configDir:p})}}const a=c.join(t,"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup"),l=await F(a).catch(()=>[]);for(const o of l){if(!o.startsWith(n)||!o.endsWith(".lnk"))continue;const s=o.slice(0,-4);if(i.some(m=>m.serviceID===s))continue;let u=null;try{const m=P(s,t),w=(await S(m,"utf8")).match(/--config-dir\s+""([^""]+)""/);w&&(u=w[1])}catch{}i.push({serviceID:s,definitionPath:`startup:${s}`,configDir:u})}return i},async isServiceLoaded({serviceID:t,runCommand:e=f,homeDir:n=d.homedir()}){const r=await e("schtasks",["/Query","/TN",t,"/NH"],{allowFailure:!0});return Number(r?.exitCode??1)===0?!0:T(j(t,n))}}}function pt(){return{platform:"linux",kind:"systemd-user",async install({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:i,stdoutPath:a,stderrPath:l,homeDir:o=d.homedir(),runCommand:s=f}){const u=tt(t,o),m=W(i,a,l,o);return await k(c.dirname(u),{recursive:!0}),await $(u,it({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:m,stdoutPath:a,stderrPath:l}),{encoding:"utf8",mode:384}),await s("systemctl",["--user","daemon-reload"]),await s("systemctl",["--user","enable",`${t}.service`]),{definitionPath:u}},async start({serviceID:t,runCommand:e=f}){await e("systemctl",["--user","start",`${t}.service`])},async stop({serviceID:t,runCommand:e=f}){await e("systemctl",["--user","stop",`${t}.service`],{allowFailure:!0})},async restart({serviceID:t,runCommand:e=f}){await e("systemctl",["--user","restart",`${t}.service`])},async uninstall({serviceID:t,definitionPath:e,runCommand:n=f}){await n("systemctl",["--user","stop",`${t}.service`],{allowFailure:!0}),await n("systemctl",["--user","disable",`${t}.service`],{allowFailure:!0}),await y(e,{force:!0}),await n("systemctl",["--user","daemon-reload"])},async discoverServices({homeDir:t=d.homedir()}={}){const e=c.join(t,".config","systemd","user"),n=await F(e).catch(()=>[]),r=D("linux"),i=[];for(const a of n){if(!a.startsWith(r)||!a.endsWith(".service"))continue;const l=a.slice(0,-8),o=c.join(e,a);let s=null;try{const u=await S(o,"utf8");s=Y(u)}catch{}i.push({serviceID:l,definitionPath:o,configDir:s})}return i},async isServiceLoaded({serviceID:t,runCommand:e=f}){const n=await e("systemctl",["--user","is-active",`${t}.service`],{allowFailure:!0});return String(n?.stdout??"").trim()==="active"}}}function E(t){try{const e=M(t,"utf8"),n=JSON.parse(e);if(n&&typeof n.service_id=="string"&&typeof n.node_path=="string"&&typeof n.cli_path=="string"&&typeof n.root_dir=="string")return n}catch{}return null}function gt(t){return c.join(t,"daemon.lock.json")}function O(t){const e=gt(t);try{const n=M(e,"utf8"),r=JSON.parse(n);if(typeof r?.pid=="number")return r.pid}catch{}return 0}function z(t){return c.join(t,".profile")}function wt(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function yt(t){const e=[A(t.nodePath),A(t.cliPath)];t.configDir&&e.push("--config-dir",A(t.configDir));const n=`env ${C}=${A(t.rootDir)} ${e.join(" ")}`;return`${n} status >/dev/null 2>&1 || (nohup ${n} start >/dev/null 2>&1 &)`}function $t(t,e){return`# >>> grix-connector autostart [${t}] >>>
98
+ `}async function mt(t){const{serviceID:e,wrapperPath:n,runCommand:r}=t,i=await ut(r)??d.userInfo().username,a=c.join(c.dirname(n),`${e}-task.xml`);try{const o=dt({userId:i,wrapperPath:n});return await $(a,"\uFEFF"+o,"utf16le"),await r("schtasks",["/Create","/TN",e,"/XML",a,"/F"]),!0}catch{return!1}finally{await y(a,{force:!0}).catch(()=>{})}}const ft=3,pt="grix-wrapper-template:";function ht(t){const e=c.join(t.rootDir,"data","upgrade-wrapper-child.json"),n=[`"${t.cliPath}"`,...t.configDir?["--config-dir",`"${t.configDir}"`]:[]],r=["$ErrorActionPreference = 'Stop'",`$recordPath = ${N(e)}`,"$exitCode = 1","try {"," [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($recordPath)) | Out-Null",` $p = Start-Process -FilePath ${N(t.nodePath)} -ArgumentList @(${n.map(N).join(", ")}) -WindowStyle Hidden -PassThru`,` $record = @{ pid = $p.Id; launched_at = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(); boot_time = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - [Environment]::TickCount64; cli_path = ${N(t.cliPath)} }`," $utf8NoBom = New-Object System.Text.UTF8Encoding($false)"," try {"," [IO.File]::WriteAllText($recordPath, ($record | ConvertTo-Json -Compress), $utf8NoBom)"," } catch {",' & "$env:SystemRoot\\System32\\taskkill.exe" /PID $p.Id /T /F 2>$null | Out-Null'," $p.WaitForExit()"," throw"," }"," $p.WaitForExit()"," $exitCode = $p.ExitCode","} finally {"," Remove-Item -LiteralPath $recordPath -Force -ErrorAction SilentlyContinue","}","exit $exitCode"].join(`
99
+ `),i=Buffer.from(r,"utf16le").toString("base64"),l=`"${c.join(process.env.SystemRoot||"C:\\Windows","System32","WindowsPowerShell","v1.0","powershell.exe")}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${i}`,o=c.join(t.rootDir,"bin","upgrade-guardian.mjs"),s=c.join(t.rootDir,"bin","upgrade-guardian.fallback.mjs"),u=c.join(t.rootDir,"data","upgrade-wrapper.pause.json"),m=c.join(t.rootDir,"data","upgrade-pending.json"),h=[`"${p(t.nodePath)}"`,`"${p(o)}"`,"activate","--root",`"${p(t.rootDir)}"`].join(" "),w=`${h} --wrapper-handoff`,L=[`"${p(t.nodePath)}"`,`"${p(s)}"`,"activate","--root",`"${p(t.rootDir)}"`].join(" "),T=`${L} --wrapper-handoff`;return[`' ${pt} ${ft}`,"Option Explicit","Dim shell, processEnv, fso, command, guardianCommand, handoffGuardianCommand, guardianFallbackCommand, handoffGuardianFallbackCommand, guardianPath, guardianFallbackPath, maintenanceFile, pendingFile, delayMs, rapidCount, guardianExit",`command = "${p(l)}"`,`guardianCommand = "${p(h)}"`,`handoffGuardianCommand = "${p(w)}"`,`guardianFallbackCommand = "${p(L)}"`,`handoffGuardianFallbackCommand = "${p(T)}"`,`guardianPath = "${p(o)}"`,`guardianFallbackPath = "${p(s)}"`,`maintenanceFile = "${p(u)}"`,`pendingFile = "${p(m)}"`,'Set shell = CreateObject("WScript.Shell")','Set processEnv = shell.Environment("Process")','Set fso = CreateObject("Scripting.FileSystemObject")',`processEnv("${C}") = "${p(t.rootDir)}"`,'processEnv("GRIX_WRAPPER_SUPERVISED") = "1"',...j().map(([K,Z])=>`processEnv("${K}") = "${Z}"`),"delayMs = 5000","rapidCount = 0","Do"," Do While fso.FileExists(maintenanceFile)"," If fso.FileExists(guardianPath) Then"," guardianExit = shell.Run(guardianCommand, 0, True)"," ElseIf fso.FileExists(guardianFallbackPath) Then"," guardianExit = shell.Run(guardianFallbackCommand, 0, True)"," Else"," On Error Resume Next"," fso.DeleteFile maintenanceFile, True"," On Error GoTo 0"," WScript.Sleep 1000"," End If"," If fso.FileExists(maintenanceFile) Then WScript.Sleep 3000"," Loop"," If fso.FileExists(pendingFile) Then"," If fso.FileExists(guardianPath) Then"," guardianExit = shell.Run(guardianCommand, 0, True)"," ElseIf fso.FileExists(guardianFallbackPath) Then"," guardianExit = shell.Run(guardianFallbackCommand, 0, True)"," End If"," End If"," shell.Run command, 0, True"," If fso.FileExists(pendingFile) Then"," If fso.FileExists(guardianPath) Then"," guardianExit = shell.Run(handoffGuardianCommand, 0, True)"," ElseIf fso.FileExists(guardianFallbackPath) Then"," guardianExit = shell.Run(handoffGuardianFallbackCommand, 0, True)"," End If"," End If"," If fso.FileExists(pendingFile) And Not PendingIsTerminal() Then"," rapidCount = 0"," WScript.Sleep delayMs"," ElseIf fso.FileExists(maintenanceFile) Then"," rapidCount = 0"," Else"," rapidCount = rapidCount + 1"," If rapidCount >= 10 Then"," WScript.Sleep 300000"," rapidCount = 0"," Else"," WScript.Sleep delayMs"," End If"," End If","Loop","","Function PendingIsTerminal()"," Dim stream, text"," PendingIsTerminal = False"," On Error Resume Next"," Set stream = fso.OpenTextFile(pendingFile, 1)"," If Err.Number <> 0 Then"," Err.Clear"," On Error GoTo 0"," Exit Function"," End If"," text = stream.ReadAll"," stream.Close"," If Err.Number <> 0 Then"," Err.Clear"," On Error GoTo 0"," Exit Function"," End If"," On Error GoTo 0",' text = Replace(Replace(Replace(text, " ", ""), vbCr, ""), vbLf, "")',' If InStr(text, """phase"":""failed""") > 0 Then PendingIsTerminal = True',' If InStr(text, """phase"":""finalizing""") > 0 Then PendingIsTerminal = True',"End Function",""].join(`\r
100
+ `)}function v(t){return`gui/${t??0}`}function B(t){const e=String(t?.stdout??"").trim();return String(t?.stderr??"").trim()||e||`exit=${Number(t?.exitCode??-1)}`}function W(t,e,n,r){if(t?.trim())return c.resolve(t);const i=e||n;return i?c.resolve(c.dirname(c.dirname(i))):c.join(r,".grix")}async function X(t){try{await Y(t)}catch{throw new Error(`launchd plist missing: ${t}`)}}async function Q(t){const{serviceID:e,domain:n,runCommand:r}=t,i=`${n}/${e}`,a=await r("launchctl",["bootout",i],{allowFailure:!0});if(Number(a?.exitCode??-1)!==0){const l=await r("launchctl",["print",i],{allowFailure:!0});if(Number(l?.exitCode??1)===0){await new Promise(s=>setTimeout(s,250));const o=await r("launchctl",["print",i],{allowFailure:!0});if(Number(o?.exitCode??1)===0)throw new Error(`launchctl bootout ${i}: ${B(a)}`)}return}for(let l=0;l<20;l+=1){const o=await r("launchctl",["print",i],{allowFailure:!0});if(Number(o?.exitCode??1)!==0)return;await new Promise(s=>setTimeout(s,250))}throw new Error(`launchctl bootout ${i}: service remains loaded`)}async function z(t){const{serviceID:e,domain:n,definitionPath:r,runCommand:i}=t,a=`${n}/${e}`,l=await i("launchctl",["bootstrap",n,r],{allowFailure:!0});if(Number(l?.exitCode??-1)===0)return;const o=await i("launchctl",["print",a],{allowFailure:!0});Number(o?.exitCode??1)===0&&await Q(t);const s=await i("launchctl",["bootstrap",n,r],{allowFailure:!0});if(Number(s?.exitCode??-1)!==0)throw new Error(`launchctl bootstrap ${n} failed: initial=${B(l)}; retry=${B(s)}`)}function gt(){return{platform:"darwin",kind:"launchd",async install({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:i,stdoutPath:a,stderrPath:l,environmentPath:o="",homeDir:s=d.homedir()}){const u=it(t,s),m=W(i,a,l,s);return await k(c.dirname(u),{recursive:!0}),await $(u,ot({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:m,stdoutPath:a,stderrPath:l,environmentPath:o}),{encoding:"utf8",mode:384}),{definitionPath:u}},async start({serviceID:t,definitionPath:e,runCommand:n=f,uid:r=process.getuid?.()??0}){const i=v(r);await X(e),await z({serviceID:t,domain:i,definitionPath:e,runCommand:n})},async stop({serviceID:t,runCommand:e=f,uid:n=process.getuid?.()??0}){const r=v(n);await e("launchctl",["bootout",`${r}/${t}`],{allowFailure:!0})},async restart({serviceID:t,definitionPath:e,runCommand:n=f,uid:r=process.getuid?.()??0}){const i=v(r),a={serviceID:t,domain:i,definitionPath:e,runCommand:n};await X(e),await Q(a),await z(a)},async uninstall({serviceID:t,definitionPath:e,runCommand:n=f,uid:r=process.getuid?.()??0}){const i=v(r);await n("launchctl",["bootout",`${i}/${t}`],{allowFailure:!0}),await y(e,{force:!0})},async discoverServices({homeDir:t=d.homedir()}={}){const e=c.join(t,"Library","LaunchAgents"),n=await F(e).catch(()=>[]),r=D("darwin"),i=[];for(const a of n){if(!a.startsWith(r)||!a.endsWith(".plist"))continue;const l=a.slice(0,-6),o=c.join(e,a);let s=null;try{const u=await S(o,"utf8");s=et(u)}catch{}i.push({serviceID:l,definitionPath:o,configDir:s})}return i},async isServiceLoaded({serviceID:t,runCommand:e=f,uid:n=process.getuid?.()??0}){const r=v(n),i=await e("launchctl",["print",`${r}/${t}`],{allowFailure:!0});return Number(i?.exitCode??1)===0}}}function wt(){return{platform:"win32",kind:"task-scheduler",async install({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:i,stdoutPath:a,stderrPath:l,runCommand:o=f,homeDir:s=d.homedir()}){const u=P(t,s),m=W(i,a,l,s);await k(c.dirname(u),{recursive:!0}),await $(u,ht({nodePath:e,cliPath:n,configDir:r,rootDir:m}),"utf8");let h=!1;tt()&&(h=await mt({serviceID:t,wrapperPath:u,runCommand:o}));const w=M(t,s);return h?await y(w,{force:!0}).catch(()=>{}):await lt({wrapperPath:u,shortcutPath:w,runCommand:o}),{definitionPath:h?`task:${t}`:`startup:${t}`}},async start({serviceID:t,definitionPath:e,runCommand:n=f,homeDir:r=d.homedir()}){const i=P(t,r);if(e.startsWith("task:"))try{await n("schtasks",["/Run","/TN",t]);return}catch{}_("wscript.exe",["//B","//NoLogo",i])},async stop({serviceID:t,runCommand:e=f,homeDir:n=d.homedir()}){await e("schtasks",["/End","/TN",t],{allowFailure:!0});const r=`${t}-wrapper.vbs`;await G(r,{platform:"win32"})},async restart({serviceID:t,definitionPath:e,runCommand:n=f,homeDir:r=d.homedir()}){await n("schtasks",["/End","/TN",t],{allowFailure:!0});const i=`${t}-wrapper.vbs`;await G(i,{platform:"win32"});const a=P(t,r);if(e?.startsWith("task:"))try{await n("schtasks",["/Run","/TN",t]);return}catch{}_("wscript.exe",["//B","//NoLogo",a])},async uninstall({serviceID:t,runCommand:e=f,homeDir:n=d.homedir()}){await e("schtasks",["/Delete","/TN",t,"/F"],{allowFailure:!0});const r=P(t,n);await y(r,{force:!0});const i=M(t,n);await y(i,{force:!0})},async discoverServices({homeDir:t=d.homedir(),runCommand:e=f}={}){const n=D("win32"),r=await e("schtasks",["/Query","/FO","CSV","/NH"],{allowFailure:!0}),i=[];if(Number(r?.exitCode??-1)===0){const o=String(r.stdout??"").split(/\r?\n/);for(const s of o){const u=s.match(/^"([^"]+)"/);if(!u)continue;const m=u[1];if(!m.startsWith(n))continue;let h=null;try{const w=P(m,t),T=(await S(w,"utf8")).match(/--config-dir\s+""([^""]+)""/);T&&(h=T[1])}catch{}i.push({serviceID:m,definitionPath:`task:${m}`,configDir:h})}}const a=c.join(t,"AppData","Roaming","Microsoft","Windows","Start Menu","Programs","Startup"),l=await F(a).catch(()=>[]);for(const o of l){if(!o.startsWith(n)||!o.endsWith(".lnk"))continue;const s=o.slice(0,-4);if(i.some(m=>m.serviceID===s))continue;let u=null;try{const m=P(s,t),w=(await S(m,"utf8")).match(/--config-dir\s+""([^""]+)""/);w&&(u=w[1])}catch{}i.push({serviceID:s,definitionPath:`startup:${s}`,configDir:u})}return i},async isServiceLoaded({serviceID:t,runCommand:e=f,homeDir:n=d.homedir()}){const r=await e("schtasks",["/Query","/TN",t,"/NH"],{allowFailure:!0});return Number(r?.exitCode??1)===0?!0:b(M(t,n))}}}function yt(){return{platform:"linux",kind:"systemd-user",async install({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:i,stdoutPath:a,stderrPath:l,homeDir:o=d.homedir(),runCommand:s=f}){const u=rt(t,o),m=W(i,a,l,o);return await k(c.dirname(u),{recursive:!0}),await $(u,st({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:m,stdoutPath:a,stderrPath:l}),{encoding:"utf8",mode:384}),await s("systemctl",["--user","daemon-reload"]),await s("systemctl",["--user","enable",`${t}.service`]),{definitionPath:u}},async start({serviceID:t,runCommand:e=f}){await e("systemctl",["--user","start",`${t}.service`])},async stop({serviceID:t,runCommand:e=f}){await e("systemctl",["--user","stop",`${t}.service`],{allowFailure:!0})},async restart({serviceID:t,runCommand:e=f}){await e("systemctl",["--user","restart",`${t}.service`])},async uninstall({serviceID:t,definitionPath:e,runCommand:n=f}){await n("systemctl",["--user","stop",`${t}.service`],{allowFailure:!0}),await n("systemctl",["--user","disable",`${t}.service`],{allowFailure:!0}),await y(e,{force:!0}),await n("systemctl",["--user","daemon-reload"])},async discoverServices({homeDir:t=d.homedir()}={}){const e=c.join(t,".config","systemd","user"),n=await F(e).catch(()=>[]),r=D("linux"),i=[];for(const a of n){if(!a.startsWith(r)||!a.endsWith(".service"))continue;const l=a.slice(0,-8),o=c.join(e,a);let s=null;try{const u=await S(o,"utf8");s=nt(u)}catch{}i.push({serviceID:l,definitionPath:o,configDir:s})}return i},async isServiceLoaded({serviceID:t,runCommand:e=f}){const n=await e("systemctl",["--user","is-active",`${t}.service`],{allowFailure:!0});return String(n?.stdout??"").trim()==="active"}}}function E(t){try{const e=U(t,"utf8"),n=JSON.parse(e);if(n&&typeof n.service_id=="string"&&typeof n.node_path=="string"&&typeof n.cli_path=="string"&&typeof n.root_dir=="string")return n}catch{}return null}function $t(t){return c.join(t,"daemon.lock.json")}function O(t){const e=$t(t);try{const n=U(e,"utf8"),r=JSON.parse(n);if(typeof r?.pid=="number")return r.pid}catch{}return 0}function J(t){return c.join(t,".profile")}function St(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function xt(t){const e=[R(t.nodePath),R(t.cliPath)];t.configDir&&e.push("--config-dir",R(t.configDir));const n=`env ${C}=${R(t.rootDir)} ${e.join(" ")}`;return`${n} status >/dev/null 2>&1 || (nohup ${n} start >/dev/null 2>&1 &)`}function Pt(t,e){return`# >>> grix-connector autostart [${t}] >>>
97
101
  ${e}
98
102
  # <<< grix-connector autostart [${t}] <<<
99
- `}function J(t){const e=wt(t);return new RegExp(`(?:^|\\n)# >>> grix-connector autostart \\[${e}\\] >>>\\n[\\s\\S]*?\\n# <<< grix-connector autostart \\[${e}\\] <<<\\n?`,"m")}async function St(t,e,n){let r="";try{r=await S(t,"utf8")}catch{}const i=$t(e,n),a=J(e);let l;if(a.test(r))l=r.replace(a,`
103
+ `}function q(t){const e=St(t);return new RegExp(`(?:^|\\n)# >>> grix-connector autostart \\[${e}\\] >>>\\n[\\s\\S]*?\\n# <<< grix-connector autostart \\[${e}\\] <<<\\n?`,"m")}async function Et(t,e,n){let r="";try{r=await S(t,"utf8")}catch{}const i=Pt(e,n),a=q(e);let l;if(a.test(r))l=r.replace(a,`
100
104
  ${i}`).replace(/^\n+/,"");else{const o=r.length===0||r.endsWith(`
101
105
  `)?"":`
102
- `;l=`${r}${o}${i}`}l!==r&&(await k(c.dirname(t),{recursive:!0}),await $(t,l,{encoding:"utf8"}))}async function xt(t,e){let n="";try{n=await S(t,"utf8")}catch{return}const r=J(e);if(!r.test(n))return;const i=n.replace(r,"").replace(/^\n+/,"");await $(t,i,{encoding:"utf8"})}function Pt(){return{platform:"linux",kind:"bare-daemon",async install({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:i,stdoutPath:a,stderrPath:l,homeDir:o=d.homedir()}){const s=x(t,o);await k(c.dirname(s),{recursive:!0});const u=W(i,a,l,o),m={schema_version:1,service_id:t,node_path:e,cli_path:n,config_dir:r??"",root_dir:u,stdout_path:a,stderr_path:l,installed_at:Date.now()};await $(s,`${JSON.stringify(m,null,2)}
103
- `,{encoding:"utf8",mode:384});const p=yt({nodePath:e,cliPath:n,configDir:r??"",rootDir:u});return await St(z(o),t,p),{definitionPath:s}},async start({serviceID:t,homeDir:e=d.homedir()}){const n=x(t,e),r=E(n);if(!r)throw new Error(`bare-daemon marker missing: ${n}`);const i=[r.cli_path];r.config_dir&&i.push("--config-dir",r.config_dir),_(r.node_path,i,{env:{...process.env,[C]:r.root_dir}})},async stop({serviceID:t,homeDir:e=d.homedir()}){const n=x(t,e),r=E(n);if(!r)return;const i=O(r.root_dir);if(i>0&&I(i))try{process.kill(i,"SIGTERM")}catch{}},async restart(t){await this.stop(t);const e=Date.now()+5e3,n=x(t.serviceID,t.homeDir??d.homedir()),r=E(n);for(;r&&Date.now()<e;){const i=O(r.root_dir);if(i<=0||!I(i))break;await new Promise(a=>setTimeout(a,100))}await this.start(t)},async uninstall({serviceID:t,homeDir:e=d.homedir()}){const n=x(t,e),r=E(n);if(r){const i=O(r.root_dir);if(i>0&&I(i))try{process.kill(i,"SIGTERM")}catch{}}await y(n,{force:!0}),await xt(z(e),t)},async discoverServices({homeDir:t=d.homedir()}={}){const e=c.join(t,".grix","service"),n=await F(e).catch(()=>[]),r=D("linux"),i=[];for(const a of n){if(!a.startsWith(r)||!a.endsWith(".bare.json"))continue;const l=a.slice(0,-10),o=c.join(e,a),s=E(o);i.push({serviceID:l,definitionPath:o,configDir:s?.config_dir||null})}return i},async isServiceLoaded({serviceID:t,homeDir:e=d.homedir()}){const n=x(t,e),r=E(n);if(!r)return!1;const i=O(r.root_dir);return i>0&&I(i)}}}function Et(t=process.getuid?.()??0){if(process.env.GRIX_FORCE_BARE_DAEMON==="1")return!1;const e=process.env.XDG_RUNTIME_DIR;return!!(e&&T(c.join(e,"bus"))||T(`/run/user/${t}/bus`))}function Dt(t=process.platform,e={}){return t==="darwin"?ft():t==="win32"?ht():e.systemdUserAvailable??Et()?pt():Pt()}export{dt as WINDOWS_WRAPPER_TEMPLATE_MARKER,ut as WINDOWS_WRAPPER_TEMPLATE_VERSION,mt as buildWindowsWrapperScript,Dt as getPlatformServiceAdapter,Et as isSystemdUserBusAvailable,xt as removeProfileAutostart,P as resolveWindowsWrapperPath,St as upsertProfileAutostart};
106
+ `;l=`${r}${o}${i}`}l!==r&&(await k(c.dirname(t),{recursive:!0}),await $(t,l,{encoding:"utf8"}))}async function kt(t,e){let n="";try{n=await S(t,"utf8")}catch{return}const r=q(e);if(!r.test(n))return;const i=n.replace(r,"").replace(/^\n+/,"");await $(t,i,{encoding:"utf8"})}function Ct(){return{platform:"linux",kind:"bare-daemon",async install({serviceID:t,nodePath:e,cliPath:n,configDir:r,rootDir:i,stdoutPath:a,stderrPath:l,homeDir:o=d.homedir()}){const s=x(t,o);await k(c.dirname(s),{recursive:!0});const u=W(i,a,l,o),m={schema_version:1,service_id:t,node_path:e,cli_path:n,config_dir:r??"",root_dir:u,stdout_path:a,stderr_path:l,installed_at:Date.now()};await $(s,`${JSON.stringify(m,null,2)}
107
+ `,{encoding:"utf8",mode:384});const h=xt({nodePath:e,cliPath:n,configDir:r??"",rootDir:u});return await Et(J(o),t,h),{definitionPath:s}},async start({serviceID:t,homeDir:e=d.homedir()}){const n=x(t,e),r=E(n);if(!r)throw new Error(`bare-daemon marker missing: ${n}`);const i=[r.cli_path];r.config_dir&&i.push("--config-dir",r.config_dir),_(r.node_path,i,{env:{...process.env,[C]:r.root_dir}})},async stop({serviceID:t,homeDir:e=d.homedir()}){const n=x(t,e),r=E(n);if(!r)return;const i=O(r.root_dir);if(i>0&&I(i))try{process.kill(i,"SIGTERM")}catch{}},async restart(t){await this.stop(t);const e=Date.now()+5e3,n=x(t.serviceID,t.homeDir??d.homedir()),r=E(n);for(;r&&Date.now()<e;){const i=O(r.root_dir);if(i<=0||!I(i))break;await new Promise(a=>setTimeout(a,100))}await this.start(t)},async uninstall({serviceID:t,homeDir:e=d.homedir()}){const n=x(t,e),r=E(n);if(r){const i=O(r.root_dir);if(i>0&&I(i))try{process.kill(i,"SIGTERM")}catch{}}await y(n,{force:!0}),await kt(J(e),t)},async discoverServices({homeDir:t=d.homedir()}={}){const e=c.join(t,".grix","service"),n=await F(e).catch(()=>[]),r=D("linux"),i=[];for(const a of n){if(!a.startsWith(r)||!a.endsWith(".bare.json"))continue;const l=a.slice(0,-10),o=c.join(e,a),s=E(o);i.push({serviceID:l,definitionPath:o,configDir:s?.config_dir||null})}return i},async isServiceLoaded({serviceID:t,homeDir:e=d.homedir()}){const n=x(t,e),r=E(n);if(!r)return!1;const i=O(r.root_dir);return i>0&&I(i)}}}function vt(t=process.getuid?.()??0){if(process.env.GRIX_FORCE_BARE_DAEMON==="1")return!1;const e=process.env.XDG_RUNTIME_DIR;return!!(e&&b(c.join(e,"bus"))||b(`/run/user/${t}/bus`))}function Nt(t=process.platform,e={}){return t==="darwin"?gt():t==="win32"?wt():e.systemdUserAvailable??vt()?yt():Ct()}export{pt as WINDOWS_WRAPPER_TEMPLATE_MARKER,ft as WINDOWS_WRAPPER_TEMPLATE_VERSION,ht as buildWindowsWrapperScript,j as collectServicePortEnv,Nt as getPlatformServiceAdapter,vt as isSystemdUserBusAvailable,kt as removeProfileAutostart,P as resolveWindowsWrapperPath,Et as upsertProfileAutostart};
@@ -1,3 +1,3 @@
1
- import{createHash as j}from"node:crypto";import c from"node:fs";import R from"node:os";import a from"node:path";import D from"node:process";import{mkdir as w}from"node:fs/promises";import{setTimeout as z}from"node:timers/promises";import{resolveRuntimePaths as P}from"../core/config/paths.js";import{inspectDaemonState as S}from"../runtime/service-state.js";import{isProcessRunning as x,runCommand as p,terminateProcessTree as I,waitForProcessExit as A}from"./process-control.js";import{isLockHolderIdentityVerified as F,readDaemonLock as _}from"../runtime/daemon-lock.js";import{ServiceInstallStore as E}from"./service-install-store.js";import{getPlatformServiceAdapter as T}from"./platform-adapter.js";import{captureLogCursor as v,formatLogDelta as y}from"./log-diagnostics.js";import{buildServiceID as L,resolveWindowsServiceRuntimeDir as b,resolveServiceInstallRecordPath as B,resolveServiceStderrPath as N,resolveServiceStdoutPath as H}from"./service-paths.js";class rt{adapter;platform;homeDir;uid;nodePath;cliPath;waitForExit;terminateTree;constructor(i){this.platform=i.platform??D.platform,this.homeDir=i.homeDir??R.homedir(),this.uid=i.uid??D.getuid?.()??0,this.nodePath=i.nodePath??D.execPath,this.cliPath=i.cliPath,this.adapter=T(this.platform),this.waitForExit=i.processControl?.waitForExit??A,this.terminateTree=i.processControl?.terminateTree??I}normalizeRootDir(i){return a.resolve(String(i??"").trim())}resolveLocalServiceBaseDir(){if(this.platform==="darwin")return a.join(this.homeDir,"Library","Application Support","grix-connector");if(this.platform==="win32"){const i=String(D.env.LOCALAPPDATA??"").trim();return a.join(i&&a.isAbsolute(i)?i:a.join(this.homeDir,"AppData","Local"),"grix-connector")}return null}resolveServiceRuntimeDir(i){const t=this.resolveLocalServiceBaseDir();return t?this.platform==="win32"?b(this.normalizeRootDir(i),D.env,this.homeDir):a.join(t,"runtime"):null}resolveServiceCliPath(i){const t=this.resolveServiceRuntimeDir(i);return t?a.join(t,"dist",a.basename(this.cliPath)):this.cliPath}prepareServiceRuntime(i){const t=this.resolveServiceRuntimeDir(i);if(!t)return;const s=a.dirname(this.cliPath),r=a.dirname(s),e=a.join(t,"dist"),o=a.join(t,"node_modules");c.mkdirSync(t,{recursive:!0}),c.rmSync(e,{recursive:!0,force:!0}),c.cpSync(s,e,{recursive:!0,force:!0});const h=a.join(r,"package.json");c.existsSync(h)&&c.copyFileSync(h,a.join(t,"package.json"));const d=a.join(r,"node_modules");if(c.existsSync(d)&&(c.rmSync(o,{recursive:!0,force:!0}),c.cpSync(d,o,{recursive:!0,force:!0})),this.platform==="win32"||this.platform==="darwin"){const n=a.join(r,"scripts","upgrade-guardian.mjs");if(c.existsSync(n)){const u=a.join(this.normalizeRootDir(i),"bin"),m=a.join(u,"upgrade-guardian.mjs"),l=a.join(u,"upgrade-guardian.fallback.mjs"),g=`${m}.tmp-${D.pid}`;c.mkdirSync(u,{recursive:!0});try{c.copyFileSync(n,g),c.renameSync(g,m),c.copyFileSync(n,l)}finally{c.rmSync(g,{force:!0})}}if(this.platform==="darwin"){const u=a.join(r,"scripts","upgrade-guardian.sh");if(c.existsSync(u)){const m=a.join(this.normalizeRootDir(i),"bin","upgrade-guardian.sh"),l=`${m}.tmp-${D.pid}`;try{c.copyFileSync(u,l),c.chmodSync(l,493),c.renameSync(l,m)}finally{c.rmSync(l,{force:!0})}}}}}buildDescriptor(i,t,s=""){const r=this.normalizeRootDir(i),e=this.resolveServiceCliPath(r);return{schema_version:1,platform:this.platform,service_id:L(r,this.platform),node_path:this.nodePath,cli_path:e,cli_hash:this.computeServiceHash(e)??"",definition_path:s,config_dir:t??"",installed_at:0,updated_at:0}}createStore(i){return new E(B(this.normalizeRootDir(i)))}toAdapterPayload(i,t){return{serviceID:i.service_id,nodePath:i.node_path,cliPath:i.cli_path,configDir:i.config_dir||void 0,rootDir:this.normalizeRootDir(t),environmentPath:this.buildServiceEnvironmentPath()}}async resolveServiceLogPaths(i){const t=this.resolveLocalServiceBaseDir();if(t){const s=a.join(this.platform==="darwin"?a.join(this.homeDir,"Library","Logs","grix-connector"):a.join(t,"logs"),L(this.normalizeRootDir(i),this.platform));return await w(s,{recursive:!0}),{stdoutPath:a.join(s,"daemon.out.log"),stderrPath:a.join(s,"daemon.err.log")}}return await w(a.join(i,"service"),{recursive:!0}),{stdoutPath:H(i),stderrPath:N(i)}}buildServiceEnvironmentPath(){const i=[a.dirname(this.nodePath),"/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin","/usr/sbin","/sbin"],t=String(D.env.PATH??"").split(a.delimiter).filter(e=>e&&a.isAbsolute(e)).filter(e=>!e.includes("/node_modules/.bin")).filter(e=>!e.includes("/node-gyp-bin")),s=[],r=new Set;for(const e of[...i,...t]){if(r.has(e))continue;if(r.add(e),[...s,e].join(a.delimiter).length>2048)break;s.push(e)}return s.join(a.delimiter)}toStartParams(i){return{serviceID:i.service_id,definitionPath:i.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:p}}toStopParams(i){return{serviceID:i.service_id,uid:this.uid,homeDir:this.homeDir,runCommand:p}}toRestartParams(i){return{serviceID:i.service_id,definitionPath:i.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:p}}toUninstallParams(i){return{serviceID:i.service_id,definitionPath:i.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:p}}async loadDescriptor(i){const t=this.createStore(i),s=await t.load();if(!s)throw new Error("\u540E\u53F0\u670D\u52A1\u8FD8\u6CA1\u6709\u5B89\u88C5\uFF0C\u8BF7\u5148\u6267\u884C service install\u3002");return{store:t,descriptor:s}}isDescriptorCurrent(i,t){if(t.node_path!==this.nodePath||t.cli_path!==this.resolveServiceCliPath(i)||!t.cli_hash)return!1;const s=this.computeServiceHash(t.cli_path),r=this.computeServiceHash(this.cliPath);return s!==null&&r!==null&&s===t.cli_hash&&r===t.cli_hash}computeFileHash(i){try{const t=c.readFileSync(i);return j("sha256").update(t).digest("hex")}catch{return null}}computeServiceHash(i){if(!this.resolveLocalServiceBaseDir())return this.computeFileHash(i);try{const t=a.dirname(i),s=j("sha256"),r=o=>{const h=c.readdirSync(o,{withFileTypes:!0}).sort((d,n)=>d.name.localeCompare(n.name));for(const d of h){const n=a.join(o,d.name);s.update(a.relative(t,n)),d.isDirectory()?r(n):d.isFile()&&s.update(c.readFileSync(n))}};r(t);const e=a.join(a.dirname(t),"package.json");return c.existsSync(e)&&(s.update("package.json"),s.update(c.readFileSync(e))),s.digest("hex")}catch{return null}}async refreshDescriptor(i,t){const s=this.normalizeRootDir(i);await w(a.join(s,"service"),{recursive:!0}),this.prepareServiceRuntime(s);const r=this.resolveServiceCliPath(s),e={...t,platform:this.platform,service_id:t.service_id||L(s,this.platform),node_path:this.nodePath,cli_path:r,cli_hash:this.computeServiceHash(r)??"",config_dir:t.config_dir},o=await this.resolveServiceLogPaths(s),h=await this.adapter.install({...this.toAdapterPayload(e,s),stdoutPath:o.stdoutPath,stderrPath:o.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:p}),d={...e,installed_at:t.installed_at,definition_path:h?.definitionPath||e.definition_path,updated_at:Date.now()};return await this.createStore(s).save(d),d}async resolveActiveDescriptor(i){const{descriptor:t}=await this.loadDescriptor(i);return this.isDescriptorCurrent(i,t)?t:this.refreshDescriptor(i,t)}async discoverConflictingServices(i){const t=this.normalizeRootDir(i),s=L(t,this.platform);return(await this.adapter.discoverServices({homeDir:this.homeDir,runCommand:p})).filter(e=>e.serviceID!==s)}async cleanupConflictingServices(i){const t=await this.discoverConflictingServices(i);if(t.length>0){const r=t.map(e=>e.serviceID).join(", ");console.log(`\u53D1\u73B0 ${t.length} \u4E2A\u6B8B\u7559\u670D\u52A1\uFF0C\u6B63\u5728\u68C0\u67E5: ${r}`)}let s=0;for(const r of t){if(await this.adapter.isServiceLoaded({serviceID:r.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:p})){console.log(`\u8DF3\u8FC7\u6B63\u5728\u8FD0\u884C\u7684\u670D\u52A1: ${r.serviceID}`);continue}try{await this.adapter.stop({serviceID:r.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:p})}catch{}try{await this.adapter.uninstall({serviceID:r.serviceID,definitionPath:r.definitionPath,uid:this.uid,homeDir:this.homeDir,runCommand:p}),s++}catch{}}return s}captureDaemonBaseline(i){return{status:S(i.daemonStatusFile),lock:_(i.daemonLockFile)}}isDaemonReady(i,t,s){const r=S(i.daemonStatusFile),e=_(i.daemonLockFile);if(!r.running||r.state!=="running"||r.pid<=0||!e||e.pid!==r.pid||!x(e.pid)||typeof e.root_dir!="string"||a.resolve(e.root_dir)!==a.resolve(i.rootDir)||typeof e.instance_id!="string"||e.instance_id.length===0||r.instance_id!==e.instance_id)return!1;const o=r.pid!==t.status.pid||r.state!==t.status.state||r.updated_at!==t.status.updated_at||r.instance_id!==t.status.instance_id,h=!t.lock||e.pid!==t.lock.pid||e.acquired_at!==t.lock.acquired_at||e.instance_id!==t.lock.instance_id,d=o&&h,n=t.status.state==="starting"&&t.status.instance_id===e.instance_id&&t.lock?.pid===e.pid&&t.lock.instance_id===e.instance_id&&r.instance_id===e.instance_id&&r.updated_at!==t.status.updated_at,u=t.lock?.pid===e.pid&&t.lock.instance_id===e.instance_id&&t.lock.acquired_at===e.acquired_at&&(t.status.pid!==e.pid||t.status.instance_id!==e.instance_id||t.status.state!=="running")&&r.pid===e.pid&&r.instance_id===e.instance_id&&r.state==="running"&&r.updated_at!==t.status.updated_at;return!F(e)&&!d&&!n&&!u?!1:s?d||n||u:!0}canForceTerminateDaemon(i,t){const s=_(i.daemonLockFile);return t>0&&s!==null&&s.pid===t&&typeof s.root_dir=="string"&&a.resolve(s.root_dir)===a.resolve(i.rootDir)&&F(s)}defaultDaemonStartTimeoutMs(){return this.platform==="darwin"?5e4:2e4}async waitForDaemonStarted(i,t={}){const s=t.timeoutMs??this.defaultDaemonStartTimeoutMs(),r=t.baseline??this.captureDaemonBaseline(i),e=t.requireNewDaemon??!1,o=t.stdoutLogFile??i.stdoutLogFile,h=t.stderrLogFile??i.stderrLogFile,d=t.stdoutLogCursor??v(o),n=t.stderrLogCursor??v(h),u=Date.now();for(;Date.now()-u<s;){if(this.isDaemonReady(i,r,e))return;const f=S(i.daemonStatusFile);if(f.state==="failed"&&(r.status.state!=="failed"||f.pid!==r.status.pid||f.updated_at!==r.status.updated_at||f.instance_id!==r.status.instance_id)){const k=_(i.daemonLockFile),C=["daemon start failed",`status=${JSON.stringify(f)}`,`lock=${k?JSON.stringify(k):"missing"}`,y("stderr",h,n),y("stdout",o,d)].filter(Boolean);throw new Error(C.join(`
2
- `))}await z(100)}const m=_(i.daemonLockFile),l=S(i.daemonStatusFile),g=[`daemon start timeout (${s}ms)`,`status=${JSON.stringify(l)}`,`lock=${m?JSON.stringify(m):"missing"}`,y("stderr",h,n),y("stdout",o,d)].filter(Boolean);throw new Error(g.join(`
3
- `))}async install(i){const t=this.normalizeRootDir(i.rootDir);await this.cleanupConflictingServices(t),await w(a.join(t,"service"),{recursive:!0}),this.prepareServiceRuntime(t);const s=P(t),r=this.captureDaemonBaseline(s),e=this.buildDescriptor(t,i.configDir),o=await this.resolveServiceLogPaths(t),h=v(o.stdoutPath),d=v(o.stderrPath),n=await this.adapter.isServiceLoaded({serviceID:e.service_id,uid:this.uid,homeDir:this.homeDir,runCommand:p}),u=await this.adapter.install({...this.toAdapterPayload(e,t),stdoutPath:o.stdoutPath,stderrPath:o.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:p}),m={...e,definition_path:u?.definitionPath??"",installed_at:Date.now(),updated_at:Date.now()};return await this.createStore(t).save(m),n?await this.adapter.restart(this.toRestartParams(m)):await this.adapter.start(this.toStartParams(m)),await this.waitForDaemonStarted(s,{baseline:r,requireNewDaemon:!0,stdoutLogFile:o.stdoutPath,stderrLogFile:o.stderrPath,stdoutLogCursor:h,stderrLogCursor:d}),this.status({rootDir:t})}async start(i){const t=this.normalizeRootDir(i.rootDir);await this.cleanupConflictingServices(t);const s=P(t),r=this.captureDaemonBaseline(s),e=r.status,o=v(s.stdoutLogFile),h=v(s.stderrLogFile),n=await this.createStore(t).load();if(e.running&&n&&this.isDescriptorCurrent(t,n)){if(e.state==="starting"){const l=r.lock;if(l!==null&&l.pid===e.pid&&typeof l.root_dir=="string"&&a.resolve(l.root_dir)===a.resolve(s.rootDir)&&typeof l.instance_id=="string"&&l.instance_id.length>0&&e.instance_id===l.instance_id&&F(l)){const f=await this.resolveServiceLogPaths(t);return await this.waitForDaemonStarted(s,{baseline:r,stdoutLogFile:f.stdoutPath,stderrLogFile:f.stderrPath,stdoutLogCursor:o,stderrLogCursor:h}),this.status({rootDir:t})}}if(this.isDaemonReady(s,r,!1))return this.status({rootDir:t})}const u=n?this.isDescriptorCurrent(t,n)&&e.running?n:await this.refreshDescriptor(t,n):await this.resolveActiveDescriptor(t),m=await this.resolveServiceLogPaths(t);try{e.running?await this.adapter.restart(this.toRestartParams(u)):await this.adapter.start(this.toStartParams(u))}catch(l){if(l instanceof Error&&l.message.includes("plist missing"))await this.adapter.install({...this.toAdapterPayload(u,t),stdoutPath:m.stdoutPath,stderrPath:m.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:p}),await this.adapter.start(this.toStartParams(u));else throw l}return await this.waitForDaemonStarted(s,{baseline:r,requireNewDaemon:!0,stdoutLogFile:m.stdoutPath,stderrLogFile:m.stderrPath,stdoutLogCursor:o,stderrLogCursor:h}),this.status({rootDir:t})}async stop(i){const t=this.normalizeRootDir(i.rootDir),{descriptor:s}=await this.loadDescriptor(t),r=P(t),e=S(r.daemonStatusFile);return await this.adapter.stop(this.toStopParams(s)),e.running&&e.pid&&(await this.waitForExit(e.pid,{timeoutMs:5e3})||this.canForceTerminateDaemon(r,e.pid)&&(await this.terminateTree(e.pid,{platform:this.platform,runCommandImpl:p}),await this.waitForExit(e.pid,{timeoutMs:5e3}))),this.status({rootDir:t})}async restart(i){const t=this.normalizeRootDir(i.rootDir);await this.cleanupConflictingServices(t);const s=await this.resolveActiveDescriptor(t),r=P(t),e=this.captureDaemonBaseline(r),o=await this.resolveServiceLogPaths(t),h=v(o.stdoutPath),d=v(o.stderrPath);try{await this.adapter.restart(this.toRestartParams(s))}catch(n){if(n instanceof Error&&n.message.includes("plist missing"))await this.adapter.install({...this.toAdapterPayload(s,t),stdoutPath:o.stdoutPath,stderrPath:o.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:p}),await this.adapter.start(this.toStartParams(s));else throw n}return await this.waitForDaemonStarted(r,{baseline:e,requireNewDaemon:!0,stdoutLogFile:o.stdoutPath,stderrLogFile:o.stderrPath,stdoutLogCursor:h,stderrLogCursor:d}),this.status({rootDir:t})}async uninstall(i){const t=this.normalizeRootDir(i.rootDir),s=this.createStore(t),r=await s.load();return r&&(await this.adapter.uninstall(this.toUninstallParams(r)),await s.clear()),this.status({rootDir:t})}async status(i){const t=this.normalizeRootDir(i.rootDir),r=await this.createStore(t).load(),e=P(t),o=S(e.daemonStatusFile);return r?{installed:!0,install_state:this.isDescriptorCurrent(t,r)?"current":"stale",service_kind:this.adapter.kind,service_id:r.service_id,definition_path:r.definition_path,root_dir:t,daemon_state:o.state,pid:o.pid,connection_state:o.connection_state,updated_at:o.updated_at,reason:o.reason}:{installed:!1,install_state:"missing",service_kind:this.adapter.kind,root_dir:t,daemon_state:o.state,pid:o.pid,reason:o.reason}}}export{rt as ServiceManager};
1
+ import{createHash as L}from"node:crypto";import h from"node:fs";import E from"node:os";import o from"node:path";import D from"node:process";import{mkdir as _}from"node:fs/promises";import{setTimeout as H}from"node:timers/promises";import{resolveRuntimePaths as S}from"../core/config/paths.js";import{inspectDaemonState as g}from"../runtime/service-state.js";import{isProcessRunning as T,runCommand as u,terminateProcessTree as $,waitForProcessExit as b}from"./process-control.js";import{isLockHolderIdentityVerified as F,readDaemonLock as P}from"../runtime/daemon-lock.js";import{ServiceInstallStore as B}from"./service-install-store.js";import{getPlatformServiceAdapter as N}from"./platform-adapter.js";import{captureLogCursor as v,formatLogDelta as w}from"./log-diagnostics.js";import{LAUNCHER_FILE as C,bootstrapVersionFromPackage as q,compareVersions as j,ensureLauncher as M,launcherPath as O,packagedLauncherPath as V,readLauncherTemplateVersion as G,readPackageVersionAt as z,readPointer as x,switchCurrent as J,versionEntry as A}from"../core/upgrade/version-store.js";import{buildServiceID as y,resolveWindowsServiceRuntimeDir as U,resolveServiceInstallRecordPath as W,resolveServiceStderrPath as K,resolveServiceStdoutPath as Q}from"./service-paths.js";class ft{adapter;platform;homeDir;uid;nodePath;cliPath;waitForExit;terminateTree;constructor(e){this.platform=e.platform??D.platform,this.homeDir=e.homeDir??E.homedir(),this.uid=e.uid??D.getuid?.()??0,this.nodePath=e.nodePath??D.execPath,this.cliPath=e.cliPath,this.adapter=N(this.platform),this.waitForExit=e.processControl?.waitForExit??b,this.terminateTree=e.processControl?.terminateTree??$}normalizeRootDir(e){return o.resolve(String(e??"").trim())}resolveLocalServiceBaseDir(){if(this.platform==="darwin")return o.join(this.homeDir,"Library","Application Support","grix-connector");if(this.platform==="win32"){const e=String(D.env.LOCALAPPDATA??"").trim();return o.join(e&&o.isAbsolute(e)?e:o.join(this.homeDir,"AppData","Local"),"grix-connector")}return null}resolveServiceRuntimeDir(e){const t=this.resolveLocalServiceBaseDir();return t?this.platform==="win32"?U(this.normalizeRootDir(e),D.env,this.homeDir):o.join(t,"runtime"):null}resolveServiceCliPath(e){const t=this.resolveServiceRuntimeDir(e);return t?O(t):this.cliPath}resolveSourcePackageDir(){return o.dirname(o.dirname(this.cliPath))}async adoptLauncher(e){const t=this.normalizeRootDir(e);if(!this.resolveServiceRuntimeDir(t))return!1;const r=await this.createStore(t).load();return!r||r.cli_path===this.resolveServiceCliPath(t)?!1:(await this.refreshDescriptor(t,r),!0)}prepareServiceRuntime(e){const t=this.resolveServiceRuntimeDir(e);if(!t)return;const r=this.resolveSourcePackageDir();h.mkdirSync(t,{recursive:!0});const s=z(r);if(s){if(!M(t,r))throw new Error(`Package at ${r} does not ship ${C}; refusing to point the service at it`);const i=x(t,"current");(!i||!h.existsSync(A(t,i))||j(s,i)>0)&&(q(t,r,s),J(t,s))}if(this.platform==="win32"||this.platform==="darwin"){const i=o.join(r,"scripts","upgrade-guardian.mjs");if(h.existsSync(i)){const a=o.join(this.normalizeRootDir(e),"bin"),d=o.join(a,"upgrade-guardian.mjs"),c=o.join(a,"upgrade-guardian.fallback.mjs"),n=`${d}.tmp-${D.pid}`;h.mkdirSync(a,{recursive:!0});try{h.copyFileSync(i,n),h.renameSync(n,d),h.copyFileSync(i,c)}finally{h.rmSync(n,{force:!0})}}if(this.platform==="darwin"){const a=o.join(r,"scripts","upgrade-guardian.sh");if(h.existsSync(a)){const d=o.join(this.normalizeRootDir(e),"bin","upgrade-guardian.sh"),c=`${d}.tmp-${D.pid}`;try{h.copyFileSync(a,c),h.chmodSync(c,493),h.renameSync(c,d)}finally{h.rmSync(c,{force:!0})}}}}}buildDescriptor(e,t,r=""){const s=this.normalizeRootDir(e),i=this.resolveServiceCliPath(s);return{schema_version:1,platform:this.platform,service_id:y(s,this.platform),node_path:this.nodePath,cli_path:i,cli_hash:this.computeServiceHash(i)??"",definition_path:r,config_dir:t??"",installed_at:0,updated_at:0}}createStore(e){return new B(W(this.normalizeRootDir(e)))}toAdapterPayload(e,t){return{serviceID:e.service_id,nodePath:e.node_path,cliPath:e.cli_path,configDir:e.config_dir||void 0,rootDir:this.normalizeRootDir(t),environmentPath:this.buildServiceEnvironmentPath()}}async resolveServiceLogPaths(e){const t=this.resolveLocalServiceBaseDir();if(t){const r=o.join(this.platform==="darwin"?o.join(this.homeDir,"Library","Logs","grix-connector"):o.join(t,"logs"),y(this.normalizeRootDir(e),this.platform));return await _(r,{recursive:!0}),{stdoutPath:o.join(r,"daemon.out.log"),stderrPath:o.join(r,"daemon.err.log")}}return await _(o.join(e,"service"),{recursive:!0}),{stdoutPath:Q(e),stderrPath:K(e)}}buildServiceEnvironmentPath(){const e=[o.dirname(this.nodePath),"/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin","/usr/sbin","/sbin"],t=String(D.env.PATH??"").split(o.delimiter).filter(i=>i&&o.isAbsolute(i)).filter(i=>!i.includes("/node_modules/.bin")).filter(i=>!i.includes("/node-gyp-bin")),r=[],s=new Set;for(const i of[...e,...t]){if(s.has(i))continue;if(s.add(i),[...r,i].join(o.delimiter).length>2048)break;r.push(i)}return r.join(o.delimiter)}toStartParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:u}}toStopParams(e){return{serviceID:e.service_id,uid:this.uid,homeDir:this.homeDir,runCommand:u}}toRestartParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:u}}toUninstallParams(e){return{serviceID:e.service_id,definitionPath:e.definition_path,uid:this.uid,homeDir:this.homeDir,runCommand:u}}async loadDescriptor(e){const t=this.createStore(e),r=await t.load();if(!r)throw new Error("\u540E\u53F0\u670D\u52A1\u8FD8\u6CA1\u6709\u5B89\u88C5\uFF0C\u8BF7\u5148\u6267\u884C service install\u3002");return{store:t,descriptor:r}}isDescriptorCurrent(e,t){if(t.node_path!==this.nodePath||t.cli_path!==this.resolveServiceCliPath(e)||!t.cli_hash)return!1;const r=this.computeServiceHash(t.cli_path),s=this.computeServiceHash(this.cliPath);return r!==null&&s!==null&&r===t.cli_hash&&s===t.cli_hash}computeFileHash(e){try{const t=h.readFileSync(e);return L("sha256").update(t).digest("hex")}catch{return null}}computeServiceHash(e){return this.resolveLocalServiceBaseDir()?this.computeLauncherHash(e):this.computeFileHash(e)}computeLauncherHash(e){try{const t=this.resolveSourcePackageDir(),r=z(t)??"",s=o.basename(e)===C,i=s?o.dirname(e):null,a=s?e:V(t);if(!a)return null;const d=G(a);if(d<0)return null;let c=!0;if(i){const n=x(i,"current");c=!!n&&h.existsSync(A(i,n))&&j(n,r)>=0}return c?L("sha256").update(`launcher:${d}`).update(`source:${r}`).digest("hex"):null}catch{return null}}computeRuntimeDirHash(e){try{const t=o.dirname(e),r=L("sha256"),s=a=>{const d=h.readdirSync(a,{withFileTypes:!0}).sort((c,n)=>c.name.localeCompare(n.name));for(const c of d){const n=o.join(a,c.name);r.update(o.relative(t,n)),c.isDirectory()?s(n):c.isFile()&&r.update(h.readFileSync(n))}};s(t);const i=o.join(o.dirname(t),"package.json");return h.existsSync(i)&&(r.update("package.json"),r.update(h.readFileSync(i))),r.digest("hex")}catch{return null}}async refreshDescriptor(e,t){const r=this.normalizeRootDir(e);await _(o.join(r,"service"),{recursive:!0}),this.prepareServiceRuntime(r);const s=this.resolveServiceCliPath(r),i={...t,platform:this.platform,service_id:t.service_id||y(r,this.platform),node_path:this.nodePath,cli_path:s,cli_hash:this.computeServiceHash(s)??"",config_dir:t.config_dir},a=await this.resolveServiceLogPaths(r),d=await this.adapter.install({...this.toAdapterPayload(i,r),stdoutPath:a.stdoutPath,stderrPath:a.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:u}),c={...i,installed_at:t.installed_at,definition_path:d?.definitionPath||i.definition_path,updated_at:Date.now()};return await this.createStore(r).save(c),c}async resolveActiveDescriptor(e){const{descriptor:t}=await this.loadDescriptor(e);return this.isDescriptorCurrent(e,t)?t:this.refreshDescriptor(e,t)}async discoverConflictingServices(e){const t=this.normalizeRootDir(e),r=y(t,this.platform);return(await this.adapter.discoverServices({homeDir:this.homeDir,runCommand:u})).filter(i=>i.serviceID!==r)}async cleanupConflictingServices(e){const t=await this.discoverConflictingServices(e);if(t.length>0){const s=t.map(i=>i.serviceID).join(", ");console.log(`\u53D1\u73B0 ${t.length} \u4E2A\u6B8B\u7559\u670D\u52A1\uFF0C\u6B63\u5728\u68C0\u67E5: ${s}`)}let r=0;for(const s of t){if(await this.adapter.isServiceLoaded({serviceID:s.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:u})){console.log(`\u8DF3\u8FC7\u6B63\u5728\u8FD0\u884C\u7684\u670D\u52A1: ${s.serviceID}`);continue}try{await this.adapter.stop({serviceID:s.serviceID,uid:this.uid,homeDir:this.homeDir,runCommand:u})}catch{}try{await this.adapter.uninstall({serviceID:s.serviceID,definitionPath:s.definitionPath,uid:this.uid,homeDir:this.homeDir,runCommand:u}),r++}catch{}}return r}captureDaemonBaseline(e){return{status:g(e.daemonStatusFile),lock:P(e.daemonLockFile)}}isDaemonReady(e,t,r){const s=g(e.daemonStatusFile),i=P(e.daemonLockFile);if(!s.running||s.state!=="running"||s.pid<=0||!i||i.pid!==s.pid||!T(i.pid)||typeof i.root_dir!="string"||o.resolve(i.root_dir)!==o.resolve(e.rootDir)||typeof i.instance_id!="string"||i.instance_id.length===0||s.instance_id!==i.instance_id)return!1;const a=s.pid!==t.status.pid||s.state!==t.status.state||s.updated_at!==t.status.updated_at||s.instance_id!==t.status.instance_id,d=!t.lock||i.pid!==t.lock.pid||i.acquired_at!==t.lock.acquired_at||i.instance_id!==t.lock.instance_id,c=a&&d,n=t.status.state==="starting"&&t.status.instance_id===i.instance_id&&t.lock?.pid===i.pid&&t.lock.instance_id===i.instance_id&&s.instance_id===i.instance_id&&s.updated_at!==t.status.updated_at,m=t.lock?.pid===i.pid&&t.lock.instance_id===i.instance_id&&t.lock.acquired_at===i.acquired_at&&(t.status.pid!==i.pid||t.status.instance_id!==i.instance_id||t.status.state!=="running")&&s.pid===i.pid&&s.instance_id===i.instance_id&&s.state==="running"&&s.updated_at!==t.status.updated_at;return!F(i)&&!c&&!n&&!m?!1:r?c||n||m:!0}canForceTerminateDaemon(e,t){const r=P(e.daemonLockFile);return t>0&&r!==null&&r.pid===t&&typeof r.root_dir=="string"&&o.resolve(r.root_dir)===o.resolve(e.rootDir)&&F(r)}defaultDaemonStartTimeoutMs(){return this.platform==="darwin"?5e4:2e4}async waitForDaemonStarted(e,t={}){const r=t.timeoutMs??this.defaultDaemonStartTimeoutMs(),s=t.baseline??this.captureDaemonBaseline(e),i=t.requireNewDaemon??!1,a=t.stdoutLogFile??e.stdoutLogFile,d=t.stderrLogFile??e.stderrLogFile,c=t.stdoutLogCursor??v(a),n=t.stderrLogCursor??v(d),m=Date.now();for(;Date.now()-m<r;){if(this.isDaemonReady(e,s,i))return;const f=g(e.daemonStatusFile);if(f.state==="failed"&&(s.status.state!=="failed"||f.pid!==s.status.pid||f.updated_at!==s.status.updated_at||f.instance_id!==s.status.instance_id)){const R=P(e.daemonLockFile),I=["daemon start failed",`status=${JSON.stringify(f)}`,`lock=${R?JSON.stringify(R):"missing"}`,w("stderr",d,n),w("stdout",a,c)].filter(Boolean);throw new Error(I.join(`
2
+ `))}await H(100)}const p=P(e.daemonLockFile),l=g(e.daemonStatusFile),k=[`daemon start timeout (${r}ms)`,`status=${JSON.stringify(l)}`,`lock=${p?JSON.stringify(p):"missing"}`,w("stderr",d,n),w("stdout",a,c)].filter(Boolean);throw new Error(k.join(`
3
+ `))}async install(e){const t=this.normalizeRootDir(e.rootDir);await this.cleanupConflictingServices(t),await _(o.join(t,"service"),{recursive:!0}),this.prepareServiceRuntime(t);const r=S(t),s=this.captureDaemonBaseline(r),i=this.buildDescriptor(t,e.configDir),a=await this.resolveServiceLogPaths(t),d=v(a.stdoutPath),c=v(a.stderrPath),n=await this.adapter.isServiceLoaded({serviceID:i.service_id,uid:this.uid,homeDir:this.homeDir,runCommand:u}),m=await this.adapter.install({...this.toAdapterPayload(i,t),stdoutPath:a.stdoutPath,stderrPath:a.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:u}),p={...i,definition_path:m?.definitionPath??"",installed_at:Date.now(),updated_at:Date.now()};return await this.createStore(t).save(p),n?await this.adapter.restart(this.toRestartParams(p)):await this.adapter.start(this.toStartParams(p)),await this.waitForDaemonStarted(r,{baseline:s,requireNewDaemon:!0,stdoutLogFile:a.stdoutPath,stderrLogFile:a.stderrPath,stdoutLogCursor:d,stderrLogCursor:c}),this.status({rootDir:t})}async start(e){const t=this.normalizeRootDir(e.rootDir);await this.cleanupConflictingServices(t);const r=S(t),s=this.captureDaemonBaseline(r),i=s.status,a=v(r.stdoutLogFile),d=v(r.stderrLogFile),n=await this.createStore(t).load();if(i.running&&n&&this.isDescriptorCurrent(t,n)){if(i.state==="starting"){const l=s.lock;if(l!==null&&l.pid===i.pid&&typeof l.root_dir=="string"&&o.resolve(l.root_dir)===o.resolve(r.rootDir)&&typeof l.instance_id=="string"&&l.instance_id.length>0&&i.instance_id===l.instance_id&&F(l)){const f=await this.resolveServiceLogPaths(t);return await this.waitForDaemonStarted(r,{baseline:s,stdoutLogFile:f.stdoutPath,stderrLogFile:f.stderrPath,stdoutLogCursor:a,stderrLogCursor:d}),this.status({rootDir:t})}}if(this.isDaemonReady(r,s,!1))return this.status({rootDir:t})}const m=n?this.isDescriptorCurrent(t,n)&&i.running?n:await this.refreshDescriptor(t,n):await this.resolveActiveDescriptor(t),p=await this.resolveServiceLogPaths(t);try{i.running?await this.adapter.restart(this.toRestartParams(m)):await this.adapter.start(this.toStartParams(m))}catch(l){if(l instanceof Error&&l.message.includes("plist missing"))await this.adapter.install({...this.toAdapterPayload(m,t),stdoutPath:p.stdoutPath,stderrPath:p.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:u}),await this.adapter.start(this.toStartParams(m));else throw l}return await this.waitForDaemonStarted(r,{baseline:s,requireNewDaemon:!0,stdoutLogFile:p.stdoutPath,stderrLogFile:p.stderrPath,stdoutLogCursor:a,stderrLogCursor:d}),this.status({rootDir:t})}async stop(e){const t=this.normalizeRootDir(e.rootDir),{descriptor:r}=await this.loadDescriptor(t),s=S(t),i=g(s.daemonStatusFile);return await this.adapter.stop(this.toStopParams(r)),i.running&&i.pid&&(await this.waitForExit(i.pid,{timeoutMs:5e3})||this.canForceTerminateDaemon(s,i.pid)&&(await this.terminateTree(i.pid,{platform:this.platform,runCommandImpl:u}),await this.waitForExit(i.pid,{timeoutMs:5e3}))),this.status({rootDir:t})}async restart(e){const t=this.normalizeRootDir(e.rootDir);await this.cleanupConflictingServices(t);const r=await this.resolveActiveDescriptor(t),s=S(t),i=this.captureDaemonBaseline(s),a=await this.resolveServiceLogPaths(t),d=v(a.stdoutPath),c=v(a.stderrPath);try{await this.adapter.restart(this.toRestartParams(r))}catch(n){if(n instanceof Error&&n.message.includes("plist missing"))await this.adapter.install({...this.toAdapterPayload(r,t),stdoutPath:a.stdoutPath,stderrPath:a.stderrPath,homeDir:this.homeDir,uid:this.uid,runCommand:u}),await this.adapter.start(this.toStartParams(r));else throw n}return await this.waitForDaemonStarted(s,{baseline:i,requireNewDaemon:!0,stdoutLogFile:a.stdoutPath,stderrLogFile:a.stderrPath,stdoutLogCursor:d,stderrLogCursor:c}),this.status({rootDir:t})}async uninstall(e){const t=this.normalizeRootDir(e.rootDir),r=this.createStore(t),s=await r.load();return s&&(await this.adapter.uninstall(this.toUninstallParams(s)),await r.clear()),this.status({rootDir:t})}async status(e){const t=this.normalizeRootDir(e.rootDir),s=await this.createStore(t).load(),i=S(t),a=g(i.daemonStatusFile);return s?{installed:!0,install_state:this.isDescriptorCurrent(t,s)?"current":"stale",service_kind:this.adapter.kind,service_id:s.service_id,definition_path:s.definition_path,root_dir:t,daemon_state:a.state,pid:a.pid,connection_state:a.connection_state,updated_at:a.updated_at,reason:a.reason}:{installed:!1,install_state:"missing",service_kind:this.adapter.kind,root_dir:t,daemon_state:a.state,pid:a.pid,reason:a.reason}}}export{ft as ServiceManager};
@@ -1 +1 @@
1
- import{existsSync as P,mkdirSync as y,copyFileSync as v,renameSync as E,rmSync as R,readFileSync as I,writeFileSync as M}from"node:fs";import O from"node:os";import s from"node:path";import{log as l}from"../core/log/index.js";import{readJSONFile as A,writeJSONFileAtomic as k}from"../core/util/json-file.js";import{WINDOWS_WRAPPER_TEMPLATE_MARKER as T,WINDOWS_WRAPPER_TEMPLATE_VERSION as u,buildWindowsWrapperScript as H,resolveWindowsWrapperPath as L}from"./platform-adapter.js";import{findProcessesByCommandLine as V,spawnDetached as G}from"./process-control.js";import{buildServiceID as q,resolveServiceInstallRecordPath as J}from"./service-paths.js";const b=3,X=360*60*1e3,Y={handoff_attempts:0,last_attempt_at:0,refreshed_template:0};function B(r){for(const e of r.split(/\r?\n/,8)){const t=e.indexOf(T);if(t<0)continue;const n=Number.parseInt(e.slice(t+T.length).trim(),10);if(Number.isFinite(n)&&n>0)return n}return 0}function C(r){const e=n=>({refresh:!1,handoff:!1,reason:n});if(r.platform!=="win32")return e("not windows");if(r.upgradePending)return e("upgrade transaction in flight");if(r.installedTemplate<0)return e("no installed wrapper");const t=r.installedTemplate<u;return r.supervised?{refresh:t,handoff:!1,reason:t?"refresh under supervision":"already current"}:r.wrapperProcessAlive?r.state.handoff_attempts>=b?{refresh:t,handoff:!1,reason:"handoff attempts exhausted"}:r.state.last_attempt_at>0&&r.now-r.state.last_attempt_at<X?{refresh:t,handoff:!1,reason:"handoff retry throttled"}:{refresh:t,handoff:!0,reason:"unsupervised daemon under a stale wrapper"}:{refresh:t,handoff:!1,reason:"daemon not running under a wrapper"}}function N(r){return s.join(r,"data","wrapper-migration.json")}function K(r){const e=A(N(r));return!e||typeof e!="object"?{...Y}:{handoff_attempts:Number.isFinite(e.handoff_attempts)?Number(e.handoff_attempts):0,last_attempt_at:Number.isFinite(e.last_attempt_at)?Number(e.last_attempt_at):0,refreshed_template:Number.isFinite(e.refreshed_template)?Number(e.refreshed_template):0}}async function F(r,e){await k(N(r),e)}function U(r){try{return B(I(r,"utf8"))}catch{return-1}}function z(r){const e=H({nodePath:r.nodePath,cliPath:r.cliPath,configDir:r.configDir,rootDir:r.rootDir});y(s.dirname(r.wrapperPath),{recursive:!0});const t=`${r.wrapperPath}.tmp-${process.pid}`;try{M(t,e,"utf8"),E(t,r.wrapperPath)}finally{R(t,{force:!0})}const n=s.join(r.packageDir,"scripts","upgrade-guardian.mjs");if(!P(n))return;const d=s.join(r.rootDir,"bin");y(d,{recursive:!0});const c=s.join(d,"upgrade-guardian.mjs"),o=s.join(d,"upgrade-guardian.fallback.mjs"),p=`${c}.tmp-${process.pid}`;try{v(n,p),E(p,c),v(n,o)}finally{R(p,{force:!0})}}async function or(r){const e=r.platform??process.platform,t=r.env??process.env,n=r.homeDir??O.homedir(),d=r.now??Date.now(),c=a=>({plan:{refresh:!1,handoff:!1,reason:a},refreshed:!1,handoffStarted:!1});if(e!=="win32")return c("not windows");const o=A(J(r.rootDir));if(!o?.node_path||!o.cli_path)return c("no service install record");const p=o.service_id||q(r.rootDir,"win32"),m=L(p,n),w=U(m),S=P(s.join(r.rootDir,"data","upgrade-pending.json")),D=t.GRIX_WRAPPER_SUPERVISED==="1",h=K(r.rootDir);let _=!1;if(!D&&!S&&w>=0){const a=r.findWrapperProcesses??V;try{_=(await a(`${p}-wrapper.vbs`)).filter($=>$!==process.pid).length>0}catch{_=!1}}const i=C({platform:e,supervised:D,installedTemplate:w,wrapperProcessAlive:_,upgradePending:S,state:h,now:d});let f=!1;if(i.refresh)try{z({wrapperPath:m,rootDir:r.rootDir,nodePath:o.node_path,cliPath:o.cli_path,configDir:o.config_dir||void 0,packageDir:s.dirname(s.dirname(o.cli_path))}),f=!0,l.info("wrapper-migration",`Refreshed Windows wrapper ${w} -> ${u}: ${m}`)}catch(a){return l.warn("wrapper-migration",`Failed to refresh Windows wrapper: ${a instanceof Error?a.message:a}`),{plan:i,refreshed:!1,handoffStarted:!1}}if(!i.handoff||!r.requestGracefulShutdown)return i.reason&&!i.handoff&&l.info("wrapper-migration",`No wrapper handoff: ${i.reason}`),f&&await F(r.rootDir,{...h,refreshed_template:u}).catch(()=>{}),{plan:i,refreshed:f,handoffStarted:!1};const g=s.join(r.rootDir,"bin","upgrade-guardian.mjs");if(!P(g))return l.warn("wrapper-migration",`Wrapper handoff skipped: guardian missing at ${g}`),{plan:i,refreshed:f,handoffStarted:!1};await F(r.rootDir,{handoff_attempts:h.handoff_attempts+1,last_attempt_at:d,refreshed_template:f?u:h.refreshed_template}).catch(()=>{});const j=r.spawnHelper??((a,W)=>{G(a,W)});try{j(o.node_path,[g,"adopt-wrapper","--root",r.rootDir,"--wrapper",m,"--service-id",p,"--daemon-pid",String(process.pid)])}catch(a){return l.warn("wrapper-migration",`Failed to spawn wrapper adopter: ${a instanceof Error?a.message:a}`),{plan:i,refreshed:f,handoffStarted:!1}}return l.info("wrapper-migration",`Handing off to the refreshed Windows wrapper (attempt ${h.handoff_attempts+1}/${b}); shutting down gracefully`),await r.requestGracefulShutdown(),{plan:i,refreshed:f,handoffStarted:!0}}export{b as WRAPPER_HANDOFF_MAX_ATTEMPTS,X as WRAPPER_HANDOFF_RETRY_INTERVAL_MS,or as migrateWindowsWrapper,B as parseWrapperTemplateVersion,C as planWrapperMigration,U as readInstalledWrapperTemplate,K as readWrapperMigrationState,z as refreshWrapperAssets,N as resolveWrapperMigrationStatePath};
1
+ import{existsSync as P,mkdirSync as A,copyFileSync as E,renameSync as R,rmSync as v,readFileSync as M,writeFileSync as O}from"node:fs";import k from"node:os";import i from"node:path";import{log as l}from"../core/log/index.js";import{readJSONFile as N,writeJSONFileAtomic as H}from"../core/util/json-file.js";import{WINDOWS_WRAPPER_TEMPLATE_MARKER as b,WINDOWS_WRAPPER_TEMPLATE_VERSION as u,buildWindowsWrapperScript as L,resolveWindowsWrapperPath as G}from"./platform-adapter.js";import{findProcessesByCommandLine as V,spawnDetached as C}from"./process-control.js";import{buildServiceID as U,resolveServiceInstallRecordPath as X}from"./service-paths.js";import{LAUNCHER_FILE as q}from"../core/upgrade/version-store.js";const T=3,J=360*60*1e3,Y={handoff_attempts:0,last_attempt_at:0,refreshed_template:0};function B(r){for(const e of r.split(/\r?\n/,8)){const t=e.indexOf(b);if(t<0)continue;const n=Number.parseInt(e.slice(t+b.length).trim(),10);if(Number.isFinite(n)&&n>0)return n}return 0}function K(r){const e=n=>({refresh:!1,handoff:!1,reason:n});if(r.platform!=="win32")return e("not windows");if(r.upgradePending)return e("upgrade transaction in flight");if(r.installedTemplate<0)return e("no installed wrapper");const t=r.installedTemplate<u;return r.supervised&&!r.launcherAdoption?{refresh:t,handoff:!1,reason:t?"refresh under supervision":"already current"}:r.wrapperProcessAlive?r.state.handoff_attempts>=T?{refresh:t,handoff:!1,reason:"handoff attempts exhausted"}:r.state.last_attempt_at>0&&r.now-r.state.last_attempt_at<J?{refresh:t,handoff:!1,reason:"handoff retry throttled"}:{refresh:t,handoff:!0,reason:r.launcherAdoption?"wrapper must re-read the launcher command line":"unsupervised daemon under a stale wrapper"}:{refresh:t,handoff:!1,reason:"daemon not running under a wrapper"}}function F(r){return i.join(r,"data","wrapper-migration.json")}function z(r){const e=N(F(r));return!e||typeof e!="object"?{...Y}:{handoff_attempts:Number.isFinite(e.handoff_attempts)?Number(e.handoff_attempts):0,last_attempt_at:Number.isFinite(e.last_attempt_at)?Number(e.last_attempt_at):0,refreshed_template:Number.isFinite(e.refreshed_template)?Number(e.refreshed_template):0}}async function j(r,e){await H(F(r),e)}function Q(r){try{return B(M(r,"utf8"))}catch{return-1}}function Z(r){const e=L({nodePath:r.nodePath,cliPath:r.cliPath,configDir:r.configDir,rootDir:r.rootDir});A(i.dirname(r.wrapperPath),{recursive:!0});const t=`${r.wrapperPath}.tmp-${process.pid}`;try{O(t,e,"utf8"),R(t,r.wrapperPath)}finally{v(t,{force:!0})}const n=i.join(r.packageDir,"scripts","upgrade-guardian.mjs");if(!P(n))return;const d=i.join(r.rootDir,"bin");A(d,{recursive:!0});const c=i.join(d,"upgrade-guardian.mjs"),o=i.join(d,"upgrade-guardian.fallback.mjs"),p=`${c}.tmp-${process.pid}`;try{E(n,p),R(p,c),E(n,o)}finally{v(p,{force:!0})}}async function pr(r){const e=r.platform??process.platform,t=r.env??process.env,n=r.homeDir??k.homedir(),d=r.now??Date.now(),c=a=>({plan:{refresh:!1,handoff:!1,reason:a},refreshed:!1,handoffStarted:!1});if(e!=="win32")return c("not windows");const o=N(X(r.rootDir));if(!o?.node_path||!o.cli_path)return c("no service install record");const p=o.service_id||U(r.rootDir,"win32"),m=G(p,n),w=Q(m),S=P(i.join(r.rootDir,"data","upgrade-pending.json")),D=t.GRIX_WRAPPER_SUPERVISED==="1",W=i.basename(o.cli_path)===q&&t.GRIX_LAUNCHER_MANAGED!=="1",h=z(r.rootDir);let _=!1;if((!D||W)&&!S&&w>=0){const a=r.findWrapperProcesses??V;try{_=(await a(`${p}-wrapper.vbs`)).filter(I=>I!==process.pid).length>0}catch{_=!1}}const s=K({platform:e,supervised:D,launcherAdoption:W,installedTemplate:w,wrapperProcessAlive:_,upgradePending:S,state:h,now:d});let f=!1;if(s.refresh)try{Z({wrapperPath:m,rootDir:r.rootDir,nodePath:o.node_path,cliPath:o.cli_path,configDir:o.config_dir||void 0,packageDir:i.dirname(i.dirname(o.cli_path))}),f=!0,l.info("wrapper-migration",`Refreshed Windows wrapper ${w} -> ${u}: ${m}`)}catch(a){return l.warn("wrapper-migration",`Failed to refresh Windows wrapper: ${a instanceof Error?a.message:a}`),{plan:s,refreshed:!1,handoffStarted:!1}}if(!s.handoff||!r.requestGracefulShutdown)return s.reason&&!s.handoff&&l.info("wrapper-migration",`No wrapper handoff: ${s.reason}`),f&&await j(r.rootDir,{...h,refreshed_template:u}).catch(()=>{}),{plan:s,refreshed:f,handoffStarted:!1};const g=i.join(r.rootDir,"bin","upgrade-guardian.mjs");if(!P(g))return l.warn("wrapper-migration",`Wrapper handoff skipped: guardian missing at ${g}`),{plan:s,refreshed:f,handoffStarted:!1};await j(r.rootDir,{handoff_attempts:h.handoff_attempts+1,last_attempt_at:d,refreshed_template:f?u:h.refreshed_template}).catch(()=>{});const $=r.spawnHelper??((a,y)=>{C(a,y)});try{$(o.node_path,[g,"adopt-wrapper","--root",r.rootDir,"--wrapper",m,"--service-id",p,"--daemon-pid",String(process.pid)])}catch(a){return l.warn("wrapper-migration",`Failed to spawn wrapper adopter: ${a instanceof Error?a.message:a}`),{plan:s,refreshed:f,handoffStarted:!1}}return l.info("wrapper-migration",`Handing off to the refreshed Windows wrapper (attempt ${h.handoff_attempts+1}/${T}); shutting down gracefully`),await r.requestGracefulShutdown(),{plan:s,refreshed:f,handoffStarted:!0}}export{T as WRAPPER_HANDOFF_MAX_ATTEMPTS,J as WRAPPER_HANDOFF_RETRY_INTERVAL_MS,pr as migrateWindowsWrapper,B as parseWrapperTemplateVersion,K as planWrapperMigration,Q as readInstalledWrapperTemplate,z as readWrapperMigrationState,Z as refreshWrapperAssets,F as resolveWrapperMigrationStatePath};
@@ -12334,7 +12334,7 @@ async function downloadPackage(params) {
12334
12334
  // src/openclaw/egg/persona-installer.ts
12335
12335
  import { join as join12 } from "node:path";
12336
12336
 
12337
- // node_modules/fflate/esm/index.mjs
12337
+ // ../grix-connector/node_modules/fflate/esm/index.mjs
12338
12338
  import { createRequire } from "module";
12339
12339
  var require2 = createRequire("/");
12340
12340
  var _a;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grix-connector",
3
- "version": "4.3.9",
3
+ "version": "4.4.0-beta.2",
4
4
  "description": "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.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,6 +17,7 @@
17
17
  "openclaw-plugin",
18
18
  "scripts/install-guardian.mjs",
19
19
  "scripts/upgrade-guardian.mjs",
20
+ "scripts/launcher.mjs",
20
21
  "scripts/analyze-audit-replays.mjs",
21
22
  "scripts/install-guardian.sh",
22
23
  "scripts/upgrade-guardian.sh",
@@ -25,7 +26,7 @@
25
26
  "scripts": {
26
27
  "postinstall": "node scripts/install-guardian.mjs",
27
28
  "prepublishOnly": "npm run build:all",
28
- "build": "npm run check:isolation && node -e \"const fs=require('fs'); for(const dir of ['dist/default-skills','dist/kimi-plugins']) fs.rmSync(dir,{recursive:true,force:true})\" && tsc && node -e \"const fs=require('fs'); for(const dir of ['default-skills','kimi-plugins']) fs.cpSync('src/'+dir,'dist/'+dir,{recursive:true,filter:s=>!s.endsWith('.ts')}); fs.cpSync('src/adapter/deepseek-harness/grix-jsonrpc.cordis.yml','dist/adapter/deepseek-harness/grix-jsonrpc.cordis.yml')\" && node scripts/build-dsh-bridge.mjs && node -e \"require('fs').cpSync('src/core/proxy/hermes-profile-relay.py','dist/core/proxy/hermes-profile-relay.py')\"",
29
+ "build": "npm run check:isolation && node -e \"const fs=require('fs'); for(const dir of ['dist/default-skills','dist/kimi-plugins']) fs.rmSync(dir,{recursive:true,force:true})\" && tsc && node -e \"const fs=require('fs'); for(const dir of ['default-skills','kimi-plugins']) fs.cpSync('src/'+dir,'dist/'+dir,{recursive:true,filter:s=>!s.endsWith('.ts')}); fs.cpSync('src/adapter/deepseek-harness/grix-jsonrpc.cordis.yml','dist/adapter/deepseek-harness/grix-jsonrpc.cordis.yml')\" && node scripts/build-dsh-bridge.mjs && node -e \"require('fs').cpSync('src/core/proxy/hermes-profile-relay.py','dist/core/proxy/hermes-profile-relay.py'); require('fs').cpSync('scripts/launcher.mjs','dist/launcher.mjs')\"",
29
30
  "check:isolation": "node scripts/check-adapter-isolation.mjs",
30
31
  "build:plugin": "node scripts/build-plugin.mjs",
31
32
  "build:all": "npm run build && node scripts/minify-dist.mjs && npm run build:plugin",
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ // grix-launcher — 稳定的守护进程启动器(并排版本 + 指针切换)。
3
+ //
4
+ // 服务注册(launchd / Task Scheduler wrapper)固定指向本文件,它本身不属于任何
5
+ // connector 版本。启动时读 `<store>/current`,拉起 `<store>/versions/<ver>/dist/grix.js`,
6
+ // 并把自身收到的参数原样转交。daemon 退出后:
7
+ // - 退出码 75(RESTART_EXIT_CODE):升级切换 / 重载请求,立即重新读指针拉起;
8
+ // - 退出码 0 或被信号终止:daemon 是被要求停下的,launcher 随之退出,保活与否交给
9
+ // 上层 supervisor(launchd KeepAlive / Windows wrapper),语义与 launcher 出现前一致;
10
+ // 否则 `grix-connector stop` 的兜底强杀会被 launcher 复活。
11
+ // - 其他非零退出:按退避重启;若该版本从未写过 `<store>/health/<ver>.ok` 且 5 分钟内快崩
12
+ // 3 次,则把 `current` 切回 `previous`,写 `<store>/rollback.json` 供旧版本上报。
13
+ // 收到 SIGTERM/SIGINT/SIGHUP 时转发给子进程并随其退出(服务停止语义)。
14
+ //
15
+ // 本文件由 daemon 在启动时按 LAUNCHER_TEMPLATE_VERSION 比对并原子重写,改动行为时必须 +1。
16
+ export const LAUNCHER_TEMPLATE_VERSION = 1;
17
+
18
+ import { spawn } from 'node:child_process';
19
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, appendFileSync } from 'node:fs';
20
+ import { dirname, join } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const RESTART_EXIT_CODE = 75;
24
+ const RAPID_CRASH_WINDOW_MS = 5 * 60 * 1000;
25
+ const RAPID_CRASH_LIMIT = 3;
26
+ const MAX_BACKOFF_MS = 30_000;
27
+
28
+ function resolveStoreDir() {
29
+ const self = fileURLToPath(import.meta.url);
30
+ let real = self;
31
+ try { real = realpathSync(self); } catch { /* keep symlink path */ }
32
+ return dirname(real);
33
+ }
34
+
35
+ function readPointer(store, name) {
36
+ try {
37
+ const value = readFileSync(join(store, name), 'utf8').trim();
38
+ return value || null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function writePointer(store, name, value) {
45
+ const file = join(store, name);
46
+ const tmp = `${file}.tmp-${process.pid}`;
47
+ writeFileSync(tmp, `${value}\n`, 'utf8');
48
+ renameSync(tmp, file);
49
+ }
50
+
51
+ function entryFor(store, version) {
52
+ return join(store, 'versions', version, 'dist', 'grix.js');
53
+ }
54
+
55
+ function log(store, message) {
56
+ const line = `[${new Date().toISOString()}] launcher pid=${process.pid} ${message}\n`;
57
+ try { appendFileSync(join(store, 'launcher.log'), line); } catch { /* best effort */ }
58
+ try { process.stderr.write(line); } catch { /* best effort */ }
59
+ }
60
+
61
+ function sleep(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+
65
+ function runChild(store, version, args) {
66
+ return new Promise((resolve) => {
67
+ const child = spawn(process.execPath, [entryFor(store, version), ...args], {
68
+ stdio: 'inherit',
69
+ windowsHide: true,
70
+ env: {
71
+ ...process.env,
72
+ GRIX_LAUNCHER_MANAGED: '1',
73
+ GRIX_LAUNCHER_STORE: store,
74
+ GRIX_LAUNCHER_VERSION: version,
75
+ },
76
+ });
77
+ let stopping = false;
78
+ const forward = (signal) => {
79
+ stopping = true;
80
+ try { child.kill(signal); } catch { /* already gone */ }
81
+ };
82
+ const handlers = ['SIGTERM', 'SIGINT', 'SIGHUP'].map((signal) => {
83
+ const handler = () => forward(signal);
84
+ process.on(signal, handler);
85
+ return [signal, handler];
86
+ });
87
+ let settled = false;
88
+ const settle = (result) => {
89
+ if (settled) return;
90
+ settled = true;
91
+ for (const [sig, handler] of handlers) process.off(sig, handler);
92
+ resolve(result);
93
+ };
94
+ child.on('error', (error) => {
95
+ // spawn 本身失败(EMFILE/EAGAIN/入口不可执行)时 Node 不保证再发 'exit',
96
+ // 必须在这里收口,否则主循环永久挂起,既不重启也不回滚。
97
+ log(store, `spawn failed for ${version}: ${error.message}`);
98
+ settle({ code: 1, signal: null, stopping });
99
+ });
100
+ child.on('exit', (code, signal) => {
101
+ settle({ code: code ?? (signal ? 128 : 1), signal, stopping });
102
+ });
103
+ });
104
+ }
105
+
106
+ async function main() {
107
+ const store = resolveStoreDir();
108
+ const args = process.argv.slice(2);
109
+ mkdirSync(join(store, 'health'), { recursive: true });
110
+ let crashes = [];
111
+ let backoffMs = 1000;
112
+ log(store, `started node=${process.version} store=${store} template=${LAUNCHER_TEMPLATE_VERSION}`);
113
+
114
+ for (;;) {
115
+ let version = readPointer(store, 'current');
116
+ if (!version || !existsSync(entryFor(store, version))) {
117
+ const previous = readPointer(store, 'previous');
118
+ if (previous && existsSync(entryFor(store, previous))) {
119
+ log(store, `current ${version ?? '<none>'} is unusable; falling back to previous ${previous}`);
120
+ writePointer(store, 'current', previous);
121
+ version = previous;
122
+ } else {
123
+ log(store, `no runnable version (current=${version ?? '<none>'}); exiting`);
124
+ process.exit(1);
125
+ }
126
+ }
127
+
128
+ const startedAt = Date.now();
129
+ log(store, `starting ${version}`);
130
+ const { code, signal, stopping } = await runChild(store, version, args);
131
+ log(store, `daemon ${version} exited code=${code} signal=${signal ?? '-'}`);
132
+ if (stopping || signal || code === 0) process.exit(typeof code === 'number' ? code : 0);
133
+ if (code === RESTART_EXIT_CODE) {
134
+ crashes = [];
135
+ backoffMs = 1000;
136
+ continue;
137
+ }
138
+
139
+ const healthy = existsSync(join(store, 'health', `${version}.ok`));
140
+ if (!healthy) {
141
+ const now = Date.now();
142
+ crashes = crashes.filter((at) => now - at < RAPID_CRASH_WINDOW_MS);
143
+ crashes.push(now);
144
+ if (crashes.length >= RAPID_CRASH_LIMIT) {
145
+ const previous = readPointer(store, 'previous');
146
+ if (previous && previous !== version && existsSync(entryFor(store, previous))) {
147
+ log(store, `${version} crashed ${crashes.length}x without becoming healthy; rolling back to ${previous}`);
148
+ const record = {
149
+ from_version: previous,
150
+ to_version: version,
151
+ exit_code: code,
152
+ crash_count: crashes.length,
153
+ uptime_ms: now - startedAt,
154
+ rolled_back_at: new Date().toISOString(),
155
+ };
156
+ const rollbackFile = join(store, 'rollback.json');
157
+ writeFileSync(`${rollbackFile}.tmp-${process.pid}`, JSON.stringify(record), 'utf8');
158
+ renameSync(`${rollbackFile}.tmp-${process.pid}`, rollbackFile);
159
+ writePointer(store, 'current', previous);
160
+ rmSync(join(store, 'previous'), { force: true });
161
+ crashes = [];
162
+ backoffMs = 1000;
163
+ continue;
164
+ }
165
+ log(store, `${version} keeps crashing and there is no previous version to roll back to`);
166
+ }
167
+ } else {
168
+ crashes = [];
169
+ }
170
+
171
+ await sleep(backoffMs);
172
+ backoffMs = Math.min(MAX_BACKOFF_MS, backoffMs * 2);
173
+ }
174
+ }
175
+
176
+ const isDirectRun = (() => {
177
+ try {
178
+ return realpathSync(process.argv[1] ?? '') === realpathSync(fileURLToPath(import.meta.url));
179
+ } catch {
180
+ return false;
181
+ }
182
+ })();
183
+ if (isDirectRun) {
184
+ main().catch((error) => {
185
+ process.stderr.write(`launcher fatal: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
186
+ process.exit(1);
187
+ });
188
+ }
@@ -1 +0,0 @@
1
- import c from"node:http";import{randomUUID as d}from"node:crypto";import{log as o}from"../../core/log/index.js";function l(t){t.writeHead(401,{"content-type":"application/json"}),t.end(JSON.stringify({error:"unauthorized"}))}function u(t){t.writeHead(404,{"content-type":"application/json"}),t.end(JSON.stringify({error:"not_found"}))}function h(t,e){t.writeHead(400,{"content-type":"application/json"}),t.end(JSON.stringify({error:e}))}function p(t,e={ok:!0}){t.writeHead(200,{"content-type":"application/json"}),t.end(JSON.stringify(e))}async function v(t){const e=[];for await(const n of t)e.push(n);const r=Buffer.concat(e).toString("utf8").trim();return r?JSON.parse(r):{}}function k(t){const e=(t.headers.authorization??"").trim();return e.toLowerCase().startsWith("bearer ")?e.slice(7).trim():""}class w{host="127.0.0.1";port=0;token;callbacks;server=null;address=null;constructor(e){this.token=d(),this.callbacks=e}getToken(){return this.token}getURL(){return this.address?`http://${this.address.address}:${this.address.port}`:""}async start(){this.server||(this.server=c.createServer(async(e,r)=>{try{await this.handleRequest(e,r)}catch(n){h(r,n instanceof Error?n.message:String(n))}}),await new Promise((e,r)=>{this.server.once("error",r),this.server.listen(this.port,this.host,()=>{this.server.off("error",r),e()})}),this.address=this.server.address(),o.info("claude-bridge",`Bridge server listening on ${this.getURL()}`))}async stop(){if(!this.server)return;const e=this.server;this.server=null,this.address=null,e.closeIdleConnections?.(),e.closeAllConnections?.(),await new Promise((r,n)=>{e.close(s=>s?n(s):r())})}async handleRequest(e,r){if(k(e)!==this.token){l(r);return}if(e.method!=="POST"){r.writeHead(405,{"content-type":"application/json"}),r.end(JSON.stringify({error:"method_not_allowed"}));return}const n=new URL(e.url,"http://localhost").pathname,s=await v(e),i=f.get(n);if(!i){u(r);return}const a=await i(this.callbacks,s);p(r,a??{ok:!0})}}const f=new Map([["/v1/worker/register",async(t,e)=>(o.info("claude-bridge",`Worker registered: ${e.worker_id} (pid=${e.pid})`),t.onRegisterWorker(e))],["/v1/worker/status",async(t,e)=>(o.info("claude-bridge",`Worker status: ${e.status}`),t.onStatusUpdate(e))],["/v1/worker/send-text",async(t,e)=>t.onSendText(e)],["/v1/worker/send-stream-chunk",async(t,e)=>t.onSendStreamChunk(e)],["/v1/worker/send-media",async(t,e)=>t.onSendMedia(e)],["/v1/worker/delete-message",async(t,e)=>t.onDeleteMessage(e)],["/v1/worker/ack-event",async(t,e)=>t.onAckEvent(e)],["/v1/worker/event-result",async(t,e)=>t.onSendEventResult(e)],["/v1/worker/event-stop-ack",async(t,e)=>t.onSendEventStopAck(e)],["/v1/worker/event-stop-result",async(t,e)=>t.onSendEventStopResult(e)],["/v1/worker/session-composing",async(t,e)=>t.onSetSessionComposing(e)],["/v1/worker/agent-invoke",async(t,e)=>t.onAgentInvoke(e)],["/v1/worker/local-action-result",async(t,e)=>t.onLocalActionResult(e)]]);export{w as ClaudeBridgeServer};
@@ -1 +0,0 @@
1
- import{randomUUID as x}from"node:crypto";import{CallToolRequestSchema as S,ListToolsRequestSchema as w}from"@modelcontextprotocol/sdk/types.js";import{log as f}from"../../core/log/index.js";import{toolCallToInvoke as k}from"../../core/mcp/tools.js";const E=new Set(["contact_search","session_search","message_history","message_search","group_create","group_detail_read","group_leave_self","group_member_add","group_member_remove","group_member_role_update","group_all_members_muted_update","group_member_speaking_update","group_dissolve","send_msg","delete_msg","agent_api_create","agent_category_list","agent_category_create","agent_category_update","agent_category_assign","agent_api_key_rotate"]),y=3e4,I=[{name:"reply",description:"Send a visible message back to the chat for this grix-claude event.",inputSchema:{type:"object",properties:{text:{type:"string",description:"The visible reply text to send."},chat_id:{type:"string",description:"The target chat/session id from the <channel> tag."},event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},reply_to:{type:"string",description:"Optional message_id to quote instead of the inbound trigger message."},files:{type:"array",items:{type:"string"},description:"Optional absolute local file paths. Each file is uploaded through Agent API OSS presign before sending."},final:{type:"boolean",description:"Whether this is the final reply for the event. Defaults to false \u2014 the event stays open while Claude continues working, and auto-completes after inactivity. Set true only when this is definitively the last message for the event."}},required:["chat_id","event_id"]}},{name:"complete",description:"Finish an event without sending a visible reply so the backend does not time out.",inputSchema:{type:"object",properties:{event_id:{type:"string",description:"The Aibot event_id from the <channel> tag."},status:{type:"string",enum:["responded","canceled","failed"]},code:{type:"string"},msg:{type:"string"}},required:["event_id","status"]}},{name:"delete_message",description:"Delete a previously sent message in the same grix-claude chat.",inputSchema:{type:"object",properties:{chat_id:{type:"string"},message_id:{type:"string"}},required:["chat_id","message_id"]}},{name:"status",description:"Show grix-claude runtime status, upstream access state, bridge health, and startup hints.",inputSchema:{type:"object",properties:{}}},{name:"send",description:"Send a message to a chat session proactively, without requiring an inbound event. Use for notifications or scheduled reports.",inputSchema:{type:"object",properties:{chat_id:{type:"string",description:"The target chat/session id."},text:{type:"string",description:"The message text to send."}},required:["chat_id","text"]}},{name:"access_pair",description:"Forward a sender pairing approval code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"access_deny",description:"Forward a sender pairing denial code to upstream access control.",inputSchema:{type:"object",properties:{code:{type:"string"}},required:["code"]}},{name:"allow_sender",description:"Ask upstream access control to allow a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"remove_sender",description:"Ask upstream access control to remove a sender_id.",inputSchema:{type:"object",properties:{sender_id:{type:"string"}},required:["sender_id"]}},{name:"access_policy",description:"Ask upstream access control to update the sender access policy.",inputSchema:{type:"object",properties:{policy:{type:"string",enum:["allowlist","open","disabled"]}},required:["policy"]}},{name:"grix_query",description:"Search contacts, sessions, message history, or messages by keyword in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},keyword:{type:"string"},id:{type:"string"},sessionId:{type:"string"},limit:{type:"number"},offset:{type:"number"},beforeId:{type:"string"}},required:["action"]}},{name:"grix_group",description:"Manage groups in the Grix/AIBot platform: create, get details, leave, dissolve, manage members and permissions.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string"},memberIds:{type:"array",items:{type:"string"}},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer",description:"Member type (for update_member_role / update_member_speaking)."},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean",description:"Allow speaking when all muted (for update_member_speaking)."}},required:["action"]}},{name:"grix_message_send",description:"Send a message to a session in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},content:{type:"string"},msgType:{type:"number"},quotedMessageId:{type:"string"},threadId:{type:"string"}},required:["sessionId","content"]}},{name:"grix_message_unsend",description:"Recall/unsend a message in the Grix/AIBot platform.",inputSchema:{type:"object",properties:{sessionId:{type:"string"},msgId:{type:"string"}},required:["sessionId","msgId"]}},{name:"grix_admin",description:"Agent and category management in the Grix/AIBot platform: create agents, manage categories, rotate API keys.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentId:{type:"string"},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"number"}},required:["action"]}}];function q(n,e){n.setRequestHandler(w,async()=>({tools:I})),n.setRequestHandler(S,async i=>{const{name:r,arguments:t}=i.params,s=t??{};try{switch(r){case"reply":return await A(s,e);case"complete":return await $(s,e);case"delete_message":return await T(s,e);case"status":return j(e);case"send":return await M(s,e);case"access_pair":case"access_deny":case"allow_sender":case"remove_sender":case"access_policy":return await R(r,s,e);case"grix_query":case"grix_group":case"grix_message_send":case"grix_message_unsend":case"grix_admin":return await O(r,s,e);default:return{content:[{type:"text",text:`Unknown tool: ${r}`}],isError:!0}}}catch(a){return f.error("claude-tools",`Tool ${r} error: ${a}`),{content:[{type:"text",text:`Error: ${a instanceof Error?a.message:String(a)}`}],isError:!0}}})}async function A(n,e){const i=e.getActiveEvent();if(!i)return{content:[{type:"text",text:"No active event to reply to"}],isError:!0};const r=String(n.text??""),t=String(n.chat_id??""),s=String(n.event_id??i.eventId),a=n.reply_to,d=n.files,_=n.final===!0;if(!t||!s)return{content:[{type:"text",text:"reply requires chat_id and event_id"}],isError:!0};if(!r.trim()&&(!d||d.length===0))return{content:[{type:"text",text:"reply requires at least one of text or files"}],isError:!0};const{text:g,quotedMessageId:v}=e.resolveQuotedMessageId(a,r),b=[];let m=0;const h=`reply_${s}_${Date.now()}`;try{if(g){const p=e.splitText(g);for(let o=0;o<p.length;o++){if(!e.isEventActive(s))return c("ignored: event no longer active");m++,e.bridge.sendStreamChunk(s,t,p[o],++i.chunkSeq,!1,h)}}if(d&&d.length>0)for(const p of d){if(!e.isEventActive(s))return c("ignored: event no longer active");m++;const o=await e.uploadFile(p,t),l=`${x()}_${m}`;e.bridge.sendMedia(s,t,o.access_url,o.file_name,v,l,o.extra),f.info("claude-tools",`File sent: ${o.file_name}`)}e.bridge.sendStreamChunk(s,t,"",++i.chunkSeq,!0,h)}catch(p){if(g&&b.length===0)try{const o=`fallback_${s}_${Date.now()}`,l=e.splitText(g);for(let u=0;u<l.length;u++)e.bridge.sendStreamChunk("",t,l[u],u+1,!1,o);return e.bridge.sendStreamChunk("",t,"",l.length+1,!0,o),e.markReplySent(s),_&&e.finalizeEvent(s,"responded"),c("sent via fallback")}catch{}if(!e.isEventActive(s))return c("ignored: event no longer active");throw e.bridge.sendEventResult(s,"failed",String(p),"send_msg_failed"),p}return e.markReplySent(s),_?e.finalizeEvent(s,"responded"):(i.responded=!0,e.clearActiveEvent("completed")),c("Reply sent")}async function $(n,e){const i=e.getActiveEvent(),r=String(n.event_id??""),t=n.status??"",s=n.code,a=n.msg;if(!r||!t)return{content:[{type:"text",text:"complete requires event_id and status"}],isError:!0};const d=["responded","canceled","failed"];return d.includes(t)?e.isEventActive(r)?(e.bridge.sendEventResult(r,t,a,s),e.clearActiveEvent(t),c("Event completed")):c("ignored: event no longer active"):{content:[{type:"text",text:`status must be one of: ${d.join(", ")}`}],isError:!0}}async function T(n,e){const i=String(n.chat_id??""),r=String(n.message_id??"");if(!i||!r)return{content:[{type:"text",text:"chat_id and message_id are required"}],isError:!0};try{return await e.bridge.agentInvoke("grix_message_unsend",{sessionId:i,msgId:r},y),c(`deleted (${r})`)}catch(t){return{content:[{type:"text",text:`Delete failed: ${t}`}],isError:!0}}}function j(n){const e=n.getStatusInfo();return{content:[{type:"text",text:JSON.stringify({alive:e.alive,active_event:e.activeEvent,pending_approvals:e.pendingPermissions,pending_questions:e.pendingElicitations})}]}}async function M(n,e){const i=String(n.chat_id??""),r=String(n.text??"");if(!i||!r)return{content:[{type:"text",text:"chat_id and text are required"}],isError:!0};try{const t=e.splitText(r),s=`send_${i}_${Date.now()}`;for(let a=0;a<t.length;a++)e.bridge.sendStreamChunk("",i,t[a],a+1,!1,s);return e.bridge.sendStreamChunk("",i,"",t.length+1,!0,s),c("sent")}catch(t){return{content:[{type:"text",text:`Send failed: ${t}`}],isError:!0}}}const C={access_pair:{verb:"pair_approve",payloadKey:"code"},access_deny:{verb:"pair_deny",payloadKey:"code"},allow_sender:{verb:"sender_allow",payloadKey:"sender_id"},remove_sender:{verb:"sender_remove",payloadKey:"sender_id"},access_policy:{verb:"policy_set",payloadKey:"policy"}};async function R(n,e,i){try{const r=C[n];if(!r)throw new Error(`Unknown access control tool: ${n}`);const t={};e.code!=null&&(t.code=e.code),e.sender_id!=null&&(t.sender_id=e.sender_id),e.policy!=null&&(t.policy=e.policy);const s=await i.bridge.agentInvoke("claude_access_control",{verb:r.verb,payload:t},y);return{content:[{type:"text",text:typeof s=="string"?s:JSON.stringify(s)}]}}catch(r){return{content:[{type:"text",text:`${n} failed: ${r}`}],isError:!0}}}async function O(n,e,i){try{const r=k(n,e);if(!E.has(r.action))throw new Error(`Action not allowed: ${r.action}`);const t=await i.bridge.agentInvoke(r.action,r.params,y);if(t&&Number(t.code??0)!==0)throw new Error(String(t.msg??"invoke failed"));return{content:[{type:"text",text:t?.data!=null?typeof t.data=="string"?t.data:JSON.stringify(t.data):JSON.stringify(t)}]}}catch(r){return{content:[{type:"text",text:`${n} failed: ${r}`}],isError:!0}}}function c(n){return{content:[{type:"text",text:n}]}}export{q as registerClaudeTools};
@@ -1 +0,0 @@
1
- import{log as l}from"../../core/log/index.js";class c{controlURL="";token="";isConfigured(){return!!(this.controlURL&&this.token)}configure(t,e){this.controlURL=t.replace(/\/+$/,""),this.token=e.trim(),l.info("claude-worker-client",`Configured with control URL: ${this.controlURL}`)}async post(t,e,s){if(!this.isConfigured())throw new Error("worker control not configured");const i=new AbortController,o=setTimeout(()=>i.abort(),s);try{const r=await fetch(`${this.controlURL}${t}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${this.token}`},body:JSON.stringify(e),signal:i.signal}),n=await r.text(),a=n.trim()?JSON.parse(n):{};if(!r.ok)throw new Error(a.error||`worker control failed ${r.status}`);return a}finally{clearTimeout(o)}}isRetryableError(t){const e=t instanceof Error?t.message:String(t);return/fetch failed|network|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|aborted/i.test(e)}async postWithRetry(t,e,s,i=1){let o;for(let r=0;r<=i;r++)try{return r>0&&l.info("claude-worker-client",`Retrying ${t} attempt=${r+1}`),await this.post(t,e,s)}catch(n){if(o=n,r>=i||!this.isRetryableError(n))break;await new Promise(a=>setTimeout(a,150))}throw o instanceof Error?o:new Error(String(o))}async deliverEvent(t){return this.postWithRetry("/v1/worker/deliver-event",{payload:t},1e4,1)}async deliverStop(t){return this.postWithRetry("/v1/worker/deliver-stop",{payload:t},1e4,1)}async deliverLocalAction(t){return this.postWithRetry("/v1/worker/deliver-local-action",{payload:t},1e4,1)}async ping(){try{return await this.postWithRetry("/v1/worker/ping",{},5e3,1),!0}catch{return!1}}}export{c as ClaudeWorkerClient};
@@ -1,2 +0,0 @@
1
- import{spawn as x,execSync as y}from"node:child_process";import{randomUUID as v}from"node:crypto";import{mkdir as S}from"node:fs/promises";import{readFileSync as C}from"node:fs";import{join as d}from"node:path";import{homedir as T,tmpdir as I}from"node:os";import{log as o}from"../../core/log/index.js";import{MCP_HTTP_CHANNEL_NAME as l}from"./protocol-contract.js";function P(e){let t=null,r=0,n=!1,i=!1;const a=v(),s=e.gatewayUrl??"http://127.0.0.1:19580/mcp";return{async start(){await F(e.command,s,e.env);const c=E(e.grix),u=[...e.args??[],"--name",`grix-mcp-${e.name}`,"--session-id",a];e.fullAuto&&u.push("--dangerously-skip-permissions"),u.push("--dangerously-load-development-channels",`server:${l}`,"--append-system-prompt",c);const f=d(I(),`grix-mcp-claude-${e.name}`);await S(f,{recursive:!0});const{expectPath:$,pidPath:g}=await M(f,e.command,u),_={...process.env,...e.env??{}};t=x("/usr/bin/expect",[$],{cwd:e.cwd,env:_,stdio:["ignore","pipe","pipe"],detached:!0}),o.info("mcp-http-launcher",`\u542F\u52A8 Claude: name=${e.name} cwd=${e.cwd} pid=${t.pid}`),r=await k(g),n=!0,o.info("mcp-http-launcher",`Claude \u5B50\u8FDB\u7A0B PID: ${r}`),t.on("exit",(m,p)=>{o.info("mcp-http-launcher",`Claude \u9000\u51FA: code=${m} signal=${p}`),n=!1,t=null,r=0,i||(o.info("mcp-http-launcher","3 \u79D2\u540E\u81EA\u52A8\u91CD\u542F..."),setTimeout(()=>{i||this.start().catch(w=>{o.error("mcp-http-launcher",`\u91CD\u542F\u5931\u8D25: ${w}`)})},3e3))}),t.stdout?.on("data",m=>{const p=m.toString().trim();p&&o.info("mcp-http-launcher",`[stdout] ${p.slice(0,300)}`)}),t.stderr?.on("data",m=>{const p=m.toString().trim();p&&o.info("mcp-http-launcher",`[stderr] ${p.slice(0,300)}`)})},async stop(){if(i=!0,n=!1,r>0)try{process.kill(r,"SIGTERM")}catch{}if(t?.pid){try{process.kill(-t.pid,"SIGTERM")}catch{}await new Promise(c=>{const u=setTimeout(()=>{if(r>0)try{process.kill(r,"SIGKILL")}catch{}if(t?.pid)try{process.kill(-t.pid,"SIGKILL")}catch{}c()},5e3);t?.once("exit",()=>{clearTimeout(u),c()})})}t=null,r=0},getStatus(){return{name:e.name,alive:n,pid:r}}}}function E(e){return["You are connected to a chat via the grix MCP server.",`On startup, immediately call grix_authorize with: agentId="${e.agentId}", apiKey="${e.apiKey}", wsUrl="${e.wsUrl}", clientType="${e.clientType}".`,"When you receive a <channel> message, you MUST respond by calling the grix_reply tool (or the grix_complete tool if no response is needed).","Never write your reply as plain text \u2014 it will NOT reach the user. Only the grix_reply tool delivers your response to the chat.","The <channel> message contains event_id and session_id \u2014 pass them to grix_reply."].join(" ")}async function F(e,t,r){const n=d(T(),".claude.json");let i=null;try{const s=C(n,"utf8");i=JSON.parse(s)?.mcpServers?.[l]??null}catch{}if(i&&String(i.type??"").trim()==="http"&&String(i.url??"").trim()===t)return;o.info("mcp-http-launcher",`\u6CE8\u518C MCP Server: ${l} -> ${t}`);const a={...process.env,...r??{}};try{y(`${e} mcp remove -s user ${l}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}catch{}y(`${e} mcp add --scope user --transport http ${l} ${t}`,{encoding:"utf8",timeout:1e4,env:a,stdio:"pipe"})}async function M(e,t,r){const{writeFile:n}=await import("node:fs/promises"),i=d(e,"claude.pid"),a=d(e,"claude.expect"),s=["log_user 1","set timeout -1","set startup_prompt_armed 1",`set claude_command [list {${h(t)}}${r.map(c=>` {${h(c)}}`).join("")}]`,"spawn -noecho {*}$claude_command",`set pid_file [open {${h(i)}} w]`,"puts $pid_file [exp_pid -i $spawn_id]","close $pid_file","expect {"," -re {(?i)(Quick.*safety.*check|trust.*folder)} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)I am using this for local development} {",' if {$startup_prompt_armed} { send -- "1\\r"; after 300 }; exp_continue'," }"," -re {(?i)(Enter.*confirm|Press.*Enter|Hit.*Enter)} {",' if {$startup_prompt_armed} { send -- "\\r"; after 300 }; exp_continue'," }"," -re {Listening for channel} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," -re {bypass permissions} {"," set startup_prompt_armed 0"," after 1000",' send -- "Call grix_authorize now as instructed in your system prompt.\\r"'," }"," eof {}","}","expect eof",""];return await n(i,"","utf8"),await n(a,s.join(`
2
- `),"utf8"),{expectPath:a,pidPath:i}}function h(e){return e.replace(/[\\{}$\[\]"]/g,"\\$&")}async function k(e,t=1e4){const{readFile:r}=await import("node:fs/promises"),n=Math.ceil(t/100);for(let i=0;i<n;i++){try{const a=await r(e,"utf8"),s=parseInt(String(a).trim(),10);if(Number.isFinite(s)&&s>0)return s}catch{}await new Promise(a=>setTimeout(a,100))}return 0}export{P as createMcpHttpLauncher};
@@ -1 +0,0 @@
1
- class m{defaultTimeoutMs;onTimeout;timers=new Map;constructor(t){this.defaultTimeoutMs=t.defaultTimeoutMs??9e4,this.onTimeout=t.onTimeout}arm(t,e){this.cancel(t);const s=e?.timeoutMs??this.defaultTimeoutMs,i=Date.now()+s,o=setTimeout(()=>{this.timers.delete(t),this.onTimeout(t).catch(()=>{})},s);return this.timers.set(t,o),i}cancel(t){const e=this.timers.get(t);e&&(clearTimeout(e),this.timers.delete(t))}has(t){return this.timers.has(t)}close(){for(const t of this.timers.values())clearTimeout(t);this.timers.clear()}}export{m as ResultTimeoutManager};