@zhengyizhao/deploy-helper 0.1.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 +120 -0
- package/package.json +24 -0
- package/src/commands/backup.js +313 -0
- package/src/commands/env.js +264 -0
- package/src/commands/init.js +261 -0
- package/src/commands/rollback.js +188 -0
- package/src/commands/status.js +105 -0
- package/src/commands/update.js +284 -0
- package/src/index.js +66 -0
- package/src/utils/config.js +23 -0
- package/src/utils/detect.js +48 -0
- package/src/utils/setup.js +126 -0
- package/src/utils/ssh.js +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# 🚀 deploy-helper
|
|
2
|
+
|
|
3
|
+
> 把项目部署到服务器,就这么简单。
|
|
4
|
+
|
|
5
|
+
不需要懂 Nginx、PM2、Certbot——回答几个问题,剩下的交给它。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 为什么有这个工具?
|
|
10
|
+
|
|
11
|
+
你写完了一个项目,想放到服务器上让别人访问。但你卡在了这些问题上:
|
|
12
|
+
|
|
13
|
+
- 服务器买好了,然后呢?
|
|
14
|
+
- Nginx 怎么配?
|
|
15
|
+
- HTTPS 证书怎么申请?
|
|
16
|
+
- 代码更新了怎么同步到服务器?
|
|
17
|
+
- 服务挂了怎么知道?
|
|
18
|
+
|
|
19
|
+
`deploy-helper` 就是来解决这些的。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 快速开始
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx deploy-helper
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
第一次运行会引导你完成全套配置,大约 5 分钟搞定:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
🚀 deploy-helper — 把项目部署到服务器,就这么简单
|
|
33
|
+
|
|
34
|
+
[1/5] 服务器连接信息
|
|
35
|
+
? 服务器 IP 地址:123.456.78.9
|
|
36
|
+
? SSH 端口:22
|
|
37
|
+
? 登录用户名:root
|
|
38
|
+
? 登录方式:SSH 密钥(推荐)
|
|
39
|
+
? SSH 密钥路径:~/.ssh/id_rsa
|
|
40
|
+
✓ 服务器连接成功!
|
|
41
|
+
|
|
42
|
+
[2/5] 项目信息
|
|
43
|
+
ℹ 自动检测到项目类型:Node.js 应用
|
|
44
|
+
? 应用名称:my-app
|
|
45
|
+
? 部署路径:/var/www/my-app
|
|
46
|
+
? 启动命令:node index.js
|
|
47
|
+
? 端口:3000
|
|
48
|
+
|
|
49
|
+
[3/5] 域名 & HTTPS
|
|
50
|
+
? 是否配置域名?是
|
|
51
|
+
? 你的域名:example.com
|
|
52
|
+
? 自动申请 HTTPS 证书?是
|
|
53
|
+
|
|
54
|
+
[4/5] 安装服务器环境
|
|
55
|
+
✓ 安装 Nginx
|
|
56
|
+
✓ 安装 Node.js 20
|
|
57
|
+
✓ 安装 PM2
|
|
58
|
+
|
|
59
|
+
[5/5] 上传代码并启动服务
|
|
60
|
+
✓ 项目文件上传完成
|
|
61
|
+
✓ 安装依赖
|
|
62
|
+
✓ 启动应用(PM2)
|
|
63
|
+
✓ Nginx 配置完成
|
|
64
|
+
✓ HTTPS 证书申请成功
|
|
65
|
+
|
|
66
|
+
🎉 部署成功!
|
|
67
|
+
|
|
68
|
+
访问地址:https://example.com
|
|
69
|
+
更新代码 → deploy-helper update
|
|
70
|
+
查看状态 → deploy-helper status
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 命令
|
|
76
|
+
|
|
77
|
+
| 命令 | 说明 |
|
|
78
|
+
|------|------|
|
|
79
|
+
| `deploy-helper` 或 `deploy-helper init` | 首次部署,全程引导 |
|
|
80
|
+
| `deploy-helper update` | 把最新代码推送到服务器并重启 |
|
|
81
|
+
| `deploy-helper status` | 查看服务运行状态、内存、最近日志 |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 支持的项目类型
|
|
86
|
+
|
|
87
|
+
| 类型 | 进程管理 | 说明 |
|
|
88
|
+
|------|---------|------|
|
|
89
|
+
| Node.js | PM2 | Express、Koa、Next.js 等 |
|
|
90
|
+
| Python | Supervisor | Flask、FastAPI、Django 等 |
|
|
91
|
+
| Docker | docker compose | 任意容器化应用 |
|
|
92
|
+
| 静态网站 | Nginx | 纯 HTML/CSS/JS |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 前提条件
|
|
97
|
+
|
|
98
|
+
- **本地**:Node.js 18+
|
|
99
|
+
- **服务器**:Ubuntu 20.04 / 22.04 / 24.04(root 或 sudo 权限)
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 安装(全局)
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install -g deploy-helper
|
|
107
|
+
deploy-helper init
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 配置文件
|
|
113
|
+
|
|
114
|
+
首次部署后,项目根目录会生成 `.deploy-config.json`,记录所有配置。建议加入 `.gitignore`(其中包含服务器密码字段)。
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhengyizhao/deploy-helper",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Interactive CLI to deploy your project to any VPS server",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"chalk": "^5.6.2",
|
|
14
|
+
"commander": "^14.0.3",
|
|
15
|
+
"fs-extra": "^11.3.5",
|
|
16
|
+
"inquirer": "^13.4.3",
|
|
17
|
+
"node-ssh": "^13.2.1",
|
|
18
|
+
"ora": "^9.4.0"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"deploy-helper": "src/index.js"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { connectSSH, runRemoteSilent } from '../utils/ssh.js';
|
|
7
|
+
import { loadConfig, saveConfig } from '../utils/config.js';
|
|
8
|
+
|
|
9
|
+
const BACKUP_BASE = '/var/deploy-helper/db-backups';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 根据数据库类型生成备份命令
|
|
13
|
+
*/
|
|
14
|
+
function buildDumpCommand(dbConfig, outputFile) {
|
|
15
|
+
const { type, host, port, user, password, database } = dbConfig;
|
|
16
|
+
const h = host || '127.0.0.1';
|
|
17
|
+
|
|
18
|
+
if (type === 'mysql') {
|
|
19
|
+
const p = port || 3306;
|
|
20
|
+
return `MYSQL_PWD='${password}' mysqldump -h ${h} -P ${p} -u ${user} ${database} > ${outputFile}`;
|
|
21
|
+
}
|
|
22
|
+
if (type === 'postgresql') {
|
|
23
|
+
const p = port || 5432;
|
|
24
|
+
return `PGPASSWORD='${password}' pg_dump -h ${h} -p ${p} -U ${user} ${database} > ${outputFile}`;
|
|
25
|
+
}
|
|
26
|
+
if (type === 'mongodb') {
|
|
27
|
+
const p = port || 27017;
|
|
28
|
+
const auth = password ? `--username ${user} --password '${password}' --authenticationDatabase admin` : '';
|
|
29
|
+
return `mongodump --host ${h} --port ${p} ${auth} --db ${database} --archive=${outputFile} --gzip`;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 列出现有备份
|
|
36
|
+
*/
|
|
37
|
+
async function listBackups(ssh, appName) {
|
|
38
|
+
const result = await runRemoteSilent(
|
|
39
|
+
ssh,
|
|
40
|
+
`ls -lt ${BACKUP_BASE}/${appName}/ 2>/dev/null | grep -E "\\.(sql|gz|archive)" | head -20 || echo ""`
|
|
41
|
+
);
|
|
42
|
+
if (!result.stdout.trim()) return [];
|
|
43
|
+
|
|
44
|
+
return result.stdout.trim().split('\n').filter(Boolean).map(line => {
|
|
45
|
+
const parts = line.trim().split(/\s+/);
|
|
46
|
+
const filename = parts[parts.length - 1];
|
|
47
|
+
const size = parts[4];
|
|
48
|
+
return { filename, size, line };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function deployBackup() {
|
|
53
|
+
const config = loadConfig();
|
|
54
|
+
if (!config) {
|
|
55
|
+
console.log(chalk.red('\n没有找到部署配置,请先运行:') + chalk.cyan(' deploy-helper init\n'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { action } = await inquirer.prompt([{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'action',
|
|
62
|
+
message: '数据库备份操作:',
|
|
63
|
+
choices: [
|
|
64
|
+
{ name: '立即备份数据库', value: 'backup' },
|
|
65
|
+
{ name: '查看历史备份列表', value: 'list' },
|
|
66
|
+
{ name: '下载备份文件到本地', value: 'download' },
|
|
67
|
+
{ name: '配置定时自动备份', value: 'schedule' },
|
|
68
|
+
],
|
|
69
|
+
}]);
|
|
70
|
+
|
|
71
|
+
if (action === 'backup') await doBackup(config);
|
|
72
|
+
else if (action === 'list') await listBackupFiles(config);
|
|
73
|
+
else if (action === 'download') await downloadBackup(config);
|
|
74
|
+
else if (action === 'schedule') await scheduleBackup(config);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function doBackup(config, silent = false) {
|
|
78
|
+
// 读取或询问数据库配置
|
|
79
|
+
let dbConfig = config.database;
|
|
80
|
+
|
|
81
|
+
if (!dbConfig) {
|
|
82
|
+
if (!silent) {
|
|
83
|
+
console.log(chalk.gray('\n首次使用,需要配置数据库连接信息。配置将保存到 .deploy-config.json\n'));
|
|
84
|
+
}
|
|
85
|
+
const answers = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'list',
|
|
88
|
+
name: 'type',
|
|
89
|
+
message: '数据库类型:',
|
|
90
|
+
choices: [
|
|
91
|
+
{ name: 'MySQL / MariaDB', value: 'mysql' },
|
|
92
|
+
{ name: 'PostgreSQL', value: 'postgresql' },
|
|
93
|
+
{ name: 'MongoDB', value: 'mongodb' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{ type: 'input', name: 'host', message: '数据库地址:', default: '127.0.0.1' },
|
|
97
|
+
{ type: 'input', name: 'port', message: '端口:', default: (a) => ({ mysql: '3306', postgresql: '5432', mongodb: '27017' }[a.type]) },
|
|
98
|
+
{ type: 'input', name: 'user', message: '用户名:', default: 'root' },
|
|
99
|
+
{ type: 'password', name: 'password', message: '密码:', mask: '*' },
|
|
100
|
+
{ type: 'input', name: 'database', message: '数据库名:', validate: v => v.trim() ? true : '请输入数据库名' },
|
|
101
|
+
]);
|
|
102
|
+
dbConfig = answers;
|
|
103
|
+
|
|
104
|
+
// 保存到 config
|
|
105
|
+
const updated = { ...config, database: dbConfig };
|
|
106
|
+
saveConfig(updated);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let ssh;
|
|
110
|
+
const connectSpinner = ora('连接服务器...').start();
|
|
111
|
+
try {
|
|
112
|
+
ssh = await connectSSH(config);
|
|
113
|
+
connectSpinner.succeed('连接成功');
|
|
114
|
+
} catch (err) {
|
|
115
|
+
connectSpinner.fail('连接失败:' + err.message);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
121
|
+
const ext = dbConfig.type === 'mongodb' ? '.archive.gz' : '.sql.gz';
|
|
122
|
+
const filename = `${dbConfig.database}_${timestamp}${ext}`;
|
|
123
|
+
const backupDir = `${BACKUP_BASE}/${config.appName}`;
|
|
124
|
+
const outputFile = `${backupDir}/${filename}`;
|
|
125
|
+
const rawFile = outputFile.replace('.gz', '');
|
|
126
|
+
|
|
127
|
+
await runRemoteSilent(ssh, `mkdir -p ${backupDir}`);
|
|
128
|
+
|
|
129
|
+
// 生成备份
|
|
130
|
+
const dumpSpinner = ora(`备份 ${dbConfig.type} 数据库 [${dbConfig.database}]...`).start();
|
|
131
|
+
const dumpCmd = buildDumpCommand(dbConfig, dbConfig.type === 'mongodb' ? outputFile : rawFile);
|
|
132
|
+
const dumpResult = await runRemoteSilent(ssh, dumpCmd);
|
|
133
|
+
|
|
134
|
+
if (dumpResult.code !== 0) {
|
|
135
|
+
dumpSpinner.fail('备份失败');
|
|
136
|
+
console.log(chalk.red(dumpResult.stdout));
|
|
137
|
+
ssh.dispose();
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// MySQL/PostgreSQL 压缩
|
|
142
|
+
if (dbConfig.type !== 'mongodb') {
|
|
143
|
+
await runRemoteSilent(ssh, `gzip -f ${rawFile}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 获取文件大小
|
|
147
|
+
const sizeResult = await runRemoteSilent(ssh, `du -sh ${outputFile} | cut -f1`);
|
|
148
|
+
dumpSpinner.succeed(`备份完成 → ${chalk.cyan(filename)} ${chalk.gray('(' + sizeResult.stdout + ')')}`);
|
|
149
|
+
|
|
150
|
+
// 只保留最近 10 个备份
|
|
151
|
+
await runRemoteSilent(
|
|
152
|
+
ssh,
|
|
153
|
+
`ls -t ${backupDir} | tail -n +11 | xargs -I{} rm -f ${backupDir}/{} 2>/dev/null || true`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
ssh.dispose();
|
|
157
|
+
return { filename, outputFile };
|
|
158
|
+
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.log(chalk.red('\n备份失败:' + err.message));
|
|
161
|
+
ssh.dispose();
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function listBackupFiles(config) {
|
|
167
|
+
let ssh;
|
|
168
|
+
const spinner = ora('连接服务器...').start();
|
|
169
|
+
try {
|
|
170
|
+
ssh = await connectSSH(config);
|
|
171
|
+
spinner.succeed('连接成功');
|
|
172
|
+
} catch (err) {
|
|
173
|
+
spinner.fail('连接失败:' + err.message);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const backups = await listBackups(ssh, config.appName);
|
|
178
|
+
ssh.dispose();
|
|
179
|
+
|
|
180
|
+
if (backups.length === 0) {
|
|
181
|
+
console.log(chalk.yellow('\n还没有任何备份记录。\n'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(chalk.bold(`\n 历史备份(共 ${backups.length} 个):\n`));
|
|
186
|
+
backups.forEach(({ filename, size }, i) => {
|
|
187
|
+
const prefix = i === 0 ? chalk.green(' ● ') : chalk.gray(' ○ ');
|
|
188
|
+
console.log(prefix + chalk.cyan(filename) + chalk.gray(` ${size}`));
|
|
189
|
+
});
|
|
190
|
+
console.log('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function downloadBackup(config) {
|
|
194
|
+
let ssh;
|
|
195
|
+
const spinner = ora('连接服务器...').start();
|
|
196
|
+
try {
|
|
197
|
+
ssh = await connectSSH(config);
|
|
198
|
+
spinner.succeed('连接成功');
|
|
199
|
+
} catch (err) {
|
|
200
|
+
spinner.fail('连接失败:' + err.message);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const backups = await listBackups(ssh, config.appName);
|
|
205
|
+
|
|
206
|
+
if (backups.length === 0) {
|
|
207
|
+
console.log(chalk.yellow('\n没有备份文件可下载。先运行备份。\n'));
|
|
208
|
+
ssh.dispose();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const { selected } = await inquirer.prompt([{
|
|
213
|
+
type: 'list',
|
|
214
|
+
name: 'selected',
|
|
215
|
+
message: '选择要下载的备份:',
|
|
216
|
+
choices: backups.map(b => ({
|
|
217
|
+
name: `${b.filename} ${chalk.gray(b.size)}`,
|
|
218
|
+
value: b.filename,
|
|
219
|
+
})),
|
|
220
|
+
}]);
|
|
221
|
+
|
|
222
|
+
const localPath = path.join(process.cwd(), selected);
|
|
223
|
+
const remotePath = `${BACKUP_BASE}/${config.appName}/${selected}`;
|
|
224
|
+
|
|
225
|
+
const dlSpinner = ora(`下载 ${selected}...`).start();
|
|
226
|
+
try {
|
|
227
|
+
await ssh.getFile(localPath, remotePath);
|
|
228
|
+
dlSpinner.succeed(`已下载到:${chalk.cyan(localPath)}`);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
dlSpinner.fail('下载失败:' + err.message);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
ssh.dispose();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function scheduleBackup(config) {
|
|
237
|
+
const { frequency } = await inquirer.prompt([{
|
|
238
|
+
type: 'list',
|
|
239
|
+
name: 'frequency',
|
|
240
|
+
message: '备份频率:',
|
|
241
|
+
choices: [
|
|
242
|
+
{ name: '每天凌晨 2 点', value: '0 2 * * *' },
|
|
243
|
+
{ name: '每 6 小时', value: '0 */6 * * *' },
|
|
244
|
+
{ name: '每周一凌晨 2 点', value: '0 2 * * 1' },
|
|
245
|
+
{ name: '自定义 Cron 表达式', value: 'custom' },
|
|
246
|
+
],
|
|
247
|
+
}]);
|
|
248
|
+
|
|
249
|
+
let cronExpr = frequency;
|
|
250
|
+
if (frequency === 'custom') {
|
|
251
|
+
const { custom } = await inquirer.prompt([{
|
|
252
|
+
type: 'input',
|
|
253
|
+
name: 'custom',
|
|
254
|
+
message: 'Cron 表达式(如 "0 3 * * *" 表示每天 3 点):',
|
|
255
|
+
validate: v => v.trim().split(/\s+/).length === 5 ? true : '请输入正确的 5 段 Cron 表达式',
|
|
256
|
+
}]);
|
|
257
|
+
cronExpr = custom;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let ssh;
|
|
261
|
+
const spinner = ora('配置定时备份...').start();
|
|
262
|
+
try {
|
|
263
|
+
ssh = await connectSSH(config);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
spinner.fail('连接失败:' + err.message);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const dbConfig = config.database;
|
|
271
|
+
if (!dbConfig) {
|
|
272
|
+
spinner.fail('未配置数据库信息,请先运行一次备份完成配置。');
|
|
273
|
+
ssh.dispose();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const backupDir = `${BACKUP_BASE}/${config.appName}`;
|
|
278
|
+
const ext = dbConfig.type === 'mongodb' ? '.archive.gz' : '.sql.gz';
|
|
279
|
+
|
|
280
|
+
// 生成备份脚本
|
|
281
|
+
const scriptContent = `#!/bin/bash
|
|
282
|
+
TIMESTAMP=$(date +%Y-%m-%dT%H-%M-%S)
|
|
283
|
+
OUTFILE="${backupDir}/${dbConfig.database}_\${TIMESTAMP}${ext}"
|
|
284
|
+
mkdir -p ${backupDir}
|
|
285
|
+
${buildDumpCommand(dbConfig, dbConfig.type === 'mongodb' ? '$OUTFILE' : '${OUTFILE%.gz}')}
|
|
286
|
+
${dbConfig.type !== 'mongodb' ? `gzip -f "\${OUTFILE%.gz}"` : ''}
|
|
287
|
+
ls -t ${backupDir} | tail -n +11 | xargs -I{} rm -f ${backupDir}/{} 2>/dev/null
|
|
288
|
+
echo "[$(date)] Backup completed: \$OUTFILE" >> /var/log/deploy-helper-backup.log
|
|
289
|
+
`;
|
|
290
|
+
|
|
291
|
+
const scriptPath = `/usr/local/bin/deploy-helper-backup-${config.appName}.sh`;
|
|
292
|
+
await runRemoteSilent(ssh, `cat > ${scriptPath} << 'SCRIPT_EOF'\n${scriptContent}\nSCRIPT_EOF`);
|
|
293
|
+
await runRemoteSilent(ssh, `chmod +x ${scriptPath}`);
|
|
294
|
+
|
|
295
|
+
// 注入 crontab
|
|
296
|
+
await runRemoteSilent(
|
|
297
|
+
ssh,
|
|
298
|
+
`(crontab -l 2>/dev/null | grep -v "deploy-helper-backup-${config.appName}"; echo "${cronExpr} ${scriptPath}") | crontab -`
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
spinner.succeed('定时备份配置完成');
|
|
302
|
+
console.log(chalk.gray(` Cron: ${cronExpr}`));
|
|
303
|
+
console.log(chalk.gray(` 日志: /var/log/deploy-helper-backup.log\n`));
|
|
304
|
+
ssh.dispose();
|
|
305
|
+
|
|
306
|
+
} catch (err) {
|
|
307
|
+
spinner.fail('配置失败:' + err.message);
|
|
308
|
+
ssh.dispose();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 供 update.js 调用:部署前自动备份
|
|
313
|
+
export { doBackup };
|