jishushell 0.4.10 → 0.4.24-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/Dockerfile.hermes-slim +193 -0
  2. package/INSTALL-NOTICE +10 -12
  3. package/apps/hermes-container.yaml +35 -0
  4. package/apps/ollama-binary.yaml +164 -0
  5. package/apps/ollama-cpu-container.yaml +37 -0
  6. package/apps/ollama-with-hollama-binary.yaml +159 -0
  7. package/apps/openclaw-binary.yaml +69 -0
  8. package/apps/openclaw-container.yaml +37 -0
  9. package/apps/openclaw-with-ollama-container.yaml +42 -0
  10. package/apps/openclaw-with-searxng-container.yaml +136 -0
  11. package/apps/openwebui-container.yaml +53 -0
  12. package/apps/playwright-container.yaml +120 -0
  13. package/apps/searxng-container.yaml +115 -0
  14. package/dist/auth.d.ts +1 -0
  15. package/dist/auth.js +15 -14
  16. package/dist/auth.js.map +1 -1
  17. package/dist/cli/app.d.ts +4 -0
  18. package/dist/cli/app.js +874 -0
  19. package/dist/cli/app.js.map +1 -0
  20. package/dist/cli/backup.d.ts +3 -0
  21. package/dist/cli/backup.js +434 -0
  22. package/dist/cli/backup.js.map +1 -0
  23. package/dist/{doctor.d.ts → cli/doctor.d.ts} +7 -1
  24. package/dist/{doctor.js → cli/doctor.js} +377 -22
  25. package/dist/cli/doctor.js.map +1 -0
  26. package/dist/cli/helpers.d.ts +4 -0
  27. package/dist/cli/helpers.js +32 -0
  28. package/dist/cli/helpers.js.map +1 -0
  29. package/dist/cli/job.d.ts +4 -0
  30. package/dist/cli/job.js +198 -0
  31. package/dist/cli/job.js.map +1 -0
  32. package/dist/cli/llm.d.ts +25 -0
  33. package/dist/cli/llm.js +599 -0
  34. package/dist/cli/llm.js.map +1 -0
  35. package/dist/cli/managed-list.d.ts +30 -0
  36. package/dist/cli/managed-list.js +129 -0
  37. package/dist/cli/managed-list.js.map +1 -0
  38. package/dist/cli/panel.d.ts +26 -0
  39. package/dist/cli/panel.js +804 -0
  40. package/dist/cli/panel.js.map +1 -0
  41. package/dist/cli/version.d.ts +1 -0
  42. package/dist/cli/version.js +12 -0
  43. package/dist/cli/version.js.map +1 -0
  44. package/dist/cli.js +48 -776
  45. package/dist/cli.js.map +1 -1
  46. package/dist/config.d.ts +69 -0
  47. package/dist/config.js +268 -7
  48. package/dist/config.js.map +1 -1
  49. package/dist/control.d.ts +17 -41
  50. package/dist/control.js +61 -1323
  51. package/dist/control.js.map +1 -1
  52. package/dist/install.d.ts +16 -0
  53. package/dist/install.js +75 -26
  54. package/dist/install.js.map +1 -1
  55. package/dist/routes/agent-apps.d.ts +15 -0
  56. package/dist/routes/agent-apps.js +78 -0
  57. package/dist/routes/agent-apps.js.map +1 -0
  58. package/dist/routes/apps.d.ts +3 -0
  59. package/dist/routes/apps.js +278 -0
  60. package/dist/routes/apps.js.map +1 -0
  61. package/dist/routes/backup.js +3 -3
  62. package/dist/routes/backup.js.map +1 -1
  63. package/dist/routes/instances.d.ts +6 -0
  64. package/dist/routes/instances.js +863 -874
  65. package/dist/routes/instances.js.map +1 -1
  66. package/dist/routes/llm.d.ts +15 -0
  67. package/dist/routes/llm.js +247 -0
  68. package/dist/routes/llm.js.map +1 -0
  69. package/dist/routes/runtime.d.ts +15 -0
  70. package/dist/routes/runtime.js +69 -0
  71. package/dist/routes/runtime.js.map +1 -0
  72. package/dist/routes/setup.js +131 -9
  73. package/dist/routes/setup.js.map +1 -1
  74. package/dist/routes/system.js +56 -9
  75. package/dist/routes/system.js.map +1 -1
  76. package/dist/server.js +107 -7
  77. package/dist/server.js.map +1 -1
  78. package/dist/services/agent-apps/catalog.d.ts +30 -0
  79. package/dist/services/agent-apps/catalog.js +60 -0
  80. package/dist/services/agent-apps/catalog.js.map +1 -0
  81. package/dist/services/agent-apps/index.d.ts +36 -0
  82. package/dist/services/agent-apps/index.js +171 -0
  83. package/dist/services/agent-apps/index.js.map +1 -0
  84. package/dist/services/agent-apps/installers/adapter-probes.d.ts +49 -0
  85. package/dist/services/agent-apps/installers/adapter-probes.js +223 -0
  86. package/dist/services/agent-apps/installers/adapter-probes.js.map +1 -0
  87. package/dist/services/agent-apps/installers/adapter.d.ts +30 -0
  88. package/dist/services/agent-apps/installers/adapter.js +171 -0
  89. package/dist/services/agent-apps/installers/adapter.js.map +1 -0
  90. package/dist/services/agent-apps/installers/registry-probe.d.ts +38 -0
  91. package/dist/services/agent-apps/installers/registry-probe.js +183 -0
  92. package/dist/services/agent-apps/installers/registry-probe.js.map +1 -0
  93. package/dist/services/agent-apps/installers/shell-script.d.ts +47 -0
  94. package/dist/services/agent-apps/installers/shell-script.js +471 -0
  95. package/dist/services/agent-apps/installers/shell-script.js.map +1 -0
  96. package/dist/services/agent-apps/types.d.ts +125 -0
  97. package/dist/services/agent-apps/types.js +17 -0
  98. package/dist/services/agent-apps/types.js.map +1 -0
  99. package/dist/services/app/app-compiler.d.ts +15 -0
  100. package/dist/services/app/app-compiler.js +172 -0
  101. package/dist/services/app/app-compiler.js.map +1 -0
  102. package/dist/services/app/app-manager.d.ts +142 -0
  103. package/dist/services/app/app-manager.js +2148 -0
  104. package/dist/services/app/app-manager.js.map +1 -0
  105. package/dist/services/app/custom-manager.d.ts +27 -0
  106. package/dist/services/app/custom-manager.js +285 -0
  107. package/dist/services/app/custom-manager.js.map +1 -0
  108. package/dist/services/app/hermes-agent-manager.d.ts +20 -0
  109. package/dist/services/app/hermes-agent-manager.js +289 -0
  110. package/dist/services/app/hermes-agent-manager.js.map +1 -0
  111. package/dist/services/app/id-normalizer.d.ts +27 -0
  112. package/dist/services/app/id-normalizer.js +77 -0
  113. package/dist/services/app/id-normalizer.js.map +1 -0
  114. package/dist/services/app/ollama-manager.d.ts +18 -0
  115. package/dist/services/app/ollama-manager.js +207 -0
  116. package/dist/services/app/ollama-manager.js.map +1 -0
  117. package/dist/services/app/openclaw-manager.d.ts +63 -0
  118. package/dist/services/app/openclaw-manager.js +1178 -0
  119. package/dist/services/app/openclaw-manager.js.map +1 -0
  120. package/dist/services/app/paths.d.ts +47 -0
  121. package/dist/services/app/paths.js +68 -0
  122. package/dist/services/app/paths.js.map +1 -0
  123. package/dist/services/app/registry.d.ts +17 -0
  124. package/dist/services/app/registry.js +31 -0
  125. package/dist/services/app/registry.js.map +1 -0
  126. package/dist/services/app/remote-spec.d.ts +14 -0
  127. package/dist/services/app/remote-spec.js +58 -0
  128. package/dist/services/app/remote-spec.js.map +1 -0
  129. package/dist/services/app/terminal-session-manager.d.ts +27 -0
  130. package/dist/services/app/terminal-session-manager.js +157 -0
  131. package/dist/services/app/terminal-session-manager.js.map +1 -0
  132. package/dist/services/app/types.d.ts +72 -0
  133. package/dist/services/app/types.js +16 -0
  134. package/dist/services/app/types.js.map +1 -0
  135. package/dist/services/backup-manager.js +60 -22
  136. package/dist/services/backup-manager.js.map +1 -1
  137. package/dist/services/instance-manager.d.ts +125 -34
  138. package/dist/services/instance-manager.js +679 -1043
  139. package/dist/services/instance-manager.js.map +1 -1
  140. package/dist/services/llm-proxy/adapters.js +5 -1
  141. package/dist/services/llm-proxy/adapters.js.map +1 -1
  142. package/dist/services/llm-proxy/circuit-breaker.js +10 -2
  143. package/dist/services/llm-proxy/circuit-breaker.js.map +1 -1
  144. package/dist/services/llm-proxy/index.d.ts +43 -0
  145. package/dist/services/llm-proxy/index.js +120 -5
  146. package/dist/services/llm-proxy/index.js.map +1 -1
  147. package/dist/services/llm-proxy/ssrf.js +1 -1
  148. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  149. package/dist/services/nomad-manager.d.ts +260 -3
  150. package/dist/services/nomad-manager.js +2921 -341
  151. package/dist/services/nomad-manager.js.map +1 -1
  152. package/dist/services/panel-manager.d.ts +50 -0
  153. package/dist/services/panel-manager.js +443 -0
  154. package/dist/services/panel-manager.js.map +1 -0
  155. package/dist/services/plugin-installer.js +28 -2
  156. package/dist/services/plugin-installer.js.map +1 -1
  157. package/dist/services/process-manager.js +42 -7
  158. package/dist/services/process-manager.js.map +1 -1
  159. package/dist/services/runtime/adapters/custom.d.ts +20 -0
  160. package/dist/services/runtime/adapters/custom.js +90 -0
  161. package/dist/services/runtime/adapters/custom.js.map +1 -0
  162. package/dist/services/runtime/adapters/hermes.d.ts +174 -0
  163. package/dist/services/runtime/adapters/hermes.js +1316 -0
  164. package/dist/services/runtime/adapters/hermes.js.map +1 -0
  165. package/dist/services/runtime/adapters/openclaw-routes.d.ts +17 -0
  166. package/dist/services/runtime/adapters/openclaw-routes.js +946 -0
  167. package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -0
  168. package/dist/services/runtime/adapters/openclaw.d.ts +188 -0
  169. package/dist/services/runtime/adapters/openclaw.js +2195 -0
  170. package/dist/services/runtime/adapters/openclaw.js.map +1 -0
  171. package/dist/services/runtime/errors.d.ts +28 -0
  172. package/dist/services/runtime/errors.js +31 -0
  173. package/dist/services/runtime/errors.js.map +1 -0
  174. package/dist/services/runtime/index.d.ts +34 -0
  175. package/dist/services/runtime/index.js +51 -0
  176. package/dist/services/runtime/index.js.map +1 -0
  177. package/dist/services/runtime/instance.d.ts +24 -0
  178. package/dist/services/runtime/instance.js +143 -0
  179. package/dist/services/runtime/instance.js.map +1 -0
  180. package/dist/services/runtime/migrations.d.ts +15 -0
  181. package/dist/services/runtime/migrations.js +25 -0
  182. package/dist/services/runtime/migrations.js.map +1 -0
  183. package/dist/services/runtime/registry.d.ts +13 -0
  184. package/dist/services/runtime/registry.js +32 -0
  185. package/dist/services/runtime/registry.js.map +1 -0
  186. package/dist/services/runtime/types.d.ts +545 -0
  187. package/dist/services/runtime/types.js +14 -0
  188. package/dist/services/runtime/types.js.map +1 -0
  189. package/dist/services/setup-manager.d.ts +70 -29
  190. package/dist/services/setup-manager.js +591 -625
  191. package/dist/services/setup-manager.js.map +1 -1
  192. package/dist/services/task-registry.d.ts +44 -0
  193. package/dist/services/task-registry.js +74 -0
  194. package/dist/services/task-registry.js.map +1 -0
  195. package/dist/services/telemetry/heartbeat.d.ts +6 -6
  196. package/dist/services/telemetry/heartbeat.js +29 -30
  197. package/dist/services/telemetry/heartbeat.js.map +1 -1
  198. package/dist/services/update-manager.d.ts +47 -0
  199. package/dist/services/update-manager.js +305 -0
  200. package/dist/services/update-manager.js.map +1 -0
  201. package/dist/types.d.ts +224 -0
  202. package/dist/utils/docker-host.d.ts +15 -0
  203. package/dist/utils/docker-host.js +64 -0
  204. package/dist/utils/docker-host.js.map +1 -0
  205. package/install/jishu-install.sh +303 -38
  206. package/install/post-install.sh +64 -5
  207. package/package.json +19 -5
  208. package/public/assets/Dashboard-rh9qpYRR.js +1 -0
  209. package/public/assets/HermesChatPanel-D6JI6lLY.js +1 -0
  210. package/public/assets/HermesConfigForm-DcbSemaj.js +4 -0
  211. package/public/assets/InitPassword-CFTKsED4.js +1 -0
  212. package/public/assets/InstanceDetail-BhNIKA6Z.js +91 -0
  213. package/public/assets/{Login-CUoEZOWR.js → Login-KB9qrtM0.js} +1 -1
  214. package/public/assets/NewInstance-CxkO8Hlq.js +1 -0
  215. package/public/assets/Settings-BVWJvOkU.js +1 -0
  216. package/public/assets/Setup-X-lzuaUT.js +1 -0
  217. package/public/assets/WeixinLoginPanel-gca0QTic.js +9 -0
  218. package/public/assets/index-C8B0cFJM.js +19 -0
  219. package/public/assets/index-CPhVFEsx.css +1 -0
  220. package/public/assets/input-paste-CrNVAyOy.js +1 -0
  221. package/public/assets/{providers-lBSOjUWy.js → providers-V-vwrExZ.js} +1 -1
  222. package/public/assets/registry-fVUSujib.js +2 -0
  223. package/public/assets/{usePolling-CK0DfI4h.js → usePolling-Do5Erqm_.js} +1 -1
  224. package/public/assets/vendor-i18n-ucpM0OR0.js +9 -0
  225. package/public/assets/{vendor-react-B1-3Yrt-.js → vendor-react-Bk1hRGiY.js} +1 -1
  226. package/public/favicon.png +0 -0
  227. package/public/index.html +9 -4
  228. package/public/logos/hermes.png +0 -0
  229. package/public/logos/ollama.png +0 -0
  230. package/public/logos/openclaw.svg +60 -0
  231. package/scripts/build-hermes-image.sh +21 -0
  232. package/scripts/build-local.sh +54 -0
  233. package/scripts/check-adapter-isolation.ts +293 -0
  234. package/scripts/fixtures/instances/hermes-sample/instance.json +37 -0
  235. package/scripts/fixtures/instances/legacy-openclaw-sample/instance.json +7 -0
  236. package/scripts/smoke/hermes-bootstrap.sh +195 -0
  237. package/templates/hermes-entrypoint.sh +154 -0
  238. package/dist/doctor.js.map +0 -1
  239. package/install/jishu-install-china.sh +0 -3092
  240. package/public/assets/Dashboard-DhsrzJ4F.js +0 -1
  241. package/public/assets/InitPassword-BjubiVdd.js +0 -1
  242. package/public/assets/InstanceDetail-DMcywsof.js +0 -17
  243. package/public/assets/NewInstance-Bk0G4EiJ.js +0 -1
  244. package/public/assets/Settings-D5tHL_h5.js +0 -1
  245. package/public/assets/Setup-4t6E3Rut.js +0 -1
  246. package/public/assets/index-BJ47MWpF.css +0 -1
  247. package/public/assets/index-DbX85irc.js +0 -16
  248. package/public/assets/vendor-i18n-CfW0RvgE.js +0 -9
