jishushell 0.4.2 → 0.4.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/Dockerfile.openclaw-slim +58 -0
  2. package/INSTALL-NOTICE +45 -0
  3. package/dist/auth.js +3 -3
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli/app.d.ts +3 -0
  6. package/dist/cli/app.js +156 -0
  7. package/dist/cli/app.js.map +1 -0
  8. package/dist/{doctor.d.ts → cli/doctor.d.ts} +6 -1
  9. package/dist/{doctor.js → cli/doctor.js} +389 -27
  10. package/dist/cli/doctor.js.map +1 -0
  11. package/dist/cli/helpers.d.ts +4 -0
  12. package/dist/cli/helpers.js +32 -0
  13. package/dist/cli/helpers.js.map +1 -0
  14. package/dist/cli/job.d.ts +3 -0
  15. package/dist/cli/job.js +260 -0
  16. package/dist/cli/job.js.map +1 -0
  17. package/dist/cli/llm.d.ts +24 -0
  18. package/dist/cli/llm.js +593 -0
  19. package/dist/cli/llm.js.map +1 -0
  20. package/dist/cli/openclaw.d.ts +12 -0
  21. package/dist/cli/openclaw.js +156 -0
  22. package/dist/cli/openclaw.js.map +1 -0
  23. package/dist/cli/panel.d.ts +25 -0
  24. package/dist/cli/panel.js +734 -0
  25. package/dist/cli/panel.js.map +1 -0
  26. package/dist/cli.js +476 -219
  27. package/dist/cli.js.map +1 -1
  28. package/dist/config.d.ts +22 -4
  29. package/dist/config.js +96 -55
  30. package/dist/config.js.map +1 -1
  31. package/dist/control.d.ts +13 -41
  32. package/dist/control.js +12 -1355
  33. package/dist/control.js.map +1 -1
  34. package/dist/install.d.ts +1 -1
  35. package/dist/install.js +15 -29
  36. package/dist/install.js.map +1 -1
  37. package/dist/routes/apps.d.ts +3 -0
  38. package/dist/routes/apps.js +99 -0
  39. package/dist/routes/apps.js.map +1 -0
  40. package/dist/routes/backup.d.ts +2 -0
  41. package/dist/routes/backup.js +370 -0
  42. package/dist/routes/backup.js.map +1 -0
  43. package/dist/routes/instances.d.ts +1 -0
  44. package/dist/routes/instances.js +61 -15
  45. package/dist/routes/instances.js.map +1 -1
  46. package/dist/routes/llm.d.ts +15 -0
  47. package/dist/routes/llm.js +246 -0
  48. package/dist/routes/llm.js.map +1 -0
  49. package/dist/routes/setup.js +32 -7
  50. package/dist/routes/setup.js.map +1 -1
  51. package/dist/routes/system.js +31 -6
  52. package/dist/routes/system.js.map +1 -1
  53. package/dist/server.js +69 -5
  54. package/dist/server.js.map +1 -1
  55. package/dist/services/app-compiler.d.ts +15 -0
  56. package/dist/services/app-compiler.js +169 -0
  57. package/dist/services/app-compiler.js.map +1 -0
  58. package/dist/services/app-manager.d.ts +17 -0
  59. package/dist/services/app-manager.js +168 -0
  60. package/dist/services/app-manager.js.map +1 -0
  61. package/dist/services/backup-manager.d.ts +253 -0
  62. package/dist/services/backup-manager.js +2014 -0
  63. package/dist/services/backup-manager.js.map +1 -0
  64. package/dist/services/backup-verify.d.ts +26 -0
  65. package/dist/services/backup-verify.js +240 -0
  66. package/dist/services/backup-verify.js.map +1 -0
  67. package/dist/services/instance-manager.d.ts +73 -5
  68. package/dist/services/instance-manager.js +446 -74
  69. package/dist/services/instance-manager.js.map +1 -1
  70. package/dist/services/job-manager.d.ts +22 -0
  71. package/dist/services/job-manager.js +102 -0
  72. package/dist/services/job-manager.js.map +1 -0
  73. package/dist/services/llm-proxy/adapters.js +5 -1
  74. package/dist/services/llm-proxy/adapters.js.map +1 -1
  75. package/dist/services/llm-proxy/index.d.ts +30 -0
  76. package/dist/services/llm-proxy/index.js +71 -1
  77. package/dist/services/llm-proxy/index.js.map +1 -1
  78. package/dist/services/llm-proxy/ssrf.js +1 -1
  79. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  80. package/dist/services/nomad-manager.js +263 -159
  81. package/dist/services/nomad-manager.js.map +1 -1
  82. package/dist/services/panel-manager.d.ts +40 -0
  83. package/dist/services/panel-manager.js +346 -0
  84. package/dist/services/panel-manager.js.map +1 -0
  85. package/dist/services/process-manager.js +24 -10
  86. package/dist/services/process-manager.js.map +1 -1
  87. package/dist/services/setup-manager.d.ts +4 -2
  88. package/dist/services/setup-manager.js +578 -154
  89. package/dist/services/setup-manager.js.map +1 -1
  90. package/dist/services/telemetry/activation.js +10 -7
  91. package/dist/services/telemetry/activation.js.map +1 -1
  92. package/dist/services/telemetry/client.js +7 -18
  93. package/dist/services/telemetry/client.js.map +1 -1
  94. package/dist/services/telemetry/heartbeat.js +12 -6
  95. package/dist/services/telemetry/heartbeat.js.map +1 -1
  96. package/dist/services/update-manager.d.ts +47 -0
  97. package/dist/services/update-manager.js +305 -0
  98. package/dist/services/update-manager.js.map +1 -0
  99. package/dist/types.d.ts +62 -0
  100. package/dist/utils/fs.d.ts +85 -0
  101. package/dist/utils/fs.js +111 -0
  102. package/dist/utils/fs.js.map +1 -0
  103. package/dist/utils/safe-json.d.ts +2 -0
  104. package/dist/utils/safe-json.js +22 -16
  105. package/dist/utils/safe-json.js.map +1 -1
  106. package/install/jishu-install.sh +582 -138
  107. package/install/jishu-uninstall.sh +276 -391
  108. package/install/post-install.sh +85 -3
  109. package/openclaw-entry.sh +15 -0
  110. package/package.json +12 -5
  111. package/public/assets/Dashboard-CQsp1Mr9.js +1 -0
  112. package/public/assets/InitPassword-BEC8SE4A.js +1 -0
  113. package/public/assets/InstanceDetail-B5wTgNEg.js +17 -0
  114. package/public/assets/{Login-RkjzTNWg.js → Login-D1Bt-Lyk.js} +1 -1
  115. package/public/assets/NewInstance-GQzm3K9D.js +1 -0
  116. package/public/assets/Settings-ByjGlqhP.js +1 -0
  117. package/public/assets/Setup-cMF21Y-8.js +1 -0
  118. package/public/assets/index-B6qQP4mH.css +1 -0
  119. package/public/assets/index-BuTQtuNy.js +16 -0
  120. package/public/assets/logo-black-theme-DywLAtFy.png +0 -0
  121. package/public/assets/logo-white-theme-DXffFAWw.png +0 -0
  122. package/public/assets/{providers-lBSOjUWy.js → providers-V-vwrExZ.js} +1 -1
  123. package/public/assets/{usePolling-CqQ8hrNc.js → usePolling-CK0DfI4h.js} +1 -1
  124. package/public/assets/{vendor-i18n-Bvxxh8Di.js → vendor-i18n-CfW0RvgE.js} +1 -1
  125. package/public/assets/vendor-react-B1-3Yrt-.js +59 -0
  126. package/public/index.html +4 -4
  127. package/dist/doctor.js.map +0 -1
  128. package/public/assets/Dashboard-CAOQDYDR.js +0 -1
  129. package/public/assets/InitPassword-CkehIkJG.js +0 -1
  130. package/public/assets/InstanceDetail-CzW2S95J.js +0 -14
  131. package/public/assets/NewInstance-DdbErdjA.js +0 -1
  132. package/public/assets/Settings-BUD7zwv9.js +0 -1
  133. package/public/assets/Setup-RRTIERGG.js +0 -1
  134. package/public/assets/index-77Ug7feY.css +0 -1
  135. package/public/assets/index-DfRnVUQR.js +0 -16
  136. package/public/assets/vendor-react-DONn7uBV.js +0 -59
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  if (process.platform === "darwin") {
3
- const extra = ["/usr/local/bin", "/opt/homebrew/bin", "/opt/homebrew/sbin", "/Applications/Docker.app/Contents/Resources/bin"];
3
+ const extra = ["/usr/local/bin", "/opt/homebrew/bin", "/opt/homebrew/sbin"];
4
4
  const current = process.env.PATH ?? "";
5
5
  const missing = extra.filter(p => !current.split(":").includes(p));
6
6
  if (missing.length > 0)
@@ -10,12 +10,17 @@ import { existsSync } from "fs";
10
10
  import { join } from "path";
11
11
  import { homedir } from "os";
12
12
  import { exec } from "child_process";
13
- import { createServer } from "./server.js";
14
- import { runInstall, parseInstallArgs } from "./install.js";
15
- import { startPanel, stopPanel, stopWithJobs, restartPanel, runDoctor, runOnboard, showStatus, resetAll, checkUpdate, runUpdate } from "./control.js";
16
- const JISHUSHELL_HOME = process.env.JISHUSHELL_HOME || join(homedir(), ".jishushell");
17
- const NOMAD_BIN = join(JISHUSHELL_HOME, "bin", "nomad");
18
- const PANEL_PORT = 8090;
13
+ if (process.platform === "darwin") {
14
+ const jHome = process.env.JISHUSHELL_HOME || join(homedir(), ".jishushell");
15
+ const sock = join(jHome, "colima", "jishushell", "docker.sock");
16
+ if (existsSync(sock))
17
+ process.env.DOCKER_HOST = `unix://${sock}`;
18
+ }
19
+ const [subcommand, ...rest] = process.argv.slice(2);
20
+ function cliError(err) {
21
+ console.error(err.message || err);
22
+ process.exit(1);
23
+ }
19
24
  function openBrowser(url) {
20
25
  const platform = process.platform;
21
26
  const cmd = platform === "darwin" ? `open "${url}"`
@@ -23,265 +28,517 @@ function openBrowser(url) {
23
28
  : `xdg-open "${url}"`;
24
29
  exec(cmd, () => { });
25
30
  }
26
- const [subcommand, ...rest] = process.argv.slice(2);
27
- // ── Helper to parse --port from remaining args ─────────────────────────────
28
- function parsePort(args) {
29
- for (let i = 0; i < args.length; i++) {
30
- if (args[i] === "--port" && args[i + 1]) {
31
- const p = parseInt(args[i + 1], 10);
32
- if (!isNaN(p) && p > 0 && p < 65536)
33
- return p;
34
- }
35
- }
36
- return undefined;
37
- }
38
- // ── install subcommand ─────────────────────────────────────────────────────
39
- if (subcommand === "install") {
40
- const opts = parseInstallArgs(rest);
41
- runInstall(opts).catch((err) => {
42
- console.error("Installation failed:", err);
43
- process.exit(1);
31
+ // ── HTTP thin-client helpers (used by backup/restore/export/import commands) ──
32
+ async function apiCall(path, opts = {}) {
33
+ const port = process.env.JISHUSHELL_PORT || "8090";
34
+ const url = `http://127.0.0.1:${port}/api${path}`;
35
+ const res = await fetch(url, {
36
+ method: opts.method || "GET",
37
+ headers: opts.body ? { "Content-Type": "application/json" } : {},
38
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
44
39
  });
45
- // runInstall is async and calls process.exit() itself
46
- // ── start ──────────────────────────────────────────────────────────────────
47
- }
48
- else if (subcommand === "start") {
49
- startPanel(parsePort(rest)).then(() => process.exit(0)).catch((err) => {
50
- console.error("start failed:", err.message);
51
- process.exit(1);
52
- });
53
- // ── stop ───────────────────────────────────────────────────────────────────
54
- }
55
- else if (subcommand === "stop") {
56
- // --jobs: also stop (but not purge) all Nomad jobs before stopping the panel
57
- const withJobs = rest.includes("--jobs") || rest.includes("-j");
58
- const stopFn = withJobs ? stopWithJobs : stopPanel;
59
- stopFn().then(() => process.exit(0)).catch((err) => {
60
- console.error("stop failed:", err.message);
61
- process.exit(1);
62
- });
63
- // ── restart ────────────────────────────────────────────────────────────────
40
+ if (!res.ok) {
41
+ const err = await res.json().catch(() => ({ detail: res.statusText }));
42
+ throw new Error(err.detail || `HTTP ${res.status}`);
43
+ }
44
+ return res.json();
64
45
  }
65
- else if (subcommand === "restart") {
66
- restartPanel(parsePort(rest)).then(() => process.exit(0)).catch((err) => {
67
- console.error("restart failed:", err.message);
68
- process.exit(1);
69
- });
70
- // ── status ─────────────────────────────────────────────────────────────────
46
+ async function waitForJob(jobId) {
47
+ while (true) {
48
+ const job = await apiCall(`/backup/jobs/${jobId}`);
49
+ if (job.status === "completed")
50
+ return job;
51
+ if (job.status === "failed")
52
+ throw new Error(job.error || "Job failed");
53
+ process.stdout.write(`\r ${job.progress || job.status}...`);
54
+ await new Promise(r => setTimeout(r, 1500));
55
+ }
71
56
  }
72
- else if (subcommand === "status") {
73
- showStatus().then(() => process.exit(0)).catch((err) => {
74
- console.error("status failed:", err.message);
75
- process.exit(1);
76
- });
77
- // ── doctor ─────────────────────────────────────────────────────────────────
57
+ // ── Subcommand dispatch ────────────────────────────────────────────────────
58
+ if (["install", "uninstall", "start", "stop", "restart", "status", "serve", "onboard", "reset", "update-check", "update", "config"].includes(subcommand)) {
59
+ const { run } = await import("./cli/panel.js");
60
+ run([subcommand, ...rest]).catch(cliError);
78
61
  }
79
62
  else if (subcommand === "doctor") {
80
- const fix = rest.includes("--fix") || rest.includes("-f");
81
- runDoctor({ fix }).then((ok) => process.exit(ok ? 0 : 1)).catch((err) => {
82
- console.error("doctor failed:", err.message);
83
- process.exit(1);
84
- });
85
- // ── onboard ────────────────────────────────────────────────────────────────
63
+ const { run } = await import("./cli/doctor.js");
64
+ run(rest).catch(cliError);
86
65
  }
87
- else if (subcommand === "onboard") {
88
- const force = rest.includes("--force") || rest.includes("-f");
89
- runOnboard({ force }).then(() => process.exit(0)).catch((err) => {
90
- console.error("onboard failed:", err.message);
91
- process.exit(1);
92
- });
93
- // ── reset ──────────────────────────────────────────────────────────────────
66
+ else if (subcommand === "openclaw") {
67
+ const { run } = await import("./cli/openclaw.js");
68
+ run(rest).catch(cliError);
94
69
  }
95
- else if (subcommand === "reset") {
96
- const yes = rest.includes("--yes") || rest.includes("-y");
97
- resetAll({ yes }).then(() => process.exit(0)).catch((err) => {
98
- console.error("reset failed:", err.message);
99
- process.exit(1);
100
- });
101
- // ── update-check ───────────────────────────────────────────────────────────
70
+ else if (subcommand === "pairing") {
71
+ // Backward-compat alias: jishushell pairing ... → jishushell openclaw pairing ...
72
+ const { run } = await import("./cli/openclaw.js");
73
+ run(["pairing", ...rest]).catch(cliError);
102
74
  }
103
- else if (subcommand === "update-check") {
104
- checkUpdate().then((info) => {
105
- console.log(JSON.stringify(info));
106
- process.exit(info.hasUpdate ? 0 : 1);
107
- }).catch((err) => {
108
- console.error("update-check failed:", err.message);
109
- process.exit(2);
110
- });
111
- // ── update-run ─────────────────────────────────────────────────────────────
75
+ else if (subcommand === "job") {
76
+ const { run } = await import("./cli/job.js");
77
+ run(rest).catch(cliError);
112
78
  }
113
- else if (subcommand === "update") {
114
- runUpdate().then(() => process.exit(0)).catch((err) => {
115
- console.error("update-run failed:", err.message);
116
- process.exit(1);
117
- });
118
- // ── pairing: approve / list DM pairing requests inside running containers ──
79
+ else if (subcommand === "app") {
80
+ const { run } = await import("./cli/app.js");
81
+ run(rest).catch(cliError);
119
82
  }
120
- else if (subcommand === "pairing") {
121
- const action = rest[0];
122
- // Parse --instance <id> flag
123
- let instanceId;
124
- const instIdx = rest.indexOf("--instance");
125
- if (instIdx !== -1 && rest[instIdx + 1])
126
- instanceId = rest[instIdx + 1];
127
- if (action === "list") {
128
- const { runPairingList } = await import("./control.js");
129
- runPairingList({ instanceId }).then((ok) => process.exit(ok ? 0 : 1)).catch((err) => {
130
- console.error("pairing list failed:", err.message);
83
+ else if (subcommand === "llm") {
84
+ const { run } = await import("./cli/llm.js");
85
+ run(rest).catch(cliError);
86
+ // ── backup ─────────────────────────────────────────────────────────────────
87
+ }
88
+ else if (subcommand === "backup") {
89
+ // Handle "backup verify <instance-id> <filename>" subcommand
90
+ if (rest[0] === "verify") {
91
+ const instanceId = rest[1];
92
+ const filename = rest[2];
93
+ if (!instanceId || !filename) {
94
+ console.error("Usage: jishushell backup verify <instance-id> <filename>");
131
95
  process.exit(1);
132
- });
96
+ }
97
+ try {
98
+ const result = await apiCall(`/instances/${instanceId}/backup/verify/${encodeURIComponent(filename)}`, { method: "POST" });
99
+ console.log(result.valid ? "Verification PASSED." : "Verification FAILED.");
100
+ for (const check of result.checks || []) {
101
+ console.log(` ${check.passed ? "+" : "x"} ${check.name}${check.detail ? ": " + check.detail : ""}`);
102
+ }
103
+ if (rest.includes("--json"))
104
+ console.log(JSON.stringify(result, null, 2));
105
+ }
106
+ catch (e) {
107
+ console.error(`Verify failed: ${e.message}`);
108
+ process.exit(1);
109
+ }
110
+ process.exit(0);
133
111
  }
134
- else if (action === "approve") {
135
- // args after "approve": [channel, code] or [code] (single-arg form)
136
- const actionArgs = rest.slice(1).filter((a) => !a.startsWith("--") && a !== instanceId);
137
- let channel;
138
- let code;
139
- if (actionArgs.length >= 2) {
140
- [channel, code] = actionArgs;
112
+ const id = rest[0];
113
+ if (!id) {
114
+ console.error("Usage: jishushell backup <id> [--with-sessions] [--only-config] [--state-only] [--no-workspace]");
115
+ process.exit(1);
116
+ }
117
+ const withSessions = rest.includes("--with-sessions");
118
+ const onlyConfig = rest.includes("--only-config");
119
+ const stateOnly = rest.includes("--state-only");
120
+ const noWorkspace = rest.includes("--no-workspace");
121
+ try {
122
+ console.log(`Creating backup for ${id}...`);
123
+ const result = await apiCall(`/instances/${id}/backup`, {
124
+ method: "POST",
125
+ body: {
126
+ include_sessions: withSessions,
127
+ only_config: onlyConfig,
128
+ scope: stateOnly ? "state" : "home",
129
+ ...(noWorkspace ? { include_workspace: false } : {}),
130
+ },
131
+ });
132
+ if (result.job_id) {
133
+ const job = await waitForJob(result.job_id);
134
+ console.log(`\nBackup complete: ${job.result?.filename} (${job.result?.size} bytes)`);
141
135
  }
142
- else if (actionArgs.length === 1) {
143
- channel = "feishu";
144
- code = actionArgs[0];
136
+ if (rest.includes("--verify") && result.job_id) {
137
+ const job = await apiCall(`/backup/jobs/${result.job_id}`);
138
+ if (job.result?.filename) {
139
+ console.log("Verifying...");
140
+ const v = await apiCall(`/instances/${id}/backup/verify/${encodeURIComponent(job.result.filename)}`, { method: "POST" });
141
+ console.log(v.valid ? "Verification passed." : "Verification FAILED.");
142
+ }
143
+ }
144
+ }
145
+ catch (e) {
146
+ console.error(`Backup failed: ${e.message}`);
147
+ process.exit(1);
148
+ }
149
+ process.exit(0);
150
+ // ── restore ────────────────────────────────────────────────────────────────
151
+ }
152
+ else if (subcommand === "restore") {
153
+ if (rest.includes("--new")) {
154
+ const fromIdx = rest.indexOf("--from");
155
+ const idxNewId = rest.indexOf("--id");
156
+ if (fromIdx !== -1) {
157
+ const sourceId = rest[fromIdx + 1];
158
+ if (!sourceId) {
159
+ console.error("Usage: jishushell restore --new --from <src-id> --latest|--pick [--id <new-id>]");
160
+ process.exit(1);
161
+ }
162
+ let backups;
163
+ try {
164
+ backups = await apiCall(`/instances/${sourceId}/backups`);
165
+ }
166
+ catch (e) {
167
+ console.error(`Restore failed: ${e.message}`);
168
+ process.exit(1);
169
+ }
170
+ if (!backups.length) {
171
+ console.error("No backups found.");
172
+ process.exit(1);
173
+ }
174
+ let backupFile = "";
175
+ if (rest.includes("--latest")) {
176
+ backupFile = backups[0].filename;
177
+ }
178
+ else {
179
+ console.log("Available backups:");
180
+ backups.forEach((b, i) => {
181
+ console.log(` ${i + 1}. [${b.type}] ${new Date(b.created_at).toLocaleString()} (${(b.size / 1024 / 1024).toFixed(1)}MB)`);
182
+ });
183
+ const readline = await import("readline");
184
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
185
+ const answer = await new Promise(r => rl.question("Select number: ", r));
186
+ rl.close();
187
+ const idx = parseInt(answer, 10) - 1;
188
+ if (idx < 0 || idx >= backups.length) {
189
+ console.error("Invalid selection.");
190
+ process.exit(1);
191
+ }
192
+ backupFile = backups[idx].filename;
193
+ }
194
+ const newId = idxNewId !== -1 ? rest[idxNewId + 1] : `${sourceId}-copy`;
195
+ console.log(`Creating new instance ${newId} from ${sourceId}/${backupFile}...`);
196
+ try {
197
+ const result = await apiCall("/instances/create-from-backup", { method: "POST", body: { source_id: sourceId, backup_file: backupFile, new_id: newId, new_name: newId } });
198
+ if (result.ok) {
199
+ console.log(`Instance created: ${result.instance_id}`);
200
+ if (result.warnings?.length)
201
+ result.warnings.forEach((w) => console.log(` Warning: ${w}`));
202
+ }
203
+ else {
204
+ console.error(`Create from backup failed: ${result.detail || "unknown error"}`);
205
+ process.exit(1);
206
+ }
207
+ }
208
+ catch (e) {
209
+ console.error(`Restore --new failed: ${e.message}`);
210
+ process.exit(1);
211
+ }
145
212
  }
146
213
  else {
147
- console.error("Usage: jishushell pairing approve <channel> <code> [--instance <id>]");
214
+ const file = rest.find((a) => !a.startsWith("--"));
215
+ if (!file) {
216
+ console.error("Usage: jishushell restore --new <file> [--id <new-id>]");
217
+ process.exit(1);
218
+ }
219
+ console.log(`Use: jishushell import ${file}`);
220
+ }
221
+ process.exit(0);
222
+ }
223
+ const id = rest[0];
224
+ if (!id) {
225
+ console.error("Usage: jishushell restore <id> <--latest|--pick|file>");
226
+ process.exit(1);
227
+ }
228
+ let file = "";
229
+ if (rest.includes("--latest") || rest.includes("--pick")) {
230
+ let backups;
231
+ try {
232
+ backups = await apiCall(`/instances/${id}/backups`);
233
+ }
234
+ catch (e) {
235
+ console.error(`Restore failed: ${e.message}`);
148
236
  process.exit(1);
149
237
  }
150
- const notify = rest.includes("--notify");
151
- const { runPairingApprove } = await import("./control.js");
152
- runPairingApprove({ instanceId, channel, code, notify }).then((ok) => process.exit(ok ? 0 : 1)).catch((err) => {
153
- console.error("pairing approve failed:", err.message);
238
+ if (!backups.length) {
239
+ console.error("No backups found.");
154
240
  process.exit(1);
155
- });
241
+ }
242
+ if (rest.includes("--latest")) {
243
+ file = backups[0].filename;
244
+ }
245
+ else {
246
+ console.log("Available backups:");
247
+ backups.forEach((b, i) => {
248
+ console.log(` ${i + 1}. ${b.type} ${new Date(b.created_at).toLocaleString()} (${(b.size / 1024 / 1024).toFixed(1)}MB)`);
249
+ });
250
+ const readline = await import("readline");
251
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
252
+ const answer = await new Promise(r => rl.question("Select number: ", r));
253
+ rl.close();
254
+ const idx = parseInt(answer, 10) - 1;
255
+ if (idx < 0 || idx >= backups.length) {
256
+ console.error("Invalid selection.");
257
+ process.exit(1);
258
+ }
259
+ file = backups[idx].filename;
260
+ }
156
261
  }
157
262
  else {
158
- console.log(`
159
- Usage: jishushell pairing <command> [options]
160
-
161
- Commands:
162
- list 列出等待审批的配对请求
163
- approve <channel> <code> 审批配对码(例: feishu LVU7PNYK)
164
- approve <code> 默认 channel=feishu 简写形式
165
-
166
- Options:
167
- --instance <id> 指定实例 ID(只有一个实例时可省略)
168
- --notify 审批后通知申请方
169
-
170
- Examples:
171
- jishushell pairing list
172
- jishushell pairing approve feishu LVU7PNYK
173
- jishushell pairing approve LVU7PNYK
174
- jishushell pairing approve feishu LVU7PNYK --instance my-instance
175
- `);
263
+ file = rest[1];
264
+ }
265
+ if (!file) {
266
+ console.error("No backup file specified.");
176
267
  process.exit(1);
177
268
  }
178
- // ── serve: foreground server (used by systemd/launchd ExecStart) ──────────
179
- }
180
- else if (subcommand === "serve") {
181
- const args = rest;
182
- let port = PANEL_PORT;
183
- let host = "0.0.0.0";
184
- for (let i = 0; i < args.length; i++) {
185
- if (args[i] === "--port" && args[i + 1]) {
186
- const parsed = parseInt(args[i + 1], 10);
187
- if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
188
- console.error(`Invalid port: ${args[i + 1]}. Must be 1-65535.`);
189
- process.exit(1);
269
+ try {
270
+ console.log(`Restoring ${id} from ${file}...`);
271
+ const result = await apiCall(`/instances/${id}/restore/${encodeURIComponent(file)}`, { method: "POST" });
272
+ if (result.job_id) {
273
+ const job = await waitForJob(result.job_id);
274
+ const r = job.result || {};
275
+ if (r.ok) {
276
+ console.log(`\nRestore complete.`);
277
+ if (r.api_key_status === "lost")
278
+ console.log("Warning: API Key may need re-verification on this host.");
279
+ if (r.warnings?.length)
280
+ r.warnings.forEach((w) => console.log(` Warning: ${w}`));
190
281
  }
191
- port = parsed;
192
- i++;
282
+ else {
283
+ console.error(`\nRestore failed.${r.rolled_back ? " Auto-rollback completed." : ""}`);
284
+ if (r.warnings?.length)
285
+ r.warnings.forEach((w) => console.log(` ${w}`));
286
+ }
287
+ }
288
+ }
289
+ catch (e) {
290
+ console.error(`Restore failed: ${e.message}`);
291
+ process.exit(1);
292
+ }
293
+ process.exit(0);
294
+ // ── export ─────────────────────────────────────────────────────────────────
295
+ }
296
+ else if (subcommand === "export") {
297
+ const id = rest[0];
298
+ if (!id) {
299
+ console.error("Usage: jishushell export <id> [--with-sessions]");
300
+ process.exit(1);
301
+ }
302
+ const withSessions = rest.includes("--with-sessions");
303
+ try {
304
+ console.log(`Exporting ${id}...`);
305
+ const result = await apiCall(`/instances/${id}/export`, { method: "POST", body: { include_sessions: withSessions } });
306
+ if (result.job_id) {
307
+ const job = await waitForJob(result.job_id);
308
+ const r = job.result || {};
309
+ console.log(`\nExport complete: ${r.filename}`);
310
+ console.log(`Download: http://127.0.0.1:${process.env.JISHUSHELL_PORT || "8090"}/api/instances/${id}/export/download/${encodeURIComponent(r.filename)}`);
311
+ if (r.warnings?.length)
312
+ r.warnings.forEach((w) => console.log(` Warning: ${w}`));
193
313
  }
194
- else if (args[i] === "--host" && args[i + 1]) {
195
- host = args[i + 1];
196
- i++;
314
+ }
315
+ catch (e) {
316
+ console.error(`Export failed: ${e.message}`);
317
+ process.exit(1);
318
+ }
319
+ process.exit(0);
320
+ // ── import ─────────────────────────────────────────────────────────────────
321
+ }
322
+ else if (subcommand === "import") {
323
+ const file = rest[0];
324
+ if (!file) {
325
+ console.error("Usage: jishushell import <file> [--id <id>] [--name <name>]");
326
+ process.exit(1);
327
+ }
328
+ const idIdx = rest.indexOf("--id");
329
+ const nameIdx = rest.indexOf("--name");
330
+ const customId = idIdx !== -1 ? rest[idIdx + 1] : undefined;
331
+ const customName = nameIdx !== -1 ? rest[nameIdx + 1] : undefined;
332
+ try {
333
+ const { readFileSync } = await import("fs");
334
+ const { basename } = await import("path");
335
+ const port = process.env.JISHUSHELL_PORT || "8090";
336
+ console.log("Uploading file...");
337
+ const fileData = readFileSync(file);
338
+ const boundary = "----JishuShellCLI" + Date.now();
339
+ const fileName = basename(file);
340
+ const body = Buffer.concat([
341
+ Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/gzip\r\n\r\n`),
342
+ fileData,
343
+ Buffer.from(`\r\n--${boundary}--\r\n`),
344
+ ]);
345
+ const uploadRes = await fetch(`http://127.0.0.1:${port}/api/instances/import/upload`, {
346
+ method: "POST",
347
+ headers: { "Content-Type": `multipart/form-data; boundary=${boundary}` },
348
+ body,
349
+ });
350
+ if (!uploadRes.ok) {
351
+ const err = await uploadRes.json().catch(() => ({ detail: uploadRes.statusText }));
352
+ throw new Error(err.detail || `Upload failed: ${uploadRes.status}`);
353
+ }
354
+ const { temp_id } = await uploadRes.json();
355
+ console.log("Previewing...");
356
+ const preview = await apiCall("/instances/import/preview", { method: "POST", body: { temp_id } });
357
+ console.log(` Name: ${preview.manifest?.name || "unknown"}`);
358
+ console.log(` Type: ${preview.manifest?.type || "unknown"}`);
359
+ console.log(` Size: ${((preview.estimated_size || 0) / 1024 / 1024).toFixed(1)}MB`);
360
+ if (preview.warnings?.length)
361
+ for (const w of preview.warnings)
362
+ console.log(` Warning: ${w}`);
363
+ const instanceId = customId || preview.manifest?.name?.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 63) || `import-${Date.now()}`;
364
+ const instanceName = customName || preview.manifest?.name || instanceId;
365
+ console.log(`Creating instance ${instanceId}...`);
366
+ const result = await apiCall("/instances/import", { method: "POST", body: { temp_id, id: instanceId, name: instanceName } });
367
+ if (result.ok) {
368
+ console.log(`Instance created: ${result.instance_id}`);
369
+ if (result.warnings?.length)
370
+ for (const w of result.warnings)
371
+ console.log(` Warning: ${w}`);
372
+ }
373
+ else {
374
+ console.error(`Import failed: ${result.detail || "unknown error"}`);
375
+ process.exit(1);
197
376
  }
198
377
  }
199
- createServer({ port, host }).catch((err) => {
200
- console.error("serve failed:", err.message);
378
+ catch (e) {
379
+ console.error(`Import failed: ${e.message}`);
201
380
  process.exit(1);
202
- });
203
- // ── uninstall ─────────────────────────────────────────────────────────────
381
+ }
382
+ process.exit(0);
383
+ // ── clone ──────────────────────────────────────────────────────────────────
204
384
  }
205
- else if (subcommand === "uninstall") {
206
- const yes = rest.includes("--yes") || rest.includes("-y");
207
- const { spawnSync } = await import("child_process");
208
- const uninstallSh = join(JISHUSHELL_HOME, "install", "post-uninstall.sh");
209
- if (!existsSync(uninstallSh)) {
210
- console.error(`Uninstall script not found: ${uninstallSh}`);
211
- console.error("Please run: sudo bash ~/.jishushell/install/jishu-uninstall.sh --data --yes");
385
+ else if (subcommand === "clone") {
386
+ const src = rest[0];
387
+ const newId = rest[1];
388
+ if (!src || !newId) {
389
+ console.error("Usage: jishushell clone <src-id> <new-id> [--with-sessions] [--no-memory]");
390
+ process.exit(1);
391
+ }
392
+ const withSessions = rest.includes("--with-sessions");
393
+ const noMemory = rest.includes("--no-memory");
394
+ try {
395
+ console.log(`Cloning ${src} to ${newId}...`);
396
+ const result = await apiCall("/instances", { method: "POST", body: { id: newId, name: newId, clone_from: src, clone_options: { include_sessions: withSessions, include_memory: !noMemory } } });
397
+ console.log(`Clone created: ${result.id || newId}`);
398
+ }
399
+ catch (e) {
400
+ console.error(`Clone failed: ${e.message}`);
212
401
  process.exit(1);
213
402
  }
214
- if (!yes) {
215
- const { createInterface } = await import("readline");
216
- const rl = createInterface({ input: process.stdin, output: process.stdout });
217
- await new Promise((resolve) => {
218
- rl.question("This will stop all services, remove auto-start and delete ~/.jishushell. Continue? [y/N] ", (ans) => {
403
+ process.exit(0);
404
+ // ── backups ────────────────────────────────────────────────────────────────
405
+ }
406
+ else if (subcommand === "backups") {
407
+ const sub = rest[0];
408
+ if (sub === "list") {
409
+ const id = rest[1];
410
+ try {
411
+ if (id) {
412
+ const backups = await apiCall(`/instances/${id}/backups`);
413
+ if (!backups.length)
414
+ console.log("No backups.");
415
+ else
416
+ backups.forEach((b, i) => console.log(` ${i + 1}. [${b.type}] ${new Date(b.created_at).toLocaleString()} ${(b.size / 1024 / 1024).toFixed(1)}MB ${b.filename}`));
417
+ }
418
+ else {
419
+ const orphans = await apiCall("/backups/orphaned");
420
+ if (orphans.length) {
421
+ console.log("Orphaned backups:");
422
+ for (const o of orphans)
423
+ console.log(` ${o.instance_id}: ${o.backups?.length || 0} files`);
424
+ }
425
+ else
426
+ console.log("No orphaned backups.");
427
+ }
428
+ }
429
+ catch (e) {
430
+ console.error(`backups list failed: ${e.message}`);
431
+ process.exit(1);
432
+ }
433
+ }
434
+ else if (sub === "clean") {
435
+ try {
436
+ const orphans = await apiCall("/backups/orphaned");
437
+ if (!orphans.length) {
438
+ console.log("No orphaned backups to clean.");
439
+ process.exit(0);
440
+ }
441
+ const force = rest.includes("--force");
442
+ console.log(`Found ${orphans.length} orphaned backup(s):`);
443
+ for (const o of orphans)
444
+ console.log(` ${o.instance_id}: ${o.backups?.length || 0} files`);
445
+ if (!force) {
446
+ const readline = await import("readline");
447
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
448
+ const answer = await new Promise(r => rl.question("Delete all orphaned backups? (y/N): ", r));
219
449
  rl.close();
220
- if (ans.toLowerCase() !== "y") {
450
+ if (answer.toLowerCase() !== "y") {
221
451
  console.log("Cancelled.");
222
452
  process.exit(0);
223
453
  }
224
- resolve();
225
- });
226
- });
454
+ }
455
+ for (const o of orphans) {
456
+ for (const b of o.backups || [])
457
+ await apiCall(`/backups/orphaned/${encodeURIComponent(o.instance_id)}/${encodeURIComponent(b.filename)}`, { method: "DELETE" });
458
+ console.log(` Cleaned: ${o.instance_id}`);
459
+ }
460
+ console.log("Done.");
461
+ }
462
+ catch (e) {
463
+ console.error(`backups clean failed: ${e.message}`);
464
+ process.exit(1);
465
+ }
227
466
  }
228
- // Step 1: stop services and remove ~/.jishushell via the backed-up script
229
- const cleanResult = spawnSync("bash", [uninstallSh], { stdio: "inherit" });
230
- // Step 2: remove the npm global package (now ~/.jishushell is already gone)
231
- const npmResult = spawnSync("npm", ["uninstall", "-g", "jishushell"], { stdio: "inherit" });
232
- process.exit((cleanResult.status ?? 0) || (npmResult.status ?? 0));
233
- // ── help ───────────────────────────────────────────────────────────────────
467
+ else {
468
+ console.error("Usage: jishushell backups list [id]\n jishushell backups clean [--force]");
469
+ process.exit(1);
470
+ }
471
+ process.exit(0);
472
+ // ── auto-backup ────────────────────────────────────────────────────────────
473
+ }
474
+ else if (subcommand === "auto-backup") {
475
+ const id = rest[0];
476
+ const action = rest[1];
477
+ if (!id || !action) {
478
+ console.error("Usage: jishushell auto-backup <id> enable|disable|status");
479
+ process.exit(1);
480
+ }
481
+ try {
482
+ if (action === "status") {
483
+ const config = await apiCall(`/instances/${id}/auto-backup`);
484
+ console.log(JSON.stringify(config, null, 2));
485
+ }
486
+ else if (action === "enable") {
487
+ const interval = parseInt(rest[rest.indexOf("--interval") + 1], 10) || 24;
488
+ const keep = parseInt(rest[rest.indexOf("--keep") + 1], 10) || 7;
489
+ await apiCall(`/instances/${id}/auto-backup`, { method: "PUT", body: { enabled: true, interval_hours: interval, keep_count: keep } });
490
+ console.log(`Auto-backup enabled for ${id} (every ${interval}h, keep ${keep})`);
491
+ }
492
+ else if (action === "disable") {
493
+ await apiCall(`/instances/${id}/auto-backup`, { method: "PUT", body: { enabled: false } });
494
+ console.log(`Auto-backup disabled for ${id}`);
495
+ }
496
+ else {
497
+ console.error(`Unknown action: ${action}. Use enable|disable|status`);
498
+ process.exit(1);
499
+ }
500
+ }
501
+ catch (e) {
502
+ console.error(`auto-backup failed: ${e.message}`);
503
+ process.exit(1);
504
+ }
505
+ process.exit(0);
234
506
  }
235
507
  else if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
508
+ const [{ brief: panelBrief }, { brief: doctorBrief }, { brief: openclawBrief }, { brief: jobBrief }, { brief: appBrief }, { brief: llmBrief },] = await Promise.all([
509
+ import("./cli/panel.js"),
510
+ import("./cli/doctor.js"),
511
+ import("./cli/openclaw.js"),
512
+ import("./cli/job.js"),
513
+ import("./cli/app.js"),
514
+ import("./cli/llm.js"),
515
+ ]);
236
516
  console.log(`
237
517
  JishuShell — OpenClaw 实例管理面板
238
518
 
239
519
  Usage:
240
- jishushell 前台启动面板服务器
520
+ jishushell serve [--port N] 前台启动面板服务器(默认端口 8090)
241
521
  jishushell <command> [options]
242
522
 
243
523
  Commands:
244
- status 显示各模块运行状态(面板、Docker、Nomad 及所有 Job)
245
- serve [--port N] [--host H] 前台启动面板服务器(供 systemd/launchd 使用)
246
- start [--port N] 在后台启动面板
247
- stop [--jobs] 停止面板;加 --jobs 同时停止所有 Nomad Job
248
- restart [--port N] 重启面板
249
- doctor 检查所有组件健康状态
250
- doctor --fix 检查并尝试自动修复问题
251
- onboard [--force] 首次引导:设置管理员密码
252
- reset [--yes] 重置配置与所有 Nomad Job(保留实例数据目录)
253
- uninstall [--yes] 停止服务、删除 ~/.jishushell 并卸载 npm 包
254
- update-check 检查是否有新版本(0=有新版本,1=已是最新)
255
- update 安装最新版本并重启面板
256
- install [options] 安装依赖并配置 JishuShell
257
- pairing list 列出等待审批的 DM 配对请求
258
- pairing approve <ch> <code> 审批飞书/Lark 配对码(在容器内执行)
259
- help 显示此帮助信息
260
-
261
- Server options:
262
- --port <port> 监听端口(默认 8090 或 panel.json 中的值)
263
- --host <host> 监听地址(默认 0.0.0.0)
264
-
265
- Install options:
266
- --port <port> 面板端口(默认 8090)
267
- --yes, -y 跳过所有交互提示(非交互模式)
268
- --no-service 跳过 systemd/launchd 注册
269
- --skip-docker 跳过 Docker 安装
270
- --skip-nomad 跳过 Nomad 安装
271
- --skip-openclaw 跳过 OpenClaw 安装
272
- --force, -f 即使已配置也强制重新安装
273
- --dry-run, -n 预演,不实际执行任何变更
524
+ ${panelBrief()}
525
+ ${doctorBrief()}
526
+ ${openclawBrief()}
527
+ ${jobBrief()}
528
+ ${appBrief()}
529
+ ${llmBrief()}
530
+ backup <id> [--with-sessions] 备份实例
531
+ restore <id> <--latest|--pick|file> 还原实例
532
+ export <id> 导出实例为归档包
533
+ import <file> [--id <id>] 导入归档包为新实例
534
+ clone <src-id> <new-id> 克隆实例
535
+ backups list [id] 列出备份文件
536
+ auto-backup <id> enable|disable|status 管理定时自动备份
537
+ help 显示此帮助
274
538
 
275
- Examples:
276
- jishushell status # 查看当前所有组件状态
277
- jishushell start # 后台启动面板
278
- jishushell stop --jobs # 停止面板及所有实例 Job
279
- jishushell restart # 重启面板
280
- jishushell doctor --fix # 检查并自动修复
281
- jishushell reset --yes # 非交互式重置(CI 场景)
539
+ 运行 'jishushell <command> help' 查看详细用法。
282
540
  `);
283
541
  process.exit(0);
284
- // ── unknown command ────────────────────────────────────────────────────────
285
542
  }
286
543
  else {
287
544
  console.error(`Unknown command: ${subcommand}\nRun 'jishushell help' for usage.`);