aws-runtime-bridge 1.9.162 → 1.9.169

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 (123) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/dist/adapter/AcodeSdkAdapter.d.ts +31 -3
  3. package/dist/adapter/AcodeSdkAdapter.js +4 -9
  4. package/dist/adapter/ClaudeSdkAdapter.d.ts +2 -0
  5. package/dist/adapter/ClaudeSdkAdapter.js +3 -3
  6. package/dist/adapter/CodexSdkAdapter.d.ts +2 -0
  7. package/dist/adapter/CodexSdkAdapter.js +3 -3
  8. package/dist/adapter/OpencodeSdkAdapter.d.ts +2 -0
  9. package/dist/adapter/OpencodeSdkAdapter.js +3 -3
  10. package/dist/adapter/prompts.d.ts +1 -1
  11. package/dist/adapter/prompts.js +1 -1
  12. package/dist/adapter/types.d.ts +41 -4
  13. package/dist/adapter/types.js +1 -1
  14. package/dist/browser/frame-encode.d.ts +25 -0
  15. package/dist/browser/frame-encode.js +2 -0
  16. package/dist/browser/screencast-differ.d.ts +2 -3
  17. package/dist/browser/screencast-differ.js +1 -1
  18. package/dist/browser/screencast-manager.js +1 -1
  19. package/dist/browser/types.d.ts +5 -1
  20. package/dist/browser/types.js +1 -1
  21. package/dist/desktop/desktop-mcp-client.js +2 -2
  22. package/dist/routes/background-tasks.js +3 -3
  23. package/dist/routes/capabilities.js +1 -1
  24. package/dist/routes/dashboard.d.ts +2 -0
  25. package/dist/routes/dashboard.js +4 -4
  26. package/dist/routes/git.js +22 -22
  27. package/dist/routes/instance.js +1 -1
  28. package/dist/routes/launch.js +10 -10
  29. package/dist/routes/mcp-extra.js +2 -2
  30. package/dist/routes/mcp.d.ts +4 -0
  31. package/dist/routes/mcp.js +1 -1
  32. package/dist/routes/plugins.js +1 -1
  33. package/dist/routes/pty.d.ts +11 -3
  34. package/dist/routes/pty.js +3 -3
  35. package/dist/routes/runtime-binding.js +1 -1
  36. package/dist/routes/sessions.js +1 -1
  37. package/dist/routes/skills.js +1 -1
  38. package/dist/routes/sub-agents.js +8 -8
  39. package/dist/routes/terminal.d.ts +16 -2
  40. package/dist/routes/terminal.js +34 -36
  41. package/dist/services/agent-process-manager.js +1 -1
  42. package/dist/services/agent-pty-manager.js +4 -4
  43. package/dist/services/aws-client-agent-mcp.js +2 -2
  44. package/dist/services/capability-config.d.ts +8 -0
  45. package/dist/services/capability-config.js +2 -2
  46. package/dist/services/desktop-mcp-manager.d.ts +3 -1
  47. package/dist/services/desktop-mcp-manager.js +1 -1
  48. package/dist/services/instance-ai-config-service.js +1 -1
  49. package/dist/services/instance-features.d.ts +10 -0
  50. package/dist/services/instance-features.js +1 -1
  51. package/dist/services/instance-init-service.js +1 -1
  52. package/dist/services/launch-manager.d.ts +9 -1
  53. package/dist/services/launch-manager.js +6 -6
  54. package/dist/services/launch-metrics.d.ts +1 -1
  55. package/dist/services/launch-metrics.js +4 -3
  56. package/dist/services/mcp-deploy.d.ts +4 -0
  57. package/dist/services/mcp-env-injector.d.ts +12 -0
  58. package/dist/services/mcp-env-injector.js +2 -0
  59. package/dist/services/mcp-launch-binding-queue.d.ts +8 -0
  60. package/dist/services/mcp-launch-binding-queue.js +1 -1
  61. package/dist/services/orphan-monitor.js +1 -1
  62. package/dist/services/panel-env.d.ts +6 -2
  63. package/dist/services/panel-env.js +1 -1
  64. package/dist/services/plugin-manager.js +9 -9
  65. package/dist/services/process-detector.d.ts +23 -3
  66. package/dist/services/process-detector.js +13 -13
  67. package/dist/services/runtime-resources-snapshot.d.ts +3 -0
  68. package/dist/services/runtime-resources-snapshot.js +3 -0
  69. package/dist/services/session-output.d.ts +13 -0
  70. package/dist/services/session-output.js +4 -4
  71. package/dist/services/tunnel-client.js +1 -1
  72. package/dist/services/workspace-files.js +1 -1
  73. package/dist/types.d.ts +4 -0
  74. package/dist/utils/env-safety.d.ts +15 -0
  75. package/dist/utils/env-safety.js +2 -0
  76. package/dist/utils/lifecycle-log.d.ts +2 -0
  77. package/dist/utils/lifecycle-log.js +3 -0
  78. package/dist/utils/mcp-utils.js +1 -1
  79. package/package/acode/dist/background-command-manager.js +4 -4
  80. package/package/acode/dist/built-in-file-tools.d.ts +13 -0
  81. package/package/acode/dist/built-in-file-tools.js +2 -2
  82. package/package/acode/dist/builtins/shellCommandPreparation.d.ts +2 -0
  83. package/package/acode/dist/builtins/shellCommandPreparation.js +2 -2
  84. package/package/acode/dist/compaction.d.ts +10 -0
  85. package/package/acode/dist/compaction.js +6 -6
  86. package/package/acode/dist/context-overflow.js +1 -1
  87. package/package/acode/dist/index.d.ts +1 -0
  88. package/package/acode/dist/index.js +1 -1
  89. package/package/acode/dist/mcpToolNames.d.ts +4 -0
  90. package/package/acode/dist/mcpToolNames.js +1 -1
  91. package/package/acode/dist/runtime-environment.d.ts +2 -0
  92. package/package/acode/dist/runtime-environment.js +3 -2
  93. package/package/acode/dist/runtime.d.ts +68 -8
  94. package/package/acode/dist/runtime.js +28 -27
  95. package/package/acode/dist/sub-agent-manager.js +1 -1
  96. package/package/acode/dist/tool-allocation.js +1 -1
  97. package/package/acode/dist/tool-group-manager.d.ts +12 -0
  98. package/package/acode/dist/tool-group-manager.js +8 -4
  99. package/package/acode/dist/types.d.ts +8 -0
  100. package/package/acode/package.json +1 -1
  101. package/package/aws-client-agent-mcp/README.md +1 -0
  102. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +22 -8
  103. package/package/aws-client-agent-mcp/dist/agent-client.js +1 -1
  104. package/package/aws-client-agent-mcp/dist/http-client.d.ts +10 -0
  105. package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
  106. package/package/aws-client-agent-mcp/dist/logger.d.ts +2 -1
  107. package/package/aws-client-agent-mcp/dist/logger.js +2 -2
  108. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +19 -2
  109. package/package/aws-client-agent-mcp/dist/mcp-server.js +18 -18
  110. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +15 -4
  111. package/package/aws-client-agent-mcp/dist/mcp-tools.js +79 -29
  112. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +2 -0
  113. package/package/aws-client-agent-mcp/dist/memory-store.js +3 -3
  114. package/package/aws-client-agent-mcp/dist/message-buffer.js +1 -1
  115. package/package/aws-client-agent-mcp/dist/office/office-workspace.d.ts +15 -0
  116. package/package/aws-client-agent-mcp/dist/office/office-workspace.js +1 -1
  117. package/package/aws-client-agent-mcp/dist/status-reporter.js +1 -1
  118. package/package/aws-client-agent-mcp/dist/tool-groups.d.ts +4 -0
  119. package/package/aws-client-agent-mcp/dist/tool-groups.js +2 -2
  120. package/package/aws-client-agent-mcp/dist/types.d.ts +52 -1
  121. package/package/aws-client-agent-mcp/dist/websocket-client.js +1 -1
  122. package/package/aws-client-agent-mcp/package.json +1 -1
  123. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import{execFile as $}from"node:child_process";import v from"node:fs";import I from"node:os";import K from"node:path";import{Router as Y}from"express";import{getBridgeVersion as Q,getRuntimeHomeDir as X,getUserHomeDir as Z}from"../config.js";import{getAccessControlEntries as ee,isValidIpEntry as te,parseAllowIps as ne,refreshAccessControlCache as se}from"../services/access-control.js";import{getAgentProcessManager as C}from"../services/agent-process-manager.js";import{autoRegister as re,getRegistrationState as H,getAutoRegisterConfigFilePath as B}from"../services/auto-register.js";import{createSession as ae,getPanelCredentials as N,validateSession as U,destroySession as O,validateCredentials as z,hasConfiguredCredentials as E,isDefaultCredentials as oe,setPanelCredentials as ie,getAllSessions as de,getActiveSessionCount as le,checkLoginAllowed as ce,recordLoginFailure as ue,recordLoginSuccess as ge}from"../services/panel-auth.js";import{getRuntimeBindingPublicState as F,getRuntimePairingCode as pe,normalizeSchedulerBaseUrl as fe}from"../services/runtime-binding.js";import{collectSystemMetrics as me}from"../services/system-metrics.js";import{loadPersistedSessions as be}from"../services/terminal-persistence.js";import{runExclusiveConfigWrite as _}from"../utils/config-write-queue.js";import{createLogger as he,getRecentLogs as ve}from"../utils/logger.js";import{sdkSessions as ye}from"./terminal.js";const x=he("dashboard");let T=null;const xe=1e4;let L=null,P=null;const we=3e4;async function D(){me().then(t=>{L={data:t,ts:Date.now()}}).catch(()=>{}),C().discoverAllProcesses().then(t=>{P={data:t,ts:Date.now()}}).catch(()=>{}),C().cleanupStaleRegistry().catch(()=>{})}D(),setInterval(D,we);const V=Q();async function Se(){const t=await import("node:fs/promises"),n=(await import("node:path")).join(X(),".aws-bridge","lifecycle-state.json");try{const s=await t.readFile(n,"utf8"),i=JSON.parse(s),r=Array.isArray(i.operations)?i.operations:[];return r.length===0?null:(r.sort((a,o)=>new Date(o.updatedAt).getTime()-new Date(a.updatedAt).getTime()),r[0]||null)}catch{return null}}const g=Y(),J="panel_sid";function Ie(t){const e={};if(!t)return e;for(const n of t.split(";")){const s=n.indexOf("=");if(s===-1)continue;const i=n.slice(0,s).trim(),r=n.slice(s+1).trim();i&&(e[i]=r)}return e}function A(t){return Ie(t.headers.cookie)[J]}function q(t,e,n){const s=t.secure||t.headers["x-forwarded-proto"]==="https";return`${J}=${e}; HttpOnly; SameSite=Lax; Path=/${s?"; Secure":""}; Max-Age=${n}`}function m(t,e){const n=A(t);return n&&U(n)?!0:t.xhr||t.headers.accept?.includes("application/json")?(e.status(401).json({ok:!1,error:"unauthorized",needLogin:!0}),!1):(e.redirect("/login"),!1)}g.post("/runtime/login",(t,e)=>{const n=t.socket.remoteAddress||"unknown",s=ce(n);if(!s.allowed){e.status(429).set("Retry-After",String(s.retryAfterSec)).json({ok:!1,error:"too many failed login attempts, try again later",retryAfterSec:s.retryAfterSec});return}const{username:i,password:r}=t.body||{};if(!i||!r){e.status(400).json({ok:!1,error:"username and password required"});return}if(!z(String(i),String(r))){ue(n),e.status(401).json({ok:!1,error:"invalid credentials"});return}ge(n);const a=ae(),o=!E();e.setHeader("Set-Cookie",q(t,a,86400)),e.json({ok:!0,needSetup:o})}),g.post("/runtime/logout",(t,e)=>{const n=A(t);n&&O(n),e.setHeader("Set-Cookie",q(t,"",0)),e.json({ok:!0})}),g.get("/runtime/credentials-status",(t,e)=>{if(!m(t,e))return;const n=N();e.json({ok:!0,isDefault:oe(),configured:E(),username:n.username})}),g.post("/runtime/change-credentials",async(t,e)=>{if(!m(t,e))return;const{username:n,password:s,passwordConfirm:i,oldPassword:r}=t.body||{};if(s&&String(s).trim()){if(String(s)!==String(i)){e.status(400).json({ok:!1,error:"passwords do not match"});return}if(E()){if(!r){e.status(400).json({ok:!1,error:"old password required"});return}const o=N();if(!z(o.username,String(r))){e.status(403).json({ok:!1,error:"old password is incorrect"});return}}}if(!n||!String(n).trim()){e.status(400).json({ok:!1,error:"username required"});return}const a=s&&String(s).trim()?String(s).trim():N().password;try{await ie(String(n).trim(),a),e.json({ok:!0})}catch(o){const l=o;x.error("Failed to change panel credentials:",l.message),e.status(500).json({ok:!1,error:l.message})}});function W(t){const e=typeof t=="string"?t.trim():"";if(e)return e.length<8?"\u2022\u2022\u2022\u2022":"\u2022\u2022\u2022\u2022"+e.slice(-4)}function G(t){const e=Array.isArray(t.autoRegisterTargets)?t.autoRegisterTargets.filter(s=>!!(s&&typeof s=="object"&&!Array.isArray(s))):[];return e.length>0?e:["serverUrl","schedulerBaseUrl","userKey"].some(s=>!!String(t[s]??"").trim())?[t]:[]}const ke=["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","projectId","roleName","workspacePath","registerAddr","registerIp","virtualIp","tunnel","preferSameLanIp","maxRetries","retryInterval"];function Ce(t){const e={};for(const n of ke)t[n]!==void 0&&(e[n]=t[n]);return e}function Be(t,e){const n=String(t.connectionKey??"").trim()||String(e??"").trim();return{serverUrl:String(t.serverUrl??t.schedulerBaseUrl??"").trim(),tenantId:String(t.tenantId??"").trim(),instanceName:String(t.instanceName??"").trim(),instanceId:String(t.instanceId??"").trim()||void 0,registerAddr:String(t.registerAddr??"").trim(),tunnel:typeof t.tunnel=="boolean"?t.tunnel:void 0,userKeyMasked:W(t.userKey),userKeySet:!!String(t.userKey??"").trim(),connectionKeyMasked:W(n),connectionKeySet:!!n}}function R(t){return fe(t)??String(t||"").trim()}g.get("/runtime/server-config",(t,e)=>{if(!m(t,e))return;const n=B();let s={};if(v.existsSync(n))try{s=JSON.parse(v.readFileSync(n,"utf-8"))}catch{e.status(500).json({ok:!1,error:"config.json is not valid JSON; fix it manually first"});return}const i=String(s.connectionKey??"").trim()||void 0,r=G(s).map(a=>Be(a,i));e.json({ok:!0,targets:r})});function M(t,e){const n=new Error(e);return n.status=t,n}g.post("/runtime/server-config",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).targets;if(!Array.isArray(n)){e.status(400).json({ok:!1,error:"targets array is required"});return}const s=[];for(let r=0;r<n.length;r++){const a=n[r];if(!a||typeof a!="object"||Array.isArray(a)){e.status(400).json({ok:!1,error:`targets[${r}] must be an object`});return}const o=a,l=String(o.serverUrl??o.schedulerBaseUrl??"").trim();if(!l){e.status(400).json({ok:!1,error:`targets[${r}].serverUrl is required`});return}try{const d=new URL(l);if(!["http:","https:"].includes(d.protocol))throw new Error("protocol must be http or https")}catch{e.status(400).json({ok:!1,error:`targets[${r}].serverUrl must be a valid http(s) URL`});return}const p=o.tunnel;let f;if(p==="auto"||p===""||p===null||p===void 0)f=void 0;else if(p===!0||p==="true")f=!0;else if(p===!1||p==="false")f=!1;else{e.status(400).json({ok:!1,error:`targets[${r}].tunnel must be auto, true or false`});return}const h=String(o.originalServerUrl??"").trim();s.push({serverUrl:l,originalServerUrl:h&&h!==l?h:"",userKey:String(o.userKey??"").trim(),tenantId:String(o.tenantId??"").trim(),instanceName:String(o.instanceName??"").trim(),connectionKey:String(o.connectionKey??"").trim(),registerAddr:String(o.registerAddr??"").trim(),tunnel:f})}const i=new Set;for(const r of s){const a=R(r.serverUrl);if(i.has(a)){e.status(400).json({ok:!1,error:"duplicate serverUrl in targets"});return}i.add(a)}try{const r=B();await _(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw M(500,"config.json is not valid JSON; fix it manually first")}const o=String(a.connectionKey??"").trim(),l=G(a),p=l.length>0&&l[0]===a,f=new Map(l.map(d=>[R(String(d.serverUrl??d.schedulerBaseUrl??"").trim()),p?Ce(d):d])),h=[];for(const d of s){let b;if(d.originalServerUrl){const w=R(d.originalServerUrl);b=f.get(w),b&&f.delete(w)}if(!b){const w=R(d.serverUrl);b=f.get(w),b&&f.delete(w)}const c=b?{...b}:{};if(c.serverUrl=d.serverUrl,c.tenantId=d.tenantId,c.instanceName=d.instanceName,c.registerAddr=d.registerAddr,d.tunnel===void 0?delete c.tunnel:c.tunnel=d.tunnel,d.userKey)c.userKey=d.userKey;else if(!String(c.userKey??"").trim())throw M(400,`userKey is required for ${d.serverUrl} (new target without inheritable userKey)`);d.connectionKey?c.connectionKey=d.connectionKey:!String(c.connectionKey??"").trim()&&o&&(c.connectionKey=o),h.push(c)}a.autoRegisterTargets=h;for(const d of["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","registerAddr","registerIp","virtualIp","tunnel"])delete a[d];v.mkdirSync(K.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8")}),x.info("Server connection config updated via panel",{configPath:r,targetCount:s.length}),e.json({ok:!0})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save server connection config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),g.get("/runtime/access-control",(t,e)=>{m(t,e)&&e.json({ok:!0,allowIps:ee(),defaultAllowIps:["0.0.0.0"]})}),g.post("/runtime/access-control",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).allowIps;if(typeof n!="string"&&!Array.isArray(n)){e.status(400).json({ok:!1,error:"allowIps must be a string or an array"});return}const s=ne(n),i=s.filter(r=>!te(r));if(i.length>0){e.status(400).json({ok:!1,error:`invalid IP/CIDR entries: ${i.join(", ")}`});return}try{const r=B();await _(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw M(500,"config.json is not valid JSON; fix it manually first")}const o=a.accessControl&&typeof a.accessControl=="object"&&!Array.isArray(a.accessControl)?{...a.accessControl}:{};o.allowIps=s,a.accessControl=o,v.mkdirSync(K.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8"),se()}),x.info("Access control config updated via panel",{configPath:r,entryCount:s.length}),e.json({ok:!0,allowIps:s})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save access control config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),g.get("/runtime/node-info",(t,e)=>{if(!m(t,e))return;const n=F(),s=H();e.json({ok:!0,hostname:I.hostname(),platform:I.platform(),arch:I.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version,bridgeVersion:V,configPath:B(),homeDir:Z(),bindingStatus:n.status,paired:n.paired,instanceId:n.instanceId||s.instanceId,schedulerBaseUrl:n.schedulerBaseUrl,registered:s.registered,pairingCode:n.pairingCode,activeSessions:le()})}),g.get("/runtime/logs",(t,e)=>{if(!m(t,e))return;const n=t.query.level||void 0,s=Math.min(Number(t.query.count)||200,500),r=n&&["debug","info","warn","error"].includes(n)?n:void 0,a=ve(s,r);e.json({ok:!0,logs:a})}),g.post("/runtime/re-register",async(t,e)=>{if(m(t,e))try{const n=await re();e.json({ok:n})}catch(n){const s=n;e.status(500).json({ok:!1,error:s.message})}}),g.get("/runtime/admin/sessions",(t,e)=>{if(!m(t,e))return;const n=de().map(s=>({token:s.token.slice(0,8)+"...",createdAt:new Date(s.createdAt).toISOString(),lastUsedAt:new Date(s.lastUsedAt).toISOString(),age:Math.floor((Date.now()-s.createdAt)/1e3)}));e.json({ok:!0,sessions:n})}),g.post("/runtime/admin/sessions/:token/destroy",(t,e)=>{m(t,e)&&(O(t.params.token),e.json({ok:!0}))});function Ee(t){return new Promise((e,n)=>{const s=I.platform()==="win32",i=s?"taskkill":"kill",r=s?["/PID",String(t),"/F"]:["-9",String(t)];$(i,r,{timeout:5e3},a=>a?n(a):e())})}g.post("/runtime/kill-process",async(t,e)=>{if(!m(t,e))return;const n=Number(t.body.pid);if(!n||!Number.isInteger(n)||n<=0){e.status(400).json({ok:!1,error:"Invalid PID"});return}const s=C(),i=()=>{const a=new Set,o=P?.data??[];for(const l of o)typeof l.pid=="number"&&a.add(l.pid);for(const l of s.getAllManagedProcesses())typeof l.pid=="number"&&a.add(l.pid);return a};let r=i();if(r.size===0)try{P={data:await s.discoverAllProcesses(),ts:Date.now()},r=i()}catch{}if(!r.has(n)){x.warn("Rejected kill-process for unrecognized PID",{pid:n}),e.status(404).json({ok:!1,error:"PID is not a bridge-discovered or registered agent process"});return}try{await Ee(n),x.info("Killed process",{pid:n}),e.json({ok:!0})}catch(a){const o=a;x.warn("Failed to kill process",{pid:n,error:o.message}),e.status(500).json({ok:!1,error:o.message})}}),g.get("/runtime/dashboard",async(t,e)=>{if(m(t,e)){if(T&&Date.now()-T.ts<xe)return void e.json(T.data);try{const n=L?.data??null,s=P?.data??null;L||D();const[i,r,a]=await Promise.all([Promise.resolve(F()),Promise.resolve(H()),be().catch(()=>[])]),l=C().getAllManagedProcesses(),p=Array.from(ye.entries()).map(([S,y])=>({sessionId:S,agentId:y.agentId,workspacePath:y.workspacePath,runtimeStatus:y.currentRuntimeStatus||"running",runtimeActionType:y.currentRuntimeActionType,runtimeActionLabel:y.currentRuntimeActionLabel,providerId:y.providerId}));let f={managed:0,orphan:0,unknown:0},h=[];if(s){f={managed:s.filter(u=>u.managementStatus==="managed").length,orphan:s.filter(u=>u.managementStatus==="orphan").length,unknown:s.filter(u=>u.managementStatus==="unknown").length};const S=new Map(l.map(u=>[u.pid,u]));h=s.map(u=>{const k=S.get(u.pid);return{agentId:u.agentId||k?.agentId,pid:u.pid,managementStatus:u.managementStatus,sessionId:k?.sessionId,state:k?.state,command:u.command,cwd:u.cwd,startedAt:u.startedAt}});const y={managed:0,orphan:1,unknown:2};h.sort((u,k)=>(y[u.managementStatus]??9)-(y[k.managementStatus]??9))}const d=new Set(p.map(S=>S.sessionId)),b=a.filter(S=>!d.has(S.sessionId)),c=i,w=await Se(),j={ok:!0,collectedAt:new Date().toISOString(),bridge:{version:V,hostname:I.hostname(),platform:I.platform(),arch:I.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version},binding:{status:c.status,paired:c.paired,instanceId:c.instanceId,userId:c.userId,schedulerBaseUrl:c.schedulerBaseUrl,pairingCode:pe()},registration:{registered:r.registered,instanceId:r.instanceId,lastAttempt:r.lastAttempt?.toISOString(),error:r.error},metrics:n,sessions:{active:p,activeCount:p.length,persisted:b,persistedCount:b.length,total:p.length+b.length},processes:{...f,total:f.managed+f.orphan+f.unknown,list:h,registryCount:l.length},lifecycleOperation:w};T={data:j,ts:Date.now()},e.json(j)}catch(n){const s=n;x.error("Dashboard aggregation failed:",s.message),e.status(500).json({ok:!1,error:s.message})}}}),g.get("/login",(t,e)=>{e.type("text/html; charset=utf-8"),e.send(Pe)}),g.get("/",(t,e)=>{const n=A(t);if(!n||!U(n)){e.redirect("/login");return}if(!E()){e.redirect("/setup");return}const s=t.query.lang==="en"?"en":"zh";e.type("text/html; charset=utf-8"),e.send(Te.replace("{{LANG}}",s))}),g.get("/setup",(t,e)=>{const n=A(t);if(!n||!U(n)){e.redirect("/login");return}e.type("text/html; charset=utf-8"),e.send(Ae)});const Te=`<!DOCTYPE html>
1
+ import{execFile as X}from"node:child_process";import v from"node:fs";import k from"node:os";import O from"node:path";import{Router as Q}from"express";import{getBridgeVersion as Z,getRuntimeHomeDir as ee,getUserHomeDir as te}from"../config.js";import{getAccessControlEntries as ne,isValidIpEntry as se,parseAllowIps as re,refreshAccessControlCache as ae}from"../services/access-control.js";import{getAgentProcessManager as E}from"../services/agent-process-manager.js";import{autoRegister as oe,getRegistrationState as z,getAutoRegisterConfigFilePath as T}from"../services/auto-register.js";import{createSession as ie,getPanelCredentials as L,validateSession as D,destroySession as F,validateCredentials as _,hasConfiguredCredentials as P,isDefaultCredentials as de,setPanelCredentials as le,getAllSessions as ce,getActiveSessionCount as ue,checkLoginAllowed as ge,recordLoginFailure as pe,recordLoginSuccess as fe}from"../services/panel-auth.js";import{getRuntimeBindingPublicState as V,getRuntimePairingCode as me,normalizeSchedulerBaseUrl as be}from"../services/runtime-binding.js";import{collectSystemMetrics as he}from"../services/system-metrics.js";import{loadPersistedSessions as ve}from"../services/terminal-persistence.js";import{runExclusiveConfigWrite as J}from"../utils/config-write-queue.js";import{createLogger as ye,getRecentLogs as xe}from"../utils/logger.js";import{sdkSessions as we}from"./terminal.js";const x=ye("dashboard");let A=null;const Se=1e4;let M=null,R=null;const Ie=3e4;async function j(){he().then(t=>{M={data:t,ts:Date.now()}}).catch(()=>{}),E().discoverAllProcesses().then(t=>{R={data:t,ts:Date.now()}}).catch(()=>{}),E().cleanupStaleRegistry().catch(()=>{})}const ke=12e4;let C=null,w=null;function Ce(){w&&(clearTimeout(w),w=null),w=setTimeout(Be,ke),w.unref?.(),C===null&&(C=setInterval(j,Ie),C.unref?.(),j())}function Be(){C&&(clearInterval(C),C=null),w&&(clearTimeout(w),w=null)}const W=Z();async function Ee(){const t=await import("node:fs/promises"),n=(await import("node:path")).join(ee(),".aws-bridge","lifecycle-state.json");try{const s=await t.readFile(n,"utf8"),i=JSON.parse(s),r=Array.isArray(i.operations)?i.operations:[];return r.length===0?null:(r.sort((a,o)=>new Date(o.updatedAt).getTime()-new Date(a.updatedAt).getTime()),r[0]||null)}catch{return null}}const c=Q(),Te=["/runtime/login","/runtime/logout","/runtime/credentials-status","/runtime/change-credentials","/runtime/server-config","/runtime/access-control","/runtime/node-info","/runtime/logs","/runtime/re-register","/runtime/admin","/runtime/kill-process","/runtime/dashboard","/login","/setup"];function Pe(t){return t==="/"?!0:Te.some(e=>t===e||t.startsWith(`${e}/`))}c.use((t,e,n)=>{Pe(t.path)&&Ce(),n()});const q="panel_sid";function Ae(t){const e={};if(!t)return e;for(const n of t.split(";")){const s=n.indexOf("=");if(s===-1)continue;const i=n.slice(0,s).trim(),r=n.slice(s+1).trim();i&&(e[i]=r)}return e}function N(t){return Ae(t.headers.cookie)[q]}function G(t,e,n){const s=t.secure||t.headers["x-forwarded-proto"]==="https";return`${q}=${e}; HttpOnly; SameSite=Lax; Path=/${s?"; Secure":""}; Max-Age=${n}`}function m(t,e){const n=N(t);return n&&D(n)?!0:t.xhr||t.headers.accept?.includes("application/json")?(e.status(401).json({ok:!1,error:"unauthorized",needLogin:!0}),!1):(e.redirect("/login"),!1)}c.post("/runtime/login",(t,e)=>{const n=t.socket.remoteAddress||"unknown",s=ge(n);if(!s.allowed){e.status(429).set("Retry-After",String(s.retryAfterSec)).json({ok:!1,error:"too many failed login attempts, try again later",retryAfterSec:s.retryAfterSec});return}const{username:i,password:r}=t.body||{};if(!i||!r){e.status(400).json({ok:!1,error:"username and password required"});return}if(!_(String(i),String(r))){pe(n),e.status(401).json({ok:!1,error:"invalid credentials"});return}fe(n);const a=ie(),o=!P();e.setHeader("Set-Cookie",G(t,a,86400)),e.json({ok:!0,needSetup:o})}),c.post("/runtime/logout",(t,e)=>{const n=N(t);n&&F(n),e.setHeader("Set-Cookie",G(t,"",0)),e.json({ok:!0})}),c.get("/runtime/credentials-status",(t,e)=>{if(!m(t,e))return;const n=L();e.json({ok:!0,isDefault:de(),configured:P(),username:n.username})}),c.post("/runtime/change-credentials",async(t,e)=>{if(!m(t,e))return;const{username:n,password:s,passwordConfirm:i,oldPassword:r}=t.body||{};if(s&&String(s).trim()){if(String(s)!==String(i)){e.status(400).json({ok:!1,error:"passwords do not match"});return}if(P()){if(!r){e.status(400).json({ok:!1,error:"old password required"});return}const o=L();if(!_(o.username,String(r))){e.status(403).json({ok:!1,error:"old password is incorrect"});return}}}if(!n||!String(n).trim()){e.status(400).json({ok:!1,error:"username required"});return}const a=s&&String(s).trim()?String(s).trim():L().password;try{await le(String(n).trim(),a),e.json({ok:!0})}catch(o){const l=o;x.error("Failed to change panel credentials:",l.message),e.status(500).json({ok:!1,error:l.message})}});function $(t){const e=typeof t=="string"?t.trim():"";if(e)return e.length<8?"\u2022\u2022\u2022\u2022":"\u2022\u2022\u2022\u2022"+e.slice(-4)}function Y(t){const e=Array.isArray(t.autoRegisterTargets)?t.autoRegisterTargets.filter(s=>!!(s&&typeof s=="object"&&!Array.isArray(s))):[];return e.length>0?e:["serverUrl","schedulerBaseUrl","userKey"].some(s=>!!String(t[s]??"").trim())?[t]:[]}const Re=["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","projectId","roleName","workspacePath","registerAddr","registerIp","virtualIp","tunnel","preferSameLanIp","maxRetries","retryInterval"];function Ne(t){const e={};for(const n of Re)t[n]!==void 0&&(e[n]=t[n]);return e}function Ue(t,e){const n=String(t.connectionKey??"").trim()||String(e??"").trim();return{serverUrl:String(t.serverUrl??t.schedulerBaseUrl??"").trim(),tenantId:String(t.tenantId??"").trim(),instanceName:String(t.instanceName??"").trim(),instanceId:String(t.instanceId??"").trim()||void 0,registerAddr:String(t.registerAddr??"").trim(),tunnel:typeof t.tunnel=="boolean"?t.tunnel:void 0,userKeyMasked:$(t.userKey),userKeySet:!!String(t.userKey??"").trim(),connectionKeyMasked:$(n),connectionKeySet:!!n}}function U(t){return be(t)??String(t||"").trim()}c.get("/runtime/server-config",(t,e)=>{if(!m(t,e))return;const n=T();let s={};if(v.existsSync(n))try{s=JSON.parse(v.readFileSync(n,"utf-8"))}catch{e.status(500).json({ok:!1,error:"config.json is not valid JSON; fix it manually first"});return}const i=String(s.connectionKey??"").trim()||void 0,r=Y(s).map(a=>Ue(a,i));e.json({ok:!0,targets:r})});function H(t,e){const n=new Error(e);return n.status=t,n}c.post("/runtime/server-config",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).targets;if(!Array.isArray(n)){e.status(400).json({ok:!1,error:"targets array is required"});return}const s=[];for(let r=0;r<n.length;r++){const a=n[r];if(!a||typeof a!="object"||Array.isArray(a)){e.status(400).json({ok:!1,error:`targets[${r}] must be an object`});return}const o=a,l=String(o.serverUrl??o.schedulerBaseUrl??"").trim();if(!l){e.status(400).json({ok:!1,error:`targets[${r}].serverUrl is required`});return}try{const d=new URL(l);if(!["http:","https:"].includes(d.protocol))throw new Error("protocol must be http or https")}catch{e.status(400).json({ok:!1,error:`targets[${r}].serverUrl must be a valid http(s) URL`});return}const p=o.tunnel;let f;if(p==="auto"||p===""||p===null||p===void 0)f=void 0;else if(p===!0||p==="true")f=!0;else if(p===!1||p==="false")f=!1;else{e.status(400).json({ok:!1,error:`targets[${r}].tunnel must be auto, true or false`});return}const h=String(o.originalServerUrl??"").trim();s.push({serverUrl:l,originalServerUrl:h&&h!==l?h:"",userKey:String(o.userKey??"").trim(),tenantId:String(o.tenantId??"").trim(),instanceName:String(o.instanceName??"").trim(),connectionKey:String(o.connectionKey??"").trim(),registerAddr:String(o.registerAddr??"").trim(),tunnel:f})}const i=new Set;for(const r of s){const a=U(r.serverUrl);if(i.has(a)){e.status(400).json({ok:!1,error:"duplicate serverUrl in targets"});return}i.add(a)}try{const r=T();await J(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw H(500,"config.json is not valid JSON; fix it manually first")}const o=String(a.connectionKey??"").trim(),l=Y(a),p=l.length>0&&l[0]===a,f=new Map(l.map(d=>[U(String(d.serverUrl??d.schedulerBaseUrl??"").trim()),p?Ne(d):d])),h=[];for(const d of s){let b;if(d.originalServerUrl){const S=U(d.originalServerUrl);b=f.get(S),b&&f.delete(S)}if(!b){const S=U(d.serverUrl);b=f.get(S),b&&f.delete(S)}const u=b?{...b}:{};if(u.serverUrl=d.serverUrl,u.tenantId=d.tenantId,u.instanceName=d.instanceName,u.registerAddr=d.registerAddr,d.tunnel===void 0?delete u.tunnel:u.tunnel=d.tunnel,d.userKey)u.userKey=d.userKey;else if(!String(u.userKey??"").trim())throw H(400,`userKey is required for ${d.serverUrl} (new target without inheritable userKey)`);d.connectionKey?u.connectionKey=d.connectionKey:!String(u.connectionKey??"").trim()&&o&&(u.connectionKey=o),h.push(u)}a.autoRegisterTargets=h;for(const d of["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","registerAddr","registerIp","virtualIp","tunnel"])delete a[d];v.mkdirSync(O.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8")}),x.info("Server connection config updated via panel",{configPath:r,targetCount:s.length}),e.json({ok:!0})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save server connection config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),c.get("/runtime/access-control",(t,e)=>{m(t,e)&&e.json({ok:!0,allowIps:ne(),defaultAllowIps:["0.0.0.0"]})}),c.post("/runtime/access-control",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).allowIps;if(typeof n!="string"&&!Array.isArray(n)){e.status(400).json({ok:!1,error:"allowIps must be a string or an array"});return}const s=re(n),i=s.filter(r=>!se(r));if(i.length>0){e.status(400).json({ok:!1,error:`invalid IP/CIDR entries: ${i.join(", ")}`});return}try{const r=T();await J(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw H(500,"config.json is not valid JSON; fix it manually first")}const o=a.accessControl&&typeof a.accessControl=="object"&&!Array.isArray(a.accessControl)?{...a.accessControl}:{};o.allowIps=s,a.accessControl=o,v.mkdirSync(O.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8"),ae()}),x.info("Access control config updated via panel",{configPath:r,entryCount:s.length}),e.json({ok:!0,allowIps:s})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save access control config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),c.get("/runtime/node-info",(t,e)=>{if(!m(t,e))return;const n=V(),s=z();e.json({ok:!0,hostname:k.hostname(),platform:k.platform(),arch:k.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version,bridgeVersion:W,configPath:T(),homeDir:te(),bindingStatus:n.status,paired:n.paired,instanceId:n.instanceId||s.instanceId,schedulerBaseUrl:n.schedulerBaseUrl,registered:s.registered,pairingCode:n.pairingCode,activeSessions:ue()})}),c.get("/runtime/logs",(t,e)=>{if(!m(t,e))return;const n=t.query.level||void 0,s=Math.min(Number(t.query.count)||200,500),r=n&&["debug","info","warn","error"].includes(n)?n:void 0,a=xe(s,r);e.json({ok:!0,logs:a})}),c.post("/runtime/re-register",async(t,e)=>{if(m(t,e))try{const n=await oe();e.json({ok:n})}catch(n){const s=n;e.status(500).json({ok:!1,error:s.message})}}),c.get("/runtime/admin/sessions",(t,e)=>{if(!m(t,e))return;const n=ce().map(s=>({token:s.token.slice(0,8)+"...",createdAt:new Date(s.createdAt).toISOString(),lastUsedAt:new Date(s.lastUsedAt).toISOString(),age:Math.floor((Date.now()-s.createdAt)/1e3)}));e.json({ok:!0,sessions:n})}),c.post("/runtime/admin/sessions/:token/destroy",(t,e)=>{m(t,e)&&(F(t.params.token),e.json({ok:!0}))});function Le(t){return new Promise((e,n)=>{const s=k.platform()==="win32",i=s?"taskkill":"kill",r=s?["/PID",String(t),"/F"]:["-9",String(t)];X(i,r,{timeout:5e3},a=>a?n(a):e())})}c.post("/runtime/kill-process",async(t,e)=>{if(!m(t,e))return;const n=Number(t.body.pid);if(!n||!Number.isInteger(n)||n<=0){e.status(400).json({ok:!1,error:"Invalid PID"});return}const s=E(),i=()=>{const a=new Set,o=R?.data??[];for(const l of o)typeof l.pid=="number"&&a.add(l.pid);for(const l of s.getAllManagedProcesses())typeof l.pid=="number"&&a.add(l.pid);return a};let r=i();if(r.size===0)try{R={data:await s.discoverAllProcesses(),ts:Date.now()},r=i()}catch{}if(!r.has(n)){x.warn("Rejected kill-process for unrecognized PID",{pid:n}),e.status(404).json({ok:!1,error:"PID is not a bridge-discovered or registered agent process"});return}try{await Le(n),x.info("Killed process",{pid:n}),e.json({ok:!0})}catch(a){const o=a;x.warn("Failed to kill process",{pid:n,error:o.message}),e.status(500).json({ok:!1,error:o.message})}}),c.get("/runtime/dashboard",async(t,e)=>{if(m(t,e)){if(A&&Date.now()-A.ts<Se)return void e.json(A.data);try{const n=M?.data??null,s=R?.data??null;M||j();const[i,r,a]=await Promise.all([Promise.resolve(V()),Promise.resolve(z()),ve().catch(()=>[])]),l=E().getAllManagedProcesses(),p=Array.from(we.entries()).map(([I,y])=>({sessionId:I,agentId:y.agentId,workspacePath:y.workspacePath,runtimeStatus:y.currentRuntimeStatus||"running",runtimeActionType:y.currentRuntimeActionType,runtimeActionLabel:y.currentRuntimeActionLabel,providerId:y.providerId}));let f={managed:0,orphan:0,unknown:0},h=[];if(s){f={managed:s.filter(g=>g.managementStatus==="managed").length,orphan:s.filter(g=>g.managementStatus==="orphan").length,unknown:s.filter(g=>g.managementStatus==="unknown").length};const I=new Map(l.map(g=>[g.pid,g]));h=s.map(g=>{const B=I.get(g.pid);return{agentId:g.agentId||B?.agentId,pid:g.pid,managementStatus:g.managementStatus,sessionId:B?.sessionId,state:B?.state,command:g.command,cwd:g.cwd,startedAt:g.startedAt}});const y={managed:0,orphan:1,unknown:2};h.sort((g,B)=>(y[g.managementStatus]??9)-(y[B.managementStatus]??9))}const d=new Set(p.map(I=>I.sessionId)),b=a.filter(I=>!d.has(I.sessionId)),u=i,S=await Ee(),K={ok:!0,collectedAt:new Date().toISOString(),bridge:{version:W,hostname:k.hostname(),platform:k.platform(),arch:k.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version},binding:{status:u.status,paired:u.paired,instanceId:u.instanceId,userId:u.userId,schedulerBaseUrl:u.schedulerBaseUrl,pairingCode:me()},registration:{registered:r.registered,instanceId:r.instanceId,lastAttempt:r.lastAttempt?.toISOString(),error:r.error},metrics:n,sessions:{active:p,activeCount:p.length,persisted:b,persistedCount:b.length,total:p.length+b.length},processes:{...f,total:f.managed+f.orphan+f.unknown,list:h,registryCount:l.length},lifecycleOperation:S};A={data:K,ts:Date.now()},e.json(K)}catch(n){const s=n;x.error("Dashboard aggregation failed:",s.message),e.status(500).json({ok:!1,error:s.message})}}}),c.get("/login",(t,e)=>{e.type("text/html; charset=utf-8"),e.send(Me)}),c.get("/",(t,e)=>{const n=N(t);if(!n||!D(n)){e.redirect("/login");return}if(!P()){e.redirect("/setup");return}const s=t.query.lang==="en"?"en":"zh";e.type("text/html; charset=utf-8"),e.send(De.replace("{{LANG}}",s))}),c.get("/setup",(t,e)=>{const n=N(t);if(!n||!D(n)){e.redirect("/login");return}e.type("text/html; charset=utf-8"),e.send(je)});const De=`<!DOCTYPE html>
2
2
  <html lang="{{LANG}}">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -2061,7 +2061,7 @@ switchSettingsPage = function(page) {
2061
2061
  };
