@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import * as clack from '@clack/prompts';
3
- import { runApp, runProject, showConfig } from '../src/app.js';
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",
3
+ "version": "1.2.0",
4
4
  "description": "MindBase 部署工具",
5
5
  "type": "module",
6
6
  "bin": {
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, getConfigPath, discoverProjectConfigs, loadProjectConfig } from './config/manager.js';
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
- let selected: { path: string; config: ProjectConfig }[];
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
- selected = [only];
65
+ selectedProjects = [only];
121
66
  } else {
122
- selected = handleCancel<typeof discovered>(
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}) → ${pc.dim(item.config.server)}`,
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
- // 校验所有选中项目的 server 都存在
135
- const missingServers = new Set<string>();
136
- for (const item of selected) {
137
- if (!globalConfig.servers[item.config.server]) {
138
- missingServers.add(item.config.server);
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
- // server 分组
148
- const groups = new Map<string, { path: string; config: ProjectConfig }[]>();
149
- for (const item of selected) {
150
- const arr = groups.get(item.config.server) ?? [];
151
- arr.push(item);
152
- groups.set(item.config.server, arr);
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
- // 依次连接每个 server,组内项目依次部署
156
- for (const [serverId, items] of groups) {
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
- const sftp = await connectSftp('deploy', serverConfig);
161
- spinner.stop('已连接');
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 items) {
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
- clack.log.error(`${config.name} 部署失败: ${err}`);
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
  }
@@ -85,8 +85,6 @@ export interface ProjectConfig {
85
85
  name: string;
86
86
  /** 本地目录(绝对路径或相对配置文件的路径,加载时解析为绝对路径) */
87
87
  localDir: string;
88
- /** 目标服务器 ID(引用全局 servers) */
89
- server: string;
90
88
  /** 远端目录 */
91
89
  remoteDir: string;
92
90
  /** 部署步骤 */
package/src/init/app.ts CHANGED
@@ -384,7 +384,6 @@ export async function runInit(targetPath?: string): Promise<void> {
384
384
  const projectConfig: ProjectConfig = {
385
385
  name,
386
386
  localDir,
387
- server: serverId,
388
387
  remoteDir,
389
388
  steps,
390
389
  };
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
+ }
@@ -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
- if (cp.stdout) cp.stdout.pipe(process.stdout);
18
- if (cp.stderr) cp.stderr.pipe(process.stderr);
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
- console.log(msg);
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
  }
@@ -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
- '[ -f ~/.bashrc ] && source ~/.bashrc',
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
- process.stdout.write(str);
53
+ outSink.push(str);
47
54
  })
48
55
  .stderr.on('data', (data: Buffer) => {
49
56
  const str = data.toString();
50
57
  errStr += str;
51
- process.stderr.write(str);
58
+ errSink.push(str);
52
59
  });
53
60
  });
54
61
  });