jishushell 0.4.17 → 0.4.24

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 (241) hide show
  1. package/Dockerfile.hermes-slim +193 -0
  2. package/apps/hermes-container.yaml +35 -0
  3. package/apps/ollama-binary.yaml +200 -0
  4. package/apps/ollama-cpu-container.yaml +37 -0
  5. package/apps/ollama-with-hollama-binary.yaml +195 -0
  6. package/apps/openclaw-binary.yaml +69 -0
  7. package/apps/openclaw-container.yaml +37 -0
  8. package/apps/openclaw-with-ollama-container.yaml +42 -0
  9. package/apps/openclaw-with-searxng-container.yaml +136 -0
  10. package/apps/openwebui-container.yaml +53 -0
  11. package/apps/playwright-container.yaml +120 -0
  12. package/apps/searxng-container.yaml +115 -0
  13. package/dist/auth.d.ts +1 -0
  14. package/dist/auth.js +15 -14
  15. package/dist/auth.js.map +1 -1
  16. package/dist/cli/app.d.ts +1 -0
  17. package/dist/cli/app.js +710 -52
  18. package/dist/cli/app.js.map +1 -1
  19. package/dist/cli/backup.d.ts +3 -0
  20. package/dist/cli/backup.js +434 -0
  21. package/dist/cli/backup.js.map +1 -0
  22. package/dist/cli/doctor.d.ts +1 -0
  23. package/dist/cli/doctor.js +61 -35
  24. package/dist/cli/doctor.js.map +1 -1
  25. package/dist/cli/job.d.ts +1 -0
  26. package/dist/cli/job.js +37 -99
  27. package/dist/cli/job.js.map +1 -1
  28. package/dist/cli/llm.d.ts +1 -0
  29. package/dist/cli/llm.js +20 -14
  30. package/dist/cli/llm.js.map +1 -1
  31. package/dist/cli/managed-list.d.ts +30 -0
  32. package/dist/cli/managed-list.js +129 -0
  33. package/dist/cli/managed-list.js.map +1 -0
  34. package/dist/cli/panel.d.ts +4 -3
  35. package/dist/cli/panel.js +94 -24
  36. package/dist/cli/panel.js.map +1 -1
  37. package/dist/cli/version.d.ts +1 -0
  38. package/dist/cli/version.js +12 -0
  39. package/dist/cli/version.js.map +1 -0
  40. package/dist/cli.js +47 -516
  41. package/dist/cli.js.map +1 -1
  42. package/dist/config.d.ts +68 -0
  43. package/dist/config.js +266 -12
  44. package/dist/config.js.map +1 -1
  45. package/dist/control.d.ts +10 -6
  46. package/dist/control.js +87 -6
  47. package/dist/control.js.map +1 -1
  48. package/dist/install.d.ts +16 -0
  49. package/dist/install.js +75 -26
  50. package/dist/install.js.map +1 -1
  51. package/dist/routes/agent-apps.d.ts +15 -0
  52. package/dist/routes/agent-apps.js +78 -0
  53. package/dist/routes/agent-apps.js.map +1 -0
  54. package/dist/routes/apps.js +186 -7
  55. package/dist/routes/apps.js.map +1 -1
  56. package/dist/routes/backup.js +3 -3
  57. package/dist/routes/backup.js.map +1 -1
  58. package/dist/routes/instances.d.ts +6 -0
  59. package/dist/routes/instances.js +862 -879
  60. package/dist/routes/instances.js.map +1 -1
  61. package/dist/routes/llm.js +9 -8
  62. package/dist/routes/llm.js.map +1 -1
  63. package/dist/routes/runtime.d.ts +15 -0
  64. package/dist/routes/runtime.js +69 -0
  65. package/dist/routes/runtime.js.map +1 -0
  66. package/dist/routes/setup.js +103 -8
  67. package/dist/routes/setup.js.map +1 -1
  68. package/dist/routes/system.js +25 -3
  69. package/dist/routes/system.js.map +1 -1
  70. package/dist/server.js +71 -7
  71. package/dist/server.js.map +1 -1
  72. package/dist/services/agent-apps/catalog.d.ts +30 -0
  73. package/dist/services/agent-apps/catalog.js +60 -0
  74. package/dist/services/agent-apps/catalog.js.map +1 -0
  75. package/dist/services/agent-apps/index.d.ts +36 -0
  76. package/dist/services/agent-apps/index.js +171 -0
  77. package/dist/services/agent-apps/index.js.map +1 -0
  78. package/dist/services/agent-apps/installers/adapter-probes.d.ts +49 -0
  79. package/dist/services/agent-apps/installers/adapter-probes.js +223 -0
  80. package/dist/services/agent-apps/installers/adapter-probes.js.map +1 -0
  81. package/dist/services/agent-apps/installers/adapter.d.ts +30 -0
  82. package/dist/services/agent-apps/installers/adapter.js +171 -0
  83. package/dist/services/agent-apps/installers/adapter.js.map +1 -0
  84. package/dist/services/agent-apps/installers/registry-probe.d.ts +38 -0
  85. package/dist/services/agent-apps/installers/registry-probe.js +183 -0
  86. package/dist/services/agent-apps/installers/registry-probe.js.map +1 -0
  87. package/dist/services/agent-apps/installers/shell-script.d.ts +47 -0
  88. package/dist/services/agent-apps/installers/shell-script.js +471 -0
  89. package/dist/services/agent-apps/installers/shell-script.js.map +1 -0
  90. package/dist/services/agent-apps/types.d.ts +125 -0
  91. package/dist/services/agent-apps/types.js +17 -0
  92. package/dist/services/agent-apps/types.js.map +1 -0
  93. package/dist/services/{app-compiler.d.ts → app/app-compiler.d.ts} +3 -3
  94. package/dist/services/{app-compiler.js → app/app-compiler.js} +10 -7
  95. package/dist/services/app/app-compiler.js.map +1 -0
  96. package/dist/services/app/app-manager.d.ts +142 -0
  97. package/dist/services/app/app-manager.js +1988 -0
  98. package/dist/services/app/app-manager.js.map +1 -0
  99. package/dist/services/app/custom-manager.d.ts +27 -0
  100. package/dist/services/app/custom-manager.js +285 -0
  101. package/dist/services/app/custom-manager.js.map +1 -0
  102. package/dist/services/app/hermes-agent-manager.d.ts +20 -0
  103. package/dist/services/app/hermes-agent-manager.js +289 -0
  104. package/dist/services/app/hermes-agent-manager.js.map +1 -0
  105. package/dist/services/app/id-normalizer.d.ts +27 -0
  106. package/dist/services/app/id-normalizer.js +77 -0
  107. package/dist/services/app/id-normalizer.js.map +1 -0
  108. package/dist/services/app/ollama-manager.d.ts +18 -0
  109. package/dist/services/app/ollama-manager.js +207 -0
  110. package/dist/services/app/ollama-manager.js.map +1 -0
  111. package/dist/services/app/openclaw-manager.d.ts +63 -0
  112. package/dist/services/app/openclaw-manager.js +1178 -0
  113. package/dist/services/app/openclaw-manager.js.map +1 -0
  114. package/dist/services/app/paths.d.ts +47 -0
  115. package/dist/services/app/paths.js +68 -0
  116. package/dist/services/app/paths.js.map +1 -0
  117. package/dist/services/app/registry.d.ts +17 -0
  118. package/dist/services/app/registry.js +31 -0
  119. package/dist/services/app/registry.js.map +1 -0
  120. package/dist/services/app/remote-spec.d.ts +14 -0
  121. package/dist/services/app/remote-spec.js +58 -0
  122. package/dist/services/app/remote-spec.js.map +1 -0
  123. package/dist/services/app/terminal-session-manager.d.ts +27 -0
  124. package/dist/services/app/terminal-session-manager.js +157 -0
  125. package/dist/services/app/terminal-session-manager.js.map +1 -0
  126. package/dist/services/app/types.d.ts +72 -0
  127. package/dist/services/app/types.js +16 -0
  128. package/dist/services/app/types.js.map +1 -0
  129. package/dist/services/backup-manager.js +60 -22
  130. package/dist/services/backup-manager.js.map +1 -1
  131. package/dist/services/instance-manager.d.ts +82 -39
  132. package/dist/services/instance-manager.js +575 -1142
  133. package/dist/services/instance-manager.js.map +1 -1
  134. package/dist/services/llm-proxy/circuit-breaker.js +10 -2
  135. package/dist/services/llm-proxy/circuit-breaker.js.map +1 -1
  136. package/dist/services/llm-proxy/index.d.ts +14 -1
  137. package/dist/services/llm-proxy/index.js +51 -6
  138. package/dist/services/llm-proxy/index.js.map +1 -1
  139. package/dist/services/nomad-manager.d.ts +260 -3
  140. package/dist/services/nomad-manager.js +2866 -449
  141. package/dist/services/nomad-manager.js.map +1 -1
  142. package/dist/services/panel-manager.d.ts +10 -0
  143. package/dist/services/panel-manager.js +97 -0
  144. package/dist/services/panel-manager.js.map +1 -1
  145. package/dist/services/plugin-installer.js +28 -2
  146. package/dist/services/plugin-installer.js.map +1 -1
  147. package/dist/services/process-manager.js +22 -0
  148. package/dist/services/process-manager.js.map +1 -1
  149. package/dist/services/runtime/adapters/custom.d.ts +20 -0
  150. package/dist/services/runtime/adapters/custom.js +90 -0
  151. package/dist/services/runtime/adapters/custom.js.map +1 -0
  152. package/dist/services/runtime/adapters/hermes.d.ts +174 -0
  153. package/dist/services/runtime/adapters/hermes.js +1316 -0
  154. package/dist/services/runtime/adapters/hermes.js.map +1 -0
  155. package/dist/services/runtime/adapters/openclaw-routes.d.ts +17 -0
  156. package/dist/services/runtime/adapters/openclaw-routes.js +946 -0
  157. package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -0
  158. package/dist/services/runtime/adapters/openclaw.d.ts +188 -0
  159. package/dist/services/runtime/adapters/openclaw.js +2195 -0
  160. package/dist/services/runtime/adapters/openclaw.js.map +1 -0
  161. package/dist/services/runtime/errors.d.ts +28 -0
  162. package/dist/services/runtime/errors.js +31 -0
  163. package/dist/services/runtime/errors.js.map +1 -0
  164. package/dist/services/runtime/index.d.ts +34 -0
  165. package/dist/services/runtime/index.js +51 -0
  166. package/dist/services/runtime/index.js.map +1 -0
  167. package/dist/services/runtime/instance.d.ts +24 -0
  168. package/dist/services/runtime/instance.js +143 -0
  169. package/dist/services/runtime/instance.js.map +1 -0
  170. package/dist/services/runtime/migrations.d.ts +15 -0
  171. package/dist/services/runtime/migrations.js +25 -0
  172. package/dist/services/runtime/migrations.js.map +1 -0
  173. package/dist/services/runtime/registry.d.ts +13 -0
  174. package/dist/services/runtime/registry.js +32 -0
  175. package/dist/services/runtime/registry.js.map +1 -0
  176. package/dist/services/runtime/types.d.ts +545 -0
  177. package/dist/services/runtime/types.js +14 -0
  178. package/dist/services/runtime/types.js.map +1 -0
  179. package/dist/services/setup-manager.d.ts +70 -29
  180. package/dist/services/setup-manager.js +278 -597
  181. package/dist/services/setup-manager.js.map +1 -1
  182. package/dist/services/task-registry.d.ts +44 -0
  183. package/dist/services/task-registry.js +74 -0
  184. package/dist/services/task-registry.js.map +1 -0
  185. package/dist/services/telemetry/heartbeat.d.ts +6 -6
  186. package/dist/services/telemetry/heartbeat.js +29 -30
  187. package/dist/services/telemetry/heartbeat.js.map +1 -1
  188. package/dist/types.d.ts +162 -2
  189. package/dist/utils/docker-host.d.ts +15 -0
  190. package/dist/utils/docker-host.js +64 -0
  191. package/dist/utils/docker-host.js.map +1 -0
  192. package/install/jishu-install.sh +25 -1
  193. package/package.json +14 -4
  194. package/public/assets/Dashboard-B-JoOjBQ.js +1 -0
  195. package/public/assets/HermesChatPanel-mFSureyc.js +1 -0
  196. package/public/assets/HermesConfigForm-DvR05LK1.js +4 -0
  197. package/public/assets/InitPassword-CVA8wQA6.js +1 -0
  198. package/public/assets/InstanceDetail-DcZW2QGO.js +91 -0
  199. package/public/assets/{Login-D1Bt-Lyk.js → Login-BWsZH2mu.js} +1 -1
  200. package/public/assets/NewInstance-BCIrAd86.js +1 -0
  201. package/public/assets/Settings-xkDcduFz.js +1 -0
  202. package/public/assets/Setup-Cfuwj4gV.js +1 -0
  203. package/public/assets/WeixinLoginPanel-CnjR8xMu.js +9 -0
  204. package/public/assets/index-CPhVFEsx.css +1 -0
  205. package/public/assets/index-DQsM6Joa.js +19 -0
  206. package/public/assets/input-paste-CrNVAyOy.js +1 -0
  207. package/public/assets/registry-B4UFJdpA.js +2 -0
  208. package/public/assets/{usePolling-CK0DfI4h.js → usePolling-Do5Erqm_.js} +1 -1
  209. package/public/assets/vendor-i18n-ucpM0OR0.js +9 -0
  210. package/public/assets/{vendor-react-B1-3Yrt-.js → vendor-react-Bk1hRGiY.js} +1 -1
  211. package/public/favicon.png +0 -0
  212. package/public/index.html +9 -4
  213. package/public/logos/hermes.png +0 -0
  214. package/public/logos/ollama.png +0 -0
  215. package/public/logos/openclaw.svg +60 -0
  216. package/scripts/build-hermes-image.sh +21 -0
  217. package/scripts/build-local.sh +54 -0
  218. package/scripts/check-adapter-isolation.ts +293 -0
  219. package/scripts/fixtures/instances/hermes-sample/instance.json +37 -0
  220. package/scripts/fixtures/instances/legacy-openclaw-sample/instance.json +7 -0
  221. package/scripts/smoke/hermes-bootstrap.sh +195 -0
  222. package/templates/hermes-entrypoint.sh +154 -0
  223. package/dist/cli/openclaw.d.ts +0 -12
  224. package/dist/cli/openclaw.js +0 -156
  225. package/dist/cli/openclaw.js.map +0 -1
  226. package/dist/services/app-compiler.js.map +0 -1
  227. package/dist/services/app-manager.d.ts +0 -17
  228. package/dist/services/app-manager.js +0 -168
  229. package/dist/services/app-manager.js.map +0 -1
  230. package/dist/services/job-manager.d.ts +0 -22
  231. package/dist/services/job-manager.js +0 -102
  232. package/dist/services/job-manager.js.map +0 -1
  233. package/public/assets/Dashboard-CQsp1Mr9.js +0 -1
  234. package/public/assets/InitPassword-BEC8SE4A.js +0 -1
  235. package/public/assets/InstanceDetail-B5wTgNEg.js +0 -17
  236. package/public/assets/NewInstance-GQzm3K9D.js +0 -1
  237. package/public/assets/Settings-ByjGlqhP.js +0 -1
  238. package/public/assets/Setup-cMF21Y-8.js +0 -1
  239. package/public/assets/index-B6qQP4mH.css +0 -1
  240. package/public/assets/index-BuTQtuNy.js +0 -16
  241. package/public/assets/vendor-i18n-CfW0RvgE.js +0 -9
