momo-ai 1.0.21 → 1.0.22

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.
Files changed (94) hide show
  1. package/.claude/skills/r2mo-rad-lain/SKILL.md +63 -374
  2. package/.trae/skills/algorithmic-art/LICENSE.txt +202 -0
  3. package/.trae/skills/algorithmic-art/SKILL.md +405 -0
  4. package/.trae/skills/algorithmic-art/templates/generator_template.js +223 -0
  5. package/.trae/skills/algorithmic-art/templates/viewer.html +599 -0
  6. package/.trae/skills/doc-coauthoring/SKILL.md +375 -0
  7. package/.trae/skills/frontend-design/LICENSE.txt +177 -0
  8. package/.trae/skills/frontend-design/SKILL.md +42 -0
  9. package/.trae/skills/r2mo-rad-lain/SKILL.md +101 -0
  10. package/README.md +9 -32
  11. package/docs/images/r2mo-lain.png +0 -0
  12. package/package.json +11 -11
  13. package/skills/r2mo-rad-domain/SKILL.md +70 -0
  14. package/src/_skill/repositories.json +9 -3
  15. package/src/_template/LAIN/.obsidian/app.json +1 -0
  16. package/src/_template/LAIN/.obsidian/appearance.json +10 -0
  17. package/src/_template/LAIN/.obsidian/community-plugins.json +7 -0
  18. package/src/_template/LAIN/.obsidian/core-plugins.json +33 -0
  19. package/src/_template/LAIN/.obsidian/plugins/dataview/main.js +20876 -0
  20. package/src/_template/LAIN/.obsidian/plugins/dataview/manifest.json +11 -0
  21. package/src/_template/LAIN/.obsidian/plugins/dataview/styles.css +141 -0
  22. package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/data.json +815 -0
  23. package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/main.js +10 -0
  24. package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json +12 -0
  25. package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/styles.css +1 -0
  26. package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/main.js +153 -0
  27. package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/manifest.json +11 -0
  28. package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/styles.css +1 -0
  29. package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/main.js +7732 -0
  30. package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/manifest.json +10 -0
  31. package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/styles.css +38 -0
  32. package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/main.js +504 -0
  33. package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
  34. package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
  35. package/src/_template/LAIN/.obsidian/snippets/body-font.css +27 -0
  36. package/src/_template/LAIN/.obsidian/themes/Primary/manifest.json +9 -0
  37. package/src/_template/LAIN/.obsidian/themes/Primary/theme.css +3878 -0
  38. package/src/_template/LAIN/.obsidian/themes/Retro Windows/manifest.json +7 -0
  39. package/src/_template/LAIN/.obsidian/themes/Retro Windows/theme.css +582 -0
  40. package/src/_template/LAIN/.obsidian/themes/RetroOS 98/manifest.json +9 -0
  41. package/src/_template/LAIN/.obsidian/themes/RetroOS 98/theme.css +2566 -0
  42. package/src/_template/LAIN/.obsidian/types.json +28 -0
  43. package/src/_template/LAIN/.obsidian/workspace.json +184 -0
  44. package/src/_template/LAIN/AGENTS.md +170 -16
  45. package/src/_template/R2MO/domain-enhance.md +10 -0
  46. package/src/commander/app.json +13 -0
  47. package/src/commander/apply.json +13 -0
  48. package/src/commander/ask.json +6 -0
  49. package/src/commander/docs.json +13 -0
  50. package/src/commander/domain.json +19 -0
  51. package/src/commander/init.json +1 -1
  52. package/src/commander/mmr0.json +6 -0
  53. package/src/commander/mmr2.json +6 -0
  54. package/src/executor/executeApp.js +133 -0
  55. package/src/executor/{executeSkills.js → executeApply.js} +166 -302
  56. package/src/executor/executeAsk.js +274 -0
  57. package/src/executor/executeDocs.js +498 -0
  58. package/src/executor/executeDomain.js +293 -0
  59. package/src/executor/executeInit.js +159 -383
  60. package/src/executor/executeMcp.js +74 -1
  61. package/src/executor/executeMmr0.js +488 -0
  62. package/src/executor/executeMmr2.js +880 -0
  63. package/src/executor/index.js +15 -3
  64. package/src/python/r2mo_proto.py +418 -0
  65. package/src/python/r2mo_proto_database.py +369 -0
  66. package/src/python/r2mo_proto_domain.py +458 -0
  67. package/src/utils/momo-menu.js +43 -13
  68. package/.claude/skills/r2mo-rad-lain/PROMPT.md +0 -281
  69. package/.claude/skills/r2mo-rad-lain/README.md +0 -192
  70. package/.claude/skills/r2mo-rad-lain/examples/argument-parsing.js +0 -154
  71. package/.claude/skills/r2mo-rad-lain/examples/file-operations.js +0 -182
  72. package/.claude/skills/r2mo-rad-lain/file-utils-api.md +0 -281
  73. package/.claude/skills/r2mo-rad-lain/menu-api.md +0 -187
  74. package/.claude/skills/r2mo-rad-lain/scripts/file-utils.js +0 -223
  75. package/.claude/skills/r2mo-rad-lain/scripts/menu.js +0 -289
  76. package/.claude/skills/r2mo-rad-lain/scripts/yaml-parser.js +0 -209
  77. package/.claude/skills/r2mo-rad-lain/templates/command.json.template +0 -13
  78. package/.claude/skills/r2mo-rad-lain/templates/executor.js.template +0 -32
  79. package/.claude/skills/r2mo-rad-lain/templates/interactive-menu.js.template +0 -221
  80. package/src/_template/LAIN/.momo/advanced/actor.md +0 -42
  81. package/src/_template/LAIN/.momo/advanced/refer.json +0 -46
  82. package/src/_template/LAIN/.momo/scripts/submodule-clean.sh +0 -56
  83. package/src/_template/LAIN/changes/proposal.md +0 -39
  84. package/src/_template/LAIN/changes/tasks/task-detail.md +0 -45
  85. package/src/_template/LAIN/changes/tasks.md +0 -49
  86. package/src/_template/LAIN/execute/admin-n-f-dashboard.md +0 -53
  87. package/src/_template/LAIN/execute/admin-n-f-form.md +0 -51
  88. package/src/_template/LAIN/execute/admin-n-f-home.md +0 -49
  89. package/src/_template/LAIN/execute/admin-n-f-list.md +0 -52
  90. package/src/_template/LAIN/execute/admin-n-f-login.md +0 -56
  91. package/src/_template/LAIN/specification/project-model.md +0 -13
  92. package/src/_template/LAIN/specification/project.md +0 -73
  93. package/src/_template/LAIN/specification/requirement.md +0 -25
  94. package/src/commander/skills.json +0 -20
