create-ssnode-cli 0.2.6 → 0.2.7
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/package.json +1 -1
- package/templates/backend/scripts/server.config.example.cjs +9 -0
- package/templates/web/package.json +4 -1
- package/templates/web/scripts/compress-src-images.cjs +332 -0
- package/templates/web/scripts/publish.cjs +280 -241
- package/templates/web/src/layouts/Background.module.less +0 -82
- package/templates/web/src/layouts/Background.tsx +26 -113
- package/templates/web/src/layouts/components/PageTabs/SortableTab.tsx +133 -0
- package/templates/web/src/layouts/components/PageTabs/index.module.less +105 -0
- package/templates/web/src/layouts/components/PageTabs/index.tsx +110 -0
- package/templates/web/src/layouts/components/PageTabs/usePageTabs.ts +224 -0
|
@@ -8,62 +8,72 @@
|
|
|
8
8
|
* v1.7: 交互化——多服务器目标菜单选择(key(host))、版本号策略选择(自增默认/维持/自定义);
|
|
9
9
|
* 部署后归档 <dist>_releases/v<版本>_<时间戳>.tar.gz(rollback 列版本回退用),按 keepVersions 轮转
|
|
10
10
|
*/
|
|
11
|
-
const shelljs = require(
|
|
12
|
-
const fs = require(
|
|
13
|
-
const path = require(
|
|
14
|
-
const os = require(
|
|
15
|
-
const { NodeSSH } = require(
|
|
16
|
-
const {
|
|
17
|
-
|
|
11
|
+
const shelljs = require("shelljs");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const os = require("os");
|
|
15
|
+
const { NodeSSH } = require("node-ssh");
|
|
16
|
+
const {
|
|
17
|
+
selectEnvKey,
|
|
18
|
+
selectVersion,
|
|
19
|
+
closeInteractive,
|
|
20
|
+
} = require("./lib/interactive.cjs");
|
|
21
|
+
const serverConfig = require("./server.config.json");
|
|
18
22
|
|
|
19
23
|
class DynamicOutput {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
constructor() {
|
|
25
|
+
this.lastMessage = "";
|
|
26
|
+
this.progressEnabled = process.stdout.isTTY;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
update(message) {
|
|
30
|
+
if (this.progressEnabled) {
|
|
31
|
+
process.stdout.write("\r" + " ".repeat(this.lastMessage.length));
|
|
32
|
+
process.stdout.write("\r" + message);
|
|
33
|
+
this.lastMessage = message;
|
|
34
|
+
} else {
|
|
35
|
+
shelljs.echo(message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
final(message) {
|
|
40
|
+
if (this.progressEnabled) {
|
|
41
|
+
process.stdout.write("\r" + message + "\n");
|
|
42
|
+
} else {
|
|
43
|
+
shelljs.echo(message);
|
|
44
|
+
}
|
|
45
|
+
this.lastMessage = "";
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const output = new DynamicOutput();
|
|
46
50
|
const SSH_STATUS = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
CONNECTING: "正在连接服务器...",
|
|
52
|
+
UPLOADING: "正在上传文件...",
|
|
53
|
+
DEPLOYING: "正在部署...",
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
// 初始化配置(--env <key> 显式选目标;未传时 TTY 下交互选,非 TTY(CI)静默用 deploy 保持旧行为。
|
|
53
57
|
// key 拼错/不存在时 fail-fast,静默回落 deploy 会发错机器)
|
|
54
|
-
const findEnvIndex = process.argv.indexOf(
|
|
58
|
+
const findEnvIndex = process.argv.indexOf("--env");
|
|
55
59
|
let enviromentConfig;
|
|
56
60
|
if (findEnvIndex !== -1) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
const configKey = process.argv[findEnvIndex + 1];
|
|
62
|
+
if (!configKey) {
|
|
63
|
+
shelljs.echo(
|
|
64
|
+
"❌ --env 缺少值: --env <目标名>(server.config.json 里的 key)",
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (!serverConfig[configKey]) {
|
|
69
|
+
shelljs.echo(
|
|
70
|
+
`❌ --env ${configKey} 在 server.config.json 中不存在(可用: ${Object.keys(
|
|
71
|
+
serverConfig,
|
|
72
|
+
).join(", ")})`,
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
enviromentConfig = serverConfig[configKey];
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
const ssh = new NodeSSH();
|
|
@@ -71,229 +81,258 @@ const startTime = Date.now();
|
|
|
71
81
|
|
|
72
82
|
// 构造 SSH 连接配置:按 authMode 决定认证方式,秘钥优先级仍可被命令行 --key 临时覆盖
|
|
73
83
|
function buildSSHConfig(config) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
84
|
+
const baseConfig = {
|
|
85
|
+
host: config.host,
|
|
86
|
+
port: config.port,
|
|
87
|
+
username: config.username,
|
|
88
|
+
readyTimeout: 120_000,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// 命令行参数 --key 临时指定秘钥路径,优先级最高,无论 authMode 如何都走秘钥登录
|
|
92
|
+
const keyIndex = process.argv.indexOf("--key");
|
|
93
|
+
const usePrivateKey = keyIndex !== -1 || config.authMode === "privateKey";
|
|
94
|
+
if (!usePrivateKey) {
|
|
95
|
+
if (!config.password) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"未配置登录凭证:请在 server.config.json 中配置 authMode=password + password,或 authMode=privateKey + privateKey",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return { ...baseConfig, password: config.password };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 秘钥登录:优先取命令行 --key,否则取配置 privateKey.privateKeyPath
|
|
104
|
+
const privateKeyPath =
|
|
105
|
+
keyIndex !== -1
|
|
106
|
+
? process.argv[keyIndex + 1]
|
|
107
|
+
: config.privateKey?.privateKeyPath;
|
|
108
|
+
|
|
109
|
+
if (!privateKeyPath) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"秘钥登录缺少 privateKeyPath:请在 server.config.json 的 privateKey.privateKeyPath 中配置,或通过命令行 --key 传入",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 支持 ~ 开头的家目录路径
|
|
116
|
+
const resolvedKeyPath = privateKeyPath.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
117
|
+
if (!fs.existsSync(resolvedKeyPath)) {
|
|
118
|
+
throw new Error(`秘钥文件不存在: ${resolvedKeyPath}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sshConfig = {
|
|
122
|
+
...baseConfig,
|
|
123
|
+
// node-ssh@13 要求 privateKey 为字符串(Buffer 会被断言拦截)
|
|
124
|
+
privateKey: fs.readFileSync(resolvedKeyPath, "utf8"),
|
|
125
|
+
};
|
|
126
|
+
if (config.privateKey?.passphrase) {
|
|
127
|
+
sshConfig.passphrase = config.privateKey.passphrase;
|
|
128
|
+
}
|
|
129
|
+
return sshConfig;
|
|
114
130
|
}
|
|
115
131
|
|
|
116
|
-
const exec = (command =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
const exec = (command = "") =>
|
|
133
|
+
new Promise((resolve, reject) => {
|
|
134
|
+
shelljs.exec(command, { silent: true }, (code, stdout, stderr) => {
|
|
135
|
+
code === 0
|
|
136
|
+
? resolve(stdout)
|
|
137
|
+
: reject(new Error(stderr || `Command failed: ${command}`));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
122
140
|
|
|
123
141
|
function getPackageJsonPath() {
|
|
124
|
-
|
|
142
|
+
return path.join(shelljs.pwd().toString(), "package.json");
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
function readVersion() {
|
|
128
|
-
|
|
146
|
+
return JSON.parse(fs.readFileSync(getPackageJsonPath(), "utf-8")).version;
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
function writeVersion(version) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
150
|
+
const packageJson = JSON.parse(
|
|
151
|
+
fs.readFileSync(getPackageJsonPath(), "utf-8"),
|
|
152
|
+
);
|
|
153
|
+
packageJson.version = version;
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
getPackageJsonPath(),
|
|
156
|
+
JSON.stringify(packageJson, null, 2) + "\n",
|
|
157
|
+
);
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
function bumpSemver(version) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
const versionParts = version.split(".");
|
|
162
|
+
versionParts[versionParts.length - 1] =
|
|
163
|
+
Number(versionParts[versionParts.length - 1]) + 1;
|
|
164
|
+
return versionParts.join(".");
|
|
141
165
|
}
|
|
142
166
|
|
|
143
167
|
// 非交互(CI)默认行为:读当前版本、自增、写回
|
|
144
168
|
function bumpVersion() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
169
|
+
const next = bumpSemver(readVersion());
|
|
170
|
+
writeVersion(next);
|
|
171
|
+
return next;
|
|
148
172
|
}
|
|
149
173
|
|
|
150
174
|
function getDirSize(dirPath) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
175
|
+
let totalSize = 0;
|
|
176
|
+
const files = shelljs.find(dirPath).filter((f) => shelljs.test("-f", f));
|
|
177
|
+
files.forEach((f) => {
|
|
178
|
+
totalSize += fs.statSync(f).size;
|
|
179
|
+
});
|
|
180
|
+
return totalSize;
|
|
157
181
|
}
|
|
158
182
|
|
|
159
183
|
function formatSize(bytes) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
184
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
185
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
186
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
163
187
|
}
|
|
164
188
|
|
|
165
189
|
async function main() {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
190
|
+
try {
|
|
191
|
+
// 未显式 --env:TTY 交互选目标(菜单 `key(host)`),非 TTY 静默回落 deploy(CI 兼容)
|
|
192
|
+
if (!enviromentConfig) {
|
|
193
|
+
const envKey = process.stdin.isTTY
|
|
194
|
+
? await selectEnvKey(serverConfig)
|
|
195
|
+
: Object.keys(serverConfig).length === 1
|
|
196
|
+
? Object.keys(serverConfig)[0]
|
|
197
|
+
: "deploy";
|
|
198
|
+
enviromentConfig = serverConfig[envKey];
|
|
199
|
+
output.final(`🎯 部署目标: ${envKey}(${enviromentConfig.host})`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 版本策略:TTY 交互选(自增默认/维持当前/自定义);非 TTY 保持旧行为自动自增
|
|
203
|
+
let newVersion;
|
|
204
|
+
if (process.stdin.isTTY) {
|
|
205
|
+
const currentVersion = readVersion();
|
|
206
|
+
newVersion = await selectVersion(
|
|
207
|
+
currentVersion,
|
|
208
|
+
bumpSemver(currentVersion),
|
|
209
|
+
);
|
|
210
|
+
if (newVersion !== currentVersion) {
|
|
211
|
+
writeVersion(newVersion);
|
|
212
|
+
output.final(`📌 版本号已更新: ${newVersion}`);
|
|
213
|
+
} else {
|
|
214
|
+
output.final(`📌 维持当前版本: ${newVersion}`);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
newVersion = bumpVersion();
|
|
218
|
+
output.final(`📌 版本号已更新: ${newVersion}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
output.update("🚀 正在打包项目...");
|
|
222
|
+
await exec("npm run build");
|
|
223
|
+
|
|
224
|
+
const distPath = path.join(
|
|
225
|
+
shelljs.pwd().toString(),
|
|
226
|
+
enviromentConfig.serverWebDist || "dist",
|
|
227
|
+
);
|
|
228
|
+
const distSize = getDirSize(distPath);
|
|
229
|
+
output.final(`📦 构建产物大小: ${formatSize(distSize)}`);
|
|
230
|
+
|
|
231
|
+
output.update("🗜️ 正在压缩构建文件...");
|
|
232
|
+
const { splitIncludes, splitUpload, serverWebDist } = enviromentConfig;
|
|
233
|
+
if (serverWebDist !== "dist") {
|
|
234
|
+
await exec(`mv dist/ ${serverWebDist}/`);
|
|
235
|
+
}
|
|
236
|
+
const splitUploadFileNames = [];
|
|
237
|
+
|
|
238
|
+
if (splitIncludes?.length) {
|
|
239
|
+
await Promise.all(
|
|
240
|
+
splitIncludes.map(async (e) => {
|
|
241
|
+
if (splitUpload) {
|
|
242
|
+
const targetTar = `${serverWebDist}.${e}.tar.gz`;
|
|
243
|
+
await exec(
|
|
244
|
+
`COPYFILE_DISABLE=1 tar zcvf ${targetTar} ${serverWebDist}/${e}`,
|
|
245
|
+
);
|
|
246
|
+
splitUploadFileNames.push(targetTar);
|
|
247
|
+
}
|
|
248
|
+
await exec(`rm -rf ${serverWebDist}/${e}`);
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const targetTar = `${serverWebDist}.tar.gz`;
|
|
254
|
+
await exec(`COPYFILE_DISABLE=1 tar zcvf ${targetTar} ${serverWebDist}/`);
|
|
255
|
+
splitUploadFileNames.unshift(targetTar);
|
|
256
|
+
|
|
257
|
+
output.update(SSH_STATUS.CONNECTING);
|
|
258
|
+
await ssh.connect(buildSSHConfig(enviromentConfig));
|
|
259
|
+
|
|
260
|
+
output.update(SSH_STATUS.UPLOADING);
|
|
261
|
+
await ssh.mkdir(enviromentConfig.serverWebPath);
|
|
262
|
+
|
|
263
|
+
const sftp = await ssh.requestSFTP();
|
|
264
|
+
const totalFiles = splitUploadFileNames.length;
|
|
265
|
+
|
|
266
|
+
await Promise.all(
|
|
267
|
+
splitUploadFileNames.map(async (file, index) => {
|
|
268
|
+
const localPath = path.join(shelljs.pwd().toString(), file);
|
|
269
|
+
const remotePath = `${enviromentConfig.serverWebPath}/${file}`;
|
|
270
|
+
|
|
271
|
+
await ssh.putFile(localPath, remotePath, sftp, {
|
|
272
|
+
concurrency: 10,
|
|
273
|
+
step: (transferred, _, total) => {
|
|
274
|
+
const progress = ((transferred / total) * 100).toFixed(1);
|
|
275
|
+
output.update(
|
|
276
|
+
`${SSH_STATUS.UPLOADING} (${
|
|
277
|
+
index + 1
|
|
278
|
+
}/${totalFiles}) ${progress}% ${file}`,
|
|
279
|
+
);
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
output.update(SSH_STATUS.DEPLOYING);
|
|
286
|
+
// keepVersions:<dist>_releases 保留的版本归档数,默认 3(rollback 交互列版本回退用)
|
|
287
|
+
const keepVersions = enviromentConfig.keepVersions || 1;
|
|
288
|
+
|
|
289
|
+
const commands = [
|
|
290
|
+
`rm -rf ${serverWebDist}_last_bak.tar.gz`,
|
|
291
|
+
`tar zcvf ${serverWebDist}_last_bak.tar.gz ${serverWebDist}_bak/`,
|
|
292
|
+
`rm -rf ${serverWebDist}_bak/`,
|
|
293
|
+
`mv ${serverWebDist}/ ${serverWebDist}_bak/`,
|
|
294
|
+
...splitUploadFileNames.map((e) => `tar zxvf ${e} && rm -rf ${e}`),
|
|
295
|
+
...(!splitUpload
|
|
296
|
+
? splitIncludes.map(
|
|
297
|
+
(e) => `cp -r ${serverWebDist}_bak/${e} ${serverWebDist}`,
|
|
298
|
+
)
|
|
299
|
+
: []),
|
|
300
|
+
// 部署完成后归档本次发布(完整 dist,dist_bak 链之外的多版本历史),再按 keepVersions 轮转清理旧归档
|
|
301
|
+
`mkdir -p ${serverWebDist}_releases`,
|
|
302
|
+
`ts=$(date +%Y%m%d%H%M%S) && tar zcf ${serverWebDist}_releases/v${newVersion}_\${ts}.tar.gz ${serverWebDist}/`,
|
|
303
|
+
`ls -t ${serverWebDist}_releases/*.tar.gz | tail -n +${
|
|
304
|
+
keepVersions + 1
|
|
305
|
+
} | xargs rm -f`,
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
for (const cmd of commands) {
|
|
309
|
+
await ssh.execCommand(cmd, { cwd: enviromentConfig.serverWebPath });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
splitUploadFileNames.forEach((f) => shelljs.rm(f));
|
|
313
|
+
if (serverWebDist !== "dist") {
|
|
314
|
+
await exec(`mv ${serverWebDist}/ dist/`);
|
|
315
|
+
}
|
|
316
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
317
|
+
const now = new Date();
|
|
318
|
+
const timeStr = now.toLocaleString("zh-CN", {
|
|
319
|
+
year: "numeric",
|
|
320
|
+
month: "2-digit",
|
|
321
|
+
day: "2-digit",
|
|
322
|
+
hour: "2-digit",
|
|
323
|
+
minute: "2-digit",
|
|
324
|
+
second: "2-digit",
|
|
325
|
+
});
|
|
326
|
+
output.final(
|
|
327
|
+
`✅ 部署成功! ${timeStr} 用时:${duration}s\n✅ 访问地址:${enviromentConfig.preview}`,
|
|
328
|
+
);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
output.final(`❌ 部署失败: ${error.message}`);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
} finally {
|
|
333
|
+
closeInteractive(); // 释放 readline 的 stdin 句柄,进程才能自然退出
|
|
334
|
+
ssh.dispose();
|
|
335
|
+
}
|
|
297
336
|
}
|
|
298
337
|
|
|
299
338
|
main();
|