@lzm04521/ssh-mcp-server 1.1.5

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 (78) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +157 -0
  3. package/admin-web/dist/assets/index-DXbr5DVR.js +494 -0
  4. package/admin-web/dist/index.html +6 -0
  5. package/admin-web/dist/logo.png +0 -0
  6. package/build/cli/command-line-parser.js +715 -0
  7. package/build/cli/command-line-parser.js.map +1 -0
  8. package/build/cli/run-mode.js +44 -0
  9. package/build/cli/run-mode.js.map +1 -0
  10. package/build/cli/stdio-proxy.js +304 -0
  11. package/build/cli/stdio-proxy.js.map +1 -0
  12. package/build/config/index.js +5 -0
  13. package/build/config/index.js.map +1 -0
  14. package/build/config/server.js +16 -0
  15. package/build/config/server.js.map +1 -0
  16. package/build/core/mcp-http-server.js +14 -0
  17. package/build/core/mcp-http-server.js.map +1 -0
  18. package/build/core/mcp-server.js +95 -0
  19. package/build/core/mcp-server.js.map +1 -0
  20. package/build/index.js +98 -0
  21. package/build/index.js.map +1 -0
  22. package/build/models/admin-types.js +135 -0
  23. package/build/models/admin-types.js.map +1 -0
  24. package/build/models/types.js +2 -0
  25. package/build/models/types.js.map +1 -0
  26. package/build/server/index.js +128 -0
  27. package/build/server/index.js.map +1 -0
  28. package/build/server/routes/admin.js +929 -0
  29. package/build/server/routes/admin.js.map +1 -0
  30. package/build/server/routes/audit.js +9 -0
  31. package/build/server/routes/audit.js.map +1 -0
  32. package/build/server/routes/backups.js +11 -0
  33. package/build/server/routes/backups.js.map +1 -0
  34. package/build/server/routes/mcp.js +52 -0
  35. package/build/server/routes/mcp.js.map +1 -0
  36. package/build/server/routes/settings.js +49 -0
  37. package/build/server/routes/settings.js.map +1 -0
  38. package/build/server/routes/system.js +147 -0
  39. package/build/server/routes/system.js.map +1 -0
  40. package/build/services/audit-store.js +123 -0
  41. package/build/services/audit-store.js.map +1 -0
  42. package/build/services/autostart-service.js +55 -0
  43. package/build/services/autostart-service.js.map +1 -0
  44. package/build/services/backup-scheduler.js +90 -0
  45. package/build/services/backup-scheduler.js.map +1 -0
  46. package/build/services/backup-service.js +113 -0
  47. package/build/services/backup-service.js.map +1 -0
  48. package/build/services/config-store.js +163 -0
  49. package/build/services/config-store.js.map +1 -0
  50. package/build/services/defaults.js +35 -0
  51. package/build/services/defaults.js.map +1 -0
  52. package/build/services/restart-helper.js +43 -0
  53. package/build/services/restart-helper.js.map +1 -0
  54. package/build/services/ssh-connection-manager.js +1775 -0
  55. package/build/services/ssh-connection-manager.js.map +1 -0
  56. package/build/services/update-service.js +93 -0
  57. package/build/services/update-service.js.map +1 -0
  58. package/build/tools/download.js +41 -0
  59. package/build/tools/download.js.map +1 -0
  60. package/build/tools/execute-command.js +53 -0
  61. package/build/tools/execute-command.js.map +1 -0
  62. package/build/tools/index.js +17 -0
  63. package/build/tools/index.js.map +1 -0
  64. package/build/tools/list-directory.js +57 -0
  65. package/build/tools/list-directory.js.map +1 -0
  66. package/build/tools/list-servers.js +51 -0
  67. package/build/tools/list-servers.js.map +1 -0
  68. package/build/tools/upload.js +41 -0
  69. package/build/tools/upload.js.map +1 -0
  70. package/build/utils/logger.js +28 -0
  71. package/build/utils/logger.js.map +1 -0
  72. package/build/utils/ssh-config-parser.js +195 -0
  73. package/build/utils/ssh-config-parser.js.map +1 -0
  74. package/build/utils/status-collector.js +226 -0
  75. package/build/utils/status-collector.js.map +1 -0
  76. package/build/utils/tool-error.js +20 -0
  77. package/build/utils/tool-error.js.map +1 -0
  78. package/package.json +69 -0
