jishushell 0.4.17 → 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 (241) hide show
  1. package/Dockerfile.hermes-slim +193 -0
  2. package/apps/hermes-container.yaml +35 -0
  3. package/apps/ollama-binary.yaml +164 -0
  4. package/apps/ollama-cpu-container.yaml +37 -0
  5. package/apps/ollama-with-hollama-binary.yaml +159 -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 +770 -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 +2148 -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 +164 -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 -2
  193. package/package.json +14 -4
  194. package/public/assets/Dashboard-rh9qpYRR.js +1 -0
  195. package/public/assets/HermesChatPanel-D6JI6lLY.js +1 -0
  196. package/public/assets/HermesConfigForm-DcbSemaj.js +4 -0
  197. package/public/assets/InitPassword-CFTKsED4.js +1 -0
  198. package/public/assets/InstanceDetail-BhNIKA6Z.js +91 -0
  199. package/public/assets/{Login-D1Bt-Lyk.js → Login-KB9qrtM0.js} +1 -1
  200. package/public/assets/NewInstance-CxkO8Hlq.js +1 -0
  201. package/public/assets/Settings-BVWJvOkU.js +1 -0
  202. package/public/assets/Setup-X-lzuaUT.js +1 -0
  203. package/public/assets/WeixinLoginPanel-gca0QTic.js +9 -0
  204. package/public/assets/index-C8B0cFJM.js +19 -0
  205. package/public/assets/index-CPhVFEsx.css +1 -0
  206. package/public/assets/input-paste-CrNVAyOy.js +1 -0
  207. package/public/assets/registry-fVUSujib.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
package/dist/cli/app.js CHANGED
@@ -1,72 +1,568 @@
1
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";
2
8
  // ── ANSI colour helpers ───────────────────────────────────────────────────
3
9
  const isTTY = process.stdout.isTTY ?? false;
4
10
  const c = {
5
11
  bold: (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s,
6
12
  green: (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s,
13
+ yellow: (s) => isTTY ? `\x1b[33m${s}\x1b[0m` : s,
7
14
  red: (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s,
8
15
  cyan: (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s,
9
16
  dim: (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s,
10
17
  };
11
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
+ }
12
384
  // ── Command implementations ───────────────────────────────────────────────
13
385
  async function cmdList(args) {
14
- const { listApps } = await import("../services/app-manager.js");
15
- const apps = listApps();
386
+ ensureNomadToken();
387
+ const items = await loadManagedListEntries();
16
388
  if (args.includes("--json")) {
17
- console.log(JSON.stringify(apps.map(a => ({
18
- id: a.spec.id, name: a.spec.name, version: a.spec.version,
19
- task_count: a.spec.tasks.length, installed_at: a.manifest.installed_at,
20
- })), null, 2));
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));
21
450
  return;
22
451
  }
23
452
  log("");
24
- log(c.bold(" Installed Apps"));
453
+ log(c.bold(" App Provides"));
25
454
  log(c.dim(" ─────────────────────────────────────────────────────"));
26
- if (apps.length === 0) {
27
- log(c.dim(" (no apps installed)"));
455
+ if (provides.length === 0) {
456
+ log(c.dim(" (no app capabilities declared)"));
457
+ log("");
458
+ return;
28
459
  }
29
- else {
30
- for (const a of apps) {
31
- log(` ${c.cyan(a.spec.id.padEnd(30))} ${(a.spec.version || "").padEnd(10)} ${c.dim(a.spec.name || "")}`);
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)}` : ""}`);
32
474
  }
33
475
  }
34
476
  log("");
35
477
  }
36
- async function cmdShow(id) {
37
- const { getApp } = await import("../services/app-manager.js");
38
- const app = getApp(id);
39
- if (!app) {
40
- log(c.red(` ✗ App "${id}" not found.`));
41
- process.exitCode = 1;
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));
42
482
  return;
43
483
  }
