@noahyu/cd-cli 1.1.1 → 1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # 简易项目部署工具
1
+ # 简易的项目部署工具
2
2
 
3
- 一个简易的部署工具,支持版本管理和零停机部署。
3
+ > 一个简易的部署工具,支持版本管理和零停机部署。
4
4
 
5
5
  ## 功能特性
6
6
 
@@ -35,7 +35,8 @@ pcli-cd init
35
35
 
36
36
  这会在项目根目录创建 `pcli-cd.config.js` 配置文件。
37
37
 
38
- - **重要** `pcli-cd.config.js` 文件不应该提交到 Git
38
+ > [!WARNING]
39
+ > `pcli-cd.config.js` 配置文件存在敏感信息,不应该提交到 Git
39
40
 
40
41
  ### 2. 部署项目
41
42
 
@@ -87,7 +88,8 @@ PM2 配置始终指向软链接 `.output/server/index.mjs`,这样切换版本
87
88
 
88
89
  配置文件 `pcli-cd.config.js` 示例:
89
90
 
90
- > **重要** `pcli-cd.config.js` 文件不应该提交到 Git
91
+ > [!WARNING]
92
+ > `pcli-cd.config.js` 配置文件存在敏感信息,不应该提交到 Git
91
93
 
92
94
  ```javascript
93
95
  // pcli-cd 部署配置文件
@@ -106,13 +108,10 @@ export default {
106
108
  port: 22,
107
109
  /** 用户名 */
108
110
  username: 'root',
109
- // SSH 认证方式(优先级:privateKey > privateKeyPath > password)
110
- /** 私钥 */
111
- privateKey: '-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----',
112
- /** 私钥文件路径 */
113
- privateKeyPath: '/home/user/.ssh/id_rsa',
114
- /** 密码 */
115
- password: 'your-password',
111
+ /** SSH 认证方式(优先级:privateKey > privateKeyPath > password) */
112
+ privateKey: '-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----', // 私钥
113
+ privateKeyPath: '/home/user/.ssh/id_rsa', // 私钥文件路径
114
+ password: 'your-password', // 密码
116
115
  /** 部署目录 */
117
116
  deployPath: '/var/www/your-app',
118
117
  },
@@ -344,7 +343,8 @@ pcli-cd rollback
344
343
 
345
344
  每个项目只需要一个 `pcli-cd.config.js` 配置文件,工具会自动读取当前目录下的配置。
346
345
 
347
- ⚠️ **重要提醒** `pcli-cd.config.js` 文件不应该提交到 Git
346
+ > [!WARNING]
347
+ > `pcli-cd.config.js` 配置文件存在敏感信息,不应该提交到 Git
348
348
 
349
349
  ## 注意事项
350
350
 
@@ -420,7 +420,3 @@ ssh -p 22 root@your-server.com
420
420
  # 测试指定私钥连接
421
421
  ssh -i /home/user/.ssh/id_rsa -p 22 root@your-server.com
422
422
  ```
423
-
424
- ## License
425
-
426
- MIT
package/dist/bin/cli.js CHANGED
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from 'commander';
3
3
  import { deployCommand, initConfig, listVersions, rollbackVersion } from '../src/index.js';
4
+ import { getVersion } from '../src/utils/get-version.js';
4
5
 
5
- program.version('1.0.0').description('Simple CI/CD deployment tool');
6
+ program
7
+ .version(getVersion('../../package.json', import.meta.url), '-v, --version')
8
+ .description('Simple CI/CD deployment tool');
6
9
  program
7
10
  .command('deploy')
8
11
  .alias('cd')
9
12
  .description('Build and deploy project to server')
10
13
  .option('-c, --config <config>', 'Configuration file path', './pcli-cd.config.js')
11
14
  .option('-v, --version <version>', 'Specify version number')
15
+ .option('-n, --name <name>', 'Specify deployment environment name (dev, prod, staging, etc.). If not specified, will prompt to select interactively')
12
16
  .action(deployCommand);
13
17
  program.command('init').description('Initialize CD configuration file').action(initConfig);
14
18
  program
@@ -16,6 +20,7 @@ program
16
20
  .alias('ls')
17
21
  .description('List deployed versions on server')
18
22
  .option('-c, --config <config>', 'Configuration file path', './pcli-cd.config.js')
23
+ .option('-n, --name <name>', 'Specify environment name to list (dev, prod, staging, etc.). If not specified, will prompt to select interactively')
19
24
  .action(listVersions);
20
25
  program
21
26
  .command('rollback')
@@ -23,5 +28,6 @@ program
23
28
  .description('Rollback to a previous version')
24
29
  .option('-c, --config <config>', 'Configuration file path', './pcli-cd.config.js')
25
30
  .option('-v, --version <version>', 'Version to rollback to')
