@mindbase/deploy 1.2.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindbase/deploy",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "MindBase 部署工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- import { createLineSink } from '../log.js';
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
- 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()));
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(new Error(`本地命令执行失败 (code=${code}): ${cmd}`));
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(err);
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
- await runLocal(cmd, cwd);
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
- await execRemote(sftp, cmd);
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.log.step(`上传目录: ${local} → ${remote}`);
63
- await sftp.mkdir(remote, true);
64
- await sftp.uploadDir(local, remote, { useFastput: true });
65
- clack.log.success('上传目录完成');
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.log.step(`上传文件: ${local} → ${remote}`);
72
- await sftp.fastPut(local, remote);
73
- clack.log.success('上传文件完成');
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.log.step(`下载目录: ${remote} → ${local}`);
80
- await sftp.downloadDir(remote, local);
81
- clack.log.success('下载目录完成');
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.log.step(`下载文件: ${remote} → ${local}`);
88
- await sftp.fastGet(remote, local);
89
- clack.log.success('下载文件完成');
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
  }
@@ -1,8 +1,27 @@
1
1
  import type SftpClient from 'ssh2-sftp-client';
2
- import { createLineSink } from '../log.js';
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 Error('SSH 客户端未连接'));
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(new Error(`远程命令执行失败 (code=${code}): ${errStr || signal}`));
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
- const str = data.toString();
52
- outStr += str;
53
- outSink.push(str);
72
+ outStr += data.toString();
54
73
  })
55
74
  .stderr.on('data', (data: Buffer) => {
56
- const str = data.toString();
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
- }