44
- console.log(JSON.stringify(app, null, 2));
45
- }
46
- async function cmdInstall(yamlPath) {
47
- if (!existsSync(yamlPath)) {
48
- log(c.red(` ✗ File not found: ${yamlPath}`));
49
- process.exitCode = 1;
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}`));
50
491
  return;
51
492
  }
52
- const yamlText = readFileSync(yamlPath, "utf-8");
53
- const { installApp } = await import("../services/app-manager.js");
54
- const result = await installApp(yamlText);
55
- log(c.green(` ✓ Installed app: ${result.spec.id} (${result.spec.name || ""} v${result.spec.version || "?"})`));
493
+ log(c.red(` ✗ App/instance "${id}" not found.`));
494
+ process.exitCode = 1;
56
495
  }
57
- async function cmdUninstall(id) {
58
- const { uninstallApp, getApp } = await import("../services/app-manager.js");
59
- if (!getApp(id)) {
60
- log(c.red(` ✗ App "${id}" not found.`));
61
- process.exitCode = 1;
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)"));
62
500
  return;
63
501
  }
64
- uninstallApp(id);
65
- log(c.green(` ✓ Uninstalled app: ${id}`));
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
+ }
66
563
  }
67
564
  async function cmdCreateInstance(appId, instanceId, name) {
68
- const { getApp, resolveRequires } = await import("../services/app-manager.js");
69
- const { createInstance } = await import("../services/instance-manager.js");
565
+ const { getApp, installApp, resolveRequires, updateInstance } = await import("../services/app/app-manager.js");
70
566
  const appData = getApp(appId);
71
567
  if (!appData) {
72
568
  log(c.red(` ✗ App "${appId}" not found.`));
@@ -85,11 +581,173 @@ async function cmdCreateInstance(appId, instanceId, name) {
85
581
  const specWithEnv = Object.keys(resolvedEnv).length > 0
86
582
  ? {
87
583
  ...appData.spec,
584
+ app_id: appData.manifest.id,
88
585
  tasks: appData.spec.tasks.map((t) => t.role === "service" ? { ...t, env: { ...t.env, ...resolvedEnv } } : t),
89
586
  }
90
- : appData.spec;
91
- await createInstance(instanceId, name, "", undefined, undefined, specWithEnv);
92
- log(c.green(` ✓ Created instance "${instanceId}" from app "${appId}"`));
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;
93
751
  }
94
752
  // ── Entry point ───────────────────────────────────────────────────────────
95
753
  export async function run(rest) {
@@ -98,14 +756,20 @@ export async function run(rest) {
98
756
  if (appCmd === "list") {
99
757
  await cmdList(rest.slice(1));
100
758
  }
759
+ else if (appCmd === "provides") {
760
+ await cmdProvides(rest.slice(1));
761
+ }
101
762
  else if (appCmd === "show" && rest[1]) {
102
763
  await cmdShow(rest[1]);
103
764
  }
104
765
  else if (appCmd === "install" && rest[1]) {
105
- await cmdInstall(rest[1]);
766
+ await cmdInstall(rest.slice(1));
767
+ }
768
+ else if (appCmd === "uninstall" && rest[1] === "--all") {
769
+ await cmdUninstallAll(rest.slice(2));
106
770
  }
107
771
  else if (appCmd === "uninstall" && rest[1]) {
108
- await cmdUninstall(rest[1]);
772
+ await cmdUninstall(rest[1], rest.slice(2));
109
773
  }
110
774
  else if (appCmd === "create-instance" && rest[1]) {
111
775
  const appId = rest[1];
@@ -117,6 +781,33 @@ export async function run(rest) {
117
781
  }
118
782
  await cmdCreateInstance(appId, instanceId, name);
119
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
+ }
120
811
  else if (appCmd === "help" || appCmd === "--help" || appCmd === "-h") {
121
812
  printHelp();
122
813
  }
@@ -131,26 +822,53 @@ export async function run(rest) {
131
822
  process.exitCode = 1;
132
823
  }
133
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
+ }
134
831
  export function brief() {
135
- return " app <install|list|show|uninstall|create-instance> App Spec 管理";
832
+ return " app <install|list|provides|show|uninstall|copy|start|stop|restart|status|logs|exec> App/实例统一管理";
136
833
  }
137
834
  export function printHelp() {
138
835
  console.log(`
139
836
  Usage: jishushell app <command> [options]
140
837
 
141
838
  Commands:
142
- list [--json] 列出已安装的 App
143
- show <app-id> 查看 App 详情(JSON)
144
- install <yaml-file> YAML 文件安装 App
145
- uninstall <app-id> 卸载 App
146
- create-instance <app-id> <inst-id> [name] 从 App 创建实例
147
- help 显示此帮助
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 显示此帮助
148
855
 
149
856
  Examples:
150
- jishushell app install ./my-app.yaml
151
- jishushell app create-instance my-app inst1 "My Instance"
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
152
861
  jishushell app list
153
- jishushell app show my-app
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
154
872
  `);
155
873
  }
156
874
  //# sourceMappingURL=app.js.map