@zhengyizhao/deploy-helper 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhengyizhao/deploy-helper",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Interactive CLI to deploy your project to any VPS server",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -2,26 +2,29 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import inquirer from 'inquirer';
4
4
  import path from 'path';
5
- import fs from 'fs';
6
5
  import { connectSSH, runRemoteSilent } from '../utils/ssh.js';
7
6
  import { loadConfig, saveConfig } from '../utils/config.js';
8
7
 
9
8
  const BACKUP_BASE = '/var/deploy-helper/db-backups';
10
9
 
11
10
  /**
12
- * 根据数据库类型生成备份命令
11
+ * 根据数据库类型生成备份命令。
12
+ * passwordEnvInline=true:把密码以 ENV='xxx' 形式内联(仅用于一次性命令,命令一执行完进程就退出)
13
+ * passwordEnvInline=false:用 cron 脚本里 source 的 credentials 文件(推荐)
13
14
  */
14
- function buildDumpCommand(dbConfig, outputFile) {
15
+ function buildDumpCommand(dbConfig, outputFile, passwordEnvInline = true) {
15
16
  const { type, host, port, user, password, database } = dbConfig;
16
17
  const h = host || '127.0.0.1';
17
18
 
18
19
  if (type === 'mysql') {
19
20
  const p = port || 3306;
20
- return `MYSQL_PWD='${password}' mysqldump -h ${h} -P ${p} -u ${user} ${database} > ${outputFile}`;
21
+ const pwd = passwordEnvInline ? `MYSQL_PWD='${password}' ` : '';
22
+ return `${pwd}mysqldump -h ${h} -P ${p} -u ${user} ${database} > ${outputFile}`;
21
23
  }
22
24
  if (type === 'postgresql') {
23
25
  const p = port || 5432;
24
- return `PGPASSWORD='${password}' pg_dump -h ${h} -p ${p} -U ${user} ${database} > ${outputFile}`;
26
+ const pwd = passwordEnvInline ? `PGPASSWORD='${password}' ` : '';
27
+ return `${pwd}pg_dump -h ${h} -p ${p} -U ${user} ${database} > ${outputFile}`;
25
28
  }
26
29
  if (type === 'mongodb') {
27
30
  const p = port || 27017;
@@ -126,14 +129,14 @@ async function doBackup(config, silent = false) {
126
129
 
127
130
  await runRemoteSilent(ssh, `mkdir -p ${backupDir}`);
128
131
 
129
- // 生成备份
132
+ // 生成备份(一次性命令:内联密码 OK,进程退出即消失)
130
133
  const dumpSpinner = ora(`备份 ${dbConfig.type} 数据库 [${dbConfig.database}]...`).start();
131
- const dumpCmd = buildDumpCommand(dbConfig, dbConfig.type === 'mongodb' ? outputFile : rawFile);
134
+ const dumpCmd = buildDumpCommand(dbConfig, dbConfig.type === 'mongodb' ? outputFile : rawFile, true);
132
135
  const dumpResult = await runRemoteSilent(ssh, dumpCmd);
133
136
 
134
137
  if (dumpResult.code !== 0) {
135
138
  dumpSpinner.fail('备份失败');
136
- console.log(chalk.red(dumpResult.stdout));
139
+ console.log(chalk.red(dumpResult.stderr || dumpResult.stdout));
137
140
  ssh.dispose();
138
141
  return null;
139
142
  }
@@ -277,30 +280,66 @@ async function scheduleBackup(config) {
277
280
  const backupDir = `${BACKUP_BASE}/${config.appName}`;
278
281
  const ext = dbConfig.type === 'mongodb' ? '.archive.gz' : '.sql.gz';
279
282
 
280
- // 生成备份脚本
283
+ // 凭据文件单独存放,权限 600;脚本通过 . credentials.sh 加载
284
+ const credPath = `/etc/deploy-helper/${config.appName}.creds`;
285
+ const credContent = (() => {
286
+ if (dbConfig.type === 'mysql') return `export MYSQL_PWD='${dbConfig.password}'\n`;
287
+ if (dbConfig.type === 'postgresql') return `export PGPASSWORD='${dbConfig.password}'\n`;
288
+ if (dbConfig.type === 'mongodb') return `export DH_MONGO_USER='${dbConfig.user}'\nexport DH_MONGO_PWD='${dbConfig.password}'\n`;
289
+ return '';
290
+ })();
291
+
292
+ await runRemoteSilent(ssh, `mkdir -p /etc/deploy-helper && chmod 700 /etc/deploy-helper`);
293
+ await runRemoteSilent(ssh, `cat > ${credPath} <<'DH_CRED_EOF'\n${credContent}DH_CRED_EOF`);
294
+ await runRemoteSilent(ssh, `chmod 600 ${credPath}`);
295
+
296
+ // 生成备份命令(不内联密码,从 cred 文件加载)
297
+ const dumpForScript = (() => {
298
+ const h = dbConfig.host || '127.0.0.1';
299
+ if (dbConfig.type === 'mysql') {
300
+ return `mysqldump -h ${h} -P ${dbConfig.port || 3306} -u ${dbConfig.user} ${dbConfig.database} > "\${OUTFILE%.gz}"`;
301
+ }
302
+ if (dbConfig.type === 'postgresql') {
303
+ return `pg_dump -h ${h} -p ${dbConfig.port || 5432} -U ${dbConfig.user} ${dbConfig.database} > "\${OUTFILE%.gz}"`;
304
+ }
305
+ if (dbConfig.type === 'mongodb') {
306
+ const auth = dbConfig.password
307
+ ? `--username "$DH_MONGO_USER" --password "$DH_MONGO_PWD" --authenticationDatabase admin`
308
+ : '';
309
+ return `mongodump --host ${h} --port ${dbConfig.port || 27017} ${auth} --db ${dbConfig.database} --archive="$OUTFILE" --gzip`;
310
+ }
311
+ return '';
312
+ })();
313
+
314
+ // 生成备份脚本(heredoc 单引号 EOF:变量不展开,原样写入)
281
315
  const scriptContent = `#!/bin/bash
316
+ set -e
317
+ . ${credPath}
282
318
  TIMESTAMP=$(date +%Y-%m-%dT%H-%M-%S)
283
319
  OUTFILE="${backupDir}/${dbConfig.database}_\${TIMESTAMP}${ext}"
284
320
  mkdir -p ${backupDir}
285
- ${buildDumpCommand(dbConfig, dbConfig.type === 'mongodb' ? '$OUTFILE' : '${OUTFILE%.gz}')}
321
+ ${dumpForScript}
286
322
  ${dbConfig.type !== 'mongodb' ? `gzip -f "\${OUTFILE%.gz}"` : ''}
287
- ls -t ${backupDir} | tail -n +11 | xargs -I{} rm -f ${backupDir}/{} 2>/dev/null
323
+ ls -t ${backupDir} | tail -n +11 | xargs -I{} rm -f ${backupDir}/{} 2>/dev/null || true
288
324
  echo "[$(date)] Backup completed: \$OUTFILE" >> /var/log/deploy-helper-backup.log
289
325
  `;
290
326
 
291
327
  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}`);
328
+ await runRemoteSilent(ssh, `cat > ${scriptPath} <<'DH_SCRIPT_EOF'\n${scriptContent}DH_SCRIPT_EOF`);
329
+ // 脚本本身含密码加载逻辑——chmod 700 仅 root 可读
330
+ await runRemoteSilent(ssh, `chmod 700 ${scriptPath} && chown root:root ${scriptPath}`);
294
331
 
295
- // 注入 crontab
332
+ // 注入 crontab(root 用户的 crontab)
296
333
  await runRemoteSilent(
297
334
  ssh,
298
335
  `(crontab -l 2>/dev/null | grep -v "deploy-helper-backup-${config.appName}"; echo "${cronExpr} ${scriptPath}") | crontab -`
299
336
  );
300
337
 
301
338
  spinner.succeed('定时备份配置完成');
302
- console.log(chalk.gray(` Cron: ${cronExpr}`));
303
- console.log(chalk.gray(` 日志: /var/log/deploy-helper-backup.log\n`));
339
+ console.log(chalk.gray(` Cron 表达式:${cronExpr}`));
340
+ console.log(chalk.gray(` 备份脚本: ${scriptPath} (chmod 700)`));
341
+ console.log(chalk.gray(` 凭据文件: ${credPath} (chmod 600)`));
342
+ console.log(chalk.gray(` 执行日志: /var/log/deploy-helper-backup.log\n`));
304
343
  ssh.dispose();
305
344
 
306
345
  } catch (err) {
@@ -153,24 +153,33 @@ async function pushEnv(config, envContent, vars) {
153
153
  await runRemoteSilent(ssh, `chmod 600 ${config.remotePath}/.env`);
154
154
  uploadSpinner.succeed('.env 上传完成,权限已设为 600');
155
155
 
156
- // 重启服务让新环境变量生效
157
- const { restart } = await inquirer.prompt([{
158
- type: 'confirm',
159
- name: 'restart',
160
- message: '是否重启服务让新变量生效?',
161
- default: true,
162
- }]);
156
+ // cron 模式不需要重启(下次定时使用新环境变量)
157
+ const appMode = config.appMode || 'web';
158
+ if (appMode === 'cron') {
159
+ console.log(chalk.gray(' ℹ 定时任务模式,新 .env 将在下次执行时生效'));
160
+ } else {
161
+ const { restart } = await inquirer.prompt([{
162
+ type: 'confirm',
163
+ name: 'restart',
164
+ message: '是否重启服务让新变量生效?',
165
+ default: true,
166
+ }]);
163
167
 
164
- if (restart) {
165
- const restartSpinner = ora('重启服务...').start();
166
- if (config.projectType === 'nodejs') {
167
- await runRemoteSilent(ssh, `pm2 restart ${config.appName}`);
168
- } else if (config.projectType === 'python') {
169
- await runRemoteSilent(ssh, `supervisorctl restart ${config.appName}`);
170
- } else if (config.projectType === 'docker') {
171
- await runRemoteSilent(ssh, `cd ${config.remotePath} && docker compose up -d`);
168
+ if (restart) {
169
+ const restartSpinner = ora('重启服务...').start();
170
+ if (config.projectType === 'nodejs') {
171
+ await runRemoteSilent(ssh, `pm2 restart ${config.appName}`);
172
+ } else if (config.projectType === 'python') {
173
+ await runRemoteSilent(ssh, `supervisorctl restart ${config.appName}`);
174
+ } else if (config.projectType === 'docker') {
175
+ if (config.composeFile) {
176
+ await runRemoteSilent(ssh, `cd ${config.remotePath} && docker compose -f ${config.composeFile} up -d`);
177
+ } else {
178
+ await runRemoteSilent(ssh, `docker restart ${config.appName}`);
179
+ }
180
+ }
181
+ restartSpinner.succeed('服务已重启');
172
182
  }
173
- restartSpinner.succeed('服务已重启');
174
183
  }
175
184
 
176
185
  ssh.dispose();