maolike-cli 1.0.7 → 2.0.1

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.
@@ -1,152 +1,139 @@
1
1
  "use strict";
2
+ var _a;
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.ConfigManager = void 0;
4
5
  const tslib_1 = require("tslib");
5
6
  const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
6
7
  const path_1 = tslib_1.__importDefault(require("path"));
7
8
  const os_1 = tslib_1.__importDefault(require("os"));
8
- const chalk_1 = tslib_1.__importDefault(require("chalk"));
9
+ const SCHEMA_VERSION = 1;
9
10
  /**
10
- * 静态配置管理器
11
+ * 分层部署配置管理器。
11
12
  *
12
- * 核心设计理念:
13
- * 1. 响应式配置:通过 Proxy 深度监听 ConfigManager.config 的任何修改。
14
- * 2. 自动持久化:配置发生变动自动同步到磁盘,用户无需手动调用 save。
15
- * 3. 拦截隔离:私有化持久化方法,确保只能通过数据驱动更新。
13
+ * 项目与服务器分别持久化,CLI 不读写 desktop/app.json,避免部署配置变更
14
+ * 覆盖桌面应用偏好。
16
15
  */
17
16
  class ConfigManager {
18
- /**
19
- * 初始化配置系统
20
- * @private
21
- * @returns 响应式配置代理
22
- */
23
- static initialize() {
24
- const rawConfig = this.loadInitialConfig();
25
- return this.createReactiveProxy(rawConfig);
26
- }
27
- /**
28
- * 强制从磁盘重新加载配置
29
- * 用于多进程环境下(如交互式 Shell 执行子命令后)同步最新状态
30
- */
17
+ /** 强制从磁盘重新加载,供交互式 Shell 的子命令执行后刷新数据。 */
31
18
  static reload() {
32
- this.config = this.initialize();
33
- }
34
- /**
35
- * 加载初始配置数据
36
- * 若磁盘无配置,则返回默认值;否则将磁盘配置与默认配置合并
37
- * @private
38
- */
39
- static loadInitialConfig() {
40
- const diskConfig = this.loadFromDisk();
41
- if (!diskConfig)
42
- return Object.assign({}, this.DEFAULT_CONFIG);
43
- this.hasFile = true;
44
- return Object.assign(Object.assign(Object.assign({}, this.DEFAULT_CONFIG), diskConfig), { servers: diskConfig.servers || [], ssh: Object.assign(Object.assign({}, this.DEFAULT_CONFIG.ssh), (diskConfig.ssh || {})), "git-list": diskConfig["git-list"] || [] });
45
- }
46
- /**
47
- * 从磁盘读取 JSON 配置文件
48
- * @private
49
- * @returns 配置对象或在读取失败时返回 null
50
- */
51
- static loadFromDisk() {
52
- if (fs_extra_1.default.existsSync(this.configFile)) {
53
- try {
54
- return fs_extra_1.default.readJsonSync(this.configFile);
55
- }
56
- catch (e) {
57
- console.error(chalk_1.default.red(`[ConfigManager] 配置文件读取失败: ${e.message}`));
58
- return null;
59
- }
60
- }
61
- return null;
19
+ this.projects = this.readProjects();
20
+ this.servers = this.readServers();
62
21
  }
63
- /**
64
- * 检查本地配置文件是否存在
65
- * @returns true if config file exists
66
- */
22
+ /** 两个部署配置文件都存在时才视为已初始化。 */
67
23
  static existsOnDisk() {
68
- return this.hasFile;
69
- }
70
- /**
71
- * 创建深层响应式代理
72
- * 递归代理对象的所有属性,在属性 Setter 中拦截并触发磁盘持久化
73
- * @param target 目标对象
74
- * @private
75
- */
76
- static createReactiveProxy(target) {
77
- if (this.proxyCache.has(target))
78
- return this.proxyCache.get(target);
79
- const self = this;
80
- const proxy = new Proxy(target, {
81
- get(target, prop, receiver) {
82
- const value = Reflect.get(target, prop, receiver);
83
- if (value !== null && typeof value === "object") {
84
- return self.createReactiveProxy(value);
85
- }
86
- return value;
87
- },
88
- set(target, prop, value, receiver) {
89
- const oldVal = Reflect.get(target, prop, receiver);
90
- const result = Reflect.set(target, prop, value, receiver);
91
- // 变更检测:值确实改变了才保存
92
- if (result && oldVal !== value) {
93
- self.persist();
94
- }
95
- return result;
96
- },
97
- deleteProperty(target, prop) {
98
- const result = Reflect.deleteProperty(target, prop);
99
- if (result) {
100
- self.persist();
101
- }
102
- return result;
103
- },
24
+ return fs_extra_1.default.existsSync(this.projectsFile) && fs_extra_1.default.existsSync(this.serversFile);
25
+ }
26
+ /** 创建尚不存在的空配置文件,不覆盖已有数据。 */
27
+ static ensureOnDisk() {
28
+ if (!fs_extra_1.default.existsSync(this.projectsFile))
29
+ this.persistProjects(this.projects);
30
+ if (!fs_extra_1.default.existsSync(this.serversFile))
31
+ this.persistServers(this.servers);
32
+ }
33
+ static replaceProjects(projects) {
34
+ this.persistProjects(projects);
35
+ this.projects = projects;
36
+ }
37
+ static replaceServers(servers) {
38
+ this.persistServers(servers);
39
+ this.servers = servers;
40
+ }
41
+ /** 写入单个项目;写前重读,避免覆盖另一个进程刚保存的其他项目。 */
42
+ static upsertProject(project) {
43
+ const projects = this.readProjects();
44
+ const index = projects.findIndex((item) => item.id === project.id);
45
+ if (index === -1)
46
+ projects.push(project);
47
+ else
48
+ projects[index] = project;
49
+ this.replaceProjects(projects);
50
+ }
51
+ static deleteProject(projectId) {
52
+ const projects = this.readProjects().filter((project) => project.id !== projectId);
53
+ this.replaceProjects(projects);
54
+ }
55
+ /** 写入单个服务器;项目环境只保存返回配置的稳定 ID。 */
56
+ static upsertServer(server) {
57
+ const servers = this.readServers();
58
+ const index = servers.findIndex((item) => item.id === server.id);
59
+ if (index === -1)
60
+ servers.push(server);
61
+ else
62
+ servers[index] = server;
63
+ this.replaceServers(servers);
64
+ }
65
+ static findProject(nameOrId) {
66
+ return this.projects.find((project) => project.name === nameOrId || project.id === nameOrId);
67
+ }
68
+ static getConfigPaths() {
69
+ return { projects: this.projectsFile, servers: this.serversFile };
70
+ }
71
+ static readProjects() {
72
+ const document = this.readConfigFile(this.projectsFile, {
73
+ schemaVersion: SCHEMA_VERSION,
74
+ projects: [],
75
+ });
76
+ if (!Array.isArray(document.projects)) {
77
+ throw new Error(`[ConfigManager] projects.json 缺少 projects 数组: ${this.projectsFile}`);
78
+ }
79
+ return document.projects;
80
+ }
81
+ static readServers() {
82
+ const document = this.readConfigFile(this.serversFile, {
83
+ schemaVersion: SCHEMA_VERSION,
84
+ servers: [],
104
85
  });
105
- this.proxyCache.set(target, proxy);
106
- return proxy;
107
- }
108
- /**
109
- * 核心持久化逻辑
110
- * 将当前内存中的配置对象同步写入到用户主目录下的配置文件中
111
- * @private
112
- */
113
- static persist() {
86
+ if (!Array.isArray(document.servers)) {
87
+ throw new Error(`[ConfigManager] servers.json 缺少 servers 数组: ${this.serversFile}`);
88
+ }
89
+ return document.servers;
90
+ }
91
+ static readConfigFile(file, fallback) {
92
+ if (!fs_extra_1.default.existsSync(file))
93
+ return fallback;
94
+ let document;
114
95
  try {
115
- fs_extra_1.default.ensureDirSync(this.configDir);
116
- // 使用同步写入确保在 CLI 进程结束前数据已落盘
117
- fs_extra_1.default.writeJsonSync(this.configFile, this.config, { spaces: 2 });
118
- this.hasFile = true;
96
+ document = fs_extra_1.default.readJsonSync(file);
119
97
  }
120
- catch (e) {
121
- console.error(chalk_1.default.red(`[ConfigManager] 无法自动保存配置: ${e}`));
98
+ catch (error) {
99
+ throw new Error(`[ConfigManager] 配置文件读取失败 ${file}: ${error.message}`);
122
100
  }
101
+ if (document.schemaVersion !== SCHEMA_VERSION) {
102
+ throw new Error(`[ConfigManager] 不支持的 schemaVersion ${document.schemaVersion}: ${file}`);
103
+ }
104
+ return document;
105
+ }
106
+ static persistProjects(projects) {
107
+ this.writeConfigFile(this.projectsFile, {
108
+ schemaVersion: SCHEMA_VERSION,
109
+ projects,
110
+ });
123
111
  }
124
- /**
125
- * 获取配置文件在系统中的绝对路径
126
- */
127
- static getConfigPath() {
128
- return this.configFile;
112
+ static persistServers(servers) {
113
+ this.writeConfigFile(this.serversFile, {
114
+ schemaVersion: SCHEMA_VERSION,
115
+ servers,
116
+ });
129
117
  }
130
- /**
131
- * 兼容方法:旧代码可能仍会尝试调用 getConfig
132
- * @returns 当前配置对象
133
- */
134
- static getConfig() {
135
- return this.config;
118
+ /** 同目录临时文件 + rename,避免进程中断留下半截 JSON。 */
119
+ static writeConfigFile(file, document) {
120
+ fs_extra_1.default.ensureDirSync(this.deploymentDir);
121
+ const temporaryFile = `${file}.${process.pid}.tmp`;
122
+ try {
123
+ fs_extra_1.default.writeJsonSync(temporaryFile, document, { spaces: 2 });
124
+ fs_extra_1.default.renameSync(temporaryFile, file);
125
+ }
126
+ catch (error) {
127
+ fs_extra_1.default.removeSync(temporaryFile);
128
+ throw error;
129
+ }
136
130
  }
137
131
  }
138
132
  exports.ConfigManager = ConfigManager;
139
- ConfigManager.configDir = path_1.default.join(os_1.default.homedir(), ".maolike");
140
- ConfigManager.configFile = path_1.default.join(os_1.default.homedir(), ".maolike", "config.json");
141
- ConfigManager.proxyCache = new WeakMap();
142
- ConfigManager.hasFile = false;
143
- ConfigManager.DEFAULT_CONFIG = {
144
- version: "1.0",
145
- ssh: { host: "", port: 22, user: "root", password: "" },
146
- "git-list": [],
147
- };
148
- /**
149
- * 响应式配置对象
150
- * 修改此对象的任何属性都会自动触发磁盘更新。
151
- */
152
- ConfigManager.config = ConfigManager.initialize();
133
+ _a = ConfigManager;
134
+ ConfigManager.storageRoot = path_1.default.join(os_1.default.homedir(), ".maolike", "desktop");
135
+ ConfigManager.deploymentDir = path_1.default.join(_a.storageRoot, "deployment");
136
+ ConfigManager.projectsFile = path_1.default.join(_a.deploymentDir, "projects.json");
137
+ ConfigManager.serversFile = path_1.default.join(_a.deploymentDir, "servers.json");
138
+ ConfigManager.projects = _a.readProjects();
139
+ ConfigManager.servers = _a.readServers();
@@ -1,124 +1,77 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
+ const crypto_1 = require("crypto");
4
5
  const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
5
6
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
6
7
  const ConfigManager_1 = require("../services/ConfigManager");
8
+ const ConfigPrompter_1 = require("../utils/ConfigPrompter");
7
9
  const Scanner_1 = require("../utils/Scanner");
8
- // 辅助函数:清理拖入的路径
9
10
  const pathFilter = (input) => input.trim().replace(/^['"]|['"]$/g, "");
10
- /**
11
- * 初始化向导处理类。
12
- * 负责引导用户完成:
13
- * 1. 全局 SSH 配置
14
- * 2. 批量或手动添加 Git 项目配置
15
- * 3. 生成并保存最终的配置文件
16
- */
11
+ /** 初始化新的 projects.json 与 servers.json。 */
17
12
  class InitWizard {
18
13
  constructor() {
19
- this.gitList = [];
14
+ this.projects = [];
15
+ this.servers = [];
20
16
  }
21
- /**
22
- * 启动初始化向导的主流程
23
- */
24
17
  run() {
25
18
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
26
19
  console.log(chalk_1.default.bold.magenta("\n✨ 欢迎使用 Maolike CLI 初始化向导 ✨\n"));
27
- // 检查现有配置
28
20
  if (ConfigManager_1.ConfigManager.existsOnDisk()) {
29
21
  const { overwrite } = yield inquirer_1.default.prompt([
30
22
  {
31
23
  type: "confirm",
32
24
  name: "overwrite",
33
- message: "发现已存在的全局配置,是否覆盖?",
25
+ message: "发现已存在的部署配置,是否覆盖 projects.json 和 servers.json?",
34
26
  default: false,
35
27
  },
36
28
  ]);
37
29
  if (!overwrite)
38
30
  return;
39
31
  }
40
- // 第一步: SSH 配置
41
- const sshConfig = yield this.askSshConfig();
42
- // 第二步: 项目配置
43
32
  const { configProjectsNow } = yield inquirer_1.default.prompt([
44
33
  {
45
34
  type: "confirm",
46
35
  name: "configProjectsNow",
47
- message: "是否立即配置 Git 项目列表?",
36
+ message: "是否立即配置部署项目?",
48
37
  default: true,
49
38
  },
50
39
  ]);
51
- if (configProjectsNow) {
40
+ if (configProjectsNow)
52
41
  yield this.configureProjects();
53
- }
54
- // 直接修改响应式配置对象,触发自动保存
55
- ConfigManager_1.ConfigManager.config.ssh = sshConfig;
56
- ConfigManager_1.ConfigManager.config["git-list"] = this.gitList;
42
+ ConfigManager_1.ConfigManager.replaceServers(this.servers);
43
+ ConfigManager_1.ConfigManager.replaceProjects(this.projects);
44
+ const paths = ConfigManager_1.ConfigManager.getConfigPaths();
57
45
  console.log(chalk_1.default.green("\n✅ 配置初始化完成!"));
58
- console.log(chalk_1.default.dim(`配置文件路径: ${ConfigManager_1.ConfigManager.getConfigPath()}`));
59
- });
60
- }
61
- /**
62
- * 第一步:收集全局 SSH 连接配置
63
- * @private
64
- */
65
- askSshConfig() {
66
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
67
- console.log(chalk_1.default.blue("--- 第一步: 配置全局 SSH (必需) ---"));
68
- return inquirer_1.default.prompt([
69
- { type: "input", name: "host", message: "服务器 IP (Host):", validate: (i) => (i ? true : "Required") },
70
- { type: "number", name: "port", message: "端口 (Port):", default: 22 },
71
- { type: "input", name: "user", message: "用户名 (User):", default: "root" },
72
- { type: "password", name: "password", message: "SSH 密码 (留空则使用 Key):" },
73
- { type: "input", name: "gitUser", message: "远程 Git 账户 (Optional):" },
74
- { type: "password", name: "gitPassword", message: "远程 Git 密码/Token (Optional):" },
75
- ]);
46
+ console.log(chalk_1.default.dim(`项目配置: ${paths.projects}`));
47
+ console.log(chalk_1.default.dim(`服务器配置: ${paths.servers}`));
76
48
  });
77
49
  }
78
- /**
79
- * 第二步:循环配置多个 Git 项目
80
- * 支持通过扫描目录或手动录入的方式添加
81
- * @private
82
- */
83
50
  configureProjects() {
84
51
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
85
- let loop = true;
86
- console.log(chalk_1.default.blue("\n--- 第二步:添加 Git 项目 ---"));
87
- while (loop) {
52
+ console.log(chalk_1.default.blue("\n--- 添加部署项目 ---"));
53
+ while (true) {
88
54
  const { addMode } = yield inquirer_1.default.prompt([
89
55
  {
90
56
  type: "list",
91
57
  name: "addMode",
92
58
  message: "请选择添加方式:",
93
59
  choices: [
94
- { name: "📂 扫描父目录批量添加 (Scan)", value: "scan" },
95
- { name: "📝 手动添加单项 (Manual)", value: "manual" },
96
- { name: "✅ 完成配置 (Finish)", value: "finish" },
60
+ { name: "📂 扫描父目录批量添加", value: "scan" },
61
+ { name: "📝 手动添加单项", value: "manual" },
62
+ { name: "✅ 完成配置", value: "finish" },
97
63
  ],
98
64
  },
99
65
  ]);
100
66
  if (addMode === "finish")
101
- break;
102
- let projectsToConfig = [];
103
- if (addMode === "scan") {
104
- projectsToConfig = yield this.scanAndSelectRepos();
105
- }
106
- else {
107
- projectsToConfig.push(yield this.manualInputRepo());
108
- }
109
- // 批量配置
110
- for (const proj of projectsToConfig) {
111
- if (!proj)
112
- continue; // 如果扫描为空则跳过
113
- yield this.collectProjectDetails(proj);
67
+ return;
68
+ const candidates = addMode === "scan" ? yield this.scanAndSelectRepos() : [yield this.manualInputRepo()];
69
+ for (const candidate of candidates) {
70
+ yield this.collectProjectDetails(candidate);
114
71
  }
115
72
  }
116
73
  });
117
74
  }
118
- /**
119
- * 扫描指定目录下的所有 Git 仓库并供用户选择
120
- * @private
121
- */
122
75
  scanAndSelectRepos() {
123
76
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
124
77
  const { parentPath } = yield inquirer_1.default.prompt([
@@ -142,95 +95,40 @@ class InitWizard {
142
95
  type: "checkbox",
143
96
  name: "selected",
144
97
  message: `找到 ${repos.length} 个仓库,请选择:`,
145
- choices: repos.map((r) => ({ name: r.name, value: r })),
146
- validate: (d) => (d.length > 0 ? true : "请至少选择一个"),
98
+ choices: repos.map((repo) => ({ name: repo.name, value: repo })),
147
99
  },
148
100
  ]);
149
101
  return selected;
150
102
  }
151
- catch (err) {
152
- console.log(chalk_1.default.red(`扫描失败: ${err.message}`));
103
+ catch (error) {
104
+ console.log(chalk_1.default.red(`扫描失败: ${error.message}`));
153
105
  return [];
154
106
  }
155
107
  });
156
108
  }
157
- /**
158
- * 手动录入单个项目的基础信息
159
- * @private
160
- */
161
109
  manualInputRepo() {
162
110
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
163
111
  return inquirer_1.default.prompt([
164
- { type: "input", name: "name", message: "项目标识 (Name):" },
112
+ { type: "input", name: "name", message: "项目名称:", validate: (input) => Boolean(input) || "项目名称不能为空" },
165
113
  { type: "input", name: "path", message: "项目路径 (支持拖入):", filter: pathFilter },
166
114
  ]);
167
115
  });
168
116
  }
