@mindbase/deploy 1.1.2 → 1.1.5
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/log.ts +31 -0
- package/src/script/runner.ts +17 -3
- package/src/ssh/executor.ts +23 -4
package/package.json
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,11 +1,26 @@
|
|
|
1
1
|
import type SftpClient from 'ssh2-sftp-client';
|
|
2
|
+
import { createLineSink } from '../log.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 在远程服务器执行命令
|
|
5
6
|
*/
|
|
6
7
|
export async function execRemote(sftp: SftpClient, command: string): Promise<string> {
|
|
7
|
-
//
|
|
8
|
-
const fullCommand =
|
|
8
|
+
// 加载常见 shell 初始化文件与 Node 版本管理器,使远程 PATH 与登录会话一致
|
|
9
|
+
const fullCommand = [
|
|
10
|
+
'[ -f /etc/profile ] && source /etc/profile',
|
|
11
|
+
'[ -f ~/.bash_profile ] && source ~/.bash_profile',
|
|
12
|
+
'[ -f ~/.bash_login ] && source ~/.bash_login',
|
|
13
|
+
'[ -f ~/.profile ] && source ~/.profile',
|
|
14
|
+
// bashrc 顶部常见 `[ -z "$PS1" ] && return` 防御代码会让非交互 shell 直接退出,
|
|
15
|
+
// 注入占位 PS1 让判断失效,bashrc 内部随后会覆盖它
|
|
16
|
+
'[ -f ~/.bashrc ] && { PS1="${PS1:-deploy}"; source ~/.bashrc; }',
|
|
17
|
+
'[ -n "$NVM_DIR" ] && [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"',
|
|
18
|
+
'[ -s "$HOME/.nvm/nvm.sh" ] && source "$HOME/.nvm/nvm.sh"',
|
|
19
|
+
'[ -s "$HOME/.fnm/env.sh" ] && source "$HOME/.fnm/env.sh"',
|
|
20
|
+
'[ -d "$HOME/.local/bin" ] && export PATH="$HOME/.local/bin:$PATH"',
|
|
21
|
+
'[ -d "$HOME/bin" ] && export PATH="$HOME/bin:$PATH"',
|
|
22
|
+
command,
|
|
23
|
+
].join('; ');
|
|
9
24
|
return new Promise((resolve, reject) => {
|
|
10
25
|
// @ts-ignore: ssh2-sftp-client types don't expose client but it exists at runtime
|
|
11
26
|
const client = (sftp as any).client;
|
|
@@ -20,8 +35,12 @@ export async function execRemote(sftp: SftpClient, command: string): Promise<str
|
|
|
20
35
|
}
|
|
21
36
|
let outStr = '';
|
|
22
37
|
let errStr = '';
|
|
38
|
+
const outSink = createLineSink('stdout');
|
|
39
|
+
const errSink = createLineSink('stderr');
|
|
23
40
|
stream
|
|
24
41
|
.on('close', (code: number, signal: string) => {
|
|
42
|
+
outSink.flush();
|
|
43
|
+
errSink.flush();
|
|
25
44
|
if (code !== 0) {
|
|
26
45
|
reject(new Error(`远程命令执行失败 (code=${code}): ${errStr || signal}`));
|
|
27
46
|
return;
|
|
@@ -31,12 +50,12 @@ export async function execRemote(sftp: SftpClient, command: string): Promise<str
|
|
|
31
50
|
.on('data', (data: Buffer) => {
|
|
32
51
|
const str = data.toString();
|
|
33
52
|
outStr += str;
|
|
34
|
-
|
|
53
|
+
outSink.push(str);
|
|
35
54
|
})
|
|
36
55
|
.stderr.on('data', (data: Buffer) => {
|
|
37
56
|
const str = data.toString();
|
|
38
57
|
errStr += str;
|
|
39
|
-
|
|
58
|
+
errSink.push(str);
|
|
40
59
|
});
|
|
41
60
|
});
|
|
42
61
|
});
|