@mindbase/deploy 1.2.1 → 1.2.3
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 +1 -1
- package/src/script/runner.ts +102 -26
- package/src/ssh/client.ts +1 -0
- package/src/ssh/executor.ts +31 -14
- package/src/log.ts +0 -31
package/package.json
CHANGED
package/src/script/runner.ts
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import type SftpClient from 'ssh2-sftp-client';
|
|
2
2
|
import spawn from 'cross-spawn';
|
|
3
3
|
import * as clack from '@clack/prompts';
|
|
4
|
+
import { noteBox } from '@mindbase/cli-ui';
|
|
4
5
|
import type { Step, ProjectConfig } from '../config/schema.js';
|
|
5
6
|
import { resolveVariables } from './parser.js';
|
|
6
|
-
import { execRemote } from '../ssh/executor.js';
|
|
7
|
-
|
|
7
|
+
import { execRemote, ExecError } from '../ssh/executor.js';
|
|
8
|
+
|
|
9
|
+
/** 本地命令执行错误(同 ExecError 结构,便于上层统一处理) */
|
|
10
|
+
class LocalExecError extends Error {
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
code: number | null;
|
|
14
|
+
constructor(
|
|
15
|
+
message: string,
|
|
16
|
+
opts: { stdout: string; stderr: string; code: number | null },
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'LocalExecError';
|
|
20
|
+
this.stdout = opts.stdout;
|
|
21
|
+
this.stderr = opts.stderr;
|
|
22
|
+
this.code = opts.code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 失败时若有 captured 输出,用 noteBox 框出,便于定位;无输出则不展示 */
|
|
27
|
+
function showCommandOutput(err: unknown): void {
|
|
28
|
+
const e = err as { stdout?: string; stderr?: string };
|
|
29
|
+
const output = [e?.stdout, e?.stderr].filter(Boolean).join('\n').trim();
|
|
30
|
+
if (output) noteBox(output, '命令输出');
|
|
31
|
+
}
|
|
8
32
|
|
|
9
33
|
/**
|
|
10
|
-
*
|
|
34
|
+
* 在本地执行命令(静默累积输出,失败时通过 LocalExecError 透出)
|
|
11
35
|
*/
|
|
12
36
|
function runLocal(cmd: string, cwd?: string): Promise<void> {
|
|
13
37
|
return new Promise((resolve, reject) => {
|
|
@@ -16,21 +40,31 @@ function runLocal(cmd: string, cwd?: string): Promise<void> {
|
|
|
16
40
|
detached: false,
|
|
17
41
|
shell: true,
|
|
18
42
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (cp.stdout) cp.stdout.on('data', (data: Buffer) =>
|
|
22
|
-
if (cp.stderr) cp.stderr.on('data', (data: Buffer) =>
|
|
43
|
+
let outStr = '';
|
|
44
|
+
let errStr = '';
|
|
45
|
+
if (cp.stdout) cp.stdout.on('data', (data: Buffer) => { outStr += data.toString(); });
|
|
46
|
+
if (cp.stderr) cp.stderr.on('data', (data: Buffer) => { errStr += data.toString(); });
|
|
23
47
|
cp.on('close', (code) => {
|
|
24
|
-
outSink.flush();
|
|
25
|
-
errSink.flush();
|
|
26
48
|
if (code === 0) {
|
|
27
49
|
resolve();
|
|
28
50
|
} else {
|
|
29
|
-
reject(
|
|
51
|
+
reject(
|
|
52
|
+
new LocalExecError(`本地命令执行失败 (code=${code}): ${cmd}`, {
|
|
53
|
+
stdout: outStr,
|
|
54
|
+
stderr: errStr,
|
|
55
|
+
code,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
30
58
|
}
|
|
31
59
|
});
|
|
32
60
|
cp.on('error', (err) => {
|
|
33
|
-
reject(
|
|
61
|
+
reject(
|
|
62
|
+
new LocalExecError(String(err), {
|
|
63
|
+
stdout: outStr,
|
|
64
|
+
stderr: errStr,
|
|
65
|
+
code: null,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
34
68
|
});
|
|
35
69
|
});
|
|
36
70
|
}
|
|
@@ -48,45 +82,87 @@ async function runStep(step: Step, project: ProjectConfig, sftp: SftpClient, now
|
|
|
48
82
|
case 'local': {
|
|
49
83
|
const cmd = resolveVariables(step.cmd, project, { now });
|
|
50
84
|
const cwd = step.cwd ? resolveVariables(step.cwd, project, { now }) : undefined;
|
|
51
|
-
|
|
85
|
+
const spinner = clack.spinner();
|
|
86
|
+
spinner.start(`本地: ${cmd}`);
|
|
87
|
+
try {
|
|
88
|
+
await runLocal(cmd, cwd);
|
|
89
|
+
spinner.stop(`✓ ${cmd}`);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
spinner.stop(`✗ ${cmd}`);
|
|
92
|
+
showCommandOutput(err);
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
52
95
|
break;
|
|
53
96
|
}
|
|
54
97
|
case 'remote': {
|
|
55
98
|
const cmd = resolveVariables(step.cmd, project, { now });
|
|
56
|
-
|
|
99
|
+
const spinner = clack.spinner();
|
|
100
|
+
spinner.start(`远程: ${cmd}`);
|
|
101
|
+
try {
|
|
102
|
+
await execRemote(sftp, cmd);
|
|
103
|
+
spinner.stop(`✓ ${cmd}`);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
spinner.stop(`✗ ${cmd}`);
|
|
106
|
+
showCommandOutput(err);
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
57
109
|
break;
|
|
58
110
|
}
|
|
59
111
|
case 'uploadDir': {
|
|
60
112
|
const local = resolveVariables(step.local, project, { now });
|
|
61
113
|
const remote = resolveVariables(step.remote, project, { now });
|
|
62
|
-
clack.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
114
|
+
const spinner = clack.spinner();
|
|
115
|
+
spinner.start(`上传目录: ${local} → ${remote}`);
|
|
116
|
+
try {
|
|
117
|
+
await sftp.mkdir(remote, true);
|
|
118
|
+
await sftp.uploadDir(local, remote, { useFastput: true });
|
|
119
|
+
spinner.stop(`✓ ${local} → ${remote}`);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
spinner.stop(`✗ ${local} → ${remote}`);
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
66
124
|
break;
|
|
67
125
|
}
|
|
68
126
|
case 'uploadFile': {
|
|
69
127
|
const local = resolveVariables(step.local, project, { now });
|
|
70
128
|
const remote = resolveVariables(step.remote, project, { now });
|
|
71
|
-
clack.
|
|
72
|
-
|
|
73
|
-
|
|
129
|
+
const spinner = clack.spinner();
|
|
130
|
+
spinner.start(`上传文件: ${local} → ${remote}`);
|
|
131
|
+
try {
|
|
132
|
+
await sftp.fastPut(local, remote);
|
|
133
|
+
spinner.stop(`✓ ${local} → ${remote}`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
spinner.stop(`✗ ${local} → ${remote}`);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
74
138
|
break;
|
|
75
139
|
}
|
|
76
140
|
case 'downloadDir': {
|
|
77
141
|
const remote = resolveVariables(step.remote, project, { now });
|
|
78
142
|
const local = resolveVariables(step.local, project, { now });
|
|
79
|
-
clack.
|
|
80
|
-
|
|
81
|
-
|
|
143
|
+
const spinner = clack.spinner();
|
|
144
|
+
spinner.start(`下载目录: ${remote} → ${local}`);
|
|
145
|
+
try {
|
|
146
|
+
await sftp.downloadDir(remote, local);
|
|
147
|
+
spinner.stop(`✓ ${remote} → ${local}`);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
spinner.stop(`✗ ${remote} → ${local}`);
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
82
152
|
break;
|
|
83
153
|
}
|
|
84
154
|
case 'downloadFile': {
|
|
85
155
|
const remote = resolveVariables(step.remote, project, { now });
|
|
86
156
|
const local = resolveVariables(step.local, project, { now });
|
|
87
|
-
clack.
|
|
88
|
-
|
|
89
|
-
|
|
157
|
+
const spinner = clack.spinner();
|
|
158
|
+
spinner.start(`下载文件: ${remote} → ${local}`);
|
|
159
|
+
try {
|
|
160
|
+
await sftp.fastGet(remote, local);
|
|
161
|
+
spinner.stop(`✓ ${remote} → ${local}`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
spinner.stop(`✗ ${remote} → ${local}`);
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
90
166
|
break;
|
|
91
167
|
}
|
|
92
168
|
}
|
package/src/ssh/client.ts
CHANGED
|
@@ -42,6 +42,7 @@ function buildConnectConfig(config: ServerConfig): Record<string, unknown> {
|
|
|
42
42
|
*/
|
|
43
43
|
export async function connectSftp(name: string, config: ServerConfig): Promise<SftpClient> {
|
|
44
44
|
const sftp = new SftpClient(name);
|
|
45
|
+
sftp.client.setMaxListeners(50);
|
|
45
46
|
await sftp.connect(buildConnectConfig(config));
|
|
46
47
|
return sftp;
|
|
47
48
|
}
|
package/src/ssh/executor.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import type SftpClient from 'ssh2-sftp-client';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
/** 命令执行错误(携带完整 stdout/stderr,供上层 noteBox 展示) */
|
|
4
|
+
export class ExecError extends Error {
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
code: number | null;
|
|
8
|
+
constructor(
|
|
9
|
+
message: string,
|
|
10
|
+
opts: { stdout: string; stderr: string; code: number | null },
|
|
11
|
+
) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'ExecError';
|
|
14
|
+
this.stdout = opts.stdout;
|
|
15
|
+
this.stderr = opts.stderr;
|
|
16
|
+
this.code = opts.code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
3
19
|
|
|
4
20
|
/**
|
|
5
21
|
* 在远程服务器执行命令
|
|
22
|
+
*
|
|
23
|
+
* 静默累积输出,不即时打印;失败时通过 ExecError 透出完整输出,
|
|
24
|
+
* 由上层决定如何展示(避免成功时刷屏)。
|
|
6
25
|
*/
|
|
7
26
|
export async function execRemote(sftp: SftpClient, command: string): Promise<string> {
|
|
8
27
|
// 加载常见 shell 初始化文件与 Node 版本管理器,使远程 PATH 与登录会话一致
|
|
@@ -25,37 +44,35 @@ export async function execRemote(sftp: SftpClient, command: string): Promise<str
|
|
|
25
44
|
// @ts-ignore: ssh2-sftp-client types don't expose client but it exists at runtime
|
|
26
45
|
const client = (sftp as any).client;
|
|
27
46
|
if (!client) {
|
|
28
|
-
reject(new
|
|
47
|
+
reject(new ExecError('SSH 客户端未连接', { stdout: '', stderr: '', code: null }));
|
|
29
48
|
return;
|
|
30
49
|
}
|
|
31
50
|
client.exec(fullCommand, (err: Error | undefined, stream: any) => {
|
|
32
51
|
if (err) {
|
|
33
|
-
reject(err);
|
|
52
|
+
reject(new ExecError(String(err), { stdout: '', stderr: '', code: null }));
|
|
34
53
|
return;
|
|
35
54
|
}
|
|
36
55
|
let outStr = '';
|
|
37
56
|
let errStr = '';
|
|
38
|
-
const outSink = createLineSink('stdout');
|
|
39
|
-
const errSink = createLineSink('stderr');
|
|
40
57
|
stream
|
|
41
58
|
.on('close', (code: number, signal: string) => {
|
|
42
|
-
outSink.flush();
|
|
43
|
-
errSink.flush();
|
|
44
59
|
if (code !== 0) {
|
|
45
|
-
reject(
|
|
60
|
+
reject(
|
|
61
|
+
new ExecError(`远程命令执行失败 (code=${code})`, {
|
|
62
|
+
stdout: outStr,
|
|
63
|
+
stderr: errStr || signal || '',
|
|
64
|
+
code,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
46
67
|
return;
|
|
47
68
|
}
|
|
48
69
|
resolve(outStr);
|
|
49
70
|
})
|
|
50
71
|
.on('data', (data: Buffer) => {
|
|
51
|
-
|
|
52
|
-
outStr += str;
|
|
53
|
-
outSink.push(str);
|
|
72
|
+
outStr += data.toString();
|
|
54
73
|
})
|
|
55
74
|
.stderr.on('data', (data: Buffer) => {
|
|
56
|
-
|
|
57
|
-
errStr += str;
|
|
58
|
-
errSink.push(str);
|
|
75
|
+
errStr += data.toString();
|
|
59
76
|
});
|
|
60
77
|
});
|
|
61
78
|
});
|
package/src/log.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
}
|