169
- /**
170
- * 收集项目的详细构建与部署配置
171
- * 包含:打包命令、产物路径、测试环境及远程路径
172
- * @param proj 包含 name 和 path 的基础项目对象
173
- * @private
174
- */
175
- collectProjectDetails(proj) {
117
+ collectProjectDetails(candidate) {
176
118
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
177
- console.log(chalk_1.default.cyan(`\n🔧 正在配置项目: [${proj.name}] ...`));
178
- const baseInfo = yield inquirer_1.default.prompt([{ type: "input", name: "buildCmd", message: "打包命令:", default: "npm run build" }]);
179
- // 构建产物配置
180
- const artifactType = yield inquirer_1.default.prompt([
181
- {
182
- type: "list",
183
- name: "type",
184
- message: "构建产物类型:",
185
- choices: [
186
- { name: "📂 文件夹 (e.g. dist)", value: "dir" },
187
- { name: "📄 文件 (e.g. bundle.js)", value: "file" },
188
- ],
189
- },
190
- ]);
191
- let distName = "dist";
192
- let distFile = undefined;
193
- if (artifactType.type === "dir") {
194
- const ans = yield inquirer_1.default.prompt([
195
- {
196
- type: "input",
197
- name: "distName",
198
- message: "文件夹名称:",
199
- default: "dist",
200
- filter: pathFilter,
201
- },
202
- ]);
203
- distName = ans.distName;
204
- }
205
- else {
206
- const ans = yield inquirer_1.default.prompt([
207
- {
208
- type: "input",
209
- name: "distFile",
210
- message: "文件路径 (逗号分隔):",
211
- filter: pathFilter,
212
- },
213
- ]);
214
- distFile = ans.distFile;
215
- distName = "";
216
- }
217
- // 测试环境配置
218
- const { hasTest } = yield inquirer_1.default.prompt([{ type: "confirm", name: "hasTest", message: "需要同步测试环境?", default: true }]);
219
- let testConfig = { path: "", branch: "" };
220
- if (hasTest) {
221
- testConfig = yield inquirer_1.default.prompt([
222
- { type: "input", name: "path", message: "测试项目路径:", filter: pathFilter },
223
- { type: "input", name: "branch", message: "测试分支:", default: "dev" },
224
- ]);
225
- }
226
- // 远程配置
227
- const remote = yield inquirer_1.default.prompt([{ type: "input", name: "remotePath", message: "服务器部署路径:", filter: pathFilter }]);
228
- this.gitList.push({
229
- name: proj.name,
230
- path: proj.path,
231
- build: { cmd: baseInfo.buildCmd, distName, distFile },
232
- test: { path: testConfig.path, branch: testConfig.branch },
233
- server: { remotePath: remote.remotePath },
119
+ console.log(chalk_1.default.cyan(`\n🔧 正在配置项目: [${candidate.name}] ...`));
120
+ const build = yield ConfigPrompter_1.ConfigPrompter.promptBuildConfig();
121
+ const test = yield ConfigPrompter_1.ConfigPrompter.promptTestEnvironment(undefined, this.servers);
122
+ const production = yield ConfigPrompter_1.ConfigPrompter.promptProductionConfig(undefined, test, null, this.servers);
123
+ build.productionCommand = production.productionCommand;
124
+ this.projects.push({
125
+ id: (0, crypto_1.randomUUID)(),
126
+ name: candidate.name.trim(),
127
+ localPath: candidate.path,
128
+ build,
129
+ environments: { test, production: production.environment },
130
+ enabled: true,
131
+ sortOrder: this.projects.length,
234
132
  });
235
133
  });
236
134
  }
@@ -7,6 +7,15 @@ const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
7
  const ConfigManager_1 = require("../services/ConfigManager");
8
8
  const CommandRegistry_1 = require("../constants/CommandRegistry");
9
9
  const child_process_1 = require("child_process");
10
+ const COMMON_COMMANDS = [
11
+ CommandRegistry_1.CommandType.LIST,
12
+ CommandRegistry_1.CommandType.VIEW,
13
+ CommandRegistry_1.CommandType.TEST,
14
+ CommandRegistry_1.CommandType.DEPLOY,
15
+ CommandRegistry_1.CommandType.CONNECT,
16
+ CommandRegistry_1.CommandType.CURSOR,
17
+ CommandRegistry_1.CommandType.HELP,
18
+ ];
10
19
  /**
11
20
  * 交互式 Shell 管理器
12
21
  * 负责提供类终端的交互体验,支持 Tab 补全、命令历史提示及即时指令执行
@@ -17,7 +26,7 @@ class InteractiveShell {
17
26
  this.line = "";
18
27
  /** 当前显示的预测/建议字符串 */
19
28
  this.suggestion = "";
20
- /** 从配置中读取的项目名称列表 */
29
+ /** 从配置中读取的项目列表 */
21
30
  this.projects = [];
22
31
  /** 支持的基础命令列表 */
23
32
  this.commands = [...CommandRegistry_1.ALL_COMMANDS, ...CommandRegistry_1.SHELL_COMMANDS];
@@ -30,11 +39,18 @@ class InteractiveShell {
30
39
  });
