@mindbase/deploy 1.1.3 → 1.2.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/bin/shared.ts +1 -24
- package/package.json +1 -1
- package/src/app.ts +60 -103
- package/src/config/schema.ts +0 -2
- package/src/init/app.ts +0 -1
- package/src/log.ts +31 -0
- package/src/script/runner.ts +17 -3
- package/src/ssh/executor.ts +10 -3
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
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,69 @@ 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
|
+
`组合: ${selectedServers.length} × ${selectedProjects.length} = ${selectedServers.length * selectedProjects.length} 次部署`,
|
|
84
|
+
].join('\n');
|
|
85
|
+
noteBox(summaryLines, '即将部署');
|
|
146
86
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
87
|
+
const confirmed = handleCancel<boolean>(
|
|
88
|
+
await clack.confirm({ message: '确认开始部署?', initialValue: true })
|
|
89
|
+
);
|
|
90
|
+
if (!confirmed) {
|
|
91
|
+
clack.outro('已取消');
|
|
92
|
+
return;
|
|
153
93
|
}
|
|
154
94
|
|
|
155
|
-
//
|
|
156
|
-
|
|
95
|
+
// 按 server 分组遍历(笛卡尔:每个 server 部署全部选中项目)
|
|
96
|
+
let anyFailure = false;
|
|
97
|
+
for (const serverId of selectedServers) {
|
|
157
98
|
const serverConfig = globalConfig.servers[serverId];
|
|
158
99
|
const spinner = clack.spinner();
|
|
159
|
-
spinner.start(`连接 ${serverId}...`);
|
|
160
|
-
|
|
161
|
-
|
|
100
|
+
spinner.start(`连接 ${serverId} (${serverConfig.host})...`);
|
|
101
|
+
let sftp;
|
|
102
|
+
try {
|
|
103
|
+
sftp = await connectSftp('deploy', serverConfig);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
spinner.stop(`连接失败: ${serverId}`);
|
|
106
|
+
clack.log.error(`${serverId} 连接失败: ${err}`);
|
|
107
|
+
anyFailure = true;
|
|
108
|
+
const cont = handleCancel<boolean>(
|
|
109
|
+
await clack.confirm({ message: `${serverId} 不可用,是否跳过并继续下一个服务器?` })
|
|
110
|
+
);
|
|
111
|
+
if (!cont) {
|
|
112
|
+
clack.outro('已中止');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
spinner.stop(`已连接 ${serverId}`);
|
|
162
118
|
|
|
163
119
|
try {
|
|
164
|
-
for (const { config } of
|
|
120
|
+
for (const { config } of selectedProjects) {
|
|
165
121
|
clack.log.step(`开始部署 ${pc.cyan(config.name)} → ${pc.cyan(serverId)}`);
|
|
166
122
|
try {
|
|
167
123
|
await runSteps(config.steps, config, sftp);
|
|
168
|
-
clack.log.success(`${config.name}
|
|
124
|
+
clack.log.success(`${config.name} → ${serverId} 完成`);
|
|
169
125
|
} catch (err) {
|
|
170
|
-
|
|
126
|
+
anyFailure = true;
|
|
127
|
+
clack.log.error(`${config.name} → ${serverId} 失败: ${err}`);
|
|
171
128
|
const cont = handleCancel<boolean>(
|
|
172
129
|
await clack.confirm({ message: '是否继续部署下一个项目?' })
|
|
173
130
|
);
|
|
@@ -179,5 +136,5 @@ export async function runApp(): Promise<void> {
|
|
|
179
136
|
}
|
|
180
137
|
}
|
|
181
138
|
|
|
182
|
-
clack.outro('全部完成');
|
|
139
|
+
clack.outro(anyFailure ? '完成(存在失败项)' : '全部完成');
|
|
183
140
|
}
|
package/src/config/schema.ts
CHANGED
package/src/init/app.ts
CHANGED
package/src/log.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as clack from '@clack/prompts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 创建按行缓冲、用 clack 日志输出的 sink。
|
|
5
|
+
* - stdout 行用 clack.log.message(中性竖线)
|
|
6
|
+
* - stderr 行用 clack.log.warn(警告符号,与 stdout 区分)
|
|
7
|
+
*
|
|
8
|
+
* 不完整行(未遇换行)暂存在 buffer,flush 时统一输出。
|
|
9
|
+
*/
|
|
10
|
+
export function createLineSink(channel: 'stdout' | 'stderr') {
|
|
11
|
+
let buffer = '';
|
|
12
|
+
const emit = (line: string) => {
|
|
13
|
+
if (!line) return; // 跳过空行,避免每行前缀带来的噪音
|
|
14
|
+
if (channel === 'stderr') clack.log.warn(line);
|
|
15
|
+
else clack.log.message(line);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
push(chunk: string) {
|
|
19
|
+
buffer += chunk;
|
|
20
|
+
const lines = buffer.split(/\r?\n/);
|
|
21
|
+
buffer = lines.pop() ?? ''; // 末尾可能是不完整行
|
|
22
|
+
for (const line of lines) emit(line);
|
|
23
|
+
},
|
|
24
|
+
flush() {
|
|
25
|
+
if (buffer) {
|
|
26
|
+
emit(buffer);
|
|
27
|
+
buffer = '';
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/script/runner.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type SftpClient from 'ssh2-sftp-client';
|
|
2
2
|
import spawn from 'cross-spawn';
|
|
3
|
+
import * as clack from '@clack/prompts';
|
|
3
4
|
import type { Step, ProjectConfig } from '../config/schema.js';
|
|
4
5
|
import { resolveVariables } from './parser.js';
|
|
5
6
|
import { execRemote } from '../ssh/executor.js';
|
|
7
|
+
import { createLineSink } from '../log.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* 在本地执行命令
|
|
@@ -14,9 +16,13 @@ function runLocal(cmd: string, cwd?: string): Promise<void> {
|
|
|
14
16
|
detached: false,
|
|
15
17
|
shell: true,
|
|
16
18
|
});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
const outSink = createLineSink('stdout');
|
|
20
|
+
const errSink = createLineSink('stderr');
|
|
21
|
+
if (cp.stdout) cp.stdout.on('data', (data: Buffer) => outSink.push(data.toString()));
|
|
22
|
+
if (cp.stderr) cp.stderr.on('data', (data: Buffer) => errSink.push(data.toString()));
|
|
19
23
|
cp.on('close', (code) => {
|
|
24
|
+
outSink.flush();
|
|
25
|
+
errSink.flush();
|
|
20
26
|
if (code === 0) {
|
|
21
27
|
resolve();
|
|
22
28
|
} else {
|
|
@@ -36,7 +42,7 @@ async function runStep(step: Step, project: ProjectConfig, sftp: SftpClient): Pr
|
|
|
36
42
|
switch (step.type) {
|
|
37
43
|
case 'log': {
|
|
38
44
|
const msg = resolveVariables(step.message, project);
|
|
39
|
-
|
|
45
|
+
clack.log.message(msg);
|
|
40
46
|
break;
|
|
41
47
|
}
|
|
42
48
|
case 'local': {
|
|
@@ -53,26 +59,34 @@ async function runStep(step: Step, project: ProjectConfig, sftp: SftpClient): Pr
|
|
|
53
59
|
case 'uploadDir': {
|
|
54
60
|
const local = resolveVariables(step.local, project);
|
|
55
61
|
const remote = resolveVariables(step.remote, project);
|
|
62
|
+
clack.log.step(`上传目录: ${local} → ${remote}`);
|
|
56
63
|
await sftp.mkdir(remote, true);
|
|
57
64
|
await sftp.uploadDir(local, remote, { useFastput: true });
|
|
65
|
+
clack.log.success('上传目录完成');
|
|
58
66
|
break;
|
|
59
67
|
}
|
|
60
68
|
case 'uploadFile': {
|
|
61
69
|
const local = resolveVariables(step.local, project);
|
|
62
70
|
const remote = resolveVariables(step.remote, project);
|
|
71
|
+
clack.log.step(`上传文件: ${local} → ${remote}`);
|
|
63
72
|
await sftp.fastPut(local, remote);
|
|
73
|
+
clack.log.success('上传文件完成');
|
|
64
74
|
break;
|
|
65
75
|
}
|
|
66
76
|
case 'downloadDir': {
|
|
67
77
|
const remote = resolveVariables(step.remote, project);
|
|
68
78
|
const local = resolveVariables(step.local, project);
|
|
79
|
+
clack.log.step(`下载目录: ${remote} → ${local}`);
|
|
69
80
|
await sftp.downloadDir(remote, local);
|
|
81
|
+
clack.log.success('下载目录完成');
|
|
70
82
|
break;
|
|
71
83
|
}
|
|
72
84
|
case 'downloadFile': {
|
|
73
85
|
const remote = resolveVariables(step.remote, project);
|
|
74
86
|
const local = resolveVariables(step.local, project);
|
|
87
|
+
clack.log.step(`下载文件: ${remote} → ${local}`);
|
|
75
88
|
await sftp.fastGet(remote, local);
|
|
89
|
+
clack.log.success('下载文件完成');
|
|
76
90
|
break;
|
|
77
91
|
}
|
|
78
92
|
}
|
package/src/ssh/executor.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type SftpClient from 'ssh2-sftp-client';
|
|
2
|
+
import { createLineSink } from '../log.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 在远程服务器执行命令
|
|
@@ -10,7 +11,9 @@ export async function execRemote(sftp: SftpClient, command: string): Promise<str
|
|
|
10
11
|
'[ -f ~/.bash_profile ] && source ~/.bash_profile',
|
|
11
12
|
'[ -f ~/.bash_login ] && source ~/.bash_login',
|
|
12
13
|
'[ -f ~/.profile ] && source ~/.profile',
|
|
13
|
-
|
|
14
|
+
// bashrc 顶部常见 `[ -z "$PS1" ] && return` 防御代码会让非交互 shell 直接退出,
|
|
15
|
+
// 注入占位 PS1 让判断失效,bashrc 内部随后会覆盖它
|
|
16
|
+
'[ -f ~/.bashrc ] && { PS1="${PS1:-deploy}"; source ~/.bashrc; }',
|
|
14
17
|
'[ -n "$NVM_DIR" ] && [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"',
|
|
15
18
|
'[ -s "$HOME/.nvm/nvm.sh" ] && source "$HOME/.nvm/nvm.sh"',
|
|
16
19
|
'[ -s "$HOME/.fnm/env.sh" ] && source "$HOME/.fnm/env.sh"',
|
|
@@ -32,8 +35,12 @@ export async function execRemote(sftp: SftpClient, command: string): Promise<str
|
|
|
32
35
|
}
|
|
33
36
|
let outStr = '';
|
|
34
37
|
let errStr = '';
|
|
38
|
+
const outSink = createLineSink('stdout');
|
|
39
|
+
const errSink = createLineSink('stderr');
|
|
35
40
|
stream
|
|
36
41
|
.on('close', (code: number, signal: string) => {
|
|
42
|
+
outSink.flush();
|
|
43
|
+
errSink.flush();
|
|
37
44
|
if (code !== 0) {
|
|
38
45
|
reject(new Error(`远程命令执行失败 (code=${code}): ${errStr || signal}`));
|
|
39
46
|
return;
|
|
@@ -43,12 +50,12 @@ export async function execRemote(sftp: SftpClient, command: string): Promise<str
|
|
|
43
50
|
.on('data', (data: Buffer) => {
|
|
44
51
|
const str = data.toString();
|
|
45
52
|
outStr += str;
|
|
46
|
-
|
|
53
|
+
outSink.push(str);
|
|
47
54
|
})
|
|
48
55
|
.stderr.on('data', (data: Buffer) => {
|
|
49
56
|
const str = data.toString();
|
|
50
57
|
errStr += str;
|
|
51
|
-
|
|
58
|
+
errSink.push(str);
|
|
52
59
|
});
|
|
53
60
|
});
|
|
54
61
|
});
|