@xubill/xx-cli 1.0.6 → 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.
package/AGENTS.md ADDED
@@ -0,0 +1,177 @@
1
+ # AGENTS.md
2
+
3
+ 本文档描述 xx-cli 项目中各个核心模块/代理的职责和交互关系。
4
+
5
+ ## 项目概述
6
+
7
+ xx-cli 是一个基于 Node.js 的命令行工具集,提供丰富的开发辅助功能。采用插件化架构,支持自定义插件开发和动态命令加载。
8
+
9
+ ## 核心模块
10
+
11
+ ### 1. Core (核心管理器)
12
+
13
+ **文件位置**: `lib/core/index.js`
14
+
15
+ **职责**:
16
+ - 初始化 CLI 系统环境
17
+ - 加载和管理插件(内置插件 + 用户插件)
18
+ - 注册命令到 Commander.js 框架
19
+ - 实现命令自动建议功能(当用户输入错误命令时提供相似命令建议)
20
+ - 管理全局钩子系统
21
+
22
+ **关键方法**:
23
+ - `init()`: 初始化核心功能
24
+ - `loadPlugins()`: 扫描并加载插件目录
25
+ - `loadPlugin(pluginFile)`: 加载单个插件,支持缓存
26
+ - `registerCommands(program)`: 注册所有命令
27
+ - `registerNewFormatPlugin()`: 注册新格式插件
28
+ - `registerCommandSuggestions()`: 注册命令自动建议
29
+ - `collectAllCommands()`: 收集所有可用命令
30
+ - `getCommandSuggestions()`: 获取命令建议
31
+ - `registerHook()` / `triggerHook()`: 钩子系统
32
+
33
+ ### 2. BasePlugin (插件基类)
34
+
35
+ **文件位置**: `lib/core/base-plugin.js`
36
+
37
+ **职责**:
38
+ - 提供插件标准化接口
39
+ - 实现通用功能(加载状态、进度条、错误处理等)
40
+ - 提供配置管理和验证能力
41
+ - 实现钩子系统
42
+
43
+ **关键方法**:
44
+ - `registerCommands(program)`: 注册命令(抽象方法,子类必须实现)
45
+ - `init()`: 初始化插件
46
+ - `startLoading()` / `stopLoading()`: 加载状态管理
47
+ - `startProgressBar()` / `updateProgressBar()` / `stopProgressBar()`: 进度条管理
48
+ - `showSuccess()` / `showWarning()` / `showError()` / `showInfo()`: 消息展示
49
+ - `loadConfig()` / `saveConfig()` / `validateConfig()`: 配置管理
50
+ - `handleError()`: `executeAsync()`: 异步操作封装错误处理
51
+ -
52
+ - `registerHook()` / `triggerHook()`: 钩子系统
53
+ - `copyToClipboard()`: 剪贴板操作
54
+
55
+ ### 3. PluginsHelper (插件辅助工具)
56
+
57
+ **文件位置**: `lib/utils/plugins-helper.js`
58
+
59
+ **职责**:
60
+ - 为新格式插件(非类继承)提供与 BasePlugin 相同的通用功能
61
+ - 无需继承即可使用插件能力
62
+
63
+ **关键方法**: 与 BasePlugin 相同(详见上方)
64
+
65
+ ### 4. ConfigManager (配置管理器)
66
+
67
+ **文件位置**: `lib/utils/config-manager.js`
68
+
69
+ **职责**:
70
+ - 管理插件配置文件(JSON 格式)
71
+ - 实现配置优先级:命令行 > 配置文件 > 默认配置
72
+ - 使用 Joi 进行配置验证
73
+ - 支持全局配置和本地配置
74
+
75
+ **关键方法**:
76
+ - `loadConfig()`: 加载配置
77
+ - `saveConfig()`: 保存配置
78
+ - `validateConfig()`: 验证配置
79
+ - `getConfigPath()`: 获取配置路径
80
+ - `mergeConfig()`: 合并配置
81
+
82
+ ### 5. PluginConfig (插件配置)
83
+
84
+ **文件位置**: `lib/utils/plugin-config.js`
85
+
86
+ **职责**:
87
+ - 管理插件的启用/禁用状态
88
+ - 提供插件配置文件路径
89
+
90
+ **关键方法**:
91
+ - `isPluginDisabled()`: 检查插件是否禁用
92
+ - `getPluginConfigPath()`: 获取插件配置路径
93
+
94
+ ## 插件类型
95
+
96
+ ### 核心插件 (Core Plugins)
97
+
98
+ 位于 `lib/plugins/`
99
+
100
+ | 插件名称 | 功能描述 |
101
+ |---------|---------|
102
+ | config-manager | 配置管理 |
103
+ | history | 命令历史记录 |
104
+ | plugin-manager | 插件管理 |
105
+
106
+ ### 功能插件 (User Plugins)
107
+ 不要修改../plugins的引入,这个是核心代码。myplugins我会复制到安装目录的,不需要你操作,你只管生成就好。
108
+ 位于 `lib/myplugins/`
109
+
110
+
111
+ ## 插件开发格式
112
+
113
+ ### 新格式 (推荐)
114
+
115
+ 使用配置对象方式,无需继承类:
116
+
117
+ ```javascript
118
+ module.exports = {
119
+ name: 'plugin-name',
120
+ description: '插件描述',
121
+ alias: '别名',
122
+ options: [
123
+ { name: '-o, --option', description: '选项描述' }
124
+ ],
125
+ action: async (args, options, helper) => {
126
+ // 插件逻辑
127
+ }
128
+ };
129
+ ```
130
+
131
+ ### 旧格式 (向后兼容)
132
+
133
+ 使用类继承方式:
134
+
135
+ ```javascript
136
+ const BasePlugin = require('../core/base-plugin');
137
+
138
+ class MyPlugin extends BasePlugin {
139
+ registerCommands(program) {
140
+ program.command('my-plugin')
141
+ .description('我的插件')
142
+ .action(() => { /* 插件逻辑 */ });
143
+ }
144
+ }
145
+
146
+ module.exports = MyPlugin;
147
+ ```
148
+
149
+ ## 交互流程
150
+
151
+ ```
152
+ 用户输入命令
153
+
154
+ bin/cli.js 入口
155
+
156
+ Commander.js 解析
157
+
158
+ Core.registerCommands() 注册命令
159
+
160
+ 插件 action 执行
161
+
162
+ PluginsHelper/BasePlugin 提供工具方法
163
+
164
+ 命令完成,输出结果
165
+ ```
166
+
167
+ ## 错误处理
168
+
169
+ - **命令不存在**: 通过 `registerCommandSuggestions` 提供相似命令建议
170
+ - **插件加载失败**: 捕获错误并跳过该插件,继续加载其他插件
171
+ - **配置错误**: 使用 Joi 验证并给出清晰错误信息
172
+
173
+ ## 配置优先级
174
+
175
+ ```
176
+ 命令行选项 > 本地配置文件 > 全局配置文件 > 默认配置
177
+ ```
package/bin/cli.js CHANGED
@@ -60,12 +60,14 @@
60
60
  // 全局错误处理