31
40
  // 加载已有项目名用于预测
32
41
  if (ConfigManager_1.ConfigManager.existsOnDisk()) {
33
- this.projects = ConfigManager_1.ConfigManager.config["git-list"].map((t) => t.name);
42
+ this.projects = ConfigManager_1.ConfigManager.projects;
34
43
  }
35
44
  this.buildSuggestionIndex();
36
45
  // 自动启动交互模式
37
46
  console.log(chalk_1.default.green("\n✨ 进入 Maolike 交互模式"));
47
+ console.log(chalk_1.default.bold.cyan("\n常用命令:"));
48
+ for (const command of COMMON_COMMANDS) {
49
+ const definition = CommandRegistry_1.COMMAND_REGISTRY[command];
50
+ const usage = definition.usage.replace(/^ml\s+/, "");
51
+ console.log(` ${chalk_1.default.yellow(usage.padEnd(26))}${chalk_1.default.gray(definition.description)}`);
52
+ }
53
+ console.log(` ${chalk_1.default.yellow("exit".padEnd(26))}${chalk_1.default.gray("退出交互模式")}`);
38
54
  console.log(chalk_1.default.gray("提示: 输入首字母获取提示,按 Tab 或 → 补全。\n"));
39
55
  this.loopNext();
40
56
  }
