remote-dsh 0.4.7 → 0.5.0

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 (2) hide show
  1. package/dist/bin.js +288 -189
  2. package/package.json +4 -4
package/dist/bin.js CHANGED
@@ -2,82 +2,57 @@
2
2
  /**
3
3
  * rdsh CLI 入口(手写参数解析,零依赖)。
4
4
  *
5
- * rdsh serve [--config <path>] [--reset] [--port <n>] ...
6
- * rdsh join <hub-url> [--token <t>] [--dsh <path>]
5
+ * rdsh host setup lan|cloud
6
+ * rdsh host join <hub-url> [--token <t>] [--name <n>] [--dsh <p>] [--insecure]
7
+ * rdsh host serve | service install|status|uninstall | leave | user ...
7
8
  * rdsh hub serve|user|host|service ... [--config <path>]
8
- * rdsh user add|passwd <name> | ls | rm <name> [--config <path>]
9
- * rdsh service install|uninstall|status [--config <path>]
10
9
  * rdsh --version | --help
11
10
  */
12
11
  import { createInterface } from "node:readline";
13
- import { serve, join, UserManager, resolveConfigPath, installService, uninstallService, serviceStatus, } from "rdsh-gateway";
12
+ import { rm } from "node:fs/promises";
13
+ import { hostname as osHostname } from "node:os";
14
+ import { dirname } from "node:path";
15
+ import { serve, join, registerJoin, findDsh, selfRevoke, readPersistedToken, clearPersistedToken, UserManager, resolveConfigPath, loadConfig, saveConfig, installService, uninstallService, serviceStatus, HOST_SERVICE_NAME, JOIN_SERVICE_NAME, HUB_SERVICE_NAME, } from "rdsh-gateway";
14
16
  import { loadHubConfig, resolveHubConfigPath, HubDb, hashPassword, serveHub, } from "rdsh-hub";
15
17
  import { VERSION } from "./index.js";
16
18
  const HELP = `rdsh — secure remote access for DeepSeek Harness
17
19
 
18
20
  Usage:
19
- rdsh serve [options] Start the gateway (LAN auth or cloud HTTPS service)
20
- rdsh join <hub-url> Connect to a hub via outbound tunnel (no ports open)
21
- rdsh hub serve Start the hub server (cloud, multi-host)
22
- rdsh hub user add <name> Create a hub user (interactive password; --no-password for first-login setup)
23
- rdsh hub user passwd <n> Reset a user's password (admin)
24
- rdsh hub user rm|ls
25
- rdsh hub host ls|revoke List / revoke hosts (revoke = tunnel drops instantly)
26
- rdsh hub service ... Install hub as a systemd/launchd service
27
- rdsh user add|passwd|ls|rm Gateway users (LAN/cloud HTTPS service)
28
- rdsh service install|status|uninstall
21
+ rdsh host setup lan|cloud Configure this machine (LAN / cloud HTTPS gateway)
22
+ rdsh host join <hub-url> Connect this machine to a hub (outbound tunnel)
23
+ rdsh host serve Run the configured mode in the foreground
24
+ rdsh host service ... Run as a systemd/launchd service
25
+ rdsh host leave Unregister this machine from the hub
26
+ rdsh host user ... Manage gateway users (LAN/cloud password auth)
27
+ rdsh hub serve Start the hub server (cloud, multi-host)
28
+ rdsh hub user add|passwd|rm|ls
29
+ rdsh hub host ls|revoke List / revoke hosts (revoke drops the tunnel instantly)
30
+ rdsh hub service ... Run hub as a systemd/launchd service
29
31
 
30
32
  Global:
31
- --config <path> Gateway config (default ~/.rdsh/config.json; also $RDSH_CONFIG)
32
- For hub commands: hub config (default ~/.rdsh/hub.json; also $RDSH_HUB_CONFIG)
33
- --version, -v Print version
34
- --help, -h Print this help
35
-
36
- Options (serve):
37
- --port <n> Listen port (overrides config; default 8443)
38
- --host <ip> Bind address (overrides config; default 0.0.0.0)
39
- --pair-code <code> Preset pairing code (overrides config)
40
- --session-ttl <sec> Session cookie lifetime (overrides config; default 43200)
41
- --dsh <path> dsh executable path (overrides config)
42
- --reset Rotate session keys (all devices must re-auth)
43
- --no-code Disable auth (trusted network ONLY!)
44
-
45
- Options (join):
46
- --token <t> Use an existing host token (skip pairing-code flow)
47
- --dsh <path> dsh executable path
48
-
49
- Options (hub serve):
50
- --port <n> Listen port (overrides hub config; default 8443)
51
- --host <ip> Bind address (overrides hub config; default 0.0.0.0)
52
-
53
- Config (persistent): host, port, sessionTtlSeconds, tls{cert,key}, behindProxy,
54
- allowFrom[], auth{mode: pair|password|none, pairCode, users[]}, dshPath.
55
- Hub config (~/.rdsh/hub.json): host, port, tls{cert,key}, dbPath, jwtKeyPath.
33
+ --config <path> Host config (default ~/.rdsh/host.json; also $RDSH_CONFIG)
34
+ For hub commands: hub config (default ~/.rdsh/hub.json; also $RDSH_HUB_CONFIG)
35
+ --version, -v Print version
36
+ --help, -h Print this help
56
37
  `;
