jishushell 0.4.17 → 0.4.24

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