@@ -1,29 +1,29 @@
1
1
  import { execFileSync, execSync, spawn as nodeSpawn } from "child_process";
2
2
  import { chmodSync, copyFileSync, existsSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync } from "fs";
3
- import { userInfo } from "node:os";
3
+ import { userInfo, platform as osPlatform } from "node:os";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, join } from "path";
7
7
  import { StringDecoder } from "string_decoder";
8
8
  import { fileURLToPath } from "url";
9
- import { JISHUSHELL_HOME, getPanelConfig, savePanelConfig, setOpenclawDockerImage, getOpenclawDockerImage, DEFAULT_OPENCLAW_DOCKER_IMAGE } from "../config.js";
9
+ import { JISHUSHELL_HOME, getPanelConfig, savePanelConfig, setOpenclawDockerImage, getOpenclawDockerImage, DEFAULT_OPENCLAW_DOCKER_IMAGE, getRuntimeCatalogEntry, OPENCLAW_MODULES, OPENCLAW_BIN_DIR, } from "../config.js";
10
10
  import { ensureDirContainer, ensureDirHost, writeConfigFile, writeSecretFile, writeExecutableFile, writeSystemTmpFile } from "../utils/fs.js";
11
+ import { buildDockerClientEnv, managedColimaSocketPath, resolveDockerHost } from "../utils/docker-host.js";
12
+ import { getAdapter, listRegisteredAdapters } from "./runtime/index.js";
13
+ // Internal usage of task primitives — re-exports further down preserve the
14
+ // public import surface used by runtime/adapters/*, routes, and CLI.
15
+ import { createTask, emitTask, getRunningTasks } from "./task-registry.js";
11
16
  const __filename = fileURLToPath(import.meta.url);
12
17
  const __dirname = dirname(__filename);
13
18
  // ── Paths ──────────────────────────────────────────────────────────
14
19
  const BIN_DIR = join(JISHUSHELL_HOME, "bin");
15
- const PACKAGES_DIR = join(JISHUSHELL_HOME, "packages");
16
- const OPENCLAW_PKG_DIR = join(PACKAGES_DIR, "openclaw");
17
- /** npm global-prefix layout: lib/node_modules/<pkg>, bin/<cmd> */
18
- const OPENCLAW_MODULES = join(OPENCLAW_PKG_DIR, "lib", "node_modules");
19
- const OPENCLAW_BIN_DIR = join(OPENCLAW_PKG_DIR, "bin");
20
20
  const NOMAD_BIN = join(BIN_DIR, "nomad");
21
21
  const NOMAD_CONFIG_DIR = join(JISHUSHELL_HOME, "nomad");
22
22
  const NOMAD_DATA_DIR = join(JISHUSHELL_HOME, "nomad", "data");