61
61
  process.on('uncaughtException', (error) => {
62
62
  console.error(chalk.red('错误:'), error.message);
63
+ console.error(chalk.red('错误堆栈:'), error.stack);
63
64
  console.error(chalk.yellow('提示:'), '请检查命令参数是否正确,或使用 --help 查看帮助信息');
64
65
  process.exit(1);
65
66
  });
66
67
 
67
68
  process.on('unhandledRejection', (error) => {
68
69
  console.error(chalk.red('错误:'), error.message);
70
+ console.error(chalk.red('错误堆栈:'), error.stack);
69
71
  console.error(chalk.yellow('提示:'), '请检查命令参数是否正确,或使用 --help 查看帮助信息');
70
72
  process.exit(1);
71
73
  });
package/lib/core/index.js CHANGED
@@ -8,6 +8,7 @@ const path = require("path");
8
8
  const logger = require("../utils/logger");
9
9
  const pkg = require("../../package.json");
10
10
  const pluginConfig = require("../utils/plugin-config");
11
+ const { createPluginsHelper } = require('../utils/plugins-helper');
11
12
 
12
13
  class Core {
13
14
  constructor() {
@@ -42,7 +43,7 @@ class Core {
42
43
  const userPluginsDir = path.join(
43
44
  process.env.HOME || process.env.USERPROFILE,
44
45
  ".xx-cli",
45
- "plugins"
46
+ "plugins",
46
47
  );
47
48
 
48
49
  // 确保用户插件目录存在
@@ -64,10 +65,10 @@ class Core {
64
65
  // 将用户插件添加到插件文件列表中
65
66
  pluginFiles = [...pluginFiles, ...userPluginFiles];
66
67
  }
67
-
68
68
  // 只收集插件文件信息,不立即加载
69
69
  this.pluginFiles = pluginFiles;
70
70
  }
71
+
71
72
  /**
72
73
  * 加载单个插件
73
74
  * @param {string} pluginFile - 插件文件名
@@ -85,7 +86,7 @@ class Core {
85
86
  const userPluginsDir = path.join(
86
87
  process.env.HOME || process.env.USERPROFILE,
87
88
  ".xx-cli",
88
- "plugins"
89
+ "plugins",
89
90
  );
90
91
 
91
92
  // 优先从内置插件目录加载
@@ -111,14 +112,14 @@ class Core {
111
112
  const coreDir = path.join(__dirname, "../core");
112
113
  pluginContent = pluginContent.replace(
113
114
  /require\('\.\.\/core\//g,
114
- `require('${coreDir}/`
115
+ `require('${coreDir}/`,
115
116
  );
116
117
 
117
118
  // 替换 ../utils 为绝对路径
118
119
  const utilsDir = path.join(__dirname, "../utils");
119
120
  pluginContent = pluginContent.replace(
120
121
  /require\('\.\.\/utils\//g,
121
- `require('${utilsDir}/`
122
+ `require('${utilsDir}/`,
122
123
  );
123
124
 
124
125
  // 创建一个临时模块来加载修改后的插件内容
@@ -132,8 +133,17 @@ class Core {
132
133
 
133
134
  const Plugin = tempModule.exports;
134
135
 
135
- // 检查是否为类
136
- if (typeof Plugin === "function" && Plugin.prototype) {
136
+ // 检查是否为新格式的插件对象
137
+ if (
138
+ Plugin &&
139
+ typeof Plugin === "object" &&
140
+ Plugin.name &&
141
+ (Plugin.action || Plugin.subcommands)
142
+ ) {
143
+ // 新格式插件对象,不需要实例化
144
+ this.pluginCache.set(pluginFile, Plugin);
145
+ return Plugin;
146
+ } else if (typeof Plugin === "function" && Plugin.prototype) {
137
147
  // 实例化类插件
138
148
  const pluginInstance = new Plugin();
139
149
  pluginInstance.pluginName =
@@ -144,16 +154,28 @@ class Core {
144
154
  return pluginInstance;
145
155
  } else {
146
156
  // 保持非类插件的兼容性
147
-
148
- // 缓存插件
149
- this.pluginCache.set(pluginFile, Plugin);
150
- return Plugin;
157
+ // 对于函数式插件,也可能是新格式
158
+ if (
159
+ Plugin &&
160
+ typeof Plugin === "object" &&
161
+ Plugin.name &&
162
+ (Plugin.action || Plugin.subcommands)
163
+ ) {
164
+ // 新格式插件对象
165
+ this.pluginCache.set(pluginFile, Plugin);
166
+ return Plugin;
167
+ } else {
168
+ // 旧格式函数式插件
169
+ this.pluginCache.set(pluginFile, Plugin);
170
+ return Plugin;
171
+ }
151
172
  }
152
173
  } catch (error) {
153
174
  logger.error(`加载插件 ${pluginFile} 失败: ${error.message}`);
154
175
  return null;
155
176
  }
156
177
  }
178
+
157
179
  /**
158
180
  * 获取所有插件信息
159
181
  * @returns {Array} 插件信息数组
@@ -187,15 +209,29 @@ class Core {
187
209
  }
188
210
 
189
211
  const plugin = this.loadPlugin(pluginFile);
190
- if (plugin && plugin.registerCommands) {
212
+ if (plugin) {
191
213
  try {
192
- plugin.registerCommands(program);
214
+ // 检查是否为新格式插件
215
+ if (
216
+ plugin.name &&
217
+ (typeof plugin.action === "function" ||
218
+ (plugin.subcommands && Array.isArray(plugin.subcommands)))
219
+ ) {
220
+ this.registerNewFormatPlugin(program, plugin, pluginName);
221
+ } else if (plugin.registerCommands) {
222
+ // 旧格式插件
223
+ plugin.registerCommands(program);
224
+ }
193
225
  } catch (error) {
194
226
  // 捕获命令别名冲突等错误,确保其他插件能够正常注册
195
- if (error.message && error.message.includes('cannot add alias')) {
196
- console.warn(`警告: 插件 ${pluginName} 的命令别名与其他插件冲突,已跳过此插件的命令注册`);
227
+ if (error.message && error.message.includes("cannot add alias")) {
228
+ console.warn(
229
+ `警告: 插件 ${pluginName} 的命令别名与其他插件冲突,已跳过此插件的命令注册`,
230
+ );
197
231
  } else {
198
- console.error(`错误: 插件 ${pluginName} 注册命令失败: ${error.message}`);
232
+ console.error(
233
+ `错误: 插件 ${pluginName} 注册命令失败: ${error.message}`,
234
+ );
199
235
  }
200
236
  }
201
237
  }
@@ -206,6 +242,117 @@ class Core {
206
242
  this.registerCommandSuggestions(program);
207
243
  }
208
244
 
245
+ /**
246
+ * 注册新格式插件命令
247
+ * @param {Object} program - Commander 实例
248
+ * @param {Object} plugin - 插件对象
249
+ * @param {string} pluginName - 插件名称
250
+ */
251
+ registerNewFormatPlugin(program, plugin, pluginName) {
252
+ const cmd = program.command(plugin.name);
253
+
254
+ // 设置描述
255
+ if (plugin.description) {
256
+ cmd.description(plugin.description);
257
+ }
258
+
259
+ // 设置别名
260
+ if (plugin.alias) {
261
+ cmd.alias(plugin.alias);
262
+ }
263
+
264
+ // 添加选项
265
+ if (plugin.options && Array.isArray(plugin.options)) {
266
+ plugin.options.forEach((option) => {
267
+ if (option.name && option.description) {
268
+ cmd.option(option.name, option.description);
269
+ }
270
+ });
271
+ }
272
+
273
+ // 如果插件定义了子命令,则注册它们
274
+ if (plugin.subcommands && Array.isArray(plugin.subcommands)) {
275
+ plugin.subcommands.forEach((subcmd) => {
276
+ const subCommand = cmd.command(subcmd.name);
277
+
278
+ if (subcmd.description) {
279
+ subCommand.description(subcmd.description);
280
+ }
281
+
282
+ if (subcmd.options && Array.isArray(subcmd.options)) {
283
+ subcmd.options.forEach((option) => {
284
+ if (option.name && option.description) {
285
+ subCommand.option(option.name, option.description);
286
+ }
287
+ });
288
+ }
289
+
290
+ subCommand.action(async (...args) => {
291
+ let commandArgs = [];
292
+ let options = {};
293
+
294
+ if (args.length > 0) {
295
+ const lastArg = args[args.length - 1];
296
+ if (lastArg && typeof lastArg === 'object' && typeof lastArg.opts === 'function') {
297
+ options = { ...lastArg.opts() };
298
+ commandArgs = lastArg.args || [];
299
+ } else {
300
+ commandArgs = [...args];
301
+ }
302
+ }
303
+
304
+ // 创建插件帮助器实例
305
+ const helper = createPluginsHelper(plugin.name);
306
+ // 调用子命令的 action 方法
307
+ await subcmd.action(commandArgs, options, helper);
308
+ });
309
+ });
310
+ }
311
+
312
+ // 设置主命令的 action
313
+ if (plugin.action) {
314
+ cmd.action(async (...args) => {
315
+ // Commander.js 会将参数和选项作为单独的参数传递
316
+ // 最后一个参数通常是 Command 对象,其中包含选项
317
+ let commandArgs = [];
318
+ let options = {};
319
+
320
+ // 如果有参数,需要正确识别哪个是 Command 对象
321
+ if (args.length > 0) {
322
+ // 从后往前检查,找到 Command 对象
323
+ let commandObjIndex = -1;
324
+ for (let i = args.length - 1; i >= 0; i--) {
325
+ const arg = args[i];
326
+ if (
327
+ arg &&
328
+ typeof arg === "object" &&
329
+ typeof arg.opts === "function"
330
+ ) {
331
+ commandObjIndex = i;
332
+ break;
333
+ }
334
+ }
335
+
336
+ if (commandObjIndex !== -1) {
337
+ // 找到了 Command 对象
338
+ options = { ...args[commandObjIndex].opts() };
339
+ // 从 Command 对象的 args 属性获取命令参数
340
+ const commandObj = args[commandObjIndex];
341
+ commandArgs = commandObj.args || [];
342
+ } else {
343
+ // 没有 Command 对象,所有参数都是命令参数
344
+ commandArgs = [...args];
345
+ }
346
+ }
347
+
348
+ // 创建插件帮助器实例
349
+ const helper = createPluginsHelper(plugin.name);
350
+ // 调用插件的 action 方法
351
+ await plugin.action(commandArgs, options, helper);
352
+ });
353
+ }
354
+ }
355
+
209
356
  /**
210
357
  * 注册命令自动建议功能
211
358
  * @param {Object} program - Commander 实例
@@ -215,7 +362,7 @@ class Core {
215
362
  const allCommands = this.collectAllCommands(program);
216
363
 
217
364
  // 添加命令自动建议
218
- program.on("command:*", (args) => {
365
+ program.on("command:*", async (args) => {
219
366
  const inputCommand = args[0];
220
367
  const suggestions = this.getCommandSuggestions(inputCommand, allCommands);
221
368
 
@@ -224,6 +371,37 @@ class Core {
224
371
  suggestions.forEach((suggestion) => {
225
372
  console.log(` - ${suggestion}`);
226
373
  });
374
+ console.log("");
375
+ process.exit(1);
376
+ } else {
377
+ // 如果没有建议,也显示错误信息
378
+ console.log(`\n未找到命令 "${inputCommand}"。`);
379
+ console.log('使用 "xx help" 查看所有可用命令。');
380
+
381
+ // 自动调用 AI 解释命令作为兜底
382
+ console.log('\n正在调用 AI 解释您的输入...');
383
+
384
+ try {
385
+ // 动态导入 ai 插件
386
+ const aiPath = path.join(__dirname, '../plugins/ai.js');
387
+ if (fs.existsSync(aiPath)) {
388
+ const AIPlugin = require(aiPath);
389
+ if (AIPlugin.prototype && typeof AIPlugin === 'function') {
390
+ // 类继承格式插件
391
+ const pluginInstance = new AIPlugin();
392
+ if (pluginInstance.handleAI) {
393
+ await pluginInstance.handleAI(args, {});
394
+ }
395
+ } else if (AIPlugin.action) {
396
+ // 配置对象格式插件
397
+ await AIPlugin.action(args, {});
398
+ }
399
+ }
400
+ } catch (error) {
401
+ // AI 调用失败时不影响主流程
402
+ console.log('AI 解释失败:', error.message);
403
+ }
404
+
227
405
  console.log("");
228
406
  process.exit(1);
229
407
  }
@@ -241,14 +419,12 @@ class Core {
241
419
  // 收集核心命令
242
420
  if (program.commands) {
243
421
  program.commands.forEach((command) => {
422
+ // 直接使用 _name 获取命令名称
423
+ const commandName = command._name;
424
+
244
425
  // 添加命令名称
245
- if (command._name && command._name !== "*") {
246
- commands.push(command._name);
247
- }
248
-
249
- // 添加命令别名
250
- if (command._alias) {
251
- commands.push(command._alias);
426
+ if (commandName && commandName !== "*") {
427
+ commands.push(commandName);
252
428
  }
253
429
  });
254
430
  }
@@ -265,7 +441,11 @@ class Core {
265
441
  getCommandSuggestions(input, commands) {
266
442
  return commands
267
443
  .filter((command) => {
268
- return command.startsWith(input.toLowerCase());
444
+ // 确保 command 是字符串类型
445
+ if (typeof command !== 'string') {
446
+ return false;
447
+ }
448
+ return command.toLowerCase().startsWith(input.toLowerCase());
269
449
  })
270
450
  .slice(0, 5); // 最多返回5个建议
271
451
  }
@@ -322,7 +502,7 @@ class Core {
322
502
  } catch (error) {
323
503
  console.error(
324
504
  `执行插件 ${pluginName} 的钩子 ${event} 时出错:`,
325
- error.message
505
+ error.message,
326
506
  );
327
507
  }
328
508
  }