jishushell 0.0.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES +387 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.js +88 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +290 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +226 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/control.d.ts +44 -0
- package/dist/control.js +1359 -0
- package/dist/control.js.map +1 -0
- package/dist/crypto-shim.d.ts +1 -0
- package/dist/crypto-shim.js +2 -0
- package/dist/crypto-shim.js.map +1 -0
- package/dist/doctor.d.ts +46 -0
- package/dist/doctor.js +937 -0
- package/dist/doctor.js.map +1 -0
- package/dist/install.d.ts +27 -0
- package/dist/install.js +570 -0
- package/dist/install.js.map +1 -0
- package/dist/routes/auth.d.ts +4 -0
- package/dist/routes/auth.js +151 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/instances.d.ts +2 -0
- package/dist/routes/instances.js +1303 -0
- package/dist/routes/instances.js.map +1 -0
- package/dist/routes/setup.d.ts +2 -0
- package/dist/routes/setup.js +139 -0
- package/dist/routes/setup.js.map +1 -0
- package/dist/routes/system.d.ts +2 -0
- package/dist/routes/system.js +102 -0
- package/dist/routes/system.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +392 -0
- package/dist/server.js.map +1 -0
- package/dist/services/instance-manager.d.ts +67 -0
- package/dist/services/instance-manager.js +1319 -0
- package/dist/services/instance-manager.js.map +1 -0
- package/dist/services/llm-proxy/adapters.d.ts +3 -0
- package/dist/services/llm-proxy/adapters.js +309 -0
- package/dist/services/llm-proxy/adapters.js.map +1 -0
- package/dist/services/llm-proxy/circuit-breaker.d.ts +9 -0
- package/dist/services/llm-proxy/circuit-breaker.js +73 -0
- package/dist/services/llm-proxy/circuit-breaker.js.map +1 -0
- package/dist/services/llm-proxy/encryption.d.ts +6 -0
- package/dist/services/llm-proxy/encryption.js +61 -0
- package/dist/services/llm-proxy/encryption.js.map +1 -0
- package/dist/services/llm-proxy/index.d.ts +24 -0
- package/dist/services/llm-proxy/index.js +708 -0
- package/dist/services/llm-proxy/index.js.map +1 -0
- package/dist/services/llm-proxy/rate-limiter.d.ts +1 -0
- package/dist/services/llm-proxy/rate-limiter.js +39 -0
- package/dist/services/llm-proxy/rate-limiter.js.map +1 -0
- package/dist/services/llm-proxy/sse.d.ts +10 -0
- package/dist/services/llm-proxy/sse.js +378 -0
- package/dist/services/llm-proxy/sse.js.map +1 -0
- package/dist/services/llm-proxy/ssrf.d.ts +16 -0
- package/dist/services/llm-proxy/ssrf.js +185 -0
- package/dist/services/llm-proxy/ssrf.js.map +1 -0
- package/dist/services/llm-proxy/types.d.ts +52 -0
- package/dist/services/llm-proxy/types.js +2 -0
- package/dist/services/llm-proxy/types.js.map +1 -0
- package/dist/services/llm-proxy/usage.d.ts +12 -0
- package/dist/services/llm-proxy/usage.js +108 -0
- package/dist/services/llm-proxy/usage.js.map +1 -0
- package/dist/services/nomad-manager.d.ts +22 -0
- package/dist/services/nomad-manager.js +828 -0
- package/dist/services/nomad-manager.js.map +1 -0
- package/dist/services/plugin-installer.d.ts +22 -0
- package/dist/services/plugin-installer.js +102 -0
- package/dist/services/plugin-installer.js.map +1 -0
- package/dist/services/process-manager.d.ts +25 -0
- package/dist/services/process-manager.js +531 -0
- package/dist/services/process-manager.js.map +1 -0
- package/dist/services/setup-manager.d.ts +93 -0
- package/dist/services/setup-manager.js +1922 -0
- package/dist/services/setup-manager.js.map +1 -0
- package/dist/services/system-monitor.d.ts +1 -0
- package/dist/services/system-monitor.js +79 -0
- package/dist/services/system-monitor.js.map +1 -0
- package/dist/services/telemetry/activation.d.ts +12 -0
- package/dist/services/telemetry/activation.js +75 -0
- package/dist/services/telemetry/activation.js.map +1 -0
- package/dist/services/telemetry/client.d.ts +21 -0
- package/dist/services/telemetry/client.js +47 -0
- package/dist/services/telemetry/client.js.map +1 -0
- package/dist/services/telemetry/device-fingerprint.d.ts +18 -0
- package/dist/services/telemetry/device-fingerprint.js +123 -0
- package/dist/services/telemetry/device-fingerprint.js.map +1 -0
- package/dist/services/telemetry/heartbeat.d.ts +13 -0
- package/dist/services/telemetry/heartbeat.js +81 -0
- package/dist/services/telemetry/heartbeat.js.map +1 -0
- package/dist/services/telemetry/index.d.ts +3 -0
- package/dist/services/telemetry/index.js +4 -0
- package/dist/services/telemetry/index.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/safe-json.d.ts +2 -0
- package/dist/utils/safe-json.js +80 -0
- package/dist/utils/safe-json.js.map +1 -0
- package/dist/utils/ttl-cache.d.ts +29 -0
- package/dist/utils/ttl-cache.js +77 -0
- package/dist/utils/ttl-cache.js.map +1 -0
- package/install/jishu-install.sh +2920 -0
- package/install/jishu-uninstall.sh +811 -0
- package/install/post-install.sh +110 -0
- package/install/post-uninstall.sh +46 -0
- package/package.json +57 -8
- package/public/assets/Dashboard-CAOQDYDR.js +1 -0
- package/public/assets/InitPassword-CkehIkJG.js +1 -0
- package/public/assets/InstanceDetail-CzW2S95J.js +14 -0
- package/public/assets/Login-RkjzTNWg.js +1 -0
- package/public/assets/NewInstance-DdbErdjA.js +1 -0
- package/public/assets/Settings-BUD7zwv9.js +1 -0
- package/public/assets/Setup-RRTIERGG.js +1 -0
- package/public/assets/index-77Ug7feY.css +1 -0
- package/public/assets/index-DfRnVUQR.js +16 -0
- package/public/assets/providers-lBSOjUWy.js +1 -0
- package/public/assets/usePolling-CqQ8hrNc.js +1 -0
- package/public/assets/vendor-i18n-Bvxxh8Di.js +9 -0
- package/public/assets/vendor-react-DONn7uBV.js +59 -0
- package/public/index.html +15 -0
- package/scripts/build-image.sh +55 -0
- package/scripts/run.sh +310 -0
- package/scripts/setup-pi.sh +80 -0
- package/scripts/start-feishu1.js +46 -0
- package/index.js +0 -0
- package/jishushell-0.0.1.tgz +0 -0
package/dist/control.js
ADDED
|
@@ -0,0 +1,1359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JishuShell process control — start / stop / restart / doctor
|
|
3
|
+
*
|
|
4
|
+
* Supports three run modes (detected automatically):
|
|
5
|
+
* systemd — /etc/systemd/system/jishushell.service exists
|
|
6
|
+
* launchd — ~/Library/LaunchAgents/com.jishushell.panel.plist exists (macOS)
|
|
7
|
+
* process — PID file at ~/.jishushell/jishushell.pid (fallback)
|
|
8
|
+
*/
|
|
9
|
+
import { execSync, execFileSync, spawn } from "child_process";
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, closeSync, } from "fs";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { checkAndReport, checkAndReportHeartbeat } from "./services/telemetry/index.js";
|
|
14
|
+
import { homedir, networkInterfaces } from "os";
|
|
15
|
+
import * as http from "http";
|
|
16
|
+
import * as https from "https";
|
|
17
|
+
import { JISHUSHELL_HOME, getPanelConfig, getNomadToken, getNomadAddr, INSTANCES_DIR } from "./config.js";
|
|
18
|
+
import { isInitialized, initPassword } from "./auth.js";
|
|
19
|
+
import { runDoctorChecks } from "./doctor.js";
|
|
20
|
+
import { loadNomadToken } from "./services/setup-manager.js";
|
|
21
|
+
import { readdirSync, statSync } from "fs";
|
|
22
|
+
// ── Paths ──────────────────────────────────────────────────────────────────
|
|
23
|
+
const PID_FILE = join(JISHUSHELL_HOME, "jishushell.pid");
|
|
24
|
+
const LOG_FILE = join(JISHUSHELL_HOME, "jishushell.log");
|
|
25
|
+
const SYSTEMD_SERVICE = "/etc/systemd/system/jishushell.service";
|
|
26
|
+
const LAUNCHD_PLIST = join(homedir(), "Library/LaunchAgents/com.jishushell.panel.plist");
|
|
27
|
+
// Locate the actual cli.js at runtime — works for both dev and global-install.
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
// In production dist/cli.js; in dev the file is src/control.ts → dist/control.js → ../../dist/cli.js
|
|
30
|
+
const CLI_PATH = join(dirname(__filename), "cli.js");
|
|
31
|
+
// ── ANSI colours (stripped when not a TTY) ─────────────────────────────────
|
|
32
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
33
|
+
const c = {
|
|
34
|
+
bold: (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s,
|
|
35
|
+
green: (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s,
|
|
36
|
+
yellow: (s) => isTTY ? `\x1b[33m${s}\x1b[0m` : s,
|
|
37
|
+
red: (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s,
|
|
38
|
+
cyan: (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s,
|
|
39
|
+
dim: (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s,
|
|
40
|
+
};
|
|
41
|
+
function log(msg) { process.stdout.write(msg + "\n"); }
|
|
42
|
+
export function detectRunMode() {
|
|
43
|
+
if (process.platform !== "darwin") {
|
|
44
|
+
// Check systemd availability
|
|
45
|
+
if (existsSync(SYSTEMD_SERVICE)) {
|
|
46
|
+
try {
|
|
47
|
+
execSync("systemctl --version 2>/dev/null", { stdio: "ignore", timeout: 2000 });
|
|
48
|
+
return "systemd";
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (existsSync(LAUNCHD_PLIST))
|
|
55
|
+
return "launchd";
|
|
56
|
+
}
|
|
57
|
+
return "process";
|
|
58
|
+
}
|
|
59
|
+
// ── Panel port resolution ──────────────────────────────────────────────────
|
|
60
|
+
// Reads from panel.json; falls back to the DEFAULT_PORT env var, then 8090.
|
|
61
|
+
function getPanelPort() {
|
|
62
|
+
try {
|
|
63
|
+
const cfg = getPanelConfig();
|
|
64
|
+
const p = parseInt(String(cfg.panel_port ?? ""), 10);
|
|
65
|
+
if (p > 0 && p < 65536)
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
const env = parseInt(process.env.JISHUSHELL_PORT ?? "", 10);
|
|
70
|
+
if (env > 0 && env < 65536)
|
|
71
|
+
return env;
|
|
72
|
+
return 8090;
|
|
73
|
+
}
|
|
74
|
+
// ── Process state helpers ──────────────────────────────────────────────────
|
|
75
|
+
/**
|
|
76
|
+
* Read PID file and check whether the process is still alive.
|
|
77
|
+
* Returns the PID if alive, null otherwise (stale file or missing).
|
|
78
|
+
*/
|
|
79
|
+
function readAlivePid() {
|
|
80
|
+
try {
|
|
81
|
+
const raw = readFileSync(PID_FILE, "utf-8").trim();
|
|
82
|
+
const pid = parseInt(raw, 10);
|
|
83
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
84
|
+
return null;
|
|
85
|
+
// kill -0 checks existence without actually sending a signal
|
|
86
|
+
process.kill(pid, 0);
|
|
87
|
+
// Verify cmdline to avoid signaling a reused PID
|
|
88
|
+
try {
|
|
89
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
90
|
+
if (!cmdline.includes("jishushell") && !cmdline.includes("cli.js"))
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
catch { /* /proc not available (macOS) — skip verification */ }
|
|
94
|
+
return pid;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function writePid(pid) {
|
|
101
|
+
mkdirSync(dirname(PID_FILE), { recursive: true });
|
|
102
|
+
writeFileSync(PID_FILE, String(pid) + "\n", { mode: 0o600 });
|
|
103
|
+
}
|
|
104
|
+
function removePidFile() {
|
|
105
|
+
try {
|
|
106
|
+
unlinkSync(PID_FILE);
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
}
|
|
110
|
+
// ── Port availability check ────────────────────────────────────────────────
|
|
111
|
+
function isPortListening(port) {
|
|
112
|
+
try {
|
|
113
|
+
if (process.platform === "darwin") {
|
|
114
|
+
const out = execFileSync("lsof", ["-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
|
|
115
|
+
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
116
|
+
});
|
|
117
|
+
return out.trim().length > 0;
|
|
118
|
+
}
|
|
119
|
+
const out = execFileSync("ss", ["-tlnp"], {
|
|
120
|
+
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
121
|
+
});
|
|
122
|
+
return new RegExp(`[:\\s]${port}\\s`).test(out);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Poll until port is no longer listening (used after stop). */
|
|
129
|
+
async function waitForPortFree(port, timeoutMs = 5000) {
|
|
130
|
+
const deadline = Date.now() + timeoutMs;
|
|
131
|
+
while (Date.now() < deadline) {
|
|
132
|
+
if (!isPortListening(port))
|
|
133
|
+
return;
|
|
134
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Poll until port is listening (used after start). */
|
|
138
|
+
async function waitForPortOpen(port, timeoutMs = 8000) {
|
|
139
|
+
const deadline = Date.now() + timeoutMs;
|
|
140
|
+
while (Date.now() < deadline) {
|
|
141
|
+
if (isPortListening(port))
|
|
142
|
+
return true;
|
|
143
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
// ── HTTP health probe ──────────────────────────────────────────────────────
|
|
148
|
+
function httpGet(url, timeoutMs = 3000) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
151
|
+
let body = "";
|
|
152
|
+
res.on("data", (d) => { body += d; });
|
|
153
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
|
|
154
|
+
});
|
|
155
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
156
|
+
req.on("error", reject);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// ── systemd mode ──────────────────────────────────────────────────────────
|
|
160
|
+
function systemdExec(action) {
|
|
161
|
+
// Always use sudo for systemd service management to avoid polkit prompts.
|
|
162
|
+
// Fall back to non-sudo if sudo is not available (e.g. macOS rootless).
|
|
163
|
+
let useSudo = true;
|
|
164
|
+
try {
|
|
165
|
+
execSync("sudo -n true 2>/dev/null", { stdio: "ignore", timeout: 3000 });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
useSudo = false;
|
|
169
|
+
}
|
|
170
|
+
if (useSudo) {
|
|
171
|
+
execFileSync("sudo", ["systemctl", action, "jishushell"], { stdio: "inherit", timeout: 15000 });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
execFileSync("systemctl", [action, "jishushell"], { stdio: "inherit", timeout: 15000 });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function systemdStatus() {
|
|
178
|
+
try {
|
|
179
|
+
return execSync("systemctl is-active jishushell 2>/dev/null", {
|
|
180
|
+
encoding: "utf-8", timeout: 3000,
|
|
181
|
+
}).trim();
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
// execSync throws when exit code != 0; "inactive" / "failed" / "activating"
|
|
185
|
+
// all exit non-zero, so read stdout from the error object.
|
|
186
|
+
if (e?.stdout)
|
|
187
|
+
return e.stdout.trim();
|
|
188
|
+
return "unknown";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Reset a failed unit before starting so `systemctl start` succeeds. */
|
|
192
|
+
function systemdResetFailed() {
|
|
193
|
+
try {
|
|
194
|
+
execFileSync("sudo", ["systemctl", "reset-failed", "jishushell"], { stdio: "ignore", timeout: 5000 });
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
try {
|
|
198
|
+
execFileSync("systemctl", ["reset-failed", "jishushell"], { stdio: "ignore", timeout: 5000 });
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ── launchd mode ──────────────────────────────────────────────────────────
|
|
204
|
+
function launchctlExec(action) {
|
|
205
|
+
const label = "com.jishushell.panel";
|
|
206
|
+
if (action === "start") {
|
|
207
|
+
try {
|
|
208
|
+
execFileSync("launchctl", ["load", "-w", LAUNCHD_PLIST], { stdio: "ignore", timeout: 10000 });
|
|
209
|
+
}
|
|
210
|
+
catch { }
|
|
211
|
+
}
|
|
212
|
+
execFileSync("launchctl", [action, label], { stdio: "inherit", timeout: 10000 });
|
|
213
|
+
}
|
|
214
|
+
// ── process mode ──────────────────────────────────────────────────────────
|
|
215
|
+
function processStart(port) {
|
|
216
|
+
mkdirSync(dirname(LOG_FILE), { recursive: true });
|
|
217
|
+
const logFd = openSync(LOG_FILE, "a");
|
|
218
|
+
const child = spawn(process.execPath, [CLI_PATH, "--port", String(port)], {
|
|
219
|
+
detached: true,
|
|
220
|
+
stdio: ["ignore", logFd, logFd],
|
|
221
|
+
env: {
|
|
222
|
+
PATH: process.env.PATH,
|
|
223
|
+
HOME: process.env.HOME,
|
|
224
|
+
USER: process.env.USER,
|
|
225
|
+
LANG: process.env.LANG,
|
|
226
|
+
NODE_ENV: "production",
|
|
227
|
+
JISHUSHELL_HOME: process.env.JISHUSHELL_HOME || "",
|
|
228
|
+
JISHUSHELL_PORT: process.env.JISHUSHELL_PORT || "",
|
|
229
|
+
JISHUSHELL_JWT_SECRET: process.env.JISHUSHELL_JWT_SECRET || "",
|
|
230
|
+
JISHUSHELL_AES_KEY: process.env.JISHUSHELL_AES_KEY || "",
|
|
231
|
+
NOMAD_ADDR: process.env.NOMAD_ADDR || "",
|
|
232
|
+
NOMAD_TOKEN: process.env.NOMAD_TOKEN || "",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
child.unref();
|
|
236
|
+
closeSync(logFd);
|
|
237
|
+
writePid(child.pid);
|
|
238
|
+
}
|
|
239
|
+
async function processStop() {
|
|
240
|
+
const pid = readAlivePid();
|
|
241
|
+
if (pid === null) {
|
|
242
|
+
removePidFile();
|
|
243
|
+
throw new Error("No running process found (PID file missing or stale).");
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
process.kill(pid, "SIGTERM");
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
if (e.code === "EPERM") {
|
|
250
|
+
throw new Error(`Permission denied to stop PID ${pid}. Try: sudo jishushell stop`);
|
|
251
|
+
}
|
|
252
|
+
removePidFile();
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
// Wait for graceful exit (up to 5s), then SIGKILL
|
|
256
|
+
const deadline = Date.now() + 5000;
|
|
257
|
+
while (Date.now() < deadline) {
|
|
258
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
259
|
+
try {
|
|
260
|
+
process.kill(pid, 0);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
removePidFile();
|
|
264
|
+
return;
|
|
265
|
+
} // exited
|
|
266
|
+
}
|
|
267
|
+
// Escalate
|
|
268
|
+
try {
|
|
269
|
+
process.kill(pid, "SIGKILL");
|
|
270
|
+
}
|
|
271
|
+
catch { }
|
|
272
|
+
removePidFile();
|
|
273
|
+
}
|
|
274
|
+
/** Find and SIGTERM all processes occupying a TCP port. Returns true if any PIDs were found. */
|
|
275
|
+
function killPortProcess(port) {
|
|
276
|
+
try {
|
|
277
|
+
let pids = [];
|
|
278
|
+
if (process.platform === "darwin") {
|
|
279
|
+
const out = execSync(`lsof -ti:${port} 2>/dev/null`, {
|
|
280
|
+
encoding: "utf-8", timeout: 3000,
|
|
281
|
+
});
|
|
282
|
+
pids = out.trim().split("\n").filter(Boolean).map(Number).filter(n => n > 1);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const out = execSync("ss -tlnp 2>/dev/null", { encoding: "utf-8", timeout: 3000 });
|
|
286
|
+
const lines = out.split("\n").filter(l => l.includes(`:${port} `));
|
|
287
|
+
for (const line of lines) {
|
|
288
|
+
for (const m of line.matchAll(/pid=(\d+)/g)) {
|
|
289
|
+
const n = parseInt(m[1], 10);
|
|
290
|
+
if (n > 1)
|
|
291
|
+
pids.push(n);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
pids = [...new Set(pids)];
|
|
296
|
+
if (pids.length === 0)
|
|
297
|
+
return false;
|
|
298
|
+
for (const pid of pids) {
|
|
299
|
+
try {
|
|
300
|
+
process.kill(pid, "SIGTERM");
|
|
301
|
+
}
|
|
302
|
+
catch { }
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
311
|
+
export async function startPanel(port) {
|
|
312
|
+
const resolvedPort = port ?? getPanelPort();
|
|
313
|
+
const mode = detectRunMode();
|
|
314
|
+
// Guard: already running?
|
|
315
|
+
if (isPortListening(resolvedPort)) {
|
|
316
|
+
log(c.yellow(`! JishuShell is already running on port ${resolvedPort}`));
|
|
317
|
+
log(c.dim(` Run ${c.bold("jishushell stop")} first, or ${c.bold("jishushell restart")}`));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
log(c.dim(` run mode: ${mode} | port: ${resolvedPort}`));
|
|
321
|
+
switch (mode) {
|
|
322
|
+
case "systemd":
|
|
323
|
+
systemdResetFailed();
|
|
324
|
+
systemdExec("start");
|
|
325
|
+
break;
|
|
326
|
+
case "launchd":
|
|
327
|
+
launchctlExec("start");
|
|
328
|
+
break;
|
|
329
|
+
case "process":
|
|
330
|
+
if (readAlivePid() !== null) {
|
|
331
|
+
log(c.yellow("! Process already tracked in PID file but port is not listening."));
|
|
332
|
+
log(c.dim(" Removing stale PID file and starting fresh…"));
|
|
333
|
+
removePidFile();
|
|
334
|
+
}
|
|
335
|
+
processStart(resolvedPort);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
log(c.dim(" Waiting for panel to start…"));
|
|
339
|
+
const ok = await waitForPortOpen(resolvedPort, 10000);
|
|
340
|
+
if (ok) {
|
|
341
|
+
log(c.green(`✓ JishuShell started → http://localhost:${resolvedPort}`));
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
log(c.yellow(`! Port ${resolvedPort} still not listening after 10s.`));
|
|
345
|
+
if (mode === "process") {
|
|
346
|
+
log(c.dim(` Check logs: ${LOG_FILE}`));
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
log(c.dim(` Check logs: journalctl -u jishushell -n 50`));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export async function stopPanel() {
|
|
354
|
+
const port = getPanelPort();
|
|
355
|
+
const mode = detectRunMode();
|
|
356
|
+
log(c.dim(` run mode: ${mode}`));
|
|
357
|
+
switch (mode) {
|
|
358
|
+
case "systemd":
|
|
359
|
+
systemdExec("stop");
|
|
360
|
+
break;
|
|
361
|
+
case "launchd":
|
|
362
|
+
launchctlExec("stop");
|
|
363
|
+
break;
|
|
364
|
+
case "process":
|
|
365
|
+
await processStop();
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
log(c.dim(" Waiting for port to be released…"));
|
|
369
|
+
await waitForPortFree(port, 5000);
|
|
370
|
+
log(c.green("✓ JishuShell stopped"));
|
|
371
|
+
}
|
|
372
|
+
export async function restartPanel(port) {
|
|
373
|
+
const mode = detectRunMode();
|
|
374
|
+
const resolvedPort = port ?? getPanelPort();
|
|
375
|
+
log(c.dim(` run mode: ${mode} | port: ${resolvedPort}`));
|
|
376
|
+
if (mode === "systemd") {
|
|
377
|
+
// systemd restart handles stop+start atomically
|
|
378
|
+
systemdResetFailed();
|
|
379
|
+
systemdExec("restart");
|
|
380
|
+
log(c.dim(" Waiting for panel to start…"));
|
|
381
|
+
const ok = await waitForPortOpen(resolvedPort, 10000);
|
|
382
|
+
if (ok) {
|
|
383
|
+
log(c.green(`✓ JishuShell restarted → http://localhost:${resolvedPort}`));
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
log(c.yellow("! Panel did not come back up within 10s."));
|
|
387
|
+
log(c.dim(" Check: journalctl -u jishushell -n 50"));
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// ── Non-systemd: guarantee a clean stop before starting ──────────────────
|
|
392
|
+
log(c.dim(" Stopping…"));
|
|
393
|
+
// Step 1: graceful stop via mode-specific mechanism
|
|
394
|
+
if (mode === "launchd") {
|
|
395
|
+
try {
|
|
396
|
+
launchctlExec("stop");
|
|
397
|
+
}
|
|
398
|
+
catch { }
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
// process mode: use PID file when available
|
|
402
|
+
if (readAlivePid() !== null) {
|
|
403
|
+
try {
|
|
404
|
+
await processStop();
|
|
405
|
+
}
|
|
406
|
+
catch { /* stale PID or permission — fall through to port kill */ }
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
removePidFile();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Step 2: if port is still occupied (orphaned process / slow shutdown), force-kill by port
|
|
413
|
+
if (isPortListening(resolvedPort)) {
|
|
414
|
+
log(c.dim(` Port ${resolvedPort} still occupied — killing remaining process…`));
|
|
415
|
+
if (killPortProcess(resolvedPort)) {
|
|
416
|
+
await waitForPortFree(resolvedPort, 5000);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Step 3: hard guard — must have a free port before starting
|
|
420
|
+
if (isPortListening(resolvedPort)) {
|
|
421
|
+
throw new Error(`Port ${resolvedPort} is still occupied after stop. Try \`jishushell stop\` first.`);
|
|
422
|
+
}
|
|
423
|
+
log(c.green("✓ stopped"));
|
|
424
|
+
log(c.dim(" Starting…"));
|
|
425
|
+
await startPanel(resolvedPort);
|
|
426
|
+
}
|
|
427
|
+
function row(item) {
|
|
428
|
+
const icon = item.ok
|
|
429
|
+
? c.green("✓")
|
|
430
|
+
: item.warn
|
|
431
|
+
? c.yellow("!")
|
|
432
|
+
: c.red("✗");
|
|
433
|
+
const label = item.ok
|
|
434
|
+
? c.green(item.label.padEnd(22))
|
|
435
|
+
: item.warn
|
|
436
|
+
? c.yellow(item.label.padEnd(22))
|
|
437
|
+
: c.red(item.label.padEnd(22));
|
|
438
|
+
return ` ${icon} ${label} ${c.dim(item.detail)}`;
|
|
439
|
+
}
|
|
440
|
+
// ── Password prompt helper (no echo) ─────────────────────────────────────
|
|
441
|
+
function readPassword(prompt) {
|
|
442
|
+
return new Promise((resolve, reject) => {
|
|
443
|
+
process.stdout.write(prompt);
|
|
444
|
+
if (process.stdin.isTTY) {
|
|
445
|
+
process.stdin.setRawMode(true);
|
|
446
|
+
process.stdin.resume();
|
|
447
|
+
let input = "";
|
|
448
|
+
const onData = (buf) => {
|
|
449
|
+
const char = buf.toString("utf-8");
|
|
450
|
+
if (char === "\r" || char === "\n") {
|
|
451
|
+
process.stdin.setRawMode(false);
|
|
452
|
+
process.stdin.pause();
|
|
453
|
+
process.stdin.off("data", onData);
|
|
454
|
+
process.stdout.write("\n");
|
|
455
|
+
resolve(input);
|
|
456
|
+
}
|
|
457
|
+
else if (char === "\u0003") { // Ctrl+C
|
|
458
|
+
process.stdin.setRawMode(false);
|
|
459
|
+
process.stdout.write("\n");
|
|
460
|
+
reject(new Error("Interrupted"));
|
|
461
|
+
}
|
|
462
|
+
else if (char === "\u007f" || char === "\b") { // Backspace
|
|
463
|
+
if (input.length > 0)
|
|
464
|
+
input = input.slice(0, -1);
|
|
465
|
+
}
|
|
466
|
+
else if (char >= " ") {
|
|
467
|
+
input += char;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
process.stdin.on("data", onData);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// Non-TTY (piped): read one line without echo concern
|
|
474
|
+
let input = "";
|
|
475
|
+
process.stdin.resume();
|
|
476
|
+
process.stdin.setEncoding("utf-8");
|
|
477
|
+
const onData = (chunk) => {
|
|
478
|
+
const nl = chunk.indexOf("\n");
|
|
479
|
+
if (nl >= 0) {
|
|
480
|
+
input += chunk.slice(0, nl);
|
|
481
|
+
process.stdin.off("data", onData);
|
|
482
|
+
process.stdin.pause();
|
|
483
|
+
resolve(input.replace(/\r$/, "").trim());
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
input += chunk;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
process.stdin.on("data", onData);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
// ── Local IP helper ────────────────────────────────────────────────────────
|
|
494
|
+
function getLocalIP() {
|
|
495
|
+
try {
|
|
496
|
+
for (const list of Object.values(networkInterfaces())) {
|
|
497
|
+
for (const iface of list ?? []) {
|
|
498
|
+
if (!iface.internal && iface.family === "IPv4")
|
|
499
|
+
return iface.address;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch { }
|
|
504
|
+
return "127.0.0.1";
|
|
505
|
+
}
|
|
506
|
+
// ── onboard ────────────────────────────────────────────────────────────────
|
|
507
|
+
export async function runOnboard(opts = {}) {
|
|
508
|
+
log("");
|
|
509
|
+
log(c.bold(c.cyan(" JishuShell Onboard")));
|
|
510
|
+
log(c.dim(" ─────────────────────────────────────────"));
|
|
511
|
+
log("");
|
|
512
|
+
// 1. Require install to have been completed
|
|
513
|
+
const cfg = getPanelConfig();
|
|
514
|
+
if (!cfg.service_manager) {
|
|
515
|
+
log(c.red(" ✗ JishuShell is not installed yet."));
|
|
516
|
+
log(c.dim(" Run ") + c.bold("jishushell install") + c.dim(" first."));
|
|
517
|
+
log("");
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
// 2. Require panel to be running
|
|
521
|
+
const port = getPanelPort();
|
|
522
|
+
if (!isPortListening(port)) {
|
|
523
|
+
log(c.yellow(` ! Panel is not running on port ${port}.`));
|
|
524
|
+
log(c.dim(" Start it with: ") + c.bold("jishushell start"));
|
|
525
|
+
log("");
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
// 3. Already initialized?
|
|
529
|
+
const initialized = isInitialized();
|
|
530
|
+
if (initialized && !opts.force) {
|
|
531
|
+
const ip = getLocalIP();
|
|
532
|
+
log(c.green(" ✓ JishuShell is already onboarded."));
|
|
533
|
+
log("");
|
|
534
|
+
log(` Panel: ${c.bold(c.cyan(`http://${ip}:${port}`))}`);
|
|
535
|
+
log(` Local: ${c.bold(c.cyan(`http://localhost:${port}`))}`);
|
|
536
|
+
log("");
|
|
537
|
+
log(c.dim(" Tip: ") + c.bold("jishushell onboard --force") + c.dim(" resets the admin password."));
|
|
538
|
+
log("");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (initialized) {
|
|
542
|
+
log(c.yellow(" ! Resetting admin password (--force)."));
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
log(" " + c.bold("Welcome!") + " Set your admin password to start using JishuShell.");
|
|
546
|
+
}
|
|
547
|
+
log("");
|
|
548
|
+
// 4. Password prompt loop
|
|
549
|
+
let password = "";
|
|
550
|
+
for (;;) {
|
|
551
|
+
try {
|
|
552
|
+
password = await readPassword(" Password (≥8 chars): ");
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
log(c.yellow(" Interrupted."));
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
if (password.length < 8) {
|
|
559
|
+
log(c.red(" ✗ Minimum 8 characters. Try again."));
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
let confirm = "";
|
|
563
|
+
try {
|
|
564
|
+
confirm = await readPassword(" Confirm password: ");
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
log(c.yellow(" Interrupted."));
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
if (password !== confirm) {
|
|
571
|
+
log(c.red(" ✗ Passwords do not match. Try again."));
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
// 5. Persist
|
|
577
|
+
await initPassword(password);
|
|
578
|
+
// 6. Success
|
|
579
|
+
const ip = getLocalIP();
|
|
580
|
+
log("");
|
|
581
|
+
log(c.bold(c.green(" ✓ Onboarding complete!")));
|
|
582
|
+
log("");
|
|
583
|
+
log(` Open your browser: ${c.bold(c.cyan(`http://${ip}:${port}`))}`);
|
|
584
|
+
log(` Or locally: ${c.bold(c.cyan(`http://localhost:${port}`))}`);
|
|
585
|
+
log("");
|
|
586
|
+
log(c.dim(" Log in with your password, then configure your LLM provider in Settings."));
|
|
587
|
+
log("");
|
|
588
|
+
}
|
|
589
|
+
// ── doctor(委托 src/doctor.ts)──────────────────────────────────────────────────────────────
|
|
590
|
+
export async function runDoctor(opts = {}) {
|
|
591
|
+
return runDoctorChecks(opts);
|
|
592
|
+
}
|
|
593
|
+
// ── Nomad API helpers (CLI-side, no server dependency) ─────────────────────
|
|
594
|
+
function nomadHeaders() {
|
|
595
|
+
const token = getNomadToken();
|
|
596
|
+
return token ? { "X-Nomad-Token": token } : {};
|
|
597
|
+
}
|
|
598
|
+
async function nomadFetch(path) {
|
|
599
|
+
const addr = getNomadAddr();
|
|
600
|
+
try {
|
|
601
|
+
const res = await fetch(`${addr}${path}`, {
|
|
602
|
+
headers: nomadHeaders(),
|
|
603
|
+
signal: AbortSignal.timeout(5000),
|
|
604
|
+
});
|
|
605
|
+
if (!res.ok)
|
|
606
|
+
return null;
|
|
607
|
+
return res.json();
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function nomadDelete(path) {
|
|
614
|
+
const addr = getNomadAddr();
|
|
615
|
+
try {
|
|
616
|
+
const res = await fetch(`${addr}${path}`, {
|
|
617
|
+
method: "DELETE",
|
|
618
|
+
headers: nomadHeaders(),
|
|
619
|
+
signal: AbortSignal.timeout(10000),
|
|
620
|
+
});
|
|
621
|
+
return res.ok;
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function listInstanceIds() {
|
|
628
|
+
if (!existsSync(INSTANCES_DIR))
|
|
629
|
+
return [];
|
|
630
|
+
try {
|
|
631
|
+
return readdirSync(INSTANCES_DIR).filter(name => {
|
|
632
|
+
try {
|
|
633
|
+
return statSync(join(INSTANCES_DIR, name)).isDirectory();
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function readInstanceMeta(id) {
|
|
645
|
+
const metaPath = join(INSTANCES_DIR, id, "instance.json");
|
|
646
|
+
try {
|
|
647
|
+
if (existsSync(metaPath))
|
|
648
|
+
return JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
649
|
+
}
|
|
650
|
+
catch { }
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
// ── status ─────────────────────────────────────────────────────────────────
|
|
654
|
+
export async function showStatus() {
|
|
655
|
+
loadNomadToken();
|
|
656
|
+
const port = getPanelPort();
|
|
657
|
+
const mode = detectRunMode();
|
|
658
|
+
const nomadAddr = getNomadAddr();
|
|
659
|
+
log("");
|
|
660
|
+
log(c.bold(c.cyan(" JishuShell Status")));
|
|
661
|
+
log(c.dim(" ─────────────────────────────────────────────────────"));
|
|
662
|
+
log("");
|
|
663
|
+
// ── 1. Panel / CLI ──────────────────────────────────────────────────────
|
|
664
|
+
log(` ${c.bold("Panel (CLI)")}`);
|
|
665
|
+
const panelListening = isPortListening(port);
|
|
666
|
+
const pid = readAlivePid();
|
|
667
|
+
if (panelListening) {
|
|
668
|
+
log(` ${c.green("●")} ${c.green("running")} → port :${port}`);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
log(` ${c.red("○")} ${c.red("stopped")}`);
|
|
672
|
+
}
|
|
673
|
+
log(` ${c.dim("run mode:")} ${mode}`);
|
|
674
|
+
if (mode === "systemd") {
|
|
675
|
+
const svcStatus = systemdStatus();
|
|
676
|
+
log(` ${c.dim("systemd:")} ${svcStatus === "active" ? c.green(svcStatus) : c.yellow(svcStatus)}`);
|
|
677
|
+
}
|
|
678
|
+
else if (mode === "process" && pid !== null) {
|
|
679
|
+
log(` ${c.dim("pid:")} ${pid}`);
|
|
680
|
+
}
|
|
681
|
+
log(` ${c.dim("url:")} http://localhost:${port}`);
|
|
682
|
+
log("");
|
|
683
|
+
// ── 2. Docker ──────────────────────────────────────────────────────────
|
|
684
|
+
log(` ${c.bold("Docker")}`);
|
|
685
|
+
let dockerOk = false;
|
|
686
|
+
let dockerDetail = "daemon unreachable";
|
|
687
|
+
try {
|
|
688
|
+
execFileSync("docker", ["info"], { stdio: "ignore", timeout: 5000 });
|
|
689
|
+
const ver = execSync("docker --version 2>/dev/null", { encoding: "utf-8", timeout: 3000 }).trim();
|
|
690
|
+
dockerOk = true;
|
|
691
|
+
dockerDetail = ver.replace("Docker version ", "").split(",")[0];
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
try {
|
|
695
|
+
execFileSync("sudo", ["-n", "docker", "info"], { stdio: "ignore", timeout: 5000 });
|
|
696
|
+
dockerOk = true;
|
|
697
|
+
dockerDetail = "running (via sudo — re-login to activate docker group)";
|
|
698
|
+
}
|
|
699
|
+
catch { }
|
|
700
|
+
}
|
|
701
|
+
if (dockerOk) {
|
|
702
|
+
log(` ${c.green("●")} ${c.green("running")} ${c.dim(dockerDetail)}`);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
log(` ${c.red("○")} ${c.red("stopped")} ${c.dim(dockerDetail)}`);
|
|
706
|
+
}
|
|
707
|
+
log("");
|
|
708
|
+
// ── 3. Nomad ───────────────────────────────────────────────────────────
|
|
709
|
+
log(` ${c.bold("Nomad")}`);
|
|
710
|
+
const nomadAgent = await nomadFetch("/v1/agent/self");
|
|
711
|
+
const nomadRunning = nomadAgent !== null;
|
|
712
|
+
if (nomadRunning) {
|
|
713
|
+
const version = nomadAgent?.config?.Version?.Version || nomadAgent?.member?.Tags?.version || "";
|
|
714
|
+
log(` ${c.green("●")} ${c.green("running")} → ${nomadAddr}${version ? " " + c.dim("v" + version) : ""}`);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
log(` ${c.red("○")} ${c.red("stopped")} ${c.dim("(" + nomadAddr + " unreachable)")}`);
|
|
718
|
+
}
|
|
719
|
+
log("");
|
|
720
|
+
// ── 4. Nomad Jobs (instances) ───────────────────────────────────────────
|
|
721
|
+
const instanceIds = listInstanceIds();
|
|
722
|
+
log(` ${c.bold("Nomad Jobs")} ${c.dim("(" + instanceIds.length + " instance" + (instanceIds.length !== 1 ? "s" : "") + ")")}`);
|
|
723
|
+
if (instanceIds.length === 0) {
|
|
724
|
+
log(` ${c.dim("(no instances created)")}`);
|
|
725
|
+
}
|
|
726
|
+
else if (!nomadRunning) {
|
|
727
|
+
log(` ${c.dim("(Nomad not reachable — cannot query job status)")}`);
|
|
728
|
+
for (const id of instanceIds) {
|
|
729
|
+
const meta = readInstanceMeta(id);
|
|
730
|
+
log(` ${c.dim("??")} ${(meta?.name || id).padEnd(20)} ${c.dim(id)}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
for (const id of instanceIds) {
|
|
735
|
+
const meta = readInstanceMeta(id);
|
|
736
|
+
const jobId = `jishushell-${id}`;
|
|
737
|
+
const jobData = await nomadFetch(`/v1/job/${jobId}`);
|
|
738
|
+
const allocsData = await nomadFetch(`/v1/job/${jobId}/allocations`);
|
|
739
|
+
let statusStr = "unknown";
|
|
740
|
+
let statusColor = c.dim;
|
|
741
|
+
if (!jobData) {
|
|
742
|
+
statusStr = "not submitted";
|
|
743
|
+
statusColor = c.dim;
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
const jobStatus = jobData.Status || "";
|
|
747
|
+
const clientStatus = allocsData
|
|
748
|
+
?.slice()
|
|
749
|
+
.sort((a, b) => b.CreateTime - a.CreateTime)[0]
|
|
750
|
+
?.ClientStatus ?? "";
|
|
751
|
+
if (clientStatus === "running") {
|
|
752
|
+
statusStr = "running";
|
|
753
|
+
statusColor = c.green;
|
|
754
|
+
}
|
|
755
|
+
else if (clientStatus === "pending") {
|
|
756
|
+
statusStr = "pending";
|
|
757
|
+
statusColor = c.yellow;
|
|
758
|
+
}
|
|
759
|
+
else if (clientStatus === "failed") {
|
|
760
|
+
statusStr = "failed";
|
|
761
|
+
statusColor = c.red;
|
|
762
|
+
}
|
|
763
|
+
else if (clientStatus === "complete") {
|
|
764
|
+
statusStr = "complete";
|
|
765
|
+
statusColor = c.dim;
|
|
766
|
+
}
|
|
767
|
+
else if (jobStatus === "dead") {
|
|
768
|
+
statusStr = "stopped";
|
|
769
|
+
statusColor = c.dim;
|
|
770
|
+
}
|
|
771
|
+
else if (jobStatus === "running") {
|
|
772
|
+
statusStr = "pending alloc";
|
|
773
|
+
statusColor = c.yellow;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
statusStr = jobStatus || "unknown";
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Gateway port
|
|
780
|
+
let portInfo = "";
|
|
781
|
+
const runtime = meta?.runtime;
|
|
782
|
+
if (runtime) {
|
|
783
|
+
const envPort = runtime.env?.OPENCLAW_GATEWAY_PORT;
|
|
784
|
+
const argsPort = (() => {
|
|
785
|
+
const args = runtime.args || [];
|
|
786
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
787
|
+
if (args[i] === "--port")
|
|
788
|
+
return args[i + 1];
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
})();
|
|
792
|
+
const gp = envPort || argsPort;
|
|
793
|
+
if (gp)
|
|
794
|
+
portInfo = c.dim(` gateway :${gp}`);
|
|
795
|
+
}
|
|
796
|
+
const dot = statusStr === "running" ? c.green("●")
|
|
797
|
+
: statusStr === "pending" || statusStr === "pending alloc" ? c.yellow("●")
|
|
798
|
+
: statusStr === "failed" ? c.red("●")
|
|
799
|
+
: c.dim("○");
|
|
800
|
+
const nameField = (meta?.name || id).padEnd(20);
|
|
801
|
+
const statusField = statusColor(statusStr.padEnd(14));
|
|
802
|
+
log(` ${dot} ${nameField} ${statusField}${portInfo}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
log("");
|
|
806
|
+
}
|
|
807
|
+
// ── stopWithJobs ────────────────────────────────────────────────────────────
|
|
808
|
+
// Stop all Nomad jobs (instances) then stop the panel.
|
|
809
|
+
// Does NOT stop the Docker daemon or Nomad itself.
|
|
810
|
+
export async function stopWithJobs() {
|
|
811
|
+
loadNomadToken();
|
|
812
|
+
const instanceIds = listInstanceIds();
|
|
813
|
+
if (instanceIds.length > 0) {
|
|
814
|
+
log(c.dim(` Stopping ${instanceIds.length} Nomad job(s)…`));
|
|
815
|
+
const nomadAddr = getNomadAddr();
|
|
816
|
+
const nomadRunning = (await nomadFetch("/v1/agent/self")) !== null;
|
|
817
|
+
if (!nomadRunning) {
|
|
818
|
+
log(c.yellow(" ! Nomad is not reachable — skipping job cleanup"));
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
const results = await Promise.allSettled(instanceIds.map(async (id) => {
|
|
822
|
+
const jobId = `jishushell-${id}`;
|
|
823
|
+
const meta = readInstanceMeta(id);
|
|
824
|
+
const name = meta?.name || id;
|
|
825
|
+
const ok = await nomadDelete(`/v1/job/${jobId}?purge=false`);
|
|
826
|
+
if (ok) {
|
|
827
|
+
log(` ${c.green("✓")} stopped job: ${name}`);
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
log(` ${c.dim("–")} job not found or already stopped: ${name}`);
|
|
831
|
+
}
|
|
832
|
+
}));
|
|
833
|
+
void results; // errors already logged per-job
|
|
834
|
+
}
|
|
835
|
+
log("");
|
|
836
|
+
}
|
|
837
|
+
log(c.dim(" Stopping JishuShell panel…"));
|
|
838
|
+
await stopPanel();
|
|
839
|
+
}
|
|
840
|
+
// ── reset ───────────────────────────────────────────────────────────────────
|
|
841
|
+
// Purge all Nomad jobs and reset JishuShell to a clean configuration state.
|
|
842
|
+
export async function resetAll(opts = {}) {
|
|
843
|
+
loadNomadToken();
|
|
844
|
+
log("");
|
|
845
|
+
log(c.bold(c.yellow(" ⚠ JishuShell Reset")));
|
|
846
|
+
log(c.dim(" ─────────────────────────────────────────────────────"));
|
|
847
|
+
log("");
|
|
848
|
+
log(" This will:");
|
|
849
|
+
log(` ${c.dim("•")} Purge all Nomad jobs (stop & remove all instances from Nomad scheduler)`);
|
|
850
|
+
log(` ${c.dim("•")} Stop the JishuShell panel`);
|
|
851
|
+
log(` ${c.dim("•")} Delete panel configuration (panel.json)`);
|
|
852
|
+
log(` ${c.dim("•")} Delete auth credentials`);
|
|
853
|
+
log("");
|
|
854
|
+
log(` ${c.bold(c.yellow("Instance data directories are kept — this is not a data wipe."))}`);
|
|
855
|
+
log(` ${c.dim("Re-run")} ${c.bold("jishushell install")} ${c.dim("then")} ${c.bold("jishushell onboard")} ${c.dim("to restore the panel.")}`);
|
|
856
|
+
log("");
|
|
857
|
+
if (!opts.yes) {
|
|
858
|
+
if (!process.stdin.isTTY) {
|
|
859
|
+
log(c.red(" ✗ Reset requires interactive confirmation. Pass --yes to skip."));
|
|
860
|
+
log("");
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
const answer = await new Promise((resolve) => {
|
|
864
|
+
process.stdout.write(` Type ${c.bold("RESET")} to continue, or press Enter to cancel: `);
|
|
865
|
+
let buf = "";
|
|
866
|
+
const onData = (chunk) => {
|
|
867
|
+
const str = chunk.toString("utf-8");
|
|
868
|
+
const nl = str.indexOf("\n");
|
|
869
|
+
if (nl >= 0) {
|
|
870
|
+
buf += str.slice(0, nl);
|
|
871
|
+
process.stdin.off("data", onData);
|
|
872
|
+
process.stdin.pause();
|
|
873
|
+
process.stdout.write("\n");
|
|
874
|
+
resolve(buf.trim());
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
buf += str;
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
process.stdin.resume();
|
|
881
|
+
process.stdin.on("data", onData);
|
|
882
|
+
});
|
|
883
|
+
if (answer !== "RESET") {
|
|
884
|
+
log(c.dim(" Cancelled."));
|
|
885
|
+
log("");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
log("");
|
|
890
|
+
// 1. Purge all Nomad jobs
|
|
891
|
+
const instanceIds = listInstanceIds();
|
|
892
|
+
if (instanceIds.length > 0) {
|
|
893
|
+
log(c.dim(` Purging ${instanceIds.length} Nomad job(s)…`));
|
|
894
|
+
const nomadRunning = (await nomadFetch("/v1/agent/self")) !== null;
|
|
895
|
+
if (!nomadRunning) {
|
|
896
|
+
log(c.yellow(" ! Nomad unreachable — skipping job purge"));
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
for (const id of instanceIds) {
|
|
900
|
+
const jobId = `jishushell-${id}`;
|
|
901
|
+
const meta = readInstanceMeta(id);
|
|
902
|
+
const name = meta?.name || id;
|
|
903
|
+
const ok = await nomadDelete(`/v1/job/${jobId}?purge=true`);
|
|
904
|
+
if (ok) {
|
|
905
|
+
log(` ${c.green("✓")} purged: ${name}`);
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
log(` ${c.dim("–")} not found (already gone): ${name}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
// 2. Stop the panel
|
|
914
|
+
const port = getPanelPort();
|
|
915
|
+
if (isPortListening(port)) {
|
|
916
|
+
log(c.dim(" Stopping panel…"));
|
|
917
|
+
try {
|
|
918
|
+
await stopPanel();
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
// best-effort
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
// 3. Remove panel config and auth
|
|
925
|
+
const PANEL_CONFIG = join(JISHUSHELL_HOME, "panel.json");
|
|
926
|
+
const AUTH_FILE = join(JISHUSHELL_HOME, "auth.json");
|
|
927
|
+
const JWT_FILE = join(JISHUSHELL_HOME, "jwt-secret");
|
|
928
|
+
for (const f of [PANEL_CONFIG, AUTH_FILE, JWT_FILE]) {
|
|
929
|
+
try {
|
|
930
|
+
if (existsSync(f)) {
|
|
931
|
+
unlinkSync(f);
|
|
932
|
+
log(` ${c.green("✓")} removed: ${f}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
catch (e) {
|
|
936
|
+
log(` ${c.yellow("!")} could not remove ${f}: ${e.message}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
log("");
|
|
940
|
+
log(c.bold(c.green(" ✓ Reset complete.")));
|
|
941
|
+
log("");
|
|
942
|
+
log(` To reinstall: ${c.bold("jishushell install")}`);
|
|
943
|
+
log(` To onboard: ${c.bold("jishushell onboard")} ${c.dim("(after starting the panel)")}`);
|
|
944
|
+
log("");
|
|
945
|
+
}
|
|
946
|
+
async function _legacyDoctorBody() {
|
|
947
|
+
const items = [];
|
|
948
|
+
// ── 1. Node.js version ────────────────────────────────────────
|
|
949
|
+
const nodeMajor = parseInt(process.version.replace("v", "").split(".")[0], 10);
|
|
950
|
+
items.push({
|
|
951
|
+
label: "Node.js",
|
|
952
|
+
ok: nodeMajor >= 22,
|
|
953
|
+
warn: nodeMajor >= 18 && nodeMajor < 22,
|
|
954
|
+
detail: nodeMajor >= 22
|
|
955
|
+
? process.version
|
|
956
|
+
: `${process.version} — requires >=22 (run: nvm install 22)`,
|
|
957
|
+
});
|
|
958
|
+
// ── 2. Docker daemon ──────────────────────────────────────────
|
|
959
|
+
let dockerOk = false;
|
|
960
|
+
let dockerDetail = "not found";
|
|
961
|
+
try {
|
|
962
|
+
execFileSync("docker", ["info"], { stdio: "ignore", timeout: 6000 });
|
|
963
|
+
const ver = execSync("docker --version 2>/dev/null", { encoding: "utf-8", timeout: 3000 }).trim();
|
|
964
|
+
dockerOk = true;
|
|
965
|
+
dockerDetail = ver.replace("Docker version ", "").split(",")[0];
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
// docker group not active — try sudo
|
|
969
|
+
try {
|
|
970
|
+
execFileSync("sudo", ["-n", "docker", "info"], { stdio: "ignore", timeout: 6000 });
|
|
971
|
+
dockerOk = true;
|
|
972
|
+
dockerDetail = "running (accessible via sudo — re-login to activate docker group)";
|
|
973
|
+
}
|
|
974
|
+
catch {
|
|
975
|
+
dockerDetail = "daemon not reachable — run: sudo systemctl start docker";
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
items.push({ label: "Docker", ok: dockerOk, detail: dockerDetail });
|
|
979
|
+
// ── 3. Nomad ──────────────────────────────────────────────────
|
|
980
|
+
let nomadRunning = false;
|
|
981
|
+
let nomadDetail = "port 4646 not listening";
|
|
982
|
+
try {
|
|
983
|
+
const listening = execSync("ss -tlnp 2>/dev/null | grep ':4646 '", {
|
|
984
|
+
encoding: "utf-8", timeout: 3000,
|
|
985
|
+
}).trim().length > 0;
|
|
986
|
+
if (listening) {
|
|
987
|
+
nomadRunning = true;
|
|
988
|
+
// Quickly check token validity
|
|
989
|
+
const nomadAddr = process.env.NOMAD_ADDR || "http://127.0.0.1:4646";
|
|
990
|
+
const token = getNomadToken();
|
|
991
|
+
try {
|
|
992
|
+
const result = await httpGet(`${nomadAddr}/v1/agent/self`, 3000);
|
|
993
|
+
if (result.status === 200) {
|
|
994
|
+
nomadDetail = "running, ACL token valid";
|
|
995
|
+
}
|
|
996
|
+
else if (result.status === 403) {
|
|
997
|
+
nomadDetail = "running, but ACL token invalid/expired";
|
|
998
|
+
nomadRunning = false;
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
nomadDetail = `running (HTTP ${result.status})`;
|
|
1002
|
+
}
|
|
1003
|
+
void token; // suppress unused warning
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
nomadDetail = "running (could not verify ACL token)";
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
// ss failed, fall back
|
|
1012
|
+
try {
|
|
1013
|
+
const out = execSync(`netstat -tlnp 2>/dev/null | grep ':4646 '`, {
|
|
1014
|
+
encoding: "utf-8", timeout: 3000,
|
|
1015
|
+
});
|
|
1016
|
+
nomadRunning = out.trim().length > 0;
|
|
1017
|
+
if (nomadRunning)
|
|
1018
|
+
nomadDetail = "running";
|
|
1019
|
+
}
|
|
1020
|
+
catch { }
|
|
1021
|
+
}
|
|
1022
|
+
items.push({ label: "Nomad", ok: nomadRunning, detail: nomadDetail });
|
|
1023
|
+
// ── 4. OpenClaw npm package ───────────────────────────────────
|
|
1024
|
+
const oclawBin = join(JISHUSHELL_HOME, "packages/openclaw/bin/openclaw");
|
|
1025
|
+
const oclawPkg = join(JISHUSHELL_HOME, "packages/openclaw/lib/node_modules/openclaw/package.json");
|
|
1026
|
+
let oclawOk = false;
|
|
1027
|
+
let oclawDetail = `not found at ${oclawBin}`;
|
|
1028
|
+
if (existsSync(oclawBin)) {
|
|
1029
|
+
try {
|
|
1030
|
+
const pkg = JSON.parse(readFileSync(oclawPkg, "utf-8"));
|
|
1031
|
+
oclawOk = true;
|
|
1032
|
+
oclawDetail = `v${pkg.version}`;
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
oclawOk = true;
|
|
1036
|
+
oclawDetail = "installed (version unknown)";
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
items.push({ label: "OpenClaw pkg", ok: oclawOk, warn: true, detail: oclawDetail });
|
|
1040
|
+
// ── 5. OpenClaw Docker image ──────────────────────────────────
|
|
1041
|
+
const imageTag = getPanelConfig().openclaw_image || "openclaw";
|
|
1042
|
+
let imageOk = false;
|
|
1043
|
+
let imageDetail = `${imageTag} not found locally`;
|
|
1044
|
+
if (dockerOk) {
|
|
1045
|
+
try {
|
|
1046
|
+
execFileSync("docker", ["image", "inspect", imageTag], { stdio: "ignore", timeout: 5000 });
|
|
1047
|
+
imageOk = true;
|
|
1048
|
+
imageDetail = imageTag;
|
|
1049
|
+
}
|
|
1050
|
+
catch {
|
|
1051
|
+
try {
|
|
1052
|
+
// maybe using sudo docker
|
|
1053
|
+
execFileSync("sudo", ["-n", "docker", "image", "inspect", imageTag], { stdio: "ignore", timeout: 5000 });
|
|
1054
|
+
imageOk = true;
|
|
1055
|
+
imageDetail = imageTag;
|
|
1056
|
+
}
|
|
1057
|
+
catch { }
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
imageDetail = "Docker not accessible — cannot check";
|
|
1062
|
+
imageOk = false;
|
|
1063
|
+
}
|
|
1064
|
+
items.push({ label: "OpenClaw image", ok: imageOk, warn: true, detail: imageDetail });
|
|
1065
|
+
// ── 6. JishuShell panel ───────────────────────────────────────
|
|
1066
|
+
const port = getPanelPort();
|
|
1067
|
+
let panelRunning = false;
|
|
1068
|
+
let panelDetail = `not reachable on port ${port}`;
|
|
1069
|
+
try {
|
|
1070
|
+
const res = await httpGet(`http://localhost:${port}/api/auth/status`, 3000);
|
|
1071
|
+
panelRunning = res.status > 0;
|
|
1072
|
+
try {
|
|
1073
|
+
const body = JSON.parse(res.body);
|
|
1074
|
+
const initialized = body.initialized ?? false;
|
|
1075
|
+
panelDetail = initialized
|
|
1076
|
+
? `running on :${port} (configured)`
|
|
1077
|
+
: `running on :${port} — ${c.yellow("visit http://localhost:" + port + " to set up password")}`;
|
|
1078
|
+
}
|
|
1079
|
+
catch {
|
|
1080
|
+
panelDetail = `running on :${port} (HTTP ${res.status})`;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
catch {
|
|
1084
|
+
panelDetail = `not reachable on :${port}`;
|
|
1085
|
+
}
|
|
1086
|
+
items.push({ label: "Panel", ok: panelRunning, warn: true, detail: panelDetail });
|
|
1087
|
+
// ── 7. Run mode ───────────────────────────────────────────────
|
|
1088
|
+
const mode = detectRunMode();
|
|
1089
|
+
const modeDetails = {
|
|
1090
|
+
systemd: `systemd (${SYSTEMD_SERVICE})`,
|
|
1091
|
+
launchd: `launchd (${LAUNCHD_PLIST})`,
|
|
1092
|
+
process: `process mode (PID file: ${PID_FILE})`,
|
|
1093
|
+
};
|
|
1094
|
+
const pidAlive = readAlivePid();
|
|
1095
|
+
const modeDetail = mode === "process"
|
|
1096
|
+
? pidAlive ? `${modeDetails.process} — PID ${pidAlive}` : `${modeDetails.process} — no PID`
|
|
1097
|
+
: modeDetails[mode];
|
|
1098
|
+
items.push({ label: "Run mode", ok: true, warn: false, detail: modeDetail });
|
|
1099
|
+
// ── 8. panel.json config ──────────────────────────────────────
|
|
1100
|
+
let cfgOk = false;
|
|
1101
|
+
let cfgDetail = "~/.jishushell/panel.json not found";
|
|
1102
|
+
try {
|
|
1103
|
+
const cfg = getPanelConfig();
|
|
1104
|
+
cfgOk = Object.keys(cfg).length > 0;
|
|
1105
|
+
cfgDetail = cfgOk
|
|
1106
|
+
? `service_manager=${cfg.service_manager ?? "process"}, port=${cfg.panel_port ?? 8090}`
|
|
1107
|
+
: "empty or corrupted";
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
cfgDetail = "corrupted — run: jishushell install --force";
|
|
1111
|
+
}
|
|
1112
|
+
items.push({ label: "Config", ok: cfgOk, warn: !cfgOk, detail: cfgDetail });
|
|
1113
|
+
// ── Print results ─────────────────────────────────────────────
|
|
1114
|
+
log("");
|
|
1115
|
+
for (const item of items)
|
|
1116
|
+
log(row(item));
|
|
1117
|
+
log("");
|
|
1118
|
+
const failures = items.filter((i) => !i.ok && !i.warn);
|
|
1119
|
+
const warns = items.filter((i) => !i.ok && i.warn);
|
|
1120
|
+
if (failures.length === 0 && warns.length === 0) {
|
|
1121
|
+
log(c.bold(c.green(" ✓ All checks passed!")));
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
if (failures.length > 0) {
|
|
1125
|
+
log(c.bold(c.red(` ✗ ${failures.length} critical issue(s) detected`)));
|
|
1126
|
+
}
|
|
1127
|
+
if (warns.length > 0) {
|
|
1128
|
+
log(c.bold(c.yellow(` ! ${warns.length} warning(s)`)));
|
|
1129
|
+
}
|
|
1130
|
+
log("");
|
|
1131
|
+
log(c.dim(" Tip: run ") + c.bold("jishushell install") + c.dim(" to fix missing components"));
|
|
1132
|
+
}
|
|
1133
|
+
log("");
|
|
1134
|
+
return failures.length === 0;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Fetch the latest published version of jishushell from the npm registry and
|
|
1138
|
+
* compare it with the current package version.
|
|
1139
|
+
* Uses a 30-second TTL cache so repeated API calls in the same process are cheap.
|
|
1140
|
+
*/
|
|
1141
|
+
let _updateCache = null;
|
|
1142
|
+
const UPDATE_CACHE_TTL = 30_000;
|
|
1143
|
+
/** Resolve the npm registry URL from panel.json, npm config, or the default. */
|
|
1144
|
+
function getNpmRegistryUrl() {
|
|
1145
|
+
// 1. panel.json override
|
|
1146
|
+
const panelReg = getPanelConfig().npm_registry;
|
|
1147
|
+
if (typeof panelReg === "string" && panelReg.trim())
|
|
1148
|
+
return panelReg.trim().replace(/\/+$/, "");
|
|
1149
|
+
// 2. npm config (respects .npmrc)
|
|
1150
|
+
try {
|
|
1151
|
+
const nodeBinDir = dirname(process.execPath);
|
|
1152
|
+
const npmBin = join(nodeBinDir, "npm");
|
|
1153
|
+
const reg = execFileSync(npmBin, ["config", "get", "registry"], { encoding: "utf-8", timeout: 5000 }).trim();
|
|
1154
|
+
if (reg && reg !== "undefined")
|
|
1155
|
+
return reg.replace(/\/+$/, "");
|
|
1156
|
+
}
|
|
1157
|
+
catch { /* fall through */ }
|
|
1158
|
+
// 3. default
|
|
1159
|
+
return "https://registry.npmjs.org";
|
|
1160
|
+
}
|
|
1161
|
+
export async function checkUpdate() {
|
|
1162
|
+
if (_updateCache && Date.now() - _updateCache.ts < UPDATE_CACHE_TTL) {
|
|
1163
|
+
return _updateCache.info;
|
|
1164
|
+
}
|
|
1165
|
+
// Read current version from package.json next to this file
|
|
1166
|
+
let currentVersion = "0.0.0";
|
|
1167
|
+
try {
|
|
1168
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
1169
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
1170
|
+
currentVersion = JSON.parse(raw).version ?? "0.0.0";
|
|
1171
|
+
}
|
|
1172
|
+
catch { /* keep default */ }
|
|
1173
|
+
let latestVersion = currentVersion;
|
|
1174
|
+
let hasUpdate = false;
|
|
1175
|
+
try {
|
|
1176
|
+
const registryUrl = getNpmRegistryUrl();
|
|
1177
|
+
const metaUrl = new URL(`/jishushell/latest`, registryUrl);
|
|
1178
|
+
const transport = metaUrl.protocol === "https:" ? https : http;
|
|
1179
|
+
latestVersion = await new Promise((resolve, reject) => {
|
|
1180
|
+
const req = transport.get(metaUrl, { headers: { accept: "application/json" }, timeout: 8000 }, (res) => {
|
|
1181
|
+
let body = "";
|
|
1182
|
+
res.on("data", (d) => (body += d));
|
|
1183
|
+
res.on("end", () => {
|
|
1184
|
+
try {
|
|
1185
|
+
resolve(JSON.parse(body).version ?? currentVersion);
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
reject(new Error("parse error"));
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1192
|
+
req.on("error", reject);
|
|
1193
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
1194
|
+
});
|
|
1195
|
+
// Simple semver compare: split on "." and compare each part numerically
|
|
1196
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
1197
|
+
const [cMaj, cMin, cPat] = parse(currentVersion);
|
|
1198
|
+
const [lMaj, lMin, lPat] = parse(latestVersion);
|
|
1199
|
+
hasUpdate =
|
|
1200
|
+
lMaj > cMaj ||
|
|
1201
|
+
(lMaj === cMaj && lMin > cMin) ||
|
|
1202
|
+
(lMaj === cMaj && lMin === cMin && lPat > cPat);
|
|
1203
|
+
}
|
|
1204
|
+
catch { /* network unavailable — return no-update */ }
|
|
1205
|
+
const info = { hasUpdate, currentVersion, latestVersion };
|
|
1206
|
+
_updateCache = { ts: Date.now(), info };
|
|
1207
|
+
// Piggyback telemetry on update checks (fire-and-forget, deduplicated internally)
|
|
1208
|
+
checkAndReport();
|
|
1209
|
+
checkAndReportHeartbeat();
|
|
1210
|
+
return info;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Install the latest version of jishushell via npm then restart the panel.
|
|
1214
|
+
* Resolves npm from the same bin directory as the running Node.js executable
|
|
1215
|
+
* so it works even when PATH does not include nvm's bin dir (e.g. systemd).
|
|
1216
|
+
*/
|
|
1217
|
+
export async function runUpdate() {
|
|
1218
|
+
log("");
|
|
1219
|
+
log(c.bold(" Updating JishuShell…"));
|
|
1220
|
+
const { currentVersion, latestVersion } = await checkUpdate();
|
|
1221
|
+
log(c.dim(` current: ${currentVersion} → latest: ${latestVersion}`));
|
|
1222
|
+
// Build childEnv FIRST — both npm and the restart command need node in PATH.
|
|
1223
|
+
// nvm-managed node lives in a user-specific bin dir that systemd strips from PATH.
|
|
1224
|
+
const nodeBinDir = dirname(process.execPath);
|
|
1225
|
+
const childEnv = {
|
|
1226
|
+
...process.env,
|
|
1227
|
+
PATH: `${nodeBinDir}${process.env.PATH ? `:${process.env.PATH}` : ""}`,
|
|
1228
|
+
};
|
|
1229
|
+
const npmBin = join(nodeBinDir, "npm");
|
|
1230
|
+
log(c.dim(` Running: ${npmBin} install -g jishushell …`));
|
|
1231
|
+
try {
|
|
1232
|
+
const out = execFileSync(npmBin, ["install", "-g", "jishushell", "--registry", getNpmRegistryUrl() + "/"], { encoding: "utf-8", timeout: 120_000, stdio: ["ignore", "pipe", "pipe"], env: childEnv });
|
|
1233
|
+
if (out)
|
|
1234
|
+
log(c.dim(out.trim()));
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
const detail = err.stderr ? err.stderr.trim() : err.message;
|
|
1238
|
+
log(c.red(` ✗ npm install failed: ${detail}`));
|
|
1239
|
+
throw new Error(detail || err.message);
|
|
1240
|
+
}
|
|
1241
|
+
log(c.green(" ✓ Package updated."));
|
|
1242
|
+
// Invalidate version cache
|
|
1243
|
+
_updateCache = null;
|
|
1244
|
+
// kills this process. Using `sleep 2` gives ~2 s for the reply to flush.
|
|
1245
|
+
// We invoke node directly (not jishushell shebang script) to avoid PATH issues.
|
|
1246
|
+
log(c.dim(" Scheduling panel restart in 2 s…"));
|
|
1247
|
+
const restartChild = spawn("sh", ["-c", `sleep 2 && exec ${JSON.stringify(process.execPath)} ${JSON.stringify(CLI_PATH)} restart`], { detached: true, stdio: "ignore", env: childEnv });
|
|
1248
|
+
restartChild.unref();
|
|
1249
|
+
log(c.green(" ✓ Restart scheduled — panel will reconnect shortly."));
|
|
1250
|
+
}
|
|
1251
|
+
// ── pairing helpers ─────────────────────────────────────────────────────────
|
|
1252
|
+
// These functions run `openclaw pairing` commands inside the instance container
|
|
1253
|
+
// via `docker exec`, so users don't need to do `docker exec` manually.
|
|
1254
|
+
async function resolvePairingInstance(instanceId) {
|
|
1255
|
+
loadNomadToken();
|
|
1256
|
+
const ids = listInstanceIds();
|
|
1257
|
+
if (ids.length === 0) {
|
|
1258
|
+
log(c.red(" ✗ No instances found."));
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
// If --instance was given, validate it; otherwise auto-detect when exactly one instance is running.
|
|
1262
|
+
let targetId = instanceId ?? null;
|
|
1263
|
+
if (!targetId) {
|
|
1264
|
+
if (ids.length === 1) {
|
|
1265
|
+
targetId = ids[0];
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
// Find currently running instances via Nomad
|
|
1269
|
+
const runningIds = [];
|
|
1270
|
+
for (const id of ids) {
|
|
1271
|
+
try {
|
|
1272
|
+
const allocsData = await nomadFetch(`/v1/job/openclaw-${id}/allocations`);
|
|
1273
|
+
const running = allocsData?.some((a) => a.ClientStatus === "running");
|
|
1274
|
+
if (running)
|
|
1275
|
+
runningIds.push(id);
|
|
1276
|
+
}
|
|
1277
|
+
catch { /* skip */ }
|
|
1278
|
+
}
|
|
1279
|
+
if (runningIds.length === 1) {
|
|
1280
|
+
targetId = runningIds[0];
|
|
1281
|
+
}
|
|
1282
|
+
else if (runningIds.length === 0) {
|
|
1283
|
+
log(c.red(" ✗ No running instances found. Start an instance first."));
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
log(c.red(` ✗ Multiple running instances: ${runningIds.join(", ")}`));
|
|
1288
|
+
log(c.dim(" Use --instance <id> to specify one."));
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else if (!ids.includes(targetId)) {
|
|
1294
|
+
log(c.red(` ✗ Instance "${targetId}" not found.`));
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
// Get alloc ID from Nomad to build container name
|
|
1298
|
+
let containerName = null;
|
|
1299
|
+
try {
|
|
1300
|
+
const allocsData = await nomadFetch(`/v1/job/openclaw-${targetId}/allocations`);
|
|
1301
|
+
const runningAlloc = allocsData
|
|
1302
|
+
?.filter((a) => a.ClientStatus === "running")
|
|
1303
|
+
.sort((a, b) => b.CreateTime - a.CreateTime)[0];
|
|
1304
|
+
if (runningAlloc?.ID) {
|
|
1305
|
+
containerName = `gateway-${runningAlloc.ID}`;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
catch { /* fall through */ }
|
|
1309
|
+
if (!containerName) {
|
|
1310
|
+
log(c.red(` ✗ Instance "${targetId}" is not running.`));
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
return { id: targetId, containerName };
|
|
1314
|
+
}
|
|
1315
|
+
export async function runPairingList(opts = {}) {
|
|
1316
|
+
const target = await resolvePairingInstance(opts.instanceId);
|
|
1317
|
+
if (!target)
|
|
1318
|
+
return false;
|
|
1319
|
+
log("");
|
|
1320
|
+
log(c.bold(c.cyan(` Pairing requests for instance: ${target.id}`)));
|
|
1321
|
+
log(c.dim(" ─────────────────────────────────────────────────────"));
|
|
1322
|
+
const { execFileSync: efs } = await import("child_process");
|
|
1323
|
+
try {
|
|
1324
|
+
const out = efs("docker", ["exec", target.containerName, "openclaw", "pairing", "list"], {
|
|
1325
|
+
encoding: "utf-8", timeout: 15_000,
|
|
1326
|
+
});
|
|
1327
|
+
log(out.trim() ? out.trim() : " (no pending requests)");
|
|
1328
|
+
}
|
|
1329
|
+
catch (e) {
|
|
1330
|
+
const msg = (e.stdout || "") + (e.stderr || e.message || "");
|
|
1331
|
+
log(msg.trim() || c.red(" ✗ Failed to list pairing requests."));
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
log("");
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
export async function runPairingApprove(opts) {
|
|
1338
|
+
const target = await resolvePairingInstance(opts.instanceId);
|
|
1339
|
+
if (!target)
|
|
1340
|
+
return false;
|
|
1341
|
+
const cmd = ["exec", target.containerName, "openclaw", "pairing", "approve", opts.channel, opts.code];
|
|
1342
|
+
if (opts.notify)
|
|
1343
|
+
cmd.push("--notify");
|
|
1344
|
+
log("");
|
|
1345
|
+
log(c.bold(` Approving pairing code ${c.cyan(opts.code)} on channel ${c.cyan(opts.channel)}…`));
|
|
1346
|
+
const { execFileSync: efs } = await import("child_process");
|
|
1347
|
+
try {
|
|
1348
|
+
const out = efs("docker", cmd, { encoding: "utf-8", timeout: 15_000 });
|
|
1349
|
+
log(c.green(` ✓ ${out.trim()}`));
|
|
1350
|
+
}
|
|
1351
|
+
catch (e) {
|
|
1352
|
+
const msg = ((e.stdout || "") + (e.stderr || e.message || "")).trim();
|
|
1353
|
+
log(c.red(` ✗ ${msg || "Approval failed"}`));
|
|
1354
|
+
return false;
|
|
1355
|
+
}
|
|
1356
|
+
log("");
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1359
|
+
//# sourceMappingURL=control.js.map
|