31
+ .option('-n, --name <name>', 'Specify environment name to rollback (dev, prod, staging, etc.). If not specified, will prompt to select interactively')
26
32
  .action(rollbackVersion);
27
33
  program.parse(process.argv);
package/dist/src/index.js CHANGED
@@ -8,24 +8,23 @@ import { NodeSSH } from 'node-ssh';
8
8
  import ora from 'ora';
9
9
  import chalk from 'chalk';
10
10
  import inquirer from 'inquirer';
11
+ import { renderConfigTemplate } from './utils/template.js';
11
12
 
12
13
  async function deployCommand(options) {
13
14
  const configPath = resolve(process.cwd(), options.config);
14
- if (!existsSync(configPath)) {
15
- console.log(chalk.red(`❌ 配置文件不存在: ${configPath}`));
16
- console.log(chalk.yellow('💡 请创建 pcli-cd.config.js 配置文件'));
17
- process.exit(1);
18
- }
15
+ const configResult = await resolveEnvConfig(configPath, options.name);
19
16
  let config;
20
- try {
21
- const configModule = await import(configPath);
22
- config = configModule.default || configModule;
17
+ if (configResult.targetConfig) {
18
+ config = configResult.targetConfig;
23
19
  }
24
- catch (error) {
25
- console.log(chalk.red(`❌ 配置文件读取失败: ${error}`));
26
- process.exit(1);
20
+ else {
21
+ const selected = await selectEnvironmentFromConfigs(configResult.allConfigs);
22
+ config = selected.targetConfig;
27
23
  }
28
- if (!config.version && !options.version) {
24
+ console.log(chalk.blue(`🚀 部署环境: ${chalk.bold(config.name)}`));
25
+ console.log(chalk.gray(`📍 部署路径: ${config.server.deployPath}`));
26
+ let version = options.version;
27
+ if (!version) {
29
28
  const answers = await inquirer.prompt([
30
29
  {
31
30
  type: 'input',
@@ -35,18 +34,14 @@ async function deployCommand(options) {
35
34
  validate: (input) => input.trim() !== '' || '版本号不能为空',
36
35
  },
37
36
  ]);
38
- config.version = answers.version;
37
+ version = answers.version;
39
38
  }
40
- else {
41
- config.version = options.version || config.version;
42
- }
43
- await deploy(config);
39
+ await deploy(config, version);
44
40
  }
45
- async function deploy(config) {
41
+ async function deploy(config, version) {
46
42
  const spinner = ora();
47
43
  const tempDir = join(process.cwd(), '.deploy-temp');
48
44
  const zipPath = join(tempDir, 'build.zip');
49
- const version = config.version || `v${Date.now()}`;
50
45
  const buildDirName = config.buildDir.split('/').pop() || 'build';
51
46
  try {
52
47
  await fse.remove(tempDir);
@@ -206,6 +201,13 @@ async function cleanOldVersions(ssh, deployPath, buildDirName, keepCount) {
206
201
  async function initConfig() {
207
202
  console.log(chalk.blue('🚀 初始化配置文件'));
208
203
  const answers = await inquirer.prompt([
204
+ {
205
+ type: 'input',
206
+ name: 'envName',
207
+ message: '环境名称 (如: dev, prod, staging):',
208
+ default: 'dev',
209
+ validate: (input) => input.trim() !== '' || '请输入环境名称',
210
+ },
209
211
  {
210
212
  type: 'input',
211
213
  name: 'buildCommand',
@@ -253,44 +255,35 @@ async function initConfig() {
253
255
  message: 'PM2 应用名称 (可选):',
254
256
  },
255
257
  ]);
256
- const config = {
258
+ const configContent = renderConfigTemplate({
259
+ envName: answers.envName,
257
260
  buildCommand: answers.buildCommand,
258
261
  buildDir: answers.buildDir,
259
- server: {
260
- host: answers.host,
261
- port: parseInt(answers.port),
262
- username: answers.username,
263
- privateKeyPath: answers.privateKeyPath || undefined,
264
- deployPath: answers.deployPath,
265
- },
266
- excludeFiles: [],
267
- };
268
- if (answers.pm2AppName) {
269
- config.pm2 = {
270
- appName: answers.pm2AppName,
271
- restart: true,
272
- };
273
- }
274
- const configContent = `// pcli-cd 部署配置文件
275
- export default ${JSON.stringify(config, null, 2)}`;
262
+ host: answers.host,
263
+ port: answers.port,
264
+ username: answers.username,
265
+ privateKeyPath: answers.privateKeyPath || undefined,
266
+ deployPath: answers.deployPath,
267
+ pm2AppName: answers.pm2AppName || undefined,
268
+ });
276
269
  await fse.writeFile('pcli-cd.config.js', configContent);
277
270
  console.log(chalk.green('✅ 配置文件已创建: pcli-cd.config.js'));
271
+ console.log(chalk.blue(`📝 默认环境: ${answers.envName}`));
272
+ console.log(chalk.gray('💡 可以在 apps 数组中添加更多环境配置'));
273
+ console.log(chalk.gray('💡 配置文件包含详细的注释说明'));
278
274
  }
279
275
  async function listVersions(options) {
280
276
  const configPath = resolve(process.cwd(), options.config);
281
- if (!existsSync(configPath)) {
282
- console.log(chalk.red(`❌ 配置文件不存在: ${configPath}`));
283
- process.exit(1);
284
- }
277
+ const configResult = await resolveEnvConfig(configPath, options.name);
285
278
  let config;
286
- try {
287
- const configModule = await import(configPath);
288
- config = configModule.default || configModule;
279
+ if (configResult.targetConfig) {
280
+ config = configResult.targetConfig;
289
281
  }
290
- catch (error) {
291
- console.log(chalk.red(`❌ 配置文件读取失败: ${error}`));
292
- process.exit(1);
282
+ else {
283
+ const selected = await selectEnvironmentFromConfigs(configResult.allConfigs);
284
+ config = selected.targetConfig;
293
285
  }
286
+ console.log(chalk.blue(`🔍 查看环境: ${chalk.bold(config.name)}`));
294
287
  const spinner = ora('正在获取版本列表...');
295
288
  spinner.start();
296
289
  try {
@@ -344,19 +337,16 @@ async function listVersions(options) {
344
337
  }
345
338
  async function rollbackVersion(options) {
346
339
  const configPath = resolve(process.cwd(), options.config);
347
- if (!existsSync(configPath)) {
348
- console.log(chalk.red(`❌ 配置文件不存在: ${configPath}`));
349
- process.exit(1);
350
- }
340
+ const configResult = await resolveEnvConfig(configPath, options.name);
351
341
  let config;
352
- try {
353
- const configModule = await import(configPath);
354
- config = configModule.default || configModule;
342
+ if (configResult.targetConfig) {
343
+ config = configResult.targetConfig;
355
344
  }
356
- catch (error) {
357
- console.log(chalk.red(`❌ 配置文件读取失败: ${error}`));
358
- process.exit(1);
345
+ else {
346
+ const selected = await selectEnvironmentFromConfigs(configResult.allConfigs);
347
+ config = selected.targetConfig;
359
348
  }
349
+ console.log(chalk.blue(`⏪ 回滚环境: ${chalk.bold(config.name)}`));
360
350
  const buildDirName = config.buildDir.split('/').pop() || 'build';
361
351
  let targetVersion = options.version;
362
352
  if (!targetVersion) {
@@ -453,6 +443,76 @@ async function performRollback(config, targetVersion, buildDirName) {
453
443
  process.exit(1);
454
444
  }
455
445
  }
446
+ async function resolveEnvConfig(configPath, envName) {
447
+ if (!existsSync(configPath)) {
448
+ console.log(chalk.red(`❌ 配置文件不存在: ${configPath}`));
449
+ console.log(chalk.yellow('💡 请创建 pcli-cd.config.js 配置文件'));
450
+ process.exit(1);
451
+ }
452
+ let rawConfig;
453
+ try {
454
+ const configModule = await import(configPath);
455
+ rawConfig = configModule.default || configModule;
456
+ }
457
+ catch (error) {
458
+ console.log(chalk.red(`❌ 配置文件读取失败: ${error}`));
459
+ process.exit(1);
460
+ }
461
+ if (!rawConfig.apps || !Array.isArray(rawConfig.apps)) {
462
+ console.log(chalk.red('❌ 配置文件格式错误:缺少 apps 数组'));
463
+ console.log(chalk.yellow('💡 配置文件应该包含一个 apps 数组,每个元素都是一个环境配置'));
464
+ process.exit(1);
465
+ }
466
+ if (rawConfig.apps.length === 0) {
467
+ console.log(chalk.red('❌ 配置文件中没有任何环境配置'));
468
+ console.log(chalk.yellow('💡 请在 apps 数组中添加至少一个环境配置'));
469
+ process.exit(1);
470
+ }
471
+ if (!envName) {
472
+ return {
473
+ targetConfig: null,
474
+ allConfigs: rawConfig.apps,
475
+ };
476
+ }
477
+ const envConfig = rawConfig.apps.find((app) => app.name === envName);
478
+ if (!envConfig) {
479
+ console.log(chalk.red(`❌ 环境配置 "${envName}" 不存在`));
480
+ console.log(chalk.yellow('💡 可用的环境配置:'));
481
+ rawConfig.apps.forEach((app) => {
482
+ console.log(chalk.gray(` - ${app.name}`));
483
+ });
484
+ process.exit(1);
485
+ }
486
+ return {
487
+ targetConfig: envConfig,
488
+ allConfigs: rawConfig.apps,
489
+ };
490
+ }
491
+ async function selectEnvironmentFromConfigs(allConfigs) {
492
+ if (allConfigs.length === 1) {
493
+ return {
494
+ envName: allConfigs[0].name,
495
+ targetConfig: allConfigs[0],
496
+ };
497
+ }
498
+ const envChoices = allConfigs.map((app) => ({
499
+ name: `${app.name} (${app.server.host})`,
500
+ value: app.name,
501
+ }));
502
+ const answers = await inquirer.prompt([
503
+ {
504
+ type: 'list',
505
+ name: 'envName',
506
+ message: '请选择环境:',
507
+ choices: envChoices,
508
+ },
509
+ ]);
510
+ const selectedConfig = allConfigs.find((app) => app.name === answers.envName);
511
+ return {
512
+ envName: answers.envName,
513
+ targetConfig: selectedConfig,
514
+ };
515
+ }
456
516
  async function createSSHConnection(server) {
457
517
  const ssh = new NodeSSH();
458
518
  const connectConfig = {
@@ -0,0 +1,66 @@
1
+ // pcli-cd.config.js
2
+
3
+ export default {
4
+ apps: [
5
+ {
6
+ // 环境名称
7
+ name: '<%= envName %>',
8
+
9
+ // 构建命令,部署前执行
10
+ buildCommand: '<%= buildCommand %>',
11
+
12
+ // 构建输出目录
13
+ buildDir: '<%= buildDir %>',
14
+
15
+ // 服务器配置
16
+ server: {
17
+ host: '<%= host %>',
18
+ port: <%= port %>,
19
+ username: '<%= username %>',<% if (privateKeyPath) { %>
20
+ privateKeyPath: '<%= privateKeyPath %>',<% } else { %>
21
+ // 私钥路径
22
+ // privateKeyPath: '~/.ssh/id_rsa',
23
+
24
+ // 或者使用私钥内容
25
+ // privateKey: `-----BEGIN OPENSSH PRIVATE KEY-----
26
+ // ...
27
+ // -----END OPENSSH PRIVATE KEY-----`,
28
+
29
+ // 或者使用密码
30
+ // password: 'your-password',<% } %>
31
+ deployPath: '<%= deployPath %>'
32
+ },<% if (pm2AppName) { %>
33
+
34
+ // PM2 进程管理配置
35
+ pm2: {
36
+ appName: '<%= pm2AppName %>',
37
+ restart: true
38
+ },<% } else { %>
39
+
40
+ // PM2 进程管理配置(可选)
41
+ // pm2: {
42
+ // appName: 'my-app',
43
+ // restart: true
44
+ // },<% } %>
45
+
46
+ // 排除文件列表(可选)
47
+ excludeFiles: [
48
+ // 'node_modules/**',
49
+ // '*.log',
50
+ // '.env*'
51
+ ],
52
+
53
+ // 部署前命令(可选)
54
+ // beforeDeploy: [
55
+ // 'npm test',
56
+ // 'npm run lint'
57
+ // ],
58
+
59
+ // 部署后命令(可选)
60
+ // afterDeploy: [
61
+ // 'npm install --production',
62
+ // 'npm run migrate'
63
+ // ]
64
+ }
65
+ ]
66
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import fse from 'fs-extra';
3
+ import parseJson from 'parse-json';
4
+
5
+ function getVersion(path, baseUrl) {
6
+ const packageJsonPath = new URL(path, baseUrl);
7
+ let packageJsonContent;
8
+ try {
9
+ packageJsonContent = fse.readFileSync(packageJsonPath, 'utf-8');
10
+ }
11
+ catch {
12
+ throw new Error(`无法找到 package.json 文件:${packageJsonPath}`);
13
+ }
14
+ let packageData;
15
+ try {
16
+ packageData = parseJson(packageJsonContent);
17
+ }
18
+ catch {
19
+ throw new Error('package.json 文件格式错误,请检查 JSON 语法');
20
+ }
21
+ if (!packageData.version) {
22
+ throw new Error('package.json 中缺少 version 字段');
23
+ }
24
+ return packageData.version || '0.0.0';
25
+ }
26
+
27
+ export { getVersion };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import ejs from 'ejs';
6
+
7
+ function renderConfigTemplate(templateData) {
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const templatePath = join(__dirname, '../templates/config.ejs');
11
+ try {
12
+ const templateContent = readFileSync(templatePath, 'utf-8');
13
+ return ejs.render(templateContent, templateData);
14
+ }
15
+ catch (error) {
16
+ throw new Error(`模板渲染失败: ${error}`);
17
+ }
18
+ }
19
+
20
+ export { renderConfigTemplate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noahyu/cd-cli",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Global CLI tool for simple project deployment with version management",
5
5
  "type": "module",
6
6
  "main": "./dist/bin/cli.js",