jishushell 0.4.10 → 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.
- package/Dockerfile.hermes-slim +193 -0
- package/INSTALL-NOTICE +10 -12
- package/apps/hermes-container.yaml +35 -0
- package/apps/ollama-binary.yaml +200 -0
- package/apps/ollama-cpu-container.yaml +37 -0
- package/apps/ollama-with-hollama-binary.yaml +195 -0
- package/apps/openclaw-binary.yaml +69 -0
- package/apps/openclaw-container.yaml +37 -0
- package/apps/openclaw-with-ollama-container.yaml +42 -0
- package/apps/openclaw-with-searxng-container.yaml +136 -0
- package/apps/openwebui-container.yaml +53 -0
- package/apps/playwright-container.yaml +120 -0
- package/apps/searxng-container.yaml +115 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +15 -14
- package/dist/auth.js.map +1 -1
- package/dist/cli/app.d.ts +4 -0
- package/dist/cli/app.js +814 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/backup.d.ts +3 -0
- package/dist/cli/backup.js +434 -0
- package/dist/cli/backup.js.map +1 -0
- package/dist/{doctor.d.ts → cli/doctor.d.ts} +7 -1
- package/dist/{doctor.js → cli/doctor.js} +377 -22
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/helpers.d.ts +4 -0
- package/dist/cli/helpers.js +32 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/job.d.ts +4 -0
- package/dist/cli/job.js +198 -0
- package/dist/cli/job.js.map +1 -0
- package/dist/cli/llm.d.ts +25 -0
- package/dist/cli/llm.js +599 -0
- package/dist/cli/llm.js.map +1 -0
- package/dist/cli/managed-list.d.ts +30 -0
- package/dist/cli/managed-list.js +129 -0
- package/dist/cli/managed-list.js.map +1 -0
- package/dist/cli/panel.d.ts +26 -0
- package/dist/cli/panel.js +804 -0
- package/dist/cli/panel.js.map +1 -0
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +12 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli.js +48 -776
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +69 -0
- package/dist/config.js +268 -7
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +17 -41
- package/dist/control.js +61 -1323
- package/dist/control.js.map +1 -1
- package/dist/install.d.ts +16 -0
- package/dist/install.js +75 -26
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +15 -0
- package/dist/routes/agent-apps.js +78 -0
- package/dist/routes/agent-apps.js.map +1 -0
- package/dist/routes/apps.d.ts +3 -0
- package/dist/routes/apps.js +278 -0
- package/dist/routes/apps.js.map +1 -0
- package/dist/routes/backup.js +3 -3
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/instances.d.ts +6 -0
- package/dist/routes/instances.js +863 -874
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/llm.d.ts +15 -0
- package/dist/routes/llm.js +247 -0
- package/dist/routes/llm.js.map +1 -0
- package/dist/routes/runtime.d.ts +15 -0
- package/dist/routes/runtime.js +69 -0
- package/dist/routes/runtime.js.map +1 -0
- package/dist/routes/setup.js +131 -9
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +56 -9
- package/dist/routes/system.js.map +1 -1
- package/dist/server.js +107 -7
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.d.ts +30 -0
- package/dist/services/agent-apps/catalog.js +60 -0
- package/dist/services/agent-apps/catalog.js.map +1 -0
- package/dist/services/agent-apps/index.d.ts +36 -0
- package/dist/services/agent-apps/index.js +171 -0
- package/dist/services/agent-apps/index.js.map +1 -0
- package/dist/services/agent-apps/installers/adapter-probes.d.ts +49 -0
- package/dist/services/agent-apps/installers/adapter-probes.js +223 -0
- package/dist/services/agent-apps/installers/adapter-probes.js.map +1 -0
- package/dist/services/agent-apps/installers/adapter.d.ts +30 -0
- package/dist/services/agent-apps/installers/adapter.js +171 -0
- package/dist/services/agent-apps/installers/adapter.js.map +1 -0
- package/dist/services/agent-apps/installers/registry-probe.d.ts +38 -0
- package/dist/services/agent-apps/installers/registry-probe.js +183 -0
- package/dist/services/agent-apps/installers/registry-probe.js.map +1 -0
- package/dist/services/agent-apps/installers/shell-script.d.ts +47 -0
- package/dist/services/agent-apps/installers/shell-script.js +471 -0
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -0
- package/dist/services/agent-apps/types.d.ts +125 -0
- package/dist/services/agent-apps/types.js +17 -0
- package/dist/services/agent-apps/types.js.map +1 -0
- package/dist/services/app/app-compiler.d.ts +15 -0
- package/dist/services/app/app-compiler.js +172 -0
- package/dist/services/app/app-compiler.js.map +1 -0
- package/dist/services/app/app-manager.d.ts +142 -0
- package/dist/services/app/app-manager.js +1988 -0
- package/dist/services/app/app-manager.js.map +1 -0
- package/dist/services/app/custom-manager.d.ts +27 -0
- package/dist/services/app/custom-manager.js +285 -0
- package/dist/services/app/custom-manager.js.map +1 -0
- package/dist/services/app/hermes-agent-manager.d.ts +20 -0
- package/dist/services/app/hermes-agent-manager.js +289 -0
- package/dist/services/app/hermes-agent-manager.js.map +1 -0
- package/dist/services/app/id-normalizer.d.ts +27 -0
- package/dist/services/app/id-normalizer.js +77 -0
- package/dist/services/app/id-normalizer.js.map +1 -0
- package/dist/services/app/ollama-manager.d.ts +18 -0
- package/dist/services/app/ollama-manager.js +207 -0
- package/dist/services/app/ollama-manager.js.map +1 -0
- package/dist/services/app/openclaw-manager.d.ts +63 -0
- package/dist/services/app/openclaw-manager.js +1178 -0
- package/dist/services/app/openclaw-manager.js.map +1 -0
- package/dist/services/app/paths.d.ts +47 -0
- package/dist/services/app/paths.js +68 -0
- package/dist/services/app/paths.js.map +1 -0
- package/dist/services/app/registry.d.ts +17 -0
- package/dist/services/app/registry.js +31 -0
- package/dist/services/app/registry.js.map +1 -0
- package/dist/services/app/remote-spec.d.ts +14 -0
- package/dist/services/app/remote-spec.js +58 -0
- package/dist/services/app/remote-spec.js.map +1 -0
- package/dist/services/app/terminal-session-manager.d.ts +27 -0
- package/dist/services/app/terminal-session-manager.js +157 -0
- package/dist/services/app/terminal-session-manager.js.map +1 -0
- package/dist/services/app/types.d.ts +72 -0
- package/dist/services/app/types.js +16 -0
- package/dist/services/app/types.js.map +1 -0
- package/dist/services/backup-manager.js +60 -22
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/instance-manager.d.ts +125 -34
- package/dist/services/instance-manager.js +679 -1043
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/adapters.js +5 -1
- package/dist/services/llm-proxy/adapters.js.map +1 -1
- package/dist/services/llm-proxy/circuit-breaker.js +10 -2
- package/dist/services/llm-proxy/circuit-breaker.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +43 -0
- package/dist/services/llm-proxy/index.js +120 -5
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/ssrf.js +1 -1
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +260 -3
- package/dist/services/nomad-manager.js +2921 -341
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/panel-manager.d.ts +50 -0
- package/dist/services/panel-manager.js +443 -0
- package/dist/services/panel-manager.js.map +1 -0
- package/dist/services/plugin-installer.js +28 -2
- package/dist/services/plugin-installer.js.map +1 -1
- package/dist/services/process-manager.js +42 -7
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/custom.d.ts +20 -0
- package/dist/services/runtime/adapters/custom.js +90 -0
- package/dist/services/runtime/adapters/custom.js.map +1 -0
- package/dist/services/runtime/adapters/hermes.d.ts +174 -0
- package/dist/services/runtime/adapters/hermes.js +1316 -0
- package/dist/services/runtime/adapters/hermes.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +17 -0
- package/dist/services/runtime/adapters/openclaw-routes.js +946 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw.d.ts +188 -0
- package/dist/services/runtime/adapters/openclaw.js +2195 -0
- package/dist/services/runtime/adapters/openclaw.js.map +1 -0
- package/dist/services/runtime/errors.d.ts +28 -0
- package/dist/services/runtime/errors.js +31 -0
- package/dist/services/runtime/errors.js.map +1 -0
- package/dist/services/runtime/index.d.ts +34 -0
- package/dist/services/runtime/index.js +51 -0
- package/dist/services/runtime/index.js.map +1 -0
- package/dist/services/runtime/instance.d.ts +24 -0
- package/dist/services/runtime/instance.js +143 -0
- package/dist/services/runtime/instance.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +15 -0
- package/dist/services/runtime/migrations.js +25 -0
- package/dist/services/runtime/migrations.js.map +1 -0
- package/dist/services/runtime/registry.d.ts +13 -0
- package/dist/services/runtime/registry.js +32 -0
- package/dist/services/runtime/registry.js.map +1 -0
- package/dist/services/runtime/types.d.ts +545 -0
- package/dist/services/runtime/types.js +14 -0
- package/dist/services/runtime/types.js.map +1 -0
- package/dist/services/setup-manager.d.ts +70 -29
- package/dist/services/setup-manager.js +591 -625
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/task-registry.d.ts +44 -0
- package/dist/services/task-registry.js +74 -0
- package/dist/services/task-registry.js.map +1 -0
- package/dist/services/telemetry/heartbeat.d.ts +6 -6
- package/dist/services/telemetry/heartbeat.js +29 -30
- package/dist/services/telemetry/heartbeat.js.map +1 -1
- package/dist/services/update-manager.d.ts +47 -0
- package/dist/services/update-manager.js +305 -0
- package/dist/services/update-manager.js.map +1 -0
- package/dist/types.d.ts +222 -0
- package/dist/utils/docker-host.d.ts +15 -0
- package/dist/utils/docker-host.js +64 -0
- package/dist/utils/docker-host.js.map +1 -0
- package/install/jishu-install.sh +303 -37
- package/install/post-install.sh +64 -5
- package/package.json +19 -5
- package/public/assets/Dashboard-B-JoOjBQ.js +1 -0
- package/public/assets/HermesChatPanel-mFSureyc.js +1 -0
- package/public/assets/HermesConfigForm-DvR05LK1.js +4 -0
- package/public/assets/InitPassword-CVA8wQA6.js +1 -0
- package/public/assets/InstanceDetail-DcZW2QGO.js +91 -0
- package/public/assets/{Login-CUoEZOWR.js → Login-BWsZH2mu.js} +1 -1
- package/public/assets/NewInstance-BCIrAd86.js +1 -0
- package/public/assets/Settings-xkDcduFz.js +1 -0
- package/public/assets/Setup-Cfuwj4gV.js +1 -0
- package/public/assets/WeixinLoginPanel-CnjR8xMu.js +9 -0
- package/public/assets/index-CPhVFEsx.css +1 -0
- package/public/assets/index-DQsM6Joa.js +19 -0
- package/public/assets/input-paste-CrNVAyOy.js +1 -0
- package/public/assets/{providers-lBSOjUWy.js → providers-V-vwrExZ.js} +1 -1
- package/public/assets/registry-B4UFJdpA.js +2 -0
- package/public/assets/{usePolling-CK0DfI4h.js → usePolling-Do5Erqm_.js} +1 -1
- package/public/assets/vendor-i18n-ucpM0OR0.js +9 -0
- package/public/assets/{vendor-react-B1-3Yrt-.js → vendor-react-Bk1hRGiY.js} +1 -1
- package/public/favicon.png +0 -0
- package/public/index.html +9 -4
- package/public/logos/hermes.png +0 -0
- package/public/logos/ollama.png +0 -0
- package/public/logos/openclaw.svg +60 -0
- package/scripts/build-hermes-image.sh +21 -0
- package/scripts/build-local.sh +54 -0
- package/scripts/check-adapter-isolation.ts +293 -0
- package/scripts/fixtures/instances/hermes-sample/instance.json +37 -0
- package/scripts/fixtures/instances/legacy-openclaw-sample/instance.json +7 -0
- package/scripts/smoke/hermes-bootstrap.sh +195 -0
- package/templates/hermes-entrypoint.sh +154 -0
- package/dist/doctor.js.map +0 -1
- package/install/jishu-install-china.sh +0 -3092
- package/public/assets/Dashboard-DhsrzJ4F.js +0 -1
- package/public/assets/InitPassword-BjubiVdd.js +0 -1
- package/public/assets/InstanceDetail-DMcywsof.js +0 -17
- package/public/assets/NewInstance-Bk0G4EiJ.js +0 -1
- package/public/assets/Settings-D5tHL_h5.js +0 -1
- package/public/assets/Setup-4t6E3Rut.js +0 -1
- package/public/assets/index-BJ47MWpF.css +0 -1
- package/public/assets/index-DbX85irc.js +0 -16
- package/public/assets/vendor-i18n-CfW0RvgE.js +0 -9
package/dist/cli/app.js
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
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";
|
|
7
|
+
// ── ANSI colour helpers ───────────────────────────────────────────────────
|
|
8
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
9
|
+
const c = {
|
|
10
|
+
bold: (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s,
|
|
11
|
+
green: (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s,
|
|
12
|
+
yellow: (s) => isTTY ? `\x1b[33m${s}\x1b[0m` : s,
|
|
13
|
+
red: (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s,
|
|
14
|
+
cyan: (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s,
|
|
15
|
+
dim: (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s,
|
|
16
|
+
};
|
|
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
|
+
}
|
|
355
|
+
// ── Command implementations ───────────────────────────────────────────────
|
|
356
|
+
async function cmdList(args) {
|
|
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();
|
|
390
|
+
if (args.includes("--json")) {
|
|
391
|
+
console.log(JSON.stringify(provides, null, 2));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
log("");
|
|
395
|
+
log(c.bold(" App Provides"));
|
|
396
|
+
log(c.dim(" ─────────────────────────────────────────────────────"));
|
|
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);
|
|
407
|
+
}
|
|
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)}` : ""}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
log("");
|
|
419
|
+
}
|
|
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));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
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}`));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
log(c.red(` ✗ App/instance "${id}" not found.`));
|
|
436
|
+
process.exitCode = 1;
|
|
437
|
+
}
|
|
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)"));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
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
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function cmdCreateInstance(appId, instanceId, name) {
|
|
507
|
+
const { getApp, installApp, resolveRequires, updateInstance } = await import("../services/app/app-manager.js");
|
|
508
|
+
const appData = getApp(appId);
|
|
509
|
+
if (!appData) {
|
|
510
|
+
log(c.red(` ✗ App "${appId}" not found.`));
|
|
511
|
+
process.exitCode = 1;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
let resolvedEnv = {};
|
|
515
|
+
try {
|
|
516
|
+
resolvedEnv = resolveRequires(appData.spec);
|
|
517
|
+
}
|
|
518
|
+
catch (e) {
|
|
519
|
+
log(c.red(` ✗ ${e.message}`));
|
|
520
|
+
process.exitCode = 1;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const specWithEnv = Object.keys(resolvedEnv).length > 0
|
|
524
|
+
? {
|
|
525
|
+
...appData.spec,
|
|
526
|
+
app_id: appData.manifest.id,
|
|
527
|
+
tasks: appData.spec.tasks.map((t) => t.role === "service" ? { ...t, env: { ...t.env, ...resolvedEnv } } : t),
|
|
528
|
+
}
|
|
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;
|
|
693
|
+
}
|
|
694
|
+
// ── Entry point ───────────────────────────────────────────────────────────
|
|
695
|
+
export async function run(rest) {
|
|
696
|
+
const appCmd = rest[0];
|
|
697
|
+
try {
|
|
698
|
+
if (appCmd === "list") {
|
|
699
|
+
await cmdList(rest.slice(1));
|
|
700
|
+
}
|
|
701
|
+
else if (appCmd === "provides") {
|
|
702
|
+
await cmdProvides(rest.slice(1));
|
|
703
|
+
}
|
|
704
|
+
else if (appCmd === "show" && rest[1]) {
|
|
705
|
+
await cmdShow(rest[1]);
|
|
706
|
+
}
|
|
707
|
+
else if (appCmd === "install" && rest[1]) {
|
|
708
|
+
await cmdInstall(rest[1], rest[2]);
|
|
709
|
+
}
|
|
710
|
+
else if (appCmd === "uninstall" && rest[1] === "--all") {
|
|
711
|
+
await cmdUninstallAll(rest.slice(2));
|
|
712
|
+
}
|
|
713
|
+
else if (appCmd === "uninstall" && rest[1]) {
|
|
714
|
+
await cmdUninstall(rest[1], rest.slice(2));
|
|
715
|
+
}
|
|
716
|
+
else if (appCmd === "create-instance" && rest[1]) {
|
|
717
|
+
const appId = rest[1];
|
|
718
|
+
const instanceId = rest[2];
|
|
719
|
+
const name = rest[3] || instanceId;
|
|
720
|
+
if (!instanceId) {
|
|
721
|
+
console.error("Usage: jishushell app create-instance <app-id> <instance-id> [name]");
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
await cmdCreateInstance(appId, instanceId, name);
|
|
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
|
+
}
|
|
753
|
+
else if (appCmd === "help" || appCmd === "--help" || appCmd === "-h") {
|
|
754
|
+
printHelp();
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
printHelp();
|
|
758
|
+
if (appCmd)
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
log(c.red(` ✗ ${err.message}`));
|
|
764
|
+
process.exitCode = 1;
|
|
765
|
+
}
|
|
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
|
+
}
|
|
773
|
+
export function brief() {
|
|
774
|
+
return " app <install|list|provides|show|uninstall|copy|start|stop|restart|status|logs|exec> App/实例统一管理";
|
|
775
|
+
}
|
|
776
|
+
export function printHelp() {
|
|
777
|
+
console.log(`
|
|
778
|
+
Usage: jishushell app <command> [options]
|
|
779
|
+
|
|
780
|
+
Commands:
|
|
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 显示此帮助
|
|
796
|
+
|
|
797
|
+
Examples:
|
|
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
|
|
801
|
+
jishushell app list
|
|
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
|
|
812
|
+
`);
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=app.js.map
|