agent-ssh-cli 0.1.3 → 0.2.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.
- package/README.md +44 -3
- package/bin/agentsshcli.js +52 -3
- package/package.json +12 -5
- package/src/cli.js +0 -464
- package/src/config.js +0 -177
- package/src/daemon-client.js +0 -133
- package/src/daemon-paths.js +0 -57
- package/src/ssh-client.js +0 -401
- package/src/ssh-daemon.js +0 -250
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<a href="https://github.com/sleepinginsummer/agent-ssh-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green" alt="License MIT"></a>
|
|
12
12
|
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D18-339933?logo=node.js&logoColor=white" alt="Node.js >=18"></a>
|
|
13
13
|
<a href="https://www.npmjs.com/"><img src="https://img.shields.io/badge/npm-%3E%3D8-CB3837?logo=npm&logoColor=white" alt="npm >=8"></a>
|
|
14
|
-
<a href="https://github.com/sleepinginsummer/agent-ssh-cli"><img src="https://img.shields.io/badge/Windows-
|
|
15
|
-
<a href="https://github.com/sleepinginsummer/agent-ssh-cli/releases"><img src="https://img.shields.io/badge/release-v0.
|
|
14
|
+
<a href="https://github.com/sleepinginsummer/agent-ssh-cli"><img src="https://img.shields.io/badge/Windows-macOS-Linux-0078D6?labelColor=0078D6&color=C0C0C0" alt="Windows/macOS/Linux"></a>
|
|
15
|
+
<a href="https://github.com/sleepinginsummer/agent-ssh-cli/releases"><img src="https://img.shields.io/badge/release-v0.2.0-blue" alt="release v0.2.0"></a>
|
|
16
16
|
<a href="https://github.com/sleepinginsummer/agent-ssh-cli/pulls"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
- 本机网络可访问目标 SSH 服务器
|
|
53
53
|
- 目标服务器已开启 SSH 服务
|
|
54
54
|
- 如使用私钥认证,私钥文件需对当前用户可读
|
|
55
|
-
-
|
|
55
|
+
- 当前版本内部执行器已迁移到 Rust,npm 仍作为安装入口
|
|
56
|
+
- `agentsshcli exec/upload/download` 默认使用 Rust daemon 连接缓存,也可以通过 `--no-cache` 直连
|
|
57
|
+
- 预编译平台包支持 macOS arm64/x64、Linux x64/arm64、Windows x64
|
|
56
58
|
|
|
57
59
|
### 安装步骤
|
|
58
60
|
|
|
@@ -67,6 +69,44 @@ agentsshcli --help
|
|
|
67
69
|
|
|
68
70
|
打开 [SKILL.md](SKILL.md),将其添加到 agent 中。
|
|
69
71
|
|
|
72
|
+
### 本地开发构建
|
|
73
|
+
|
|
74
|
+
当前仓库保留 npm 安装入口,实际执行逻辑由 Rust 原生二进制完成。开发或源码安装时需要先构建原生二进制:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm run build:native
|
|
78
|
+
npm run build:native-bin
|
|
79
|
+
npm run build:native-package
|
|
80
|
+
npm test
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
执行链路:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
agentsshcli 命令
|
|
87
|
+
-> bin/agentsshcli.js
|
|
88
|
+
-> native/target/release/agentsshcli-native
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
已由 Rust 实现:
|
|
92
|
+
|
|
93
|
+
- `agentsshcli list`
|
|
94
|
+
- `agentsshcli init-config`
|
|
95
|
+
- `agentsshcli exec ...` / `agentsshcli exec --no-cache ...`
|
|
96
|
+
- `agentsshcli upload ...` / `agentsshcli upload --no-cache ...`
|
|
97
|
+
- `agentsshcli download ...` / `agentsshcli download --no-cache ...`
|
|
98
|
+
- Rust daemon 连接缓存与 `--cache-ttl`
|
|
99
|
+
|
|
100
|
+
发布 npm 包前先生成对应平台的预编译产物和平台子包,并检查包内容:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm run build:native-package
|
|
104
|
+
npm pack --dry-run
|
|
105
|
+
(cd npm/darwin-arm64 && npm pack --dry-run)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
发布形态为主包 `agent-ssh-cli` 加平台 optional 子包:`@agent-ssh-cli/darwin-arm64`、`@agent-ssh-cli/darwin-x64`、`@agent-ssh-cli/linux-x64`、`@agent-ssh-cli/linux-arm64`、`@agent-ssh-cli/win32-x64`。预编译产物目录格式:`native-bin/<platform>-<arch>/agentsshcli-native`,Windows 文件名为 `agentsshcli-native.exe`。
|
|
109
|
+
|
|
70
110
|
## 配置
|
|
71
111
|
|
|
72
112
|
初始化配置(格式参数和ssh-mcp-server一致):
|
|
@@ -147,6 +187,7 @@ AGENT_SSH_CONFIG=/path/to/config.json
|
|
|
147
187
|
|
|
148
188
|
```bash
|
|
149
189
|
agentsshcli list
|
|
190
|
+
agentsshcli exec --no-cache 密码服务器 "pwd"
|
|
150
191
|
```
|
|
151
192
|
|
|
152
193
|
完成安装!
|
package/bin/agentsshcli.js
CHANGED
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const projectRoot = path.resolve(currentDir, "..");
|
|
11
|
+
|
|
12
|
+
function getExecutableName() {
|
|
13
|
+
return process.platform === "win32" ? "agentsshcli-native.exe" : "agentsshcli-native";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getPlatformPackageBinary() {
|
|
17
|
+
const packageName = `@agent-ssh-cli/${process.platform}-${process.arch}`;
|
|
18
|
+
try {
|
|
19
|
+
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
|
20
|
+
return path.join(path.dirname(packageJsonPath), "bin", getExecutableName());
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getCandidatePaths() {
|
|
27
|
+
const executableName = getExecutableName();
|
|
28
|
+
return [
|
|
29
|
+
getPlatformPackageBinary(),
|
|
30
|
+
path.join(projectRoot, "native-bin", `${process.platform}-${process.arch}`, executableName),
|
|
31
|
+
path.join(projectRoot, "native-bin", executableName),
|
|
32
|
+
path.join(projectRoot, "native", "target", "release", executableName),
|
|
33
|
+
path.join(projectRoot, "native", "target", "debug", executableName)
|
|
34
|
+
].filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function findNativeBinary() {
|
|
38
|
+
return getCandidatePaths().find((candidate) => fs.existsSync(candidate));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const binaryPath = findNativeBinary();
|
|
42
|
+
if (!binaryPath) {
|
|
43
|
+
console.error(`未找到 ${process.platform}-${process.arch} 的 Rust 原生可执行文件,请安装对应平台包或先运行 npm run build:native-bin`);
|
|
6
44
|
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
48
|
+
stdio: "inherit"
|
|
7
49
|
});
|
|
50
|
+
|
|
51
|
+
if (result.error) {
|
|
52
|
+
console.error(result.error.message);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.exit(result.status ?? 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-ssh-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "基于 CLI 的 SSH 代理工具,按 ssh-mcp-server 的能力一一映射",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
|
-
"src/",
|
|
13
12
|
"example.config.json",
|
|
14
13
|
"README.md"
|
|
15
14
|
],
|
|
@@ -17,9 +16,17 @@
|
|
|
17
16
|
"agentsshcli": "bin/agentsshcli.js"
|
|
18
17
|
},
|
|
19
18
|
"scripts": {
|
|
20
|
-
"test": "node --check bin/agentsshcli.js &&
|
|
19
|
+
"test": "node --check bin/agentsshcli.js && cargo test --manifest-path native/Cargo.toml",
|
|
20
|
+
"build:native": "cargo build --release --manifest-path native/Cargo.toml",
|
|
21
|
+
"test:native": "cargo test --manifest-path native/Cargo.toml",
|
|
22
|
+
"build:native-bin": "node scripts/build-native-bin.js",
|
|
23
|
+
"build:native-package": "npm run build:native-bin && node scripts/build-native-package.js"
|
|
21
24
|
},
|
|
22
|
-
"
|
|
23
|
-
"
|
|
25
|
+
"optionalDependencies": {
|
|
26
|
+
"@agent-ssh-cli/darwin-arm64": "0.2.0",
|
|
27
|
+
"@agent-ssh-cli/darwin-x64": "0.2.0",
|
|
28
|
+
"@agent-ssh-cli/linux-x64": "0.2.0",
|
|
29
|
+
"@agent-ssh-cli/win32-x64": "0.2.0",
|
|
30
|
+
"@agent-ssh-cli/linux-arm64": "0.2.0"
|
|
24
31
|
}
|
|
25
32
|
}
|
package/src/cli.js
DELETED
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { loadConfig, findConnection, validateLocalPath, getDefaultConfigPath } from "./config.js";
|
|
6
|
-
import { requestDaemon } from "./daemon-client.js";
|
|
7
|
-
import { normalizeCacheTtl } from "./daemon-paths.js";
|
|
8
|
-
import { executeRemoteCommand, uploadFile, downloadFile } from "./ssh-client.js";
|
|
9
|
-
|
|
10
|
-
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(currentDir, "..", "package.json"), "utf8"));
|
|
12
|
-
|
|
13
|
-
const HELP_TEXT = {
|
|
14
|
-
agentsshcli: `
|
|
15
|
-
用法:
|
|
16
|
-
agentsshcli list [--config <path>] [--json]
|
|
17
|
-
agentsshcli exec [--config <path>] [--no-cache] [--cache-ttl <ms>] <connectionName> <command>
|
|
18
|
-
agentsshcli exec [--config <path>] [--no-cache] [--cache-ttl <ms>] --connection <name> --command <command> [--directory <dir>] [--timeout <ms>]
|
|
19
|
-
agentsshcli upload [--config <path>] [--no-cache] [--cache-ttl <ms>] <connectionName> <localPath> <remotePath>
|
|
20
|
-
agentsshcli upload [--config <path>] [--no-cache] [--cache-ttl <ms>] --connection <name> --local <path> --remote <path>
|
|
21
|
-
agentsshcli download [--config <path>] [--no-cache] [--cache-ttl <ms>] <connectionName> <remotePath> <localPath>
|
|
22
|
-
agentsshcli download [--config <path>] [--no-cache] [--cache-ttl <ms>] --connection <name> --remote <path> --local <path>
|
|
23
|
-
agentsshcli init-config
|
|
24
|
-
agentsshcli help [list|exec|upload|download]
|
|
25
|
-
agentsshcli --help
|
|
26
|
-
agentsshcli --version
|
|
27
|
-
|
|
28
|
-
说明:
|
|
29
|
-
agent-ssh-cli 统一入口。
|
|
30
|
-
`,
|
|
31
|
-
sshls: `
|
|
32
|
-
用法:
|
|
33
|
-
agentsshcli list [--config <path>] [--json]
|
|
34
|
-
agentsshcli help list
|
|
35
|
-
agentsshcli --version
|
|
36
|
-
|
|
37
|
-
说明:
|
|
38
|
-
列出当前配置文件中的 SSH 连接。
|
|
39
|
-
`,
|
|
40
|
-
sshx: `
|
|
41
|
-
用法:
|
|
42
|
-
agentsshcli exec [--config <path>] [--no-cache] [--cache-ttl <ms>] <connectionName> <command>
|
|
43
|
-
agentsshcli exec [--config <path>] [--no-cache] [--cache-ttl <ms>] --connection <name> --command <command> [--directory <dir>] [--timeout <ms>]
|
|
44
|
-
agentsshcli help exec
|
|
45
|
-
agentsshcli --version
|
|
46
|
-
|
|
47
|
-
说明:
|
|
48
|
-
在远端执行命令。
|
|
49
|
-
`,
|
|
50
|
-
sshupload: `
|
|
51
|
-
用法:
|
|
52
|
-
agentsshcli upload [--config <path>] [--no-cache] [--cache-ttl <ms>] <connectionName> <localPath> <remotePath>
|
|
53
|
-
agentsshcli upload [--config <path>] [--no-cache] [--cache-ttl <ms>] --connection <name> --local <path> --remote <path>
|
|
54
|
-
agentsshcli help upload
|
|
55
|
-
agentsshcli --version
|
|
56
|
-
|
|
57
|
-
说明:
|
|
58
|
-
上传本地文件到远端。
|
|
59
|
-
`,
|
|
60
|
-
sshdownload: `
|
|
61
|
-
用法:
|
|
62
|
-
agentsshcli download [--config <path>] [--no-cache] [--cache-ttl <ms>] <connectionName> <remotePath> <localPath>
|
|
63
|
-
agentsshcli download [--config <path>] [--no-cache] [--cache-ttl <ms>] --connection <name> --remote <path> --local <path>
|
|
64
|
-
agentsshcli help download
|
|
65
|
-
agentsshcli --version
|
|
66
|
-
|
|
67
|
-
说明:
|
|
68
|
-
下载远端文件到本地。
|
|
69
|
-
`
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
function printHelp(commandName) {
|
|
73
|
-
process.stdout.write(`${HELP_TEXT[commandName].trim()}\n`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function printVersion() {
|
|
77
|
-
process.stdout.write(`${packageJson.version}\n`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function getHelpName(commandName) {
|
|
81
|
-
const names = {
|
|
82
|
-
list: "sshls",
|
|
83
|
-
exec: "sshx",
|
|
84
|
-
upload: "sshupload",
|
|
85
|
-
download: "sshdownload"
|
|
86
|
-
};
|
|
87
|
-
return names[commandName] || commandName;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function initConfig() {
|
|
91
|
-
const target = getDefaultConfigPath();
|
|
92
|
-
const source = path.join(currentDir, "..", "example.config.json");
|
|
93
|
-
if (fs.existsSync(target)) {
|
|
94
|
-
throw new Error(`${target} 已存在,未覆盖`);
|
|
95
|
-
}
|
|
96
|
-
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
97
|
-
fs.copyFileSync(source, target);
|
|
98
|
-
process.stdout.write(`已创建 ${target}\n`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function parseGlobalArgs(argv) {
|
|
102
|
-
const args = [...argv];
|
|
103
|
-
let configPath = getDefaultConfigPath();
|
|
104
|
-
let help = false;
|
|
105
|
-
let version = false;
|
|
106
|
-
let json = false;
|
|
107
|
-
let noCache = false;
|
|
108
|
-
let cacheTtlMs;
|
|
109
|
-
const remaining = [];
|
|
110
|
-
|
|
111
|
-
while (args.length > 0) {
|
|
112
|
-
const current = args.shift();
|
|
113
|
-
if (current === "--help" || current === "-h") {
|
|
114
|
-
help = true;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (current === "--version" || current === "-v") {
|
|
118
|
-
version = true;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (current === "--json") {
|
|
122
|
-
json = true;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (current === "--no-cache") {
|
|
126
|
-
noCache = true;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (current === "--cache-ttl") {
|
|
130
|
-
const value = args.shift();
|
|
131
|
-
if (!value) {
|
|
132
|
-
throw new Error("--cache-ttl 缺少毫秒值");
|
|
133
|
-
}
|
|
134
|
-
cacheTtlMs = normalizeCacheTtl(value);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (current === "--config") {
|
|
138
|
-
const value = args.shift();
|
|
139
|
-
if (!value) {
|
|
140
|
-
throw new Error("--config 缺少路径");
|
|
141
|
-
}
|
|
142
|
-
configPath = path.resolve(value);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
remaining.push(current, ...args);
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
configPath,
|
|
151
|
-
help,
|
|
152
|
-
version,
|
|
153
|
-
json,
|
|
154
|
-
noCache,
|
|
155
|
-
cacheTtlMs,
|
|
156
|
-
args: remaining
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function findOptionIndexes(args, names) {
|
|
161
|
-
const indexes = [];
|
|
162
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
163
|
-
if (names.includes(args[index])) {
|
|
164
|
-
indexes.push(index);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return indexes;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function takeOption(args, names) {
|
|
171
|
-
const indexes = findOptionIndexes(args, names);
|
|
172
|
-
if (indexes.length > 1) {
|
|
173
|
-
throw new Error(`参数重复声明: ${names[0]}`);
|
|
174
|
-
}
|
|
175
|
-
if (indexes.length === 0) {
|
|
176
|
-
return {
|
|
177
|
-
present: false,
|
|
178
|
-
value: undefined
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const index = indexes[0];
|
|
182
|
-
const value = args[index + 1];
|
|
183
|
-
if (!value || value.startsWith("--")) {
|
|
184
|
-
throw new Error(`${args[index]} 缺少参数值`);
|
|
185
|
-
}
|
|
186
|
-
args.splice(index, 2);
|
|
187
|
-
return {
|
|
188
|
-
present: true,
|
|
189
|
-
value
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function ensureNoUnknownOptions(args) {
|
|
194
|
-
const unknown = args.find((item) => item.startsWith("--"));
|
|
195
|
-
if (unknown) {
|
|
196
|
-
throw new Error(`不支持的参数: ${unknown}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function ensureNoExtraPositionals(args) {
|
|
201
|
-
if (args.length > 0) {
|
|
202
|
-
throw new Error(`存在多余的位置参数: ${args.join(" ")}`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function takePositional(args, fieldName) {
|
|
207
|
-
const value = args.shift();
|
|
208
|
-
if (value && value.startsWith("--")) {
|
|
209
|
-
throw new Error(`${fieldName} 位置参数非法: ${value}`);
|
|
210
|
-
}
|
|
211
|
-
return value;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function ensureNoMixedInput(namedValue, positionalValue, fieldName) {
|
|
215
|
-
if (namedValue !== undefined && positionalValue !== undefined) {
|
|
216
|
-
throw new Error(`${fieldName} 同时使用了命名参数和位置参数,保留一种即可`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function resolveValue(args, optionNames, fieldName) {
|
|
221
|
-
const namedOption = takeOption(args, optionNames);
|
|
222
|
-
if (namedOption.present) {
|
|
223
|
-
return namedOption.value;
|
|
224
|
-
}
|
|
225
|
-
return takePositional(args, fieldName);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function parseListArgs(argv) {
|
|
229
|
-
const parsed = parseGlobalArgs(argv);
|
|
230
|
-
if (parsed.help || parsed.version) {
|
|
231
|
-
return parsed;
|
|
232
|
-
}
|
|
233
|
-
if (parsed.args.length > 0) {
|
|
234
|
-
throw new Error(`agentsshcli list 不接受位置参数: ${parsed.args.join(" ")}`);
|
|
235
|
-
}
|
|
236
|
-
return parsed;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function parseExecuteArgs(argv) {
|
|
240
|
-
const parsed = parseGlobalArgs(argv);
|
|
241
|
-
if (parsed.help || parsed.version) {
|
|
242
|
-
return parsed;
|
|
243
|
-
}
|
|
244
|
-
const args = [...parsed.args];
|
|
245
|
-
const connectionNameOption = takeOption(args, ["--connection", "-c"]);
|
|
246
|
-
const commandOption = takeOption(args, ["--command"]);
|
|
247
|
-
const directoryOption = takeOption(args, ["--directory", "-d"]);
|
|
248
|
-
const timeoutOption = takeOption(args, ["--timeout", "-t"]);
|
|
249
|
-
const connectionNamePositional = takePositional(args, "connectionName");
|
|
250
|
-
const commandPositional = takePositional(args, "command");
|
|
251
|
-
|
|
252
|
-
ensureNoMixedInput(connectionNameOption.present ? connectionNameOption.value : undefined, connectionNamePositional, "connectionName");
|
|
253
|
-
ensureNoMixedInput(commandOption.present ? commandOption.value : undefined, commandPositional, "command");
|
|
254
|
-
|
|
255
|
-
const connectionName = connectionNameOption.present ? connectionNameOption.value : connectionNamePositional;
|
|
256
|
-
const command = commandOption.present ? commandOption.value : commandPositional;
|
|
257
|
-
const directory = directoryOption.present ? directoryOption.value : undefined;
|
|
258
|
-
const timeoutValue = timeoutOption.present ? timeoutOption.value : undefined;
|
|
259
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
260
|
-
if (args[index]?.startsWith("--")) {
|
|
261
|
-
throw new Error(`不支持的参数: ${args[index]}`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
ensureNoUnknownOptions(args);
|
|
265
|
-
ensureNoExtraPositionals(args);
|
|
266
|
-
if (!connectionName || !command) {
|
|
267
|
-
throw new Error("缺少必填参数 connectionName 或 command,使用 --help 查看说明");
|
|
268
|
-
}
|
|
269
|
-
const timeout = timeoutValue === undefined ? 30000 : Number(timeoutValue);
|
|
270
|
-
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
271
|
-
throw new Error("timeout 必须是正整数毫秒值");
|
|
272
|
-
}
|
|
273
|
-
return {
|
|
274
|
-
...parsed,
|
|
275
|
-
connectionName,
|
|
276
|
-
command,
|
|
277
|
-
directory,
|
|
278
|
-
timeout
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function parseTransferArgs(argv, mode) {
|
|
283
|
-
const parsed = parseGlobalArgs(argv);
|
|
284
|
-
if (parsed.help || parsed.version) {
|
|
285
|
-
return parsed;
|
|
286
|
-
}
|
|
287
|
-
const args = [...parsed.args];
|
|
288
|
-
const connectionName = resolveValue(args, ["--connection", "-c"], "connectionName");
|
|
289
|
-
const localOptionNames = ["--local", "-l"];
|
|
290
|
-
const remoteOptionNames = ["--remote", "-r"];
|
|
291
|
-
let localPath;
|
|
292
|
-
let remotePath;
|
|
293
|
-
|
|
294
|
-
if (mode === "upload") {
|
|
295
|
-
localPath = resolveValue(args, localOptionNames, "localPath");
|
|
296
|
-
remotePath = resolveValue(args, remoteOptionNames, "remotePath");
|
|
297
|
-
} else {
|
|
298
|
-
remotePath = resolveValue(args, remoteOptionNames, "remotePath");
|
|
299
|
-
localPath = resolveValue(args, localOptionNames, "localPath");
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
ensureNoUnknownOptions(args);
|
|
303
|
-
ensureNoExtraPositionals(args);
|
|
304
|
-
|
|
305
|
-
if (!connectionName || !localPath || !remotePath) {
|
|
306
|
-
throw new Error("缺少必填参数,使用 --help 查看说明");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
...parsed,
|
|
311
|
-
connectionName,
|
|
312
|
-
localPath,
|
|
313
|
-
remotePath
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export async function runListServers(argv) {
|
|
318
|
-
const parsed = parseListArgs(argv);
|
|
319
|
-
if (parsed.help) {
|
|
320
|
-
printHelp("sshls");
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
if (parsed.version) {
|
|
324
|
-
printVersion();
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const configs = loadConfig(parsed.configPath);
|
|
328
|
-
const output = configs.map((item) => ({
|
|
329
|
-
name: item.name,
|
|
330
|
-
host: item.host,
|
|
331
|
-
port: item.port,
|
|
332
|
-
username: item.username
|
|
333
|
-
}));
|
|
334
|
-
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export async function runExecute(argv) {
|
|
338
|
-
const parsed = parseExecuteArgs(argv);
|
|
339
|
-
if (parsed.help) {
|
|
340
|
-
printHelp("sshx");
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (parsed.version) {
|
|
344
|
-
printVersion();
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const configs = loadConfig(parsed.configPath);
|
|
348
|
-
const connection = findConnection(configs, parsed.connectionName);
|
|
349
|
-
const result = parsed.noCache
|
|
350
|
-
? await executeRemoteCommand(connection, parsed.command, parsed.directory, parsed.timeout)
|
|
351
|
-
: (await requestDaemon(parsed.configPath, {
|
|
352
|
-
operation: "execute",
|
|
353
|
-
configPath: parsed.configPath,
|
|
354
|
-
cwd: process.cwd(),
|
|
355
|
-
connectionName: parsed.connectionName,
|
|
356
|
-
command: parsed.command,
|
|
357
|
-
directory: parsed.directory,
|
|
358
|
-
timeout: parsed.timeout,
|
|
359
|
-
cacheTtlMs: parsed.cacheTtlMs
|
|
360
|
-
})).stdout;
|
|
361
|
-
if (result) {
|
|
362
|
-
process.stdout.write(`${result}\n`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export async function runUpload(argv) {
|
|
367
|
-
const parsed = parseTransferArgs(argv, "upload");
|
|
368
|
-
if (parsed.help) {
|
|
369
|
-
printHelp("sshupload");
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (parsed.version) {
|
|
373
|
-
printVersion();
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const configs = loadConfig(parsed.configPath);
|
|
377
|
-
const connection = findConnection(configs, parsed.connectionName);
|
|
378
|
-
if (parsed.noCache) {
|
|
379
|
-
const validatedLocalPath = validateLocalPath(configs, parsed.localPath);
|
|
380
|
-
await uploadFile(connection, validatedLocalPath, parsed.remotePath);
|
|
381
|
-
} else {
|
|
382
|
-
await requestDaemon(parsed.configPath, {
|
|
383
|
-
operation: "upload",
|
|
384
|
-
configPath: parsed.configPath,
|
|
385
|
-
cwd: process.cwd(),
|
|
386
|
-
connectionName: parsed.connectionName,
|
|
387
|
-
localPath: parsed.localPath,
|
|
388
|
-
remotePath: parsed.remotePath,
|
|
389
|
-
cacheTtlMs: parsed.cacheTtlMs
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
process.stdout.write("File uploaded successfully\n");
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export async function runDownload(argv) {
|
|
396
|
-
const parsed = parseTransferArgs(argv, "download");
|
|
397
|
-
if (parsed.help) {
|
|
398
|
-
printHelp("sshdownload");
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
if (parsed.version) {
|
|
402
|
-
printVersion();
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
const configs = loadConfig(parsed.configPath);
|
|
406
|
-
const connection = findConnection(configs, parsed.connectionName);
|
|
407
|
-
if (parsed.noCache) {
|
|
408
|
-
const validatedLocalPath = validateLocalPath(configs, parsed.localPath);
|
|
409
|
-
fs.mkdirSync(path.dirname(validatedLocalPath), { recursive: true });
|
|
410
|
-
await downloadFile(connection, parsed.remotePath, validatedLocalPath);
|
|
411
|
-
} else {
|
|
412
|
-
await requestDaemon(parsed.configPath, {
|
|
413
|
-
operation: "download",
|
|
414
|
-
configPath: parsed.configPath,
|
|
415
|
-
cwd: process.cwd(),
|
|
416
|
-
connectionName: parsed.connectionName,
|
|
417
|
-
localPath: parsed.localPath,
|
|
418
|
-
remotePath: parsed.remotePath,
|
|
419
|
-
cacheTtlMs: parsed.cacheTtlMs
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
process.stdout.write("File downloaded successfully\n");
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export async function runAgentSshCli(argv) {
|
|
426
|
-
const [command, ...args] = argv;
|
|
427
|
-
if (!command || command === "--help" || command === "-h") {
|
|
428
|
-
printHelp("agentsshcli");
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
if (command === "--version" || command === "-v" || command === "version") {
|
|
432
|
-
printVersion();
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (command === "help") {
|
|
436
|
-
const target = getHelpName(args[0] || "agentsshcli");
|
|
437
|
-
if (!HELP_TEXT[target]) {
|
|
438
|
-
throw new Error(`未知帮助命令: ${args[0]}`);
|
|
439
|
-
}
|
|
440
|
-
printHelp(target);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
if (command === "init-config") {
|
|
444
|
-
initConfig();
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
if (command === "list") {
|
|
448
|
-
await runListServers(args);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
if (command === "exec") {
|
|
452
|
-
await runExecute(args);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
if (command === "upload") {
|
|
456
|
-
await runUpload(args);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
if (command === "download") {
|
|
460
|
-
await runDownload(args);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
throw new Error(`未知命令: ${command},使用 agentsshcli --help 查看说明`);
|
|
464
|
-
}
|