@@ -0,0 +1,880 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const Ec = require('../epic');
4
+ const fsAsync = require('fs').promises;
5
+ const { execSync } = require('child_process');
6
+ const { parseFile, exists, gitClone, scanDir } = require('../utils/momo-file-utils');
7
+ const { selectSingle, selectMultiple } = require('../utils/momo-menu');
8
+ require('colors');
9
+
10
+ // 仓库配置
11
+ const SPEC_REPO_URL = 'https://gitee.com/silentbalanceyh/r2mo-spec.git';
12
+ const LOCAL_CACHE_DIR = '.r2mo/repo/r2mo-spec';
13
+
14
+ // BaseEntity 中已存在的字段(不需要重复生成)
15
+ // 注意:需要同时检查下划线命名(SQL 格式)和驼峰命名(Java 格式)
16
+ const BASE_ENTITY_FIELDS = new Set([
17
+ // 下划线命名(SQL 格式)
18
+ 'id', 'code', 'created_by', 'created_at', 'updated_by', 'updated_at',
19
+ 'active', 'language', 'version', 'sigma', 'tenant_id', 'app_id', 'metadata',
20
+ // 驼峰命名(Java 格式)
21
+ 'createdBy', 'createdAt', 'updatedBy', 'updatedAt',
22
+ 'tenantId', 'appId'
23
+ ]);
24
+
25
+ /**
26
+ * 检查命令是否可用
27
+ */
28
+ const _isCommandAvailable = (command) => {
29
+ try {
30
+ const whereCmd = process.platform === 'win32' ? 'where' : 'which';
31
+ execSync(`${whereCmd} ${command}`, { stdio: 'ignore' });
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ };
37
+
38
+ /**
39
+ * 解析 pom.xml 提取 artifactId(排除 parent 节点)
40
+ */
41
+ const _parsePomXml = (pomPath) => {
42
+ try {
43
+ const content = fs.readFileSync(pomPath, 'utf8');
44
+ const withoutParent = content.replace(/<parent>[\s\S]*?<\/parent>/gi, '');
45
+ const match = withoutParent.match(/<artifactId>([^<]+)<\/artifactId>/);
46
+ return match && match[1] ? match[1].trim() : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ };
51
+
52
+ /**
53
+ * 检查仓库是否是最新的
54
+ */
55
+ const _isRepositoryUpToDate = (repoPath) => {
56
+ try {
57
+ // 获取远程最新 commit
58
+ execSync(`cd "${repoPath}" && git fetch --quiet origin`, { stdio: 'ignore' });
59
+ const remoteCommit = execSync(`cd "${repoPath}" && git rev-parse origin/HEAD`, { encoding: 'utf8' }).trim();
60
+ // 获取本地 commit
61
+ const localCommit = execSync(`cd "${repoPath}" && git rev-parse HEAD`, { encoding: 'utf8' }).trim();
62
+ return remoteCommit === localCommit;
63
+ } catch (error) {
64
+ return false;
65
+ }
66
+ };
67
+
68
+ /**
69
+ * 克隆或更新仓库
70
+ */
71
+ const _cloneOrUpdateRepository = async (projectDir) => {
72
+ const repoPath = path.join(projectDir, LOCAL_CACHE_DIR);
73
+
74
+ if (exists(repoPath)) {
75
+ // 检查是否是最新的
76
+ Ec.waiting('正在检查仓库状态...');
77
+ if (_isRepositoryUpToDate(repoPath)) {
78
+ Ec.info('✓ 仓库已是最新版本,无需更新');
79
+ return repoPath;
80
+ }
81
+
82
+ // 需要更新
83
+ try {
84
+ Ec.waiting('正在更新仓库...');
85
+ execSync(`cd "${repoPath}" && git pull --quiet`, { stdio: 'ignore' });
86
+ Ec.info('✓ 仓库已更新');
87
+ } catch (error) {
88
+ Ec.warn('⚠ 更新失败,尝试重新克隆...');
89
+ await fsAsync.rm(repoPath, { recursive: true, force: true });
90
+ Ec.waiting('正在克隆仓库...');
91
+ gitClone(SPEC_REPO_URL, repoPath, { shallow: true });
92
+ Ec.info('✓ 仓库已克隆');
93
+ }
94
+ } else {
95
+ Ec.waiting('正在克隆仓库...');
96
+ await fsAsync.mkdir(path.dirname(repoPath), { recursive: true });
97
+ gitClone(SPEC_REPO_URL, repoPath, { shallow: true });
98
+ Ec.info('✓ 仓库已克隆');
99
+ }
100
+
101
+ return repoPath;
102
+ };
103
+
104
+ /**
105
+ * 递归扫描所有 .md 文件
106
+ */
107
+ const _scanMarkdownFiles = (dir) => {
108
+ const files = [];
109
+
110
+ const scan = (currentDir) => {
111
+ if (!exists(currentDir)) return;
112
+
113
+ const items = fs.readdirSync(currentDir);
114
+ for (const item of items) {
115
+ const itemPath = path.join(currentDir, item);
116
+ const stat = fs.statSync(itemPath);
117
+
118
+ if (stat.isDirectory()) {
119
+ scan(itemPath);
120
+ } else if (item.endsWith('.md')) {
121
+ files.push(itemPath);
122
+ }
123
+ }
124
+ };
125
+
126
+ scan(dir);
127
+ return files;
128
+ };
129
+
130
+ /**
131
+ * 解析 MD 文件的 front-matter
132
+ */
133
+ const _parseMarkdownFile = (filePath) => {
134
+ const parsed = parseFile(filePath);
135
+ if (!parsed || !parsed.attributes) {
136
+ return null;
137
+ }
138
+
139
+ const attrs = parsed.attributes;
140
+ // 检查是否包含必需的字段
141
+ if (!attrs.name || !attrs.java || !attrs.table) {
142
+ return null;
143
+ }
144
+
145
+ return {
146
+ name: attrs.name,
147
+ alias: attrs.alias || '',
148
+ identifier: attrs.identifier || '',
149
+ table: attrs.table,
150
+ java: attrs.java,
151
+ sql: attrs.sql || '',
152
+ body: parsed.body || '',
153
+ filePath: filePath
154
+ };
155
+ };
156
+
157
+ /**
158
+ * 从 SQL 中提取字段信息
159
+ */
160
+ const _extractFieldsFromSql = (sqlContent, tableName) => {
161
+ const fields = [];
162
+
163
+ if (!sqlContent || !sqlContent.trim()) {
164
+ return fields;
165
+ }
166
+
167
+ // 查找 CREATE TABLE 语句(支持多种格式)
168
+ const createTableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`?[\w.]+`?\.)?`?(\w+)`?\s*\(([\s\S]+?)\)(?:\s*ENGINE|\s*DEFAULT\s*CHARSET|$)/i;
169
+ const createTableMatch = sqlContent.match(createTableRegex);
170
+ if (!createTableMatch) {
171
+ return fields;
172
+ }
173
+
174
+ const tableDef = createTableMatch[2];
175
+ // 更智能的分割:考虑括号嵌套和字符串引号
176
+ const parts = [];
177
+ let current = '';
178
+ let depth = 0;
179
+ let inString = false;
180
+ let stringChar = null;
181
+ let inComment = false; // 处理行注释
182
+
183
+ for (let i = 0; i < tableDef.length; i++) {
184
+ const char = tableDef[i];
185
+ const nextChar = i < tableDef.length - 1 ? tableDef[i + 1] : null;
186
+ const prevChar = i > 0 ? tableDef[i - 1] : null;
187
+
188
+ // 处理行注释 --
189
+ if (char === '-' && nextChar === '-' && !inString) {
190
+ // 跳过注释直到行尾
191
+ while (i < tableDef.length && tableDef[i] !== '\n') {
192
+ i++;
193
+ }
194
+ continue;
195
+ }
196
+
197
+ // 处理字符串引号
198
+ if ((char === '"' || char === "'") && !inString) {
199
+ inString = true;
200
+ stringChar = char;
201
+ current += char;
202
+ } else if (char === stringChar && inString) {
203
+ // 检查是否是转义的引号
204
+ if (nextChar === stringChar) {
205
+ current += char + nextChar;
206
+ i++; // 跳过下一个字符
207
+ } else {
208
+ inString = false;
209
+ stringChar = null;
210
+ current += char;
211
+ }
212
+ } else if (inString) {
213
+ current += char;
214
+ } else if (char === '(') {
215
+ depth++;
216
+ current += char;
217
+ } else if (char === ')') {
218
+ depth--;
219
+ current += char;
220
+ } else if (char === ',' && depth === 0 && !inString) {
221
+ parts.push(current.trim());
222
+ current = '';
223
+ } else {
224
+ current += char;
225
+ }
226
+ }
227
+ if (current.trim()) {
228
+ parts.push(current.trim());
229
+ }
230
+
231
+ for (const part of parts) {
232
+ // 移除行注释(-- 开头的注释)
233
+ let trimmed = part.trim();
234
+ const commentIndex = trimmed.indexOf('--');
235
+ if (commentIndex !== -1) {
236
+ trimmed = trimmed.substring(0, commentIndex).trim();
237
+ }
238
+
239
+ if (!trimmed ||
240
+ trimmed.match(/^(PRIMARY\s+KEY|UNIQUE\s+(?:KEY|INDEX)?|KEY|INDEX|CONSTRAINT|FOREIGN\s+KEY)/i)) {
241
+ continue;
242
+ }
243
+
244
+ // 匹配字段定义:`field_name` 或 field_name TYPE [约束] [COMMENT 'comment']
245
+ // 先提取字段名(支持反引号)
246
+ let fieldNameMatch = trimmed.match(/^`?([a-zA-Z_][a-zA-Z0-9_]*)`?\s+/i);
247
+ if (!fieldNameMatch) {
248
+ // 尝试匹配不带反引号的字段名
249
+ fieldNameMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s+/i);
250
+ }
251
+ if (!fieldNameMatch) continue;
252
+
253
+ const fieldName = fieldNameMatch[1];
254
+
255
+ // 提取字段类型(支持带括号的类型,如 VARCHAR(128))
256
+ const typeMatch = trimmed.substring(fieldNameMatch[0].length).match(/^(\w+(?:\([^)]+\))?)/i);
257
+ if (!typeMatch) continue;
258
+
259
+ const fieldType = typeMatch[1].toUpperCase();
260
+
261
+ // 单独匹配 COMMENT 部分,因为 COMMENT 可能在字段定义的任何位置
262
+ let fullComment = '';
263
+ const commentMatch = trimmed.match(/COMMENT\s+(['"])(.*?)\1/i);
264
+ if (commentMatch) {
265
+ fullComment = commentMatch[2] || '';
266
+ }
267
+
268
+ // 从 COMMENT 中提取 "-" 之后的中文部分
269
+ // 例如:'「alias」- 别名' -> '别名'
270
+ const extractCommentAfterDash = (text) => {
271
+ if (!text || !text.trim()) return '';
272
+
273
+ // 查找 "-" 的位置(可能是中文或英文的破折号)
274
+ const dashMatch = text.match(/[--—]/);
275
+ if (!dashMatch) {
276
+ // 如果没有 "-",尝试提取所有中文
277
+ const chineseMatch = text.match(/[\u4e00-\u9fa5]+/g);
278
+ return chineseMatch && chineseMatch.length > 0 ? chineseMatch.join('') : '';
279
+ }
280
+
281
+ // 找到 "-" 的位置
282
+ const dashIndex = dashMatch.index;
283
+ // 提取 "-" 之后的所有内容(跳过破折号字符)
284
+ let afterDash = text.substring(dashIndex + 1);
285
+ // 去除前后空格和可能的其他字符
286
+ afterDash = afterDash.trim();
287
+
288
+ // 提取中文部分(只提取中文字符,连续的中文字符)
289
+ const chineseMatch = afterDash.match(/[\u4e00-\u9fa5]+/g);
290
+ if (chineseMatch && chineseMatch.length > 0) {
291
+ // 返回第一个匹配的中文部分(通常是描述)
292
+ return chineseMatch[0];
293
+ }
294
+
295
+ // 如果没有找到中文,返回空字符串
296
+ return '';
297
+ };
298
+ const comment = extractCommentAfterDash(fullComment);
299
+
300
+ // 跳过 BaseEntity 中已存在的字段
301
+ // 检查下划线命名(SQL 格式)和驼峰命名(Java 格式)
302
+ const fieldNameLower = fieldName.toLowerCase();
303
+ const fieldNameCamel = _toCamelCase(fieldName);
304
+
305
+ if (BASE_ENTITY_FIELDS.has(fieldName) ||
306
+ BASE_ENTITY_FIELDS.has(fieldNameLower) ||
307
+ BASE_ENTITY_FIELDS.has(fieldNameCamel)) {
308
+ continue;
309
+ }
310
+
311
+ // 转换 SQL 类型到 Java 类型
312
+ let javaType = 'String';
313
+ let imports = new Set();
314
+ let useTypedUUID = false;
315
+ let existFalse = false;
316
+
317
+ if (fieldType.match(/INT(?!EGER)/) && !fieldType.includes('BIGINT')) {
318
+ javaType = 'int';
319
+ } else if (fieldType.includes('BIGINT')) {
320
+ javaType = 'Long';
321
+ imports.add('java.lang.Long');
322
+ } else if (fieldType.includes('DECIMAL') || fieldType.includes('NUMERIC') ||
323
+ fieldType.includes('FLOAT') || fieldType.includes('DOUBLE')) {
324
+ javaType = 'BigDecimal';
325
+ imports.add('java.math.BigDecimal');
326
+ } else if (fieldType.includes('BOOLEAN') || fieldType.match(/TINYINT\s*\(\s*1\s*\)/)) {
327
+ javaType = 'boolean';
328
+ } else if (fieldType.includes('DATE') || fieldType.includes('TIME')) {
329
+ javaType = 'LocalDateTime';
330
+ imports.add('java.time.LocalDateTime');
331
+ } else if (fieldType.includes('UUID') || fieldType.match(/CHAR\s*\(\s*36\s*\)/)) {
332
+ javaType = 'UUID';
333
+ imports.add('java.util.UUID');
334
+ useTypedUUID = true;
335
+ }
336
+
337
+ // 检查字段名是否包含特殊后缀(如 _id 结尾可能是 UUID)
338
+ if (fieldName.toLowerCase().endsWith('_id') && !useTypedUUID) {
339
+ javaType = 'UUID';
340
+ imports.add('java.util.UUID');
341
+ useTypedUUID = true;
342
+ }
343
+
344
+ fields.push({
345
+ name: fieldName,
346
+ javaName: _toCamelCase(fieldName),
347
+ type: javaType,
348
+ comment: comment,
349
+ imports: imports,
350
+ useTypedUUID: useTypedUUID,
351
+ existFalse: existFalse
352
+ });
353
+ }
354
+
355
+ return fields;
356
+ };
357
+
358
+ /**
359
+ * 转换为驼峰命名
360
+ */
361
+ const _toCamelCase = (str) => {
362
+ return str.split('_').map((word, index) => {
363
+ if (index === 0) {
364
+ return word.toLowerCase();
365
+ }
366
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
367
+ }).join('');
368
+ };
369
+
370
+ /**
371
+ * 转换为 PascalCase
372
+ */
373
+ const _toPascalCase = (str) => {
374
+ const camel = _toCamelCase(str);
375
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
376
+ };
377
+
378
+ /**
379
+ * 查找 Java 包路径(优先查找 domain 包)
380
+ */
381
+ const _findJavaPackage = (domainPath) => {
382
+ const javaSrcPath = path.join(domainPath, 'src', 'main', 'java');
383
+ if (!exists(javaSrcPath)) {
384
+ return null;
385
+ }
386
+
387
+ // 优先查找 domain 包
388
+ const findDomainPackage = (dir, packageParts = []) => {
389
+ const items = fs.readdirSync(dir);
390
+
391
+ // 先查找 domain 目录
392
+ for (const item of items) {
393
+ const itemPath = path.join(dir, item);
394
+ try {
395
+ const stat = fs.statSync(itemPath);
396
+
397
+ if (stat.isDirectory() && item.toLowerCase() === 'domain') {
398
+ const newParts = [...packageParts, item];
399
+ // 在 domain 目录中查找 Java 文件来确定完整包路径
400
+ const found = findDomainPackage(itemPath, newParts);
401
+ if (found) return found;
402
+ }
403
+ } catch (e) {
404
+ // 忽略错误,继续查找
405
+ }
406
+ }
407
+
408
+ // 如果当前目录有 Java 文件,返回包路径
409
+ for (const item of items) {
410
+ const itemPath = path.join(dir, item);
411
+ try {
412
+ const stat = fs.statSync(itemPath);
413
+ if (stat.isFile() && item.endsWith('.java')) {
414
+ return packageParts.length > 0 ? packageParts.join('.') : null;
415
+ }
416
+ } catch (e) {
417
+ // 忽略错误
418
+ }
419
+ }
420
+
421
+ // 递归查找子目录
422
+ for (const item of items) {
423
+ const itemPath = path.join(dir, item);
424
+ try {
425
+ const stat = fs.statSync(itemPath);
426
+ if (stat.isDirectory()) {
427
+ const newParts = [...packageParts, item];
428
+ const found = findDomainPackage(itemPath, newParts);
429
+ if (found) return found;
430
+ }
431
+ } catch (e) {
432
+ // 忽略错误
433
+ }
434
+ }
435
+
436
+ return null;
437
+ };
438
+
439
+ return findDomainPackage(javaSrcPath);
440
+ };
441
+
442
+ /**
443
+ * 生成 Java Entity 类
444
+ */
445
+ const _generateEntityClass = (spec, fields, packageName) => {
446
+ const className = spec.java;
447
+ const tableName = spec.table;
448
+
449
+ // 收集所有需要的导入
450
+ const imports = new Set([
451
+ 'com.baomidou.mybatisplus.annotation.TableField',
452
+ 'com.baomidou.mybatisplus.annotation.TableName',
453
+ 'io.r2mo.dbe.mybatisplus.core.domain.BaseEntity',
454
+ 'io.r2mo.dbe.mybatisplus.core.typehandler.TypedUUIDHandler',
455
+ 'io.swagger.v3.oas.annotations.media.Schema',
456
+ 'lombok.Data',
457
+ 'lombok.EqualsAndHashCode'
458
+ ]);
459
+
460
+ // 添加字段需要的导入
461
+ fields.forEach(field => {
462
+ field.imports.forEach(imp => imports.add(imp));
463
+ });
464
+
465
+ // 检查是否需要 BigDecimal
466
+ if (fields.some(f => f.type === 'BigDecimal')) {
467
+ imports.add('java.math.BigDecimal');
468
+ }
469
+
470
+ // 检查是否需要 UUID
471
+ if (fields.some(f => f.type === 'UUID')) {
472
+ imports.add('java.util.UUID');
473
+ }
474
+
475
+ // 检查是否需要 LocalDateTime
476
+ if (fields.some(f => f.type === 'LocalDateTime')) {
477
+ imports.add('java.time.LocalDateTime');
478
+ }
479
+
480
+ // 生成导入语句
481
+ const importStatements = Array.from(imports).sort().map(imp => `import ${imp};`).join('\n');
482
+
483
+ // 生成字段代码
484
+ const fieldStatements = fields.map(field => {
485
+ const comment = field.comment ? `\n /**\n * ${field.comment}\n */` : '';
486
+ // Schema description 使用提取的中文注释,如果没有则使用字段的 Java 名称
487
+ const schemaDescription = field.comment || field.javaName;
488
+ const schemaAnnotation = ` @Schema(description = "${schemaDescription}")`;
489
+
490
+ // 生成 TableField 注解,设置列名 value 属性
491
+ let tableFieldAnnotation = '';
492
+ const tableFieldValue = `value = "${field.name}"`;
493
+
494
+ if (field.useTypedUUID) {
495
+ tableFieldAnnotation = `\n @TableField(${tableFieldValue}, typeHandler = TypedUUIDHandler.class)`;
496
+ } else if (field.existFalse) {
497
+ tableFieldAnnotation = `\n @TableField(${tableFieldValue}, exist = false)`;
498
+ } else {
499
+ tableFieldAnnotation = `\n @TableField(${tableFieldValue})`;
500
+ }
501
+
502
+ const fieldDef = ` private ${field.type} ${field.javaName};`;
503
+
504
+ const annotations = [schemaAnnotation, tableFieldAnnotation];
505
+
506
+ return `${comment}\n${annotations.join('')}\n${fieldDef}`;
507
+ }).join('\n\n');
508
+
509
+ // 获取当前日期
510
+ const now = new Date();
511
+ const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
512
+
513
+ // 生成完整的类代码
514
+ // 类级别的 @Schema 使用 alias
515
+ const schemaName = spec.alias || spec.name;
516
+ const classCode = `package ${packageName};
517
+
518
+ ${importStatements}
519
+
520
+ /**
521
+ * @author lang : ${dateStr}
522
+ */
523
+ @Data
524
+ @EqualsAndHashCode(callSuper = true)
525
+ @TableName("${tableName}")
526
+ @Schema(name = "${schemaName}")
527
+ public class ${className} extends BaseEntity {
528
+
529
+ ${fieldStatements}
530
+ }
531
+ `;
532
+
533
+ return classCode;
534
+ };
535
+
536
+ /**
537
+ * 读取 metadata.json 文件
538
+ */
539
+ const _loadMetadata = (repoPath) => {
540
+ const metadataPath = path.join(repoPath, 'metadata', 'io', 'metadata', 'domain', 'metadata.json');
541
+ if (!exists(metadataPath)) {
542
+ return {};
543
+ }
544
+
545
+ try {
546
+ const content = fs.readFileSync(metadataPath, 'utf8');
547
+ return JSON.parse(content);
548
+ } catch (error) {
549
+ Ec.warn(`⚠ 无法解析 metadata.json: ${error.message}`);
550
+ return {};
551
+ }
552
+ };
553
+
554
+ /**
555
+ * 获取目录的备注信息
556
+ */
557
+ const _getDirectoryNote = (dirName, metadata) => {
558
+ if (!metadata || typeof metadata !== 'object') {
559
+ return '';
560
+ }
561
+
562
+ // 尝试多种可能的键名格式
563
+ const possibleKeys = [
564
+ dirName,
565
+ dirName.toLowerCase(),
566
+ dirName.toUpperCase(),
567
+ path.basename(dirName)
568
+ ];
569
+
570
+ for (const key of possibleKeys) {
571
+ if (metadata[key] && typeof metadata[key] === 'object' && metadata[key].note) {
572
+ return metadata[key].note;
573
+ }
574
+ if (metadata[key] && typeof metadata[key] === 'string') {
575
+ return metadata[key];
576
+ }
577
+ }
578
+
579
+ return '';
580
+ };
581
+
582
+ /**
583
+ * 按目录组织 MD 文件
584
+ */
585
+ const _organizeByDirectory = (files, metadata = {}) => {
586
+ const dirMap = new Map();
587
+
588
+ for (const file of files) {
589
+ const dir = path.dirname(file);
590
+ if (!dirMap.has(dir)) {
591
+ dirMap.set(dir, []);
592
+ }
593
+ dirMap.get(dir).push(file);
594
+ }
595
+
596
+ return Array.from(dirMap.entries()).map(([dir, files]) => {
597
+ const dirName = path.basename(dir);
598
+ const note = _getDirectoryNote(dirName, metadata);
599
+ return {
600
+ name: dirName,
601
+ path: dir,
602
+ files: files,
603
+ note: note
604
+ };
605
+ });
606
+ };
607
+
608
+ /**
609
+ * 显示规范列表表格(四列:identifier, name, table, alias)
610
+ */
611
+ const _displaySpecsTable = (specs) => {
612
+ if (specs.length === 0) {
613
+ return;
614
+ }
615
+
616
+ // 固定列宽
617
+ const identifierWidth = 20;
618
+ const nameWidth = 25;
619
+ const tableWidth = 25;
620
+ const aliasWidth = 20;
621
+
622
+ // 计算实际需要的宽度(用于截断过长的内容)
623
+ const maxIdentifierLen = Math.max('Identifier'.length, ...specs.map(s => (s.identifier || '').length));
624
+ const maxNameLen = Math.max('Name'.length, ...specs.map(s => (s.name || '').length));
625
+ const maxTableLen = Math.max('Table'.length, ...specs.map(s => (s.table || '').length));
626
+ const maxAliasLen = Math.max('Alias'.length, ...specs.map(s => (s.alias || '').length));
627
+
628
+ // 表头
629
+ console.log('');
630
+ console.log('─'.repeat(95));
631
+ console.log(' 规范列表'.green.bold);
632
+ console.log('─'.repeat(95));
633
+
634
+ // 表头行
635
+ const header = ` ${'Identifier'.padEnd(identifierWidth)}│${'Name'.padEnd(nameWidth)}│${'Table'.padEnd(tableWidth)}│${'Alias'.padEnd(aliasWidth)}`;
636
+ console.log(header.cyan);
637
+ console.log('─'.repeat(95));
638
+
639
+ // 数据行
640
+ specs.forEach((spec, index) => {
641
+ const identifier = (spec.identifier || '').substring(0, identifierWidth).padEnd(identifierWidth);
642
+ const name = (spec.name || '').substring(0, nameWidth).padEnd(nameWidth);
643
+ const table = (spec.table || '').substring(0, tableWidth).padEnd(tableWidth);
644
+ const alias = (spec.alias || '').substring(0, aliasWidth).padEnd(aliasWidth);
645
+ console.log(` ${identifier}│${name}│${table}│${alias}`);
646
+ });
647
+
648
+ console.log('─'.repeat(95));
649
+ console.log('');
650
+ };
651
+
652
+ module.exports = async (options) => {
653
+ try {
654
+ const projectDir = process.cwd();
655
+
656
+ // 1. 检查 git 命令
657
+ if (!_isCommandAvailable('git')) {
658
+ Ec.error('❌ 未找到 git 命令');
659
+ Ec.waiting('请先安装 Git');
660
+ process.exit(1);
661
+ }
662
+
663
+ // 2. 确保 .r2mo/repo 在 .gitignore 中
664
+ const gitignorePath = path.join(projectDir, '.gitignore');
665
+ const ignoreEntry = '.r2mo/repo';
666
+ if (exists(gitignorePath)) {
667
+ const content = fs.readFileSync(gitignorePath, 'utf8');
668
+ const lines = content.split('\n');
669
+ const hasEntry = lines.some(line => line.trim() === ignoreEntry);
670
+ if (!hasEntry) {
671
+ const newContent = content.endsWith('\n') || content === ''
672
+ ? content + ignoreEntry + '\n'
673
+ : content + '\n' + ignoreEntry + '\n';
674
+ fs.writeFileSync(gitignorePath, newContent);
675
+ }
676
+ } else {
677
+ fs.writeFileSync(gitignorePath, ignoreEntry + '\n');
678
+ }
679
+
680
+ // 3. 克隆或更新仓库
681
+ const repoPath = await _cloneOrUpdateRepository(projectDir);
682
+
683
+ // 3. 扫描所有 MD 文件
684
+ Ec.waiting('正在扫描 Markdown 文件...');
685
+ const allMdFiles = _scanMarkdownFiles(repoPath);
686
+
687
+ if (allMdFiles.length === 0) {
688
+ Ec.error('❌ 未找到任何 Markdown 文件');
689
+ process.exit(1);
690
+ }
691
+
692
+ Ec.info(`✓ 找到 ${allMdFiles.length} 个 Markdown 文件`);
693
+
694
+ // 4. 解析所有 MD 文件
695
+ Ec.waiting('正在解析文件...');
696
+ const specs = [];
697
+ for (const file of allMdFiles) {
698
+ const spec = _parseMarkdownFile(file);
699
+ if (spec) {
700
+ specs.push(spec);
701
+ }
702
+ }
703
+
704
+ if (specs.length === 0) {
705
+ Ec.error('❌ 未找到有效的规范文件(需要包含 name, java, table 字段)');
706
+ process.exit(1);
707
+ }
708
+
709
+ Ec.info(`✓ 找到 ${specs.length} 个有效规范`);
710
+
711
+ // 5. 加载 metadata.json
712
+ const metadata = _loadMetadata(repoPath);
713
+
714
+ // 6. 按目录组织并显示目录清单
715
+ const dirs = _organizeByDirectory(specs.map(s => s.filePath), metadata);
716
+
717
+ // 显示目录表格(包含备注)
718
+ console.log('');
719
+ console.log('─'.repeat(100));
720
+ console.log(' 目录列表'.green.bold);
721
+ console.log('─'.repeat(100));
722
+
723
+ const nameWidth = Math.max('目录名'.length, ...dirs.map(d => d.name.length));
724
+ const fileCountWidth = Math.max('文件数'.length, 8);
725
+ const noteWidth = 40;
726
+
727
+ const header = ` ${'目录名'.padEnd(nameWidth)}│${'文件数'.padEnd(fileCountWidth)}│${'备注'.padEnd(noteWidth)}`;
728
+ console.log(header.cyan);
729
+ console.log('─'.repeat(100));
730
+
731
+ dirs.forEach((dir, idx) => {
732
+ const name = dir.name.padEnd(nameWidth);
733
+ const fileCount = `${dir.files.length} 个文件`.padEnd(fileCountWidth);
734
+ const note = (dir.note || '').substring(0, noteWidth).padEnd(noteWidth);
735
+ console.log(` ${name}│${fileCount}│${note}`);
736
+ });
737
+
738
+ console.log('─'.repeat(100));
739
+ console.log('');
740
+
741
+ const dirItems = dirs.map((dir, idx) => ({
742
+ name: `${dir.name.padEnd(nameWidth)}│${`${dir.files.length} 个文件`.padEnd(fileCountWidth)}│${(dir.note || '').substring(0, noteWidth).padEnd(noteWidth)}`,
743
+ description: '',
744
+ index: idx,
745
+ dir: dir
746
+ }));
747
+
748
+ // 一级菜单:选择目录(组)
749
+ const selectedDir = await selectSingle(dirItems, '请选择目录(组)');
750
+ if (selectedDir === null || selectedDir === undefined) {
751
+ Ec.warn('已取消操作');
752
+ process.exit(0);
753
+ }
754
+
755
+ // 获取该目录下的所有规范
756
+ const dirSpecs = dirs[selectedDir.index].files.map(file =>
757
+ specs.find(s => s.filePath === file)
758
+ ).filter(Boolean);
759
+
760
+ if (dirSpecs.length === 0) {
761
+ Ec.warn('该目录下没有有效的规范文件');
762
+ process.exit(0);
763
+ }
764
+
765
+ // 二级菜单:显示该组中的所有模型,让用户选择
766
+ _displaySpecsTable(dirSpecs);
767
+
768
+ const specItems = dirSpecs.map((spec, idx) => ({
769
+ name: `${(spec.identifier || '').padEnd(20)}│${(spec.name || '').padEnd(25)}│${(spec.table || '').padEnd(25)}│${(spec.alias || '').padEnd(20)}`,
770
+ description: '',
771
+ index: idx
772
+ }));
773
+
774
+ const selected = await selectMultiple(specItems, `请选择要安装的规范(可多选)- 组: ${dirs[selectedDir.index].name}`);
775
+
776
+ // 处理退出情况
777
+ if (!selected || (selected.indices && selected.indices.length === 0) ||
778
+ (Array.isArray(selected) && selected.length === 0)) {
779
+ Ec.warn('已取消操作');
780
+ process.exit(0);
781
+ }
782
+
783
+ // 处理返回格式
784
+ let selectedSpecs = [];
785
+ if (selected.indices && Array.isArray(selected.indices)) {
786
+ selectedSpecs = selected.indices.map(idx => dirSpecs[idx]).filter(Boolean);
787
+ } else if (Array.isArray(selected)) {
788
+ selectedSpecs = selected.map(item => {
789
+ if (typeof item === 'object' && item.index !== undefined) {
790
+ return dirSpecs[item.index];
791
+ }
792
+ return null;
793
+ }).filter(Boolean);
794
+ } else {
795
+ Ec.error('❌ 无效的选择结果');
796
+ process.exit(1);
797
+ }
798
+
799
+ Ec.info(`✓ 已选择 ${selectedSpecs.length} 个规范`);
800
+
801
+ // 6. 解析 pom.xml 获取 artifactId
802
+ const pomPath = path.join(projectDir, 'pom.xml');
803
+ if (!exists(pomPath)) {
804
+ Ec.error('❌ 当前目录未找到 pom.xml');
805
+ Ec.waiting('请确保在 Maven 项目根目录执行此命令');
806
+ process.exit(1);
807
+ }
808
+
809
+ const artifactId = _parsePomXml(pomPath);
810
+ if (!artifactId) {
811
+ Ec.error('❌ 无法从 pom.xml 中提取 artifactId');
812
+ process.exit(1);
813
+ }
814
+
815
+ Ec.info(`✓ 项目 ID: ${artifactId}`);
816
+
817
+ // 7. 检查 domain 模块
818
+ const domainModulePath = path.join(projectDir, `${artifactId}-domain`);
819
+ if (!exists(domainModulePath)) {
820
+ Ec.error(`❌ 未找到 ${artifactId}-domain 模块`);
821
+ Ec.waiting('请先创建 domain 模块');
822
+ process.exit(1);
823
+ }
824
+
825
+ // 8. 查找 domain 包
826
+ const packageName = _findJavaPackage(domainModulePath);
827
+ if (!packageName) {
828
+ Ec.error('❌ 未找到 domain 包');
829
+ Ec.waiting('请先创建 domain 包结构');
830
+ process.exit(1);
831
+ }
832
+
833
+ const domainPackagePath = packageName.replace(/\./g, path.sep);
834
+ const domainJavaPath = path.join(domainModulePath, 'src', 'main', 'java', domainPackagePath);
835
+
836
+ if (!exists(domainJavaPath)) {
837
+ Ec.error(`❌ 未找到 domain 包目录: ${domainJavaPath}`);
838
+ Ec.waiting('请先创建 domain 包目录');
839
+ process.exit(1);
840
+ }
841
+
842
+ Ec.info(`✓ 找到 domain 包: ${packageName}`);
843
+
844
+ // 9. 处理每个选中的规范
845
+ for (const spec of selectedSpecs) {
846
+ Ec.waiting(`正在处理: ${spec.name} (${spec.java})...`);
847
+
848
+ // 从 SQL 中提取字段
849
+ const fields = _extractFieldsFromSql(spec.body, spec.table);
850
+
851
+ if (fields.length === 0) {
852
+ Ec.warn(`⚠ ${spec.name} 未找到字段信息,跳过`);
853
+ continue;
854
+ }
855
+
856
+ // 生成 Entity 类
857
+ const classCode = _generateEntityClass(spec, fields, packageName);
858
+
859
+ // 写入文件
860
+ const javaFilePath = path.join(domainJavaPath, `${spec.java}.java`);
861
+ const fileExists = exists(javaFilePath);
862
+
863
+ await fsAsync.writeFile(javaFilePath, classCode, 'utf8');
864
+
865
+ if (fileExists) {
866
+ Ec.info(`✓ 已覆盖: ${spec.java}.java`.yellow);
867
+ } else {
868
+ Ec.info(`✓ 已生成: ${spec.java}.java`.green);
869
+ }
870
+ }
871
+
872
+ Ec.info('🎉 所有规范处理完成!');
873
+ process.exit(0);
874
+
875
+ } catch (error) {
876
+ Ec.error(`❌ 执行失败: ${error.message}`);
877
+ console.error(error);
878
+ process.exit(1);
879
+ }
880
+ };