@@ -44,11 +60,16 @@ class InteractiveShell {
44
60
  * @private
45
61
  */
46
62
  buildSuggestionIndex() {
63
+ var _a;
47
64
  this.suggestionIndex.push(...this.commands);
48
- const targetCmds = ["test", "update", "view", "delete"];
65
+ const targetCmds = ["test", "deploy", "update", "view", "delete"];
49
66
  for (const cmd of targetCmds) {
50
- for (const proj of this.projects) {
51
- this.suggestionIndex.push(`${cmd} ${proj}`);
67
+ for (const project of this.projects) {
68
+ if (cmd === "test" && !project.environments.test.workspacePath.trim())
69
+ continue;
70
+ if (cmd === "deploy" && !((_a = project.environments.production) === null || _a === void 0 ? void 0 : _a.workspacePath.trim()))
71
+ continue;
72
+ this.suggestionIndex.push(`${cmd} ${project.name}`);
52
73
  }
53
74
  }
54
75
  }
@@ -173,7 +194,7 @@ class InteractiveShell {
173
194
  // 每次循环重新加载配置,确保增删改查后补全列表是最新的
174
195
  ConfigManager_1.ConfigManager.reload();
175
196
  if (ConfigManager_1.ConfigManager.existsOnDisk()) {
176
- this.projects = ConfigManager_1.ConfigManager.config["git-list"].map((t) => t.name);
197
+ this.projects = ConfigManager_1.ConfigManager.projects;
177
198
  }
178
199
  // 重建索引
179
200
  this.suggestionIndex = [];