ylyx-cli 1.0.4 → 1.0.6
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 +7 -1
- package/lib/deploy.js +222 -97
- package/package.json +1 -2
package/bin/ylyx.js
CHANGED
|
@@ -114,23 +114,29 @@ program
|
|
|
114
114
|
program
|
|
115
115
|
.command('deploy')
|
|
116
116
|
.description('部署:递归上传本地目录到远端目录(读取当前项目 .ylyxrc.json 的 deploy 字段)')
|
|
117
|
-
.option('--env <env>', '部署环境:test|prod
|
|
117
|
+
.option('--env <env>', '部署环境:test|prod(不传会交互选择)')
|
|
118
|
+
.option('--build-dir <name>', '压缩文件名使用的 buildDir(默认取 .ylyxrc.json 的 buildDir)')
|
|
118
119
|
.option('--host <host>', '覆盖 deploy.host')
|
|
119
120
|
.option('--port <port>', '覆盖 deploy.port')
|
|
120
121
|
.option('-u, --username <username>', '覆盖 deploy.username')
|
|
121
122
|
.option('-p, --password <password>', '覆盖 deploy.password')
|
|
122
123
|
.option('--local <path>', '覆盖 deploy.localDir(默认 ./EXTERNAL_DIGIC)')
|
|
123
124
|
.option('--remote <path>', '覆盖 deploy.remoteDir')
|
|
125
|
+
.option('--zip', '部署后把 localDir 压缩成 zip(参考 compressing.zip.compressDir)')
|
|
126
|
+
.option('--zip-out <path>', 'zip 输出目录(默认:./.ylyx-deploy)')
|
|
124
127
|
.action(async (options) => {
|
|
125
128
|
try {
|
|
126
129
|
await deploy({
|
|
127
130
|
env: options.env,
|
|
131
|
+
buildDir: options.buildDir,
|
|
128
132
|
host: options.host,
|
|
129
133
|
port: options.port,
|
|
130
134
|
username: options.username,
|
|
131
135
|
password: options.password,
|
|
132
136
|
localDir: options.local,
|
|
133
137
|
remoteDir: options.remote,
|
|
138
|
+
zipAfter: !!options.zip,
|
|
139
|
+
zipOutDir: options.zipOut,
|
|
134
140
|
});
|
|
135
141
|
} catch (error) {
|
|
136
142
|
console.error('❌ 部署失败:', error.message);
|
package/lib/deploy.js
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs-extra');
|
|
4
3
|
const ora = require('ora');
|
|
5
|
-
const
|
|
4
|
+
const inquirer = require('inquirer');
|
|
6
5
|
const archiver = require('archiver');
|
|
7
6
|
const { readProjectConfig } = require('./utils');
|
|
8
7
|
|
|
9
8
|
let spinner = null; // 加载实例
|
|
10
9
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function formatTimestampForFolderName(date = new Date()) {
|
|
16
|
-
// 用户要求:YYYY-MM-DD HH:mm:ss(注意:该字符串用于 zip 内的顶层目录名,不受 Windows 文件名限制)
|
|
10
|
+
function formatTimestampCompact(date = new Date()) {
|
|
11
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
17
12
|
const yyyy = date.getFullYear();
|
|
18
13
|
const MM = pad2(date.getMonth() + 1);
|
|
19
14
|
const dd = pad2(date.getDate());
|
|
20
15
|
const HH = pad2(date.getHours());
|
|
21
16
|
const mm = pad2(date.getMinutes());
|
|
22
17
|
const ss = pad2(date.getSeconds());
|
|
23
|
-
return `${yyyy}
|
|
18
|
+
return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
function formatBytes(bytes) {
|
|
22
|
+
const n = Number(bytes) || 0;
|
|
23
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
24
|
+
let v = n;
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
27
|
+
v /= 1024;
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
const fixed = i === 0 ? 0 : v < 10 ? 2 : v < 100 ? 1 : 0;
|
|
31
|
+
return `${v.toFixed(fixed)}${units[i]}`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function
|
|
34
|
+
function listLocalFilesWithSize(rootDir) {
|
|
35
35
|
const out = [];
|
|
36
36
|
const stack = [rootDir];
|
|
37
37
|
while (stack.length) {
|
|
@@ -41,34 +41,30 @@ function listFilesRecursively(rootDir) {
|
|
|
41
41
|
const p = path.join(current, e);
|
|
42
42
|
const st = fs.statSync(p);
|
|
43
43
|
if (st.isDirectory()) stack.push(p);
|
|
44
|
-
else if (st.isFile())
|
|
44
|
+
else if (st.isFile()) {
|
|
45
|
+
out.push({
|
|
46
|
+
localPath: p,
|
|
47
|
+
relPosix: path.relative(rootDir, p).replace(/\\/g, '/'),
|
|
48
|
+
size: st.size,
|
|
49
|
+
mtimeMs: st.mtimeMs,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
|
-
|
|
48
|
-
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
54
|
+
out.sort((a, b) => a.relPosix.localeCompare(b.relPosix));
|
|
49
55
|
return out;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
h.update('\n');
|
|
59
|
-
h.update(String(f.size));
|
|
60
|
-
h.update('\n');
|
|
61
|
-
h.update(String(Math.floor(f.mtimeMs)));
|
|
62
|
-
h.update('\n');
|
|
63
|
-
}
|
|
64
|
-
return h.digest('hex').slice(0, 8);
|
|
58
|
+
function sanitizeBuildDirName(name) {
|
|
59
|
+
// 用户要求:项目名称来源于 buildDir 字段;同时确保文件名安全
|
|
60
|
+
return String(name || 'build')
|
|
61
|
+
.replace(/[\\/]/g, '')
|
|
62
|
+
.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
63
|
+
.slice(0, 80);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
async function
|
|
68
|
-
fs.
|
|
69
|
-
const zipFileName = `${sanitizeFileName(topFolderName)}.zip`;
|
|
70
|
-
const zipPath = path.join(outDir, zipFileName);
|
|
71
|
-
|
|
66
|
+
async function zipDirAsTopFolder({ srcDir, zipPath, topFolderName }) {
|
|
67
|
+
fs.ensureDirSync(path.dirname(zipPath));
|
|
72
68
|
await new Promise((resolve, reject) => {
|
|
73
69
|
const output = fs.createWriteStream(zipPath);
|
|
74
70
|
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
@@ -76,20 +72,127 @@ async function zipDirWithTopFolder({ srcDir, outDir, topFolderName }) {
|
|
|
76
72
|
output.on('close', resolve);
|
|
77
73
|
output.on('error', reject);
|
|
78
74
|
archive.on('warning', (err) => {
|
|
79
|
-
// 非致命 warning(如 ENOENT)按 warning 处理;其它仍抛出
|
|
80
75
|
if (err && err.code === 'ENOENT') return;
|
|
81
76
|
reject(err);
|
|
82
77
|
});
|
|
83
78
|
archive.on('error', reject);
|
|
84
79
|
|
|
85
80
|
archive.pipe(output);
|
|
86
|
-
|
|
87
|
-
// 把 srcDir 内容放到 zip 的顶层目录 topFolderName 下
|
|
81
|
+
// 关键:zip 内顶层目录名强制为 buildDirName
|
|
88
82
|
archive.directory(srcDir, topFolderName);
|
|
89
83
|
archive.finalize();
|
|
90
84
|
});
|
|
85
|
+
}
|
|
91
86
|
|
|
92
|
-
|
|
87
|
+
function hashLettersFromSha256(buf, lettersLen = 12) {
|
|
88
|
+
const alphabet = 'abcdefghijklmnop'; // 16 letters for 0..15
|
|
89
|
+
let out = '';
|
|
90
|
+
for (let i = 0; i < buf.length && out.length < lettersLen; i++) {
|
|
91
|
+
const b = buf[i];
|
|
92
|
+
out += alphabet[(b >> 4) & 0xf];
|
|
93
|
+
if (out.length >= lettersLen) break;
|
|
94
|
+
out += alphabet[b & 0xf];
|
|
95
|
+
}
|
|
96
|
+
return out.slice(0, lettersLen);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function computeDirHashLetters(localDir) {
|
|
100
|
+
const crypto = require('crypto');
|
|
101
|
+
const h = crypto.createHash('sha256');
|
|
102
|
+
const files = listLocalFilesWithSize(localDir);
|
|
103
|
+
for (const f of files) {
|
|
104
|
+
h.update(f.relPosix);
|
|
105
|
+
h.update('\n');
|
|
106
|
+
h.update(String(f.size || 0));
|
|
107
|
+
h.update('\n');
|
|
108
|
+
h.update(String(Math.floor(f.mtimeMs || 0)));
|
|
109
|
+
h.update('\n');
|
|
110
|
+
}
|
|
111
|
+
const digest = h.digest();
|
|
112
|
+
return hashLettersFromSha256(digest, 12);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function chooseDeployEnv(env) {
|
|
116
|
+
if (env === 'test' || env === 'prod') return env;
|
|
117
|
+
const ans = await inquirer.prompt([
|
|
118
|
+
{
|
|
119
|
+
type: 'list',
|
|
120
|
+
name: 'env',
|
|
121
|
+
message: '请选择部署环境',
|
|
122
|
+
choices: [
|
|
123
|
+
{ name: '部署测试服(上传到服务器)', value: 'test' },
|
|
124
|
+
{ name: '部署正式服(不上传,仅压缩)', value: 'prod' },
|
|
125
|
+
],
|
|
126
|
+
default: 'test',
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
return ans.env;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function uploadDirWithProgress({ sftp, localDir, remoteDirPosix, onProgress }) {
|
|
133
|
+
const files = listLocalFilesWithSize(localDir);
|
|
134
|
+
const totalBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
135
|
+
if (files.length === 0) return { totalBytes: 0, uploadedBytes: 0, fileCount: 0 };
|
|
136
|
+
|
|
137
|
+
// 先创建需要的目录(去重)
|
|
138
|
+
const dirSet = new Set();
|
|
139
|
+
for (const f of files) {
|
|
140
|
+
const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
|
|
141
|
+
dirSet.add(path.posix.dirname(remoteFile));
|
|
142
|
+
}
|
|
143
|
+
// 目录从浅到深创建,避免多余失败
|
|
144
|
+
const dirs = Array.from(dirSet).sort((a, b) => a.length - b.length);
|
|
145
|
+
for (const d of dirs) {
|
|
146
|
+
await sftp.mkdir(d, true);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let uploadedBytes = 0;
|
|
150
|
+
let lastRenderAt = 0;
|
|
151
|
+
const render = (state) => {
|
|
152
|
+
if (typeof onProgress !== 'function') return;
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
// 轻微节流,避免刷屏
|
|
155
|
+
if (now - lastRenderAt < 80 && state && state.force !== true) return;
|
|
156
|
+
lastRenderAt = now;
|
|
157
|
+
onProgress(state);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < files.length; i++) {
|
|
161
|
+
const f = files[i];
|
|
162
|
+
const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
|
|
163
|
+
let lastTransferred = 0;
|
|
164
|
+
|
|
165
|
+
await sftp.fastPut(f.localPath, remoteFile, {
|
|
166
|
+
step: (transferred) => {
|
|
167
|
+
// transferred:当前文件已传输的总字节数(累计)
|
|
168
|
+
const delta = Math.max(0, Number(transferred || 0) - lastTransferred);
|
|
169
|
+
lastTransferred = Number(transferred || 0);
|
|
170
|
+
uploadedBytes += delta;
|
|
171
|
+
render({
|
|
172
|
+
uploadedBytes,
|
|
173
|
+
totalBytes,
|
|
174
|
+
fileIndex: i + 1,
|
|
175
|
+
fileCount: files.length,
|
|
176
|
+
currentFile: f.relPosix,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// fastPut 有时不会触发 step(极小文件/实现差异),这里兜底补齐本文件剩余字节
|
|
182
|
+
if (lastTransferred < f.size) {
|
|
183
|
+
uploadedBytes += f.size - lastTransferred;
|
|
184
|
+
}
|
|
185
|
+
render({
|
|
186
|
+
uploadedBytes,
|
|
187
|
+
totalBytes,
|
|
188
|
+
fileIndex: i + 1,
|
|
189
|
+
fileCount: files.length,
|
|
190
|
+
currentFile: f.relPosix,
|
|
191
|
+
force: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { totalBytes, uploadedBytes, fileCount: files.length };
|
|
93
196
|
}
|
|
94
197
|
|
|
95
198
|
function resolveDeployConfig(overrides = {}) {
|
|
@@ -97,12 +200,16 @@ function resolveDeployConfig(overrides = {}) {
|
|
|
97
200
|
const deploy = (projectConfig && projectConfig.deploy) || {};
|
|
98
201
|
|
|
99
202
|
const cfg = {
|
|
203
|
+
env: overrides.env ?? deploy.env,
|
|
204
|
+
buildDir: overrides.buildDir ?? deploy.buildDir ?? projectConfig.buildDir,
|
|
100
205
|
host: overrides.host ?? deploy.host,
|
|
101
206
|
port: String(overrides.port ?? deploy.port ?? '22'),
|
|
102
207
|
username: overrides.username ?? deploy.username,
|
|
103
208
|
password: overrides.password ?? deploy.password,
|
|
104
209
|
localDir: overrides.localDir ?? deploy.localDir ?? './EXTERNAL_DIGIC',
|
|
105
210
|
remoteDir: overrides.remoteDir ?? deploy.remoteDir ?? '/usr/local/nginx/html/EXTERNAL_DIGIC',
|
|
211
|
+
zipAfter: overrides.zipAfter ?? deploy.zipAfter ?? false,
|
|
212
|
+
zipOutDir: overrides.zipOutDir ?? deploy.zipOutDir ?? path.join(process.cwd(), '.ylyx-deploy'),
|
|
106
213
|
};
|
|
107
214
|
|
|
108
215
|
return cfg;
|
|
@@ -110,69 +217,54 @@ function resolveDeployConfig(overrides = {}) {
|
|
|
110
217
|
|
|
111
218
|
/**
|
|
112
219
|
* 执行部署(递归上传目录到远端)
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* @param {{env?:'test'|'prod',host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string}} overrides
|
|
220
|
+
* 测试服:上传到服务器(可选部署后压缩)
|
|
221
|
+
* 正式服:不上传,仅压缩
|
|
222
|
+
* @param {{env?:'test'|'prod',host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string}} overrides
|
|
116
223
|
*/
|
|
117
224
|
async function deploy(overrides = {}) {
|
|
118
225
|
const cfg = resolveDeployConfig(overrides);
|
|
119
226
|
const localFolderPath = cfg.localDir;
|
|
120
227
|
const remoteFolderPath = cfg.remoteDir;
|
|
121
228
|
|
|
122
|
-
|
|
123
|
-
let env = overrides.env;
|
|
124
|
-
if (!env) {
|
|
125
|
-
const ans = await prompts({
|
|
126
|
-
type: 'select',
|
|
127
|
-
name: 'env',
|
|
128
|
-
message: '请选择部署环境',
|
|
129
|
-
choices: [
|
|
130
|
-
{ title: '测试服(压缩 + 上传)', value: 'test' },
|
|
131
|
-
{ title: '正式服(只压缩)', value: 'prod' },
|
|
132
|
-
],
|
|
133
|
-
initial: 0,
|
|
134
|
-
});
|
|
135
|
-
env = ans.env;
|
|
136
|
-
}
|
|
137
|
-
if (env !== 'test' && env !== 'prod') {
|
|
138
|
-
throw new Error(`不支持的 env:${env}(仅支持 test/prod)`);
|
|
139
|
-
}
|
|
229
|
+
const env = await chooseDeployEnv(cfg.env);
|
|
140
230
|
|
|
141
231
|
if (!fs.existsSync(localFolderPath)) {
|
|
142
232
|
throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
|
|
143
233
|
}
|
|
144
234
|
|
|
145
|
-
//
|
|
146
|
-
const buildDirName = path.basename(path.resolve(localFolderPath));
|
|
147
|
-
const ts = formatTimestampForFolderName(new Date());
|
|
148
|
-
const hash = computeDirHashShort(localFolderPath);
|
|
149
|
-
const topFolderName = `${buildDirName}-${ts}-${hash}`;
|
|
150
|
-
|
|
151
|
-
// zip 输出目录(本地)
|
|
152
|
-
const zipOutDir = path.join(process.cwd(), '.ylyx-deploy');
|
|
153
|
-
|
|
154
|
-
spinner = ora('开始压缩...').start();
|
|
155
|
-
const { zipPath } = await zipDirWithTopFolder({
|
|
156
|
-
srcDir: localFolderPath,
|
|
157
|
-
outDir: zipOutDir,
|
|
158
|
-
topFolderName,
|
|
159
|
-
});
|
|
160
|
-
spinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
161
|
-
|
|
235
|
+
// 正式服:仅压缩
|
|
162
236
|
if (env === 'prod') {
|
|
163
|
-
|
|
237
|
+
const buildDirName = sanitizeBuildDirName(
|
|
238
|
+
cfg.buildDir != null && cfg.buildDir !== ''
|
|
239
|
+
? String(cfg.buildDir).replace(/\//g, '').replace(/\\/g, '')
|
|
240
|
+
: path.basename(path.resolve(localFolderPath)).replace(/[\\/]/g, '')
|
|
241
|
+
);
|
|
242
|
+
const ts = formatTimestampCompact(new Date());
|
|
243
|
+
const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
|
|
244
|
+
const zipBaseName = `${buildDirName}${ts}${hashLetters}`;
|
|
245
|
+
fs.ensureDirSync(cfg.zipOutDir);
|
|
246
|
+
const zipName = `${zipBaseName}.zip`;
|
|
247
|
+
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
248
|
+
|
|
249
|
+
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
250
|
+
try {
|
|
251
|
+
await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
|
|
252
|
+
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
253
|
+
} catch (e) {
|
|
254
|
+
zipSpinner.fail('压缩失败');
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
164
257
|
return;
|
|
165
258
|
}
|
|
166
259
|
|
|
167
|
-
//
|
|
168
|
-
if (!cfg.host) throw new Error('缺少 deploy.host
|
|
169
|
-
if (!cfg.username) throw new Error('缺少 deploy.username
|
|
170
|
-
if (!cfg.password) throw new Error('缺少 deploy.password
|
|
171
|
-
if (!remoteFolderPath) throw new Error('缺少 deploy.remoteDir
|
|
260
|
+
// 测试服:上传到服务器
|
|
261
|
+
if (!cfg.host) throw new Error('缺少 deploy.host(测试服上传需要)');
|
|
262
|
+
if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要)');
|
|
263
|
+
if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要)');
|
|
264
|
+
if (!remoteFolderPath) throw new Error('缺少 deploy.remoteDir(测试服上传需要)');
|
|
172
265
|
|
|
173
266
|
let Client;
|
|
174
267
|
try {
|
|
175
|
-
// 仅在需要“上传”时才加载,避免“正式服只压缩”也因为缺依赖而崩
|
|
176
268
|
Client = require('ssh2-sftp-client');
|
|
177
269
|
} catch (e) {
|
|
178
270
|
throw new Error(
|
|
@@ -192,18 +284,27 @@ async function deploy(overrides = {}) {
|
|
|
192
284
|
password: cfg.password,
|
|
193
285
|
});
|
|
194
286
|
|
|
195
|
-
spinner.text = '
|
|
287
|
+
spinner.text = '服务器连接成功,准备清理远端目录...';
|
|
288
|
+
try {
|
|
289
|
+
await sftp.rmdir(remoteFolderPath, true);
|
|
290
|
+
} catch (e) {
|
|
291
|
+
// ignore
|
|
292
|
+
}
|
|
196
293
|
await sftp.mkdir(remoteFolderPath, true);
|
|
197
294
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
295
|
+
const remoteDirPosix = remoteFolderPath.replace(/\\/g, '/');
|
|
296
|
+
spinner.text = '开始上传目录...';
|
|
297
|
+
const { uploadedBytes, totalBytes, fileCount } = await uploadDirWithProgress({
|
|
298
|
+
sftp,
|
|
299
|
+
localDir: localFolderPath,
|
|
300
|
+
remoteDirPosix,
|
|
301
|
+
onProgress: ({ uploadedBytes: up, totalBytes: tot, fileIndex, fileCount: cnt, currentFile }) => {
|
|
302
|
+
const percent = tot ? ((up / tot) * 100).toFixed(2) : '100.00';
|
|
303
|
+
const shortFile = currentFile ? currentFile.slice(-60) : '';
|
|
304
|
+
spinner.text = `上传进度: ${percent}% (${formatBytes(up)}/${formatBytes(tot)}) 文件:${fileIndex}/${cnt} ${shortFile}`;
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
spinner.succeed(`上传成功:${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)},共 ${fileCount} 个文件`);
|
|
207
308
|
} catch (err) {
|
|
208
309
|
if (spinner) spinner.fail('部署失败');
|
|
209
310
|
throw err;
|
|
@@ -213,6 +314,30 @@ async function deploy(overrides = {}) {
|
|
|
213
314
|
} catch (e) {}
|
|
214
315
|
if (spinner) spinner.info('部署结束');
|
|
215
316
|
}
|
|
317
|
+
|
|
318
|
+
// 测试服可选:部署后压缩(延续之前的可选开关)
|
|
319
|
+
if (cfg.zipAfter) {
|
|
320
|
+
const buildDirName = sanitizeBuildDirName(
|
|
321
|
+
cfg.buildDir != null && cfg.buildDir !== ''
|
|
322
|
+
? String(cfg.buildDir).replace(/\//g, '').replace(/\\/g, '')
|
|
323
|
+
: path.basename(path.resolve(localFolderPath)).replace(/[\\/]/g, '')
|
|
324
|
+
);
|
|
325
|
+
const ts = formatTimestampCompact(new Date());
|
|
326
|
+
const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
|
|
327
|
+
const zipBaseName = `${buildDirName}${ts}${hashLetters}`;
|
|
328
|
+
fs.ensureDirSync(cfg.zipOutDir);
|
|
329
|
+
const zipName = `${zipBaseName}.zip`;
|
|
330
|
+
const zipPath = path.join(cfg.zipOutDir, zipName);
|
|
331
|
+
|
|
332
|
+
const zipSpinner = ora(`开始压缩:${zipName}`).start();
|
|
333
|
+
try {
|
|
334
|
+
await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
|
|
335
|
+
zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
zipSpinner.fail('压缩失败');
|
|
338
|
+
throw e;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
216
341
|
}
|
|
217
342
|
|
|
218
343
|
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.6",
|
|
4
4
|
"description": "公司内部代码生成模板脚手架工具,支持快速生成项目初始结构和代码模板",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -51,7 +51,6 @@
|
|
|
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": {
|