@mindbase/deploy 1.1.5 → 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 +229 -0
- package/bin/shared.ts +1 -24
- package/package.json +2 -1
- package/src/app.ts +59 -103
- package/src/config/schema.ts +0 -2
- package/src/init/app.ts +8 -4
- package/src/script/parser.ts +49 -9
- package/src/script/runner.ts +15 -14
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# deploy CLI
|
|
2
|
+
|
|
3
|
+
多项目部署工具,支持批量部署到多台服务器。
|
|
4
|
+
|
|
5
|
+
## 功能特点
|
|
6
|
+
|
|
7
|
+
- **交互式部署**:通过友好的命令行界面选择目标服务器和项目
|
|
8
|
+
- **笛卡尔组合部署**:选中的每个服务器都会部署全部选中的项目(N 个服务器 × M 个项目 = N×M 次部署)
|
|
9
|
+
- **灵活配置**:项目部署步骤完全自定义,支持本地命令、远程命令、文件上传/下载等
|
|
10
|
+
- **多环境支持**:部署时现场选择目标服务器,不再写死在配置中
|
|
11
|
+
|
|
12
|
+
## 命令结构
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
deploy # 部署:选择服务器 → 选择项目 → 开始部署
|
|
16
|
+
deploy init [path] # 初始化项目配置(path 缺省为当前目录)
|
|
17
|
+
deploy server ... # 服务器管理
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 快速开始
|
|
21
|
+
|
|
22
|
+
### 1. 配置服务器
|
|
23
|
+
|
|
24
|
+
首次使用需要先添加目标服务器:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 添加服务器
|
|
28
|
+
deploy server add
|
|
29
|
+
|
|
30
|
+
# 列出所有服务器
|
|
31
|
+
deploy server list
|
|
32
|
+
|
|
33
|
+
# 测试连接
|
|
34
|
+
deploy server test
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
服务器配置保存在 `~/.mindbase/deploy.json`,支持密码和私钥两种认证方式。
|
|
38
|
+
|
|
39
|
+
### 2. 初始化项目
|
|
40
|
+
|
|
41
|
+
在项目目录下运行:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd /path/to/your/project
|
|
45
|
+
deploy init
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
交互式引导会询问:
|
|
49
|
+
- 项目名称
|
|
50
|
+
- 目标服务器(选择已配置的服务器,用于 SFTP 连接浏览远端目录)
|
|
51
|
+
- 本地目录(默认 `./`)
|
|
52
|
+
- 远端目录(可浏览服务器目录或手动输入)
|
|
53
|
+
- 部署步骤(添加构建、上传、重启等步骤)
|
|
54
|
+
|
|
55
|
+
完成后会在当前目录生成 `deploy.config.json` 配置文件。
|
|
56
|
+
|
|
57
|
+
### 3. 部署
|
|
58
|
+
|
|
59
|
+
在包含多个 `deploy.config.json` 的目录(如 monorepo 根目录)运行:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
deploy
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
交互流程:
|
|
66
|
+
1. **选择服务器**(多选,空格切换,a 全选)
|
|
67
|
+
2. **选择项目**(多选)
|
|
68
|
+
3. **确认摘要**(显示 N×M=K 次部署)
|
|
69
|
+
4. **开始部署**(依次连接每个服务器,部署所有选中项目)
|
|
70
|
+
|
|
71
|
+
## 配置文件结构
|
|
72
|
+
|
|
73
|
+
`deploy.config.json` 示例:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"name": "photo",
|
|
78
|
+
"localDir": "./",
|
|
79
|
+
"remoteDir": "/application/lightUp-web/photo/",
|
|
80
|
+
"steps": [
|
|
81
|
+
{ "type": "log", "message": "构建相册" },
|
|
82
|
+
{ "type": "local", "cmd": "pnpm run build", "cwd": "${localDir}" },
|
|
83
|
+
{ "type": "remote", "cmd": "mkdir -p ${remoteDir}" },
|
|
84
|
+
{ "type": "uploadDir", "local": "${localDir}/dist", "remote": "${remoteDir}" }
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 配置字段说明
|
|
90
|
+
|
|
91
|
+
| 字段 | 类型 | 说明 |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `name` | string | 项目显示名称 |
|
|
94
|
+
| `localDir` | string | 本地目录(相对或绝对路径) |
|
|
95
|
+
| `remoteDir` | string | 远端部署目录 |
|
|
96
|
+
| `steps` | Step[] | 部署步骤数组 |
|
|
97
|
+
|
|
98
|
+
### 步骤类型(Step)
|
|
99
|
+
|
|
100
|
+
| 类型 | 说明 | 示例 |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `log` | 输出日志信息 | `{ "type": "log", "message": "开始构建" }` |
|
|
103
|
+
| `local` | 本地执行命令 | `{ "type": "local", "cmd": "npm run build", "cwd": "${localDir}" }` |
|
|
104
|
+
| `remote` | 远程执行命令 | `{ "type": "remote", "cmd": "pm2 restart myapp" }` |
|
|
105
|
+
| `uploadDir` | 上传整个目录 | `{ "type": "uploadDir", "local": "./dist", "remote": "/app/dist" }` |
|
|
106
|
+
| `uploadFile` | 上传单个文件 | `{ "type": "uploadFile", "local": "./package.json", "remote": "/app/package.json" }` |
|
|
107
|
+
| `downloadDir` | 下载目录(备份) | `{ "type": "downloadDir", "remote": "/app/data", "local": "./backup" }` |
|
|
108
|
+
| `downloadFile` | 下载文件(备份) | `{ "type": "downloadFile", "remote": "/app/config.json", "local": "./backup.json" }` |
|
|
109
|
+
|
|
110
|
+
### 变量替换
|
|
111
|
+
|
|
112
|
+
步骤中支持以下变量,运行时自动替换:
|
|
113
|
+
|
|
114
|
+
| 变量 | 说明 | 示例值 |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `${localDir}` | 项目本地目录 | `/path/to/project` |
|
|
117
|
+
| `${remoteDir}` | 项目远端目录 | `/app/photo/` |
|
|
118
|
+
| `${projectName}` | 项目名称 | `photo` |
|
|
119
|
+
| `${date}` | 当前日期 | `2026-06-17` |
|
|
120
|
+
| `${datetime}` | 当前日期时间 | `2026-06-17_14-30-52` |
|
|
121
|
+
| `${timestamp}` | Unix 毫秒时间戳 | `1718621452000` |
|
|
122
|
+
|
|
123
|
+
日期变量可用 `${name:格式}` 自定义格式,如 `${datetime:YYYYMMDD-HHmmss}` → `20260617-143052`。
|
|
124
|
+
|
|
125
|
+
备份示例:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"type": "downloadDir",
|
|
130
|
+
"remote": "${remoteDir}",
|
|
131
|
+
"local": "${localDir}/backup_${datetime}"
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 服务器管理
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# 添加服务器
|
|
139
|
+
deploy server add
|
|
140
|
+
|
|
141
|
+
# 编辑服务器
|
|
142
|
+
deploy server edit
|
|
143
|
+
|
|
144
|
+
# 删除服务器
|
|
145
|
+
deploy server delete
|
|
146
|
+
|
|
147
|
+
# 测试连接
|
|
148
|
+
deploy server test
|
|
149
|
+
|
|
150
|
+
# 列出所有服务器
|
|
151
|
+
deploy server list
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 全局配置文件
|
|
155
|
+
|
|
156
|
+
服务器配置保存在 `~/.mindbase/deploy.json`:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"servers": {
|
|
161
|
+
"own": {
|
|
162
|
+
"host": "192.168.1.100",
|
|
163
|
+
"port": 22,
|
|
164
|
+
"username": "root",
|
|
165
|
+
"password": "***"
|
|
166
|
+
},
|
|
167
|
+
"backup": {
|
|
168
|
+
"host": "192.168.1.101",
|
|
169
|
+
"port": 22,
|
|
170
|
+
"username": "root",
|
|
171
|
+
"privateKeyPath": "~/.ssh/id_rsa",
|
|
172
|
+
"passphrase": "***"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 使用场景
|
|
179
|
+
|
|
180
|
+
### Monorepo 批量部署
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
cd /path/to/monorepo # 包含多个子项目,每个都有 deploy.config.json
|
|
184
|
+
deploy # 选择服务器和项目,一键部署所有
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 单项目部署
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
cd /path/to/project
|
|
191
|
+
deploy # 单选本项目,部署到选中服务器
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 多环境部署
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
deploy # 多选 staging、production 两台服务器
|
|
198
|
+
# 选择所有项目,一次性部署到所有环境
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## 边界情况处理
|
|
202
|
+
|
|
203
|
+
- **服务器连接失败**:询问是否跳过并继续下一个服务器
|
|
204
|
+
- **项目部署失败**:询问是否继续部署下一个项目(当前服务器连接保持)
|
|
205
|
+
- **无可用服务器**:提示先运行 `deploy server add`
|
|
206
|
+
- **无可用项目**:提示先运行 `deploy init`
|
|
207
|
+
- **用户取消**:任何交互步骤按 Ctrl+C 均可安全退出
|
|
208
|
+
|
|
209
|
+
## 技术实现
|
|
210
|
+
|
|
211
|
+
- **依赖**:`@clack/prompts`(交互)、`ssh2-sftp-client`(SFTP)、`commander.js`(命令框架)
|
|
212
|
+
- **变量解析**:步骤中的变量在运行时替换
|
|
213
|
+
- **错误处理**:失败时询问用户决策,支持跳过/中止
|
|
214
|
+
- **日志输出**:统一使用 `@clack/prompts` 格式,逐行显示命令输出
|
|
215
|
+
|
|
216
|
+
## 开发
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# 直接运行(无需构建)
|
|
220
|
+
cd cli/apps/deploy
|
|
221
|
+
tsx bin/index.ts
|
|
222
|
+
|
|
223
|
+
# 或在项目根目录
|
|
224
|
+
tsx cli/apps/deploy/bin/index.ts
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT
|
package/bin/shared.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import * as clack from '@clack/prompts';
|
|
3
|
-
import { runApp
|
|
3
|
+
import { runApp } from '../src/app.js';
|
|
4
4
|
import { runInit } from '../src/init/app.js';
|
|
5
5
|
import { createServerCommand } from '../src/server/app.js';
|
|
6
6
|
|
|
@@ -14,14 +14,12 @@ export function createDeployProgram(): Command {
|
|
|
14
14
|
.description('部署工具')
|
|
15
15
|
.version('1.0.0')
|
|
16
16
|
.action(async () => {
|
|
17
|
-
clack.intro('deploy');
|
|
18
17
|
try {
|
|
19
18
|
await runApp();
|
|
20
19
|
} catch (err) {
|
|
21
20
|
clack.log.error(`执行失败: ${err}`);
|
|
22
21
|
process.exit(1);
|
|
23
22
|
}
|
|
24
|
-
clack.outro('再见');
|
|
25
23
|
});
|
|
26
24
|
|
|
27
25
|
program
|
|
@@ -37,27 +35,6 @@ export function createDeployProgram(): Command {
|
|
|
37
35
|
}
|
|
38
36
|
});
|
|
39
37
|
|
|
40
|
-
program
|
|
41
|
-
.command('run [path]')
|
|
42
|
-
.description('部署单个项目(path 缺省为 cwd,可传目录或 deploy.config.json)')
|
|
43
|
-
.option('-s, --server <server>', '覆盖项目配置中的 server')
|
|
44
|
-
.action(async (path: string | undefined, options: { server?: string }) => {
|
|
45
|
-
clack.intro('deploy run');
|
|
46
|
-
try {
|
|
47
|
-
await runProject(path, options.server);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
clack.log.error(`执行失败: ${err}`);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
program
|
|
55
|
-
.command('config')
|
|
56
|
-
.description('查看全局 servers 配置')
|
|
57
|
-
.action(() => {
|
|
58
|
-
showConfig();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
38
|
// 服务器管理子命令
|
|
62
39
|
program.addCommand(createServerCommand());
|
|
63
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindbase/deploy",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "MindBase 部署工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@clack/prompts": "^0.9.1",
|
|
22
22
|
"commander": "^12.0.0",
|
|
23
23
|
"cross-spawn": "^7.0.6",
|
|
24
|
+
"dayjs": "^1.11.13",
|
|
24
25
|
"picocolors": "^1.1.1",
|
|
25
26
|
"ssh2-sftp-client": "^9.1.0",
|
|
26
27
|
"tsx": "^4.19.0",
|
package/src/app.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as clack from '@clack/prompts';
|
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import { relative } from 'path';
|
|
4
4
|
import { noteBox } from '@mindbase/cli-ui';
|
|
5
|
-
import { getConfig,
|
|
5
|
+
import { getConfig, discoverProjectConfigs } from './config/manager.js';
|
|
6
6
|
import type { ProjectConfig } from './config/schema.js';
|
|
7
7
|
import { connectSftp } from './ssh/client.js';
|
|
8
8
|
import { runSteps } from './script/runner.js';
|
|
@@ -17,86 +17,18 @@ function handleCancel<T>(result: T | symbol): T {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*/
|
|
22
|
-
export function showConfig(): void {
|
|
23
|
-
clack.intro('配置信息');
|
|
24
|
-
const config = getConfig();
|
|
25
|
-
const path = getConfigPath();
|
|
26
|
-
noteBox(`配置文件: ${path}\n\n${JSON.stringify(config, null, 2)}`, '当前配置');
|
|
27
|
-
clack.outro('完成');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 显示项目配置摘要并让用户确认部署
|
|
32
|
-
*/
|
|
33
|
-
async function confirmProjectDeploy(project: ProjectConfig, serverId: string): Promise<boolean> {
|
|
34
|
-
const summary = [
|
|
35
|
-
`名称: ${project.name}`,
|
|
36
|
-
`服务器: ${serverId}`,
|
|
37
|
-
`本地目录: ${project.localDir}`,
|
|
38
|
-
`远端目录: ${project.remoteDir}`,
|
|
39
|
-
`步骤数: ${project.steps.length}`,
|
|
40
|
-
].join('\n');
|
|
41
|
-
noteBox(summary, '即将部署');
|
|
42
|
-
return handleCancel<boolean>(
|
|
43
|
-
await clack.confirm({ message: '确认部署?', initialValue: true })
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 直接运行指定项目(单项目模式)
|
|
49
|
-
*/
|
|
50
|
-
export async function runProject(targetPath?: string, serverOverride?: string): Promise<void> {
|
|
51
|
-
const project = loadProjectConfig(targetPath);
|
|
52
|
-
const globalConfig = getConfig();
|
|
53
|
-
|
|
54
|
-
const serverId = serverOverride || project.server;
|
|
55
|
-
const serverConfig = globalConfig.servers[serverId];
|
|
56
|
-
if (!serverConfig) {
|
|
57
|
-
clack.log.error(`服务器 "${serverId}" 不存在,请先运行 deploy server add`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
clack.intro(`部署 ${pc.cyan(project.name)} → ${pc.cyan(serverId)}`);
|
|
62
|
-
|
|
63
|
-
if (!(await confirmProjectDeploy(project, serverId))) {
|
|
64
|
-
clack.log.info('已取消');
|
|
65
|
-
clack.outro('完成');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const spinner = clack.spinner();
|
|
70
|
-
spinner.start('连接服务器...');
|
|
71
|
-
const sftp = await connectSftp(project.name, serverConfig);
|
|
72
|
-
spinner.stop('已连接');
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await runSteps(project.steps, project, sftp);
|
|
76
|
-
clack.log.success(`${project.name} 部署完成`);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
clack.log.error(`部署失败: ${err}`);
|
|
79
|
-
} finally {
|
|
80
|
-
await sftp.end();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
clack.outro('完成');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* 交互式主流程:扫描 cwd 下所有 deploy.config.json → 多选 → 按 server 分组部署
|
|
20
|
+
* 交互式主流程:选 server → 选项目 → 按笛卡尔分组部署
|
|
88
21
|
*/
|
|
89
22
|
export async function runApp(): Promise<void> {
|
|
90
23
|
const cwd = process.cwd();
|
|
91
24
|
const discovered = discoverProjectConfigs(cwd);
|
|
92
25
|
const globalConfig = getConfig();
|
|
93
26
|
|
|
27
|
+
// 边界校验(intro 之前)
|
|
94
28
|
if (discovered.length === 0) {
|
|
95
29
|
clack.log.error(`未在 ${cwd} 下找到任何 deploy.config.json,请先运行 deploy init`);
|
|
96
30
|
return;
|
|
97
31
|
}
|
|
98
|
-
|
|
99
|
-
// 0 个服务器直接报错
|
|
100
32
|
if (Object.keys(globalConfig.servers).length === 0) {
|
|
101
33
|
clack.log.error('没有已配置的服务器,请先运行 deploy server add');
|
|
102
34
|
return;
|
|
@@ -104,8 +36,21 @@ export async function runApp(): Promise<void> {
|
|
|
104
36
|
|
|
105
37
|
clack.intro('deploy');
|
|
106
38
|
|
|
107
|
-
//
|
|
108
|
-
|
|
39
|
+
// 多选 server
|
|
40
|
+
const serverIds = Object.keys(globalConfig.servers);
|
|
41
|
+
const selectedServers = handleCancel<string[]>(
|
|
42
|
+
await clack.multiselect({
|
|
43
|
+
message: '选择要部署到的服务器(空格切换,a 全选)',
|
|
44
|
+
options: serverIds.map((id) => ({
|
|
45
|
+
value: id,
|
|
46
|
+
label: `${id} (${globalConfig.servers[id].host})`,
|
|
47
|
+
})),
|
|
48
|
+
required: true,
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// 多选项目
|
|
53
|
+
let selectedProjects: typeof discovered;
|
|
109
54
|
|
|
110
55
|
if (discovered.length === 1) {
|
|
111
56
|
const only = discovered[0];
|
|
@@ -117,57 +62,68 @@ export async function runApp(): Promise<void> {
|
|
|
117
62
|
clack.outro('已取消');
|
|
118
63
|
return;
|
|
119
64
|
}
|
|
120
|
-
|
|
65
|
+
selectedProjects = [only];
|
|
121
66
|
} else {
|
|
122
|
-
|
|
67
|
+
selectedProjects = handleCancel<typeof discovered>(
|
|
123
68
|
await clack.multiselect({
|
|
124
69
|
message: '选择要部署的项目(空格切换,a 全选)',
|
|
125
70
|
options: discovered.map((item) => ({
|
|
126
71
|
value: item,
|
|
127
|
-
label: `${item.config.name} (${relative(cwd, item.path) || item.path})
|
|
72
|
+
label: `${item.config.name} (${relative(cwd, item.path) || item.path})`,
|
|
128
73
|
})),
|
|
129
74
|
required: true,
|
|
130
75
|
})
|
|
131
76
|
);
|
|
132
77
|
}
|
|
133
78
|
|
|
134
|
-
//
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
if (missingServers.size > 0) {
|
|
142
|
-
clack.log.error(`以下服务器未配置: ${[...missingServers].join(', ')}`);
|
|
143
|
-
clack.outro('已中止');
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
79
|
+
// 确认摘要
|
|
80
|
+
const summaryLines = [
|
|
81
|
+
`服务器: ${selectedServers.map((s) => `${s} (${globalConfig.servers[s].host})`).join(', ')}`,
|
|
82
|
+
`项目: ${selectedProjects.map((p) => p.config.name).join(', ')}`,
|
|
83
|
+
].join('\n');
|
|
84
|
+
noteBox(summaryLines, '即将部署');
|
|
146
85
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
86
|
+
const confirmed = handleCancel<boolean>(
|
|
87
|
+
await clack.confirm({ message: '确认开始部署?', initialValue: true })
|
|
88
|
+
);
|
|
89
|
+
if (!confirmed) {
|
|
90
|
+
clack.outro('已取消');
|
|
91
|
+
return;
|
|
153
92
|
}
|
|
154
93
|
|
|
155
|
-
//
|
|
156
|
-
|
|
94
|
+
// 按 server 分组遍历(笛卡尔:每个 server 部署全部选中项目)
|
|
95
|
+
let anyFailure = false;
|
|
96
|
+
for (const serverId of selectedServers) {
|
|
157
97
|
const serverConfig = globalConfig.servers[serverId];
|
|
158
98
|
const spinner = clack.spinner();
|
|
159
|
-
spinner.start(`连接 ${serverId}...`);
|
|
160
|
-
|
|
161
|
-
|
|
99
|
+
spinner.start(`连接 ${serverId} (${serverConfig.host})...`);
|
|
100
|
+
let sftp;
|
|
101
|
+
try {
|
|
102
|
+
sftp = await connectSftp('deploy', serverConfig);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
spinner.stop(`连接失败: ${serverId}`);
|
|
105
|
+
clack.log.error(`${serverId} 连接失败: ${err}`);
|
|
106
|
+
anyFailure = true;
|
|
107
|
+
const cont = handleCancel<boolean>(
|
|
108
|
+
await clack.confirm({ message: `${serverId} 不可用,是否跳过并继续下一个服务器?` })
|
|
109
|
+
);
|
|
110
|
+
if (!cont) {
|
|
111
|
+
clack.outro('已中止');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
spinner.stop(`已连接 ${serverId}`);
|
|
162
117
|
|
|
163
118
|
try {
|
|
164
|
-
for (const { config } of
|
|
119
|
+
for (const { config } of selectedProjects) {
|
|
165
120
|
clack.log.step(`开始部署 ${pc.cyan(config.name)} → ${pc.cyan(serverId)}`);
|
|
166
121
|
try {
|
|
167
122
|
await runSteps(config.steps, config, sftp);
|
|
168
|
-
clack.log.success(`${config.name}
|
|
123
|
+
clack.log.success(`${config.name} → ${serverId} 完成`);
|
|
169
124
|
} catch (err) {
|
|
170
|
-
|
|
125
|
+
anyFailure = true;
|
|
126
|
+
clack.log.error(`${config.name} → ${serverId} 失败: ${err}`);
|
|
171
127
|
const cont = handleCancel<boolean>(
|
|
172
128
|
await clack.confirm({ message: '是否继续部署下一个项目?' })
|
|
173
129
|
);
|
|
@@ -179,5 +135,5 @@ export async function runApp(): Promise<void> {
|
|
|
179
135
|
}
|
|
180
136
|
}
|
|
181
137
|
|
|
182
|
-
clack.outro('全部完成');
|
|
138
|
+
clack.outro(anyFailure ? '完成(存在失败项)' : '全部完成');
|
|
183
139
|
}
|
package/src/config/schema.ts
CHANGED
package/src/init/app.ts
CHANGED
|
@@ -50,6 +50,11 @@ const VARS_REFERENCE = [
|
|
|
50
50
|
' ${localDir} 项目本地目录',
|
|
51
51
|
' ${remoteDir} 项目远程目录',
|
|
52
52
|
' ${projectName} 项目名称',
|
|
53
|
+
' ${date} 日期 (YYYY-MM-DD)',
|
|
54
|
+
' ${datetime} 日期时间 (YYYY-MM-DD_HH-mm-ss)',
|
|
55
|
+
' ${timestamp} Unix 毫秒时间戳',
|
|
56
|
+
'',
|
|
57
|
+
' 可用 ${name:格式} 自定义,如 ${datetime:YYYYMMDD-HHmmss}',
|
|
53
58
|
].join('\n');
|
|
54
59
|
|
|
55
60
|
/** 步骤类型示例 */
|
|
@@ -94,15 +99,15 @@ const STEP_EXAMPLES: Record<StepType, string> = {
|
|
|
94
99
|
VARS_REFERENCE,
|
|
95
100
|
'',
|
|
96
101
|
'示例:',
|
|
97
|
-
' 远端: ${remoteDir}
|
|
98
|
-
' 本地: ${localDir}/
|
|
102
|
+
' 远端: ${remoteDir}',
|
|
103
|
+
' 本地: ${localDir}/backup_${datetime}',
|
|
99
104
|
].join('\n'),
|
|
100
105
|
downloadFile: [
|
|
101
106
|
VARS_REFERENCE,
|
|
102
107
|
'',
|
|
103
108
|
'示例:',
|
|
104
109
|
' 远端: ${remoteDir}config.json',
|
|
105
|
-
' 本地: ${localDir}/config.json.
|
|
110
|
+
' 本地: ${localDir}/config.json.bak_${date}',
|
|
106
111
|
].join('\n'),
|
|
107
112
|
};
|
|
108
113
|
|
|
@@ -384,7 +389,6 @@ export async function runInit(targetPath?: string): Promise<void> {
|
|
|
384
389
|
const projectConfig: ProjectConfig = {
|
|
385
390
|
name,
|
|
386
391
|
localDir,
|
|
387
|
-
server: serverId,
|
|
388
392
|
remoteDir,
|
|
389
393
|
steps,
|
|
390
394
|
};
|
package/src/script/parser.ts
CHANGED
|
@@ -1,22 +1,62 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
1
2
|
import type { ProjectConfig } from '../config/schema.js';
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/** 内置变量名(含日期时间变量) */
|
|
5
|
+
const BUILT_IN_VARS = ['localDir', 'remoteDir', 'projectName', 'date', 'datetime', 'timestamp'] as const;
|
|
6
|
+
|
|
7
|
+
/** 日期变量预设及其默认格式(timestamp 固定为毫秒,无 format) */
|
|
8
|
+
const DATE_VAR_DEFAULTS: Record<string, string | null> = {
|
|
9
|
+
date: 'YYYY-MM-DD',
|
|
10
|
+
datetime: 'YYYY-MM-DD_HH-mm-ss',
|
|
11
|
+
timestamp: null, // 特殊:输出毫秒
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** 变量解析上下文(运行时动态变量) */
|
|
15
|
+
export interface ResolveContext {
|
|
16
|
+
/** 时间锚点;缺省取当前时间。同一次部署应传同一个 Date 保证一致性 */
|
|
17
|
+
now?: Date;
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
/**
|
|
9
|
-
* 替换字符串中的 ${varName} 占位符
|
|
21
|
+
* 替换字符串中的 ${varName} 或 ${name:format} 占位符
|
|
22
|
+
* - 静态变量:${localDir}、${remoteDir}、${projectName}
|
|
23
|
+
* - 日期变量:${date}、${datetime}、${timestamp},可加 :format 自定义格式,如 ${datetime:YYYYMMDD-HHmmss}
|
|
24
|
+
* - 未知变量:原样保留(向后兼容)
|
|
10
25
|
*/
|
|
11
|
-
export function resolveVariables(
|
|
12
|
-
|
|
26
|
+
export function resolveVariables(
|
|
27
|
+
template: string,
|
|
28
|
+
project: ProjectConfig,
|
|
29
|
+
ctx: ResolveContext = {},
|
|
30
|
+
): string {
|
|
31
|
+
const staticVars: Record<string, string> = {
|
|
13
32
|
localDir: project.localDir,
|
|
14
33
|
remoteDir: project.remoteDir,
|
|
15
34
|
projectName: project.name,
|
|
16
35
|
};
|
|
36
|
+
const now = ctx.now ?? new Date();
|
|
37
|
+
|
|
38
|
+
return template.replace(/\$\{([^}]+)\}/g, (match, expr: string) => {
|
|
39
|
+
// 按第一个冒号分割 name 和 format(format 内部可能含冒号,如 HH:mm:ss)
|
|
40
|
+
const colon = expr.indexOf(':');
|
|
41
|
+
const name = colon === -1 ? expr : expr.slice(0, colon);
|
|
42
|
+
const format = colon === -1 ? '' : expr.slice(colon + 1);
|
|
43
|
+
|
|
44
|
+
// 1. 静态变量优先
|
|
45
|
+
if (staticVars[name] !== undefined) {
|
|
46
|
+
return staticVars[name];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. 日期变量
|
|
50
|
+
if (name in DATE_VAR_DEFAULTS) {
|
|
51
|
+
if (name === 'timestamp') {
|
|
52
|
+
return String(dayjs(now).valueOf());
|
|
53
|
+
}
|
|
54
|
+
// date/datetime 用默认格式或自定义格式
|
|
55
|
+
return dayjs(now).format(format || (DATE_VAR_DEFAULTS[name] as string));
|
|
56
|
+
}
|
|
17
57
|
|
|
18
|
-
|
|
19
|
-
return
|
|
58
|
+
// 3. 未知变量原样保留(向后兼容)
|
|
59
|
+
return match;
|
|
20
60
|
});
|
|
21
61
|
}
|
|
22
62
|
|
package/src/script/runner.ts
CHANGED
|
@@ -38,27 +38,27 @@ function runLocal(cmd: string, cwd?: string): Promise<void> {
|
|
|
38
38
|
/**
|
|
39
39
|
* 执行单个步骤
|
|
40
40
|
*/
|
|
41
|
-
async function runStep(step: Step, project: ProjectConfig, sftp: SftpClient): Promise<void> {
|
|
41
|
+
async function runStep(step: Step, project: ProjectConfig, sftp: SftpClient, now: Date): Promise<void> {
|
|
42
42
|
switch (step.type) {
|
|
43
43
|
case 'log': {
|
|
44
|
-
const msg = resolveVariables(step.message, project);
|
|
44
|
+
const msg = resolveVariables(step.message, project, { now });
|
|
45
45
|
clack.log.message(msg);
|
|
46
46
|
break;
|
|
47
47
|
}
|
|
48
48
|
case 'local': {
|
|
49
|
-
const cmd = resolveVariables(step.cmd, project);
|
|
50
|
-
const cwd = step.cwd ? resolveVariables(step.cwd, project) : undefined;
|
|
49
|
+
const cmd = resolveVariables(step.cmd, project, { now });
|
|
50
|
+
const cwd = step.cwd ? resolveVariables(step.cwd, project, { now }) : undefined;
|
|
51
51
|
await runLocal(cmd, cwd);
|
|
52
52
|
break;
|
|
53
53
|
}
|
|
54
54
|
case 'remote': {
|
|
55
|
-
const cmd = resolveVariables(step.cmd, project);
|
|
55
|
+
const cmd = resolveVariables(step.cmd, project, { now });
|
|
56
56
|
await execRemote(sftp, cmd);
|
|
57
57
|
break;
|
|
58
58
|
}
|
|
59
59
|
case 'uploadDir': {
|
|
60
|
-
const local = resolveVariables(step.local, project);
|
|
61
|
-
const remote = resolveVariables(step.remote, project);
|
|
60
|
+
const local = resolveVariables(step.local, project, { now });
|
|
61
|
+
const remote = resolveVariables(step.remote, project, { now });
|
|
62
62
|
clack.log.step(`上传目录: ${local} → ${remote}`);
|
|
63
63
|
await sftp.mkdir(remote, true);
|
|
64
64
|
await sftp.uploadDir(local, remote, { useFastput: true });
|
|
@@ -66,24 +66,24 @@ async function runStep(step: Step, project: ProjectConfig, sftp: SftpClient): Pr
|
|
|
66
66
|
break;
|
|
67
67
|
}
|
|
68
68
|
case 'uploadFile': {
|
|
69
|
-
const local = resolveVariables(step.local, project);
|
|
70
|
-
const remote = resolveVariables(step.remote, project);
|
|
69
|
+
const local = resolveVariables(step.local, project, { now });
|
|
70
|
+
const remote = resolveVariables(step.remote, project, { now });
|
|
71
71
|
clack.log.step(`上传文件: ${local} → ${remote}`);
|
|
72
72
|
await sftp.fastPut(local, remote);
|
|
73
73
|
clack.log.success('上传文件完成');
|
|
74
74
|
break;
|
|
75
75
|
}
|
|
76
76
|
case 'downloadDir': {
|
|
77
|
-
const remote = resolveVariables(step.remote, project);
|
|
78
|
-
const local = resolveVariables(step.local, project);
|
|
77
|
+
const remote = resolveVariables(step.remote, project, { now });
|
|
78
|
+
const local = resolveVariables(step.local, project, { now });
|
|
79
79
|
clack.log.step(`下载目录: ${remote} → ${local}`);
|
|
80
80
|
await sftp.downloadDir(remote, local);
|
|
81
81
|
clack.log.success('下载目录完成');
|
|
82
82
|
break;
|
|
83
83
|
}
|
|
84
84
|
case 'downloadFile': {
|
|
85
|
-
const remote = resolveVariables(step.remote, project);
|
|
86
|
-
const local = resolveVariables(step.local, project);
|
|
85
|
+
const remote = resolveVariables(step.remote, project, { now });
|
|
86
|
+
const local = resolveVariables(step.local, project, { now });
|
|
87
87
|
clack.log.step(`下载文件: ${remote} → ${local}`);
|
|
88
88
|
await sftp.fastGet(remote, local);
|
|
89
89
|
clack.log.success('下载文件完成');
|
|
@@ -96,7 +96,8 @@ async function runStep(step: Step, project: ProjectConfig, sftp: SftpClient): Pr
|
|
|
96
96
|
* 执行项目的所有步骤
|
|
97
97
|
*/
|
|
98
98
|
export async function runSteps(steps: Step[], project: ProjectConfig, sftp: SftpClient): Promise<void> {
|
|
99
|
+
const now = new Date(); // 整个项目部署共用同一时间戳
|
|
99
100
|
for (const step of steps) {
|
|
100
|
-
await runStep(step, project, sftp);
|
|
101
|
+
await runStep(step, project, sftp, now);
|
|
101
102
|
}
|
|
102
103
|
}
|