@@ -0,0 +1,715 @@
1
+ import { parseArgs } from "node:util";
2
+ import { DEFAULT_ADMIN_PORT } from "../models/admin-types.js";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import os from "os";
6
+ import { lookupSshConfig } from "../utils/ssh-config-parser.js";
7
+ import { Logger } from "../utils/logger.js";
8
+ /**
9
+ * Command line argument parser class
10
+ */
11
+ export class CommandLineParser {
12
+ static DEFAULT_TRANSPORT_MODE = "exec";
13
+ static DEFAULT_SHELL_READY_TIMEOUT_MS = 10000;
14
+ static parseBoolean(value) {
15
+ if (value === undefined) {
16
+ return undefined;
17
+ }
18
+ if (typeof value === "boolean") {
19
+ return value;
20
+ }
21
+ if (typeof value === "string") {
22
+ const normalized = value.trim().toLowerCase();
23
+ if (normalized === "true") {
24
+ return true;
25
+ }
26
+ if (normalized === "false") {
27
+ return false;
28
+ }
29
+ }
30
+ return Boolean(value);
31
+ }
32
+ static parseTransportMode(value) {
33
+ if (value === undefined || value === null || value === "") {
34
+ return undefined;
35
+ }
36
+ if (value === "exec" || value === "shell") {
37
+ return value;
38
+ }
39
+ throw new Error(`transportMode must be either 'exec' or 'shell', got: ${String(value)}`);
40
+ }
41
+ static parseTimeout(value, fieldName) {
42
+ if (value === undefined || value === null || value === "") {
43
+ return undefined;
44
+ }
45
+ const parsed = typeof value === "number" ? value : parseInt(String(value), 10);
46
+ if (!Number.isFinite(parsed) || parsed <= 0) {
47
+ throw new Error(`${fieldName} must be a positive number, got: ${String(value)}`);
48
+ }
49
+ return parsed;
50
+ }
51
+ static parseMaxOutputBytes(value) {
52
+ if (value === undefined || value === null || value === "") {
53
+ return undefined;
54
+ }
55
+ const parsed = typeof value === "number" ? value : Number(String(value));
56
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
57
+ throw new Error(`maxOutputBytes must be a non-negative integer, got: ${String(value)}`);
58
+ }
59
+ return parsed;
60
+ }
61
+ /**
62
+ * Parse command line arguments
63
+ */
64
+ static parseArgs() {
65
+ const { values, positionals } = parseArgs({
66
+ args: process.argv.slice(2),
67
+ options: {
68
+ "config-file": { type: "string" },
69
+ "ssh-config-file": { type: "string" },
70
+ ssh: { type: "string", multiple: true },
71
+ // Compatible with single connection legacy parameters
72
+ host: { type: "string", short: "h" },
73
+ port: { type: "string", short: "p" },
74
+ username: { type: "string", short: "u" },
75
+ password: { type: "string", short: "w" },
76
+ privateKey: { type: "string", short: "k" },
77
+ passphrase: { type: "string", short: "P" },
78
+ agent: { type: "string", short: "a" },
79
+ whitelist: { type: "string", short: "W" },
80
+ blacklist: { type: "string", short: "B" },
81
+ proxy: { type: "string" },
82
+ socksProxy: { type: "string", short: "s" },
83
+ "allowed-local-paths": { type: "string" },
84
+ "allowed-remote-paths": { type: "string" },
85
+ "transport-mode": { type: "string" },
86
+ "shell-ready-timeout": { type: "string" },
87
+ "command-template": { type: "string" },
88
+ pty: { type: "boolean" },
89
+ "try-keyboard": { type: "boolean" },
90
+ "pre-connect": { type: "boolean" },
91
+ admin: { type: "boolean" },
92
+ "admin-port": { type: "string" },
93
+ },
94
+ allowPositionals: true,
95
+ });
96
+ const configMap = {};
97
+ // Priority 1: Load from config file if specified
98
+ if (values["config-file"]) {
99
+ const configFilePath = path.resolve(values["config-file"]);
100
+ if (!fs.existsSync(configFilePath)) {
101
+ throw new Error(`Config file not found: ${configFilePath}`);
102
+ }
103
+ try {
104
+ const configContent = fs.readFileSync(configFilePath, "utf-8");
105
+ const fileConfig = JSON.parse(configContent);
106
+ // Support both array format and object format
107
+ if (Array.isArray(fileConfig)) {
108
+ // Array format: [{name: "dev", host: "...", ...}, ...]
109
+ for (const config of fileConfig) {
110
+ if (!config.name || !config.host || !config.port || !config.username) {
111
+ throw new Error("Each config in array must include name, host, port, username");
112
+ }
113
+ configMap[config.name] = this.normalizeConfig(config);
114
+ }
115
+ }
116
+ else if (typeof fileConfig === "object" && fileConfig !== null) {
117
+ // Object format: {"dev": {host: "...", ...}, "prod": {...}}
118
+ for (const [name, config] of Object.entries(fileConfig)) {
119
+ const normalizedConfig = this.normalizeConfig(config);
120
+ normalizedConfig.name = name;
121
+ configMap[name] = normalizedConfig;
122
+ }
123
+ }
124
+ else {
125
+ throw new Error("Config file must contain an array or object of SSH configurations");
126
+ }
127
+ }
128
+ catch (err) {
129
+ if (err instanceof SyntaxError) {
130
+ throw new Error(`Invalid JSON in config file: ${err.message}`);
131
+ }
132
+ throw err;
133
+ }
134
+ }
135
+ // Priority 2: Parse --ssh parameters (only if no config file was loaded)
136
+ if (Object.keys(configMap).length === 0) {
137
+ const sshParams = Array.isArray(values.ssh)
138
+ ? values.ssh
139
+ : values.ssh
140
+ ? [values.ssh]
141
+ : [];
142
+ for (const sshStr of sshParams) {
143
+ let conf;
144
+ // Try to parse as JSON first
145
+ if (sshStr.trim().startsWith("{")) {
146
+ try {
147
+ const jsonConfig = JSON.parse(sshStr);
148
+ conf = this.normalizeConfig(jsonConfig);
149
+ if (!conf.name) {
150
+ throw new Error("JSON config must include 'name' field");
151
+ }
152
+ }
153
+ catch (err) {
154
+ throw new Error(`Invalid JSON format in --ssh parameter: ${err.message}`);
155
+ }
156
+ }
157
+ else {
158
+ // Fallback to legacy comma-separated format for backward compatibility
159
+ conf = this.parseLegacySshFormat(sshStr);
160
+ }
161
+ if (!conf.name || !conf.host || !conf.port || !conf.username) {
162
+ throw new Error("Each --ssh must include name, host, port, username");
163
+ }
164
+ configMap[conf.name] = conf;
165
+ }
166
+ }
167
+ // Priority 3: skip legacy single-host validation when --admin (GUI 模式允许零连接)
168
+ if (values.admin === true && Object.keys(configMap).length === 0) {
169
+ return {
170
+ configs: configMap,
171
+ preConnect: values["pre-connect"] === true,
172
+ admin: true,
173
+ adminPort: values["admin-port"] ? parseInt(String(values["admin-port"]), 10) : undefined,
174
+ configFile: values["config-file"] ? String(values["config-file"]) : undefined,
175
+ };
176
+ }
177
+ // Priority 3: Compatible with single connection legacy parameters
178
+ if (Object.keys(configMap).length === 0) {
179
+ const host = values.host || positionals[0];
180
+ // 尝试从 SSH config 读取配置
181
+ let sshConfigEntry = null;
182
+ if (host) {
183
+ try {
184
+ sshConfigEntry = lookupSshConfig(host, values["ssh-config-file"]);
185
+ }
186
+ catch (err) {
187
+ // 显式指定配置文件但读取失败时抛错
188
+ throw err;
189
+ }
190
+ }
191
+ const portStr = values.port || positionals[1] || sshConfigEntry?.port?.toString() || "22";
192
+ const username = values.username || positionals[2] || sshConfigEntry?.user;
193
+ const password = values.password || positionals[3];
194
+ const privateKey = values.privateKey || sshConfigEntry?.identityFile;
195
+ const passphrase = values.passphrase || process.env.SSH_MCP_PASSPHRASE;
196
+ const resolvedAgent = values.agent !== undefined
197
+ ? values.agent
198
+ : !password && !privateKey
199
+ ? process.env.SSH_AUTH_SOCK
200
+ : undefined;
201
+ const whitelist = values.whitelist;
202
+ const blacklist = values.blacklist;
203
+ const allowedLocalPaths = values["allowed-local-paths"];
204
+ const allowedRemotePaths = values["allowed-remote-paths"];
205
+ const commandTemplate = values["command-template"];
206
+ const pty = values.pty;
207
+ const tryKeyboard = values["try-keyboard"];
208
+ // 实际连接地址:优先使用 SSH config 的 HostName
209
+ const actualHost = sshConfigEntry?.hostName || host;
210
+ if (!actualHost || !portStr || !username || (!password && !privateKey && !resolvedAgent)) {
211
+ throw new Error("Missing required parameters, need to provide host, port, username and password, private key or agent");
212
+ }
213
+ const port = parseInt(portStr, 10);
214
+ if (isNaN(port)) {
215
+ throw new Error("Port must be a valid number");
216
+ }
217
+ configMap["default"] = this.normalizeConfig({
218
+ name: "default",
219
+ host: actualHost,
220
+ port,
221
+ username,
222
+ password,
223
+ privateKey,
224
+ passphrase,
225
+ agent: resolvedAgent,
226
+ proxy: values.proxy,
227
+ socksProxy: values.socksProxy,
228
+ pty: pty !== undefined ? pty : undefined,
229
+ tryKeyboard: tryKeyboard !== undefined ? tryKeyboard : undefined,
230
+ transportMode: values["transport-mode"],
231
+ shellReadyTimeoutMs: values["shell-ready-timeout"],
232
+ commandTemplate,
233
+ commandWhitelist: whitelist
234
+ ? whitelist
235
+ .split(",")
236
+ .map((pattern) => pattern.trim())
237
+ .filter(Boolean)
238
+ : undefined,
239
+ commandBlacklist: blacklist
240
+ ? blacklist
241
+ .split(",")
242
+ .map((pattern) => pattern.trim())
243
+ .filter(Boolean)
244
+ : undefined,
245
+ allowedLocalPaths: allowedLocalPaths
246
+ ? allowedLocalPaths
247
+ .split(",")
248
+ .map((allowedPath) => allowedPath.trim())
249
+ .filter(Boolean)
250
+ : undefined,
251
+ allowedRemotePaths: allowedRemotePaths
252
+ ? allowedRemotePaths
253
+ .split(",")
254
+ .map((allowedPath) => allowedPath.trim())
255
+ .filter(Boolean)
256
+ : undefined,
257
+ });
258
+ }
259
+ return {
260
+ configs: configMap,
261
+ preConnect: values["pre-connect"] === true,
262
+ admin: values.admin === true,
263
+ adminPort: values["admin-port"] ? parseInt(String(values["admin-port"]), 10) : undefined,
264
+ configFile: values["config-file"] ? String(values["config-file"]) : undefined,
265
+ };
266
+ }
267
+ /**
268
+ * Parse legacy comma-separated format: name=dev,host=1.2.3.4,port=22,user=alice,password=xxx
269
+ * @private
270
+ */
271
+ static parseLegacySshFormat(sshStr) {
272
+ const conf = {};
273
+ const parts = sshStr.split(",");
274
+ for (const part of parts) {
275
+ // Only split on the first '=' to handle values containing '='
276
+ const equalIndex = part.indexOf("=");
277
+ if (equalIndex > 0) {
278
+ const k = part.substring(0, equalIndex).trim();
279
+ const v = part.substring(equalIndex + 1).trim();
280
+ if (k && v) {
281
+ conf[k] = v;
282
+ }
283
+ }
284
+ }
285
+ const port = parseInt(conf.port, 10);
286
+ if (isNaN(port)) {
287
+ throw new Error(`Port for connection ${conf.name || "unknown"} must be a valid number`);
288
+ }
289
+ return this.normalizeConfig(conf);
290
+ }
291
+ /**
292
+ * Normalize SSH config object to ensure proper types and structure
293
+ * @private
294
+ */
295
+ static normalizeConfig(config) {
296
+ const port = typeof config.port === "number"
297
+ ? config.port
298
+ : parseInt(config.port, 10);
299
+ if (isNaN(port)) {
300
+ throw new Error(`Port must be a valid number, got: ${config.port}`);
301
+ }
302
+ return {
303
+ name: config.name,
304
+ host: config.host,
305
+ port,
306
+ username: config.username || config.user,
307
+ password: config.password,
308
+ privateKey: config.privateKey
309
+ ? this.normalizeLocalPath(String(config.privateKey))
310
+ : undefined,
311
+ passphrase: config.passphrase || process.env.SSH_MCP_PASSPHRASE,
312
+ agent: config.agent,
313
+ algorithms: config.algorithms,
314
+ proxy: config.proxy,
315
+ socksProxy: config.socksProxy,
316
+ pty: this.parseBoolean(config.pty),
317
+ tryKeyboard: this.parseBoolean(config.tryKeyboard),
318
+ transportMode: this.parseTransportMode(config.transportMode) ||
319
+ this.DEFAULT_TRANSPORT_MODE,
320
+ shellReadyTimeoutMs: this.parseTimeout(config.shellReadyTimeoutMs, "shellReadyTimeoutMs") || this.DEFAULT_SHELL_READY_TIMEOUT_MS,
321
+ shellCommandTimeoutMs: this.parseTimeout(config.shellCommandTimeoutMs, "shellCommandTimeoutMs"),
322
+ commandTimeoutMs: this.parseTimeout(config.commandTimeoutMs, "commandTimeoutMs"),
323
+ connectionTimeoutMs: this.parseTimeout(config.connectionTimeoutMs, "connectionTimeoutMs"),
324
+ sftpTimeoutMs: this.parseTimeout(config.sftpTimeoutMs, "sftpTimeoutMs"),
325
+ maxOutputBytes: this.parseMaxOutputBytes(config.maxOutputBytes),
326
+ keepaliveIntervalMs: this.parseTimeout(config.keepaliveIntervalMs, "keepaliveIntervalMs"),
327
+ keepaliveCountMax: this.parseTimeout(config.keepaliveCountMax, "keepaliveCountMax"),
328
+ commandWhitelist: Array.isArray(config.commandWhitelist)
329
+ ? config.commandWhitelist
330
+ : config.whitelist
331
+ ? typeof config.whitelist === "string"
332
+ ? config.whitelist.split("|").map((s) => s.trim()).filter(Boolean)
333
+ : config.whitelist
334
+ : undefined,
335
+ commandBlacklist: Array.isArray(config.commandBlacklist)
336
+ ? config.commandBlacklist
337
+ : config.blacklist
338
+ ? typeof config.blacklist === "string"
339
+ ? config.blacklist.split("|").map((s) => s.trim()).filter(Boolean)
340
+ : config.blacklist
341
+ : undefined,
342
+ allowedLocalPaths: Array.isArray(config.allowedLocalPaths)
343
+ ? config.allowedLocalPaths
344
+ .map((allowedPath) => this.normalizeLocalPath(String(allowedPath)))
345
+ .filter(Boolean)
346
+ : typeof config.allowedLocalPaths === "string"
347
+ ? config.allowedLocalPaths
348
+ .split("|")
349
+ .map((allowedPath) => this.normalizeLocalPath(allowedPath.trim()))
350
+ .filter(Boolean)
351
+ : undefined,
352
+ allowedRemotePaths: Array.isArray(config.allowedRemotePaths)
353
+ ? config.allowedRemotePaths
354
+ .map((allowedPath) => this.normalizeRemotePath(String(allowedPath)))
355
+ : typeof config.allowedRemotePaths === "string"
356
+ ? config.allowedRemotePaths
357
+ .split("|")
358
+ .map((allowedPath) => this.normalizeRemotePath(allowedPath.trim()))
359
+ .filter(Boolean)
360
+ : undefined,
361
+ commandTemplate: this.parseCommandTemplate(config.commandTemplate),
362
+ };
363
+ }
364
+ static parseCommandTemplate(value) {
365
+ if (value === undefined || value === null || value === "") {
366
+ return undefined;
367
+ }
368
+ const template = String(value);
369
+ if (!template.includes("<command>") && !template.includes("<quotedCommand>")) {
370
+ throw new Error(`commandTemplate must contain '<command>' or '<quotedCommand>' placeholder, got: ${template}`);
371
+ }
372
+ return template;
373
+ }
374
+ static normalizeLocalPath(localPath) {
375
+ return path.resolve(this.expandHomePath(localPath));
376
+ }
377
+ static expandHomePath(localPath) {
378
+ if (localPath === "~") {
379
+ return os.homedir();
380
+ }
381
+ if (localPath.startsWith("~/")) {
382
+ return path.join(os.homedir(), localPath.slice(2));
383
+ }
384
+ return localPath;
385
+ }
386
+ static normalizeRemotePath(remotePath) {
387
+ if (!remotePath) {
388
+ return "";
389
+ }
390
+ if (!path.posix.isAbsolute(remotePath)) {
391
+ throw new Error(`allowedRemotePaths entries must be absolute POSIX paths, got: ${remotePath}`);
392
+ }
393
+ const normalized = path.posix.normalize(remotePath);
394
+ if (normalized.length > 1 && normalized.endsWith("/")) {
395
+ return normalized.slice(0, -1);
396
+ }
397
+ return normalized;
398
+ }
399
+ static migrateLegacy(raw) {
400
+ if (!raw || typeof raw !== "object" || !raw.connections) {
401
+ return raw;
402
+ }
403
+ Logger.log("检测到旧格式 connections,已迁移至 projects.default.environments.default", "info");
404
+ const hosts = Array.isArray(raw.connections)
405
+ ? Object.fromEntries(raw.connections.map((c) => [c.name, c]))
406
+ : raw.connections;
407
+ raw.projects = raw.projects || {};
408
+ if (!raw.projects.default) {
409
+ raw.projects.default = {
410
+ displayName: "默认项目",
411
+ environments: {},
412
+ };
413
+ }
414
+ if (!raw.projects.default.displayName) {
415
+ raw.projects.default.displayName = "默认项目";
416
+ }
417
+ raw.projects.default.environments = raw.projects.default.environments || {};
418
+ if (!raw.projects.default.environments.default) {
419
+ raw.projects.default.environments.default = {
420
+ displayName: "默认环境",
421
+ hosts: {},
422
+ };
423
+ }
424
+ if (!raw.projects.default.environments.default.displayName) {
425
+ raw.projects.default.environments.default.displayName = "默认环境";
426
+ }
427
+ raw.projects.default.environments.default.hosts =
428
+ raw.projects.default.environments.default.hosts || {};
429
+ for (const [k, v] of Object.entries(hosts)) {
430
+ const key = v?.name ? String(v.name) : k;
431
+ raw.projects.default.environments.default.hosts[key] = v;
432
+ }
433
+ delete raw.connections;
434
+ return raw;
435
+ }
436
+ static buildDefaultHierarchy(hostCfg) {
437
+ return {
438
+ default: {
439
+ displayName: "默认项目",
440
+ environments: {
441
+ default: {
442
+ displayName: "默认环境",
443
+ hosts: {
444
+ default: hostCfg,
445
+ },
446
+ },
447
+ },
448
+ },
449
+ };
450
+ }
451
+ static parse(argv) {
452
+ const args = argv ?? process.argv.slice(2);
453
+ const { values, positionals } = parseArgs({
454
+ args,
455
+ options: {
456
+ "config-file": { type: "string" },
457
+ "ssh-config-file": { type: "string" },
458
+ ssh: { type: "string", multiple: true },
459
+ host: { type: "string", short: "h" },
460
+ port: { type: "string", short: "p" },
461
+ username: { type: "string", short: "u" },
462
+ password: { type: "string", short: "w" },
463
+ privateKey: { type: "string", short: "k" },
464
+ passphrase: { type: "string", short: "P" },
465
+ agent: { type: "string", short: "a" },
466
+ whitelist: { type: "string", short: "W" },
467
+ blacklist: { type: "string", short: "B" },
468
+ proxy: { type: "string" },
469
+ socksProxy: { type: "string", short: "s" },
470
+ "allowed-local-paths": { type: "string" },
471
+ "allowed-remote-paths": { type: "string" },
472
+ "transport-mode": { type: "string" },
473
+ "shell-ready-timeout": { type: "string" },
474
+ "command-template": { type: "string" },
475
+ pty: { type: "boolean" },
476
+ "try-keyboard": { type: "boolean" },
477
+ "pre-connect": { type: "boolean" },
478
+ admin: { type: "boolean" },
479
+ "admin-port": { type: "string" },
480
+ },
481
+ allowPositionals: true,
482
+ });
483
+ // Priority 1: config file
484
+ if (values["config-file"]) {
485
+ const configFilePath = path.resolve(values["config-file"]);
486
+ if (!fs.existsSync(configFilePath)) {
487
+ throw new Error(`Config file not found: ${configFilePath}`);
488
+ }
489
+ const configContent = fs.readFileSync(configFilePath, "utf-8");
490
+ const raw = JSON.parse(configContent);
491
+ // legacy connections migration
492
+ if (raw.connections) {
493
+ this.migrateLegacy(raw);
494
+ return {
495
+ port: raw.port ?? DEFAULT_ADMIN_PORT,
496
+ projects: raw.projects,
497
+ audit: raw.audit,
498
+ backups: raw.backups,
499
+ security: raw.security,
500
+ preConnect: raw.preConnect,
501
+ };
502
+ }
503
+ // new hierarchy file
504
+ if (raw.projects) {
505
+ return {
506
+ port: raw.port ?? DEFAULT_ADMIN_PORT,
507
+ projects: raw.projects,
508
+ audit: raw.audit,
509
+ backups: raw.backups,
510
+ security: raw.security,
511
+ preConnect: raw.preConnect,
512
+ };
513
+ }
514
+ // flat legacy without wrapper: array or object of hosts -> migrate to default/default
515
+ if (Array.isArray(raw)) {
516
+ const hosts = {};
517
+ for (const c of raw) {
518
+ if (c.name)
519
+ hosts[c.name] = this.normalizeConfig(c);
520
+ }
521
+ Logger.log("检测到旧格式 connections,已迁移至 projects.default.environments.default", "info");
522
+ return {
523
+ port: DEFAULT_ADMIN_PORT,
524
+ projects: {
525
+ default: {
526
+ displayName: "默认项目",
527
+ environments: {
528
+ default: { displayName: "默认环境", hosts },
529
+ },
530
+ },
531
+ },
532
+ };
533
+ }
534
+ if (typeof raw === "object" && raw !== null) {
535
+ const maybeHosts = Object.values(raw).some((v) => v && typeof v === "object" && "host" in v);
536
+ if (maybeHosts) {
537
+ const hosts = {};
538
+ for (const [k, v] of Object.entries(raw)) {
539
+ const conf = v;
540
+ const normalized = this.normalizeConfig({ ...conf, name: conf.name || k });
541
+ hosts[normalized.name || k] = normalized;
542
+ }
543
+ Logger.log("检测到旧格式 connections,已迁移至 projects.default.environments.default", "info");
544
+ return {
545
+ port: DEFAULT_ADMIN_PORT,
546
+ projects: {
547
+ default: {
548
+ displayName: "默认项目",
549
+ environments: {
550
+ default: { displayName: "默认环境", hosts },
551
+ },
552
+ },
553
+ },
554
+ };
555
+ }
556
+ }
557
+ return raw;
558
+ }
559
+ // Priority 2: --ssh params -> hierarchy default/default
560
+ const sshParams = Array.isArray(values.ssh)
561
+ ? values.ssh
562
+ : values.ssh
563
+ ? [values.ssh]
564
+ : [];
565
+ if (sshParams.length > 0) {
566
+ const hosts = {};
567
+ for (const sshStr of sshParams) {
568
+ let conf;
569
+ if (sshStr.trim().startsWith("{")) {
570
+ const jsonConfig = JSON.parse(sshStr);
571
+ conf = this.normalizeConfig(jsonConfig);
572
+ if (!conf.name)
573
+ throw new Error("JSON config must include 'name' field");
574
+ }
575
+ else {
576
+ conf = this.parseLegacySshFormat(sshStr);
577
+ }
578
+ if (!conf.name || !conf.host || !conf.port || !conf.username) {
579
+ throw new Error("Each --ssh must include name, host, port, username");
580
+ }
581
+ hosts[conf.name] = conf;
582
+ }
583
+ Logger.log("检测到旧格式 connections,已迁移至 projects.default.environments.default", "info");
584
+ return {
585
+ port: DEFAULT_ADMIN_PORT,
586
+ projects: {
587
+ default: {
588
+ displayName: "默认项目",
589
+ environments: {
590
+ default: { displayName: "默认环境", hosts },
591
+ },
592
+ },
593
+ },
594
+ preConnect: values["pre-connect"] === true,
595
+ admin: values.admin === true,
596
+ adminPort: values["admin-port"] ? parseInt(String(values["admin-port"]), 10) : undefined,
597
+ configFile: values["config-file"] ? String(values["config-file"]) : undefined,
598
+ };
599
+ }
600
+ // Priority: --admin without hosts
601
+ if (values.admin === true) {
602
+ const host = values.host || positionals[0];
603
+ if (!host) {
604
+ return {
605
+ port: DEFAULT_ADMIN_PORT,
606
+ projects: {},
607
+ preConnect: values["pre-connect"] === true,
608
+ admin: true,
609
+ adminPort: values["admin-port"] ? parseInt(String(values["admin-port"]), 10) : undefined,
610
+ configFile: values["config-file"] ? String(values["config-file"]) : undefined,
611
+ };
612
+ }
613
+ }
614
+ // Priority 3: single host -> default/default/default
615
+ const host = values.host || positionals[0];
616
+ if (host) {
617
+ let sshConfigEntry = null;
618
+ try {
619
+ sshConfigEntry = lookupSshConfig(host, values["ssh-config-file"]);
620
+ }
621
+ catch (err) {
622
+ throw err;
623
+ }
624
+ const portStr = values.port ||
625
+ positionals[1] ||
626
+ sshConfigEntry?.port?.toString() ||
627
+ "22";
628
+ const username = values.username ||
629
+ positionals[2] ||
630
+ sshConfigEntry?.user;
631
+ const password = values.password || positionals[3];
632
+ const privateKey = values.privateKey || sshConfigEntry?.identityFile;
633
+ const passphrase = values.passphrase || process.env.SSH_MCP_PASSPHRASE;
634
+ const resolvedAgent = values.agent !== undefined
635
+ ? values.agent
636
+ : !password && !privateKey
637
+ ? process.env.SSH_AUTH_SOCK
638
+ : undefined;
639
+ const whitelist = values.whitelist;
640
+ const blacklist = values.blacklist;
641
+ const allowedLocalPaths = values["allowed-local-paths"];
642
+ const allowedRemotePaths = values["allowed-remote-paths"];
643
+ const commandTemplate = values["command-template"];
644
+ const pty = values.pty;
645
+ const tryKeyboard = values["try-keyboard"];
646
+ const actualHost = sshConfigEntry?.hostName || host;
647
+ if (!actualHost || !portStr || !username || (!password && !privateKey && !resolvedAgent)) {
648
+ throw new Error("Missing required parameters, need to provide host, port, username and password, private key or agent");
649
+ }
650
+ const port = parseInt(portStr, 10);
651
+ if (isNaN(port))
652
+ throw new Error("Port must be a valid number");
653
+ const hostCfg = this.normalizeConfig({
654
+ name: "default",
655
+ host: actualHost,
656
+ port,
657
+ username,
658
+ password,
659
+ privateKey,
660
+ passphrase,
661
+ agent: resolvedAgent,
662
+ proxy: values.proxy,
663
+ socksProxy: values.socksProxy,
664
+ pty: pty !== undefined ? pty : undefined,
665
+ tryKeyboard: tryKeyboard !== undefined ? tryKeyboard : undefined,
666
+ transportMode: values["transport-mode"],
667
+ shellReadyTimeoutMs: values["shell-ready-timeout"],
668
+ commandTemplate,
669
+ commandWhitelist: whitelist
670
+ ? whitelist
671
+ .split(",")
672
+ .map((pattern) => pattern.trim())
673
+ .filter(Boolean)
674
+ : undefined,
675
+ commandBlacklist: blacklist
676
+ ? blacklist
677
+ .split(",")
678
+ .map((pattern) => pattern.trim())
679
+ .filter(Boolean)
680
+ : undefined,
681
+ allowedLocalPaths: allowedLocalPaths
682
+ ? allowedLocalPaths
683
+ .split(",")
684
+ .map((allowedPath) => allowedPath.trim())
685
+ .filter(Boolean)
686
+ : undefined,
687
+ allowedRemotePaths: allowedRemotePaths
688
+ ? allowedRemotePaths
689
+ .split(",")
690
+ .map((allowedPath) => allowedPath.trim())
691
+ .filter(Boolean)
692
+ : undefined,
693
+ });
694
+ Logger.log("单机参数已归入 projects.default.environments.default.hosts.default", "info");
695
+ return {
696
+ port: DEFAULT_ADMIN_PORT,
697
+ projects: this.buildDefaultHierarchy(hostCfg),
698
+ preConnect: values["pre-connect"] === true,
699
+ admin: values.admin === true,
700
+ adminPort: values["admin-port"] ? parseInt(String(values["admin-port"]), 10) : undefined,
701
+ configFile: values["config-file"] ? String(values["config-file"]) : undefined,
702
+ };
703
+ }
704
+ // fallback: empty
705
+ return {
706
+ port: DEFAULT_ADMIN_PORT,
707
+ projects: {},
708
+ preConnect: values["pre-connect"] === true,
709
+ admin: values.admin === true,
710
+ adminPort: values["admin-port"] ? parseInt(String(values["admin-port"]), 10) : undefined,
711
+ configFile: values["config-file"] ? String(values["config-file"]) : undefined,
712
+ };
713
+ }
714
+ }
715
+ //# sourceMappingURL=command-line-parser.js.map