@@ -0,0 +1,874 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { basename, extname } from "path";
3
+ import { stringify } from "yaml";
4
+ import { parseFlag } from "./helpers.js";
5
+ import { loadManagedListEntries, printManagedList } from "./managed-list.js";
6
+ import { ensureNomadToken, execInInstance, getInstanceLogs, getInstanceStatus, readInstanceMeta, restartNomadJobInstance, startNomadJobInstance, stopNomadJobInstance, } from "../services/nomad-manager.js";
7
+ import { getTask, subscribeTask } from "../services/task-registry.js";
8
+ // ── ANSI colour helpers ───────────────────────────────────────────────────
9
+ const isTTY = process.stdout.isTTY ?? false;
10
+ const c = {
11
+ bold: (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s,
12
+ green: (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s,
13
+ yellow: (s) => isTTY ? `\x1b[33m${s}\x1b[0m` : s,
14
+ red: (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s,
15
+ cyan: (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s,
16
+ dim: (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s,
17
+ };
18
+ function log(msg) { process.stdout.write(msg + "\n"); }
19
+ async function waitForLocalTask(taskId) {
20
+ const task = getTask(taskId);
21
+ if (!task)
22
+ return { status: "missing" };
23
+ const latestTerminalEvent = [...task.events]
24
+ .reverse()
25
+ .find((event) => event.type === "done" || event.type === "error");
26
+ if (task.status !== "running") {
27
+ return {
28
+ status: task.status === "error" ? "error" : "done",
29
+ message: latestTerminalEvent?.message,
30
+ };
31
+ }
32
+ return new Promise((resolve) => {
33
+ let settled = false;
34
+ let renderedProgress = false;
35
+ let unsubscribe = null;
36
+ const finish = (status, message) => {
37
+ if (settled)
38
+ return;
39
+ settled = true;
40
+ if (renderedProgress)
41
+ process.stdout.write("\n");
42
+ unsubscribe?.();
43
+ resolve({ status, message });
44
+ };
45
+ unsubscribe = subscribeTask(taskId, (event) => {
46
+ if (settled)
47
+ return;
48
+ if (event.type === "log") {
49
+ if (event.message.trim())
50
+ log(c.dim(` ${event.message}`));
51
+ return;
52
+ }
53
+ if (event.type === "progress") {
54
+ renderedProgress = true;
55
+ const progress = typeof event.progress === "number"
56
+ ? ` ${c.dim(`[${String(event.progress).padStart(3)}%]`)}`
57
+ : "";
58
+ process.stdout.write(`\r ${event.message}${progress} `);
59
+ return;
60
+ }
61
+ if (event.type === "done") {
62
+ finish("done", event.message);
63
+ return;
64
+ }
65
+ finish("error", event.message);
66
+ });
67
+ if (!unsubscribe)
68
+ finish("missing");
69
+ });
70
+ }
71
+ async function runManagedAppTask(starter, successMessage) {
72
+ const started = starter();
73
+ if (!started.ok) {
74
+ log(c.red(` ✗ ${started.error || "Task failed"}`));
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ if (started.taskId) {
79
+ const result = await waitForLocalTask(started.taskId);
80
+ if (result.status === "error") {
81
+ log(c.red(` ✗ ${result.message || "Task failed"}`));
82
+ process.exitCode = 1;
83
+ return;
84
+ }
85
+ }
86
+ log(c.green(` ✓ ${successMessage}`));
87
+ }
88
+ function readPassword(prompt) {
89
+ return new Promise((resolve, reject) => {
90
+ process.stdout.write(prompt);
91
+ if (process.stdin.isTTY) {
92
+ process.stdin.setRawMode(true);
93
+ process.stdin.resume();
94
+ let input = "";
95
+ const onData = (buf) => {
96
+ const char = buf.toString("utf-8");
97
+ if (char === "\r" || char === "\n") {
98
+ process.stdin.setRawMode(false);
99
+ process.stdin.pause();
100
+ process.stdin.off("data", onData);
101
+ process.stdout.write("\n");
102
+ resolve(input);
103
+ }
104
+ else if (char === "\u0003") {
105
+ process.stdin.setRawMode(false);
106
+ process.stdout.write("\n");
107
+ reject(new Error("Interrupted"));
108
+ }
109
+ else if (char === "\u007f" || char === "\b") {
110
+ if (input.length > 0)
111
+ input = input.slice(0, -1);
112
+ }
113
+ else if (char >= " ") {
114
+ input += char;
115
+ }
116
+ };
117
+ process.stdin.on("data", onData);
118
+ return;
119
+ }
120
+ let input = "";
121
+ process.stdin.resume();
122
+ process.stdin.setEncoding("utf-8");
123
+ const onData = (chunk) => {
124
+ const nl = chunk.indexOf("\n");
125
+ if (nl >= 0) {
126
+ input += chunk.slice(0, nl);
127
+ process.stdin.off("data", onData);
128
+ process.stdin.pause();
129
+ resolve(input.replace(/\r$/, "").trim());
130
+ }
131
+ else {
132
+ input += chunk;
133
+ }
134
+ };
135
+ process.stdin.on("data", onData);
136
+ });
137
+ }
138
+ function isSudoPasswordError(message) {
139
+ return /sudo 密码|请输入 sudo 密码|password required/i.test(message);
140
+ }
141
+ function getCliSudoState(args) {
142
+ const flagValue = parseFlag(args, "--sudo-password", "");
143
+ const flagPassword = typeof flagValue === "string" ? flagValue.trim() : "";
144
+ const envPassword = process.env.JISHUSHELL_SUDO_PASSWORD?.trim() ?? "";
145
+ return {
146
+ password: flagPassword || envPassword,
147
+ prompted: false,
148
+ };
149
+ }
150
+ async function promptForCliSudoPassword(state) {
151
+ if (state.prompted || !process.stdin.isTTY)
152
+ return false;
153
+ state.prompted = true;
154
+ const password = (await readPassword(" sudo 密码: ")).trim();
155
+ if (!password)
156
+ return false;
157
+ state.password = password;
158
+ return true;
159
+ }
160
+ function formatSudoHint(message, state) {
161
+ if (isSudoPasswordError(message) && !state.password && !process.stdin.isTTY) {
162
+ return `${message};可通过 --sudo-password 或环境变量 JISHUSHELL_SUDO_PASSWORD 提供 sudo 密码。`;
163
+ }
164
+ return message;
165
+ }
166
+ async function runManagedUninstallTask(starter, successMessage, sudoState) {
167
+ while (true) {
168
+ const started = sudoState.password
169
+ ? starter({ sudoPassword: sudoState.password })
170
+ : starter();
171
+ if (!started.ok) {
172
+ const message = started.error || "Task failed";
173
+ if (isSudoPasswordError(message) && await promptForCliSudoPassword(sudoState)) {
174
+ continue;
175
+ }
176
+ log(c.red(` ✗ ${formatSudoHint(message, sudoState)}`));
177
+ process.exitCode = 1;
178
+ return;
179
+ }
180
+ if (started.taskId) {
181
+ const result = await waitForLocalTask(started.taskId);
182
+ if (result.status === "error") {
183
+ const message = result.message || "Task failed";
184
+ if (isSudoPasswordError(message) && await promptForCliSudoPassword(sudoState)) {
185
+ continue;
186
+ }
187
+ log(c.red(` ✗ ${formatSudoHint(message, sudoState)}`));
188
+ process.exitCode = 1;
189
+ return;
190
+ }
191
+ }
192
+ log(c.green(` ✓ ${successMessage}`));
193
+ return;
194
+ }
195
+ }
196
+ function rewriteInstalledAppYaml(yamlText, sourceId, targetId) {
197
+ const homeDir = process.env.HOME ?? "";
198
+ return yamlText
199
+ .split(`~/.jishushell/apps/${sourceId}`).join(`~/.jishushell/apps/${targetId}`)
200
+ .split(`$HOME/.jishushell/apps/${sourceId}`).join(`$HOME/.jishushell/apps/${targetId}`)
201
+ .split(`${homeDir}/.jishushell/apps/${sourceId}`).join(`${homeDir}/.jishushell/apps/${targetId}`);
202
+ }
203
+ function healthTargetsForTask(task) {
204
+ if (!task?.health)
205
+ return [];
206
+ const targets = [];
207
+ if (task.health.http) {
208
+ targets.push({
209
+ method: "http",
210
+ port: task.health.http.port,
211
+ path: task.health.http.path,
212
+ });
213
+ }
214
+ return targets;
215
+ }
216
+ function normalizeHealthStatus(status, methods) {
217
+ if (methods.length === 0)
218
+ return "none";
219
+ const normalized = String(status ?? "unknown").toLowerCase();
220
+ if (["success", "passing", "healthy"].includes(normalized))
221
+ return "healthy";
222
+ if (["failure", "critical", "warning", "unhealthy"].includes(normalized))
223
+ return "unhealthy";
224
+ if (!normalized || normalized === "pending")
225
+ return "unknown";
226
+ return normalized;
227
+ }
228
+ function fallbackTaskState(appStatus) {
229
+ if (appStatus === "stopped")
230
+ return "stopped";
231
+ if (appStatus === "pending")
232
+ return "pending";
233
+ return "unknown";
234
+ }
235
+ function resolveProvidePort(spec, provide) {
236
+ if (typeof provide?.port === "number")
237
+ return provide.port;
238
+ const serviceTask = spec.tasks.find((task) => (task.role ?? "service") === "service");
239
+ return serviceTask?.ports?.[0]?.port;
240
+ }
241
+ function provideAddress(port, path) {
242
+ if (typeof port !== "number")
243
+ return undefined;
244
+ const normalizedPath = path ? (path.startsWith("/") ? path : `/${path}`) : "";
245
+ return `127.0.0.1:${port}${normalizedPath}`;
246
+ }
247
+ function describeHealthTargets(targets) {
248
+ return targets
249
+ .map((target) => `${target.method}${typeof target.port === "number" ? `:${target.port}` : ""}`)
250
+ .join(",");
251
+ }
252
+ function enrichStatusForCli(status, spec) {
253
+ const taskSpecs = new Map((spec?.tasks ?? []).map((task) => [task.name, task]));
254
+ const taskNames = new Set([
255
+ ...taskSpecs.keys(),
256
+ ...Object.keys(status.tasks ?? {}),
257
+ ]);
258
+ const tasks = {};
259
+ for (const taskName of taskNames) {
260
+ const taskSpec = taskSpecs.get(taskName);
261
+ const runtimeTask = status.tasks?.[taskName];
262
+ const targets = healthTargetsForTask(taskSpec);
263
+ const methods = targets.map((target) => target.method);
264
+ const checks = (runtimeTask?.health_checks ?? []).map((check, index) => {
265
+ const target = targets[index] ?? targets[0] ?? { method: "unknown" };
266
+ return {
267
+ name: check.name,
268
+ method: target.method,
269
+ ...(typeof target.port === "number" ? { port: target.port } : {}),
270
+ ...(target.path ? { path: target.path } : {}),
271
+ status: normalizeHealthStatus(check.status, methods),
272
+ };
273
+ });
274
+ tasks[taskName] = {
275
+ state: runtimeTask?.state ?? fallbackTaskState(status.status),
276
+ restarts: runtimeTask?.restarts ?? 0,
277
+ ...(runtimeTask?.started_at ? { started_at: runtimeTask.started_at } : {}),
278
+ health: {
279
+ methods,
280
+ status: normalizeHealthStatus(runtimeTask?.health_status, methods),
281
+ targets,
282
+ checks,
283
+ },
284
+ };
285
+ }
286
+ const provides = (spec?.provides ?? []).map((provide) => {
287
+ const port = resolveProvidePort(spec, provide);
288
+ return {
289
+ capability: provide.capability,
290
+ ...(typeof port === "number" ? { port } : {}),
291
+ ...(provide.path ? { path: provide.path } : {}),
292
+ ...(provide.description ? { description: provide.description } : {}),
293
+ ...(provideAddress(port, provide.path) ? { address: provideAddress(port, provide.path) } : {}),
294
+ };
295
+ });
296
+ return { tasks, provides };
297
+ }
298
+ function colorTaskState(state) {
299
+ if (state === "running")
300
+ return c.green(state);
301
+ if (state === "pending")
302
+ return c.yellow(state);
303
+ if (state === "dead" || state === "failed")
304
+ return c.red(state);
305
+ return c.dim(state);
306
+ }
307
+ function colorHealthState(state) {
308
+ if (state === "healthy")
309
+ return c.green(state);
310
+ if (state === "unhealthy")
311
+ return c.red(state);
312
+ if (state === "unknown")
313
+ return c.yellow(state);
314
+ return c.dim(state);
315
+ }
316
+ function parseRemoteYamlUrl(value) {
317
+ try {
318
+ const url = new URL(value);
319
+ if (url.protocol === "https:")
320
+ return url;
321
+ if (url.protocol === "http:") {
322
+ throw new Error("Only HTTPS app spec URLs are supported");
323
+ }
324
+ return null;
325
+ }
326
+ catch (e) {
327
+ if (e instanceof TypeError)
328
+ return null;
329
+ throw e;
330
+ }
331
+ }
332
+ const BUILTIN_INSTALL_ALIASES = {
333
+ ollama: "ollama-binary.yaml",
334
+ };
335
+ async function loadBuiltinInstallYaml(source) {
336
+ const normalizedSource = source.trim();
337
+ if (!normalizedSource)
338
+ return null;
339
+ const { listBuiltinAppSpecs } = await import("../services/app/app-manager.js");
340
+ const templates = listBuiltinAppSpecs();
341
+ const normalizedWithoutExt = normalizedSource.replace(/\.ya?ml$/i, "");
342
+ const aliasTarget = BUILTIN_INSTALL_ALIASES[normalizedWithoutExt] ?? BUILTIN_INSTALL_ALIASES[normalizedSource];
343
+ const template = templates.find((entry) => {
344
+ if (aliasTarget && entry.fileName === aliasTarget)
345
+ return true;
346
+ if (entry.fileName === normalizedSource)
347
+ return true;
348
+ if (basename(entry.fileName, extname(entry.fileName)) === normalizedWithoutExt)
349
+ return true;
350
+ if (entry.id === normalizedWithoutExt || entry.id === normalizedSource)
351
+ return true;
352
+ return false;
353
+ });
354
+ return template?.yaml ?? null;
355
+ }
356
+ async function loadInstallYaml(source) {
357
+ const remoteUrl = parseRemoteYamlUrl(source);
358
+ if (remoteUrl) {
359
+ let resp;
360
+ try {
361
+ resp = await fetch(remoteUrl, { signal: AbortSignal.timeout(30_000) });
362
+ }
363
+ catch (e) {
364
+ throw new Error(`Failed to download app YAML: ${e.message}`);
365
+ }
366
+ if (!resp.ok) {
367
+ throw new Error(`Failed to download app YAML: HTTP ${resp.status}`);
368
+ }
369
+ const body = await resp.text();
370
+ if (!body.trim()) {
371
+ throw new Error(`Downloaded app YAML is empty: ${remoteUrl}`);
372
+ }
373
+ return body;
374
+ }
375
+ const builtinYaml = await loadBuiltinInstallYaml(source);
376
+ if (builtinYaml) {
377
+ return builtinYaml;
378
+ }
379
+ if (!existsSync(source)) {
380
+ throw new Error(`File not found: ${source}`);
381
+ }
382
+ return readFileSync(source, "utf-8");
383
+ }
384
+ // ── Command implementations ───────────────────────────────────────────────
385
+ async function cmdList(args) {
386
+ ensureNomadToken();
387
+ const items = await loadManagedListEntries();
388
+ if (args.includes("--json")) {
389
+ console.log(JSON.stringify(items, null, 2));
390
+ return;
391
+ }
392
+ printManagedList("Managed Apps / Instances", items, c, log);
393
+ }
394
+ async function cmdShow(id) {
395
+ const { getApp, getInstance } = await import("../services/app/app-manager.js");
396
+ const app = getApp(id);
397
+ if (app) {
398
+ console.log(JSON.stringify(app, null, 2));
399
+ return;
400
+ }
401
+ const instance = getInstance(id);
402
+ if (!instance) {
403
+ log(c.red(` ✗ App/instance "${id}" not found.`));
404
+ process.exitCode = 1;
405
+ return;
406
+ }
407
+ console.log(JSON.stringify(instance, null, 2));
408
+ }
409
+ async function cmdInstall(args) {
410
+ const installSource = args[0];
411
+ let requestedAppId;
412
+ for (let index = 1; index < args.length; index += 1) {
413
+ const value = args[index];
414
+ if (value === "--sudo-password") {
415
+ index += 1;
416
+ continue;
417
+ }
418
+ if (value.startsWith("--"))
419
+ continue;
420
+ requestedAppId = value;
421
+ break;
422
+ }
423
+ const yamlText = await loadInstallYaml(installSource);
424
+ const { installApp } = await import("../services/app/app-manager.js");
425
+ const sudoState = getCliSudoState(args);
426
+ while (true) {
427
+ try {
428
+ const installOptions = sudoState.password ? { exec: { sudoPassword: sudoState.password } } : undefined;
429
+ const result = installOptions
430
+ ? await installApp(yamlText, requestedAppId, installOptions)
431
+ : await installApp(yamlText, requestedAppId);
432
+ const installModeLabel = result.manifest.install_mode === "instance-dir" ? "instance" : "app";
433
+ log(c.green(` ✓ Installed: ${result.manifest.id} (${result.spec.name || ""} v${result.spec.version || "?"}, ${installModeLabel})`));
434
+ return;
435
+ }
436
+ catch (err) {
437
+ const message = err?.message || "Install failed";
438
+ if (isSudoPasswordError(message) && await promptForCliSudoPassword(sudoState)) {
439
+ continue;
440
+ }
441
+ throw err;
442
+ }
443
+ }
444
+ }
445
+ async function cmdProvides(args) {
446
+ const { listProvidedCapabilities } = await import("../services/app/app-manager.js");
447
+ const provides = listProvidedCapabilities();
448
+ if (args.includes("--json")) {
449
+ console.log(JSON.stringify(provides, null, 2));
450
+ return;
451
+ }
452
+ log("");
453
+ log(c.bold(" App Provides"));
454
+ log(c.dim(" ─────────────────────────────────────────────────────"));
455
+ if (provides.length === 0) {
456
+ log(c.dim(" (no app capabilities declared)"));
457
+ log("");
458
+ return;
459
+ }
460
+ const grouped = new Map();
461
+ for (const provide of provides) {
462
+ const items = grouped.get(provide.appId) ?? [];
463
+ items.push(provide);
464
+ grouped.set(provide.appId, items);
465
+ }
466
+ for (const [appId, items] of grouped.entries()) {
467
+ log(` ${c.cyan(appId)}`);
468
+ for (const provide of items) {
469
+ const endpoint = provide.registeredAddress ?? provide.address ?? "-";
470
+ const registration = provide.registered
471
+ ? c.green("registered")
472
+ : c.dim("declared");
473
+ log(` ${provide.capability.padEnd(24)} ${c.cyan(endpoint)} ${registration}${provide.description ? ` ${c.dim(provide.description)}` : ""}`);
474
+ }
475
+ }
476
+ log("");
477
+ }
478
+ async function cmdUninstall(id, args = []) {
479
+ const { uninstallAppTask, getApp, getInstance } = await import("../services/app/app-manager.js");
480
+ if (getApp(id)) {
481
+ await runManagedUninstallTask((exec) => exec ? uninstallAppTask(id, exec) : uninstallAppTask(id), `Uninstalled: ${id}`, getCliSudoState(args));
482
+ return;
483
+ }
484
+ if (getInstance(id)) {
485
+ const { deleteInstance } = await import("../services/instance-manager.js");
486
+ const deleteResult = await deleteInstance(id);
487
+ if (!deleteResult.ok) {
488
+ throw new Error(`Failed to uninstall instance '${id}'`);
489
+ }
490
+ log(c.green(` ✓ Uninstalled: ${id}`));
491
+ return;
492
+ }
493
+ log(c.red(` ✗ App/instance "${id}" not found.`));
494
+ process.exitCode = 1;
495
+ }
496
+ async function cmdUninstallAll(args = []) {
497
+ const items = await loadManagedListEntries();
498
+ if (items.length === 0) {
499
+ log(c.dim(" (no managed apps or instances to uninstall)"));
500
+ return;
501
+ }
502
+ const { uninstallAppTask } = await import("../services/app/app-manager.js");
503
+ const { deleteInstance } = await import("../services/instance-manager.js");
504
+ const errors = [];
505
+ const sudoState = getCliSudoState(args);
506
+ for (const item of items) {
507
+ try {
508
+ if (item.install_mode === "legacy-instance") {
509
+ const deleteResult = await deleteInstance(item.id);
510
+ if (!deleteResult.ok) {
511
+ throw new Error(`Failed to uninstall instance '${item.id}'`);
512
+ }
513
+ }
514
+ else {
515
+ const started = sudoState.password
516
+ ? uninstallAppTask(item.id, { sudoPassword: sudoState.password })
517
+ : uninstallAppTask(item.id);
518
+ if (!started.ok) {
519
+ const startedMessage = started.error || `Failed to uninstall '${item.id}'`;
520
+ if (isSudoPasswordError(startedMessage) && await promptForCliSudoPassword(sudoState)) {
521
+ const retried = uninstallAppTask(item.id, { sudoPassword: sudoState.password });
522
+ if (!retried.ok) {
523
+ throw new Error(formatSudoHint(retried.error || `Failed to uninstall '${item.id}'`, sudoState));
524
+ }
525
+ const retriedResult = retried.taskId ? await waitForLocalTask(retried.taskId) : { status: "done" };
526
+ if (retriedResult.status === "error") {
527
+ throw new Error(formatSudoHint(retriedResult.message || `Failed to uninstall '${item.id}'`, sudoState));
528
+ }
529
+ log(c.green(` ✓ Uninstalled: ${item.id}`));
530
+ continue;
531
+ }
532
+ throw new Error(formatSudoHint(startedMessage, sudoState));
533
+ }
534
+ const taskResult = started.taskId ? await waitForLocalTask(started.taskId) : { status: "done" };
535
+ if (taskResult.status === "error") {
536
+ const taskMessage = taskResult.message || `Failed to uninstall '${item.id}'`;
537
+ if (isSudoPasswordError(taskMessage) && await promptForCliSudoPassword(sudoState)) {
538
+ const retried = uninstallAppTask(item.id, { sudoPassword: sudoState.password });
539
+ if (!retried.ok) {
540
+ throw new Error(formatSudoHint(retried.error || `Failed to uninstall '${item.id}'`, sudoState));
541
+ }
542
+ const retriedResult = retried.taskId ? await waitForLocalTask(retried.taskId) : { status: "done" };
543
+ if (retriedResult.status === "error") {
544
+ throw new Error(formatSudoHint(retriedResult.message || `Failed to uninstall '${item.id}'`, sudoState));
545
+ }
546
+ log(c.green(` ✓ Uninstalled: ${item.id}`));
547
+ continue;
548
+ }
549
+ throw new Error(formatSudoHint(taskMessage, sudoState));
550
+ }
551
+ }
552
+ log(c.green(` ✓ Uninstalled: ${item.id}`));
553
+ }
554
+ catch (err) {
555
+ const message = err?.message || `Failed to uninstall '${item.id}'`;
556
+ errors.push(`${item.id}: ${message}`);
557
+ log(c.red(` ✗ ${item.id}: ${message}`));
558
+ }
559
+ }
560
+ if (errors.length > 0) {
561
+ process.exitCode = 1;
562
+ }
563
+ }
564
+ async function cmdCreateInstance(appId, instanceId, name) {
565
+ const { getApp, installApp, resolveRequires, updateInstance } = await import("../services/app/app-manager.js");
566
+ const appData = getApp(appId);
567
+ if (!appData) {
568
+ log(c.red(` ✗ App "${appId}" not found.`));
569
+ process.exitCode = 1;
570
+ return;
571
+ }
572
+ let resolvedEnv = {};
573
+ try {
574
+ resolvedEnv = resolveRequires(appData.spec);
575
+ }
576
+ catch (e) {
577
+ log(c.red(` ✗ ${e.message}`));
578
+ process.exitCode = 1;
579
+ return;
580
+ }
581
+ const specWithEnv = Object.keys(resolvedEnv).length > 0
582
+ ? {
583
+ ...appData.spec,
584
+ app_id: appData.manifest.id,
585
+ tasks: appData.spec.tasks.map((t) => t.role === "service" ? { ...t, env: { ...t.env, ...resolvedEnv } } : t),
586
+ }
587
+ : { ...appData.spec, app_id: appData.manifest.id };
588
+ const rawYaml = rewriteInstalledAppYaml(stringify({
589
+ ...specWithEnv,
590
+ name,
591
+ }), appData.manifest.id, instanceId);
592
+ await installApp(rawYaml, instanceId, {
593
+ bootstrap: {
594
+ name,
595
+ description: "",
596
+ },
597
+ });
598
+ updateInstance(instanceId, name, "");
599
+ log(c.green(` ✓ Created application "${instanceId}" from app "${appId}"`));
600
+ }
601
+ async function cmdCopy(sourceId) {
602
+ const { copyApp } = await import("../services/app/app-manager.js");
603
+ const result = await copyApp(sourceId);
604
+ log(c.green(` ✓ Copied ${sourceId} → ${result.manifest.id}`));
605
+ }
606
+ async function cmdStart(appId) {
607
+ ensureNomadToken();
608
+ const { getApp, startAppTask } = await import("../services/app/app-manager.js");
609
+ const app = getApp(appId);
610
+ if (app) {
611
+ await runManagedAppTask(() => startAppTask(appId), `Started: ${app.spec.name || readInstanceMeta(appId)?.name || appId}`);
612
+ return;
613
+ }
614
+ const result = await startNomadJobInstance(appId);
615
+ if (result.ok) {
616
+ log(c.green(` ✓ Started: ${readInstanceMeta(appId)?.name || appId}`));
617
+ return;
618
+ }
619
+ log(c.red(` ✗ ${result.error || "Start failed"}`));
620
+ process.exitCode = 1;
621
+ }
622
+ async function cmdStop(appId, purge) {
623
+ ensureNomadToken();
624
+ const { getApp, stopAppTask } = await import("../services/app/app-manager.js");
625
+ const app = getApp(appId);
626
+ if (app) {
627
+ await runManagedAppTask(() => stopAppTask(appId, purge), `Stopped: ${app.spec.name || readInstanceMeta(appId)?.name || appId}${purge ? " (purged)" : ""}`);
628
+ return;
629
+ }
630
+ const result = await stopNomadJobInstance(appId, purge);
631
+ if (result.ok) {
632
+ log(c.green(` ✓ Stopped: ${readInstanceMeta(appId)?.name || appId}${purge ? " (purged)" : ""}`));
633
+ return;
634
+ }
635
+ log(c.red(` ✗ ${result.error || "Stop failed"}`));
636
+ process.exitCode = 1;
637
+ }
638
+ async function cmdRestart(appId) {
639
+ ensureNomadToken();
640
+ const { getApp, restartAppTask } = await import("../services/app/app-manager.js");
641
+ const app = getApp(appId);
642
+ if (app) {
643
+ await runManagedAppTask(() => restartAppTask(appId), `Restarted: ${app.spec.name || readInstanceMeta(appId)?.name || appId}`);
644
+ return;
645
+ }
646
+ const result = await restartNomadJobInstance(appId);
647
+ if (result.ok) {
648
+ log(c.green(` ✓ Restarted: ${readInstanceMeta(appId)?.name || appId}`));
649
+ return;
650
+ }
651
+ log(c.red(` ✗ ${result.error || "Restart failed"}`));
652
+ process.exitCode = 1;
653
+ }
654
+ async function cmdStatus(appId, json) {
655
+ ensureNomadToken();
656
+ const { getApp, getAppStatus, getInstance } = await import("../services/app/app-manager.js");
657
+ const app = getApp(appId);
658
+ if (!app) {
659
+ const instance = getInstance(appId);
660
+ if (!instance) {
661
+ log(c.red(` ✗ App/instance "${appId}" not found.`));
662
+ process.exitCode = 1;
663
+ return;
664
+ }
665
+ const status = await getInstanceStatus(appId);
666
+ if (json) {
667
+ console.log(JSON.stringify({ id: appId, name: instance.name, ...status }, null, 2));
668
+ return;
669
+ }
670
+ const upStr = status.uptime ? `${Math.floor(status.uptime / 3600)}h ${Math.floor((status.uptime % 3600) / 60)}m` : "—";
671
+ log("");
672
+ log(` ${c.bold("Job:")} ${c.cyan(instance.name || appId)} ${c.dim(`(${appId})`)}`);
673
+ log(` ${c.bold("Status:")} ${status.status === "running" ? c.green(status.status) : status.status}`);
674
+ log(` ${c.bold("Uptime:")} ${upStr}`);
675
+ if (status.memory_mb)
676
+ log(` ${c.bold("Memory:")} ${status.memory_mb} MB`);
677
+ if (status.cpu_percent !== null)
678
+ log(` ${c.bold("CPU:")} ${status.cpu_percent}%`);
679
+ if (status.pid)
680
+ log(` ${c.bold("PID:")} ${status.pid}`);
681
+ log("");
682
+ return;
683
+ }
684
+ const status = await getAppStatus(appId);
685
+ const enriched = enrichStatusForCli(status, app.spec);
686
+ if (json) {
687
+ console.log(JSON.stringify({ id: appId, ...status, tasks: enriched.tasks, provides: enriched.provides }, null, 2));
688
+ return;
689
+ }
690
+ const stColor = status.status === "running" ? c.green
691
+ : status.status === "stopped" ? c.dim
692
+ : status.status === "pending" ? c.yellow
693
+ : c.red;
694
+ log("");
695
+ log(` ${c.bold("App:")} ${c.cyan(appId)}`);
696
+ log(` ${c.bold("Status:")} ${stColor(status.status)}`);
697
+ if (status.uptime != null)
698
+ log(` ${c.bold("Uptime:")} ${status.uptime}s`);
699
+ if (status.memory_mb)
700
+ log(` ${c.bold("Memory:")} ${status.memory_mb} MB`);
701
+ if (Object.keys(enriched.tasks).length > 0) {
702
+ log(` ${c.bold("Tasks:")}`);
703
+ for (const [name, t] of Object.entries(enriched.tasks)) {
704
+ log(` ${name.padEnd(20)} ${colorTaskState(t.state)} restarts=${t.restarts}`);
705
+ if (t.health.methods.length > 0) {
706
+ log(` health=${describeHealthTargets(t.health.targets)} status=${colorHealthState(t.health.status)}`);
707
+ }
708
+ else {
709
+ log(` health=${c.dim("none")}`);
710
+ }
711
+ }
712
+ }
713
+ if (enriched.provides.length > 0) {
714
+ log(` ${c.bold("Provides:")}`);
715
+ for (const provide of enriched.provides) {
716
+ const endpoint = provide.address ?? (typeof provide.port === "number" ? `127.0.0.1:${provide.port}` : "-");
717
+ log(` ${provide.capability.padEnd(24)} ${c.cyan(endpoint)}${provide.description ? ` ${c.dim(provide.description)}` : ""}`);
718
+ }
719
+ }
720
+ if (status.error)
721
+ log(c.red(` ${status.error}`));
722
+ log("");
723
+ }
724
+ async function cmdLogs(appId, args) {
725
+ ensureNomadToken();
726
+ const { getApp, getAppLogs } = await import("../services/app/app-manager.js");
727
+ const taskArg = args.find((a) => !a.startsWith("--"));
728
+ const lines = parseFlag(args, "--lines", 200);
729
+ const logType = args.includes("--stdout") ? "stdout" : "stderr";
730
+ const logLines = getApp(appId)
731
+ ? await getAppLogs(appId, taskArg ?? "", lines, logType)
732
+ : await getInstanceLogs(appId, lines, logType);
733
+ if (logLines.length === 0) {
734
+ log(c.dim(" (no logs)"));
735
+ return;
736
+ }
737
+ process.stdout.write(logLines.join("\n") + "\n");
738
+ }
739
+ async function cmdExec(appId, command) {
740
+ ensureNomadToken();
741
+ const { getApp, execInApp } = await import("../services/app/app-manager.js");
742
+ const result = getApp(appId)
743
+ ? await execInApp(appId, command)
744
+ : await execInInstance(appId, command);
745
+ if (result.stdout)
746
+ process.stdout.write(result.stdout);
747
+ if (result.stderr)
748
+ process.stderr.write(result.stderr);
749
+ if (result.exitCode !== 0)
750
+ process.exitCode = result.exitCode ?? 1;
751
+ }
752
+ // ── Entry point ───────────────────────────────────────────────────────────
753
+ export async function run(rest) {
754
+ const appCmd = rest[0];
755
+ try {
756
+ if (appCmd === "list") {
757
+ await cmdList(rest.slice(1));
758
+ }
759
+ else if (appCmd === "provides") {
760
+ await cmdProvides(rest.slice(1));
761
+ }
762
+ else if (appCmd === "show" && rest[1]) {
763
+ await cmdShow(rest[1]);
764
+ }
765
+ else if (appCmd === "install" && rest[1]) {
766
+ await cmdInstall(rest.slice(1));
767
+ }
768
+ else if (appCmd === "uninstall" && rest[1] === "--all") {
769
+ await cmdUninstallAll(rest.slice(2));
770
+ }
771
+ else if (appCmd === "uninstall" && rest[1]) {
772
+ await cmdUninstall(rest[1], rest.slice(2));
773
+ }
774
+ else if (appCmd === "create-instance" && rest[1]) {
775
+ const appId = rest[1];
776
+ const instanceId = rest[2];
777
+ const name = rest[3] || instanceId;
778
+ if (!instanceId) {
779
+ console.error("Usage: jishushell app create-instance <app-id> <instance-id> [name]");
780
+ process.exit(1);
781
+ }
782
+ await cmdCreateInstance(appId, instanceId, name);
783
+ }
784
+ else if (appCmd === "copy" && rest[1]) {
785
+ await cmdCopy(rest[1]);
786
+ }
787
+ else if (appCmd === "start" && rest[1]) {
788
+ await cmdStart(rest[1]);
789
+ }
790
+ else if (appCmd === "stop" && rest[1]) {
791
+ await cmdStop(rest[1], rest.includes("--purge"));
792
+ }
793
+ else if (appCmd === "restart" && rest[1]) {
794
+ await cmdRestart(rest[1]);
795
+ }
796
+ else if (appCmd === "status" && rest[1]) {
797
+ await cmdStatus(rest[1], rest.includes("--json"));
798
+ }
799
+ else if (appCmd === "logs" && rest[1]) {
800
+ await cmdLogs(rest[1], rest.slice(2));
801
+ }
802
+ else if (appCmd === "exec" && rest[1]) {
803
+ const dashDash = rest.indexOf("--");
804
+ const execCmd = dashDash >= 0 ? rest.slice(dashDash + 1) : [];
805
+ if (execCmd.length === 0) {
806
+ console.error("Usage: jishushell app exec <app-id> -- <command...>");
807
+ process.exit(1);
808
+ }
809
+ await cmdExec(rest[1], execCmd);
810
+ }
811
+ else if (appCmd === "help" || appCmd === "--help" || appCmd === "-h") {
812
+ printHelp();
813
+ }
814
+ else {
815
+ printHelp();
816
+ if (appCmd)
817
+ process.exit(1);
818
+ }
819
+ }
820
+ catch (err) {
821
+ log(c.red(` ✗ ${err.message}`));
822
+ process.exitCode = 1;
823
+ }
824
+ }
825
+ export async function dispatch(argv) {
826
+ if (argv[0] !== "app")
827
+ return false;
828
+ await run(argv.slice(1));
829
+ return true;
830
+ }
831
+ export function brief() {
832
+ return " app <install|list|provides|show|uninstall|copy|start|stop|restart|status|logs|exec> App/实例统一管理";
833
+ }
834
+ export function printHelp() {
835
+ console.log(`
836
+ Usage: jishushell app <command> [options]
837
+
838
+ Commands:
839
+ list [--json] 列出统一受管列表(apps 下已安装应用 + instances 下遗留实例)
840
+ provides [--json] 列出已安装 App 声明/注册的能力
841
+ show <id> 查看 App 或实例详情(JSON)
842
+ install <yaml-file|https-url|builtin-id> [app-id] [--sudo-password <password>]
843
+ 从本地、HTTPS 或内置模板安装 App,可选指定安装 id
844
+ uninstall <app-id> [--sudo-password <password>] 卸载单个 App 或实例(停止 + 删除目录)
845
+ uninstall --all [--sudo-password <password>] 卸载全部 App 与实例
846
+ create-instance <app-id> <inst-id> [name] 从 App 创建实例(Agent 通道由 spec.agentType 决定)
847
+ copy <app-id> 复制 App 实例(singleInstance 不可 copy)
848
+ start <id> 启动 App 或实例
849
+ stop <id> [--purge] 停止 App 或实例(--purge 同时清除 Nomad job)
850
+ restart <id> 重启 App 或实例
851
+ status <id> [--json] 查看 App 或实例状态
852
+ logs <id> [<task>] [--lines N] [--stdout] 查看日志(默认 stderr)
853
+ exec <id> -- <cmd...> 在 App 或实例内执行命令
854
+ help 显示此帮助
855
+
856
+ Examples:
857
+ jishushell app install ./apps/ollama-with-hollama-binary.yaml
858
+ jishushell app install ./apps/ollama-with-hollama-binary.yaml ollama-local
859
+ jishushell app install https://example.com/apps/ollama-with-hollama-binary.yaml
860
+ jishushell app install ollama
861
+ jishushell app list
862
+ jishushell app provides
863
+ jishushell app copy ollama-1
864
+ jishushell app start searxng-container
865
+ jishushell app status ollama
866
+ jishushell app logs ollama --lines 100
867
+ jishushell app exec ollama -- ollama list
868
+ jishushell app stop searxng-container
869
+ jishushell app uninstall ollama
870
+ jishushell app uninstall ollama --sudo-password 'your-sudo-password'
871
+ jishushell app uninstall --all
872
+ `);
873
+ }
874
+ //# sourceMappingURL=app.js.map