23
23
  const NOMAD_ALLOC_DIR = join(JISHUSHELL_HOME, "nomad", "data", "alloc");
24
24
  const COLIMA_DIR = join(JISHUSHELL_HOME, "colima");
25
25
  const COLIMA_PROFILE = "jishushell";
26
- const COLIMA_SOCKET = join(COLIMA_DIR, COLIMA_PROFILE, "docker.sock");
26
+ const COLIMA_SOCKET = managedColimaSocketPath(JISHUSHELL_HOME, COLIMA_PROFILE);
27
27
  const NOMAD_VERSION = "1.6.5";
28
28
  let _serverPort = 8090;
29
29
  export function setServerPort(port) { _serverPort = port; }
@@ -76,72 +76,13 @@ function resolveServiceUser() {
76
76
  catch { /* /etc/passwd unreadable */ }
77
77
  throw new Error("Cannot determine service user. Run with a non-root user or set SUDO_UID.");
78
78
  }
79
- const tasks = new Map();
80
- const TASK_MAX_AGE = 600000; // 10 minutes
81
- // Periodically clean up completed tasks to prevent memory accumulation
82
- setInterval(() => {
83
- const now = Date.now();
84
- for (const [id, task] of tasks) {
85
- if (task.status !== "running" && now - parseInt(id.split("-").pop() || "0") > TASK_MAX_AGE) {
86
- tasks.delete(id);
87
- }
88
- }
89
- }, 60000).unref();
90
- function createTask(name) {
91
- const id = `${name}-${Date.now()}`;
92
- const task = { id, name, status: "running", events: [], listeners: new Set() };
93
- tasks.set(id, task);
94
- return task;
95
- }
96
- const MAX_TASK_EVENTS = 500;
97
- function emitTask(task, event) {
98
- task.events.push(event);
99
- // Cap events to prevent unbounded memory growth on long-running tasks
100
- // (e.g., Docker pull/build can produce thousands of progress lines)
101
- if (task.events.length > MAX_TASK_EVENTS) {
102
- task.events.splice(0, task.events.length - MAX_TASK_EVENTS);
103
- }
104
- for (const listener of task.listeners) {
105
- listener(event);
106
- }
107
- }
108
- export function getTask(id) {
109
- return tasks.get(id);
110
- }
111
- export function getTaskSnapshot(id) {
112
- const task = tasks.get(id);
113
- if (!task)
114
- return undefined;
115
- return {
116
- id: task.id,
117
- name: task.name,
118
- status: task.status,
119
- events: [...task.events],
120
- };
121
- }
122
- /** Find running tasks, optionally filtered by name prefix */
123
- export function getRunningTasks(namePrefix) {
124
- const result = [];
125
- for (const [id, task] of tasks) {
126
- if (task.status !== "running")
127
- continue;
128
- if (namePrefix && !task.name.startsWith(namePrefix))
129
- continue;
130
- result.push({ id, name: task.name });
131
- }
132
- return result;
133
- }
134
- export function subscribeTask(id, listener) {
135
- const task = tasks.get(id);
136
- if (!task)
137
- return null;
138
- task.listeners.add(listener);
139
- // Send existing events
140
- for (const event of task.events) {
141
- listener(event);
142
- }
143
- return () => task.listeners.delete(listener);
144
- }
79
+ // ── Task tracker (for SSE progress) ────────────────────────────────
80
+ // Storage, event dispatch, and subscribe plumbing now live in
81
+ // task-registry.ts so non-setup callers (runtime-apps/*) can publish
82
+ // tasks without pulling in the rest of setup-manager's bootstrap
83
+ // surface. These re-exports preserve the original import surface used
84
+ // by runtime/adapters/*, routes, and CLI.
85
+ export { createTask, emitTask, getTask, getTaskSnapshot, getRunningTasks, subscribeTask, } from "./task-registry.js";
145
86
  const ANSI_ESCAPE_RE = /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
