@wxuns/zp-cli 0.0.1
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/.github/workflows/publish.yml +51 -0
- package/DEBUG.md +129 -0
- package/README.md +289 -0
- package/bin/zp-cli.js +84 -0
- package/lib/commands/init.js +93 -0
- package/lib/commands/upload.js +231 -0
- package/lib/core/configManager.js +166 -0
- package/lib/core/gitHelper.js +167 -0
- package/lib/core/sshDeployer.js +384 -0
- package/lib/utils/logger.js +64 -0
- package/package.json +27 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sshDeployer.js - SSH 部署执行模块
|
|
3
|
+
* 负责建立 SSH 连接、上传文件、在远程服务器上解压和部署
|
|
4
|
+
*/
|
|
5
|
+
const { NodeSSH } = require('node-ssh');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 建立 SSH 连接
|
|
14
|
+
* @param {Object} server - 服务器配置对象
|
|
15
|
+
* @returns {Promise<NodeSSH>} 已连接的 SSH 实例
|
|
16
|
+
*/
|
|
17
|
+
async function connect(server) {
|
|
18
|
+
const ssh = new NodeSSH();
|
|
19
|
+
const config = {
|
|
20
|
+
host: server.host,
|
|
21
|
+
port: server.port || 22,
|
|
22
|
+
username: server.username
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 优先使用私钥认证,其次使用密码
|
|
26
|
+
if (server.privateKeyPath && fs.existsSync(server.privateKeyPath)) {
|
|
27
|
+
config.privateKey = server.privateKeyPath;
|
|
28
|
+
} else if (server.password) {
|
|
29
|
+
config.password = server.password;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 忽略主机密钥验证(首次连接时避免交互确认)
|
|
33
|
+
config.readyTimeout = 30000;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await ssh.connect(config);
|
|
37
|
+
return ssh;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new Error(`SSH 连接失败 (${server.host}:${config.port}): ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 在服务器上执行命令
|
|
45
|
+
* @param {NodeSSH} ssh - SSH 实例
|
|
46
|
+
* @param {string} cmd - 要执行的命令
|
|
47
|
+
* @returns {Promise<{ stdout: string, stderr: string, code: number }>}
|
|
48
|
+
*/
|
|
49
|
+
async function execRemote(ssh, cmd) {
|
|
50
|
+
const result = await ssh.execCommand(cmd);
|
|
51
|
+
return {
|
|
52
|
+
stdout: result.stdout,
|
|
53
|
+
stderr: result.stderr,
|
|
54
|
+
code: result.code
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 使用 expect 脚本切换到 root 并执行多条命令
|
|
60
|
+
* 参考 vue_zte3.0/build/deploy.exp 的实现
|
|
61
|
+
*
|
|
62
|
+
* @param {NodeSSH} ssh - SSH 实例
|
|
63
|
+
* @param {string[]} cmds - 需要 root 权限执行的命令数组
|
|
64
|
+
* @param {string} rootPassword - root 密码
|
|
65
|
+
* @returns {Promise<{ stdout: string, stderr: string, code: number }>}
|
|
66
|
+
*/
|
|
67
|
+
async function execAsRoot(ssh, cmds, rootPassword) {
|
|
68
|
+
if (!rootPassword) {
|
|
69
|
+
throw new Error('未配置 rootPassword,无法执行提权操作');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pw = rootPassword.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
73
|
+
|
|
74
|
+
// 为每条命令生成 expect 片段:send "cmd\r" + expect "#"
|
|
75
|
+
const cmdLines = cmds.map(c => {
|
|
76
|
+
const escaped = c.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
77
|
+
return `send \\"${escaped}\\r\\"\nexpect \\"#\\"`;
|
|
78
|
+
}).join('\n');
|
|
79
|
+
|
|
80
|
+
// 完整 expect 脚本,与 deploy.exp 行为一致:
|
|
81
|
+
// spawn su root → 匹配密码提示 → 等待 # → 逐条执行命令 → exit
|
|
82
|
+
const script = `expect -c "
|
|
83
|
+
set timeout 120
|
|
84
|
+
spawn su root
|
|
85
|
+
expect {
|
|
86
|
+
-re \\"口令|Password|密码\\" { send \\"${pw}\\r\\" }
|
|
87
|
+
}
|
|
88
|
+
expect \\"#\\"
|
|
89
|
+
send \\"\\r\\"
|
|
90
|
+
expect \\"#\\"
|
|
91
|
+
${cmdLines}
|
|
92
|
+
send \\"exit\\r\\"
|
|
93
|
+
expect eof
|
|
94
|
+
"`;
|
|
95
|
+
|
|
96
|
+
return execRemote(ssh, script);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 使用本地 tar 命令将目录打包成 .tar.gz 文件
|
|
101
|
+
* @param {string} dirPath - 要打包的目录路径
|
|
102
|
+
* @returns {string} 生成的临时 tar.gz 文件路径
|
|
103
|
+
*/
|
|
104
|
+
function createTarball(dirPath) {
|
|
105
|
+
const absDir = path.resolve(dirPath);
|
|
106
|
+
if (!fs.existsSync(absDir)) {
|
|
107
|
+
throw new Error(`目录不存在: ${absDir}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const dirName = path.basename(absDir);
|
|
111
|
+
const tmpFile = path.join(os.tmpdir(), `zp-cli-${dirName}-${Date.now()}.tar.gz`);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Windows 上使用 Git Bash 自带的 tar,或系统 tar
|
|
115
|
+
execSync(`tar -czf "${tmpFile}" -C "${path.dirname(absDir)}" "${dirName}"`, {
|
|
116
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
117
|
+
});
|
|
118
|
+
return tmpFile;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
throw new Error(`打包目录失败: ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取文件/目录大小(字节)
|
|
126
|
+
* @param {string} filePath - 文件或目录路径
|
|
127
|
+
* @returns {number} 大小(字节)
|
|
128
|
+
*/
|
|
129
|
+
function getSize(filePath) {
|
|
130
|
+
const stat = fs.statSync(filePath);
|
|
131
|
+
if (stat.isDirectory()) {
|
|
132
|
+
// 对于目录,粗略计算大小
|
|
133
|
+
try {
|
|
134
|
+
const output = execSync(`du -sh "${filePath}"`, { encoding: 'utf-8' });
|
|
135
|
+
const sizeStr = output.split('\t')[0].trim();
|
|
136
|
+
return parseHumanSize(sizeStr);
|
|
137
|
+
} catch {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return stat.size;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 解析人类可读的文件大小字符串
|
|
146
|
+
* @param {string} str - 如 "2.3M", "1.5G"
|
|
147
|
+
* @returns {number} 字节数
|
|
148
|
+
*/
|
|
149
|
+
function parseHumanSize(str) {
|
|
150
|
+
const units = { K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024 };
|
|
151
|
+
const match = str.match(/^([\d.]+)([KMG]?)$/i);
|
|
152
|
+
if (!match) return 0;
|
|
153
|
+
const num = parseFloat(match[1]);
|
|
154
|
+
const unit = (match[2] || '').toUpperCase();
|
|
155
|
+
return num * (units[unit] || 1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 格式化文件大小为人类可读字符串
|
|
160
|
+
* @param {number} bytes - 字节数
|
|
161
|
+
* @returns {string} 格式化后的字符串
|
|
162
|
+
*/
|
|
163
|
+
function formatSize(bytes) {
|
|
164
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
165
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
166
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
167
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 执行完整的部署流程
|
|
172
|
+
* @param {Object} options - 部署参数
|
|
173
|
+
* @param {Object} options.server - 服务器配置
|
|
174
|
+
* @param {string} options.localPath - 本地文件/目录路径
|
|
175
|
+
* @param {string} options.remoteBase - 远程目标基础路径
|
|
176
|
+
* @param {string} options.relativePath - 文件相对于基础路径的相对路径
|
|
177
|
+
* @param {boolean} [options.isDir] - 是否为目录上传
|
|
178
|
+
* @param {Object} [options.permissions] - 权限配置 { user, group }
|
|
179
|
+
*/
|
|
180
|
+
async function deploy(options) {
|
|
181
|
+
const { server, localPath, remoteBase, relativePath, isDir, permissions } = options;
|
|
182
|
+
const ssh = new NodeSSH();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// 1. 连接服务器
|
|
186
|
+
logger.progress(`正在连接服务器 ${server.host}:${server.port || 22} ...`);
|
|
187
|
+
await connectToServer(ssh, server);
|
|
188
|
+
logger.success('SSH 连接成功');
|
|
189
|
+
|
|
190
|
+
const tmpFiles = []; // 记录需要清理的临时文件
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
let uploadFile = localPath;
|
|
194
|
+
let remoteFileName;
|
|
195
|
+
|
|
196
|
+
if (isDir) {
|
|
197
|
+
// 2a. 目录上传:先打包
|
|
198
|
+
logger.progress('正在打包目录 ...');
|
|
199
|
+
uploadFile = createTarball(localPath);
|
|
200
|
+
tmpFiles.push({ local: uploadFile, remote: null });
|
|
201
|
+
|
|
202
|
+
const size = getSize(uploadFile);
|
|
203
|
+
logger.progress(`打包完成,大小: ${formatSize(size)}`);
|
|
204
|
+
|
|
205
|
+
remoteFileName = `zp-cli-tmp-${Date.now()}.tar.gz`;
|
|
206
|
+
} else {
|
|
207
|
+
// 2b. 单文件上传
|
|
208
|
+
const size = getSize(localPath);
|
|
209
|
+
logger.progress(`准备上传文件,大小: ${formatSize(size)}`);
|
|
210
|
+
remoteFileName = path.basename(localPath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 3. 计算远程目标路径
|
|
214
|
+
const remoteTmpDir = '/tmp/zp-cli-deploy';
|
|
215
|
+
const remoteTmpPath = `${remoteTmpDir}/${remoteFileName}`;
|
|
216
|
+
const finalPath = relativePath
|
|
217
|
+
? `${remoteBase}/${relativePath}`
|
|
218
|
+
: remoteBase;
|
|
219
|
+
|
|
220
|
+
// 4. 创建临时目录
|
|
221
|
+
await execRemote(ssh, `mkdir -p ${remoteTmpDir}`);
|
|
222
|
+
|
|
223
|
+
// 5. 上传文件
|
|
224
|
+
logger.progress('正在上传文件 ...');
|
|
225
|
+
await ssh.putFile(uploadFile, remoteTmpPath);
|
|
226
|
+
logger.success('文件上传完成');
|
|
227
|
+
|
|
228
|
+
// 6. 创建目标目录
|
|
229
|
+
const targetDir = path.posix.dirname(finalPath);
|
|
230
|
+
const needRoot = !!server.rootPassword;
|
|
231
|
+
|
|
232
|
+
if (needRoot) {
|
|
233
|
+
// ── 提权模式:参考 deploy.exp,一次性执行所有命令 ──
|
|
234
|
+
logger.info('检测到 rootPassword,将使用提权模式执行');
|
|
235
|
+
logger.progress('正在解压部署 ...');
|
|
236
|
+
|
|
237
|
+
const cmds = [`mkdir -p ${targetDir}`];
|
|
238
|
+
|
|
239
|
+
if (isDir) {
|
|
240
|
+
cmds.push(`tar -xzf ${remoteTmpPath} -C ${targetDir}`);
|
|
241
|
+
cmds.push(`rm -f ${remoteTmpPath}`);
|
|
242
|
+
} else {
|
|
243
|
+
cmds.push(`cp ${remoteTmpPath} ${finalPath}`);
|
|
244
|
+
cmds.push(`rm -f ${remoteTmpPath}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 权限操作:cd 到目标目录后对 * 操作(与原 deploy.exp 一致)
|
|
248
|
+
if (permissions) {
|
|
249
|
+
cmds.push(`cd ${targetDir}`);
|
|
250
|
+
if (permissions.user) {
|
|
251
|
+
cmds.push(`chown -R ${permissions.user} *`);
|
|
252
|
+
}
|
|
253
|
+
if (permissions.group) {
|
|
254
|
+
cmds.push(`chgrp -R ${permissions.group} *`);
|
|
255
|
+
}
|
|
256
|
+
// 让普通用户能读取目录链(root mkdir -p / tar 创建的目录可能不允许普通用户访问)
|
|
257
|
+
cmds.push(`chmod o+rx ${remoteBase}`);
|
|
258
|
+
if (targetDir !== remoteBase) {
|
|
259
|
+
cmds.push(`chmod o+rx ${targetDir}`);
|
|
260
|
+
}
|
|
261
|
+
if (finalPath !== targetDir) {
|
|
262
|
+
cmds.push(`chmod o+rx ${finalPath}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ls -l 在 expect 里用 root 执行
|
|
267
|
+
cmds.push(`ls -l ${finalPath}`);
|
|
268
|
+
|
|
269
|
+
const result = await execAsRoot(ssh, cmds, server.rootPassword);
|
|
270
|
+
|
|
271
|
+
if (result.code !== 0) {
|
|
272
|
+
throw new Error(`部署失败: ${result.stderr}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
logger.success('解压完成');
|
|
276
|
+
|
|
277
|
+
// 从 expect 输出中提取 ls -l 结果(只保留 total 行和文件权限行)
|
|
278
|
+
if (result.stdout) {
|
|
279
|
+
const lines = result.stdout.split(/\r?\n/);
|
|
280
|
+
const filtered = lines.filter(l => /^(total\s|[-d])/i.test(l.trim()));
|
|
281
|
+
if (filtered.length > 0) {
|
|
282
|
+
logger.newline();
|
|
283
|
+
logger.info(`远程目录: ${finalPath}`);
|
|
284
|
+
logger.log(filtered.join('\n'));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (permissions) {
|
|
289
|
+
logger.info(`已设置权限: ${permissions.user || '-'}:${permissions.group || '-'}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
} else {
|
|
293
|
+
// ── 普通模式:无需提权 ──
|
|
294
|
+
await execRemote(ssh, `mkdir -p ${targetDir}`);
|
|
295
|
+
|
|
296
|
+
if (isDir) {
|
|
297
|
+
logger.progress('正在解压部署 ...');
|
|
298
|
+
const result = await execRemote(ssh, `tar -xzf ${remoteTmpPath} -C ${targetDir}`);
|
|
299
|
+
if (result.code !== 0) {
|
|
300
|
+
throw new Error(`解压失败: ${result.stderr}`);
|
|
301
|
+
}
|
|
302
|
+
logger.success('解压完成');
|
|
303
|
+
} else {
|
|
304
|
+
logger.progress('正在部署文件 ...');
|
|
305
|
+
const result = await execRemote(ssh, `cp ${remoteTmpPath} ${finalPath}`);
|
|
306
|
+
if (result.code !== 0) {
|
|
307
|
+
throw new Error(`文件部署失败: ${result.stderr}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 清理临时文件
|
|
312
|
+
await execRemote(ssh, `rm -f ${remoteTmpPath}`);
|
|
313
|
+
|
|
314
|
+
// 设置权限
|
|
315
|
+
if (permissions) {
|
|
316
|
+
const { user, group } = permissions;
|
|
317
|
+
if (user) {
|
|
318
|
+
await execRemote(ssh, `chown -R ${user} ${targetDir}`);
|
|
319
|
+
}
|
|
320
|
+
if (group) {
|
|
321
|
+
await execRemote(ssh, `chgrp -R ${group} ${targetDir}`);
|
|
322
|
+
}
|
|
323
|
+
logger.info(`已设置权限: ${user || '-'}:${group || '-'}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 清理远程临时文件(提权模式已在 expect 中清理)
|
|
328
|
+
if (!needRoot) {
|
|
329
|
+
await execRemote(ssh, `rm -f ${remoteTmpPath}`);
|
|
330
|
+
}
|
|
331
|
+
logger.success('远程临时文件已清理');
|
|
332
|
+
|
|
333
|
+
logger.newline();
|
|
334
|
+
logger.success(`部署成功!远程路径: ${finalPath}`);
|
|
335
|
+
|
|
336
|
+
} finally {
|
|
337
|
+
// 清理本地临时文件
|
|
338
|
+
for (const tmp of tmpFiles) {
|
|
339
|
+
if (tmp.local && fs.existsSync(tmp.local)) {
|
|
340
|
+
try { fs.unlinkSync(tmp.local); } catch {}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// 关闭 SSH 连接
|
|
344
|
+
ssh.dispose();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
} catch (err) {
|
|
348
|
+
ssh.dispose();
|
|
349
|
+
throw err;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 建立 SSH 连接(内部方法)
|
|
355
|
+
*/
|
|
356
|
+
async function connectToServer(ssh, server) {
|
|
357
|
+
const config = {
|
|
358
|
+
host: server.host,
|
|
359
|
+
port: server.port || 22,
|
|
360
|
+
username: server.username,
|
|
361
|
+
readyTimeout: 30000
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
if (server.privateKeyPath && fs.existsSync(server.privateKeyPath)) {
|
|
365
|
+
config.privateKey = fs.readFileSync(server.privateKeyPath, 'utf-8');
|
|
366
|
+
} else if (server.password) {
|
|
367
|
+
config.password = server.password;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await ssh.connect(config);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
throw new Error(`SSH 连接失败 (${server.host}:${config.port}): ${err.message}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
connect,
|
|
379
|
+
execRemote,
|
|
380
|
+
execAsRoot,
|
|
381
|
+
createTarball,
|
|
382
|
+
deploy,
|
|
383
|
+
formatSize
|
|
384
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger.js - 终端日志输出工具
|
|
3
|
+
* 使用 chalk 为终端输出添加颜色和图标
|
|
4
|
+
*/
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
const logger = {
|
|
8
|
+
/**
|
|
9
|
+
* 输出成功信息(绿色 ✓)
|
|
10
|
+
* @param {string} msg - 日志信息
|
|
11
|
+
*/
|
|
12
|
+
success(msg) {
|
|
13
|
+
console.log(chalk.green(`✓ ${msg}`));
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 输出普通信息(蓝色 ℹ)
|
|
18
|
+
* @param {string} msg - 日志信息
|
|
19
|
+
*/
|
|
20
|
+
info(msg) {
|
|
21
|
+
console.log(chalk.blue(`ℹ ${msg}`));
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 输出警告信息(黄色 ⚠)
|
|
26
|
+
* @param {string} msg - 日志信息
|
|
27
|
+
*/
|
|
28
|
+
warn(msg) {
|
|
29
|
+
console.log(chalk.yellow(`⚠ ${msg}`));
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 输出错误信息(红色 ✗)
|
|
34
|
+
* @param {string} msg - 日志信息
|
|
35
|
+
*/
|
|
36
|
+
error(msg) {
|
|
37
|
+
console.error(chalk.red(`✗ ${msg}`));
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 输出进度信息(青色 →)
|
|
42
|
+
* @param {string} msg - 日志信息
|
|
43
|
+
*/
|
|
44
|
+
progress(msg) {
|
|
45
|
+
console.log(chalk.cyan(`→ ${msg}`));
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 输出普通文本(无前缀)
|
|
50
|
+
* @param {string} msg - 日志信息
|
|
51
|
+
*/
|
|
52
|
+
log(msg) {
|
|
53
|
+
console.log(msg);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 输出空行
|
|
58
|
+
*/
|
|
59
|
+
newline() {
|
|
60
|
+
console.log('');
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = logger;
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wxuns/zp-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "一个高效的命令行部署工具,支持将本地代码通过 SSH 部署到远程服务器",
|
|
5
|
+
"main": "bin/zp-cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zp-cli": "./bin/zp-cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node bin/zp-cli.js --help"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"deploy",
|
|
15
|
+
"ssh",
|
|
16
|
+
"upload",
|
|
17
|
+
"server"
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^4.1.2",
|
|
23
|
+
"commander": "^9.4.1",
|
|
24
|
+
"inquirer": "^8.2.5",
|
|
25
|
+
"node-ssh": "^12.0.2"
|
|
26
|
+
}
|
|
27
|
+
}
|