@mindbase/deploy 1.0.1 → 1.1.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,5 @@
1
1
  import { Command } from 'commander';
2
2
  import * as clack from '@clack/prompts';
3
- import { resolve } from 'path';
4
3
  import { runApp, runProject, showConfig } from '../src/app.js';
5
4
  import { runInit } from '../src/init/app.js';
6
5
  import { createServerCommand } from '../src/server/app.js';
@@ -26,12 +25,12 @@ export function createDeployProgram(): Command {
26
25
  });
27
26
 
28
27
  program
29
- .command('init')
30
- .description('交互式初始化项目部署配置')
31
- .action(async () => {
28
+ .command('init [path]')
29
+ .description('在 cwd(或指定目录)生成本地 deploy.config.json')
30
+ .action(async (path?: string) => {
32
31
  clack.intro('deploy init');
33
32
  try {
34
- await runInit(resolve('.'));
33
+ await runInit(path);
35
34
  } catch (err) {
36
35
  clack.log.error(`执行失败: ${err}`);
37
36
  process.exit(1);
@@ -39,13 +38,13 @@ export function createDeployProgram(): Command {
39
38
  });
40
39
 
41
40
  program
42
- .command('run <project>')
43
- .description('直接运行指定项目')
44
- .option('-s, --server <server>', '覆盖项目默认服务器')
45
- .action(async (project: string, options: { server?: string }) => {
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 }) => {
46
45
  clack.intro('deploy run');
47
46
  try {
48
- await runProject(project, options.server);
47
+ await runProject(path, options.server);
49
48
  } catch (err) {
50
49
  clack.log.error(`执行失败: ${err}`);
51
50
  process.exit(1);
@@ -54,7 +53,7 @@ export function createDeployProgram(): Command {
54
53
 
55
54
  program
56
55
  .command('config')
57
- .description('查看配置')
56
+ .description('查看全局 servers 配置')
58
57
  .action(() => {
59
58
  showConfig();
60
59
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindbase/deploy",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "MindBase 部署工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,24 +17,24 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
+ "scripts": {
21
+ "dev": "tsx bin/index.ts",
22
+ "start": "tsx bin/index.ts",
23
+ "postinstall": "node scripts/ensure-tsx.cjs"
24
+ },
20
25
  "dependencies": {
21
26
  "@clack/prompts": "^0.9.1",
27
+ "@mindbase/cli-ui": "workspace:*",
22
28
  "commander": "^12.0.0",
23
29
  "cross-spawn": "^7.0.6",
24
30
  "picocolors": "^1.1.1",
25
31
  "ssh2-sftp-client": "^9.1.0",
26
- "tsx": "^4.19.0",
27
- "@mindbase/cli-ui": "1.0.0"
32
+ "tsx": "^4.19.0"
28
33
  },
29
34
  "devDependencies": {
30
35
  "@types/cross-spawn": "^6.0.6",
31
36
  "@types/node": "^20.0.0",
32
37
  "@types/ssh2-sftp-client": "^9.0.4",
33
38
  "typescript": "^5.1.3"
34
- },
35
- "scripts": {
36
- "dev": "tsx bin/index.ts",
37
- "start": "tsx bin/index.ts",
38
- "postinstall": "node scripts/ensure-tsx.cjs"
39
39
  }
40
- }
40
+ }
File without changes
package/src/app.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as clack from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
+ import { relative } from 'path';
3
4
  import { noteBox } from '@mindbase/cli-ui';
4
- import { getConfig, getConfigPath } from './config/manager.js';
5
- import type { DeployConfig, ProjectConfig } from './config/schema.js';
5
+ import { getConfig, getConfigPath, discoverProjectConfigs, loadProjectConfig } from './config/manager.js';
6
+ import type { ProjectConfig } from './config/schema.js';
6
7
  import { connectSftp } from './ssh/client.js';
7
8
  import { runSteps } from './script/runner.js';
8
9
 
@@ -27,33 +28,47 @@ export function showConfig(): void {
27
28
  }
28
29
 
29
30
  /**
30
- * 直接运行指定项目
31
+ * 显示项目配置摘要并让用户确认部署
31
32
  */
32
- export async function runProject(projectId: string, serverOverride?: string): Promise<void> {
33
- const config = getConfig();
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
+ }
34
46
 
35
- const project = config.projects[projectId];
36
- if (!project) {
37
- clack.log.error(`项目 "${projectId}" 不存在`);
38
- const available = Object.keys(config.projects);
39
- if (available.length > 0) {
40
- clack.log.info(`可用项目: ${available.join(', ')}`);
41
- }
42
- process.exit(1);
43
- }
47
+ /**
48
+ * 直接运行指定项目(单项目模式)
49
+ */
50
+ export async function runProject(targetPath?: string, serverOverride?: string): Promise<void> {
51
+ const project = loadProjectConfig(targetPath);
52
+ const globalConfig = getConfig();
44
53
 
45
54
  const serverId = serverOverride || project.server;
46
- const serverConfig = config.servers[serverId];
55
+ const serverConfig = globalConfig.servers[serverId];
47
56
  if (!serverConfig) {
48
- clack.log.error(`服务器 "${serverId}" 不存在`);
57
+ clack.log.error(`服务器 "${serverId}" 不存在,请先运行 deploy server add`);
49
58
  process.exit(1);
50
59
  }
51
60
 
52
61
  clack.intro(`部署 ${pc.cyan(project.name)} → ${pc.cyan(serverId)}`);
53
62
 
63
+ if (!(await confirmProjectDeploy(project, serverId))) {
64
+ clack.log.info('已取消');
65
+ clack.outro('完成');
66
+ return;
67
+ }
68
+
54
69
  const spinner = clack.spinner();
55
70
  spinner.start('连接服务器...');
56
- const sftp = await connectSftp(projectId, serverConfig);
71
+ const sftp = await connectSftp(project.name, serverConfig);
57
72
  spinner.stop('已连接');
58
73
 
59
74
  try {
@@ -69,87 +84,99 @@ export async function runProject(projectId: string, serverOverride?: string): Pr
69
84
  }
70
85
 
71
86
  /**
72
- * 交互式主流程:选服务器多选项目依次部署
87
+ * 交互式主流程:扫描 cwd 下所有 deploy.config.json 多选按 server 分组部署
73
88
  */
74
89
  export async function runApp(): Promise<void> {
75
- const config = getConfig();
90
+ const cwd = process.cwd();
91
+ const discovered = discoverProjectConfigs(cwd);
92
+ const globalConfig = getConfig();
76
93
 
77
- // 检查配置
78
- const serverIds = Object.keys(config.servers);
79
- const projectIds = Object.keys(config.projects);
80
-
81
- if (serverIds.length === 0) {
82
- clack.log.error('没有已配置的服务器,请先运行 deploy server add');
94
+ if (discovered.length === 0) {
95
+ clack.log.error(`未在 ${cwd} 下找到任何 deploy.config.json,请先运行 deploy init`);
83
96
  return;
84
97
  }
85
- if (projectIds.length === 0) {
86
- clack.log.error('没有已配置的项目,请先运行 deploy init');
98
+
99
+ // 0 个服务器直接报错
100
+ if (Object.keys(globalConfig.servers).length === 0) {
101
+ clack.log.error('没有已配置的服务器,请先运行 deploy server add');
87
102
  return;
88
103
  }
89
104
 
90
105
  clack.intro('deploy');
91
106
 
92
- // 选择服务器
93
- const serverId = handleCancel<string>(
94
- await clack.select({
95
- message: '选择目标服务器',
96
- options: serverIds.map((id) => ({
97
- value: id,
98
- label: `${id} (${config.servers[id].host}:${config.servers[id].port})`,
99
- })),
100
- })
101
- );
107
+ // 选择项目
108
+ let selected: { path: string; config: ProjectConfig }[];
109
+
110
+ if (discovered.length === 1) {
111
+ const only = discovered[0];
112
+ clack.log.info(`仅发现一个项目: ${only.config.name}`);
113
+ const go = handleCancel<boolean>(
114
+ await clack.confirm({ message: `是否部署 ${only.config.name}?`, initialValue: true })
115
+ );
116
+ if (!go) {
117
+ clack.outro('已取消');
118
+ return;
119
+ }
120
+ selected = [only];
121
+ } else {
122
+ selected = handleCancel<typeof discovered>(
123
+ await clack.multiselect({
124
+ message: '选择要部署的项目(空格切换,a 全选)',
125
+ options: discovered.map((item) => ({
126
+ value: item,
127
+ label: `${item.config.name} (${relative(cwd, item.path) || item.path}) → ${pc.dim(item.config.server)}`,
128
+ })),
129
+ required: true,
130
+ })
131
+ );
132
+ }
102
133
 
103
- // 筛选该项目服务器下的项目(或显示全部)
104
- const filteredProjects = projectIds.filter(
105
- (id) => config.projects[id].server === serverId
106
- );
107
- const selectableProjects = filteredProjects.length > 0 ? filteredProjects : projectIds;
108
-
109
- // 多选项目
110
- const selectedProjects = handleCancel<string[]>(
111
- await clack.multiselect({
112
- message: '选择要部署的项目(空格切换,a 全选)',
113
- options: selectableProjects.map((id) => ({
114
- value: id,
115
- label: `${config.projects[id].name} (${id})`,
116
- })),
117
- required: true,
118
- })
119
- );
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
+ }
120
146
 
121
- // 连接服务器
122
- const spinner = clack.spinner();
123
- spinner.start(`连接 ${serverId}...`);
124
- const serverConfig = config.servers[serverId];
125
- const sftp = await connectSftp('deploy', serverConfig);
126
- spinner.stop('已连接');
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);
153
+ }
127
154
 
128
- try {
129
- // 依次部署选中的项目
130
- for (const projectId of selectedProjects) {
131
- const project = config.projects[projectId];
132
- // 运行时使用选中的服务器覆盖项目默认服务器
133
- const effectiveProject: ProjectConfig = {
134
- ...project,
135
- server: serverId,
136
- };
137
-
138
- clack.log.step(`开始部署 ${pc.cyan(project.name)}`);
139
-
140
- try {
141
- await runSteps(project.steps, effectiveProject, sftp);
142
- clack.log.success(`${project.name} 部署完成`);
143
- } catch (err) {
144
- clack.log.error(`${project.name} 部署失败: ${err}`);
145
- const continueDeploy = handleCancel<boolean>(
146
- await clack.confirm({ message: '是否继续部署下一个项目?' })
147
- );
148
- if (!continueDeploy) break;
155
+ // 依次连接每个 server,组内项目依次部署
156
+ for (const [serverId, items] of groups) {
157
+ const serverConfig = globalConfig.servers[serverId];
158
+ const spinner = clack.spinner();
159
+ spinner.start(`连接 ${serverId}...`);
160
+ const sftp = await connectSftp('deploy', serverConfig);
161
+ spinner.stop('已连接');
162
+
163
+ try {
164
+ for (const { config } of items) {
165
+ clack.log.step(`开始部署 ${pc.cyan(config.name)} → ${pc.cyan(serverId)}`);
166
+ try {
167
+ await runSteps(config.steps, config, sftp);
168
+ clack.log.success(`${config.name} 部署完成`);
169
+ } catch (err) {
170
+ clack.log.error(`${config.name} 部署失败: ${err}`);
171
+ const cont = handleCancel<boolean>(
172
+ await clack.confirm({ message: '是否继续部署下一个项目?' })
173
+ );
174
+ if (!cont) break;
175
+ }
149
176
  }
177
+ } finally {
178
+ await sftp.end();
150
179
  }
151
- } finally {
152
- await sftp.end();
153
180
  }
154
181
 
155
182
  clack.outro('全部完成');
@@ -1,15 +1,32 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
- import { dirname, join } from 'path';
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
2
+ import { dirname, join, resolve, isAbsolute } from 'path';
3
3
  import { homedir } from 'os';
4
- import type { DeployConfig } from './schema.js';
4
+ import type { GlobalConfig, ProjectConfig } from './schema.js';
5
5
 
6
- /** 默认配置 */
7
- const defaultConfig: DeployConfig = {
6
+ /** 项目级配置文件名 */
7
+ export const PROJECT_CONFIG_FILE = 'deploy.config.json';
8
+
9
+ /** 扫描时忽略的目录名 */
10
+ const SCAN_IGNORE_DIRS = new Set([
11
+ 'node_modules',
12
+ '.git',
13
+ 'dist',
14
+ 'build',
15
+ 'target',
16
+ '.next',
17
+ 'coverage',
18
+ '.cache',
19
+ ]);
20
+
21
+ /** 扫描深度上限 */
22
+ const SCAN_MAX_DEPTH = 5;
23
+
24
+ /** 默认全局配置 */
25
+ const defaultGlobalConfig: GlobalConfig = {
8
26
  servers: {},
9
- projects: {},
10
27
  };
11
28
 
12
- /** 配置文件路径 */
29
+ /** 全局配置目录与文件路径 */
13
30
  const CONFIG_DIR = join(homedir(), '.mindbase');
14
31
  const CONFIG_PATH = join(CONFIG_DIR, 'deploy.json');
15
32
 
@@ -23,34 +40,133 @@ function ensureConfigDir(): void {
23
40
  }
24
41
 
25
42
  /**
26
- * 读取部署配置
43
+ * 读取全局配置(servers)
27
44
  */
28
- export function getConfig(): DeployConfig {
45
+ export function getConfig(): GlobalConfig {
29
46
  if (!existsSync(CONFIG_PATH)) {
30
47
  ensureConfigDir();
31
- writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2), 'utf-8');
32
- return structuredClone(defaultConfig);
48
+ writeFileSync(CONFIG_PATH, JSON.stringify(defaultGlobalConfig, null, 2), 'utf-8');
49
+ return structuredClone(defaultGlobalConfig);
33
50
  }
34
51
 
35
52
  try {
36
53
  const content = readFileSync(CONFIG_PATH, 'utf-8');
37
- return JSON.parse(content);
54
+ const parsed = JSON.parse(content);
55
+ return { servers: parsed.servers ?? {} };
38
56
  } catch {
39
- return structuredClone(defaultConfig);
57
+ return structuredClone(defaultGlobalConfig);
40
58
  }
41
59
  }
42
60
 
43
61
  /**
44
- * 写入部署配置
62
+ * 写入全局配置
45
63
  */
46
- export function setConfig(config: DeployConfig): void {
64
+ export function setConfig(config: GlobalConfig): void {
47
65
  ensureConfigDir();
48
66
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
49
67
  }
50
68
 
51
69
  /**
52
- * 获取配置文件路径
70
+ * 获取全局配置文件路径
53
71
  */
54
72
  export function getConfigPath(): string {
55
73
  return CONFIG_PATH;
56
74
  }
75
+
76
+ /**
77
+ * 解析配置文件实际路径:目录 → 拼 deploy.config.json;文件 → 直接用
78
+ */
79
+ function resolveConfigFilePath(targetPath?: string): string {
80
+ const cwd = targetPath ?? process.cwd();
81
+ const abs = isAbsolute(cwd) ? cwd : resolve(process.cwd(), cwd);
82
+ const stat = existsSync(abs) ? statSync(abs) : null;
83
+ if (stat && stat.isFile()) return abs;
84
+ return join(abs, PROJECT_CONFIG_FILE);
85
+ }
86
+
87
+ /**
88
+ * 解析 localDir 为绝对路径(相对配置文件所在目录)
89
+ */
90
+ function resolveLocalDir(localDir: string, configFilePath: string): string {
91
+ if (isAbsolute(localDir)) return localDir;
92
+ return resolve(dirname(configFilePath), localDir);
93
+ }
94
+
95
+ /**
96
+ * 加载项目级配置
97
+ * @param targetPath 目录或文件路径,缺省为 cwd
98
+ */
99
+ export function loadProjectConfig(targetPath?: string): ProjectConfig {
100
+ const configFilePath = resolveConfigFilePath(targetPath);
101
+ if (!existsSync(configFilePath)) {
102
+ throw new Error(`项目配置文件不存在: ${configFilePath}`);
103
+ }
104
+
105
+ const raw = readFileSync(configFilePath, 'utf-8');
106
+ const parsed = JSON.parse(raw) as ProjectConfig;
107
+ parsed.localDir = resolveLocalDir(parsed.localDir ?? '.', configFilePath);
108
+ return parsed;
109
+ }
110
+
111
+ /**
112
+ * 写入项目级配置
113
+ * @param config 项目配置
114
+ * @param targetPath 目录或文件路径,缺省为 cwd
115
+ */
116
+ export function saveProjectConfig(config: ProjectConfig, targetPath?: string): void {
117
+ const configFilePath = resolveConfigFilePath(targetPath);
118
+ mkdirSync(dirname(configFilePath), { recursive: true });
119
+ writeFileSync(configFilePath, JSON.stringify(config, null, 2), 'utf-8');
120
+ }
121
+
122
+ /** 发现结果 */
123
+ export interface DiscoveredProject {
124
+ /** 配置文件绝对路径 */
125
+ path: string;
126
+ /** 已解析的 ProjectConfig */
127
+ config: ProjectConfig;
128
+ }
129
+
130
+ /**
131
+ * 扫描 rootPath 下所有 deploy.config.json
132
+ */
133
+ export function discoverProjectConfigs(rootPath?: string): DiscoveredProject[] {
134
+ const root = rootPath ?? process.cwd();
135
+ const results: DiscoveredProject[] = [];
136
+
137
+ function walk(dir: string, depth: number): void {
138
+ if (depth > SCAN_MAX_DEPTH) return;
139
+
140
+ let entries;
141
+ try {
142
+ entries = readdirSync(dir);
143
+ } catch {
144
+ return;
145
+ }
146
+
147
+ for (const entry of entries) {
148
+ if (SCAN_IGNORE_DIRS.has(entry)) continue;
149
+ const full = join(dir, entry);
150
+ let stat;
151
+ try {
152
+ stat = statSync(full);
153
+ } catch {
154
+ continue;
155
+ }
156
+
157
+ if (stat.isDirectory()) {
158
+ walk(full, depth + 1);
159
+ } else if (entry === PROJECT_CONFIG_FILE) {
160
+ try {
161
+ const config = loadProjectConfig(full);
162
+ results.push({ path: full, config });
163
+ } catch {
164
+ /* 跳过损坏的配置文件 */
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ walk(root, 0);
171
+ return results;
172
+ }
@@ -79,13 +79,13 @@ export interface PrivateKeyServerConfig {
79
79
  /** 服务器配置 */
80
80
  export type ServerConfig = PasswordServerConfig | PrivateKeyServerConfig;
81
81
 
82
- /** 项目配置 */
82
+ /** 项目配置(项目级 deploy.config.json) */
83
83
  export interface ProjectConfig {
84
84
  /** 项目显示名称 */
85
85
  name: string;
86
- /** 本地目录(绝对路径) */
86
+ /** 本地目录(绝对路径或相对配置文件的路径,加载时解析为绝对路径) */
87
87
  localDir: string;
88
- /** 默认目标服务器 ID */
88
+ /** 目标服务器 ID(引用全局 servers) */
89
89
  server: string;
90
90
  /** 远端目录 */
91
91
  remoteDir: string;
@@ -93,10 +93,8 @@ export interface ProjectConfig {
93
93
  steps: Step[];
94
94
  }
95
95
 
96
- /** 部署配置文件结构 */
97
- export interface DeployConfig {
96
+ /** 全局配置结构(~/.mindbase/deploy.json) */
97
+ export interface GlobalConfig {
98
98
  /** 服务器列表 */
99
99
  servers: Record<string, ServerConfig>;
100
- /** 项目列表 */
101
- projects: Record<string, ProjectConfig>;
102
100
  }
package/src/index.ts CHANGED
File without changes
package/src/init/app.ts CHANGED
@@ -3,8 +3,8 @@ import pc from 'picocolors';
3
3
  import { resolve } from 'path';
4
4
  import { existsSync, readFileSync } from 'fs';
5
5
  import { noteBox } from '@mindbase/cli-ui';
6
- import { getConfig, setConfig } from '../config/manager.js';
7
- import type { Step, StepType, ProjectConfig, ServerConfig } from '../config/schema.js';
6
+ import { getConfig, saveProjectConfig } from '../config/manager.js';
7
+ import type { Step, StepType, ProjectConfig } from '../config/schema.js';
8
8
  import { connectSftp } from '../ssh/client.js';
9
9
 
10
10
  /** 取消处理 */
@@ -262,29 +262,21 @@ async function collectStep(): Promise<Step | null> {
262
262
 
263
263
  /**
264
264
  * 运行 deploy init 主流程
265
+ * @param targetPath 项目目录(缺省 cwd),生成的 deploy.config.json 写入此目录
265
266
  */
266
- export async function runInit(cwd: string): Promise<void> {
267
+ export async function runInit(targetPath?: string): Promise<void> {
267
268
  clack.intro('deploy init');
268
269
 
270
+ const cwd = resolve(targetPath ?? '.');
271
+
269
272
  const deployConfig = getConfig();
270
273
 
271
274
  // 1. 项目名称
272
275
  const defaultName = getDefaultProjectName(cwd);
273
- const projectId = handleCancel<string>(
274
- await clack.text({
275
- message: '项目 ID(用于标识)',
276
- initialValue: defaultName,
277
- })
278
- );
279
-
280
- if (deployConfig.projects[projectId]) {
281
- clack.log.warn(`项目 "${projectId}" 已存在,将覆盖配置`);
282
- }
283
-
284
276
  const name = handleCancel<string>(
285
277
  await clack.text({
286
278
  message: '项目显示名称',
287
- initialValue: projectId,
279
+ initialValue: defaultName,
288
280
  })
289
281
  );
290
282
 
@@ -307,11 +299,11 @@ export async function runInit(cwd: string): Promise<void> {
307
299
  })
308
300
  );
309
301
 
310
- // 4. 本地目录(绝对路径)
302
+ // 4. 本地目录(用相对路径 ./,便于跨设备/入库)
311
303
  const localDir = handleCancel<string>(
312
304
  await clack.text({
313
- message: '本地目录(绝对路径)',
314
- initialValue: cwd,
305
+ message: '本地目录(相对 deploy.config.json,默认 ./)',
306
+ initialValue: './',
315
307
  })
316
308
  );
317
309
 
@@ -391,7 +383,7 @@ export async function runInit(cwd: string): Promise<void> {
391
383
  // 7. 预览配置
392
384
  const projectConfig: ProjectConfig = {
393
385
  name,
394
- localDir: resolve(localDir),
386
+ localDir,
395
387
  server: serverId,
396
388
  remoteDir,
397
389
  steps,
@@ -404,9 +396,8 @@ export async function runInit(cwd: string): Promise<void> {
404
396
  );
405
397
 
406
398
  if (confirmSave) {
407
- deployConfig.projects[projectId] = projectConfig;
408
- setConfig(deployConfig);
409
- clack.log.success(`项目 "${projectId}" 配置已保存`);
399
+ saveProjectConfig(projectConfig, cwd);
400
+ clack.log.success(`配置已写入 ${resolve(cwd, 'deploy.config.json')}`);
410
401
  } else {
411
402
  clack.log.info('已取消保存');
412
403
  }
File without changes
File without changes
package/src/server/app.ts CHANGED
File without changes
package/src/ssh/client.ts CHANGED
File without changes
File without changes
package/LICENSE DELETED
@@ -1,201 +0,0 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright [yyyy] [name of copyright owner]
190
-
191
- Licensed under the Apache License, Version 2.0 (the "License");
192
- you may not use this file except in compliance with the License.
193
- You may obtain a copy of the License at
194
-
195
- http://www.apache.org/licenses/LICENSE-2.0
196
-
197
- Unless required by applicable law or agreed to in writing, software
198
- distributed under the License is distributed on an "AS IS" BASIS,
199
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
- See the License for the specific language governing permissions and
201
- limitations under the License.