146
87
  function sanitizeTaskLine(line) {
147
88
  return line
@@ -150,7 +91,7 @@ function sanitizeTaskLine(line) {
150
91
  .trimEnd();
151
92
  }
152
93
  /** Run a shell command as a spawned process, streaming output to a task */
153
- function spawnWithTask(task, cmd, args, options = {}) {
94
+ export function spawnWithTask(task, cmd, args, options = {}) {
154
95
  return new Promise((resolve) => {
155
96
  const env = { ...process.env, ...options.env };
156
97
  const child = nodeSpawn(cmd, args, {
@@ -214,7 +155,7 @@ function spawnWithTask(task, cmd, args, options = {}) {
214
155
  });
215
156
  }
216
157
  // ── Progress parsers ───────────────────────────────────────────────
217
- function npmProgressParser(line) {
158
+ export function npmProgressParser(line) {
218
159
  // npm shows "added X packages" at the end
219
160
  if (line.includes("added") && line.includes("packages"))
220
161
  return 100;
@@ -223,7 +164,7 @@ function npmProgressParser(line) {
223
164
  return null; // just a log line
224
165
  return null;
225
166
  }
226
- function dockerBuildProgressParser(line) {
167
+ export function dockerBuildProgressParser(line) {
227
168
  // Docker build steps: "Step 1/6", "Step 2/6", etc.
228
169
  const legacyMatch = line.match(/Step\s+(\d+)\/(\d+)/);
229
170
  if (legacyMatch) {
@@ -248,7 +189,7 @@ function curlProgressParser(line) {
248
189
  return null;
249
190
  }
250
191
  // ── Dir size tracker for npm installs ──────────────────────────────
251
- function getDirSizeMB(dir) {
192
+ export function getDirSizeMB(dir) {
252
193
  try {
253
194
  const result = execFileSync("du", ["-sm", dir], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
254
195
  return parseInt(result.split("\t")[0]) || 0;
@@ -284,21 +225,34 @@ function isPortListening(port) {
284
225
  if (!Number.isInteger(port) || port < 1 || port > 65535)
285
226
  return false;
286
227
  const p = String(port);
228
+ const matchesPort = (output) => new RegExp(`:${p}\\s`).test(output);
287
229
  try {
288
230
  if (process.platform === "darwin") {
289
- const result = execFileSync("lsof", ["-iTCP:" + p, "-sTCP:LISTEN", "-t"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
290
- return result.trim().length > 0;
291
- }
292
- else {
293
- // Linux: prefer ss, fall back to netstat
231
+ try {
232
+ const result = execFileSync("lsof", ["-iTCP:" + p, "-sTCP:LISTEN", "-t"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
233
+ if (result.trim().length > 0)
234
+ return true;
235
+ }
236
+ catch {
237
+ }
294
238
  try {
295
239
  const result = execFileSync("ss", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
296
- return new RegExp(`:${p}\\s`).test(result);
240
+ if (matchesPort(result))
241
+ return true;
297
242
  }
298
243
  catch {
299
- const result = execFileSync("netstat", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
300
- return new RegExp(`:${p}\\s`).test(result);
301
244
  }
245
+ const result = execFileSync("netstat", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
246
+ return matchesPort(result);
247
+ }
248
+ // Linux: prefer ss, fall back to netstat
249
+ try {
250
+ const result = execFileSync("ss", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
251
+ return matchesPort(result);
252
+ }
253
+ catch {
254
+ const result = execFileSync("netstat", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
255
+ return matchesPort(result);
302
256
  }
303
257
  }
304
258
  catch {
@@ -360,9 +314,10 @@ export function ensureCgroupMemory() {
360
314
  return false;
361
315
  }
362
316
  function canAccessDockerDaemon(timeout = 10000) {
363
- const env = process.platform === "darwin" && existsSync(COLIMA_SOCKET)
364
- ? { ...process.env, DOCKER_HOST: `unix://${COLIMA_SOCKET}` }
365
- : undefined;
317
+ const env = buildDockerClientEnv({
318
+ jishuHome: JISHUSHELL_HOME,
319
+ colimaProfile: COLIMA_PROFILE,
320
+ });
366
321
  try {
367
322
  execFileSync("docker", ["info"], { timeout, stdio: "ignore", env });
368
323
  return true;
@@ -386,7 +341,7 @@ function getDockerVersionLine(timeout = 10000) {
386
341
  catch { }
387
342
  return "installed";
388
343
  }
389
- export function getSetupStatus() {
344
+ export async function getSetupStatus() {
390
345
  // Fast path: if setup is already complete, do lightweight checks before returning cached result
391
346
  const config = getPanelConfig();
392
347
  if (config.service_manager) {
@@ -433,6 +388,7 @@ export function getSetupStatus() {
433
388
  ready,
434
389
  providerConfigured: !!config.default_provider,
435
390
  hasSudo: true,
391
+ runtimes: await buildRuntimesStatus(),
436
392
  };
437
393
  }
438
394
  }
@@ -512,7 +468,42 @@ export function getSetupStatus() {
512
468
  needsReboot = true;
513
469
  }
514
470
  const runningTasks = getRunningTasks();
515
- return { node: nodeStatus, docker: dockerStatus, nomad: nomadStatus, openclaw: openclawStatus, ready, providerConfigured, dockerImageReady, hasSudo: checkSudo(), needsReboot, runningTasks: runningTasks.length ? runningTasks : undefined };
471
+ return { node: nodeStatus, docker: dockerStatus, nomad: nomadStatus, openclaw: openclawStatus, ready, providerConfigured, dockerImageReady, hasSudo: checkSudo(), needsReboot, runningTasks: runningTasks.length ? runningTasks : undefined, runtimes: await buildRuntimesStatus() };
472
+ }
473
+ /**
474
+ * Build the per-runtime install status block by iterating over every
475
+ * registered adapter. A new adapter that implements `getInstallStatus()`
476
+ * automatically appears here — no change required in this function.
477
+ *
478
+ * Each probe is bounded by a short timeout so a hung adapter (slow docker
479
+ * socket, DNS stall) cannot block the `/api/setup/status` endpoint. This
480
+ * mirrors the pattern used by `routes/runtime.ts:probeInstallStatus` so
481
+ * Setup wizard and `/api/runtime/catalog` see a consistent view.
482
+ */
483
+ const BUILD_RUNTIMES_PROBE_TIMEOUT_MS = 2500;
484
+ async function buildRuntimesStatus() {
485
+ const adapters = listRegisteredAdapters().filter((a) => typeof a.getInstallStatus === "function");
486
+ const probes = adapters.map(async (adapter) => {
487
+ try {
488
+ const status = await Promise.race([
489
+ Promise.resolve(adapter.getInstallStatus()),
490
+ new Promise((_, reject) => setTimeout(() => reject(new Error("install-status probe timed out")), BUILD_RUNTIMES_PROBE_TIMEOUT_MS)),
491
+ ]);
492
+ if (!status)
493
+ return null;
494
+ const sync = status;
495
+ return [adapter.agentType, { ...sync, required: !!adapter.required }];
496
+ }
497
+ catch {
498
+ return null;
499
+ }
500
+ });
501
+ const out = {};
502
+ for (const result of await Promise.all(probes)) {
503
+ if (result)
504
+ out[result[0]] = result[1];
505
+ }
506
+ return Object.keys(out).length ? out : undefined;
516
507
  }
517
508
  // ── Upgrade Node.js ────────────────────────────────────────────────
518
509
  export async function upgradeNode(targetMajor = 22) {
@@ -1025,6 +1016,7 @@ export async function installNomad() {
1025
1016
  if (versionLine) {
1026
1017
  const match = versionLine.match(/v(\d+\.\d+\.\d+)/);
1027
1018
  const currentVersion = match ? match[1] : "";
1019
+ let migrated = false;
1028
1020
  if (currentVersion && isNomadVersionGreater(currentVersion, NOMAD_VERSION)) {
1029
1021
  // Current > target — auto-migrate (nomad 1.11.3 BSL → 1.6.5 MPL).
1030
1022
  // Migration failure is a hard stop: the old state has been
@@ -1040,9 +1032,12 @@ export async function installNomad() {
1040
1032
  error: migErr?.message || String(migErr),
1041
1033
  };
1042
1034
  }
1043
- const newLine = execSync(`"${NOMAD_BIN}" version`, { encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
1044
- return { ok: true, message: `Nomad migrated to ${newLine}` };
1035
+ migrated = true;
1036
+ versionLine = execSync(`"${NOMAD_BIN}" version`, { encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
1045
1037
  }
1038
+ // Always (re)write config so the installed nomad.hcl stays up-to-date
1039
+ // with the current defaults (e.g. raw_exec plugin, acl, limits).
1040
+ writeNomadConfig();
1046
1041
  // Ensure Nomad is started even if already installed
1047
1042
  if (!isPortListening(4646)) {
1048
1043
  try {
@@ -1053,7 +1048,9 @@ export async function installNomad() {
1053
1048
  await startNomad();
1054
1049
  }
1055
1050
  }
1056
- return { ok: true, message: `Nomad already installed: ${versionLine}` };
1051
+ return migrated
1052
+ ? { ok: true, message: `Nomad migrated to ${versionLine}` }
1053
+ : { ok: true, message: `Nomad already installed: ${versionLine}` };
1057
1054
  }
1058
1055
  }
1059
1056
  }
@@ -1067,6 +1064,7 @@ export async function installNomad() {
1067
1064
  symlinkSync(systemNomad, NOMAD_BIN);
1068
1065
  const version = execSync(`${NOMAD_BIN} version`, { encoding: "utf-8", timeout: 5000 }).trim();
1069
1066
  console.log(`[nomad] Linked system nomad ${systemNomad} → ${NOMAD_BIN}`);
1067
+ writeNomadConfig();
1070
1068
  return { ok: true, message: `Nomad linked from system: ${version.split("\n")[0]}` };
1071
1069
  }
1072
1070
  }
@@ -1144,7 +1142,16 @@ function writeNomadConfig() {
1144
1142
  ensureDirHost(NOMAD_CONFIG_DIR);
1145
1143
  ensureDirContainer(NOMAD_DATA_DIR);
1146
1144
  ensureDirContainer(NOMAD_ALLOC_DIR);
1147
- const loopbackIface = process.platform === "darwin" ? "lo0" : "lo";
1145
+ const platform = osPlatform();
1146
+ const loopbackIface = platform === "darwin" ? "lo0" : "lo";
1147
+ const externalIface = detectNomadExternalInterface(loopbackIface);
1148
+ const externalHostNetworkBlock = externalIface
1149
+ ? `
1150
+ host_network "external" {
1151
+ interface = "${externalIface}"
1152
+ }
1153
+ `
1154
+ : "";
1148
1155
  const config = `
1149
1156
  data_dir = "${NOMAD_DATA_DIR}"
1150
1157
 
@@ -1168,6 +1175,7 @@ client {
1168
1175
  servers = ["127.0.0.1:4647"]
1169
1176
  network_interface = "${loopbackIface}"
1170
1177
  alloc_dir = "${NOMAD_ALLOC_DIR}"
1178
+ ${externalHostNetworkBlock}
1171
1179
 
1172
1180
  # drain_on_shutdown intentionally omitted: on single-node Pi there is
1173
1181
  # nowhere to drain workloads to, and draining on every systemctl restart
@@ -1184,12 +1192,45 @@ plugin "docker" {
1184
1192
  }
1185
1193
  }
1186
1194
 
1195
+ plugin "raw_exec" {
1196
+ config {
1197
+ enabled = true
1198
+ }
1199
+ }
1200
+
1187
1201
  acl {
1188
1202
  enabled = true
1189
1203
  }
1204
+
1205
+ limits {
1206
+ http_max_conns_per_client = 0
1207
+ }
1190
1208
  `;
1191
1209
  writeConfigFile(join(NOMAD_CONFIG_DIR, "nomad.hcl"), config);
1192
1210
  }
1211
+ function detectNomadExternalInterface(loopbackIface) {
1212
+ try {
1213
+ if (osPlatform() === "darwin") {
1214
+ const route = execFileSync("route", ["-n", "get", "default"], {
1215
+ encoding: "utf8",
1216
+ timeout: 3000,
1217
+ });
1218
+ const match = route.match(/interface:\s*(\S+)/);
1219
+ const iface = match?.[1]?.trim() ?? "";
1220
+ return iface && iface !== loopbackIface ? iface : "";
1221
+ }
1222
+ const route = execFileSync("ip", ["route", "show", "default"], {
1223
+ encoding: "utf8",
1224
+ timeout: 3000,
1225
+ });
1226
+ const match = route.match(/\bdev\s+(\S+)/);
1227
+ const iface = match?.[1]?.trim() ?? "";
1228
+ return iface && iface !== loopbackIface ? iface : "";
1229
+ }
1230
+ catch {
1231
+ return "";
1232
+ }
1233
+ }
1193
1234
  export function loadNomadToken() {
1194
1235
  if (process.env.NOMAD_TOKEN)
1195
1236
  return;
@@ -1533,7 +1574,14 @@ export function installNomadSystemd() {
1533
1574
  if (process.platform === "darwin") {
1534
1575
  const plistLabel = "com.jishushell.nomad";
1535
1576
  const logPath = join(NOMAD_CONFIG_DIR, "nomad.log");
1536
- const dockerSock = COLIMA_SOCKET;
1577
+ const dockerHost = resolveDockerHost({
1578
+ jishuHome: JISHUSHELL_HOME,
1579
+ colimaProfile: COLIMA_PROFILE,
1580
+ });
1581
+ const dockerHostEntry = dockerHost
1582
+ ? `
1583
+ <key>DOCKER_HOST</key><string>${dockerHost}</string>`
1584
+ : "";
1537
1585
  const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
1538
1586
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1539
1587
  <plist version="1.0">
@@ -1546,8 +1594,7 @@ export function installNomadSystemd() {
1546
1594
  <string>-config=${configPath}</string>
1547
1595
  </array>
1548
1596
  <key>EnvironmentVariables</key>
1549
- <dict>
1550
- <key>DOCKER_HOST</key><string>unix://${dockerSock}</string>
1597
+ <dict>${dockerHostEntry}
1551
1598
  <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
1552
1599
  </dict>
1553
1600
  <key>RunAtLoad</key><true/>
@@ -1582,6 +1629,8 @@ EnvironmentFile=-/etc/jishushell/nomad.env
1582
1629
  ExecStart=${nomadPath} agent -config=${configPath}
1583
1630
  Restart=on-failure
1584
1631
  RestartSec=3
1632
+ Delegate=yes
1633
+ TasksMax=infinity
1585
1634
 
1586
1635
  [Install]
1587
1636
  WantedBy=multi-user.target
@@ -1624,7 +1673,6 @@ export function installJishushellSystemd(port) {
1624
1673
  export JISHUSHELL_HOME="${JISHUSHELL_HOME}"
1625
1674
  export HOME="${realHome}"
1626
1675
  export NODE_ENV=production
1627
- export DOCKER_HOST="unix://${COLIMA_SOCKET}"
1628
1676
  export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:${dirname(nodeBin)}:\${PATH}"
1629
1677
  exec "${nodeBin}" "${cliBin}" serve --port ${resolvedPort}
1630
1678
  `;
@@ -1693,64 +1741,77 @@ WantedBy=multi-user.target
1693
1741
  }
1694
1742
  }
1695
1743
  // ── Install OpenClaw (async with progress) ─────────────────────────
1696
- const OPENCLAW_EXPECTED_SIZE_MB = 700;
1744
+ /**
1745
+ * Install OpenClaw runtime — thin dispatch wrapper.
1746
+ *
1747
+ * The heavy lifting lives in `OpenClawAdapter.installRuntime()` (§32.2.4).
1748
+ * This wrapper is kept only for back-compat with the existing
1749
+ * `routes/setup.ts` endpoint and the CLI installer.
1750
+ */
1697
1751
  export async function installOpenclaw(version = "latest") {
1752
+ const adapter = getAdapter("openclaw");
1753
+ if (typeof adapter.installRuntime !== "function") {
1754
+ return { ok: false, message: "OpenClawAdapter.installRuntime is not implemented" };
1755
+ }
1756
+ return adapter.installRuntime({ version });
1757
+ }
1758
+ // ── Hermes install (§32.1 / §32.3) ─────────────────────────────────
1759
+ //
1760
+ // Hermes constants (HERMES_DEFAULT_IMAGE, HERMES_RUNTIME_DIR,
1761
+ // HERMES_SHIM_FILENAME) and the shim template resolver have moved to
1762
+ // `src/config.ts` (§32 Phase 8). setup-manager no longer defines them.
1763
+ // The HermesAdapter imports them directly from config.ts.
1764
+ /**
1765
+ * Capture the immutable digest of a locally-present image so HermesAdapter
1766
+ * can pin via digest rather than the mutable tag. Returns undefined if the
1767
+ * image has no RepoDigests (e.g. locally-built image) — caller may still
1768
+ * proceed using the tag alone.
1769
+ */
1770
+ export function captureImageDigest(imageRef) {
1698
1771
  try {
1699
- const openclawPkgDir = join(OPENCLAW_MODULES, "openclaw");
1700
- if (existsSync(openclawPkgDir)) {
1701
- const ver = getLocalOpenclawVersion() || "unknown";
1702
- return { ok: true, message: `OpenClaw already installed: ${ver}` };
1703
- }
1704
- const task = createTask("openclaw");
1705
- ensureDirHost(OPENCLAW_PKG_DIR);
1706
- emitTask(task, { type: "progress", message: "开始安装 OpenClaw...", progress: 0 });
1707
- // Monitor directory size for progress estimation
1708
- const sizeTracker = setInterval(() => {
1709
- const sizeMB = getDirSizeMB(OPENCLAW_PKG_DIR);
1710
- const pct = Math.min(95, Math.round((sizeMB / OPENCLAW_EXPECTED_SIZE_MB) * 95));
1711
- if (pct > 0) {
1712
- emitTask(task, { type: "progress", message: `下载安装中... ${sizeMB}MB / ~${OPENCLAW_EXPECTED_SIZE_MB}MB`, progress: pct });
1713
- }
1714
- }, 3000);
1715
- // Use npm install -g with --prefix so npm uses global-install semantics:
1716
- // packages go to <prefix>/lib/node_modules/, bins to <prefix>/bin/
1717
- // This makes postinstall scripts run naturally (no manual workarounds needed).
1718
- const result = await spawnWithTask(task, "npm", ["install", "-g", "--prefix", OPENCLAW_PKG_DIR, `openclaw@${version}`], { timeout: 600000, progressParser: npmProgressParser });
1719
- clearInterval(sizeTracker);
1720
- if (!result.ok) {
1721
- emitTask(task, { type: "error", message: "OpenClaw 安装失败" });
1722
- task.status = "error";
1723
- return { ok: false, message: "OpenClaw installation failed", error: result.output, taskId: task.id };
1724
- }
1725
- // Read version from package.json since openclaw --version needs Node 22+
1726
- let ver = "installed";
1727
- try {
1728
- const pkg = join(OPENCLAW_MODULES, "openclaw", "package.json");
1729
- if (existsSync(pkg)) {
1730
- const pkgJson = JSON.parse(readFileSync(pkg, "utf-8"));
1731
- ver = pkgJson.version || "installed";
1732
- }
1733
- }
1734
- catch { }
1735
- emitTask(task, { type: "done", message: `OpenClaw 安装完成: ${ver}`, progress: 100 });
1736
- task.status = "done";
1737
- return { ok: true, message: `OpenClaw installed: ${ver}`, taskId: task.id };
1772
+ const raw = execFileSync("docker", [
1773
+ "image",
1774
+ "inspect",
1775
+ "--format",
1776
+ "{{range .RepoDigests}}{{.}}{{\"\\n\"}}{{end}}",
1777
+ imageRef,
1778
+ ], { encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
1779
+ const first = raw.split("\n").map((l) => l.trim()).find(Boolean);
1780
+ return first || undefined;
1738
1781
  }
1739
- catch (e) {
1740
- return { ok: false, message: "OpenClaw installation failed", error: e.message };
1782
+ catch {
1783
+ return undefined;
1741
1784
  }
1742
1785
  }
1743
- // ── Helpers ──────────────────────────────────────────────────────
1744
- function getLocalOpenclawVersion() {
1745
- try {
1746
- const pkg = join(OPENCLAW_MODULES, "openclaw", "package.json");
1747
- if (existsSync(pkg))
1748
- return JSON.parse(readFileSync(pkg, "utf-8")).version || "";
1786
+ /**
1787
+ * Install Hermes runtime — thin dispatch wrapper.
1788
+ *
1789
+ * Heavy lifting lives in `HermesAdapter.installRuntime()` (§32.2.4).
1790
+ */
1791
+ export async function installHermes() {
1792
+ const adapter = getAdapter("hermes");
1793
+ if (typeof adapter.installRuntime !== "function") {
1794
+ return { ok: false, message: "HermesAdapter.installRuntime is not implemented" };
1749
1795
  }
1750
- catch { }
1751
- return "";
1796
+ return adapter.installRuntime();
1797
+ }
1798
+ /**
1799
+ * Non-blocking variant — returns a task id immediately. Implementation
1800
+ * dispatches via `HermesAdapter.startInstallRuntime()`.
1801
+ */
1802
+ export function startInstallHermes() {
1803
+ const adapter = getAdapter("hermes");
1804
+ if (typeof adapter.startInstallRuntime !== "function") {
1805
+ return { ok: false, message: "HermesAdapter.startInstallRuntime is not implemented" };
1806
+ }
1807
+ return adapter.startInstallRuntime();
1808
+ }
1809
+ /** Helper for setup status — true when runtime catalog has hermes entry. */
1810
+ export function isHermesInstalled() {
1811
+ const entry = getRuntimeCatalogEntry("hermes");
1812
+ return !!entry && !!entry.defaultImage;
1752
1813
  }
1753
- function resolveDockerInvocation() {
1814
+ export function resolveDockerInvocation() {
1754
1815
  try {
1755
1816
  execFileSync("docker", ["info"], { timeout: 5000, stdio: "ignore" });
1756
1817
  return { cmd: "docker", argsPrefix: [] };
@@ -1810,452 +1871,31 @@ function checkDockerImageExists() {
1810
1871
  return false;
1811
1872
  }
1812
1873
  }
1813
- // The stable tag for the JishuShell base image (slimno OpenClaw binary baked in).
1874
+ // The stable tag for the JishuShell base image. Reads panel.json framework-level.
1814
1875
  function resolveDockerImageTag() {
1815
1876
  return getOpenclawDockerImage();
1816
1877
  }
1817
- // Base image and mirror list for the OpenClaw Docker build.
1818
- // Mirrors are tried in order when docker.io is unreachable (e.g. behind GFW or rate-limited).
1819
- const DOCKER_BASE_IMAGE = "node:22-slim";
1820
- const DOCKER_BASE_MIRRORS = [
1821
- "node:22-slim",
1822
- "hub-mirror.c.163.com/library/node:22-slim",
1823
- "mirrors.tencent.com/library/node:22-slim",
1824
- "registry.cn-hangzhou.aliyuncs.com/library/node:22-slim",
1825
- ];
1826
- // Pull DOCKER_BASE_IMAGE from mirrors if not already cached locally.
1827
- // Returns the image content digest (sha256:…) so that the subsequent
1828
- // docker build can use `FROM <digest>`, which BuildKit resolves from
1829
- // the local daemon without any outbound registry metadata request.
1830
- async function ensureDockerBaseImage(invocation, task) {
1831
- // Fast path: already in local daemon cache — return image name (not digest) for Dockerfile FROM
1832
- try {
1833
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", DOCKER_BASE_IMAGE], {
1834
- timeout: 5000,
1835
- stdio: "ignore",
1836
- });
1837
- emitTask(task, { type: "log", message: `基础镜像已缓存: ${DOCKER_BASE_IMAGE}` });
1838
- return DOCKER_BASE_IMAGE;
1839
- }
1840
- catch { /* not cached, fall through */ }
1841
- for (const mirror of DOCKER_BASE_MIRRORS) {
1842
- emitTask(task, { type: "log", message: `拉取基础镜像: ${mirror} ...` });
1843
- const result = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "pull", mirror], { timeout: 300000 });
1844
- if (result.ok) {
1845
- if (mirror !== DOCKER_BASE_IMAGE) {
1846
- try {
1847
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "tag", mirror, DOCKER_BASE_IMAGE], { timeout: 10000 });
1848
- }
1849
- catch { /* tag failure is non-fatal */ }
1850
- }
1851
- emitTask(task, { type: "log", message: `基础镜像就绪: ${DOCKER_BASE_IMAGE}` });
1852
- return DOCKER_BASE_IMAGE;
1853
- }
1854
- emitTask(task, { type: "log", message: ` → ${mirror} 不可达,尝试下一个镜像源...` });
1855
- }
1856
- throw new Error(`无法获取基础镜像 ${DOCKER_BASE_IMAGE}。请检查网络或手动执行: docker pull ${DOCKER_BASE_MIRRORS[1]}`);
1857
- }
1858
- function resolveVersionedBuildTag() {
1859
- try {
1860
- const pkg = join(OPENCLAW_MODULES, "openclaw", "package.json");
1861
- if (existsSync(pkg)) {
1862
- const ver = JSON.parse(readFileSync(pkg, "utf-8")).version;
1863
- if (ver)
1864
- return `jishushell-openclaw:${ver}`;
1865
- }
1866
- }
1867
- catch { }
1868
- return resolveDockerImageTag();
1869
- }
1870
- async function buildOpenclawDockerImageWithTask(task, tag) {
1871
- const targetTag = tag || resolveVersionedBuildTag();
1872
- try {
1873
- const invocation = resolveDockerInvocation();
1874
- // Fast check: if image with this exact tag exists, skip build (tag encodes version)
1875
- try {
1876
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", targetTag], {
1877
- timeout: 10000,
1878
- stdio: "ignore",
1879
- });
1880
- setOpenclawDockerImage(targetTag);
1881
- emitTask(task, { type: "done", message: `Docker 镜像已存在: ${targetTag}`, progress: 100 });
1882
- task.status = "done";
1883
- return { ok: true, message: `Docker image ${targetTag} already exists`, taskId: task.id };
1884
- }
1885
- catch { /* image not found, proceed to build */ }
1886
- // Clean up old openclaw:* images that predate the new slim-base architecture.
1887
- // Skip if the stored tag matches targetTag or is a remote registry image.
1888
- const oldTag = getPanelConfig().openclaw_image;
1889
- if (oldTag && oldTag !== targetTag && !oldTag.includes("/") && /^openclaw:/i.test(oldTag)) {
1890
- try {
1891
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "rmi", oldTag], { timeout: 15000, stdio: "ignore" });
1892
- }
1893
- catch { }
1894
- }
1895
- // Ensure node:22-slim base image is in the local daemon cache before building.
1896
- emitTask(task, { type: "progress", message: "准备基础镜像...", progress: 5 });
1897
- const baseImageId = await ensureDockerBaseImage(invocation, task);
1898
- // Slim Dockerfile: no COPY of openclaw node_modules — the binary is bind-mounted
1899
- // from the host at runtime, so this image needs no OpenClaw-specific layers.
1900
- // Build context is an empty temp directory (no files to COPY).
1901
- const dockerfile = `FROM ${baseImageId}
1902
- RUN apt-get update && apt-get install -y --no-install-recommends \\
1903
- ca-certificates curl \\
1904
- python3 python3-pip python3-venv python3-dev && \\
1905
- ln -sf /usr/bin/python3 /usr/local/bin/python && \\
1906
- rm -rf /var/lib/apt/lists/*
1907
- WORKDIR /data
1908
- USER node
1909
- ENTRYPOINT ["node", "/usr/lib/node_modules/openclaw/openclaw.mjs"]
1910
- CMD ["gateway", "run", "--port", "18789", "--allow-unconfigured"]
1911
- `;
1912
- // Use a temp dir as build context — no files to COPY means no large transfer.
1913
- const buildDir = join(tmpdir(), `jishushell-base-build-${Date.now()}`);
1914
- ensureDirHost(buildDir);
1915
- const dockerfilePath = join(buildDir, "Dockerfile");
1916
- writeConfigFile(dockerfilePath, dockerfile);
1917
- emitTask(task, { type: "progress", message: `构建基础镜像 ${targetTag}(无需拷贝二进制,速度极快)...`, progress: 10 });
1918
- let result;
1919
- try {
1920
- result = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "build", "--network=host", "-t", targetTag, buildDir], {
1921
- timeout: 1800000,
1922
- progressParser: dockerBuildProgressParser,
1923
- env: {},
1924
- });
1925
- }
1926
- finally {
1927
- // Always clean up temp build dir
1928
- try {
1929
- execSync(`rm -rf "${buildDir}"`, { timeout: 5000 });
1930
- }
1931
- catch { }
1932
- }
1933
- if (!result.ok) {
1934
- // Clean up dangling images from failed build
1935
- try {
1936
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "prune", "-f"], { timeout: 15000, stdio: "ignore" });
1937
- }
1938
- catch { }
1939
- emitTask(task, { type: "error", message: "Docker 镜像构建失败" });
1940
- task.status = "error";
1941
- return { ok: false, message: "Docker image build failed", error: result.output, taskId: task.id };
1942
- }
1943
- const localTag = "jishushell-openclaw:local";
1944
- if (targetTag !== localTag) {
1945
- try {
1946
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "tag", targetTag, localTag], { timeout: 10000, stdio: "ignore" });
1947
- }
1948
- catch { }
1949
- }
1950
- setOpenclawDockerImage(localTag);
1951
- emitTask(task, { type: "done", message: `Docker 镜像构建完成: ${targetTag}`, progress: 100 });
1952
- task.status = "done";
1953
- return { ok: true, message: `Docker image ${targetTag} built`, taskId: task.id };
1954
- }
1955
- catch (e) {
1956
- // Clean up build artifacts on unexpected errors
1957
- try {
1958
- execSync("docker image prune -f", { timeout: 15000, stdio: "ignore" });
1959
- }
1960
- catch { }
1961
- emitTask(task, { type: "error", message: `Docker 镜像构建失败: ${e.message}` });
1962
- task.status = "error";
1963
- return { ok: false, message: "Docker image build failed", error: e.message, taskId: task.id };
1964
- }
1965
- }
1966
- export async function buildOpenclawDockerImage(tag) {
1967
- const task = createTask("openclaw-docker");
1968
- return buildOpenclawDockerImageWithTask(task, tag);
1969
- }
1970
- export function startBuildOpenclawDockerImage(tag) {
1971
- const task = createTask("openclaw-docker");
1972
- void buildOpenclawDockerImageWithTask(task, tag).catch((err) => {
1973
- emitTask(task, { type: "error", message: `Docker 镜像构建失败: ${err?.message || err}` });
1974
- task.status = "error";
1975
- });
1976
- return { ok: true, message: "Docker image build started", taskId: task.id };
1977
- }
1978
- // ── Build OpenClaw Docker image from npm package + Python ─────────
1979
- async function buildCustomOpenclawImageWithTask(task, tag) {
1980
- const targetTag = tag || DEFAULT_OPENCLAW_DOCKER_IMAGE;
1981
- try {
1982
- const invocation = resolveDockerInvocation();
1983
- // Fast check: if image already exists locally, skip
1984
- try {
1985
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", targetTag], {
1986
- timeout: 10000,
1987
- stdio: "ignore",
1988
- });
1989
- setOpenclawDockerImage(targetTag);
1990
- emitTask(task, { type: "done", message: `Docker 镜像已存在: ${targetTag}`, progress: 100 });
1991
- task.status = "done";
1992
- return { ok: true, message: `Docker image ${targetTag} already exists`, taskId: task.id };
1993
- }
1994
- catch { /* image not found, proceed */ }
1995
- // Clean up old legacy images
1996
- const oldTag = getPanelConfig().openclaw_image;
1997
- if (oldTag && oldTag !== targetTag && /^jishushell-base:/i.test(oldTag)) {
1998
- try {
1999
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "rmi", oldTag], { timeout: 15000, stdio: "ignore" });
2000
- }
2001
- catch { }
2002
- }
2003
- // Step 1: Ensure OpenClaw npm package is installed (used as build context)
2004
- const openclawPkgDir = join(OPENCLAW_MODULES, "openclaw");
2005
- if (!existsSync(openclawPkgDir)) {
2006
- emitTask(task, { type: "progress", message: "安装 OpenClaw npm 包...", progress: 5 });
2007
- const installResult = await installOpenclaw();
2008
- if (!installResult.ok) {
2009
- emitTask(task, { type: "error", message: "OpenClaw 安装失败" });
2010
- task.status = "error";
2011
- return { ok: false, message: "OpenClaw npm install failed", error: installResult.error, taskId: task.id };
2012
- }
2013
- }
2014
- // Step 2: Ensure base image is available
2015
- emitTask(task, { type: "progress", message: "准备基础镜像...", progress: 10 });
2016
- const baseImageId = await ensureDockerBaseImage(invocation, task);
2017
- // Step 3: Build image — COPY node_modules + Python
2018
- emitTask(task, { type: "progress", message: `构建 OpenClaw 镜像(含 Python): ${targetTag} ...`, progress: 30 });
2019
- const dockerfile = `FROM ${baseImageId}
2020
- USER root
2021
- RUN apt-get update && apt-get install -y --no-install-recommends \\
2022
- procps hostname curl git lsof openssl \\
2023
- python3 python3-pip python3-venv python3-dev && \\
2024
- ln -sf /usr/bin/python3 /usr/local/bin/python && \\
2025
- rm -rf /var/lib/apt/lists/*
2026
- WORKDIR /app
2027
- COPY --chown=node:node lib/node_modules/ ./node_modules/
2028
- RUN ln -sf /app/node_modules/openclaw/openclaw.mjs /app/openclaw.mjs && \\
2029
- ln -sf /app/node_modules/openclaw/openclaw.mjs /usr/local/bin/openclaw && \\
2030
- cp /app/node_modules/openclaw/package.json /app/package.json 2>/dev/null || true
2031
- USER node
2032
- CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
2033
- `;
2034
- // Write Dockerfile into the npm package directory (build context)
2035
- const dockerfilePath = join(OPENCLAW_PKG_DIR, "Dockerfile");
2036
- writeConfigFile(dockerfilePath, dockerfile);
2037
- let buildResult;
2038
- try {
2039
- buildResult = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "build", "--network=host", "-t", targetTag, OPENCLAW_PKG_DIR], { timeout: 1800000, progressParser: dockerBuildProgressParser });
2040
- }
2041
- finally {
2042
- try {
2043
- unlinkSync(dockerfilePath);
2044
- }
2045
- catch { }
2046
- }
2047
- if (!buildResult.ok) {
2048
- try {
2049
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "prune", "-f"], { timeout: 15000, stdio: "ignore" });
2050
- }
2051
- catch { }
2052
- emitTask(task, { type: "error", message: "Docker 镜像构建失败" });
2053
- task.status = "error";
2054
- return { ok: false, message: "Docker image build failed", error: buildResult.output, taskId: task.id };
2055
- }
2056
- setOpenclawDockerImage(targetTag);
2057
- emitTask(task, { type: "done", message: `OpenClaw 镜像就绪: ${targetTag}(含 Python)`, progress: 100 });
2058
- task.status = "done";
2059
- return { ok: true, message: `Docker image ${targetTag} built`, taskId: task.id };
2060
- }
2061
- catch (e) {
2062
- emitTask(task, { type: "error", message: `镜像构建失败: ${e.message}` });
2063
- task.status = "error";
2064
- return { ok: false, message: "Docker image build failed", error: e.message, taskId: task.id };
2065
- }
2066
- }
2067
- // ── Pull or build OpenClaw Docker image ───────────────────────────
2068
- /** Matches a semver-ish tag suffix, e.g. "...:2026.4.9" or "...:v1.2.3-beta". */
2069
- const PINNED_IMAGE_TAG_RE = /:[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$/;
2070
- /**
2071
- * Query the npm registry for the current OpenClaw version. Used to bust the
2072
- * Docker layer cache for `RUN npm install openclaw@${ver}` during local build.
2073
- * Returns "latest" when npm is unreachable so the build can still proceed.
2074
- */
2075
- function resolveOpenclawNpmVersion() {
2076
- try {
2077
- const out = execFileSync("npm", ["view", "openclaw", "version"], {
2078
- timeout: 15000,
2079
- encoding: "utf-8",
2080
- stdio: ["ignore", "pipe", "ignore"],
2081
- }).trim();
2082
- if (/^\d+\.\d+\.\d+/.test(out))
2083
- return out;
2084
- }
2085
- catch { /* npm not reachable */ }
2086
- return "latest";
2087
- }
2088
- /**
2089
- * Read the OpenClaw version actually bundled at /app/ inside a Docker image,
2090
- * bypassing `openclaw-entry.sh`'s `.npm-global/` override. This is the
2091
- * authoritative source of truth — the image's OCI label can be wrong
2092
- * (CI bugs, layer cache reuse), but `/app/node_modules/openclaw/package.json`
2093
- * is the exact content that ran through `npm install`.
2094
- *
2095
- * Spawns a throw-away container with `--entrypoint node` so Node prints the
2096
- * version directly. Returns "" when docker is unavailable or the path is
2097
- * missing (e.g. a non-openclaw image).
2098
- */
2099
- function readBundledOpenclawVersion(invocation, image) {
2100
- try {
2101
- const out = execFileSync(invocation.cmd, [
2102
- ...invocation.argsPrefix,
2103
- "run", "--rm",
2104
- "--entrypoint", "node",
2105
- image,
2106
- "-p",
2107
- "require('/app/node_modules/openclaw/package.json').version",
2108
- ], { timeout: 20000, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2109
- if (/^\d+\.\d+\.\d+/.test(out))
2110
- return out;
2111
- }
2112
- catch { /* docker unavailable, image missing, or path not present */ }
2113
- return "";
2114
- }
2115
- /**
2116
- * After a successful pull or build, capture the image's real OpenClaw version
2117
- * and return a pinned tag of the form `ghcr.io/.../openclaw-runtime:<version>`.
2118
- * The pinned tag is added as a local alias via `docker tag` so subsequent
2119
- * Nomad allocations see an immutable reference and never re-pull on restart.
2120
- *
2121
- * Version discovery order:
2122
- * 1. `explicitVersion` when the caller already knows it (e.g. the local build
2123
- * path, which queries npm for the version and passes it as `--build-arg`).
2124
- * 2. Bundled `/app/node_modules/openclaw/package.json` inside the image
2125
- * (authoritative — bypasses both the `.npm-global/` override layer and a
2126
- * potentially stale OCI label).
2127
- *
2128
- * Returns the original tag unchanged when:
2129
- * - the target is already a pinned version tag
2130
- * - no version can be discovered
2131
- * - docker tag fails for any reason
2132
- */
2133
- function capturePinnedImageTag(invocation, targetTag, explicitVersion) {
2134
- // Already pinned? Nothing to do.
2135
- if (PINNED_IMAGE_TAG_RE.test(targetTag))
2136
- return targetTag;
2137
- let version = explicitVersion && /^\d+\.\d+\.\d+/.test(explicitVersion) ? explicitVersion : "";
2138
- if (!version) {
2139
- version = readBundledOpenclawVersion(invocation, targetTag);
2140
- }
2141
- if (!version || !/^\d+\.\d+\.\d+/.test(version))
2142
- return targetTag;
2143
- // Build the pinned tag by replacing the mutable tag portion.
2144
- // "ghcr.io/foo/bar:latest" → "ghcr.io/foo/bar:2026.4.9"
2145
- // "ghcr.io/foo/bar" → "ghcr.io/foo/bar:2026.4.9"
2146
- const colonIdx = targetTag.lastIndexOf(":");
2147
- const slashIdx = targetTag.lastIndexOf("/");
2148
- const hasTag = colonIdx > slashIdx;
2149
- const repo = hasTag ? targetTag.slice(0, colonIdx) : targetTag;
2150
- const pinnedTag = `${repo}:${version}`;
2151
- if (pinnedTag === targetTag)
2152
- return targetTag;
2153
- try {
2154
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "tag", targetTag, pinnedTag], { timeout: 10000, stdio: "ignore" });
2155
- }
2156
- catch {
2157
- // Could not create the local alias — fall back to original tag.
2158
- return targetTag;
2159
- }
2160
- // Drop the mutable original alias (`:latest` / `:slim`) now that the pinned
2161
- // tag is in place. Removing a tag is cheap and leaves the underlying image
2162
- // alive because the new pinned reference still points to it. Best-effort:
2163
- // silent when the tag is already gone or in use.
2164
- if (/:(latest|slim)$/.test(targetTag)) {
2165
- try {
2166
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "rmi", targetTag], { timeout: 10000, stdio: "ignore" });
2167
- }
2168
- catch { /* best-effort cleanup */ }
2169
- }
2170
- return pinnedTag;
2171
- }
2172
- async function pullOrBuildOpenclawImageWithTask(task, tag) {
2173
- const targetTag = tag || DEFAULT_OPENCLAW_DOCKER_IMAGE;
2174
- try {
2175
- const invocation = resolveDockerInvocation();
2176
- // Fast check: if image already exists locally, skip
2177
- try {
2178
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", targetTag], {
2179
- timeout: 10000,
2180
- stdio: "ignore",
2181
- });
2182
- const pinned = capturePinnedImageTag(invocation, targetTag);
2183
- setOpenclawDockerImage(pinned);
2184
- emitTask(task, { type: "done", message: `Docker 镜像已存在: ${pinned}`, progress: 100 });
2185
- task.status = "done";
2186
- return { ok: true, message: `Docker image ${pinned} already exists`, taskId: task.id };
2187
- }
2188
- catch { /* image not found, proceed */ }
2189
- // ── Step 1: Try docker pull from registry ─────────────────────
2190
- emitTask(task, { type: "progress", message: `正在拉取镜像: ${targetTag} ...`, progress: 10 });
2191
- const pullResult = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "pull", targetTag], { timeout: 600000 });
2192
- if (pullResult.ok) {
2193
- const pinned = capturePinnedImageTag(invocation, targetTag);
2194
- setOpenclawDockerImage(pinned);
2195
- emitTask(task, { type: "done", message: `镜像拉取成功: ${pinned}`, progress: 100 });
2196
- task.status = "done";
2197
- return { ok: true, message: `Docker image ${pinned} pulled`, taskId: task.id };
2198
- }
2199
- // ── Step 2: Fallback to local build ───────────────────────────
2200
- console.log(`[setup] docker pull failed for ${targetTag}, falling back to local build...`);
2201
- emitTask(task, { type: "progress", message: `拉取失败,正在本地构建镜像: ${targetTag} ...`, progress: 20 });
2202
- const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
2203
- const dockerfilePath = join(projectRoot, "Dockerfile.openclaw-slim");
2204
- if (!existsSync(dockerfilePath)) {
2205
- emitTask(task, { type: "error", message: "Dockerfile.openclaw-slim not found, cannot fallback to local build" });
2206
- task.status = "error";
2207
- return { ok: false, message: "Docker pull failed and Dockerfile.openclaw-slim not found", taskId: task.id };
2208
- }
2209
- // Resolve the OpenClaw version from npm so the build-arg busts the Docker
2210
- // layer cache for `RUN npm install openclaw@${ver}`. The Dockerfile's
2211
- // ARG OPENCLAW_VERSION=latest default would otherwise cause the layer to
2212
- // be silently reused across releases.
2213
- const openclawVersion = resolveOpenclawNpmVersion();
2214
- console.log(`[setup] building openclaw image with OPENCLAW_VERSION=${openclawVersion}`);
2215
- const buildResult = await spawnWithTask(task, invocation.cmd, [
2216
- ...invocation.argsPrefix,
2217
- "build",
2218
- "--network=host",
2219
- "--build-arg", `OPENCLAW_VERSION=${openclawVersion}`,
2220
- "-f", dockerfilePath,
2221
- "-t", targetTag,
2222
- projectRoot,
2223
- ], { timeout: 1800000, progressParser: dockerBuildProgressParser });
2224
- if (!buildResult.ok) {
2225
- try {
2226
- execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "prune", "-f"], { timeout: 15000, stdio: "ignore" });
2227
- }
2228
- catch { }
2229
- emitTask(task, { type: "error", message: "Docker 镜像构建失败" });
2230
- task.status = "error";
2231
- return { ok: false, message: "Docker image build failed", error: buildResult.output, taskId: task.id };
2232
- }
2233
- // Local builds don't get labels from the GitHub Action's `labels:` field,
2234
- // so pass the npm version we already know to let capturePinnedImageTag
2235
- // mint the pinned tag without relying on docker inspect.
2236
- const pinned = capturePinnedImageTag(invocation, targetTag, openclawVersion);
2237
- setOpenclawDockerImage(pinned);
2238
- emitTask(task, { type: "done", message: `OpenClaw 镜像就绪 (本地构建): ${pinned}`, progress: 100 });
2239
- task.status = "done";
2240
- return { ok: true, message: `Docker image ${pinned} built locally`, taskId: task.id };
2241
- }
2242
- catch (e) {
2243
- emitTask(task, { type: "error", message: `镜像获取失败: ${e.message}` });
2244
- task.status = "error";
2245
- return { ok: false, message: "Docker image pull/build failed", error: e.message, taskId: task.id };
2246
- }
2247
- }
1878
+ // ── Docker image build dispatch wrappers ───────────────────────────────
1879
+ //
1880
+ // All OpenClaw-specific image build logic now lives in
1881
+ // `OpenClawAdapter.buildRuntimeImage()` (§32.2.4 physical migration). These
1882
+ // exports remain as thin wrappers so `routes/setup.ts` and
1883
+ // `install.ts` callers don't need to learn adapter dispatch.
1884
+ /** Blocking: pull or build the OpenClaw docker image. */
2248
1885
  export async function buildSlimOpenclawImage(tag) {
2249
- const task = createTask("openclaw-docker-pull");
2250
- return pullOrBuildOpenclawImageWithTask(task, tag);
1886
+ const adapter = getAdapter("openclaw");
1887
+ if (typeof adapter.buildRuntimeImage !== "function") {
1888
+ return { ok: false, message: "OpenClawAdapter.buildRuntimeImage is not implemented" };
1889
+ }
1890
+ return adapter.buildRuntimeImage({ tag });
2251
1891
  }
1892
+ /** Non-blocking: returns immediately with a task id for SSE polling. */
2252
1893
  export function startBuildSlimOpenclawImage(tag) {
2253
- const task = createTask("openclaw-docker-pull");
2254
- void pullOrBuildOpenclawImageWithTask(task, tag).catch((err) => {
2255
- emitTask(task, { type: "error", message: `镜像获取失败: ${err?.message || err}` });
2256
- task.status = "error";
2257
- });
2258
- return { ok: true, message: "Docker image pull started", taskId: task.id };
1894
+ const adapter = getAdapter("openclaw");
1895
+ if (typeof adapter.startBuildRuntimeImage !== "function") {
1896
+ return { ok: false, message: "OpenClawAdapter.startBuildRuntimeImage is not implemented" };
1897
+ }
1898
+ return adapter.startBuildRuntimeImage({ tag });
2259
1899
  }
2260
1900
  /** @deprecated Use buildSlimOpenclawImage instead */
2261
1901
  export async function buildCustomOpenclawImage(tag) {
@@ -2306,7 +1946,10 @@ export async function runFullSetup(options = {}) {
2306
1946
  }
2307
1947
  }
2308
1948
  }
2309
- // Prepare Docker image: pull official image or build slim base (legacy).
1949
+ // Prepare each registered agent's runtime artefacts (docker image, shim,
1950
+ // etc.). This loops over every adapter so onboarding a new agent only
1951
+ // requires implementing `buildRuntimeImage` or `installRuntime` in its
1952
+ // adapter — runFullSetup picks it up automatically.
2310
1953
  if (defaults.buildDockerImage) {
2311
1954
  // Restart Nomad so it re-detects Docker driver after Docker was installed
2312
1955
  try {
@@ -2318,12 +1961,50 @@ export async function runFullSetup(options = {}) {
2318
1961
  }
2319
1962
  }
2320
1963
  catch { }
2321
- steps.push({ step: "docker-image", status: "running", message: "Building OpenClaw Docker image..." });
2322
- const imgResult = await buildSlimOpenclawImage();
2323
- steps[steps.length - 1].status = imgResult.ok ? "done" : "error";
2324
- steps[steps.length - 1].message = imgResult.message;
2325
- if (!imgResult.ok)
2326
- allOk = false;
1964
+ for (const adapter of listRegisteredAdapters()) {
1965
+ const stepName = `runtime-${adapter.agentType}`;
1966
+ // Prefer buildRuntimeImage (pure docker image prep) over
1967
+ // installRuntime (legacy host-mode npm install). Adapters that only
1968
+ // implement one will use whichever is present; adapters with neither
1969
+ // are skipped silently.
1970
+ const runner = adapter.buildRuntimeImage
1971
+ ? adapter.buildRuntimeImage.bind(adapter)
1972
+ : adapter.installRuntime
1973
+ ? adapter.installRuntime.bind(adapter)
1974
+ : null;
1975
+ if (!runner)
1976
+ continue;
1977
+ const isRequired = !!adapter.required;
1978
+ steps.push({
1979
+ step: stepName,
1980
+ status: "running",
1981
+ message: `Preparing ${adapter.agentType} runtime…`,
1982
+ });
1983
+ try {
1984
+ const imgResult = (await runner());
1985
+ const okStep = !!imgResult.ok;
1986
+ steps[steps.length - 1].status = okStep ? "done" : "error";
1987
+ steps[steps.length - 1].message = okStep
1988
+ ? imgResult.message
1989
+ : isRequired
1990
+ ? imgResult.message
1991
+ : `${imgResult.message} (optional — panel will continue)`;
1992
+ // Required adapter failures abort the overall setup; optional ones
1993
+ // are warnings so that upgrade paths don't break when a third-party
1994
+ // agent's registry is unreachable.
1995
+ if (!okStep && isRequired)
1996
+ allOk = false;
1997
+ }
1998
+ catch (e) {
1999
+ const msg = e?.message || "runtime prepare failed";
2000
+ steps[steps.length - 1].status = "error";
2001
+ steps[steps.length - 1].message = isRequired
2002
+ ? msg
2003
+ : `${msg} (optional — panel will continue)`;
2004
+ if (isRequired)
2005
+ allOk = false;
2006
+ }
2007
+ }
2327
2008
  }
2328
2009
  if (isPortListening(4646)) {
2329
2010
  await finalizeNomadStartup();