57
38
  /** 子命令级用法(`rdsh <cmd> --help`)。 */
58
39
  const SUB_HELP = {
59
- serve: `Usage: rdsh serve [options]
60
-
61
- Start the gateway (LAN auth or cloud HTTPS service).
40
+ host: `Usage: rdsh host <subcommand>
62
41
 
63
- Options:
64
- --config <path> Config file (default ~/.rdsh/config.json; also $RDSH_CONFIG)
65
- --port <n> Listen port (overrides config; default 8443)
66
- --host <ip> Bind address (overrides config; default 0.0.0.0)
67
- --pair-code <code> Preset pairing code (overrides config)
68
- --session-ttl <sec> Session cookie lifetime (overrides config; default 43200)
69
- --dsh <path> dsh executable path (overrides config)
70
- --reset Rotate session keys (all devices must re-auth)
71
- --no-code Disable auth (trusted network ONLY!)
72
- `,
73
- join: `Usage: rdsh join <hub-url> [options]
42
+ Configure and run this machine (the DSH host).
74
43
 
75
- Connect to a hub via an outbound tunnel (no ports open on this machine).
44
+ Subcommands:
45
+ setup lan Configure a LAN gateway (pair auth, plain http)
46
+ setup cloud Configure a cloud HTTPS gateway (password + TLS + allowFrom)
47
+ join <hub-url> Connect to a hub (interactive token paste; --token for scripts)
48
+ serve Run the configured mode in the foreground
49
+ service install|status|uninstall Run as a systemd/launchd service
50
+ leave Unregister this machine from the hub
51
+ user add|passwd|ls|rm Manage gateway users
76
52
 
77
- Options:
78
- --token <t> Use an existing host token (skip the pair-code flow)
79
- --dsh <path> dsh executable path
80
- --insecure Skip TLS certificate verification (self-signed hub)
53
+ Options (setup lan): --port <n> [--pair-code <code>]
54
+ Options (setup cloud): --tls-cert <path> --tls-key <path> [--port <n>] [--allow-from <cidr,...>]
55
+ Options (join): --token <t> --name <n> --dsh <path> --insecure
81
56
  `,
82
57
  hub: `Usage: rdsh hub <subcommand> [options]
83
58
 
@@ -93,31 +68,6 @@ Options (serve):
93
68
  --config <path> Hub config (default ~/.rdsh/hub.json; also $RDSH_HUB_CONFIG)
94
69
  --port <n> Listen port (overrides hub config; default 8443)
95
70
  --host <ip> Bind address (overrides hub config; default 0.0.0.0)
96
- `,
97
- user: `Usage: rdsh user <action> [name]
98
-
99
- Manage gateway users (LAN / cloud HTTPS service).
100
-
101
- Actions:
102
- add <name> Add a user (interactive password)
103
- passwd <name> Change a user's password (revokes all their sessions)
104
- ls List users
105
- rm <name> Remove a user
106
-
107
- Options:
108
- --config <path> Config file (default ~/.rdsh/config.json; also $RDSH_CONFIG)
109
- `,
110
- service: `Usage: rdsh service <action>
111
-
112
- Manage the gateway as a systemd/launchd service.
113
-
114
- Actions:
115
- install Install and start (auto-start on boot, restart on crash)
116
- status Show service status
117
- uninstall Stop and remove the service
118
-
119
- Options:
120
- --config <path> Config file (default ~/.rdsh/config.json; also $RDSH_CONFIG)
121
71
  `,
122
72
  };
123
73
  /** 参数里是否有 --help / -h。 */
@@ -149,28 +99,14 @@ async function main() {
149
99
  return;
150
100
  }
151
101
  switch (command) {
152
- case "serve": {
153
- const opts = parseServeArgs(rest, cli.configPath);
154
- await serve(opts);
155
- return;
156
- }
157
- case "join": {
158
- const opts = parseJoinArgs(rest);
159
- await join(opts);
102
+ case "host": {
103
+ await handleHost(rest, cli.configPath);
160
104
  return;
161
105
  }
162
106
  case "hub": {
163
107
  await handleHub(rest, cli.configPath);
164
108
  return;
165
109
  }
166
- case "user": {
167
- await handleUser(rest, cli.configPath);
168
- return;
169
- }
170
- case "service": {
171
- await handleService(rest, cli.configPath);
172
- return;
173
- }
174
110
  default:
175
111
  console.error(`rdsh: unknown command '${command}'. Run 'rdsh --help'.`);
176
112
  process.exitCode = 1;
@@ -196,59 +132,118 @@ function parseGlobal(argv) {
196
132
  flags.push(argv[i]);
197
133
  }
198
134
  }
199
- // 返回原始值(undefined 或显式 --config);gateway/hub 子命令分别按自己的默认路径 resolve
200
135
  return { configPath, flags };
201
136
  }
202
- function parseServeArgs(args, configPath) {
203
- const opts = { configPath };
204
- for (let i = 0; i < args.length; i++) {
205
- const flag = args[i];
206
- const value = (name) => {
207
- i += 1;
208
- if (i >= args.length)
209
- throw new Error(`missing value for ${name}`);
210
- return args[i] ?? "";
211
- };
212
- switch (flag) {
213
- case "--port": {
214
- const v = Number(value(flag));
215
- if (!Number.isInteger(v) || v < 0 || v > 65535)
216
- throw new Error(`invalid --port '${args[i]}'`);
217
- opts.port = v;
218
- break;
219
- }
220
- case "--host":
221
- opts.host = value(flag);
222
- break;
223
- case "--pair-code":
224
- opts.pairCode = value(flag);
225
- break;
226
- case "--session-ttl": {
227
- const v = Number(value(flag));
228
- if (!Number.isInteger(v) || v <= 0)
229
- throw new Error(`invalid --session-ttl '${args[i]}'`);
230
- opts.sessionTtlSeconds = v;
231
- break;
232
- }
233
- case "--dsh":
234
- opts.dshPath = value(flag);
235
- break;
236
- case "--reset":
237
- opts.reset = true;
238
- break;
239
- case "--no-code":
240
- opts.noCode = true;
241
- break;
242
- default:
137
+ function parsePort(v) {
138
+ const n = Number(v);
139
+ if (!Number.isInteger(n) || n < 0 || n > 65535)
140
+ throw new Error(`invalid port '${v}'`);
141
+ return n;
142
+ }
143
+ /** 服务 unit 的子进程 PATH:node 目录(nvm/自装)+ 系统路径,保证 spawn dsh 时 shebang 能解析 node。 */
144
+ function nodePathEnv() {
145
+ return `${dirname(process.execPath)}:/usr/local/bin:/usr/bin:/bin`;
146
+ }
147
+ async function handleHost(args, configPath) {
148
+ const sub = args[0];
149
+ const rest = args.slice(1);
150
+ if (sub === undefined || hasHelp(rest)) {
151
+ console.log(SUB_HELP["host"] ?? HELP);
152
+ return;
153
+ }
154
+ switch (sub) {
155
+ case "setup":
156
+ await handleHostSetup(rest, configPath);
157
+ return;
158
+ case "join":
159
+ await handleHostJoin(rest, configPath);
160
+ return;
161
+ case "serve":
162
+ await handleHostServe(rest, configPath);
163
+ return;
164
+ case "service":
165
+ await handleHostService(rest, configPath);
166
+ return;
167
+ case "leave":
168
+ await handleHostLeave(rest, configPath);
169
+ return;
170
+ case "user":
171
+ await handleHostUser(rest, configPath);
172
+ return;
173
+ default:
174
+ throw new Error("usage: rdsh host setup|join|serve|service|leave|user");
175
+ }
176
+ }
177
+ /** `rdsh host setup lan|cloud`:写 host.json(mode + 预设字段),配置完退出。 */
178
+ async function handleHostSetup(args, configPath) {
179
+ const which = args[0];
180
+ const target = resolveConfigPath(configPath);
181
+ const config = await loadConfig(target);
182
+ if (which === "lan") {
183
+ let port = config.port;
184
+ let pairCode = config.auth.pairCode;
185
+ for (let i = 1; i < args.length; i++) {
186
+ const flag = args[i];
187
+ if (flag === "--port")
188
+ port = parsePort(args[++i]);
189
+ else if (flag === "--pair-code")
190
+ pairCode = args[++i];
191
+ else
243
192
  throw new Error(`unknown option '${flag}'`);
244
193
  }
194
+ config.mode = "lan";
195
+ config.port = port;
196
+ config.tls = undefined;
197
+ config.behindProxy = false;
198
+ config.allowFrom = [];
199
+ config.auth.mode = "pair";
200
+ config.auth.pairCode = pairCode;
201
+ config.dshPath = config.dshPath ?? findDsh() ?? undefined;
202
+ await saveConfig(target, config);
203
+ console.log(`rdsh: host 配置为 LAN 网关(pair,端口 ${port})→ ${target}`);
204
+ console.log("rdsh: 运行 `rdsh host serve` 前台启动,或 `rdsh host service install` 常驻。");
205
+ return;
245
206
  }
246
- return opts;
207
+ if (which === "cloud") {
208
+ let port = config.port;
209
+ let cert;
210
+ let key;
211
+ let allowFrom = [];
212
+ for (let i = 1; i < args.length; i++) {
213
+ const flag = args[i];
214
+ if (flag === "--port")
215
+ port = parsePort(args[++i]);
216
+ else if (flag === "--tls-cert")
217
+ cert = args[++i];
218
+ else if (flag === "--tls-key")
219
+ key = args[++i];
220
+ else if (flag === "--allow-from")
221
+ allowFrom = (args[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
222
+ else
223
+ throw new Error(`unknown option '${flag}'`);
224
+ }
225
+ if (cert === undefined || key === undefined) {
226
+ throw new Error("usage: rdsh host setup cloud --tls-cert <path> --tls-key <path> [--port <n>] [--allow-from <cidr,...>]");
227
+ }
228
+ config.mode = "cloud";
229
+ config.port = port;
230
+ config.tls = { cert, key };
231
+ config.behindProxy = false;
232
+ config.allowFrom = allowFrom;
233
+ config.auth.mode = "password";
234
+ config.dshPath = config.dshPath ?? findDsh() ?? undefined;
235
+ await saveConfig(target, config);
236
+ console.log(`rdsh: host 配置为云 HTTPS 网关(password,端口 ${port})→ ${target}`);
237
+ console.log("rdsh: 用 `rdsh host user add <name>` 建用户,再 `rdsh host serve` / `rdsh host service install`。");
238
+ return;
239
+ }
240
+ throw new Error("usage: rdsh host setup lan|cloud");
247
241
  }
248
- function parseJoinArgs(args) {
242
+ /** `rdsh host join <hub>`:注册(join token)→ 写 host.json + session,配置完退出。 */
243
+ async function handleHostJoin(args, configPath) {
249
244
  const hubUrl = args[0];
250
245
  if (hubUrl === undefined || !/^https?:\/\//.test(hubUrl)) {
251
- throw new Error("usage: rdsh join <hub-url> [--token <t>] [--dsh <path>]");
246
+ throw new Error("usage: rdsh host join <hub-url> [--token <t>] [--name <n>] [--dsh <p>] [--insecure]");
252
247
  }
253
248
  const opts = { hubUrl };
254
249
  for (let i = 1; i < args.length; i++) {
@@ -259,42 +254,153 @@ function parseJoinArgs(args) {
259
254
  throw new Error(`missing value for ${name}`);
260
255
  return args[i] ?? "";
261
256
  };
262
- switch (flag) {
263
- case "--token":
264
- opts.token = value(flag);
265
- break;
266
- case "--dsh":
267
- opts.dshPath = value(flag);
268
- break;
269
- case "--insecure":
270
- opts.insecure = true;
271
- break;
272
- default:
273
- throw new Error(`unknown option '${flag}'`);
257
+ if (flag === "--token")
258
+ opts.token = value(flag);
259
+ else if (flag === "--name")
260
+ opts.name = value(flag);
261
+ else if (flag === "--dsh")
262
+ opts.dshPath = value(flag);
263
+ else if (flag === "--insecure")
264
+ opts.insecure = true;
265
+ else if (flag === "--reset")
266
+ opts.reset = true;
267
+ else
268
+ throw new Error(`unknown option '${flag}'`);
269
+ }
270
+ if (opts.token === undefined && readPersistedToken(hubUrl) === null) {
271
+ // 无 --token 且无持久化 session:TTY 交互粘贴 token;非 TTY 报错不 hang
272
+ if (process.stdin.isTTY) {
273
+ const pasted = (await promptLine("Paste your join token (hub portal → Add host): ")).trim();
274
+ if (pasted === "")
275
+ throw new Error("需要 join token(hub portal → 添加主机 生成)");
276
+ opts.token = pasted;
277
+ }
278
+ else {
279
+ throw new Error("无持久化 session 且未提供 --token;交互式运行粘贴 token");
274
280
  }
275
281
  }
276
- return opts;
282
+ const outcome = await registerJoin(opts);
283
+ const target = resolveConfigPath(configPath);
284
+ const config = await loadConfig(target);
285
+ config.mode = "join";
286
+ config.hub = hubUrl;
287
+ config.name = opts.name ?? osHostname();
288
+ config.insecure = outcome.insecure;
289
+ config.dshPath = opts.dshPath ?? findDsh() ?? config.dshPath;
290
+ await saveConfig(target, config);
291
+ console.log(`rdsh: host 已接入 ${hubUrl}(${config.name})—— session 已保存`);
292
+ console.log("rdsh: 运行 `rdsh host serve` 前台启动,或 `rdsh host service install` 常驻。");
293
+ }
294
+ /** `rdsh host serve`:前台常驻,读 host.json 按 mode 分发(join→隧道,lan/cloud→网关)。 */
295
+ async function handleHostServe(_args, configPath) {
296
+ const target = resolveConfigPath(configPath);
297
+ const config = await loadConfig(target);
298
+ if (config.mode === "join") {
299
+ if (config.hub === undefined)
300
+ throw new Error("host.json 缺 hub(join 模式);先 `rdsh host join <hub>`");
301
+ await join({ hubUrl: config.hub, name: config.name, insecure: config.insecure, dshPath: config.dshPath });
302
+ return;
303
+ }
304
+ await serve({ configPath: target });
277
305
  }
278
- async function handleUser(args, configPath) {
306
+ /** `rdsh host service install|status|uninstall`:读 host.json mode 装对应服务(unit 不含 token)。 */
307
+ async function handleHostService(args, configPath) {
308
+ const action = args[0];
309
+ const target = resolveConfigPath(configPath);
310
+ switch (action) {
311
+ case "install": {
312
+ await maybeRegisterForServiceInstall(args, target);
313
+ const config = await loadConfig(target);
314
+ const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
315
+ // host 服务会 spawn dsh(shebang `#!/usr/bin/env node`),nvm/自装 node 下需补 node 目录到 PATH(防 127)
316
+ console.log(await installService({ name, args: ["host", "serve"], configPath: target, pathEnv: nodePathEnv() }));
317
+ console.log(`rdsh: host service installed (${name}) —— 开机自启 + 崩溃重启。`);
318
+ return;
319
+ }
320
+ case "status": {
321
+ const config = await loadConfig(target);
322
+ const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
323
+ console.log(`rdsh host service (${name}): ${await serviceStatus(name)}`);
324
+ return;
325
+ }
326
+ case "uninstall": {
327
+ const config = await loadConfig(target);
328
+ const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
329
+ console.log(await uninstallService(name));
330
+ return;
331
+ }
332
+ default:
333
+ throw new Error("usage: rdsh host service install|status|uninstall");
334
+ }
335
+ }
336
+ /** 一行接入:`host service install <hub-url> --token <t> [--name <n>]` → 注册 + 写 host.json(再装服务)。 */
337
+ async function maybeRegisterForServiceInstall(args, target) {
338
+ const maybeHub = args[1];
339
+ if (maybeHub === undefined || !/^https?:\/\//.test(maybeHub))
340
+ return; // 无 <hub>,走读 host.json 路径
341
+ const joinOpts = { hubUrl: maybeHub };
342
+ for (let i = 2; i < args.length; i++) {
343
+ const flag = args[i];
344
+ if (flag === "--token")
345
+ joinOpts.token = args[++i];
346
+ else if (flag === "--name")
347
+ joinOpts.name = args[++i];
348
+ else if (flag === "--dsh")
349
+ joinOpts.dshPath = args[++i];
350
+ else if (flag === "--insecure")
351
+ joinOpts.insecure = true;
352
+ else
353
+ throw new Error(`unknown option '${flag}'`);
354
+ }
355
+ const outcome = await registerJoin(joinOpts);
356
+ const cfg = await loadConfig(target);
357
+ cfg.mode = "join";
358
+ cfg.hub = maybeHub;
359
+ cfg.name = joinOpts.name ?? osHostname();
360
+ cfg.insecure = outcome.insecure;
361
+ cfg.dshPath = joinOpts.dshPath ?? findDsh() ?? cfg.dshPath;
362
+ await saveConfig(target, cfg);
363
+ }
364
+ /** `rdsh host leave`:self-revoke + 清 session + 删 host.json → 未配置。 */
365
+ async function handleHostLeave(_args, configPath) {
366
+ const target = resolveConfigPath(configPath);
367
+ const config = await loadConfig(target);
368
+ if (config.mode !== "join" || config.hub === undefined) {
369
+ throw new Error("当前不是 join 模式(或缺 hub),无需 leave");
370
+ }
371
+ const token = readPersistedToken(config.hub);
372
+ if (token === null) {
373
+ console.log("rdsh: 本地无 session 文件,仅清理配置");
374
+ }
375
+ else {
376
+ await selfRevoke(config.hub, token, config.insecure === true);
377
+ console.log("rdsh: 已在 hub 自吊销本机");
378
+ }
379
+ clearPersistedToken(config.hub);
380
+ await rm(target, { force: true });
381
+ console.log(`rdsh: host 已注销(清理 ${target})—— 回到未配置状态`);
382
+ }
383
+ /** `rdsh host user add|passwd|ls|rm`:本机网关用户(写 host.json auth.users)。 */
384
+ async function handleHostUser(args, configPath) {
279
385
  const action = args[0];
280
386
  const um = new UserManager(resolveConfigPath(configPath));
281
387
  switch (action) {
282
388
  case "add": {
283
389
  const name = args[1];
284
390
  if (name === undefined)
285
- throw new Error("usage: rdsh user add <name>");
391
+ throw new Error("usage: rdsh host user add <name>");
286
392
  const password = await promptPassword(`password for ${name}: `);
287
393
  const again = await promptPassword("confirm: ");
288
394
  if (password !== again)
289
395
  throw new Error("passwords do not match");
290
396
  await um.add(name, password);
291
- console.log(`rdsh: user '${name}' added (config: ${configPath})`);
397
+ console.log(`rdsh: user '${name}' added`);
292
398
  return;
293
399
  }
294
400
  case "passwd": {
295
401
  const name = args[1];
296
402
  if (name === undefined)
297
- throw new Error("usage: rdsh user passwd <name>");
403
+ throw new Error("usage: rdsh host user passwd <name>");
298
404
  const password = await promptPassword(`new password for ${name}: `);
299
405
  const again = await promptPassword("confirm: ");
300
406
  if (password !== again)
@@ -313,7 +419,7 @@ async function handleUser(args, configPath) {
313
419
  case "rm": {
314
420
  const name = args[1];
315
421
  if (name === undefined)
316
- throw new Error("usage: rdsh user rm <name>");
422
+ throw new Error("usage: rdsh host user rm <name>");
317
423
  const ok = await um.remove(name);
318
424
  if (!ok)
319
425
  throw new Error(`user '${name}' not found`);
@@ -321,24 +427,7 @@ async function handleUser(args, configPath) {
321
427
  return;
322
428
  }
323
429
  default:
324
- throw new Error("usage: rdsh user add|passwd|ls|rm");
325
- }
326
- }
327
- async function handleService(args, configPath) {
328
- const action = args[0];
329
- switch (action) {
330
- case "install":
331
- console.log(await installService(resolveConfigPath(configPath)));
332
- console.log("rdsh: service installed — it starts on boot and restarts on crash.");
333
- return;
334
- case "status":
335
- console.log(`rdsh service: ${await serviceStatus()}`);
336
- return;
337
- case "uninstall":
338
- console.log(await uninstallService());
339
- return;
340
- default:
341
- throw new Error("usage: rdsh service install|status|uninstall");
430
+ throw new Error("usage: rdsh host user add|passwd|ls|rm");
342
431
  }
343
432
  }
344
433
  /** 打开 hub 数据库(读 hub config 定位 dbPath)。 */
@@ -373,14 +462,14 @@ async function handleHub(args, configPath) {
373
462
  const hubConfigPath = resolveHubConfigPath(configPath);
374
463
  switch (action) {
375
464
  case "install":
376
- console.log(await installService(hubConfigPath, ["hub", "serve"]));
377
- console.log("rdsh: hub service installed starts on boot and restarts on crash.");
465
+ console.log(await installService({ name: HUB_SERVICE_NAME, args: ["hub", "serve"], configPath: hubConfigPath }));
466
+ console.log(`rdsh: hub service installed (${HUB_SERVICE_NAME}) —— 开机自启 + 崩溃重启。`);
378
467
  return;
379
468
  case "status":
380
- console.log(`rdsh hub service: ${await serviceStatus()}`);
469
+ console.log(`rdsh hub service (${HUB_SERVICE_NAME}): ${await serviceStatus(HUB_SERVICE_NAME)}`);
381
470
  return;
382
471
  case "uninstall":
383
- console.log(await uninstallService());
472
+ console.log(await uninstallService(HUB_SERVICE_NAME));
384
473
  return;
385
474
  default:
386
475
  throw new Error("usage: rdsh hub service install|status|uninstall");
@@ -445,7 +534,6 @@ async function handleHubUser(args, configPath) {
445
534
  if (db.getUserByName(name) !== null)
446
535
  throw new Error(`user '${name}' already exists`);
447
536
  if (noPassword) {
448
- // 首次登录自设密码(must_change=1,密码占位不可用)
449
537
  db.createUser(name, "scrypt:disabled", new Date().toISOString(), true);
450
538
  console.log(`rdsh: user '${name}' created — they must set a password on first sign-in (${hubConfigPath})`);
451
539
  }
@@ -489,7 +577,7 @@ async function handleHubUser(args, configPath) {
489
577
  }
490
578
  }
491
579
  finally {
492
- db.close(); // 抛错路径也必须关闭 SQLite 句柄(否则进程挂住)
580
+ db.close();
493
581
  }
494
582
  }
495
583
  async function handleHubHost(args, configPath) {
@@ -557,11 +645,21 @@ function promptPassword(prompt) {
557
645
  });
558
646
  }
559
647
  // 非 TTY(管道/重定向):一次性读完 stdin(EOF),逐行消费。
560
- // 注意:不能每次调用新建 readline —— 第二个 interface 会读不到剩余行。
561
648
  process.stdout.write(prompt);
562
649
  return nextPipeLine();
563
650
  }
564
651
  let pipeLines = null;
652
+ /** 读取一行(echo,用于粘贴 token 等非敏感输入)。 */
653
+ function promptLine(prompt) {
654
+ process.stdout.write(prompt);
655
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
656
+ return new Promise((resolve) => {
657
+ rl.question("", (answer) => {
658
+ rl.close();
659
+ resolve(answer);
660
+ });
661
+ });
662
+ }
565
663
  function nextPipeLine() {
566
664
  if (pipeLines !== null) {
567
665
  return Promise.resolve(pipeLines.shift() ?? "");
@@ -573,6 +671,7 @@ function nextPipeLine() {
573
671
  rl.on("close", () => resolve(pipeLines.shift() ?? ""));
574
672
  });
575
673
  }
576
- // 管理命令(serve/join 常驻不 resolve)完成后显式退出:避免 TTY stdin / 句柄残留导致进程挂住
674
+ // 管理命令(host setup/join/leave/service/user 完成后)显式退出:
675
+ // 常驻命令(host serve / hub serve)不 resolve,靠信号退出。
577
676
  void main().then(() => process.exit(process.exitCode ?? 0));
578
677
  //# sourceMappingURL=bin.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-dsh",
3
- "version": "0.4.7",
3
+ "version": "0.5.0",
4
4
  "description": "Secure remote access for DeepSeek Harness: rdsh serve / rdsh join / rdsh hub",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,11 +23,11 @@
23
23
  "node": ">=22"
24
24
  },
25
25
  "dependencies": {
26
- "rdsh-hub": "0.2.3",
27
- "rdsh-gateway": "0.2.2"
26
+ "rdsh-gateway": "0.3.0",
27
+ "rdsh-hub": "0.3.0"
28
28
  },
29
29
  "scripts": {
30
30
  "build": "tsc -p tsconfig.json",
31
- "test": "node --test test/"
31
+ "test": "node --test \"test/*.test.ts\""
32
32
  }
33
33
  }