ylyx-cli 1.0.3 → 1.0.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.
- package/bin/ylyx.js +4 -2
- package/lib/deploy.js +144 -114
- package/package.json +2 -3
package/bin/ylyx.js
CHANGED
|
@@ -114,23 +114,25 @@ program
|
|
|
114
114
|
program
|
|
115
115
|
.command('deploy')
|
|
116
116
|
.description('部署:递归上传本地目录到远端目录(读取当前项目 .ylyxrc.json 的 deploy 字段)')
|
|
117
|
-
.option('--env <env>', '部署环境:test|prod(不传会提示选择)')
|
|
118
117
|
.option('--host <host>', '覆盖 deploy.host')
|
|
119
118
|
.option('--port <port>', '覆盖 deploy.port')
|
|
120
119
|
.option('-u, --username <username>', '覆盖 deploy.username')
|
|
121
120
|
.option('-p, --password <password>', '覆盖 deploy.password')
|
|
122
121
|
.option('--local <path>', '覆盖 deploy.localDir(默认 ./EXTERNAL_DIGIC)')
|
|
123
122
|
.option('--remote <path>', '覆盖 deploy.remoteDir')
|
|
123
|
+
.option('--zip', '部署后把 localDir 压缩成 zip(参考 compressing.zip.compressDir)')
|
|
124
|
+
.option('--zip-out <path>', 'zip 输出目录(默认:./.ylyx-deploy)')
|
|
124
125
|
.action(async (options) => {
|
|
125
126
|
try {
|
|
126
127
|
await deploy({
|
|
127
|
-
env: options.env,
|
|
128
128
|
host: options.host,
|
|
129
129
|
port: options.port,
|
|
130
130
|
username: options.username,
|
|
131
131
|
password: options.password,
|
|
132
132
|
localDir: options.local,
|
|
133
133
|
remoteDir: options.remote,
|
|
134
|
+
zipAfter: !!options.zip,
|
|
135
|
+
zipOutDir: options.zipOut,
|
|
134
136
|
});
|
|
135
137
|
} catch (error) {
|
|
136
138
|
console.error('❌ 部署失败:', error.message);
|
package/lib/deploy.js
CHANGED
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const Client = require('ssh2-sftp-client');
|
|
2
|
+
const fs = require('fs-extra');
|
|
5
3
|
const ora = require('ora');
|
|
6
|
-
const prompts = require('prompts');
|
|
7
|
-
const archiver = require('archiver');
|
|
8
4
|
const { readProjectConfig } = require('./utils');
|
|
9
5
|
|
|
10
6
|
let spinner = null; // 加载实例
|
|
11
7
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function formatTimestampForFolderName(date = new Date()) {
|
|
17
|
-
// 用户要求:YYYY-MM-DD HH:mm:ss(注意:该字符串用于 zip 内的顶层目录名,不受 Windows 文件名限制)
|
|
8
|
+
function formatTimestampForZipName(date = new Date()) {
|
|
9
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
18
10
|
const yyyy = date.getFullYear();
|
|
19
11
|
const MM = pad2(date.getMonth() + 1);
|
|
20
12
|
const dd = pad2(date.getDate());
|
|
21
13
|
const HH = pad2(date.getHours());
|
|
22
14
|
const mm = pad2(date.getMinutes());
|
|
23
15
|
const ss = pad2(date.getSeconds());
|
|
24
|
-
|
|
16
|
+
// zip 文件名避免冒号
|
|
17
|
+
return `${yyyy}-${MM}-${dd}_${HH}-${mm}-${ss}`;
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
function formatBytes(bytes) {
|
|
21
|
+
const n = Number(bytes) || 0;
|
|
22
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
23
|
+
let v = n;
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
26
|
+
v /= 1024;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
const fixed = i === 0 ? 0 : v < 10 ? 2 : v < 100 ? 1 : 0;
|
|
30
|
+
return `${v.toFixed(fixed)}${units[i]}`;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
function
|
|
33
|
+
function listLocalFilesWithSize(rootDir) {
|
|
36
34
|
const out = [];
|
|
37
35
|
const stack = [rootDir];
|
|
38
36
|
while (stack.length) {
|
|
@@ -42,55 +40,83 @@ function listFilesRecursively(rootDir) {
|
|
|
42
40
|
const p = path.join(current, e);
|
|
43
41
|
const st = fs.statSync(p);
|
|
44
42
|
if (st.isDirectory()) stack.push(p);
|
|
45
|
-
else if (st.isFile())
|
|
43
|
+
else if (st.isFile()) {
|
|
44
|
+
out.push({
|
|
45
|
+
localPath: p,
|
|
46
|
+
relPosix: path.relative(rootDir, p).replace(/\\/g, '/'),
|
|
47
|
+
size: st.size,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
52
|
+
out.sort((a, b) => a.relPosix.localeCompare(b.relPosix));
|
|
50
53
|
return out;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
+
async function uploadDirWithProgress({ sftp, localDir, remoteDirPosix, onProgress }) {
|
|
57
|
+
const files = listLocalFilesWithSize(localDir);
|
|
58
|
+
const totalBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
59
|
+
if (files.length === 0) return { totalBytes: 0, uploadedBytes: 0, fileCount: 0 };
|
|
60
|
+
|
|
61
|
+
// 先创建需要的目录(去重)
|
|
62
|
+
const dirSet = new Set();
|
|
56
63
|
for (const f of files) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
|
|
65
|
+
dirSet.add(path.posix.dirname(remoteFile));
|
|
66
|
+
}
|
|
67
|
+
// 目录从浅到深创建,避免多余失败
|
|
68
|
+
const dirs = Array.from(dirSet).sort((a, b) => a.length - b.length);
|
|
69
|
+
for (const d of dirs) {
|
|
70
|
+
await sftp.mkdir(d, true);
|
|
64
71
|
}
|
|
65
|
-
return h.digest('hex').slice(0, 8);
|
|
66
|
-
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
output.on('error', reject);
|
|
79
|
-
archive.on('warning', (err) => {
|
|
80
|
-
// 非致命 warning(如 ENOENT)按 warning 处理;其它仍抛出
|
|
81
|
-
if (err && err.code === 'ENOENT') return;
|
|
82
|
-
reject(err);
|
|
83
|
-
});
|
|
84
|
-
archive.on('error', reject);
|
|
73
|
+
let uploadedBytes = 0;
|
|
74
|
+
let lastRenderAt = 0;
|
|
75
|
+
const render = (state) => {
|
|
76
|
+
if (typeof onProgress !== 'function') return;
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
// 轻微节流,避免刷屏
|
|
79
|
+
if (now - lastRenderAt < 80 && state && state.force !== true) return;
|
|
80
|
+
lastRenderAt = now;
|
|
81
|
+
onProgress(state);
|
|
82
|
+
};
|
|
85
83
|
|
|
86
|
-
|
|
84
|
+
for (let i = 0; i < files.length; i++) {
|
|
85
|
+
const f = files[i];
|
|
86
|
+
const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
|
|
87
|
+
let lastTransferred = 0;
|
|
88
|
+
|
|
89
|
+
await sftp.fastPut(f.localPath, remoteFile, {
|
|
90
|
+
step: (transferred) => {
|
|
91
|
+
// transferred:当前文件已传输的总字节数(累计)
|
|
92
|
+
const delta = Math.max(0, Number(transferred || 0) - lastTransferred);
|
|
93
|
+
lastTransferred = Number(transferred || 0);
|
|
94
|
+
uploadedBytes += delta;
|
|
95
|
+
render({
|
|
96
|
+
uploadedBytes,
|
|
97
|
+
totalBytes,
|
|
98
|
+
fileIndex: i + 1,
|
|
99
|
+
fileCount: files.length,
|
|
100
|
+
currentFile: f.relPosix,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
87
104
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
// fastPut 有时不会触发 step(极小文件/实现差异),这里兜底补齐本文件剩余字节
|
|
106
|
+
if (lastTransferred < f.size) {
|
|
107
|
+
uploadedBytes += f.size - lastTransferred;
|
|
108
|
+
}
|
|
109
|
+
render({
|
|
110
|
+
uploadedBytes,
|
|
111
|
+
totalBytes,
|
|
112
|
+
fileIndex: i + 1,
|
|
113
|
+
fileCount: files.length,
|
|
114
|
+
currentFile: f.relPosix,
|
|
115
|
+
force: true,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
92
118
|
|
|
93
|
-
return {
|
|
119
|
+
return { totalBytes, uploadedBytes, fileCount: files.length };
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
function resolveDeployConfig(overrides = {}) {
|
|
@@ -104,6 +130,8 @@ function resolveDeployConfig(overrides = {}) {
|
|
|
104
130
|
password: overrides.password ?? deploy.password,
|
|
105
131
|
localDir: overrides.localDir ?? deploy.localDir ?? './EXTERNAL_DIGIC',
|
|
106
132
|
remoteDir: overrides.remoteDir ?? deploy.remoteDir ?? '/usr/local/nginx/html/EXTERNAL_DIGIC',
|
|
133
|
+
zipAfter: overrides.zipAfter ?? deploy.zipAfter ?? false,
|
|
134
|
+
zipOutDir: overrides.zipOutDir ?? deploy.zipOutDir ?? path.join(process.cwd(), '.ylyx-deploy'),
|
|
107
135
|
};
|
|
108
136
|
|
|
109
137
|
return cfg;
|
|
@@ -111,66 +139,33 @@ function resolveDeployConfig(overrides = {}) {
|
|
|
111
139
|
|
|
112
140
|
/**
|
|
113
141
|
* 执行部署(递归上传目录到远端)
|
|
114
|
-
*
|
|
115
|
-
* 正式服:只压缩(不上传)
|
|
116
|
-
* @param {{env?:'test'|'prod',host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string}} overrides
|
|
142
|
+
* @param {{host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string}} overrides
|
|
117
143
|
*/
|
|
118
144
|
async function deploy(overrides = {}) {
|
|
119
145
|
const cfg = resolveDeployConfig(overrides);
|
|
120
146
|
const localFolderPath = cfg.localDir;
|
|
121
147
|
const remoteFolderPath = cfg.remoteDir;
|
|
122
148
|
|
|
123
|
-
// 选择部署环境(prompts)
|
|
124
|
-
let env = overrides.env;
|
|
125
|
-
if (!env) {
|
|
126
|
-
const ans = await prompts({
|
|
127
|
-
type: 'select',
|
|
128
|
-
name: 'env',
|
|
129
|
-
message: '请选择部署环境',
|
|
130
|
-
choices: [
|
|
131
|
-
{ title: '测试服(压缩 + 上传)', value: 'test' },
|
|
132
|
-
{ title: '正式服(只压缩)', value: 'prod' },
|
|
133
|
-
],
|
|
134
|
-
initial: 0,
|
|
135
|
-
});
|
|
136
|
-
env = ans.env;
|
|
137
|
-
}
|
|
138
|
-
if (env !== 'test' && env !== 'prod') {
|
|
139
|
-
throw new Error(`不支持的 env:${env}(仅支持 test/prod)`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
149
|
if (!fs.existsSync(localFolderPath)) {
|
|
143
150
|
throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const topFolderName = `${buildDirName}-${ts}-${hash}`;
|
|
151
|
-
|
|
152
|
-
// zip 输出目录(本地)
|
|
153
|
-
const zipOutDir = path.join(process.cwd(), '.ylyx-deploy');
|
|
153
|
+
if (!cfg.host) throw new Error('缺少 deploy.host(请在 .ylyxrc.json 的 deploy 字段配置)');
|
|
154
|
+
if (!cfg.username) throw new Error('缺少 deploy.username(请在 .ylyxrc.json 的 deploy 字段配置)');
|
|
155
|
+
if (!cfg.password) throw new Error('缺少 deploy.password(请在 .ylyxrc.json 的 deploy 字段配置)');
|
|
156
|
+
if (!remoteFolderPath) throw new Error('缺少 deploy.remoteDir(请在 .ylyxrc.json 的 deploy 字段配置)');
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.log(`正式服:仅压缩不上传。zip 顶层目录名:${topFolderName}`);
|
|
165
|
-
return;
|
|
158
|
+
let Client;
|
|
159
|
+
try {
|
|
160
|
+
Client = require('ssh2-sftp-client');
|
|
161
|
+
} catch (e) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"缺少依赖:ssh2-sftp-client。\n" +
|
|
164
|
+
"如果你在 ylyx-cli 仓库本地跑:请先在 ylyx-cli 目录执行 `npm i`。\n" +
|
|
165
|
+
"如果你用 npx:请确认你运行的是已发布且包含依赖的版本,或清理 npx 缓存后重试。"
|
|
166
|
+
);
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
// test 环境需要上传
|
|
169
|
-
if (!cfg.host) throw new Error('缺少 deploy.host(测试服上传需要,在 .ylyxrc.json 的 deploy 字段配置)');
|
|
170
|
-
if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要,在 .ylyxrc.json 的 deploy 字段配置)');
|
|
171
|
-
if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要,在 .ylyxrc.json 的 deploy 字段配置)');
|
|
172
|
-
if (!remoteFolderPath) throw new Error('缺少 deploy.remoteDir(测试服上传需要,在 .ylyxrc.json 的 deploy 字段配置)');
|
|
173
|
-
|
|
174
169
|
const sftp = new Client();
|
|
175
170
|
try {
|
|
176
171
|
spinner = ora('连接服务器...').start();
|
|
@@ -181,18 +176,27 @@ async function deploy(overrides = {}) {
|
|
|
181
176
|
password: cfg.password,
|
|
182
177
|
});
|
|
183
178
|
|
|
184
|
-
spinner.text = '
|
|
179
|
+
spinner.text = '服务器连接成功,准备清理远端目录...';
|
|
180
|
+
try {
|
|
181
|
+
await sftp.rmdir(remoteFolderPath, true);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// 远端目录不存在/权限问题:继续尝试 mkdir
|
|
184
|
+
}
|
|
185
185
|
await sftp.mkdir(remoteFolderPath, true);
|
|
186
186
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
187
|
+
const remoteDirPosix = remoteFolderPath.replace(/\\/g, '/');
|
|
188
|
+
spinner.text = '开始上传目录...';
|
|
189
|
+
const { uploadedBytes, totalBytes, fileCount } = await uploadDirWithProgress({
|
|
190
|
+
sftp,
|
|
191
|
+
localDir: localFolderPath,
|
|
192
|
+
remoteDirPosix,
|
|
193
|
+
onProgress: ({ uploadedBytes: up, totalBytes: tot, fileIndex, fileCount: cnt, currentFile }) => {
|
|
194
|
+
const percent = tot ? ((up / tot) * 100).toFixed(2) : '100.00';
|
|
195
|
+
const shortFile = currentFile ? currentFile.slice(-60) : '';
|
|
196
|
+
spinner.text = `上传进度: ${percent}% (${formatBytes(up)}/${formatBytes(tot)}) 文件:${fileIndex}/${cnt} ${shortFile}`;
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
spinner.succeed(`上传成功:${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)},共 ${fileCount} 个文件`);
|
|
196
200
|
} catch (err) {
|
|
197
201
|
if (spinner) spinner.fail('部署失败');
|
|
198
202
|
throw err;
|
|
@@ -202,6 +206,32 @@ async function deploy(overrides = {}) {
|
|
|
202
206
|
} catch (e) {}
|
|
203
207
|
if (spinner) spinner.info('部署结束');
|
|
204
208
|
}
|
|
209
|
+
|
|
210
|
+
// 可选:部署后压缩(参考:compressing.zip.compressDir)
|
|
211
|
+
if (cfg.zipAfter) {
|
|
212
|
+
let compressing;
|
|
213
|
+
try {
|
|
214
|
+
compressing = require('compressing');
|
|
215
|
+
} catch (e) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"缺少依赖:compressing(用于压缩)。请在 ylyx-cli 目录执行 `npm i` 或安装该依赖后重试。"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const buildDirName = path.basename(path.resolve(localFolderPath));
|
|
222
|
+
fs.ensureDirSync(cfg.zipOutDir);
|
|
223
|
+
const zipName = `${buildDirName}.${formatTimestampForZipName(new Date())}.zip`;
|
|
224
|
+
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
225
|
+
|
|
226
|
+
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
227
|
+
try {
|
|
228
|
+
await compressing.zip.compressDir(localFolderPath, zipPath);
|
|
229
|
+
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
zipSpinner.fail('压缩失败');
|
|
232
|
+
throw e;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
205
235
|
}
|
|
206
236
|
|
|
207
237
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylyx-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "公司内部代码生成模板脚手架工具,支持快速生成项目初始结构和代码模板",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,15 +43,14 @@
|
|
|
43
43
|
"node": ">=12.0.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"archiver": "^5.3.2",
|
|
47
46
|
"commander": "^11.1.0",
|
|
47
|
+
"compressing": "^1.10.1",
|
|
48
48
|
"inquirer": "^8.2.6",
|
|
49
49
|
"chalk": "^4.1.2",
|
|
50
50
|
"fs-extra": "^11.2.0",
|
|
51
51
|
"handlebars": "^4.7.8",
|
|
52
52
|
"glob": "^10.3.10",
|
|
53
53
|
"ora": "^5.4.1",
|
|
54
|
-
"prompts": "^2.4.2",
|
|
55
54
|
"ssh2-sftp-client": "^9.0.4"
|
|
56
55
|
},
|
|
57
56
|
"devDependencies": {
|