2062
2062
  </script>
2063
2063
  </body>
2064
- </html>`,Pe=`<!DOCTYPE html>
2064
+ </html>`,Me=`<!DOCTYPE html>
2065
2065
  <html lang="zh-CN">
2066
2066
  <head>
2067
2067
  <meta charset="UTF-8">
@@ -2232,7 +2232,7 @@ document.getElementById("loginForm").addEventListener("keydown", function(e) {
2232
2232
  });
2233
2233
  </script>
2234
2234
  </body>
2235
- </html>`,Ae=`<!DOCTYPE html>
2235
+ </html>`,je=`<!DOCTYPE html>
2236
2236
  <html lang="zh-CN">
2237
2237
  <head>
2238
2238
  <meta charset="UTF-8">
@@ -2519,5 +2519,5 @@ async function doSkip() {
2519
2519
  document.getElementById("setupForm").addEventListener("submit", doSave);
2520
2520
  </script>
2521
2521
  </body>
2522
- </html>`;export{g as dashboardRouter};
2522
+ </html>`;export{c as dashboardRouter,Ce as noteDashboardActivity};
2523
2523
 
@@ -1,37 +1,37 @@
1
- import{spawn as st}from"node:child_process";import{promises as b}from"node:fs";import ot from"node:os";import x from"node:path";import L from"axios";import{Router as it}from"express";import{validateToken as k}from"../middleware/auth.js";import{logger as m}from"../utils/logger.js";const S=it(),at=1e4;function nt(){const r=Number(process.env.AWS_RUNTIME_GIT_COMMAND_TIMEOUT_MS);return Number.isFinite(r)&&r>=1e3?r:at}async function ct(r){let t;try{t=await b.stat(r)}catch(e){const i=e;throw i.code==="ENOENT"?new Error(`workspace path does not exist: ${r}`):new Error(`workspace path is not accessible: ${r}${i.message?` (${i.message})`:""}`)}if(!t.isDirectory())throw new Error(`workspace path is not a directory: ${r}`)}const ut=/^[0-9a-f]{40}$/i;function lt(r,t,e,i){return r==="added"||r==="deleted"||r==="renamed"?!0:i||t>0||e>0}function ft(r){const t=String(r||"").toLowerCase();return t.includes("you do not have the initial commit yet")||t.includes("bad revision")&&t.includes("head")||t.includes("ambiguous argument")&&t.includes("head")}function ht(){return"\u5F53\u524D Git \u4ED3\u5E93\u8FD8\u6CA1\u6709\u521D\u59CB\u63D0\u4EA4\uFF0C\u65E0\u6CD5\u521B\u5EFA\u5FEB\u7167\u3002\u8BF7\u5148\u63D0\u4EA4\u4E00\u6B21\u521D\u59CB\u63D0\u4EA4\u540E\u518D\u521B\u5EFA\u5FEB\u7167\u3002"}function dt(r){const t=String(r||"").toLowerCase();return t.includes("no local changes to save")||t.includes("no changes to save")||t.includes("\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58")}function pt(r){const t=String(r||"").trim();return ut.test(t)?`refs/agentswork/stash-snapshots/${t.toLowerCase()}`:null}function _(r){const t=String(r||"").trim();if(!t)return null;const e=t.split("\0");if(e.length>=3){const a=e[0]?.trim(),o=e[1]?.trim(),c=e[2]?.trim()||o,u=e[3]?.trim()||null;if(!a||!o)return null;const l=O(c||"");return{ref:a,stashName:o,message:l.message,branch:l.branch,createdAt:u}}const i=t.match(/^(stash@\{\d+\})(?::\s*)?(.*)$/);if(!i)return null;const s=i[1],n=O(i[2]?.trim()||s);return{ref:s,stashName:s,message:n.message,branch:n.branch,createdAt:null}}function O(r){const t=String(r||"").trim(),e=t.match(/^(?:On|WIP on)\s+([^:]+):\s*(.*)$/);return e?{branch:e[1].trim(),message:e[2].trim()||t}:{branch:"",message:t}}function I(r){const t=String(r||"").split(`
2
- `).find(i=>i.trim());if(!t)return null;const e=_(t);return e?{ref:e.ref,stashName:e.stashName||e.ref,message:e.message||e.ref}:null}async function h(r,t,e){return new Promise(i=>{let s=!1,n=null;const a=f=>{s||(s=!0,n&&clearTimeout(n),i(f))},o=st("git",t,{cwd:r,stdio:["ignore","pipe","pipe"]});let c="",u="";o.stdout?.on("data",f=>{c+=String(f||"")}),o.stderr?.on("data",f=>{u+=String(f||"")});const l=e??nt();n=setTimeout(()=>{m.warn(`[runtime-bridge] git command timed out after ${l}ms: git ${t.join(" ")} (cwd=${r})`),o.kill("SIGKILL"),a({stdout:c,stderr:u||`git command timed out after ${l}ms`,exitCode:124})},l),o.on("error",f=>{a({stdout:"",stderr:f.message,exitCode:1})}),o.on("close",f=>{a({stdout:c,stderr:u,exitCode:f??1})})})}const mt=6e4;function gt(){const r=Number(process.env.AWS_RUNTIME_GIT_PUSH_TIMEOUT_MS);return Number.isFinite(r)&&r>=1e3?r:mt}async function z(r,t){const e=pt(t);if(!e)return null;const i=await h(r,["update-ref",e,t]);return i.exitCode!==0?(m.warn(`[Git] \u6301\u4E45\u5316 stash \u5FEB\u7167 ref \u5931\u8D25: ref=${e}, error=${i.stderr||i.stdout}`),null):e}function R(r){return String(r||"").replace(/\\/g,"/").replace(/^\.\//,"").trim()}async function j(r){const t=x.resolve(String(r).trim());await ct(t);const e=await h(t,["rev-parse","--show-toplevel"]);if(e.exitCode!==0)return{workspacePath:t,repositoryRootPath:null,hasRepository:!1,isGitRepo:!1,usingParentGitRepo:!1,relativeWorkspacePath:""};const i=x.resolve(e.stdout.trim()),s=i===t,n=s?"":R(x.relative(i,t));return{workspacePath:t,repositoryRootPath:i,hasRepository:!0,isGitRepo:s,usingParentGitRepo:!s,relativeWorkspacePath:n}}function A(r,t){if(!t.relativeWorkspacePath)return[...r];const e=!r.includes("status");return[...r,...e?[`--relative=${t.relativeWorkspacePath}`]:[],"--",t.relativeWorkspacePath]}function E(r,t){const e=R(r);return t.relativeWorkspacePath?R(x.posix.join(t.relativeWorkspacePath,e)):e}function D(r){const t=R(r);if(x.posix.isAbsolute(t)||t.split("/").some(e=>e===".."||e==="."))throw new Error(`unsafe repository relative path: ${r}`)}function yt(r){const t=String(r||"").trim().toUpperCase();return t.startsWith("A")?"added":t.startsWith("D")?"deleted":t.startsWith("R")?"renamed":"modified"}function F(r){const t=R(r);if(!t.includes("=>"))return t;const e=t.match(/^(.*)\{([^{}]+)=>([^{}]+)\}(.*)$/);if(e){const s=e[1],n=e[3],a=e[4];return R(`${s}${n}${a}`)}const i=t.lastIndexOf("=>");return i>=0?R(t.slice(i+2)):t}function Pt(r){const t=new Set,e=String(r||"").split(`
1
+ import{spawn as st}from"node:child_process";import{promises as b}from"node:fs";import ot from"node:os";import x from"node:path";import L from"axios";import{Router as it}from"express";import{validateToken as k}from"../middleware/auth.js";import{noteDashboardActivity as at}from"./dashboard.js";import{logger as m}from"../utils/logger.js";const S=it(),nt=1e4;function ct(){const r=Number(process.env.AWS_RUNTIME_GIT_COMMAND_TIMEOUT_MS);return Number.isFinite(r)&&r>=1e3?r:nt}async function ut(r){let t;try{t=await b.stat(r)}catch(e){const i=e;throw i.code==="ENOENT"?new Error(`workspace path does not exist: ${r}`):new Error(`workspace path is not accessible: ${r}${i.message?` (${i.message})`:""}`)}if(!t.isDirectory())throw new Error(`workspace path is not a directory: ${r}`)}const lt=/^[0-9a-f]{40}$/i;function ft(r,t,e,i){return r==="added"||r==="deleted"||r==="renamed"?!0:i||t>0||e>0}function ht(r){const t=String(r||"").toLowerCase();return t.includes("you do not have the initial commit yet")||t.includes("bad revision")&&t.includes("head")||t.includes("ambiguous argument")&&t.includes("head")}function dt(){return"\u5F53\u524D Git \u4ED3\u5E93\u8FD8\u6CA1\u6709\u521D\u59CB\u63D0\u4EA4\uFF0C\u65E0\u6CD5\u521B\u5EFA\u5FEB\u7167\u3002\u8BF7\u5148\u63D0\u4EA4\u4E00\u6B21\u521D\u59CB\u63D0\u4EA4\u540E\u518D\u521B\u5EFA\u5FEB\u7167\u3002"}function pt(r){const t=String(r||"").toLowerCase();return t.includes("no local changes to save")||t.includes("no changes to save")||t.includes("\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58")}function mt(r){const t=String(r||"").trim();return lt.test(t)?`refs/agentswork/stash-snapshots/${t.toLowerCase()}`:null}function _(r){const t=String(r||"").trim();if(!t)return null;const e=t.split("\0");if(e.length>=3){const a=e[0]?.trim(),o=e[1]?.trim(),c=e[2]?.trim()||o,u=e[3]?.trim()||null;if(!a||!o)return null;const l=O(c||"");return{ref:a,stashName:o,message:l.message,branch:l.branch,createdAt:u}}const i=t.match(/^(stash@\{\d+\})(?::\s*)?(.*)$/);if(!i)return null;const s=i[1],n=O(i[2]?.trim()||s);return{ref:s,stashName:s,message:n.message,branch:n.branch,createdAt:null}}function O(r){const t=String(r||"").trim(),e=t.match(/^(?:On|WIP on)\s+([^:]+):\s*(.*)$/);return e?{branch:e[1].trim(),message:e[2].trim()||t}:{branch:"",message:t}}function I(r){const t=String(r||"").split(`
2
+ `).find(i=>i.trim());if(!t)return null;const e=_(t);return e?{ref:e.ref,stashName:e.stashName||e.ref,message:e.message||e.ref}:null}async function h(r,t,e){return new Promise(i=>{let s=!1,n=null;const a=f=>{s||(s=!0,n&&clearTimeout(n),i(f))},o=st("git",t,{cwd:r,stdio:["ignore","pipe","pipe"]});let c="",u="";o.stdout?.on("data",f=>{c+=String(f||"")}),o.stderr?.on("data",f=>{u+=String(f||"")});const l=e??ct();n=setTimeout(()=>{m.warn(`[runtime-bridge] git command timed out after ${l}ms: git ${t.join(" ")} (cwd=${r})`),o.kill("SIGKILL"),a({stdout:c,stderr:u||`git command timed out after ${l}ms`,exitCode:124})},l),o.on("error",f=>{a({stdout:"",stderr:f.message,exitCode:1})}),o.on("close",f=>{a({stdout:c,stderr:u,exitCode:f??1})})})}const gt=6e4;function yt(){const r=Number(process.env.AWS_RUNTIME_GIT_PUSH_TIMEOUT_MS);return Number.isFinite(r)&&r>=1e3?r:gt}async function z(r,t){const e=mt(t);if(!e)return null;const i=await h(r,["update-ref",e,t]);return i.exitCode!==0?(m.warn(`[Git] \u6301\u4E45\u5316 stash \u5FEB\u7167 ref \u5931\u8D25: ref=${e}, error=${i.stderr||i.stdout}`),null):e}function R(r){return String(r||"").replace(/\\/g,"/").replace(/^\.\//,"").trim()}async function j(r){const t=x.resolve(String(r).trim());await ut(t);const e=await h(t,["rev-parse","--show-toplevel"]);if(e.exitCode!==0)return{workspacePath:t,repositoryRootPath:null,hasRepository:!1,isGitRepo:!1,usingParentGitRepo:!1,relativeWorkspacePath:""};const i=x.resolve(e.stdout.trim()),s=i===t,n=s?"":R(x.relative(i,t));return{workspacePath:t,repositoryRootPath:i,hasRepository:!0,isGitRepo:s,usingParentGitRepo:!s,relativeWorkspacePath:n}}function A(r,t){if(!t.relativeWorkspacePath)return[...r];const e=!r.includes("status");return[...r,...e?[`--relative=${t.relativeWorkspacePath}`]:[],"--",t.relativeWorkspacePath]}function E(r,t){const e=R(r);return t.relativeWorkspacePath?R(x.posix.join(t.relativeWorkspacePath,e)):e}function M(r){const t=R(r);if(x.posix.isAbsolute(t)||t.split("/").some(e=>e===".."||e==="."))throw new Error(`unsafe repository relative path: ${r}`)}function Pt(r){const t=String(r||"").trim().toUpperCase();return t.startsWith("A")?"added":t.startsWith("D")?"deleted":t.startsWith("R")?"renamed":"modified"}function F(r){const t=R(r);if(!t.includes("=>"))return t;const e=t.match(/^(.*)\{([^{}]+)=>([^{}]+)\}(.*)$/);if(e){const s=e[1],n=e[3],a=e[4];return R(`${s}${n}${a}`)}const i=t.lastIndexOf("=>");return i>=0?R(t.slice(i+2)):t}function wt(r){const t=new Set,e=String(r||"").split(`
3
3
  `).filter(i=>i.trim());for(const i of e){const s=i[0]||" ";if(s===" "||s==="?")continue;const n=i.slice(3).trim(),a=n.split(" -> "),o=F(a[a.length-1]||n);o&&t.add(o)}return t}function H(r,t,e=new Set,i=!1){const s=new Map,n=r.split(`
4
- `).filter(c=>c.trim());for(const c of n){const u=c.split(" ");if(u.length<2)continue;const l=yt(u[0]),f=l==="renamed"&&u.length>=3?2:1,d=F(u[f]);d&&s.set(d,l)}const a=[],o=t.split(`
5
- `).filter(c=>c.trim());for(const c of o){const u=c.split(" ");if(u.length<3)continue;const l=u[0]==="-"||u[1]==="-",f=u[0]==="-"?0:parseInt(u[0],10)||0,d=u[1]==="-"?0:parseInt(u[1],10)||0,p=F(u.slice(2).join(" ").trim()),y=s.get(p)||"modified";!p||!lt(y,f,d,l)||a.push({path:p,status:y,additions:f,deletions:d,staged:i||e.has(p)})}return a}function wt(r){const t=[],e=String(r||"").split(`
6
- `).filter(i=>i.trim());for(const i of e)if(i.startsWith("?? ")){const s=i.slice(3).trim();s&&t.push(s.replace(/\/$/,""))}return t}function kt(r){if(!r||r.includes("\0"))return 0;let t=0;for(let e=0;e<r.length;e++)r.charCodeAt(e)===10&&t++;return r.endsWith(`
7
- `)?t:t+1}async function q(r){try{const t=await b.readFile(r,"utf8");return kt(t)}catch{return 0}}async function St(r,t,e,i=10){const s=[];async function n(a,o){if(o>i)return;const c=x.join(r,a);let u;try{u=await b.readdir(c,{withFileTypes:!0})}catch{return}for(const l of u){if(l.name===".git"||l.name==="node_modules")continue;const f=a?`${a}/${l.name}`:l.name;if(l.isDirectory())await n(f,o+1);else if(l.isFile()){const d=R(f);d&&!e.has(d)&&s.push({path:d,status:"added",additions:await q(x.join(c,l.name)),deletions:0,staged:!1})}}}for(const a of t){const o=R(a);if(!o)continue;const c=x.join(r,a);let u;try{u=await b.stat(c)}catch{continue}u.isFile()?e.has(o)||s.push({path:o,status:"added",additions:await q(c),deletions:0,staged:!1}):u.isDirectory()&&await n(o,0)}return s}function Rt(r,t){const e=new Map;for(const i of r)e.set(i.path,{...i});for(const i of t){const s=e.get(i.path);if(!s){e.set(i.path,{...i,staged:!0});continue}e.set(i.path,{...s,status:s.status==="modified"?i.status:s.status,additions:s.additions+i.additions,deletions:s.deletions+i.deletions,staged:!0})}return[...e.values()]}function B(r){return String(r||"").trim()}function xt(r){return String(r||"").trim()}function jt(r){return["--",r.relativeWorkspacePath||"."]}function G(r,t){return["--",E(r,t)]}function Ct(r){const t=r.split("\0"),e=[];for(let i=0;i+6<t.length;i+=7){const s=t[i]?.trim();if(!s)continue;const n=(t[i+5]?.trim()||"").split(/\s+/).filter(Boolean);e.push({hash:s,shortHash:t[i+1]?.trim()||s.slice(0,8),authorName:t[i+2]?.trim()||"unknown",authorDate:t[i+3]?.trim()||"",subject:t[i+4]?.trim()||s,body:t[i+6]?.trim()||"",parents:n})}return e}async function U(r){const t=new Set,e=await h(r,["for-each-ref","--format=%(refname)","refs/remotes"]);if(e.exitCode!==0)return t;const i=e.stdout.split(`
4
+ `).filter(c=>c.trim());for(const c of n){const u=c.split(" ");if(u.length<2)continue;const l=Pt(u[0]),f=l==="renamed"&&u.length>=3?2:1,d=F(u[f]);d&&s.set(d,l)}const a=[],o=t.split(`
5
+ `).filter(c=>c.trim());for(const c of o){const u=c.split(" ");if(u.length<3)continue;const l=u[0]==="-"||u[1]==="-",f=u[0]==="-"?0:parseInt(u[0],10)||0,d=u[1]==="-"?0:parseInt(u[1],10)||0,p=F(u.slice(2).join(" ").trim()),y=s.get(p)||"modified";!p||!ft(y,f,d,l)||a.push({path:p,status:y,additions:f,deletions:d,staged:i||e.has(p)})}return a}function kt(r){const t=[],e=String(r||"").split(`
6
+ `).filter(i=>i.trim());for(const i of e)if(i.startsWith("?? ")){const s=i.slice(3).trim();s&&t.push(s.replace(/\/$/,""))}return t}function St(r){if(!r||r.includes("\0"))return 0;let t=0;for(let e=0;e<r.length;e++)r.charCodeAt(e)===10&&t++;return r.endsWith(`
7
+ `)?t:t+1}async function q(r){try{const t=await b.readFile(r,"utf8");return St(t)}catch{return 0}}async function Rt(r,t,e,i=10){const s=[];async function n(a,o){if(o>i)return;const c=x.join(r,a);let u;try{u=await b.readdir(c,{withFileTypes:!0})}catch{return}for(const l of u){if(l.name===".git"||l.name==="node_modules")continue;const f=a?`${a}/${l.name}`:l.name;if(l.isDirectory())await n(f,o+1);else if(l.isFile()){const d=R(f);d&&!e.has(d)&&s.push({path:d,status:"added",additions:await q(x.join(c,l.name)),deletions:0,staged:!1})}}}for(const a of t){const o=R(a);if(!o)continue;const c=x.join(r,a);let u;try{u=await b.stat(c)}catch{continue}u.isFile()?e.has(o)||s.push({path:o,status:"added",additions:await q(c),deletions:0,staged:!1}):u.isDirectory()&&await n(o,0)}return s}function xt(r,t){const e=new Map;for(const i of r)e.set(i.path,{...i});for(const i of t){const s=e.get(i.path);if(!s){e.set(i.path,{...i,staged:!0});continue}e.set(i.path,{...s,status:s.status==="modified"?i.status:s.status,additions:s.additions+i.additions,deletions:s.deletions+i.deletions,staged:!0})}return[...e.values()]}function B(r){return String(r||"").trim()}function jt(r){return String(r||"").trim()}function Ct(r){return["--",r.relativeWorkspacePath||"."]}function G(r,t){return["--",E(r,t)]}function bt(r){const t=r.split("\0"),e=[];for(let i=0;i+6<t.length;i+=7){const s=t[i]?.trim();if(!s)continue;const n=(t[i+5]?.trim()||"").split(/\s+/).filter(Boolean);e.push({hash:s,shortHash:t[i+1]?.trim()||s.slice(0,8),authorName:t[i+2]?.trim()||"unknown",authorDate:t[i+3]?.trim()||"",subject:t[i+4]?.trim()||s,body:t[i+6]?.trim()||"",parents:n})}return e}async function U(r){const t=new Set,e=await h(r,["for-each-ref","--format=%(refname)","refs/remotes"]);if(e.exitCode!==0)return t;const i=e.stdout.split(`
8
8
  `).map(n=>n.trim()).filter(Boolean);if(i.length===0)return t;const s=await h(r,["rev-list",...i]);if(s.exitCode!==0)return t;for(const n of s.stdout.split(`
9
9
  `)){const a=n.trim();a&&t.add(a)}return t}async function N(r){const t=await h(r,["rev-parse","--abbrev-ref","HEAD"]);return t.exitCode!==0||!t.stdout.trim()?"":t.stdout.trim()}async function J(r){const t=await h(r,["for-each-ref","--format=%(refname)%00%(objectname)%00%(HEAD)%00%(upstream:short)","refs/heads","refs/remotes"]);if(t.exitCode!==0)return[];const e=[];for(const i of t.stdout.split(`
10
- `)){const s=i.split("\0"),n=s[0]?.trim(),a=s[1]?.trim();if(!n||!a)continue;const o=n.startsWith("refs/remotes/"),c=o?n.slice(13):n.slice(11);e.push({name:c,ref:n,hash:a,isCurrent:s[2]?.trim()==="*",isRemote:o,upstream:s[3]?.trim()||""})}return e}function bt(r,t){return["diff",t?r:`${r}^!`]}async function $t(r){const t=await h(r,["rev-parse","--abbrev-ref","HEAD"]),e=t.stdout.trim();if(t.exitCode!==0||!e||e==="HEAD")throw new Error("\u5F53\u524D\u5904\u4E8E detached HEAD\uFF08\u5206\u79BB\u5934\u6307\u9488\uFF09\u72B6\u6001\uFF0C\u65E0\u6CD5\u63A8\u9001");const i=await h(r,["rev-parse","--abbrev-ref","--symbolic-full-name","@{u}"]);if(i.exitCode!==0||!i.stdout.trim())throw new Error(`\u5206\u652F ${e} \u672A\u914D\u7F6E\u4E0A\u6E38\u5206\u652F\uFF0C\u65E0\u6CD5\u63A8\u9001\u3002\u8BF7\u5148\u624B\u52A8\u6267\u884C\u4E00\u6B21 git push -u origin ${e} \u8BBE\u7F6E\u4E0A\u6E38\u5206\u652F`);const s=await h(r,["push"],gt());if(s.exitCode!==0)throw new Error(s.stderr||s.stdout||"git push failed");const n=`${s.stdout}
11
- ${s.stderr}`.trim(),a=/everything up-to-date|up to date/i.test(n);return{branch:e,pushed:!a,message:a?"\u6CA1\u6709\u9700\u8981\u63A8\u9001\u7684\u63D0\u4EA4":"\u63A8\u9001\u5B8C\u6210",output:n}}S.post("/git/check",k,async(r,t)=>{const{workspacePath:e}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const i=await j(String(e).trim());t.json({ok:!0,isGitRepo:i.isGitRepo,hasRepository:i.hasRepository,usingParentGitRepo:i.usingParentGitRepo,workspacePath:i.workspacePath,repositoryRootPath:i.repositoryRootPath,relativeWorkspacePath:i.relativeWorkspacePath});return}catch(i){const s=i;t.status(500).json({error:s.message||"check git repo failed"})}});async function vt(r,t){const e=`\u5FEB\u7167\uFF08${new Date().toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})}\uFF09`,i=t&&String(t).trim()?`\u5FEB\u7167: ${String(t).trim()}`:e,s=await h(r,["stash","push","-m",i]);if(dt(`${s.stdout}
12
- ${s.stderr}`)){const u=await h(r,["stash","list","-n","1","--format=%H%x00%gd%x00%gs%x00%cr"]),l=u.exitCode===0?I(u.stdout):null;return{stashRef:(l?await z(r,l.ref):null)??l?.ref??null,stashName:l?.stashName??null,noChanges:!0,reusedLatestStash:!!l,message:l?`\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58\uFF0C\u5DF2\u590D\u7528\u6700\u8FD1\u5FEB\u7167 ${l.stashName}`:"\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58"}}if(s.exitCode!==0)throw ft(`${s.stdout}
13
- ${s.stderr}`)?new Error(ht()):new Error(s.stderr||"git stash push failed");const n=await h(r,["stash","list","-n","1","--format=%H%x00%gd%x00%gs%x00%cr"]),a=n.exitCode===0?I(n.stdout):null;return{stashRef:(a?await z(r,a.ref):null)??a?.ref??"stash@{0}",stashName:a?.stashName??null,noChanges:!1,reusedLatestStash:!1,message:i}}S.post("/git/stash/create",k,async(r,t)=>{const{workspacePath:e,message:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=x.resolve(String(e).trim()),n=await vt(s,i?String(i):void 0);t.json({ok:!0,stashRef:n.stashRef,stashName:n.stashName,message:n.message,noChanges:n.noChanges,reusedLatestStash:n.reusedLatestStash,workspacePath:s})}catch(s){const n=s;t.status(400).json({error:n.message||"create stash failed"})}}),S.post("/git/stash/list",k,async(r,t)=>{const{workspacePath:e,limit:i=20}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=x.resolve(String(e).trim()),n=Math.min(Math.max(1,Number(i)||20),100),a=await h(s,["stash","list","-n",String(n),"--format=%H%x00%gd%x00%gs%x00%cr"]);if(a.exitCode!==0){t.status(400).json({error:a.stderr||"git stash list failed"});return}const o=a.stdout.split(`
14
- `).map(_).filter(c=>c!==null);t.json({ok:!0,stashes:o,count:o.length,workspacePath:s})}catch(s){const n=s;t.status(500).json({error:n.message||"list stash failed"})}});async function K(r,t,e){const{workspacePath:i,ref:s}=r.body||{};if(!i||!String(i).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const n=x.resolve(String(i).trim()),a=s&&String(s).trim()?String(s).trim():"stash@{0}";await h(n,["checkout","--","."]);const o=await h(n,["stash",e,a]);if(o.exitCode!==0){if(o.stdout.includes("CONFLICT")||o.stderr.includes("CONFLICT")){t.json({ok:!1,hasConflict:!0,message:"\u6062\u590D\u65F6\u53D1\u751F\u51B2\u7A81\uFF0C\u8BF7\u624B\u52A8\u89E3\u51B3",details:o.stdout||o.stderr,workspacePath:n});return}if(o.stderr.includes("is not a valid reference")){t.status(404).json({error:`\u5FEB\u7167 ${a} \u4E0D\u5B58\u5728`});return}t.status(400).json({error:o.stderr||`git stash ${e} failed`});return}t.json({ok:!0,stashRef:a,message:e==="apply"?`\u5DF2\u5E94\u7528 ${a}`:`\u5DF2\u6062\u590D ${a}`,preservedStash:e==="apply",workspacePath:n})}catch(n){const a=n;t.status(500).json({error:a.message||`${e} stash failed`})}}S.post("/git/stash/apply",k,async(r,t)=>{await K(r,t,"apply")}),S.post("/git/stash/pop",k,async(r,t)=>{await K(r,t,"pop")}),S.post("/git/stash/drop",k,async(r,t)=>{const{workspacePath:e,ref:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=x.resolve(String(e).trim()),n=i&&String(i).trim()?String(i).trim():"stash@{0}",a=await h(s,["stash","drop",n]);if(a.exitCode!==0){if(a.stderr.includes("is not a valid reference")){t.status(404).json({error:`\u5FEB\u7167 ${n} \u4E0D\u5B58\u5728`});return}t.status(400).json({error:a.stderr||"git stash drop failed"});return}t.json({ok:!0,stashRef:n,message:`\u5DF2\u5220\u9664 ${n}`,workspacePath:s})}catch(s){const n=s;t.status(500).json({error:n.message||"drop stash failed"})}}),S.post("/git/diff",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=await j(String(i||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.json({ok:!0,isGitRepo:s.isGitRepo,hasRepository:!1,usingParentGitRepo:!1,hasChanges:!1,files:[],workspacePath:s.workspacePath,repositoryRootPath:null,relativeWorkspacePath:""});return}const n=await h(s.repositoryRootPath,A(["diff","--name-status"],s));if(n.exitCode!==0){t.status(400).json({error:n.stderr||"git diff failed"});return}const a=await h(s.repositoryRootPath,A(["diff","--numstat"],s));if(a.exitCode!==0){t.status(400).json({error:a.stderr||"git diff failed"});return}const o=await h(s.repositoryRootPath,A(["diff","--cached","--name-status"],s));if(o.exitCode!==0){t.status(400).json({error:o.stderr||"git staged diff failed"});return}const c=await h(s.repositoryRootPath,A(["diff","--cached","--numstat"],s));if(c.exitCode!==0){t.status(400).json({error:c.stderr||"git staged diff failed"});return}const u=await h(s.repositoryRootPath,A(["status","--porcelain=v1"],s));if(u.exitCode!==0){t.status(400).json({error:u.stderr||"git status failed"});return}const l=Pt(u.stdout),f=Rt(H(n.stdout,a.stdout,l),H(o.stdout,c.stdout,l,!0)),d=wt(u.stdout);if(d.length>0){const p=new Set(f.map(g=>g.path)),y=await St(s.repositoryRootPath,d,p);f.push(...y)}t.json({ok:!0,isGitRepo:s.isGitRepo,hasRepository:!0,usingParentGitRepo:s.usingParentGitRepo,hasChanges:f.length>0,files:f,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const n=s;t.status(500).json({error:n.message||"git diff failed"})}});async function Y(r){const t=[],e=await h(r,["config","user.name"]);(e.exitCode!==0||!e.stdout.trim())&&t.push("-c","user.name=AgentsWork");const i=await h(r,["config","user.email"]);return(i.exitCode!==0||!i.stdout.trim())&&t.push("-c","user.email=agent@agentswork.com"),t}async function At(r,t,e){if(!t)throw new Error("commit message is required");if(e.length===0)throw new Error("filePaths must be valid workspace-relative paths");const i=e.map(c=>`:(literal)${c}`),s=await h(r,["add","--",...i]);if(s.exitCode!==0)throw new Error(s.stderr||"git add failed");const n=await h(r,["diff","--cached","--quiet","--",...i]);if(n.exitCode===0)throw new Error("\u6240\u9009\u6587\u4EF6\u6CA1\u6709\u53EF\u63D0\u4EA4\u7684\u53D8\u66F4");if(n.exitCode!==1)throw new Error(n.stderr||"git staged diff failed");const a=await Y(r),o=await h(r,[...a,"commit","-m",t,"--",...i]);if(o.exitCode!==0)throw new Error(o.stderr||o.stdout||"git commit failed");return{committedPaths:[...e],output:o.stdout}}S.post("/git/commit",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,message:s,filePaths:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=xt(s);if(!a){t.status(400).json({error:"commit message is required"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const c=Array.isArray(n)?n.map(y=>R(String(y||"").trim())).filter(Boolean):[];if(c.length>0){const y=c.map(w=>E(w,o)),g=[];for(const w of y)if(w)try{D(w),g.push(w)}catch{t.status(400).json({error:`unsafe file path: ${w}`});return}if(g.length===0){t.status(400).json({error:"filePaths must be valid workspace-relative paths"});return}try{const w=await At(o.repositoryRootPath,a,g);t.json({ok:!0,message:"\u63D0\u4EA4\u5B8C\u6210",committedPaths:c,output:w.output,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(w){const P=w;t.status(400).json({error:P.message||"git commit failed"})}return}const u=await Y(o.repositoryRootPath),l=jt(o),f=await h(o.repositoryRootPath,["add","--all",...l]);if(f.exitCode!==0){t.status(400).json({error:f.stderr||"git add failed"});return}const d=await h(o.repositoryRootPath,["diff","--cached","--quiet",...l]);if(d.exitCode===0){t.status(400).json({error:"\u6CA1\u6709\u53EF\u63D0\u4EA4\u7684\u53D8\u66F4"});return}if(d.exitCode!==1){t.status(400).json({error:d.stderr||"git staged diff failed"});return}const p=await h(o.repositoryRootPath,[...u,"commit","-m",a,...l]);if(p.exitCode!==0){t.status(400).json({error:p.stderr||p.stdout||"git commit failed"});return}t.json({ok:!0,message:"\u63D0\u4EA4\u5B8C\u6210",output:p.stdout,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const c=o;t.status(500).json({error:c.message||"git commit failed"})}}),S.post("/git/add-file",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePath:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const n=R(String(s||"").trim());if(!n){t.status(400).json({error:"filePath is required"});return}try{const a=await j(String(i||e).trim());if(!a.hasRepository||!a.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const o=await h(a.repositoryRootPath,["add",...G(n,a)]);if(o.exitCode!==0){t.status(400).json({error:o.stderr||"git add file failed"});return}t.json({ok:!0,filePath:n,workspacePath:a.workspacePath,repositoryRootPath:a.repositoryRootPath,relativeWorkspacePath:a.relativeWorkspacePath})}catch(a){const o=a;t.status(500).json({error:o.message||"git add file failed"})}}),S.post("/git/add-files",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePaths:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=(Array.isArray(s)?s:[]).map(o=>R(String(o||"").trim())).filter(o=>o.length>0);if(a.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const c=a.flatMap(d=>G(d,o)),u=["add"],l=[];for(let d=0;d<c.length;d++)c[d]!=="--"&&l.push(c[d]);u.push("--",...l);const f=await h(o.repositoryRootPath,u);if(f.exitCode!==0){t.status(400).json({error:f.stderr||"git add files failed"});return}t.json({ok:!0,filePaths:a,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const c=o;t.status(500).json({error:c.message||"git add files failed"})}}),S.post("/git/commits",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,limit:s,branch:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const a=await j(String(i||e).trim());if(!a.hasRepository||!a.repositoryRootPath){t.json({ok:!0,commits:[],currentBranch:"",branchName:null,workspacePath:a.workspacePath,repositoryRootPath:null});return}const o=Number(s),c=!!r.body?.all,u=c?200:100,l=Number.isInteger(o)&&o>0&&o<=u?o:c?100:30,f=await N(a.repositoryRootPath),p=(n&&String(n).trim()?String(n).trim():null)||f||"HEAD",y="%H%x00%h%x00%an%x00%ad%x00%s%x00%P%x00%b%x00";let g;if(c?g=await h(a.repositoryRootPath,["log",`-${l}`,"--all","--topo-order","--date=iso-strict",`--pretty=format:${y}`]):g=await h(a.repositoryRootPath,["log",`-${l}`,"--date=iso-strict",`--pretty=format:${y}`,"--end-of-options",p,"--",a.relativeWorkspacePath||"."]),g.exitCode!==0){t.status(400).json({error:g.stderr||"git log failed"});return}const w=await U(a.repositoryRootPath),P=Ct(g.stdout).map(v=>({...v,isRemote:w.has(v.hash)})),C=c?await J(a.repositoryRootPath):void 0;t.json({ok:!0,commits:P,currentBranch:f,branchName:c?null:p,...C?{branches:C}:{},workspacePath:a.workspacePath,repositoryRootPath:a.repositoryRootPath,relativeWorkspacePath:a.relativeWorkspacePath})}catch(a){const o=a;t.status(500).json({error:o.message||"git commits failed"})}}),S.post("/git/branches",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=await j(String(i||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.json({ok:!0,branches:[],currentBranch:"",workspacePath:s.workspacePath,repositoryRootPath:null});return}const n=await J(s.repositoryRootPath),a=await N(s.repositoryRootPath);t.json({ok:!0,branches:n,currentBranch:a,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const n=s;t.status(500).json({error:n.message||"git branches failed"})}}),S.post("/git/push",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=await j(String(i||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const n=await $t(s.repositoryRootPath);t.json({ok:!0,branch:n.branch,pushed:n.pushed,message:n.message,output:n.output,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const n=s;if(n.message&&/上游分支|detached HEAD|分离头指针|不存在|未配置/.test(n.message)){t.status(400).json({error:n.message});return}t.status(500).json({error:n.message||"git push failed"})}});const Et=/^[0-9a-fA-F]{7,40}$/;function Gt(r){const t=String(r||"").trim();return Et.test(t)?t:null}async function V(r,t){const e=await N(r);if(!e||e==="HEAD")throw new Error("\u5F53\u524D\u5904\u4E8E detached HEAD\uFF08\u5206\u79BB\u5934\u6307\u9488\uFF09\u72B6\u6001\uFF0C\u65E0\u6CD5\u56DE\u9000");const i=await h(r,["rev-parse","--verify","--quiet",`${t}^{commit}`]);if(i.exitCode!==0||!i.stdout.trim())throw new Error("\u76EE\u6807\u63D0\u4EA4\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u63D0\u4EA4\u5217\u8868\u540E\u91CD\u8BD5");const s=i.stdout.trim();if((await h(r,["rev-parse","HEAD"])).stdout.trim()===s)throw new Error("\u76EE\u6807\u63D0\u4EA4\u5DF2\u662F\u5F53\u524D HEAD\uFF0C\u65E0\u9700\u56DE\u9000");if((await h(r,["merge-base","--is-ancestor",s,"HEAD"])).exitCode!==0)throw new Error("\u76EE\u6807\u63D0\u4EA4\u4E0D\u5728\u5F53\u524D\u5206\u652F\u5386\u53F2\u4E2D\uFF0C\u65E0\u6CD5\u56DE\u9000");const o=await h(r,["rev-list","--count",`${s}..HEAD`]),c=parseInt(o.stdout.trim()||"0",10);let u=!1;const l=await U(r);if(l.size>0){const f=await h(r,["rev-list",`${s}..HEAD`]);if(f.exitCode===0){for(const d of f.stdout.split(`
15
- `))if(l.has(d.trim())){u=!0;break}}}return{resolvedHash:s,branch:e,resetCount:c,affectsRemote:u}}async function X(r){const t=await h(r,["status","--porcelain"]);return t.exitCode===0&&!!t.stdout.trim()}async function Tt(r,t,e){const i=await V(r,t),s=await X(r);return{mode:e,commitHash:i.resolvedHash,branch:i.branch,resetCount:i.resetCount,hasLocalChanges:s,affectsRemote:i.affectsRemote}}async function Mt(r,t,e,i=""){const s=await V(r,t),n=await X(r),a=e==="hard"?["reset","--hard",s.resolvedHash]:["reset","--soft",s.resolvedHash],o=await h(r,a);if(o.exitCode!==0)throw new Error(o.stderr||o.stdout||"git reset \u6267\u884C\u5931\u8D25");if(e==="hard"){const c=["clean","-fd"];i&&c.push("--",i);const u=await h(r,c);u.exitCode!==0&&m.warn(`[Git] \u786C\u56DE\u9000\u540E\u6E05\u7406\u672A\u8DDF\u8E2A\u6587\u4EF6\u5931\u8D25: cwd=${r}, scope=${i||"(\u6574\u4ED3)"}, error=${u.stderr||u.stdout}`)}return{mode:e,commitHash:s.resolvedHash,branch:s.branch,resetCount:s.resetCount,hasLocalChanges:n,affectsRemote:s.affectsRemote}}S.post("/git/reset",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,commitHash:s,mode:n,preview:a}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const o=Gt(s);if(!o){t.status(400).json({error:"commitHash \u683C\u5F0F\u975E\u6CD5\uFF0C\u65E0\u6CD5\u56DE\u9000"});return}const c=String(n||"").trim();if(c!=="soft"&&c!=="hard"){t.status(400).json({error:"mode must be soft or hard"});return}const u=a===!0||a==="true";try{const l=await j(String(i||e).trim());if(!l.hasRepository||!l.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}if(u){const y=await Tt(l.repositoryRootPath,o,c);t.json({ok:!0,preview:!0,...y,workspacePath:l.workspacePath,repositoryRootPath:l.repositoryRootPath,relativeWorkspacePath:l.relativeWorkspacePath});return}const f=await Mt(l.repositoryRootPath,o,c,l.relativeWorkspacePath),d=f.commitHash.slice(0,7),p=f.mode==="hard"?`\u5DF2\u786C\u56DE\u9000\u5230 ${d}\uFF0C\u5171\u56DE\u9000 ${f.resetCount} \u4E2A\u63D0\u4EA4\uFF0C\u5DE5\u4F5C\u533A\u5DF2\u5B8C\u5168\u6062\u590D\u5230\u8BE5\u63D0\u4EA4\u72B6\u6001`:`\u5DF2\u8F6F\u56DE\u9000\u5230 ${d}\uFF0C\u5171\u56DE\u9000 ${f.resetCount} \u4E2A\u63D0\u4EA4\uFF0C\u5DE5\u4F5C\u533A\u6587\u4EF6\u4FDD\u6301\u4E0D\u53D8`;t.json({ok:!0,...f,message:p,workspacePath:l.workspacePath,repositoryRootPath:l.repositoryRootPath,relativeWorkspacePath:l.relativeWorkspacePath})}catch(l){const f=l;if(f.message&&/回退|快照|detached HEAD|分离头指针|不存在|无需|HEAD/.test(f.message)){t.status(400).json({error:f.message});return}t.status(500).json({error:f.message||"git reset failed"})}}),S.post("/git/commit-diff",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,commitHash:s,aggregate:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=B(s);if(!a){t.status(400).json({error:"commitHash is required"});return}const o=n===!0||n==="true";try{const c=await j(String(i||e).trim());if(!c.hasRepository||!c.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const u=bt(a,o),l=await h(c.repositoryRootPath,A([...u,"--name-status"],c));if(l.exitCode!==0){t.status(400).json({error:l.stderr||"git commit diff failed"});return}const f=await h(c.repositoryRootPath,A([...u,"--numstat"],c));if(f.exitCode!==0){t.status(400).json({error:f.stderr||"git commit diff failed"});return}const d=H(l.stdout,f.stdout);t.json({ok:!0,commitHash:a,isGitRepo:c.isGitRepo,hasRepository:!0,usingParentGitRepo:c.usingParentGitRepo,hasChanges:d.length>0,files:d,workspacePath:c.workspacePath,repositoryRootPath:c.repositoryRootPath,relativeWorkspacePath:c.relativeWorkspacePath})}catch(c){const u=c;t.status(500).json({error:u.message||"git commit diff failed"})}}),S.post("/git/diff-file",k,async(r,t)=>{const{workspacePath:e,filePath:i,commitHash:s,aggregate:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!i||!String(i).trim()){t.status(400).json({error:"filePath is required"});return}try{const a=await j(String(e).trim()),o=R(String(i).trim());if(!a.hasRepository||!a.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const c=E(o,a),u=B(s),l=!!(u&&(n===!0||n==="true")),f=!u||l,d=u?l?["diff",u,"--",c]:["show","--format=","--no-ext-diff",u,"--",c]:["diff","HEAD","--",c];let p=await h(a.repositoryRootPath,d);if(f&&p.exitCode!==0&&(p=await h(a.repositoryRootPath,["diff","--",c])),p.exitCode!==0){t.status(400).json({error:p.stderr||"git diff file failed"});return}if(!u&&!p.stdout.trim()){const g=await h(a.repositoryRootPath,["diff","--cached","--",c]);if(g.exitCode!==0){t.status(400).json({error:g.stderr||"git staged diff file failed"});return}p=g}if(f&&!p.stdout.trim()){const g=x.join(a.repositoryRootPath,c);try{const w=await b.readFile(g,"utf-8");if(w){const P=w.split(`
10
+ `)){const s=i.split("\0"),n=s[0]?.trim(),a=s[1]?.trim();if(!n||!a)continue;const o=n.startsWith("refs/remotes/"),c=o?n.slice(13):n.slice(11);e.push({name:c,ref:n,hash:a,isCurrent:s[2]?.trim()==="*",isRemote:o,upstream:s[3]?.trim()||""})}return e}function $t(r,t){return["diff",t?r:`${r}^!`]}async function vt(r){const t=await h(r,["rev-parse","--abbrev-ref","HEAD"]),e=t.stdout.trim();if(t.exitCode!==0||!e||e==="HEAD")throw new Error("\u5F53\u524D\u5904\u4E8E detached HEAD\uFF08\u5206\u79BB\u5934\u6307\u9488\uFF09\u72B6\u6001\uFF0C\u65E0\u6CD5\u63A8\u9001");const i=await h(r,["rev-parse","--abbrev-ref","--symbolic-full-name","@{u}"]);if(i.exitCode!==0||!i.stdout.trim())throw new Error(`\u5206\u652F ${e} \u672A\u914D\u7F6E\u4E0A\u6E38\u5206\u652F\uFF0C\u65E0\u6CD5\u63A8\u9001\u3002\u8BF7\u5148\u624B\u52A8\u6267\u884C\u4E00\u6B21 git push -u origin ${e} \u8BBE\u7F6E\u4E0A\u6E38\u5206\u652F`);const s=await h(r,["push"],yt());if(s.exitCode!==0)throw new Error(s.stderr||s.stdout||"git push failed");const n=`${s.stdout}
11
+ ${s.stderr}`.trim(),a=/everything up-to-date|up to date/i.test(n);return{branch:e,pushed:!a,message:a?"\u6CA1\u6709\u9700\u8981\u63A8\u9001\u7684\u63D0\u4EA4":"\u63A8\u9001\u5B8C\u6210",output:n}}S.post("/git/check",k,async(r,t)=>{const{workspacePath:e}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const i=await j(String(e).trim());t.json({ok:!0,isGitRepo:i.isGitRepo,hasRepository:i.hasRepository,usingParentGitRepo:i.usingParentGitRepo,workspacePath:i.workspacePath,repositoryRootPath:i.repositoryRootPath,relativeWorkspacePath:i.relativeWorkspacePath});return}catch(i){const s=i;t.status(500).json({error:s.message||"check git repo failed"})}});async function At(r,t){const e=`\u5FEB\u7167\uFF08${new Date().toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})}\uFF09`,i=t&&String(t).trim()?`\u5FEB\u7167: ${String(t).trim()}`:e,s=await h(r,["stash","push","-m",i]);if(pt(`${s.stdout}
12
+ ${s.stderr}`)){const u=await h(r,["stash","list","-n","1","--format=%H%x00%gd%x00%gs%x00%cr"]),l=u.exitCode===0?I(u.stdout):null;return{stashRef:(l?await z(r,l.ref):null)??l?.ref??null,stashName:l?.stashName??null,noChanges:!0,reusedLatestStash:!!l,message:l?`\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58\uFF0C\u5DF2\u590D\u7528\u6700\u8FD1\u5FEB\u7167 ${l.stashName}`:"\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58"}}if(s.exitCode!==0)throw ht(`${s.stdout}
13
+ ${s.stderr}`)?new Error(dt()):new Error(s.stderr||"git stash push failed");const n=await h(r,["stash","list","-n","1","--format=%H%x00%gd%x00%gs%x00%cr"]),a=n.exitCode===0?I(n.stdout):null;return{stashRef:(a?await z(r,a.ref):null)??a?.ref??"stash@{0}",stashName:a?.stashName??null,noChanges:!1,reusedLatestStash:!1,message:i}}S.post("/git/stash/create",k,async(r,t)=>{const{workspacePath:e,message:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=x.resolve(String(e).trim()),n=await At(s,i?String(i):void 0);t.json({ok:!0,stashRef:n.stashRef,stashName:n.stashName,message:n.message,noChanges:n.noChanges,reusedLatestStash:n.reusedLatestStash,workspacePath:s})}catch(s){const n=s;t.status(400).json({error:n.message||"create stash failed"})}}),S.post("/git/stash/list",k,async(r,t)=>{const{workspacePath:e,limit:i=20}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=x.resolve(String(e).trim()),n=Math.min(Math.max(1,Number(i)||20),100),a=await h(s,["stash","list","-n",String(n),"--format=%H%x00%gd%x00%gs%x00%cr"]);if(a.exitCode!==0){t.status(400).json({error:a.stderr||"git stash list failed"});return}const o=a.stdout.split(`
14
+ `).map(_).filter(c=>c!==null);t.json({ok:!0,stashes:o,count:o.length,workspacePath:s})}catch(s){const n=s;t.status(500).json({error:n.message||"list stash failed"})}});async function K(r,t,e){const{workspacePath:i,ref:s}=r.body||{};if(!i||!String(i).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const n=x.resolve(String(i).trim()),a=s&&String(s).trim()?String(s).trim():"stash@{0}";await h(n,["checkout","--","."]);const o=await h(n,["stash",e,a]);if(o.exitCode!==0){if(o.stdout.includes("CONFLICT")||o.stderr.includes("CONFLICT")){t.json({ok:!1,hasConflict:!0,message:"\u6062\u590D\u65F6\u53D1\u751F\u51B2\u7A81\uFF0C\u8BF7\u624B\u52A8\u89E3\u51B3",details:o.stdout||o.stderr,workspacePath:n});return}if(o.stderr.includes("is not a valid reference")){t.status(404).json({error:`\u5FEB\u7167 ${a} \u4E0D\u5B58\u5728`});return}t.status(400).json({error:o.stderr||`git stash ${e} failed`});return}t.json({ok:!0,stashRef:a,message:e==="apply"?`\u5DF2\u5E94\u7528 ${a}`:`\u5DF2\u6062\u590D ${a}`,preservedStash:e==="apply",workspacePath:n})}catch(n){const a=n;t.status(500).json({error:a.message||`${e} stash failed`})}}S.post("/git/stash/apply",k,async(r,t)=>{await K(r,t,"apply")}),S.post("/git/stash/pop",k,async(r,t)=>{await K(r,t,"pop")}),S.post("/git/stash/drop",k,async(r,t)=>{const{workspacePath:e,ref:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=x.resolve(String(e).trim()),n=i&&String(i).trim()?String(i).trim():"stash@{0}",a=await h(s,["stash","drop",n]);if(a.exitCode!==0){if(a.stderr.includes("is not a valid reference")){t.status(404).json({error:`\u5FEB\u7167 ${n} \u4E0D\u5B58\u5728`});return}t.status(400).json({error:a.stderr||"git stash drop failed"});return}t.json({ok:!0,stashRef:n,message:`\u5DF2\u5220\u9664 ${n}`,workspacePath:s})}catch(s){const n=s;t.status(500).json({error:n.message||"drop stash failed"})}}),S.post("/git/diff",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=await j(String(i||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.json({ok:!0,isGitRepo:s.isGitRepo,hasRepository:!1,usingParentGitRepo:!1,hasChanges:!1,files:[],workspacePath:s.workspacePath,repositoryRootPath:null,relativeWorkspacePath:""});return}const n=await h(s.repositoryRootPath,A(["diff","--name-status"],s));if(n.exitCode!==0){t.status(400).json({error:n.stderr||"git diff failed"});return}const a=await h(s.repositoryRootPath,A(["diff","--numstat"],s));if(a.exitCode!==0){t.status(400).json({error:a.stderr||"git diff failed"});return}const o=await h(s.repositoryRootPath,A(["diff","--cached","--name-status"],s));if(o.exitCode!==0){t.status(400).json({error:o.stderr||"git staged diff failed"});return}const c=await h(s.repositoryRootPath,A(["diff","--cached","--numstat"],s));if(c.exitCode!==0){t.status(400).json({error:c.stderr||"git staged diff failed"});return}const u=await h(s.repositoryRootPath,A(["status","--porcelain=v1"],s));if(u.exitCode!==0){t.status(400).json({error:u.stderr||"git status failed"});return}const l=wt(u.stdout),f=xt(H(n.stdout,a.stdout,l),H(o.stdout,c.stdout,l,!0)),d=kt(u.stdout);if(d.length>0){const p=new Set(f.map(g=>g.path)),y=await Rt(s.repositoryRootPath,d,p);f.push(...y)}t.json({ok:!0,isGitRepo:s.isGitRepo,hasRepository:!0,usingParentGitRepo:s.usingParentGitRepo,hasChanges:f.length>0,files:f,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const n=s;t.status(500).json({error:n.message||"git diff failed"})}});async function Y(r){const t=[],e=await h(r,["config","user.name"]);(e.exitCode!==0||!e.stdout.trim())&&t.push("-c","user.name=AgentsWork");const i=await h(r,["config","user.email"]);return(i.exitCode!==0||!i.stdout.trim())&&t.push("-c","user.email=agent@agentswork.com"),t}async function Et(r,t,e){if(!t)throw new Error("commit message is required");if(e.length===0)throw new Error("filePaths must be valid workspace-relative paths");const i=e.map(c=>`:(literal)${c}`),s=await h(r,["add","--",...i]);if(s.exitCode!==0)throw new Error(s.stderr||"git add failed");const n=await h(r,["diff","--cached","--quiet","--",...i]);if(n.exitCode===0)throw new Error("\u6240\u9009\u6587\u4EF6\u6CA1\u6709\u53EF\u63D0\u4EA4\u7684\u53D8\u66F4");if(n.exitCode!==1)throw new Error(n.stderr||"git staged diff failed");const a=await Y(r),o=await h(r,[...a,"commit","-m",t,"--",...i]);if(o.exitCode!==0)throw new Error(o.stderr||o.stdout||"git commit failed");return{committedPaths:[...e],output:o.stdout}}S.post("/git/commit",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,message:s,filePaths:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=jt(s);if(!a){t.status(400).json({error:"commit message is required"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const c=Array.isArray(n)?n.map(y=>R(String(y||"").trim())).filter(Boolean):[];if(c.length>0){const y=c.map(w=>E(w,o)),g=[];for(const w of y)if(w)try{M(w),g.push(w)}catch{t.status(400).json({error:`unsafe file path: ${w}`});return}if(g.length===0){t.status(400).json({error:"filePaths must be valid workspace-relative paths"});return}try{const w=await Et(o.repositoryRootPath,a,g);t.json({ok:!0,message:"\u63D0\u4EA4\u5B8C\u6210",committedPaths:c,output:w.output,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(w){const P=w;t.status(400).json({error:P.message||"git commit failed"})}return}const u=await Y(o.repositoryRootPath),l=Ct(o),f=await h(o.repositoryRootPath,["add","--all",...l]);if(f.exitCode!==0){t.status(400).json({error:f.stderr||"git add failed"});return}const d=await h(o.repositoryRootPath,["diff","--cached","--quiet",...l]);if(d.exitCode===0){t.status(400).json({error:"\u6CA1\u6709\u53EF\u63D0\u4EA4\u7684\u53D8\u66F4"});return}if(d.exitCode!==1){t.status(400).json({error:d.stderr||"git staged diff failed"});return}const p=await h(o.repositoryRootPath,[...u,"commit","-m",a,...l]);if(p.exitCode!==0){t.status(400).json({error:p.stderr||p.stdout||"git commit failed"});return}t.json({ok:!0,message:"\u63D0\u4EA4\u5B8C\u6210",output:p.stdout,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const c=o;t.status(500).json({error:c.message||"git commit failed"})}}),S.post("/git/add-file",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePath:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const n=R(String(s||"").trim());if(!n){t.status(400).json({error:"filePath is required"});return}try{const a=await j(String(i||e).trim());if(!a.hasRepository||!a.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const o=await h(a.repositoryRootPath,["add",...G(n,a)]);if(o.exitCode!==0){t.status(400).json({error:o.stderr||"git add file failed"});return}t.json({ok:!0,filePath:n,workspacePath:a.workspacePath,repositoryRootPath:a.repositoryRootPath,relativeWorkspacePath:a.relativeWorkspacePath})}catch(a){const o=a;t.status(500).json({error:o.message||"git add file failed"})}}),S.post("/git/add-files",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePaths:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=(Array.isArray(s)?s:[]).map(o=>R(String(o||"").trim())).filter(o=>o.length>0);if(a.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const c=a.flatMap(d=>G(d,o)),u=["add"],l=[];for(let d=0;d<c.length;d++)c[d]!=="--"&&l.push(c[d]);u.push("--",...l);const f=await h(o.repositoryRootPath,u);if(f.exitCode!==0){t.status(400).json({error:f.stderr||"git add files failed"});return}t.json({ok:!0,filePaths:a,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const c=o;t.status(500).json({error:c.message||"git add files failed"})}}),S.post("/git/commits",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,limit:s,branch:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const a=await j(String(i||e).trim());if(!a.hasRepository||!a.repositoryRootPath){t.json({ok:!0,commits:[],currentBranch:"",branchName:null,workspacePath:a.workspacePath,repositoryRootPath:null});return}const o=Number(s),c=!!r.body?.all,u=c?200:100,l=Number.isInteger(o)&&o>0&&o<=u?o:c?100:30,f=await N(a.repositoryRootPath),p=(n&&String(n).trim()?String(n).trim():null)||f||"HEAD",y="%H%x00%h%x00%an%x00%ad%x00%s%x00%P%x00%b%x00";let g;if(c?g=await h(a.repositoryRootPath,["log",`-${l}`,"--all","--topo-order","--date=iso-strict",`--pretty=format:${y}`]):g=await h(a.repositoryRootPath,["log",`-${l}`,"--date=iso-strict",`--pretty=format:${y}`,"--end-of-options",p,"--",a.relativeWorkspacePath||"."]),g.exitCode!==0){t.status(400).json({error:g.stderr||"git log failed"});return}const w=await U(a.repositoryRootPath),P=bt(g.stdout).map(v=>({...v,isRemote:w.has(v.hash)})),C=c?await J(a.repositoryRootPath):void 0;t.json({ok:!0,commits:P,currentBranch:f,branchName:c?null:p,...C?{branches:C}:{},workspacePath:a.workspacePath,repositoryRootPath:a.repositoryRootPath,relativeWorkspacePath:a.relativeWorkspacePath})}catch(a){const o=a;t.status(500).json({error:o.message||"git commits failed"})}}),S.post("/git/branches",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=await j(String(i||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.json({ok:!0,branches:[],currentBranch:"",workspacePath:s.workspacePath,repositoryRootPath:null});return}const n=await J(s.repositoryRootPath),a=await N(s.repositoryRootPath);t.json({ok:!0,branches:n,currentBranch:a,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const n=s;t.status(500).json({error:n.message||"git branches failed"})}}),S.post("/git/push",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const s=await j(String(i||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const n=await vt(s.repositoryRootPath);t.json({ok:!0,branch:n.branch,pushed:n.pushed,message:n.message,output:n.output,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const n=s;if(n.message&&/上游分支|detached HEAD|分离头指针|不存在|未配置/.test(n.message)){t.status(400).json({error:n.message});return}t.status(500).json({error:n.message||"git push failed"})}});const Gt=/^[0-9a-fA-F]{7,40}$/;function Tt(r){const t=String(r||"").trim();return Gt.test(t)?t:null}async function V(r,t){const e=await N(r);if(!e||e==="HEAD")throw new Error("\u5F53\u524D\u5904\u4E8E detached HEAD\uFF08\u5206\u79BB\u5934\u6307\u9488\uFF09\u72B6\u6001\uFF0C\u65E0\u6CD5\u56DE\u9000");const i=await h(r,["rev-parse","--verify","--quiet",`${t}^{commit}`]);if(i.exitCode!==0||!i.stdout.trim())throw new Error("\u76EE\u6807\u63D0\u4EA4\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u63D0\u4EA4\u5217\u8868\u540E\u91CD\u8BD5");const s=i.stdout.trim();if((await h(r,["rev-parse","HEAD"])).stdout.trim()===s)throw new Error("\u76EE\u6807\u63D0\u4EA4\u5DF2\u662F\u5F53\u524D HEAD\uFF0C\u65E0\u9700\u56DE\u9000");if((await h(r,["merge-base","--is-ancestor",s,"HEAD"])).exitCode!==0)throw new Error("\u76EE\u6807\u63D0\u4EA4\u4E0D\u5728\u5F53\u524D\u5206\u652F\u5386\u53F2\u4E2D\uFF0C\u65E0\u6CD5\u56DE\u9000");const o=await h(r,["rev-list","--count",`${s}..HEAD`]),c=parseInt(o.stdout.trim()||"0",10);let u=!1;const l=await U(r);if(l.size>0){const f=await h(r,["rev-list",`${s}..HEAD`]);if(f.exitCode===0){for(const d of f.stdout.split(`
15
+ `))if(l.has(d.trim())){u=!0;break}}}return{resolvedHash:s,branch:e,resetCount:c,affectsRemote:u}}async function X(r){const t=await h(r,["status","--porcelain"]);return t.exitCode===0&&!!t.stdout.trim()}async function Dt(r,t,e){const i=await V(r,t),s=await X(r);return{mode:e,commitHash:i.resolvedHash,branch:i.branch,resetCount:i.resetCount,hasLocalChanges:s,affectsRemote:i.affectsRemote}}async function Mt(r,t,e,i=""){const s=await V(r,t),n=await X(r),a=e==="hard"?["reset","--hard",s.resolvedHash]:["reset","--soft",s.resolvedHash],o=await h(r,a);if(o.exitCode!==0)throw new Error(o.stderr||o.stdout||"git reset \u6267\u884C\u5931\u8D25");if(e==="hard"){const c=["clean","-fd"];i&&c.push("--",i);const u=await h(r,c);u.exitCode!==0&&m.warn(`[Git] \u786C\u56DE\u9000\u540E\u6E05\u7406\u672A\u8DDF\u8E2A\u6587\u4EF6\u5931\u8D25: cwd=${r}, scope=${i||"(\u6574\u4ED3)"}, error=${u.stderr||u.stdout}`)}return{mode:e,commitHash:s.resolvedHash,branch:s.branch,resetCount:s.resetCount,hasLocalChanges:n,affectsRemote:s.affectsRemote}}S.post("/git/reset",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,commitHash:s,mode:n,preview:a}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const o=Tt(s);if(!o){t.status(400).json({error:"commitHash \u683C\u5F0F\u975E\u6CD5\uFF0C\u65E0\u6CD5\u56DE\u9000"});return}const c=String(n||"").trim();if(c!=="soft"&&c!=="hard"){t.status(400).json({error:"mode must be soft or hard"});return}const u=a===!0||a==="true";try{const l=await j(String(i||e).trim());if(!l.hasRepository||!l.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}if(u){const y=await Dt(l.repositoryRootPath,o,c);t.json({ok:!0,preview:!0,...y,workspacePath:l.workspacePath,repositoryRootPath:l.repositoryRootPath,relativeWorkspacePath:l.relativeWorkspacePath});return}const f=await Mt(l.repositoryRootPath,o,c,l.relativeWorkspacePath),d=f.commitHash.slice(0,7),p=f.mode==="hard"?`\u5DF2\u786C\u56DE\u9000\u5230 ${d}\uFF0C\u5171\u56DE\u9000 ${f.resetCount} \u4E2A\u63D0\u4EA4\uFF0C\u5DE5\u4F5C\u533A\u5DF2\u5B8C\u5168\u6062\u590D\u5230\u8BE5\u63D0\u4EA4\u72B6\u6001`:`\u5DF2\u8F6F\u56DE\u9000\u5230 ${d}\uFF0C\u5171\u56DE\u9000 ${f.resetCount} \u4E2A\u63D0\u4EA4\uFF0C\u5DE5\u4F5C\u533A\u6587\u4EF6\u4FDD\u6301\u4E0D\u53D8`;t.json({ok:!0,...f,message:p,workspacePath:l.workspacePath,repositoryRootPath:l.repositoryRootPath,relativeWorkspacePath:l.relativeWorkspacePath})}catch(l){const f=l;if(f.message&&/回退|快照|detached HEAD|分离头指针|不存在|无需|HEAD/.test(f.message)){t.status(400).json({error:f.message});return}t.status(500).json({error:f.message||"git reset failed"})}}),S.post("/git/commit-diff",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,commitHash:s,aggregate:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=B(s);if(!a){t.status(400).json({error:"commitHash is required"});return}const o=n===!0||n==="true";try{const c=await j(String(i||e).trim());if(!c.hasRepository||!c.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const u=$t(a,o),l=await h(c.repositoryRootPath,A([...u,"--name-status"],c));if(l.exitCode!==0){t.status(400).json({error:l.stderr||"git commit diff failed"});return}const f=await h(c.repositoryRootPath,A([...u,"--numstat"],c));if(f.exitCode!==0){t.status(400).json({error:f.stderr||"git commit diff failed"});return}const d=H(l.stdout,f.stdout);t.json({ok:!0,commitHash:a,isGitRepo:c.isGitRepo,hasRepository:!0,usingParentGitRepo:c.usingParentGitRepo,hasChanges:d.length>0,files:d,workspacePath:c.workspacePath,repositoryRootPath:c.repositoryRootPath,relativeWorkspacePath:c.relativeWorkspacePath})}catch(c){const u=c;t.status(500).json({error:u.message||"git commit diff failed"})}}),S.post("/git/diff-file",k,async(r,t)=>{const{workspacePath:e,filePath:i,commitHash:s,aggregate:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!i||!String(i).trim()){t.status(400).json({error:"filePath is required"});return}try{const a=await j(String(e).trim()),o=R(String(i).trim());if(!a.hasRepository||!a.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const c=E(o,a),u=B(s),l=!!(u&&(n===!0||n==="true")),f=!u||l,d=u?l?["diff",u,"--",c]:["show","--format=","--no-ext-diff",u,"--",c]:["diff","HEAD","--",c];let p=await h(a.repositoryRootPath,d);if(f&&p.exitCode!==0&&(p=await h(a.repositoryRootPath,["diff","--",c])),p.exitCode!==0){t.status(400).json({error:p.stderr||"git diff file failed"});return}if(!u&&!p.stdout.trim()){const g=await h(a.repositoryRootPath,["diff","--cached","--",c]);if(g.exitCode!==0){t.status(400).json({error:g.stderr||"git staged diff file failed"});return}p=g}if(f&&!p.stdout.trim()){const g=x.join(a.repositoryRootPath,c);try{const w=await b.readFile(g,"utf-8");if(w){const P=w.split(`
16
16
  `).map(v=>`+${v}`).join(`
17
17
  `),C=w.split(`
18
18
  `).length;p={stdout:[`diff --git a/${c} b/${c}`,"new file mode 100644","--- /dev/null",`+++ b/${c}`,`@@ -0,0 +1,${C} @@`,P].join(`
19
- `),stderr:"",exitCode:0}}}catch{}}D(c);let y="";if(f){const g=x.join(a.repositoryRootPath,c);try{y=await b.readFile(g,"utf-8")}catch{}}else{const g=await h(a.repositoryRootPath,["show",`${u}:${c}`]);g.exitCode===0&&(y=g.stdout)}t.json({ok:!0,filePath:o,repositoryRelativeFilePath:c,commitHash:u||null,rangeFromCommit:l,diffContent:p.stdout,fileContent:y,workspacePath:a.workspacePath,repositoryRootPath:a.repositoryRootPath,usingParentGitRepo:a.usingParentGitRepo})}catch(a){const o=a;t.status(500).json({error:o.message||"git diff file failed"})}});const Z=["STRICTLY act as a Git commit message generator. NEVER explain, narrate, or describe your task.",'NEVER start your reply with phrases like "\u6211\u4EEC\u88AB\u95EE\u5230", "\u4EE5\u4E0B\u662F", "\u597D\u7684", "Sure", "Here is", "I will", "I can", "Let me", "\u6839\u636E", "\u4E0B\u9762".',"NEVER include any meta-language or thinking out loud. Output ONLY the commit message text.","","Format (Conventional Commits, 1 line, optional body):"," <type>(<scope>): <description>","","Allowed types: feat, fix, refactor, test, docs, style, chore, perf, ci, build, revert.","First line MUST be <= 72 characters. <description> MUST be in Chinese.","Add a body (separated by a blank line) ONLY when the change needs more context."].join(`
19
+ `),stderr:"",exitCode:0}}}catch{}}M(c);let y="";if(f){const g=x.join(a.repositoryRootPath,c);try{y=await b.readFile(g,"utf-8")}catch{}}else{const g=await h(a.repositoryRootPath,["show",`${u}:${c}`]);g.exitCode===0&&(y=g.stdout)}t.json({ok:!0,filePath:o,repositoryRelativeFilePath:c,commitHash:u||null,rangeFromCommit:l,diffContent:p.stdout,fileContent:y,workspacePath:a.workspacePath,repositoryRootPath:a.repositoryRootPath,usingParentGitRepo:a.usingParentGitRepo})}catch(a){const o=a;t.status(500).json({error:o.message||"git diff file failed"})}});const Z=["STRICTLY act as a Git commit message generator. NEVER explain, narrate, or describe your task.",'NEVER start your reply with phrases like "\u6211\u4EEC\u88AB\u95EE\u5230", "\u4EE5\u4E0B\u662F", "\u597D\u7684", "Sure", "Here is", "I will", "I can", "Let me", "\u6839\u636E", "\u4E0B\u9762".',"NEVER include any meta-language or thinking out loud. Output ONLY the commit message text.","","Format (Conventional Commits, 1 line, optional body):"," <type>(<scope>): <description>","","Allowed types: feat, fix, refactor, test, docs, style, chore, perf, ci, build, revert.","First line MUST be <= 72 characters. <description> MUST be in Chinese.","Add a body (separated by a blank line) ONLY when the change needs more context."].join(`
20
20
  `);function Q(r){return["\u8BF7\u76F4\u63A5\u8F93\u51FA\u4E0A\u8FF0\u683C\u5F0F\u7684\u63D0\u4EA4\u4FE1\u606F,\u4E0D\u8981\u4EFB\u4F55\u989D\u5916\u6587\u5B57\u3002","\u4EE5\u4E0B\u662F\u6682\u5B58\u533A diff:","",r].join(`
21
- `)}const tt=/\b(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(?:\s*\([^)]*\))?\s*:/i,Dt=/^(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)\s*:/i;function Ft(r){const t=r.split(`
22
- `)[0]?.trim()??"";if(!t||t.length<3)return!1;const e=(t.match(/[\u4e00-\u9fa5]/g)||[]).length,i=(t.match(/[a-zA-Z]{2,}/g)||[]).length;if(e===0&&i===0)return!1;const s=[/^以下是/,/^上面是/,/^生成的/,/^这是/,/^下面/,/^(?:Here|This|This is|That is)/i];for(const n of s)if(n.test(t))return!1;return e>=2||i>=2}const Ht=[/^\s*(?:我们|我)\s*(?:被\s*)?(?:问[到过]?|需要|要|将|来|先)[^.:\n]{0,60}?(?:提交信息|commit\s*message|[::,,。])/i,/^\s*(?:好的|好)\s*[,,::]/i,/^\s*(?:好的|好)\s*(?:以下是|上面是|提供|生成|给出)[^.:\n]{0,20}?[::]?/i,/^\s*是\s*[::]/i,/^\s*(?:Sure|Here(?:'s|\s+is)|We(?:'re| are)|I\s+(?:will|can|'ll)|Let\s+me|Certainly|Of\s+course)[^.:\n]{0,40}?[,,::.]/i,/^\s*根据\s*(?:您|你|您所|你所|所)?\s*(?:提供|上述|给[我您你])?\s*(?:的)?\s*diff\s*[,,::]?/i,/^\s*(?:以下|下面)[^,,::\n]{0,30}?提交信息\s*[,,::]?/i,/^\s*(?:以下|下面)\s*是\s*[,,::]/i,/^\s*生成\s*(?:一条|一个|了)?\s*(?:符合|新的)?\s*(?:conventional\s*commit|git)?\s*提交信息\s*[,,::]?/i,/^\s*```(?:text|markdown|bash|shell)?\s*\n?/i];function T(r){if(!r)return m.info("[Git/sanitize] \u8F93\u5165\u4E3A\u7A7A, \u8FD4\u56DE fallback"),"chore: update staged changes";let t=r.replace(/\r\n/g,`
23
- `).trim();const e=JSON.stringify(r.substring(0,120)),i=t.match(tt);if(i&&i.index!==void 0&&i.index<=80)t=t.slice(i.index);else{for(let f=0;f<5;f+=1){const d=t;for(const p of Ht)t=t.replace(p,"");if(t=t.replace(/^[\s*\-`,,']+/,"").trim(),t===d)break}const a=t.split(`
24
- `)[0]?.trim()??"",o=!!t.match(tt),c=!!t.match(Dt),u=Ft(t),l=a.length>100;if(!o&&!c&&!u&&!l)return m.info(`[Git/sanitize] \u5265\u79BB\u540E\u65E0\u6709\u6548\u5185\u5BB9, \u8FD4\u56DE fallback. raw=${e}, afterStrip=${JSON.stringify(t.substring(0,100))}`),"chore: update staged changes";!o&&(c||u||l)&&(t=a||t)}const s=t.search(/\n\s*\n/);if(s>0&&(t=t.slice(0,s)),t=t.replace(/\n+\s*```\s*$/g,"").trim(),!t)return m.info(`[Git/sanitize] \u622A\u65AD\u540E\u4E3A\u7A7A, \u8FD4\u56DE fallback. raw=${e}`),"chore: update staged changes";const n=t.split(`
25
- `)[0]?.trim()??"";return n.length>100?(m.info(`[Git/sanitize] \u9996\u884C\u8D85\u8FC7 100 \u5B57\u7B26, \u4EC5\u4FDD\u7559\u9996\u884C. raw=${e}, firstLine=${JSON.stringify(n.substring(0,100))}`),n):(m.info(`[Git/sanitize] \u7ED3\u679C: ${JSON.stringify(t.substring(0,120))} (from raw=${e})`),t)}function et(r){const t=r.split(/\r?\n/);let e="";for(const i of t){const s=i.trim();if(!s.startsWith("data:"))continue;const n=s.slice(5).trim();if(!(!n||n==="[DONE]"))try{const o=JSON.parse(n)?.choices?.[0]?.delta?.content;typeof o=="string"&&o.length>0&&(e+=o)}catch{}}return e}function Nt(r,t){if(t>5)return;const e=r.split(/\r?\n/);for(const i of e){const s=i.trim();if(!s.startsWith("data:"))continue;const n=s.slice(5).trim();if(!(!n||n==="[DONE]"))try{const a=JSON.parse(n),o=Object.keys(a),c=a?.choices,u=Array.isArray(c)?c[0]:null,l=u?.delta?Object.keys(u.delta):null,f=u?.delta?.content;m.info(`[Git/extract] chunk#${t} structure: topKeys=${JSON.stringify(o)}, choicesLen=${Array.isArray(c)?c.length:typeof c}, deltaKeys=${JSON.stringify(l)}, content=${JSON.stringify(f?.substring(0,50))}, finishReason=${JSON.stringify(u?.finish_reason)}`)}catch{}}}async function Wt(r,t,e,i){const s=`${r.replace(/\/+$/,"")}/chat/completions`,o=(await L.post(s,{model:e,stream:!1,messages:[{role:"system",content:Z},{role:"user",content:Q(i)}],max_tokens:200,temperature:.2},{headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},timeout:3e4})).data?.choices?.[0]?.message?.content?.trim()||"";return T(o)}async function Lt(r,t,e,i,s,n){const a=`${r.replace(/\/+$/,"")}/chat/completions`,c=(await L.post(a,{model:e,stream:!0,messages:[{role:"system",content:Z},{role:"user",content:Q(i)}],max_tokens:200,temperature:.2},{headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`,Accept:"text/event-stream"},timeout:Number(process.env.AWS_AI_STREAM_TIMEOUT_MS)||6e4,responseType:"stream",signal:n})).data;let u="",l="",f="",d=0,p=0;return m.info(`[Git/streamAi] AI \u6D41\u5F00\u59CB: aiModel=${e}`),await new Promise((y,g)=>{const w=P=>{c.removeAllListeners("data"),c.removeAllListeners("end"),c.removeAllListeners("error"),c.removeAllListeners("aborted"),m.info(`[Git/streamAi] AI \u6D41\u7ED3\u675F: totalChunks=${d}, totalDeltas=${p}, bufferLength=${u.length}, sanitized=${JSON.stringify(P).substring(0,200)}`),y(P)};c.on("data",P=>{const C=typeof P=="string"?P:P.toString("utf8");d++,f+=C,d<=3&&m.info(`[Git/streamAi] chunk#${d}: length=${C.length}, preview=${JSON.stringify(C.substring(0,80))}`),Nt(C,d);const v=f.split(/\n\n/);f=v.pop()??"";for(const rt of v){const M=et(rt);M&&(p++,u+=M,l=T(u),s(M,l))}}),c.on("end",()=>{if(f.trim()){const P=et(f);P?(m.info(`[Git/streamAi] \u5C3E\u90E8\u6B8B\u7559 delta: length=${P.length}`),u+=P):m.info(`[Git/streamAi] \u5C3E\u90E8\u6B8B\u7559\u65E0 delta, pendingData=${JSON.stringify(f.substring(0,100))}`)}w(T(u))}),c.on("error",P=>{m.error(`[Git/streamAi] \u6D41\u9519\u8BEF: ${P.message}`),g(P)}),c.on("aborted",()=>{m.info(`[Git/streamAi] \u6D41\u88AB\u4E2D\u6B62, bufferLength=${u.length}`),w(T(u))})})}async function W(r,t,e){const i=await j(String(r||t).trim());if(!i.hasRepository||!i.repositoryRootPath)return m.warn(`[Git/collectDiff] \u4E0D\u5728 Git \u4ED3\u5E93\u4E2D: workspacePath=${t}`),{truncatedDiff:"",error:{status:400,message:"workspace is not inside a git repository"}};const s=e.map(u=>u.path).filter(Boolean);m.info(`[Git/collectDiff] \u5F00\u59CB\u6536\u96C6 ${s.length} \u4E2A\u6682\u5B58\u6587\u4EF6\u5DEE\u5F02: ${s.join(", ").substring(0,200)}`);const n=[];for(const u of s){const l=E(u,i),f=await h(i.repositoryRootPath,["diff","--cached","--",l]);f.exitCode===0&&f.stdout.trim()?n.push(`--- ${u} ---
21
+ `)}const tt=/\b(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(?:\s*\([^)]*\))?\s*:/i,Ft=/^(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)\s*:/i;function Ht(r){const t=r.split(`
22
+ `)[0]?.trim()??"";if(!t||t.length<3)return!1;const e=(t.match(/[\u4e00-\u9fa5]/g)||[]).length,i=(t.match(/[a-zA-Z]{2,}/g)||[]).length;if(e===0&&i===0)return!1;const s=[/^以下是/,/^上面是/,/^生成的/,/^这是/,/^下面/,/^(?:Here|This|This is|That is)/i];for(const n of s)if(n.test(t))return!1;return e>=2||i>=2}const Nt=[/^\s*(?:我们|我)\s*(?:被\s*)?(?:问[到过]?|需要|要|将|来|先)[^.:\n]{0,60}?(?:提交信息|commit\s*message|[::,,。])/i,/^\s*(?:好的|好)\s*[,,::]/i,/^\s*(?:好的|好)\s*(?:以下是|上面是|提供|生成|给出)[^.:\n]{0,20}?[::]?/i,/^\s*是\s*[::]/i,/^\s*(?:Sure|Here(?:'s|\s+is)|We(?:'re| are)|I\s+(?:will|can|'ll)|Let\s+me|Certainly|Of\s+course)[^.:\n]{0,40}?[,,::.]/i,/^\s*根据\s*(?:您|你|您所|你所|所)?\s*(?:提供|上述|给[我您你])?\s*(?:的)?\s*diff\s*[,,::]?/i,/^\s*(?:以下|下面)[^,,::\n]{0,30}?提交信息\s*[,,::]?/i,/^\s*(?:以下|下面)\s*是\s*[,,::]/i,/^\s*生成\s*(?:一条|一个|了)?\s*(?:符合|新的)?\s*(?:conventional\s*commit|git)?\s*提交信息\s*[,,::]?/i,/^\s*```(?:text|markdown|bash|shell)?\s*\n?/i];function T(r){if(!r)return m.info("[Git/sanitize] \u8F93\u5165\u4E3A\u7A7A, \u8FD4\u56DE fallback"),"chore: update staged changes";let t=r.replace(/\r\n/g,`
23
+ `).trim();const e=JSON.stringify(r.substring(0,120)),i=t.match(tt);if(i&&i.index!==void 0&&i.index<=80)t=t.slice(i.index);else{for(let f=0;f<5;f+=1){const d=t;for(const p of Nt)t=t.replace(p,"");if(t=t.replace(/^[\s*\-`,,']+/,"").trim(),t===d)break}const a=t.split(`
24
+ `)[0]?.trim()??"",o=!!t.match(tt),c=!!t.match(Ft),u=Ht(t),l=a.length>100;if(!o&&!c&&!u&&!l)return m.info(`[Git/sanitize] \u5265\u79BB\u540E\u65E0\u6709\u6548\u5185\u5BB9, \u8FD4\u56DE fallback. raw=${e}, afterStrip=${JSON.stringify(t.substring(0,100))}`),"chore: update staged changes";!o&&(c||u||l)&&(t=a||t)}const s=t.search(/\n\s*\n/);if(s>0&&(t=t.slice(0,s)),t=t.replace(/\n+\s*```\s*$/g,"").trim(),!t)return m.info(`[Git/sanitize] \u622A\u65AD\u540E\u4E3A\u7A7A, \u8FD4\u56DE fallback. raw=${e}`),"chore: update staged changes";const n=t.split(`
25
+ `)[0]?.trim()??"";return n.length>100?(m.info(`[Git/sanitize] \u9996\u884C\u8D85\u8FC7 100 \u5B57\u7B26, \u4EC5\u4FDD\u7559\u9996\u884C. raw=${e}, firstLine=${JSON.stringify(n.substring(0,100))}`),n):(m.info(`[Git/sanitize] \u7ED3\u679C: ${JSON.stringify(t.substring(0,120))} (from raw=${e})`),t)}function et(r){const t=r.split(/\r?\n/);let e="";for(const i of t){const s=i.trim();if(!s.startsWith("data:"))continue;const n=s.slice(5).trim();if(!(!n||n==="[DONE]"))try{const o=JSON.parse(n)?.choices?.[0]?.delta?.content;typeof o=="string"&&o.length>0&&(e+=o)}catch{}}return e}function Wt(r,t){if(t>5)return;const e=r.split(/\r?\n/);for(const i of e){const s=i.trim();if(!s.startsWith("data:"))continue;const n=s.slice(5).trim();if(!(!n||n==="[DONE]"))try{const a=JSON.parse(n),o=Object.keys(a),c=a?.choices,u=Array.isArray(c)?c[0]:null,l=u?.delta?Object.keys(u.delta):null,f=u?.delta?.content;m.info(`[Git/extract] chunk#${t} structure: topKeys=${JSON.stringify(o)}, choicesLen=${Array.isArray(c)?c.length:typeof c}, deltaKeys=${JSON.stringify(l)}, content=${JSON.stringify(f?.substring(0,50))}, finishReason=${JSON.stringify(u?.finish_reason)}`)}catch{}}}async function Lt(r,t,e,i){const s=`${r.replace(/\/+$/,"")}/chat/completions`,o=(await L.post(s,{model:e,stream:!1,messages:[{role:"system",content:Z},{role:"user",content:Q(i)}],max_tokens:200,temperature:.2},{headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},timeout:3e4})).data?.choices?.[0]?.message?.content?.trim()||"";return T(o)}async function _t(r,t,e,i,s,n){const a=`${r.replace(/\/+$/,"")}/chat/completions`,c=(await L.post(a,{model:e,stream:!0,messages:[{role:"system",content:Z},{role:"user",content:Q(i)}],max_tokens:200,temperature:.2},{headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`,Accept:"text/event-stream"},timeout:Number(process.env.AWS_AI_STREAM_TIMEOUT_MS)||6e4,responseType:"stream",signal:n})).data;let u="",l="",f="",d=0,p=0;return m.info(`[Git/streamAi] AI \u6D41\u5F00\u59CB: aiModel=${e}`),await new Promise((y,g)=>{const w=P=>{c.removeAllListeners("data"),c.removeAllListeners("end"),c.removeAllListeners("error"),c.removeAllListeners("aborted"),m.info(`[Git/streamAi] AI \u6D41\u7ED3\u675F: totalChunks=${d}, totalDeltas=${p}, bufferLength=${u.length}, sanitized=${JSON.stringify(P).substring(0,200)}`),y(P)};c.on("data",P=>{const C=typeof P=="string"?P:P.toString("utf8");d++,f+=C,d<=3&&m.info(`[Git/streamAi] chunk#${d}: length=${C.length}, preview=${JSON.stringify(C.substring(0,80))}`),Wt(C,d);const v=f.split(/\n\n/);f=v.pop()??"";for(const rt of v){const D=et(rt);D&&(p++,u+=D,l=T(u),s(D,l))}}),c.on("end",()=>{if(f.trim()){const P=et(f);P?(m.info(`[Git/streamAi] \u5C3E\u90E8\u6B8B\u7559 delta: length=${P.length}`),u+=P):m.info(`[Git/streamAi] \u5C3E\u90E8\u6B8B\u7559\u65E0 delta, pendingData=${JSON.stringify(f.substring(0,100))}`)}w(T(u))}),c.on("error",P=>{m.error(`[Git/streamAi] \u6D41\u9519\u8BEF: ${P.message}`),g(P)}),c.on("aborted",()=>{m.info(`[Git/streamAi] \u6D41\u88AB\u4E2D\u6B62, bufferLength=${u.length}`),w(T(u))})})}async function W(r,t,e){const i=await j(String(r||t).trim());if(!i.hasRepository||!i.repositoryRootPath)return m.warn(`[Git/collectDiff] \u4E0D\u5728 Git \u4ED3\u5E93\u4E2D: workspacePath=${t}`),{truncatedDiff:"",error:{status:400,message:"workspace is not inside a git repository"}};const s=e.map(u=>u.path).filter(Boolean);m.info(`[Git/collectDiff] \u5F00\u59CB\u6536\u96C6 ${s.length} \u4E2A\u6682\u5B58\u6587\u4EF6\u5DEE\u5F02: ${s.join(", ").substring(0,200)}`);const n=[];for(const u of s){const l=E(u,i),f=await h(i.repositoryRootPath,["diff","--cached","--",l]);f.exitCode===0&&f.stdout.trim()?n.push(`--- ${u} ---
26
26
  ${f.stdout.trim()}`):m.info(`[Git/collectDiff] \u6587\u4EF6\u65E0\u6682\u5B58\u5DEE\u5F02\u6216\u83B7\u53D6\u5931\u8D25: ${u}, exitCode=${f.exitCode}, stdout.length=${f.stdout.length}`)}if(n.length===0){m.info("[Git/collectDiff] \u9010\u6587\u4EF6 diff \u4E3A\u7A7A, \u56DE\u9000\u5230\u5168\u91CF diff --cached");const u=await h(i.repositoryRootPath,["diff","--cached"]);u.exitCode===0&&u.stdout.trim()?(n.push(u.stdout.trim()),m.info(`[Git/collectDiff] \u5168\u91CF diff \u83B7\u53D6\u6210\u529F: length=${u.stdout.trim().length}`)):m.info(`[Git/collectDiff] \u5168\u91CF diff \u4E5F\u4E3A\u7A7A, exitCode=${u.exitCode}`)}if(n.length===0)return m.info("[Git/collectDiff] \u6682\u5B58\u533A\u6CA1\u6709\u5DEE\u5F02\u5185\u5BB9 (diffParts \u4E3A\u7A7A)"),{truncatedDiff:""};const a=n.join(`
27
27
 
28
28
  `),o=15e3,c=a.length>o?`${a.slice(0,o)}
29
29
  ...(diff truncated due to length)`:a;return m.info(`[Git/collectDiff] \u6536\u96C6\u5B8C\u6210: diffContent.length=${a.length}, truncated=${c.length}`),{truncatedDiff:c}}function $(r,t,e){try{r.write(`event: ${t}
30
30
  `),r.write(`data: ${JSON.stringify(e)}
31
31
 
32
- `)}catch{}}S.post("/git/generate-commit-message",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,stagedFiles:s,aiBaseUrl:n,aiApiKey:a,aiModel:o}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(s)||s.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}if(!n||!String(n).trim()||!a||!String(a).trim()||!o||!String(o).trim()){t.status(400).json({error:"aiBaseUrl, aiApiKey and aiModel are required"});return}try{const{truncatedDiff:c,error:u}=await W(String(i||""),String(e),s);if(u){t.status(u.status).json({error:u.message});return}if(!c){t.json({message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"});return}const l=await Wt(n,a,o,c);t.json({message:l})}catch(c){const u=c;m.error(`[Git] generate commit message failed: ${u.message}`),t.status(500).json({error:u.message||"generate commit message failed"})}}),S.post("/git/generate-commit-message-stream",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,stagedFiles:s,aiBaseUrl:n,aiApiKey:a,aiModel:o}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(s)||s.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}if(!n||!String(n).trim()||!a||!String(a).trim()||!o||!String(o).trim()){t.status(400).json({error:"aiBaseUrl, aiApiKey and aiModel are required"});return}m.info(`[Git/SSE] \u6536\u5230 generate-commit-message-stream \u8BF7\u6C42: workspacePath=${String(e||"").substring(0,40)}, stagedFiles=${s?.length??0}, aiModel=${o}, aiBaseUrl=${n}`),t.status(200),t.setHeader("Content-Type","text/event-stream; charset=utf-8"),t.setHeader("Cache-Control","no-cache, no-transform"),t.setHeader("Connection","keep-alive"),t.setHeader("X-Accel-Buffering","no"),r.socket?.setNoDelay(!0),t.flushHeaders?.();let c=!1,u=null;const l={delta:0},f=`${r.ip}:${r.socket?.remotePort??"?"}`;r.on("close",()=>{c||(c=!0,m.info(`[Git/SSE] \u5BA2\u6237\u7AEF\u65AD\u5F00\u8FDE\u63A5: workspacePath=${String(e||"").substring(0,40)}, deltaEventsSent=${l.delta}, peer=${f}`))}),$(t,"init",{status:"generating"});try{const{truncatedDiff:d,error:p}=await W(String(i||""),String(e),s);if(p){m.warn(`[Git/SSE] collectStagedDiffForAi \u8FD4\u56DE\u9519\u8BEF: ${p.message}`),$(t,"error",{message:p.message}),t.end();return}if(!d){m.info("[Git/SSE] \u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9, \u76F4\u63A5\u8FD4\u56DE done"),$(t,"delta",{contentDelta:"",message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"}),$(t,"done",{message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"}),t.end();return}m.info(`[Git/SSE] \u6536\u96C6\u5230 diff: truncatedDiff.length=${d.length}`),c&&m.info("[Git/SSE] \u5BA2\u6237\u7AEF\u5728 collectDiff \u671F\u95F4\u65AD\u5F00, \u4F46\u4ECD\u5C1D\u8BD5 AI \u8C03\u7528"),m.info(`[Git/SSE] \u5F00\u59CB\u8C03\u7528 AI streamAiForCommitMessage: aiModel=${o}`),u=new AbortController;let y="";const g=await Lt(n,a,o,d,(w,P)=>{l.delta++,P!==y?(y=P,$(t,"delta",{contentDelta:w,message:P})):$(t,"delta",{contentDelta:w,message:P})},u.signal);m.info(`[Git/SSE] AI streamAiForCommitMessage \u5B8C\u6210: finalMessage=${JSON.stringify(g).substring(0,200)}, deltaEventsSent=${l.delta}`),c||$(t,"done",{message:g}),t.end()}catch(d){const p=d;m.error(`[Git/SSE] stream commit message failed: ${p.message}${p.stack?`
33
- `+p.stack:""}`),c||$(t,"error",{message:p.message||"generate commit message failed"}),t.end()}}),S.post("/git/unstage-files",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePaths:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=(Array.isArray(s)?s:[]).map(o=>R(String(o||"").trim())).filter(o=>o.length>0);if(a.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}let c="";for(const u of a){const l=G(u,o),f=await h(o.repositoryRootPath,["restore","--staged",...l]);f.exitCode!==0&&(c=f.stderr||`git restore --staged failed for ${u}`)}if(c){t.status(400).json({error:c});return}t.json({ok:!0,filePaths:a,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const c=o;t.status(500).json({error:c.message||"git unstage files failed"})}}),S.post("/git/restore-files",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePaths:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=(Array.isArray(s)?s:[]).map(o=>R(String(o||"").trim())).filter(o=>o.length>0);if(a.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}let c="";for(const u of a){const l=E(u,o),f=G(u,o),p=(await h(o.repositoryRootPath,["ls-files","--error-unmatch","--",l])).exitCode===0,y=p?await h(o.repositoryRootPath,["restore",...f]):await h(o.repositoryRootPath,["clean","-f",...f]);y.exitCode!==0&&(c=y.stderr||`${p?"git restore":"git clean -f"} failed for ${u}`)}if(c){t.status(400).json({error:c});return}t.json({ok:!0,filePaths:a,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const c=o;t.status(500).json({error:c.message||"git restore files failed"})}});function _t(r){const t=r.split(`
32
+ `)}catch{}}S.post("/git/generate-commit-message",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,stagedFiles:s,aiBaseUrl:n,aiApiKey:a,aiModel:o}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(s)||s.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}if(!n||!String(n).trim()||!a||!String(a).trim()||!o||!String(o).trim()){t.status(400).json({error:"aiBaseUrl, aiApiKey and aiModel are required"});return}try{const{truncatedDiff:c,error:u}=await W(String(i||""),String(e),s);if(u){t.status(u.status).json({error:u.message});return}if(!c){t.json({message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"});return}const l=await Lt(n,a,o,c);t.json({message:l})}catch(c){const u=c;m.error(`[Git] generate commit message failed: ${u.message}`),t.status(500).json({error:u.message||"generate commit message failed"})}}),S.post("/git/generate-commit-message-stream",k,async(r,t)=>{at();const{workspacePath:e,repositoryPath:i,stagedFiles:s,aiBaseUrl:n,aiApiKey:a,aiModel:o}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(s)||s.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}if(!n||!String(n).trim()||!a||!String(a).trim()||!o||!String(o).trim()){t.status(400).json({error:"aiBaseUrl, aiApiKey and aiModel are required"});return}m.info(`[Git/SSE] \u6536\u5230 generate-commit-message-stream \u8BF7\u6C42: workspacePath=${String(e||"").substring(0,40)}, stagedFiles=${s?.length??0}, aiModel=${o}, aiBaseUrl=${n}`),t.status(200),t.setHeader("Content-Type","text/event-stream; charset=utf-8"),t.setHeader("Cache-Control","no-cache, no-transform"),t.setHeader("Connection","keep-alive"),t.setHeader("X-Accel-Buffering","no"),r.socket?.setNoDelay(!0),t.flushHeaders?.();let c=!1,u=null;const l={delta:0},f=`${r.ip}:${r.socket?.remotePort??"?"}`;r.on("close",()=>{c||(c=!0,m.info(`[Git/SSE] \u5BA2\u6237\u7AEF\u65AD\u5F00\u8FDE\u63A5: workspacePath=${String(e||"").substring(0,40)}, deltaEventsSent=${l.delta}, peer=${f}`))}),$(t,"init",{status:"generating"});try{const{truncatedDiff:d,error:p}=await W(String(i||""),String(e),s);if(p){m.warn(`[Git/SSE] collectStagedDiffForAi \u8FD4\u56DE\u9519\u8BEF: ${p.message}`),$(t,"error",{message:p.message}),t.end();return}if(!d){m.info("[Git/SSE] \u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9, \u76F4\u63A5\u8FD4\u56DE done"),$(t,"delta",{contentDelta:"",message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"}),$(t,"done",{message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"}),t.end();return}m.info(`[Git/SSE] \u6536\u96C6\u5230 diff: truncatedDiff.length=${d.length}`),c&&m.info("[Git/SSE] \u5BA2\u6237\u7AEF\u5728 collectDiff \u671F\u95F4\u65AD\u5F00, \u4F46\u4ECD\u5C1D\u8BD5 AI \u8C03\u7528"),m.info(`[Git/SSE] \u5F00\u59CB\u8C03\u7528 AI streamAiForCommitMessage: aiModel=${o}`),u=new AbortController;let y="";const g=await _t(n,a,o,d,(w,P)=>{l.delta++,P!==y?(y=P,$(t,"delta",{contentDelta:w,message:P})):$(t,"delta",{contentDelta:w,message:P})},u.signal);m.info(`[Git/SSE] AI streamAiForCommitMessage \u5B8C\u6210: finalMessage=${JSON.stringify(g).substring(0,200)}, deltaEventsSent=${l.delta}`),c||$(t,"done",{message:g}),t.end()}catch(d){const p=d;m.error(`[Git/SSE] stream commit message failed: ${p.message}${p.stack?`
33
+ `+p.stack:""}`),c||$(t,"error",{message:p.message||"generate commit message failed"}),t.end()}}),S.post("/git/unstage-files",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePaths:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=(Array.isArray(s)?s:[]).map(o=>R(String(o||"").trim())).filter(o=>o.length>0);if(a.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}let c="";for(const u of a){const l=G(u,o),f=await h(o.repositoryRootPath,["restore","--staged",...l]);f.exitCode!==0&&(c=f.stderr||`git restore --staged failed for ${u}`)}if(c){t.status(400).json({error:c});return}t.json({ok:!0,filePaths:a,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const c=o;t.status(500).json({error:c.message||"git unstage files failed"})}}),S.post("/git/restore-files",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePaths:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const a=(Array.isArray(s)?s:[]).map(o=>R(String(o||"").trim())).filter(o=>o.length>0);if(a.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}let c="";for(const u of a){const l=E(u,o),f=G(u,o),p=(await h(o.repositoryRootPath,["ls-files","--error-unmatch","--",l])).exitCode===0,y=p?await h(o.repositoryRootPath,["restore",...f]):await h(o.repositoryRootPath,["clean","-f",...f]);y.exitCode!==0&&(c=y.stderr||`${p?"git restore":"git clean -f"} failed for ${u}`)}if(c){t.status(400).json({error:c});return}t.json({ok:!0,filePaths:a,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const c=o;t.status(500).json({error:c.message||"git restore files failed"})}});function Ot(r){const t=r.split(`
34
34
  `),e=[];for(let i=0;i<t.length;i++){const s=t[i],n=s.endsWith("\r")?"\r":"",o=(n?s.slice(0,-1):s).match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);if(!o){e.push(s);continue}let c=0,u=0;for(let l=i+1;l<t.length;l++){const f=t[l].replace(/\r$/,"");if(/^diff --git /.test(f)||/^@@ /.test(f))break;f.startsWith("+")?u++:f.startsWith("-")?c++:f.startsWith(" ")&&(c++,u++)}e.push(`@@ -${o[1]},${c} +${o[2]},${u} @@${n}`)}return e.join(`
35
- `)}S.post("/git/revert-hunk",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePath:s,patch:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!s||!String(s).trim()){t.status(400).json({error:"filePath is required"});return}const a=String(n||"").replace(/^\uFEFF/,"").trim();if(!a){t.status(400).json({error:"patch is required"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const c=R(String(s).trim()),u=E(c,o);if(D(u),(a.match(/^diff --git /gm)||[]).length!==1){t.status(400).json({error:"patch must contain exactly one file diff"});return}const f=a.match(/^\+\+\+ b\/(.+)$/m),d=f?R(f[1].trim()):"";if(!d||d!==u){t.status(400).json({error:"patch target path does not match filePath"});return}let p=null;try{p=await b.mkdtemp(x.join(ot.tmpdir(),"agentswork-git-revert-"));const y=x.join(p,"revert.patch");await b.writeFile(y,_t(a)+`
36
- `,"utf-8");const g=await h(o.repositoryRootPath,["apply","--reverse",y]);if(g.exitCode!==0){t.status(400).json({error:g.stderr||"git apply --reverse failed; \u8BE5\u5904\u5185\u5BB9\u53EF\u80FD\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u5DEE\u5F02\u540E\u91CD\u8BD5"});return}t.json({ok:!0,filePath:c,repositoryRelativeFilePath:u,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath})}finally{p&&await b.rm(p,{recursive:!0,force:!0}).catch(()=>{})}}catch(o){const c=o;t.status(500).json({error:c.message||"git revert hunk failed"})}}),S.post("/git/diff-content",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,stagedFiles:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(s)||s.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}try{const{truncatedDiff:n,error:a}=await W(String(i||""),String(e),s);if(a){t.status(a.status).json({error:a.message});return}t.json({ok:!0,diff:n||""})}catch(n){const a=n;m.error(`[Git/diff-content] \u83B7\u53D6\u6682\u5B58\u533A\u5DEE\u5F02\u5931\u8D25: ${a.message}`),t.status(500).json({error:a.message||"get diff content failed"})}});export{ct as assertGitWorkspacePathAccessible,bt as buildCommitDiffBaseArgs,Wt as callAiForCommitMessage,St as collectUntrackedFiles,At as commitSelectedFiles,kt as countUntrackedFileAdditions,pt as createAgentsWorkStashSnapshotRef,vt as createGitStashSnapshot,ht as createMissingInitialCommitSnapshotError,G as createScopedFilePathspecArgs,jt as createScopedGitPathspecArgs,Nt as debugAiStreamChunk,et as extractOpenAiStreamDelta,S as gitRouter,ft as isGitStashMissingInitialCommitError,dt as isGitStashNoChangesOutput,J as listGitBranches,Rt as mergeGitDiffSummaryFiles,xt as normalizeGitCommitMessage,H as parseGitDiffSummary,_ as parseGitStashListLine,Pt as parseGitStatusStagedPaths,wt as parseGitStatusUntrackedPaths,I as parseLatestGitStash,z as persistAgentsWorkStashSnapshotRef,Tt as previewRepositoryReset,$t as pushBranchToRemote,Mt as resetRepositoryTo,N as resolveCurrentBranchName,nt as resolveGitCommandTimeoutMs,gt as resolveGitPushTimeoutMs,U as resolveRemoteCommitHashes,T as sanitizeCommitMessage,lt as shouldIncludeGitDiffSummaryFile,Lt as streamAiForCommitMessage};
35
+ `)}S.post("/git/revert-hunk",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,filePath:s,patch:n}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!s||!String(s).trim()){t.status(400).json({error:"filePath is required"});return}const a=String(n||"").replace(/^\uFEFF/,"").trim();if(!a){t.status(400).json({error:"patch is required"});return}try{const o=await j(String(i||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const c=R(String(s).trim()),u=E(c,o);if(M(u),(a.match(/^diff --git /gm)||[]).length!==1){t.status(400).json({error:"patch must contain exactly one file diff"});return}const f=a.match(/^\+\+\+ b\/(.+)$/m),d=f?R(f[1].trim()):"";if(!d||d!==u){t.status(400).json({error:"patch target path does not match filePath"});return}let p=null;try{p=await b.mkdtemp(x.join(ot.tmpdir(),"agentswork-git-revert-"));const y=x.join(p,"revert.patch");await b.writeFile(y,Ot(a)+`
36
+ `,"utf-8");const g=await h(o.repositoryRootPath,["apply","--reverse",y]);if(g.exitCode!==0){t.status(400).json({error:g.stderr||"git apply --reverse failed; \u8BE5\u5904\u5185\u5BB9\u53EF\u80FD\u5DF2\u53D8\u5316\uFF0C\u8BF7\u5237\u65B0\u5DEE\u5F02\u540E\u91CD\u8BD5"});return}t.json({ok:!0,filePath:c,repositoryRelativeFilePath:u,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath})}finally{p&&await b.rm(p,{recursive:!0,force:!0}).catch(()=>{})}}catch(o){const c=o;t.status(500).json({error:c.message||"git revert hunk failed"})}}),S.post("/git/diff-content",k,async(r,t)=>{const{workspacePath:e,repositoryPath:i,stagedFiles:s}=r.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(s)||s.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}try{const{truncatedDiff:n,error:a}=await W(String(i||""),String(e),s);if(a){t.status(a.status).json({error:a.message});return}t.json({ok:!0,diff:n||""})}catch(n){const a=n;m.error(`[Git/diff-content] \u83B7\u53D6\u6682\u5B58\u533A\u5DEE\u5F02\u5931\u8D25: ${a.message}`),t.status(500).json({error:a.message||"get diff content failed"})}});export{ut as assertGitWorkspacePathAccessible,$t as buildCommitDiffBaseArgs,Lt as callAiForCommitMessage,Rt as collectUntrackedFiles,Et as commitSelectedFiles,St as countUntrackedFileAdditions,mt as createAgentsWorkStashSnapshotRef,At as createGitStashSnapshot,dt as createMissingInitialCommitSnapshotError,G as createScopedFilePathspecArgs,Ct as createScopedGitPathspecArgs,Wt as debugAiStreamChunk,et as extractOpenAiStreamDelta,S as gitRouter,ht as isGitStashMissingInitialCommitError,pt as isGitStashNoChangesOutput,J as listGitBranches,xt as mergeGitDiffSummaryFiles,jt as normalizeGitCommitMessage,H as parseGitDiffSummary,_ as parseGitStashListLine,wt as parseGitStatusStagedPaths,kt as parseGitStatusUntrackedPaths,I as parseLatestGitStash,z as persistAgentsWorkStashSnapshotRef,Dt as previewRepositoryReset,vt as pushBranchToRemote,Mt as resetRepositoryTo,N as resolveCurrentBranchName,ct as resolveGitCommandTimeoutMs,yt as resolveGitPushTimeoutMs,U as resolveRemoteCommitHashes,T as sanitizeCommitMessage,ft as shouldIncludeGitDiffSummaryFile,_t as streamAiForCommitMessage};
37
37
 
@@ -1,2 +1,2 @@
1
- import{spawn as A}from"node:child_process";import{createHash as W,timingSafeEqual as G}from"node:crypto";import J from"node:fs/promises";import b from"node:path";import{fileURLToPath as _}from"node:url";import O from"axios";import{Router as Q}from"express";import{adapterRegistry as Y}from"../adapter/index.js";import{getBridgeVersion as R,resolveSchedulerBaseUrlFrom as Z}from"../config.js";import{validateToken as h,validateTokenOrPanelSession as T}from"../middleware/auth.js";import{getConfiguredConnectionKeys as x,getRegistrationState as ee,requestRuntimeAccessTokenRefreshForServer as te,resolveSchedulerBaseUrlFromToken as re}from"../services/auto-register.js";import{getEffective as v,getOverrides as j}from"../services/capability-config.js";import{appendChangelogSection as se,readChangelog as ne}from"../services/changelog.js";import{setAgentFeatures as oe,setAgentCapabilities as ae,getAgentFeatures as C}from"../services/instance-features.js";import{initInstance as ie}from"../services/instance-init-service.js";import{loadInstanceState as U,saveInstanceState as L}from"../services/instance-state.js";import{createBridgeLifecycleOperation as P,deliverPendingBridgeLifecycleNotifications as $,loadLifecycleState as ce,removeBridgeLifecycleOperation as de,upsertBridgeLifecycleOperation as k}from"../services/lifecycle-state.js";import{mcpDeployManager as le}from"../services/mcp-deploy.js";import{pluginManager as ue}from"../services/plugin-manager.js";import{getRuntimeAccessToken as ge}from"../services/runtime-binding.js";import{skillManager as pe}from"../services/skill-manager.js";import{detectToolStatuses as E,ensureToolsInstalled as fe,SUPPORTED_INSTALLABLE_TOOLS as D,SUPPORTED_UNINSTALLABLE_TOOLS as N,uninstallTools as me}from"../services/tool-installer.js";import{appendUpdateLog as m,readUpdateLog as he}from"../services/update-log.js";import{createLogger as Se}from"../utils/logger.js";import{checkBridgeUpdate as F}from"../utils/npm-version.js";const c=Se("instance");import{buildToolStatusDetectionTargets as ye}from"./instance-tool-status.js";let I=null;function nt(r){I=r}const g=Q();function M(r){return String(r||"").trim()||void 0}function q(r){const t=r.get("host");if(t)return`${r.protocol}://${t}`}function K(){const r=b.resolve(b.dirname(_(import.meta.url)),"..","..");return b.join(r,"dist","index.js")}function we(){return new Promise((r,t)=>{const e=A(process.execPath,[K(),"update","--no-kill"],{shell:!1,windowsHide:!0});let s="",n="";e.stdout.on("data",o=>{s+=o.toString("utf8")}),e.stderr.on("data",o=>{n+=o.toString("utf8")}),e.on("error",t),e.on("close",o=>{r({exitCode:o,stdout:s,stderr:n})})})}function z(){const r=A(process.execPath,[K(),"service","restart"],{shell:!1,windowsHide:!0,stdio:["ignore","pipe","pipe"]});r.stdout.on("data",t=>{m(String(t)).catch(()=>{})}),r.stderr.on("data",t=>{m(String(t)).catch(()=>{})})}async function be(){try{const r=b.join(b.dirname(b.dirname(_(import.meta.url))),"package.json"),t=JSON.parse(await J.readFile(r,{encoding:"utf8"}));return(typeof t.version=="string"?t.version.trim():"")||null}catch{return null}}const V=3e3;async function ke(r){const t={...r,status:"running",message:"Bridge \u66F4\u65B0\u6B63\u5728\u540E\u53F0\u6267\u884C\u3002"};await k(t);try{c.info("[update-bridge] \u6B63\u5728\u540E\u53F0\u6267\u884C awsb update --yes"),await m(`===== Bridge \u66F4\u65B0\u5F00\u59CB\uFF1Av${R()} -> latest\uFF08\u64CD\u4F5C ${r.operationId}\uFF09 =====`);const e=await we();if(e.stdout&&await m(e.stdout),e.stderr&&await m(e.stderr),e.exitCode===0){const s=await be(),n=r.latestVersion||s||R();await se(n,`Bridge \u5DF2\u66F4\u65B0\u81F3 ${n}\u3002`).catch(a=>{const i=a;c.warn(`[update-bridge] \u8FFD\u52A0 changelog \u6BB5\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09: ${i.message}`)}),await m("===== Bridge \u66F4\u65B0\u6210\u529F\uFF0C\u51C6\u5907\u81EA\u52A8\u91CD\u542F ===== ");const o={...t,status:"restarting",message:"Bridge \u66F4\u65B0\u5B8C\u6210\uFF0C\u6B63\u5728\u91CD\u542F..."};await k(o),c.info("[update-bridge] \u6B63\u5728\u6267\u884C awsb service restart"),z(),setTimeout(()=>{if(process.exitCode=42,I){I("BRIDGE_UPDATE_RESTART",!0).catch(a=>{const i=a;c.error("[update-bridge] graceful shutdown failed:",i),process.exit(42)});return}process.exit(42)},V)}else await m(`===== Bridge \u66F4\u65B0\u5931\u8D25\uFF1A\u9000\u51FA\u7801 ${e.exitCode??"unknown"} =====`),await k({...t,status:"failed",message:`Bridge \u66F4\u65B0\u5931\u8D25\uFF0Cnpm \u9000\u51FA\u7801: ${e.exitCode??"unknown"}`,error:e.stderr||e.stdout}),await $()}catch(e){const s=e;c.error("[update-bridge] background update failed:",s),await m(`===== Bridge \u66F4\u65B0\u5F02\u5E38\uFF1A${s.message} =====`).catch(()=>{}),await k({...t,status:"failed",message:"Bridge \u66F4\u65B0\u5931\u8D25\u3002",error:s.message||"update bridge failed"}),await $()}}async function X(r){const t=await import("node:fs/promises"),e=await import("node:path"),s=await import("node:os"),n=e.join(s.tmpdir(),"agentswork-runtime-bridge",".preserve-sessions");if(r){await t.mkdir(e.dirname(n),{recursive:!0}),await t.writeFile(n,JSON.stringify({preserveSessions:!0,timestamp:new Date().toISOString()}),"utf-8"),c.info(`[prepare-restart] \u5DF2\u521B\u5EFA\u4FDD\u7559\u4F1A\u8BDD\u6807\u8BB0\u6587\u4EF6: ${n}`);return}try{await t.unlink(n),c.info("[prepare-restart] \u5DF2\u5220\u9664\u4FDD\u7559\u4F1A\u8BDD\u6807\u8BB0\u6587\u4EF6")}catch{}}function Be(r){return W("md5").update(r,"utf8").digest("hex")}function Ie(r){const t=r.trim().toLowerCase();if(!/^[a-f0-9]{32}$/.test(t))return!1;const e=Buffer.from(t,"hex");return x().map(s=>s.trim()).filter(Boolean).some(s=>{const n=Buffer.from(Be(s),"hex");return n.length===e.length&&G(n,e)})}function Re(r){const e=x().length>0,s=R();return e?Ie(r)?{status:200,body:{ok:!0,runtimeBridge:"healthy",version:s,connectionKeyMatched:!0,connectionKeyRequired:!0}}:{status:401,body:{ok:!1,error:"connection_key_mismatch",version:s,connectionKeyMatched:!1,connectionKeyRequired:!0}}:{status:200,body:{ok:!0,runtimeBridge:"healthy",version:s,connectionKeyMatched:!0,connectionKeyRequired:!1}}}function Te(r,t){const s=t.trim()||"http://localhost:8080",n=/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?(?:\/|$)/i.test(s);return{ok:!1,error:r.message||"scheduler ping failed",failureStage:"scheduler_ping",runtimeBridge:"healthy",schedulerBaseUrl:s,hint:n?"aws-runtime-bridge \u5DF2\u8FDE\u901A\uFF0C\u4F46\u5B83\u56DE\u8FDE\u8C03\u5EA6\u4E2D\u5FC3\u5931\u8D25\u3002\u8BF7\u5728 bridge \u542F\u52A8\u73AF\u5883\u4E2D\u5C06 AWS_RUNTIME_SCHEDULER_BASE_URL \u914D\u7F6E\u4E3A\u8BE5\u5B9E\u4F8B\u53EF\u8BBF\u95EE\u7684\u8C03\u5EA6\u4E2D\u5FC3\u5730\u5740\uFF0C\u4F8B\u5982 http://<server-host>:7380\u3002":"aws-runtime-bridge \u5DF2\u8FDE\u901A\uFF0C\u4F46\u5B83\u56DE\u8FDE\u8C03\u5EA6\u4E2D\u5FC3\u5931\u8D25\u3002\u8BF7\u786E\u8BA4 AWS_RUNTIME_SCHEDULER_BASE_URL \u6307\u5411\u7684\u8C03\u5EA6\u4E2D\u5FC3\u5730\u5740\u53EF\u4ECE bridge \u673A\u5668\u8BBF\u95EE\uFF0C\u5E76\u4E14\u8FD0\u884C\u65F6\u8BBF\u95EE\u4EE4\u724C\u4ECD\u6709\u6548\u3002"}}function ve(r){if(typeof r!="object"||r===null)return;const t=Reflect.get(r,"response");if(typeof t!="object"||t===null)return;const e=Reflect.get(t,"status");return typeof e=="number"?e:void 0}async function je(r,t){try{const e=await O.get(`${r}/api/runtime/ping`,{headers:{"X-Runtime-Token":t}});return{schedulerReachable:e.status>=200&&e.status<300,tokenRefreshed:!1}}catch(e){if(ve(e)!==401)throw e;const s=await te(r);if(!s.success||!s.runtimeAccessToken)throw e;const n=await O.get(`${r}/api/runtime/ping`,{headers:{"X-Runtime-Token":s.runtimeAccessToken}});return{schedulerReachable:n.status>=200&&n.status<300,tokenRefreshed:!0}}}function Ue(r){return{ok:!1,error:"ambiguous_scheduler_base_url",failureStage:"scheduler_ping",runtimeBridge:"healthy",schedulerBaseUrl:"",autoRegisterTargetUrls:r,hint:"Multiple autoRegisterTargets.serverUrl values were found, so bridge cannot safely infer which scheduler /runtime/ping should call back. Set AWS_RUNTIME_SCHEDULER_BASE_URL explicitly, or use future schedulerId/token-bound multi-scheduler routing."}}function Ee(r){return Object.fromEntries(Object.entries(r||{}).map(([t,e])=>[t,{installed:!!e?.installed,version:e?.version||null,executable:e?.executable||null,error:e?.error||null}]))}g.get("/check-update",async(r,t)=>{t.json(await F())}),g.get("/update-log",T,async(r,t)=>{try{const e=await he();t.json({ok:!0,...e})}catch(e){const s=e;c.error("[update-log] Error:",s),t.status(500).json({ok:!1,error:s.message||"read update log failed"})}}),g.get("/changelog",T,async(r,t)=>{const e=String(r.query?.version||"").trim();if(!e){t.status(400).json({ok:!1,error:"version is required"});return}try{const s=await ne(e);t.json({ok:!0,version:e,...s})}catch(s){const n=s;c.error("[changelog] Error:",n),t.status(500).json({ok:!1,error:n.message||"read changelog failed"})}}),g.get("/healthz",(r,t)=>{t.json({ok:!0,runtimeBridge:"healthy"})}),g.get("/connection-check",(r,t)=>{const e=String(r.header("X-Bridge-Connection-Key-MD5")||""),s=Re(e);t.status(s.status).json(s.body)}),g.get("/ping",h,async(r,t)=>{let e=Z(r.header("X-Scheduler-Base-Url"),r.header("X-Bridge-Instance-Id"),r.header("X-Bridge-Instance-Name"));if(e.source==="ambiguous-auto-register-targets"){const n=re(r.header("X-Runtime-Token"));if(n)e={url:n,source:"token-match",ambiguousTargetUrls:[]};else{const o=r.header("X-Bridge-Instance-Id");if(o){const a=ee(),i=Object.entries(a.registrations).find(([,l])=>l===o)?.[0];i&&(e={url:i,source:"instance-id-match",ambiguousTargetUrls:[]})}if(!e.url||e.source==="ambiguous-auto-register-targets"){t.status(400).json(Ue(e.ambiguousTargetUrls));return}}}const s=e.url;try{const n=ge(s);if(!n){t.status(401).json({ok:!1,error:"runtime_access_token_required"});return}const o=await je(s,n);t.json({ok:!0,runtimeBridge:"healthy",version:R(),schedulerReachable:o.schedulerReachable,tokenRefreshed:o.tokenRefreshed,schedulerBaseUrl:s,instanceId:e.instanceId,instanceName:e.instanceName})}catch(n){const o=n;t.status(502).json(Te(o,s))}}),g.post("/init-instance",h,async(r,t)=>{const{agentId:e,workspacePath:s,skillEnabled:n,mcpEnabled:o,ccSwitchEnabledTools:a,skillPackage:i,skillPackages:l,pluginPackages:f,mcpServers:S}=r.body||{};if(!e||!s){t.status(400).json({error:"agentId and workspacePath are required"});return}try{c.info("[init-instance] Request received",{agentId:String(e),workspacePath:String(s),skillEnabled:n,mcpEnabled:o,ccSwitchEnabledTools:a});const u=await ie(e,s,{skillEnabled:n,mcpEnabled:o,ccSwitchEnabledTools:a,skillPackage:i,skillPackages:l,pluginPackages:f,mcpServers:S});c.info("[init-instance] Initialization completed",{agentId:String(e),ok:u.ok,logCount:u.logs?.length||0,enabledTools:u.enabledTools}),t.json(u)}catch(u){const p=u;c.error("[init-instance] Initialization failed",{agentId:String(e),workspacePath:String(s),error:p?.message||String(u),stack:p?.stack||null}),t.status(400).json({error:p?.message||"initialize instance failed"})}}),g.post("/features",h,async(r,t)=>{const{agentId:e,features:s}=r.body||{};if(!e){t.status(400).json({error:"agentId is required"});return}oe(String(e),s),r.body.capabilities!==void 0&&ae(String(e),r.body.capabilities),c.info("[instance/features] \u80FD\u529B\u5F00\u5173\u5DF2\u66F4\u65B0",{agentId:String(e),features:C(String(e))});let n=0;for(const o of Y.getAll())if(typeof o.applyAgentFeatures=="function")try{await o.applyAgentFeatures(String(e)),n+=1}catch(a){c.error("[instance/features] adapter \u70ED\u540C\u6B65\u5931\u8D25",{agentId:String(e),providerId:o.providerId,error:a?.message||String(a)})}t.json({ok:!0,agentId:String(e),features:C(String(e)),hotSyncedAdapters:n})}),g.post("/cc-switch/state",h,async(r,t)=>{const{agentId:e}=r.body||{};if(!e){t.status(400).json({error:"agentId is required"});return}try{c.info("[cc-switch/state] Request received",{agentId:String(e)});const s=await U(e),n=ye(s.enabledTools||[]);c.info("[cc-switch/state] Detecting SDK statuses",{agentId:String(e),detectionTargets:n});const o=await E(n),a=await ue.listDeployed(),i=pe.listDeployed(),l=le.listDeployed(),f=j(e,"skills"),S=j(e,"mcp"),u=j(e,"plugins"),p=i.map(d=>({...d,enabled:v(e,"skills",d.skillName),source:f[d.skillName]!==void 0?"agent":"bridge"})),y=l.map(d=>({...d,enabled:v(e,"mcp",d.name),source:S[d.name]!==void 0?"agent":"bridge"})),w=a.map(d=>({...d,enabled:v(e,"plugins",d.pluginName),source:u[d.pluginName]!==void 0?"agent":"bridge"}));c.info("[cc-switch/state] SDK status detection completed",{agentId:String(e),toolStatus:Ee(o)}),t.json({ok:!0,agentId:String(e),imported:{mcpServers:0,skills:0},deployedPlugins:w,deployedSkills:p,deployedMcp:y,state:{...s,toolStatus:o}})}catch(s){const n=s;c.error("[cc-switch/state] Failed to load state",{agentId:String(e),error:n?.message||String(s),stack:n?.stack||null}),t.status(400).json({error:n?.message||"load state failed"})}}),g.post("/tools/install",h,async(r,t)=>{const{agentId:e,tools:s}=r.body||{};if(!e){t.status(400).json({error:"agentId is required"});return}const n=Array.isArray(s)?s.map(i=>String(i||"").trim().toLowerCase()).filter(Boolean):[],o=new Set(D),a=n.filter(i=>o.has(i));if(a.length===0){t.status(400).json({error:`tools must include ${D.join(", ")}`});return}try{const i=await U(e),l=Array.from(new Set([...i.enabledTools||[],...a])),f=await fe(a),u={...await E(l),...f},p=await L(e,{...i,enabledTools:l,toolStatus:u}),y=new Set(a),w=Object.entries(u).filter(([d])=>y.has(d)).filter(([,d])=>!d.installed).map(([d,B])=>({tool:d,error:B.error||null}));t.json({ok:w.length===0,agentId:String(e),toolStatus:u,failedTools:w,state:p})}catch(i){const l=i;t.status(400).json({error:l?.message||"install tools failed"})}}),g.post("/tools/uninstall",h,async(r,t)=>{const{agentId:e,tools:s}=r.body||{};if(!e){t.status(400).json({error:"agentId is required"});return}const n=Array.isArray(s)?s.map(i=>String(i||"").trim().toLowerCase()).filter(Boolean):[],o=new Set(N),a=n.filter(i=>o.has(i));if(a.length===0){t.status(400).json({error:`tools must include ${N.join(", ")}`});return}try{const i=await U(e),l=new Set(a),f=(i.enabledTools||[]).filter(B=>!l.has(String(B||"").trim().toLowerCase())),S=await me(a),p={...await E([...f,...a]),...S},y=a.filter(B=>p[B]?.installed),w=y.length>0?i.enabledTools||[]:f,d=await L(e,{...i,enabledTools:w,toolStatus:p});if(y.length>0){t.status(400).json({ok:!1,agentId:String(e),error:"uninstall incomplete",toolStatus:p,state:d});return}t.json({ok:!0,agentId:String(e),toolStatus:p,state:d})}catch(i){const l=i;t.status(400).json({error:l?.message||"uninstall tools failed"})}}),g.post("/prepare-restart",h,async(r,t)=>{const{preserveSessions:e=!0}=r.body||{};try{c.info(`[prepare-restart] \u6536\u5230\u51C6\u5907\u91CD\u542F\u901A\u77E5\uFF0CpreserveSessions=${e}`),await X(!!e),t.json({ok:!0,preserveSessions:e,message:e?"Bridge will preserve sessions for aws-mcp-server restart":"Bridge will clean up sessions on shutdown"})}catch(s){const n=s;c.error("[prepare-restart] Error:",n),t.status(500).json({error:n.message})}});const Ae={update:15*60*1e3,restart:5*60*1e3};async function _e(r){try{const t=await ce(),e=Date.now(),s=Ae[r],n=[];for(const o of t.operations){if(o.operationType!==r||!["accepted","running","restarting"].includes(o.status))continue;const a=new Date(o.updatedAt).getTime();if(!Number.isNaN(a)&&e-a<=s)return{operationId:o.operationId};n.push(o.operationId)}for(const o of n)try{await de(o),c.info(`[${r}-bridge] \u5DF2\u6E05\u7406\u8D85\u65F6\u672A\u4EA4\u4ED8\u7684\u9648\u65E7\u64CD\u4F5C: ${o}`)}catch(a){const i=a;c.warn(`[${r}-bridge] \u6E05\u7406\u9648\u65E7\u64CD\u4F5C\u5931\u8D25\uFF0C\u7EE7\u7EED\u653E\u884C: ${o}, ${i.message}`)}return null}catch(t){const e=t;return c.warn(`[${r}-bridge] \u5728\u9014\u64CD\u4F5C\u68C0\u67E5\u5931\u8D25\uFF0C\u653E\u884C\u8BF7\u6C42: ${e.message}`),null}}async function H(r,t,e){const s=await _e(r);return s?(e.status(409).json({error:t,operationId:s.operationId}),!0):!1}g.post("/update-bridge",T,async(r,t)=>{const e=M(r.body?.requestedByUserId);try{if(!e){t.status(400).json({error:"requestedByUserId is required"});return}if(await H("update","\u5B9E\u4F8B Bridge \u66F4\u65B0\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8BF7\u52FF\u91CD\u590D\u89E6\u53D1",t))return;const s=await F(),n=P({operationType:"update",requestedByUserId:e,schedulerBaseUrl:r.body?.schedulerBaseUrl,runtimeBridgeBaseUrl:q(r),status:"accepted",message:"Bridge \u66F4\u65B0\u8BF7\u6C42\u5DF2\u6536\u5230\uFF0C\u5C06\u5728\u540E\u53F0\u6267\u884C\u5E76\u81EA\u52A8\u91CD\u542F\u3002",latestVersion:s.ok&&s.latestVersion||void 0});await k(n),ke(n),t.json({ok:!0,operationId:n.operationId,message:"Bridge \u66F4\u65B0\u5DF2\u5728\u540E\u53F0\u5F00\u59CB\uFF0C\u66F4\u65B0\u5B8C\u6210\u540E\u5C06\u81EA\u52A8\u91CD\u542F\u5B9E\u4F8B Bridge\u3002"})}catch(s){const n=s;c.error("[update-bridge] Error:",n),t.status(500).json({error:n.message||"update bridge failed"})}}),g.post("/restart-bridge",T,async(r,t)=>{const{preserveSessions:e=!0}=r.body||{},s=M(r.body?.requestedByUserId);try{if(!s){t.status(400).json({error:"requestedByUserId is required"});return}if(await H("restart","\u5B9E\u4F8B Bridge \u6B63\u5728\u91CD\u542F\u4E2D\uFF0C\u8BF7\u7A0D\u5019\u518D\u8BD5",t))return;c.info(`[restart-bridge] \u6536\u5230\u5B9E\u4F8B Bridge \u91CD\u542F\u8BF7\u6C42\uFF0CpreserveSessions=${e}`);const n=P({operationType:"restart",requestedByUserId:s,schedulerBaseUrl:r.body?.schedulerBaseUrl,runtimeBridgeBaseUrl:q(r),status:"accepted",message:"Bridge \u5DF2\u6536\u5230\u91CD\u542F\u8BF7\u6C42\uFF1B\u4E0B\u6B21\u542F\u52A8\u540E\u4F1A\u53D1\u9001\u786E\u8BA4\u901A\u77E5\u3002"});await k(n),await X(!!e),t.json({ok:!0,operationId:n.operationId,preserveSessions:!!e,message:"Bridge \u6B63\u5728\u91CD\u542F\uFF0C\u670D\u52A1\u7BA1\u7406\u5668\u4F1A\u81EA\u52A8\u62C9\u8D77\u5B9E\u4F8B\uFF1B\u4E0B\u6B21\u542F\u52A8\u540E\u4F1A\u901A\u8FC7\u6D88\u606F\u4E2D\u5FC3\u901A\u77E5\u7ED3\u679C\u3002"}),c.info("[restart-bridge] \u6B63\u5728\u6267\u884C awsb service restart"),z(),setTimeout(()=>{if(process.exitCode=42,I){I("PANEL_RESTART",!!e).catch(o=>{const a=o;c.error("[restart-bridge] graceful shutdown failed:",a),process.exit(42)});return}process.exit(42)},V)}catch(n){const o=n;c.error("[restart-bridge] Error:",o),t.status(500).json({error:o.message||"restart bridge failed"})}});export{Ue as buildAmbiguousSchedulerBaseUrlResponse,Re as buildConnectionCheckResponse,Te as buildSchedulerPingFailureResponse,g as instanceRouter,nt as setGracefulShutdownFn,Ee as summarizeToolStatus};
1
+ import{spawn as A}from"node:child_process";import{createHash as W,timingSafeEqual as G}from"node:crypto";import J from"node:fs/promises";import w from"node:path";import{fileURLToPath as _}from"node:url";import O from"axios";import{Router as Q}from"express";import{adapterRegistry as Y}from"../adapter/index.js";import{getBridgeVersion as R,resolveSchedulerBaseUrlFrom as Z}from"../config.js";import{validateToken as h,validateTokenOrPanelSession as T}from"../middleware/auth.js";import{getConfiguredConnectionKeys as C,getRegistrationState as ee,requestRuntimeAccessTokenRefreshForServer as te,resolveSchedulerBaseUrlFromToken as re}from"../services/auto-register.js";import{getEffective as v,getOverrides as j}from"../services/capability-config.js";import{appendChangelogSection as se,readChangelog as ne}from"../services/changelog.js";import{setAgentFeatures as oe,setAgentCapabilities as ae,setAgentInheritedFeatures as ie,setAgentCapabilitiesInherited as de,getAgentFeatures as x,getAgentInheritedFeatures as ce}from"../services/instance-features.js";import{initInstance as le}from"../services/instance-init-service.js";import{loadInstanceState as U,saveInstanceState as L}from"../services/instance-state.js";import{createBridgeLifecycleOperation as P,deliverPendingBridgeLifecycleNotifications as $,loadLifecycleState as ue,removeBridgeLifecycleOperation as ge,upsertBridgeLifecycleOperation as k}from"../services/lifecycle-state.js";import{mcpDeployManager as pe}from"../services/mcp-deploy.js";import{pluginManager as fe}from"../services/plugin-manager.js";import{getRuntimeAccessToken as me}from"../services/runtime-binding.js";import{skillManager as he}from"../services/skill-manager.js";import{detectToolStatuses as E,ensureToolsInstalled as Se,SUPPORTED_INSTALLABLE_TOOLS as F,SUPPORTED_UNINSTALLABLE_TOOLS as D,uninstallTools as ye}from"../services/tool-installer.js";import{appendUpdateLog as m,readUpdateLog as be}from"../services/update-log.js";import{createLogger as we}from"../utils/logger.js";import{checkBridgeUpdate as N}from"../utils/npm-version.js";const d=we("instance");import{buildToolStatusDetectionTargets as ke}from"./instance-tool-status.js";let I=null;function it(r){I=r}const g=Q();function M(r){return String(r||"").trim()||void 0}function K(r){const t=r.get("host");if(t)return`${r.protocol}://${t}`}function q(){const r=w.resolve(w.dirname(_(import.meta.url)),"..","..");return w.join(r,"dist","index.js")}function Be(){return new Promise((r,t)=>{const e=A(process.execPath,[q(),"update","--no-kill"],{shell:!1,windowsHide:!0});let s="",n="";e.stdout.on("data",o=>{s+=o.toString("utf8")}),e.stderr.on("data",o=>{n+=o.toString("utf8")}),e.on("error",t),e.on("close",o=>{r({exitCode:o,stdout:s,stderr:n})})})}function z(){const r=A(process.execPath,[q(),"service","restart"],{shell:!1,windowsHide:!0,stdio:["ignore","pipe","pipe"]});r.stdout.on("data",t=>{m(String(t)).catch(()=>{})}),r.stderr.on("data",t=>{m(String(t)).catch(()=>{})})}async function Ie(){try{const r=w.join(w.dirname(w.dirname(_(import.meta.url))),"package.json"),t=JSON.parse(await J.readFile(r,{encoding:"utf8"}));return(typeof t.version=="string"?t.version.trim():"")||null}catch{return null}}const V=3e3;async function Re(r){const t={...r,status:"running",message:"Bridge \u66F4\u65B0\u6B63\u5728\u540E\u53F0\u6267\u884C\u3002"};await k(t);try{d.info("[update-bridge] \u6B63\u5728\u540E\u53F0\u6267\u884C awsb update --yes"),await m(`===== Bridge \u66F4\u65B0\u5F00\u59CB\uFF1Av${R()} -> latest\uFF08\u64CD\u4F5C ${r.operationId}\uFF09 =====`);const e=await Be();if(e.stdout&&await m(e.stdout),e.stderr&&await m(e.stderr),e.exitCode===0){const s=await Ie(),n=r.latestVersion||s||R();await se(n,`Bridge \u5DF2\u66F4\u65B0\u81F3 ${n}\u3002`).catch(a=>{const i=a;d.warn(`[update-bridge] \u8FFD\u52A0 changelog \u6BB5\u5931\u8D25\uFF08\u5DF2\u5FFD\u7565\uFF09: ${i.message}`)}),await m("===== Bridge \u66F4\u65B0\u6210\u529F\uFF0C\u51C6\u5907\u81EA\u52A8\u91CD\u542F ===== ");const o={...t,status:"restarting",message:"Bridge \u66F4\u65B0\u5B8C\u6210\uFF0C\u6B63\u5728\u91CD\u542F..."};await k(o),d.info("[update-bridge] \u6B63\u5728\u6267\u884C awsb service restart"),z(),setTimeout(()=>{if(process.exitCode=42,I){I("BRIDGE_UPDATE_RESTART",!0).catch(a=>{const i=a;d.error("[update-bridge] graceful shutdown failed:",i),process.exit(42)});return}process.exit(42)},V)}else await m(`===== Bridge \u66F4\u65B0\u5931\u8D25\uFF1A\u9000\u51FA\u7801 ${e.exitCode??"unknown"} =====`),await k({...t,status:"failed",message:`Bridge \u66F4\u65B0\u5931\u8D25\uFF0Cnpm \u9000\u51FA\u7801: ${e.exitCode??"unknown"}`,error:e.stderr||e.stdout}),await $()}catch(e){const s=e;d.error("[update-bridge] background update failed:",s),await m(`===== Bridge \u66F4\u65B0\u5F02\u5E38\uFF1A${s.message} =====`).catch(()=>{}),await k({...t,status:"failed",message:"Bridge \u66F4\u65B0\u5931\u8D25\u3002",error:s.message||"update bridge failed"}),await $()}}async function X(r){const t=await import("node:fs/promises"),e=await import("node:path"),s=await import("node:os"),n=e.join(s.tmpdir(),"agentswork-runtime-bridge",".preserve-sessions");if(r){await t.mkdir(e.dirname(n),{recursive:!0}),await t.writeFile(n,JSON.stringify({preserveSessions:!0,timestamp:new Date().toISOString()}),"utf-8"),d.info(`[prepare-restart] \u5DF2\u521B\u5EFA\u4FDD\u7559\u4F1A\u8BDD\u6807\u8BB0\u6587\u4EF6: ${n}`);return}try{await t.unlink(n),d.info("[prepare-restart] \u5DF2\u5220\u9664\u4FDD\u7559\u4F1A\u8BDD\u6807\u8BB0\u6587\u4EF6")}catch{}}function Te(r){return W("md5").update(r,"utf8").digest("hex")}function ve(r){const t=r.trim().toLowerCase();if(!/^[a-f0-9]{32}$/.test(t))return!1;const e=Buffer.from(t,"hex");return C().map(s=>s.trim()).filter(Boolean).some(s=>{const n=Buffer.from(Te(s),"hex");return n.length===e.length&&G(n,e)})}function je(r){const e=C().length>0,s=R();return e?ve(r)?{status:200,body:{ok:!0,runtimeBridge:"healthy",version:s,connectionKeyMatched:!0,connectionKeyRequired:!0}}:{status:401,body:{ok:!1,error:"connection_key_mismatch",version:s,connectionKeyMatched:!1,connectionKeyRequired:!0}}:{status:200,body:{ok:!0,runtimeBridge:"healthy",version:s,connectionKeyMatched:!0,connectionKeyRequired:!1}}}function Ue(r,t){const s=t.trim()||"http://localhost:8080",n=/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?(?:\/|$)/i.test(s);return{ok:!1,error:r.message||"scheduler ping failed",failureStage:"scheduler_ping",runtimeBridge:"healthy",schedulerBaseUrl:s,hint:n?"aws-runtime-bridge \u5DF2\u8FDE\u901A\uFF0C\u4F46\u5B83\u56DE\u8FDE\u8C03\u5EA6\u4E2D\u5FC3\u5931\u8D25\u3002\u8BF7\u5728 bridge \u542F\u52A8\u73AF\u5883\u4E2D\u5C06 AWS_RUNTIME_SCHEDULER_BASE_URL \u914D\u7F6E\u4E3A\u8BE5\u5B9E\u4F8B\u53EF\u8BBF\u95EE\u7684\u8C03\u5EA6\u4E2D\u5FC3\u5730\u5740\uFF0C\u4F8B\u5982 http://<server-host>:7380\u3002":"aws-runtime-bridge \u5DF2\u8FDE\u901A\uFF0C\u4F46\u5B83\u56DE\u8FDE\u8C03\u5EA6\u4E2D\u5FC3\u5931\u8D25\u3002\u8BF7\u786E\u8BA4 AWS_RUNTIME_SCHEDULER_BASE_URL \u6307\u5411\u7684\u8C03\u5EA6\u4E2D\u5FC3\u5730\u5740\u53EF\u4ECE bridge \u673A\u5668\u8BBF\u95EE\uFF0C\u5E76\u4E14\u8FD0\u884C\u65F6\u8BBF\u95EE\u4EE4\u724C\u4ECD\u6709\u6548\u3002"}}function Ee(r){if(typeof r!="object"||r===null)return;const t=Reflect.get(r,"response");if(typeof t!="object"||t===null)return;const e=Reflect.get(t,"status");return typeof e=="number"?e:void 0}async function Ae(r,t){try{const e=await O.get(`${r}/api/runtime/ping`,{headers:{"X-Runtime-Token":t}});return{schedulerReachable:e.status>=200&&e.status<300,tokenRefreshed:!1}}catch(e){if(Ee(e)!==401)throw e;const s=await te(r);if(!s.success||!s.runtimeAccessToken)throw e;const n=await O.get(`${r}/api/runtime/ping`,{headers:{"X-Runtime-Token":s.runtimeAccessToken}});return{schedulerReachable:n.status>=200&&n.status<300,tokenRefreshed:!0}}}function _e(r){return{ok:!1,error:"ambiguous_scheduler_base_url",failureStage:"scheduler_ping",runtimeBridge:"healthy",schedulerBaseUrl:"",autoRegisterTargetUrls:r,hint:"Multiple autoRegisterTargets.serverUrl values were found, so bridge cannot safely infer which scheduler /runtime/ping should call back. Set AWS_RUNTIME_SCHEDULER_BASE_URL explicitly, or use future schedulerId/token-bound multi-scheduler routing."}}function Oe(r){return Object.fromEntries(Object.entries(r||{}).map(([t,e])=>[t,{installed:!!e?.installed,version:e?.version||null,executable:e?.executable||null,error:e?.error||null}]))}g.get("/check-update",async(r,t)=>{t.json(await N())}),g.get("/update-log",T,async(r,t)=>{try{const e=await be();t.json({ok:!0,...e})}catch(e){const s=e;d.error("[update-log] Error:",s),t.status(500).json({ok:!1,error:s.message||"read update log failed"})}}),g.get("/changelog",T,async(r,t)=>{const e=String(r.query?.version||"").trim();if(!e){t.status(400).json({ok:!1,error:"version is required"});return}try{const s=await ne(e);t.json({ok:!0,version:e,...s})}catch(s){const n=s;d.error("[changelog] Error:",n),t.status(500).json({ok:!1,error:n.message||"read changelog failed"})}}),g.get("/healthz",(r,t)=>{t.json({ok:!0,runtimeBridge:"healthy"})}),g.get("/connection-check",(r,t)=>{const e=String(r.header("X-Bridge-Connection-Key-MD5")||""),s=je(e);t.status(s.status).json(s.body)}),g.get("/ping",h,async(r,t)=>{let e=Z(r.header("X-Scheduler-Base-Url"),r.header("X-Bridge-Instance-Id"),r.header("X-Bridge-Instance-Name"));if(e.source==="ambiguous-auto-register-targets"){const n=re(r.header("X-Runtime-Token"));if(n)e={url:n,source:"token-match",ambiguousTargetUrls:[]};else{const o=r.header("X-Bridge-Instance-Id");if(o){const a=ee(),i=Object.entries(a.registrations).find(([,l])=>l===o)?.[0];i&&(e={url:i,source:"instance-id-match",ambiguousTargetUrls:[]})}if(!e.url||e.source==="ambiguous-auto-register-targets"){t.status(400).json(_e(e.ambiguousTargetUrls));return}}}const s=e.url;try{const n=me(s);if(!n){t.status(401).json({ok:!1,error:"runtime_access_token_required"});return}const o=await Ae(s,n);t.json({ok:!0,runtimeBridge:"healthy",version:R(),schedulerReachable:o.schedulerReachable,tokenRefreshed:o.tokenRefreshed,schedulerBaseUrl:s,instanceId:e.instanceId,instanceName:e.instanceName})}catch(n){const o=n;t.status(502).json(Ue(o,s))}}),g.post("/init-instance",h,async(r,t)=>{const{agentId:e,workspacePath:s,skillEnabled:n,mcpEnabled:o,ccSwitchEnabledTools:a,skillPackage:i,skillPackages:l,pluginPackages:f,mcpServers:S}=r.body||{};if(!e||!s){t.status(400).json({error:"agentId and workspacePath are required"});return}try{d.info("[init-instance] Request received",{agentId:String(e),workspacePath:String(s),skillEnabled:n,mcpEnabled:o,ccSwitchEnabledTools:a});const u=await le(e,s,{skillEnabled:n,mcpEnabled:o,ccSwitchEnabledTools:a,skillPackage:i,skillPackages:l,pluginPackages:f,mcpServers:S});d.info("[init-instance] Initialization completed",{agentId:String(e),ok:u.ok,logCount:u.logs?.length||0,enabledTools:u.enabledTools}),t.json(u)}catch(u){const p=u;d.error("[init-instance] Initialization failed",{agentId:String(e),workspacePath:String(s),error:p?.message||String(u),stack:p?.stack||null}),t.status(400).json({error:p?.message||"initialize instance failed"})}}),g.post("/features",h,async(r,t)=>{const{agentId:e,features:s}=r.body||{};if(!e){t.status(400).json({error:"agentId is required"});return}oe(String(e),s),r.body.inheritedFeatures!==void 0&&ie(String(e),r.body.inheritedFeatures),r.body.capabilities!==void 0&&ae(String(e),r.body.capabilities),r.body.capabilitiesInherited!==void 0&&de(String(e),r.body.capabilitiesInherited),d.info("[instance/features] \u80FD\u529B\u5F00\u5173\u5DF2\u66F4\u65B0",{agentId:String(e),features:x(String(e)),inheritedFeatures:ce(String(e))});let n=0;for(const o of Y.getAll())if(typeof o.applyAgentFeatures=="function")try{await o.applyAgentFeatures(String(e)),n+=1}catch(a){d.error("[instance/features] adapter \u70ED\u540C\u6B65\u5931\u8D25",{agentId:String(e),providerId:o.providerId,error:a?.message||String(a)})}t.json({ok:!0,agentId:String(e),features:x(String(e)),hotSyncedAdapters:n})}),g.post("/cc-switch/state",h,async(r,t)=>{const{agentId:e}=r.body||{};if(!e){t.status(400).json({error:"agentId is required"});return}try{d.info("[cc-switch/state] Request received",{agentId:String(e)});const s=await U(e),n=ke(s.enabledTools||[]);d.info("[cc-switch/state] Detecting SDK statuses",{agentId:String(e),detectionTargets:n});const o=await E(n),a=await fe.listDeployed(),i=he.listDeployed(),l=pe.listDeployed(),f=j(e,"skills"),S=j(e,"mcp"),u=j(e,"plugins"),p=i.map(c=>({...c,enabled:v(e,"skills",c.skillName),source:f[c.skillName]!==void 0?"agent":"bridge"})),y=l.map(c=>({...c,enabled:v(e,"mcp",c.name),source:S[c.name]!==void 0?"agent":"bridge"})),b=a.map(c=>({...c,enabled:v(e,"plugins",c.pluginName),source:u[c.pluginName]!==void 0?"agent":"bridge"}));d.info("[cc-switch/state] SDK status detection completed",{agentId:String(e),toolStatus:Oe(o)}),t.json({ok:!0,agentId:String(e),imported:{mcpServers:0,skills:0},deployedPlugins:b,deployedSkills:p,deployedMcp:y,state:{...s,toolStatus:o}})}catch(s){const n=s;d.error("[cc-switch/state] Failed to load state",{agentId:String(e),error:n?.message||String(s),stack:n?.stack||null}),t.status(400).json({error:n?.message||"load state failed"})}}),g.post("/tools/install",h,async(r,t)=>{const{agentId:e,tools:s}=r.body||{};if(!e){t.status(400).json({error:"agentId is required"});return}const n=Array.isArray(s)?s.map(i=>String(i||"").trim().toLowerCase()).filter(Boolean):[],o=new Set(F),a=n.filter(i=>o.has(i));if(a.length===0){t.status(400).json({error:`tools must include ${F.join(", ")}`});return}try{const i=await U(e),l=Array.from(new Set([...i.enabledTools||[],...a])),f=await Se(a),u={...await E(l),...f},p=await L(e,{...i,enabledTools:l,toolStatus:u}),y=new Set(a),b=Object.entries(u).filter(([c])=>y.has(c)).filter(([,c])=>!c.installed).map(([c,B])=>({tool:c,error:B.error||null}));t.json({ok:b.length===0,agentId:String(e),toolStatus:u,failedTools:b,state:p})}catch(i){const l=i;t.status(400).json({error:l?.message||"install tools failed"})}}),g.post("/tools/uninstall",h,async(r,t)=>{const{agentId:e,tools:s}=r.body||{};if(!e){t.status(400).json({error:"agentId is required"});return}const n=Array.isArray(s)?s.map(i=>String(i||"").trim().toLowerCase()).filter(Boolean):[],o=new Set(D),a=n.filter(i=>o.has(i));if(a.length===0){t.status(400).json({error:`tools must include ${D.join(", ")}`});return}try{const i=await U(e),l=new Set(a),f=(i.enabledTools||[]).filter(B=>!l.has(String(B||"").trim().toLowerCase())),S=await ye(a),p={...await E([...f,...a]),...S},y=a.filter(B=>p[B]?.installed),b=y.length>0?i.enabledTools||[]:f,c=await L(e,{...i,enabledTools:b,toolStatus:p});if(y.length>0){t.status(400).json({ok:!1,agentId:String(e),error:"uninstall incomplete",toolStatus:p,state:c});return}t.json({ok:!0,agentId:String(e),toolStatus:p,state:c})}catch(i){const l=i;t.status(400).json({error:l?.message||"uninstall tools failed"})}}),g.post("/prepare-restart",h,async(r,t)=>{const{preserveSessions:e=!0}=r.body||{};try{d.info(`[prepare-restart] \u6536\u5230\u51C6\u5907\u91CD\u542F\u901A\u77E5\uFF0CpreserveSessions=${e}`),await X(!!e),t.json({ok:!0,preserveSessions:e,message:e?"Bridge will preserve sessions for aws-mcp-server restart":"Bridge will clean up sessions on shutdown"})}catch(s){const n=s;d.error("[prepare-restart] Error:",n),t.status(500).json({error:n.message})}});const Ce={update:15*60*1e3,restart:5*60*1e3};async function xe(r){try{const t=await ue(),e=Date.now(),s=Ce[r],n=[];for(const o of t.operations){if(o.operationType!==r||!["accepted","running","restarting"].includes(o.status))continue;const a=new Date(o.updatedAt).getTime();if(!Number.isNaN(a)&&e-a<=s)return{operationId:o.operationId};n.push(o.operationId)}for(const o of n)try{await ge(o),d.info(`[${r}-bridge] \u5DF2\u6E05\u7406\u8D85\u65F6\u672A\u4EA4\u4ED8\u7684\u9648\u65E7\u64CD\u4F5C: ${o}`)}catch(a){const i=a;d.warn(`[${r}-bridge] \u6E05\u7406\u9648\u65E7\u64CD\u4F5C\u5931\u8D25\uFF0C\u7EE7\u7EED\u653E\u884C: ${o}, ${i.message}`)}return null}catch(t){const e=t;return d.warn(`[${r}-bridge] \u5728\u9014\u64CD\u4F5C\u68C0\u67E5\u5931\u8D25\uFF0C\u653E\u884C\u8BF7\u6C42: ${e.message}`),null}}async function H(r,t,e){const s=await xe(r);return s?(e.status(409).json({error:t,operationId:s.operationId}),!0):!1}g.post("/update-bridge",T,async(r,t)=>{const e=M(r.body?.requestedByUserId);try{if(!e){t.status(400).json({error:"requestedByUserId is required"});return}if(await H("update","\u5B9E\u4F8B Bridge \u66F4\u65B0\u6B63\u5728\u6267\u884C\u4E2D\uFF0C\u8BF7\u52FF\u91CD\u590D\u89E6\u53D1",t))return;const s=await N(),n=P({operationType:"update",requestedByUserId:e,schedulerBaseUrl:r.body?.schedulerBaseUrl,runtimeBridgeBaseUrl:K(r),status:"accepted",message:"Bridge \u66F4\u65B0\u8BF7\u6C42\u5DF2\u6536\u5230\uFF0C\u5C06\u5728\u540E\u53F0\u6267\u884C\u5E76\u81EA\u52A8\u91CD\u542F\u3002",latestVersion:s.ok&&s.latestVersion||void 0});await k(n),Re(n),t.json({ok:!0,operationId:n.operationId,message:"Bridge \u66F4\u65B0\u5DF2\u5728\u540E\u53F0\u5F00\u59CB\uFF0C\u66F4\u65B0\u5B8C\u6210\u540E\u5C06\u81EA\u52A8\u91CD\u542F\u5B9E\u4F8B Bridge\u3002"})}catch(s){const n=s;d.error("[update-bridge] Error:",n),t.status(500).json({error:n.message||"update bridge failed"})}}),g.post("/restart-bridge",T,async(r,t)=>{const{preserveSessions:e=!0}=r.body||{},s=M(r.body?.requestedByUserId);try{if(!s){t.status(400).json({error:"requestedByUserId is required"});return}if(await H("restart","\u5B9E\u4F8B Bridge \u6B63\u5728\u91CD\u542F\u4E2D\uFF0C\u8BF7\u7A0D\u5019\u518D\u8BD5",t))return;d.info(`[restart-bridge] \u6536\u5230\u5B9E\u4F8B Bridge \u91CD\u542F\u8BF7\u6C42\uFF0CpreserveSessions=${e}`);const n=P({operationType:"restart",requestedByUserId:s,schedulerBaseUrl:r.body?.schedulerBaseUrl,runtimeBridgeBaseUrl:K(r),status:"accepted",message:"Bridge \u5DF2\u6536\u5230\u91CD\u542F\u8BF7\u6C42\uFF1B\u4E0B\u6B21\u542F\u52A8\u540E\u4F1A\u53D1\u9001\u786E\u8BA4\u901A\u77E5\u3002"});await k(n),await X(!!e),t.json({ok:!0,operationId:n.operationId,preserveSessions:!!e,message:"Bridge \u6B63\u5728\u91CD\u542F\uFF0C\u670D\u52A1\u7BA1\u7406\u5668\u4F1A\u81EA\u52A8\u62C9\u8D77\u5B9E\u4F8B\uFF1B\u4E0B\u6B21\u542F\u52A8\u540E\u4F1A\u901A\u8FC7\u6D88\u606F\u4E2D\u5FC3\u901A\u77E5\u7ED3\u679C\u3002"}),d.info("[restart-bridge] \u6B63\u5728\u6267\u884C awsb service restart"),z(),setTimeout(()=>{if(process.exitCode=42,I){I("PANEL_RESTART",!!e).catch(o=>{const a=o;d.error("[restart-bridge] graceful shutdown failed:",a),process.exit(42)});return}process.exit(42)},V)}catch(n){const o=n;d.error("[restart-bridge] Error:",o),t.status(500).json({error:o.message||"restart bridge failed"})}});export{_e as buildAmbiguousSchedulerBaseUrlResponse,je as buildConnectionCheckResponse,Ue as buildSchedulerPingFailureResponse,g as instanceRouter,it as setGracefulShutdownFn,Oe as summarizeToolStatus};
2
2
 
@@ -1,7 +1,7 @@
1
- import b from"node:fs/promises";import w from"node:path";import{Router as M}from"express";import{validateToken as l}from"../middleware/auth.js";import{getLaunchManager as c,LaunchAlreadyRunningError as P}from"../services/launch-manager.js";import{METRICS_HISTORY_CAPACITY as v}from"../services/launch-metrics.js";import{getLaunchStreamHub as j}from"../services/launch-streams.js";import{createLogger as N}from"../utils/logger.js";const d=N("launch"),m=M(),k=3e5,I=20;m.put("/launch-configs/file",l,async(a,t)=>{try{const r=a.body||{},e=typeof r.workspacePath=="string"?r.workspacePath.trim():"";if(!e||!w.isAbsolute(e)){t.status(400).json({error:"workspacePath must be a non-empty absolute path"});return}if(!r.content||typeof r.content!="object"||Array.isArray(r.content)){t.status(400).json({error:"content must be an object"});return}const n=w.join(e,".agentswork","launcher","launcher-config.json");await b.mkdir(w.dirname(n),{recursive:!0});const s=`${n}.${process.pid}.tmp`;await b.writeFile(s,JSON.stringify(r.content,null,2)+`
2
- `,"utf-8"),await b.rename(s,n),d.info(`[launch/file] \u542F\u52A8\u9879\u5FEB\u7167\u5DF2\u5199\u5165: ${n}`),t.json({ok:!0,path:n})}catch(r){const e=r;d.error("[launch/file] Error:",e),t.status(500).json({error:e.message})}}),m.get("/launch-configs/file",l,async(a,t)=>{try{const r=typeof a.query.workspacePath=="string"?a.query.workspacePath.trim():"";if(!r||!w.isAbsolute(r)){t.status(400).json({error:"workspacePath must be a non-empty absolute path"});return}const e=w.join(r,".agentswork","launcher","launcher-config.json");try{const n=await b.readFile(e,"utf-8");t.json({ok:!0,found:!0,path:e,content:JSON.parse(n)})}catch(n){if(n.code==="ENOENT"){t.json({ok:!0,found:!1,path:e});return}throw n}}catch(r){const e=r;d.error("[launch/file:read] Error:",e),t.status(500).json({error:e.message})}}),m.get("/launch-configs/runs",l,async(a,t)=>{try{const r=await c().listRuns();t.json({ok:!0,runs:r})}catch(r){const e=r;d.error("[launch/runs] Error:",e),t.status(500).json({error:e.message})}}),m.post("/launch-configs/:id/start",l,async(a,t)=>{const r=a.params.id;try{const e=a.body||{},n=Number(e.waitTimeoutMs),s=Number.isFinite(n)&&n>0?Math.min(n,k):0;let o;try{o=await c().start({configId:r,command:e.command,cwd:e.cwd,env:e.env,jdkPath:e.jdkPath,workspacePath:e.workspacePath,readinessPattern:e.readinessPattern,startedBy:e.startedBy})}catch(i){if(i instanceof P){t.json({ok:!0,skipped:!0,message:"\u5F53\u524D\u5DF2\u5904\u4E8E\u542F\u52A8/\u6B63\u5728\u542F\u52A8\u72B6\u6001\uFF0C\u8DF3\u8FC7",run:i.existing});return}throw i}if(s>0){const i=await c().waitStartupResult(r,s);t.json({ok:!0,run:o,wait:i});return}t.json({ok:!0,run:o})}catch(e){const n=e;d.warn(`[launch/start] configId=${r}, error=${n.message}`),t.status(400).json({error:n.message})}}),m.post("/launch-configs/:id/restart",l,async(a,t)=>{const r=a.params.id;try{const e=a.body||{},n=Number(e.waitTimeoutMs),s=Number.isFinite(n)&&n>0?Math.min(n,k):0,o=await c().restart({configId:r,command:e.command,cwd:e.cwd,env:e.env,jdkPath:e.jdkPath,workspacePath:e.workspacePath,readinessPattern:e.readinessPattern,startedBy:e.startedBy});if(s>0){const i=await c().waitStartupResult(r,s);t.json({ok:!0,run:o,wait:i});return}t.json({ok:!0,run:o})}catch(e){const n=e;d.warn(`[launch/restart] configId=${r}, error=${n.message}`),t.status(400).json({error:n.message})}}),m.post("/launch-configs/start-batch",l,async(a,t)=>{try{const r=a.body||{};if(!Array.isArray(r.items)||r.items.length===0){t.status(400).json({error:"items must be a non-empty array"});return}if(r.items.length>I){t.status(400).json({error:`items exceeds limit ${I}`});return}const e=Number(r.waitTimeoutMs),n=Number.isFinite(e)&&e>0?Math.min(e,k):0,s=await c().startBatch(r.items,n);t.json({ok:!0,...s})}catch(r){const e=r;d.warn(`[launch/start-batch] error=${e.message}`),t.status(400).json({error:e.message})}}),m.post("/launch-configs/stop-all",l,async(a,t)=>{try{const r=await c().stopAll();t.json({ok:!0,...r})}catch(r){const e=r;d.warn(`[launch/stop-all] error: ${e.message}`),t.status(500).json({error:e.message})}}),m.post("/launch-configs/:id/stop",l,async(a,t)=>{const r=a.params.id;try{const e=a.body?.actor,n=typeof e=="string"&&e.trim()?e.trim():void 0,s=await c().stop(r,"manual",n);t.json({ok:s.success,...s})}catch(e){const n=e;d.warn(`[launch/stop] configId=${r}, error=${n.message}`),t.status(400).json({error:n.message})}}),m.get("/launch-configs/:id/logs",l,async(a,t)=>{const r=a.params.id,e=Number(a.query.since)||0;try{const n=await c().getLogs(r,e);if(!n){t.status(404).json({error:"launch run not found"});return}t.json({ok:!0,...n})}catch(n){const s=n;d.error(`[launch/logs] configId=${r}, error:`,s),t.status(500).json({error:s.message})}}),m.get("/launch-configs/:id/metrics",l,async(a,t)=>{const r=a.params.id,e=Number(a.query.limit),n=Number.isFinite(e)&&e>0?Math.min(Math.floor(e),v):v;try{const s=c().getRun(r);if(!s){t.status(404).json({error:"launch run not found"});return}const o=c().getMetrics(r,n);t.json({ok:!0,configId:r,state:s.state,metrics:o})}catch(s){const o=s;d.error(`[launch/metrics] configId=${r}, error:`,o),t.status(500).json({error:o.message})}}),m.get("/launch-configs/:id/logs/stream",l,async(a,t)=>{const r=a.params.id;if(!c().getRun(r)){t.status(404).json({error:"launch run not found"});return}const n=Number(a.headers["last-event-id"]),s=Number.isFinite(n)&&n>=0?n:-1;t.on("error",()=>{});let o=null,i=null;try{t.writeHead(200,{"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"}),t.write(`: connected
1
+ import b from"node:fs/promises";import w from"node:path";import{Router as M}from"express";import{validateToken as l}from"../middleware/auth.js";import{getLaunchManager as c,LaunchAlreadyRunningError as N}from"../services/launch-manager.js";import{METRICS_HISTORY_CAPACITY as P}from"../services/launch-metrics.js";import{getLaunchStreamHub as j}from"../services/launch-streams.js";import{createLogger as $}from"../utils/logger.js";import{noteDashboardActivity as k}from"./dashboard.js";const d=$("launch"),h=M(),v=3e5,I=20;h.put("/launch-configs/file",l,async(a,t)=>{try{const r=a.body||{},e=typeof r.workspacePath=="string"?r.workspacePath.trim():"";if(!e||!w.isAbsolute(e)){t.status(400).json({error:"workspacePath must be a non-empty absolute path"});return}if(!r.content||typeof r.content!="object"||Array.isArray(r.content)){t.status(400).json({error:"content must be an object"});return}const n=w.join(e,".agentswork","launcher","launcher-config.json");await b.mkdir(w.dirname(n),{recursive:!0});const s=`${n}.${process.pid}.tmp`;await b.writeFile(s,JSON.stringify(r.content,null,2)+`
2
+ `,"utf-8"),await b.rename(s,n),d.info(`[launch/file] \u542F\u52A8\u9879\u5FEB\u7167\u5DF2\u5199\u5165: ${n}`),t.json({ok:!0,path:n})}catch(r){const e=r;d.error("[launch/file] Error:",e),t.status(500).json({error:e.message})}}),h.get("/launch-configs/file",l,async(a,t)=>{try{const r=typeof a.query.workspacePath=="string"?a.query.workspacePath.trim():"";if(!r||!w.isAbsolute(r)){t.status(400).json({error:"workspacePath must be a non-empty absolute path"});return}const e=w.join(r,".agentswork","launcher","launcher-config.json");try{const n=await b.readFile(e,"utf-8");t.json({ok:!0,found:!0,path:e,content:JSON.parse(n)})}catch(n){if(n.code==="ENOENT"){t.json({ok:!0,found:!1,path:e});return}throw n}}catch(r){const e=r;d.error("[launch/file:read] Error:",e),t.status(500).json({error:e.message})}}),h.get("/launch-configs/runs",l,async(a,t)=>{try{const r=a.query.includePorts,e=!(r==="0"||r==="false"),n=await c().listRuns({withPorts:e});t.json({ok:!0,runs:n})}catch(r){const e=r;d.error("[launch/runs] Error:",e),t.status(500).json({error:e.message})}}),h.post("/launch-configs/:id/start",l,async(a,t)=>{const r=a.params.id;try{const e=a.body||{},n=Number(e.waitTimeoutMs),s=Number.isFinite(n)&&n>0?Math.min(n,v):0;let o;try{o=await c().start({configId:r,command:e.command,cwd:e.cwd,env:e.env,jdkPath:e.jdkPath,workspacePath:e.workspacePath,readinessPattern:e.readinessPattern,startedBy:e.startedBy})}catch(i){if(i instanceof N){t.json({ok:!0,skipped:!0,message:"\u5F53\u524D\u5DF2\u5904\u4E8E\u542F\u52A8/\u6B63\u5728\u542F\u52A8\u72B6\u6001\uFF0C\u8DF3\u8FC7",run:i.existing});return}throw i}if(s>0){const i=await c().waitStartupResult(r,s);t.json({ok:!0,run:o,wait:i});return}t.json({ok:!0,run:o})}catch(e){const n=e;d.warn(`[launch/start] configId=${r}, error=${n.message}`),t.status(400).json({error:n.message})}}),h.post("/launch-configs/:id/restart",l,async(a,t)=>{const r=a.params.id;try{const e=a.body||{},n=Number(e.waitTimeoutMs),s=Number.isFinite(n)&&n>0?Math.min(n,v):0,o=await c().restart({configId:r,command:e.command,cwd:e.cwd,env:e.env,jdkPath:e.jdkPath,workspacePath:e.workspacePath,readinessPattern:e.readinessPattern,startedBy:e.startedBy});if(s>0){const i=await c().waitStartupResult(r,s);t.json({ok:!0,run:o,wait:i});return}t.json({ok:!0,run:o})}catch(e){const n=e;d.warn(`[launch/restart] configId=${r}, error=${n.message}`),t.status(400).json({error:n.message})}}),h.post("/launch-configs/start-batch",l,async(a,t)=>{try{const r=a.body||{};if(!Array.isArray(r.items)||r.items.length===0){t.status(400).json({error:"items must be a non-empty array"});return}if(r.items.length>I){t.status(400).json({error:`items exceeds limit ${I}`});return}const e=Number(r.waitTimeoutMs),n=Number.isFinite(e)&&e>0?Math.min(e,v):0,s=await c().startBatch(r.items,n);t.json({ok:!0,...s})}catch(r){const e=r;d.warn(`[launch/start-batch] error=${e.message}`),t.status(400).json({error:e.message})}}),h.post("/launch-configs/stop-all",l,async(a,t)=>{try{const r=await c().stopAll();t.json({ok:!0,...r})}catch(r){const e=r;d.warn(`[launch/stop-all] error: ${e.message}`),t.status(500).json({error:e.message})}}),h.post("/launch-configs/:id/stop",l,async(a,t)=>{const r=a.params.id;try{const e=a.body?.actor,n=typeof e=="string"&&e.trim()?e.trim():void 0,s=await c().stop(r,"manual",n);t.json({ok:s.success,...s})}catch(e){const n=e;d.warn(`[launch/stop] configId=${r}, error=${n.message}`),t.status(400).json({error:n.message})}}),h.get("/launch-configs/:id/logs",l,async(a,t)=>{const r=a.params.id,e=Number(a.query.since)||0;try{const n=await c().getLogs(r,e);if(!n){t.status(404).json({error:"launch run not found"});return}t.json({ok:!0,...n})}catch(n){const s=n;d.error(`[launch/logs] configId=${r}, error:`,s),t.status(500).json({error:s.message})}}),h.get("/launch-configs/:id/metrics",l,async(a,t)=>{const r=a.params.id,e=Number(a.query.limit),n=Number.isFinite(e)&&e>0?Math.min(Math.floor(e),P):P;try{const s=c().getRun(r);if(!s){t.status(404).json({error:"launch run not found"});return}const o=c().getMetrics(r,n);t.json({ok:!0,configId:r,state:s.state,metrics:o})}catch(s){const o=s;d.error(`[launch/metrics] configId=${r}, error:`,o),t.status(500).json({error:o.message})}}),h.get("/launch-configs/:id/logs/stream",l,async(a,t)=>{k();const r=a.params.id;if(!c().getRun(r)){t.status(404).json({error:"launch run not found"});return}const n=Number(a.headers["last-event-id"]),s=Number.isFinite(n)&&n>=0?n:-1;t.on("error",()=>{});let o=null,i=null;try{t.writeHead(200,{"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"}),t.write(`: connected
3
3
 
4
- `);const g=f=>{t.destroyed||t.write(f)};let h=-1,y=!1;if(o=j().subscribe(r,"logs",f=>{if(t.destroyed)return;g(f),f.startsWith("event: run_state")&&(y=!0);const p=/^id: (\d+)\n/.exec(f);p&&(h=Math.max(h,Number(p[1])))}),a.on("close",()=>{o?.(),i&&clearInterval(i);try{t.end()}catch{}}),s>=-1){const f=await c().getLogs(r,Math.max(s,h));if(f)for(const p of f.entries)p.index<=h||g(`id: ${p.index}
4
+ `);const g=f=>{t.destroyed||t.write(f)};let m=-1,y=!1;if(o=j().subscribe(r,"logs",f=>{if(t.destroyed)return;g(f),f.startsWith("event: run_state")&&(y=!0);const p=/^id: (\d+)\n/.exec(f);p&&(m=Math.max(m,Number(p[1])))}),a.on("close",()=>{o?.(),i&&clearInterval(i);try{t.end()}catch{}}),s>=-1){const f=await c().getLogs(r,Math.max(s,m));if(f)for(const p of f.entries)p.index<=m||g(`id: ${p.index}
5
5
  data: ${JSON.stringify(p.data)}
6
6
 
7
7
  `)}const u=c().getRun(r);if(!y&&u&&u.state!=="running"){const f={configId:r,state:u.state};u.state==="exited"&&(f.exitCode=u.exitCode??null),g(`event: run_state
@@ -9,22 +9,22 @@ data: ${JSON.stringify(f)}
9
9
 
10
10
  `)}i=setInterval(()=>{g(`: heartbeat
11
11
 
12
- `)},15e3)}catch(g){const h=g;if(d.error(`[launch/logs/stream] configId=${r}, error:`,h),o?.(),i&&clearInterval(i),t.headersSent){t.end();return}t.status(500).json({error:h.message})}}),m.get("/launch-configs/:id/metrics/stream",l,(a,t)=>{const r=a.params.id;if(!c().getRun(r)){t.status(404).json({error:"launch run not found"});return}const n=Number(a.headers["last-event-id"]),s=Number.isFinite(n)&&n>=0?n:-1;t.on("error",()=>{}),t.writeHead(200,{"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"}),t.write(`: connected
12
+ `)},15e3)}catch(g){const m=g;if(d.error(`[launch/logs/stream] configId=${r}, error:`,m),o?.(),i&&clearInterval(i),t.headersSent){t.end();return}t.status(500).json({error:m.message})}}),h.get("/launch-configs/:id/metrics/stream",l,(a,t)=>{k();const r=a.params.id;if(!c().getRun(r)){t.status(404).json({error:"launch run not found"});return}const n=Number(a.headers["last-event-id"]),s=Number.isFinite(n)&&n>=0?n:-1;t.on("error",()=>{}),t.writeHead(200,{"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"}),t.write(`: connected
13
13
 
14
14
  `);const o=u=>{t.destroyed||t.write(u)};if(s>=0)for(const u of c().getMetricsSince(r,s))o(`id: ${u.index}
15
15
  data: ${JSON.stringify(u.sample)}
16
16
 
17
- `);let i=!1,g=null,h=null;g=j().subscribe(r,"metrics",u=>{t.destroyed||(o(u),u.startsWith("event: run_state")&&(i=!0))}),a.on("close",()=>{g?.(),h&&clearInterval(h);try{t.end()}catch{}});const y=c().getRun(r);if(!i&&y&&y.state!=="running"){const u={configId:r,state:y.state};y.state==="exited"&&(u.exitCode=y.exitCode??null),o(`event: run_state
17
+ `);let i=!1,g=null,m=null;g=j().subscribe(r,"metrics",u=>{t.destroyed||(o(u),u.startsWith("event: run_state")&&(i=!0))}),a.on("close",()=>{g?.(),m&&clearInterval(m);try{t.end()}catch{}});const y=c().getRun(r);if(!i&&y&&y.state!=="running"){const u={configId:r,state:y.state};y.state==="exited"&&(u.exitCode=y.exitCode??null),o(`event: run_state
18
18
  data: ${JSON.stringify(u)}
19
19
 
20
- `)}h=setInterval(()=>{o(`: heartbeat
20
+ `)}m=setInterval(()=>{o(`: heartbeat
21
21
 
22
- `)},15e3)}),m.get("/launch-configs/status/stream",l,async(a,t)=>{t.on("error",()=>{});let r=null,e=null;try{t.writeHead(200,{"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"}),t.write(`: connected
22
+ `)},15e3)}),h.get("/launch-configs/status/stream",l,async(a,t)=>{k(),t.on("error",()=>{});let r=null,e=null;try{t.writeHead(200,{"Content-Type":"text/event-stream; charset=utf-8","Cache-Control":"no-cache",Connection:"keep-alive","X-Accel-Buffering":"no"}),t.write(`: connected
23
23
 
24
- `);const n=o=>{t.destroyed||t.write(o)};r=j().subscribeStatus(o=>{t.destroyed||n(o)}),a.on("close",()=>{r?.(),e&&clearInterval(e);try{t.end()}catch{}});const s=await c().listRuns();t.destroyed||n(`event: snapshot
24
+ `);const n=o=>{t.destroyed||t.write(o)};r=j().subscribeStatus(o=>{t.destroyed||n(o)}),a.on("close",()=>{r?.(),e&&clearInterval(e);try{t.end()}catch{}});const s=await c().listRuns({withPorts:!1});t.destroyed||n(`event: snapshot
25
25
  data: ${JSON.stringify({runs:s})}
26
26
 
27
- `),e=setInterval(()=>{n(`: heartbeat
27
+ `),c().publishRunningPorts(),e=setInterval(()=>{n(`: heartbeat
28
28
 
29
- `)},15e3)}catch(n){const s=n;if(d.error("[launch/status/stream] error:",s),r?.(),e&&clearInterval(e),t.headersSent){t.end();return}t.status(500).json({error:s.message})}});export{I as MAX_BATCH_START_ITEMS,k as MAX_START_WAIT_MS,m as launchRouter};
29
+ `)},15e3)}catch(n){const s=n;if(d.error("[launch/status/stream] error:",s),r?.(),e&&clearInterval(e),t.headersSent){t.end();return}t.status(500).json({error:s.message})}});export{I as MAX_BATCH_START_ITEMS,v as MAX_START_WAIT_MS,h as launchRouter};
30
30