momo-ai 1.0.0 → 1.0.2

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 (107) hide show
  1. package/README.md +117 -0
  2. package/package.json +1 -1
  3. package/src/_template/LAIN/.momo/advanced/actor.md +42 -0
  4. package/src/_template/LAIN/.momo/scripts/submodule-clean.sh +56 -0
  5. package/src/_template/LAIN/AGENTS.md +103 -0
  6. package/src/_template/LAIN/changes/proposal.md +39 -0
  7. package/src/_template/LAIN/changes/tasks/task-detail.md +45 -0
  8. package/src/_template/LAIN/changes/tasks.md +49 -0
  9. package/src/_template/LAIN/execute/admin-n-f-dashboard.md +53 -0
  10. package/src/_template/LAIN/execute/admin-n-f-form.md +51 -0
  11. package/src/_template/LAIN/execute/admin-n-f-home.md +49 -0
  12. package/src/_template/LAIN/execute/admin-n-f-list.md +52 -0
  13. package/src/_template/LAIN/execute/admin-n-f-login.md +56 -0
  14. package/src/_template/LAIN/specification/project-model.md +13 -0
  15. package/src/_template/LAIN/specification/project.md +73 -0
  16. package/src/_template/LAIN/specification/requirement.md +25 -0
  17. package/src/_template/PROMPT/add.md.ejs +5 -0
  18. package/src/_template/PROMPT/plan.md.ejs +5 -0
  19. package/src/_template/PROMPT/run.md.ejs +9 -0
  20. package/src/_template/PROMPT/tasks.md.ejs +5 -0
  21. package/src/commander/actor.json +12 -0
  22. package/src/commander/actors.json +6 -0
  23. package/src/commander/add.json +12 -0
  24. package/src/commander/archive.json +12 -0
  25. package/src/commander/env.json +7 -0
  26. package/src/commander/help.json +7 -0
  27. package/src/commander/init.json +13 -0
  28. package/src/commander/list.json +7 -0
  29. package/src/commander/open.json +7 -0
  30. package/src/commander/plan.json +12 -0
  31. package/src/commander/repo.json +18 -0
  32. package/src/commander/run.json +18 -0
  33. package/src/commander/show.json +12 -0
  34. package/src/commander/tasks.json +18 -0
  35. package/src/commander/unlock.json +6 -0
  36. package/src/commander/validate.json +12 -0
  37. package/src/epic/history/ai.economy.impl.fn.excel.js +148 -0
  38. package/src/epic/history/ai.economy.impl.fn.execute.js +117 -0
  39. package/src/epic/history/ai.economy.impl.fn.java.js +140 -0
  40. package/src/epic/history/ai.economy.impl.fn.json.js +23 -0
  41. package/src/epic/history/ai.economy.impl.fn.plugin.js +160 -0
  42. package/src/epic/history/ai.economy.impl.fn.react.js +219 -0
  43. package/src/epic/history/ai.export.impl.fn.parse.js +345 -0
  44. package/src/epic/history/ai.export.impl.fn.seek.js +42 -0
  45. package/src/epic/history/ai.export.interface.fn.string.js +5 -0
  46. package/src/epic/history/ai.export.interface.io.js +69 -0
  47. package/src/epic/history/ai.export.interface.util.js +17 -0
  48. package/src/epic/history/ai.parse.metadata.js +94 -0
  49. package/src/epic/history/ai.path.fn.dir.operation.js +87 -0
  50. package/src/epic/history/ai.path.fn.io.command.js +43 -0
  51. package/src/epic/history/ai.path.fn.io.specification.js +59 -0
  52. package/src/epic/history/ai.path.fn.io.typed.js +63 -0
  53. package/src/epic/history/ai.path.fn.out.content.js +47 -0
  54. package/src/epic/history/ai.string.fn.str.util.js +63 -0
  55. package/src/epic/history/ai.uncork.fn.element.feature.js +52 -0
  56. package/src/epic/history/ai.uncork.fn.it.feature.js +118 -0
  57. package/src/epic/history/ai.uncork.fn.to.typed.js +74 -0
  58. package/src/epic/history/ai.under.fn.cx.evaluate.js +81 -0
  59. package/src/epic/history/ai.under.fn.fx.terminal.js +143 -0
  60. package/src/epic/history/ai.unified.fn.fn.error.code.js +108 -0
  61. package/src/epic/history/ai.unified.fn.is.decision.js +20 -0
  62. package/src/epic/history/ai.unified.fn.sorter.element.js +26 -0
  63. package/src/epic/history/zero.__.fn.find.util.js +37 -0
  64. package/src/epic/history/zero.__.v.constant.js +5 -0
  65. package/src/epic/index.js +50 -0
  66. package/src/epic/lain.fn.execute.js +116 -0
  67. package/src/epic/lain.fn.parse.js +94 -0
  68. package/src/epic/momo.fn.cx.js +81 -0
  69. package/src/epic/momo.fn.dir.js +87 -0
  70. package/src/epic/momo.fn.element.js +52 -0
  71. package/src/epic/momo.fn.find.js +37 -0
  72. package/src/epic/momo.fn.fx.js +143 -0
  73. package/src/epic/momo.fn.io.js +157 -0
  74. package/src/epic/momo.fn.is.js +20 -0
  75. package/src/epic/momo.fn.it.js +118 -0
  76. package/src/epic/momo.fn.log.js +50 -0
  77. package/src/epic/momo.fn.out.js +47 -0
  78. package/src/epic/momo.fn.sorter.js +26 -0
  79. package/src/epic/momo.fn.str.js +63 -0
  80. package/src/epic/momo.fn.to.js +74 -0
  81. package/src/epic/momo.v.constant.js +5 -0
  82. package/src/epic/momo.v.errorcode.js +108 -0
  83. package/src/executor/executeActor.js +113 -0
  84. package/src/executor/executeActors.js +58 -0
  85. package/src/executor/executeAdd.js +248 -0
  86. package/src/executor/executeArchive.js +124 -0
  87. package/src/executor/executeEnv.js +158 -0
  88. package/src/executor/executeHelp.js +23 -0
  89. package/src/executor/executeInit.js +321 -0
  90. package/src/executor/executeList.js +111 -0
  91. package/src/executor/executeOpen.js +150 -0
  92. package/src/executor/executePlan.js +134 -0
  93. package/src/executor/executeRepo.js +234 -0
  94. package/src/executor/executeRun.js +527 -0
  95. package/src/executor/executeShow.js +164 -0
  96. package/src/executor/executeTasks.js +323 -0
  97. package/src/executor/executeUnlock.js +110 -0
  98. package/src/executor/executeValidate.js +210 -0
  99. package/src/executor/index.js +35 -0
  100. package/src/momo.js +39 -1
  101. package/.idea/copilot.data.migration.agent.xml +0 -6
  102. package/.idea/copilot.data.migration.ask.xml +0 -6
  103. package/.idea/copilot.data.migration.ask2agent.xml +0 -6
  104. package/.idea/copilot.data.migration.edit.xml +0 -6
  105. package/.idea/modules.xml +0 -8
  106. package/.idea/r2mo-lain.iml +0 -12
  107. package/.idea/vcs.xml +0 -6
@@ -0,0 +1,111 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const Ec = require('../epic');
4
+
5
+ /**
6
+ * 从 Markdown 文件中提取编号信息
7
+ * @param {string} filePath 文件路径
8
+ * @returns {string} 编号信息或 'N/A'
9
+ */
10
+ const _extractId = (filePath) => {
11
+ try {
12
+ if (!fs.existsSync(filePath)) {
13
+ return 'N/A';
14
+ }
15
+
16
+ const content = fs.readFileSync(filePath, 'utf8');
17
+
18
+ // 首先尝试查找 <!-- CODE: XXX --> 格式
19
+ const codeMatch = content.match(/<!--\s*CODE:\s*(.+?)\s*-->/i);
20
+ if (codeMatch) {
21
+ return codeMatch[1].trim();
22
+ }
23
+
24
+ // 如果没有找到CODE格式,尝试查找 "- 编号:" 格式
25
+ const lines = content.split('\n');
26
+ for (const line of lines) {
27
+ const idMatch = line.match(/-\s*编号[::]\s*(.+)/);
28
+ if (idMatch) {
29
+ return idMatch[1].trim();
30
+ }
31
+ }
32
+
33
+ return 'N/A';
34
+ } catch (error) {
35
+ return 'N/A';
36
+ }
37
+ };
38
+
39
+ /**
40
+ * 统计任务数量
41
+ * @param {string} tasksFile 任务文件路径
42
+ * @returns {number} 任务数量
43
+ */
44
+ const _countTasks = (tasksFile) => {
45
+ try {
46
+ if (!fs.existsSync(tasksFile)) {
47
+ return 0;
48
+ }
49
+
50
+ const content = fs.readFileSync(tasksFile, 'utf8');
51
+ const lines = content.split('\n');
52
+ let count = 0;
53
+
54
+ // 使用更直接的方法匹配任务
55
+ for (const line of lines) {
56
+ // 匹配任务格式,例如: - [ ] [任务1简要描述](tasks/M01-T001-01.md)
57
+ if (line.trim().startsWith('- [') && line.includes('] [') && line.includes('](') && line.includes('tasks/')) {
58
+ count++;
59
+ }
60
+ }
61
+
62
+ return count;
63
+ } catch (error) {
64
+ return 0;
65
+ }
66
+ };
67
+
68
+ module.exports = (options) => {
69
+ try {
70
+ // 构建路径
71
+ const projectDir = process.cwd();
72
+ const changesDir = path.join(projectDir, 'specification', 'changes');
73
+
74
+ // 检查 changes 目录是否存在
75
+ if (!fs.existsSync(changesDir)) {
76
+ Ec.waiting('未找到任何需求');
77
+ process.exit(0);
78
+ }
79
+
80
+ // 读取所有需求目录
81
+ const requirements = fs.readdirSync(changesDir)
82
+ .filter(item => {
83
+ const itemPath = path.join(changesDir, item);
84
+ return fs.statSync(itemPath).isDirectory();
85
+ });
86
+
87
+ if (requirements.length === 0) {
88
+ Ec.waiting('未找到任何需求');
89
+ process.exit(0);
90
+ }
91
+
92
+ // 显示表头
93
+ Ec.waiting('编号\t\t名称\t\t\t任务数量');
94
+ Ec.waiting('------------------------------------------------');
95
+
96
+ // 显示需求列表
97
+ requirements.forEach((requirement) => {
98
+ const proposalFile = path.join(changesDir, requirement, 'proposal.md');
99
+ const tasksFile = path.join(changesDir, requirement, 'tasks.md');
100
+ const id = _extractId(proposalFile);
101
+ const taskCount = _countTasks(tasksFile);
102
+ Ec.waiting(`${id}\t\t${requirement}\t\t\t${taskCount}`);
103
+ });
104
+
105
+ Ec.info(`✅ 共找到 ${requirements.length} 个需求`);
106
+ process.exit(0);
107
+ } catch (error) {
108
+ Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
109
+ process.exit(1);
110
+ }
111
+ };
@@ -0,0 +1,150 @@
1
+ const Ec = require('../epic');
2
+ const { spawn } = require('child_process');
3
+
4
+ module.exports = async (options) => {
5
+ // 参数提取
6
+ const parsed = Ec.parseArgument(options);
7
+
8
+ try {
9
+ // 定义可用的AI工具
10
+ const aiTools = [
11
+ { name: 'Trae', command: 'trae' },
12
+ { name: 'Cursor', command: 'cursor' },
13
+ { name: 'Lingma', command: 'lingma' },
14
+ { name: 'Kiro', command: 'kiro' }
15
+ ];
16
+
17
+ // 检查哪些工具可用
18
+ const availableTools = [];
19
+ for (const tool of aiTools) {
20
+ if (await _isCommandAvailable(tool.command)) {
21
+ availableTools.push(tool);
22
+ }
23
+ }
24
+
25
+ // 如果没有可用工具,提示用户安装
26
+ if (availableTools.length === 0) {
27
+ Ec.error('❌ 未找到可用的AI工具,请确保已安装以下工具之一:Trae、Cursor、Lingma、Kiro');
28
+ Ec.askClose();
29
+ process.exit(1);
30
+ }
31
+
32
+ // 如果只有一个工具可用,直接使用它
33
+ if (availableTools.length === 1) {
34
+ const tool = availableTools[0];
35
+ let toolDisplayName = tool.name;
36
+ if (tool.name === 'Trae' || tool.name === 'Cursor') {
37
+ toolDisplayName = `${tool.name}(${'Multi-Agent'.cyan})`;
38
+ }
39
+ if (tool.name === 'Trae') {
40
+ Ec.waiting(`🔍 检测到可用工具: ${toolDisplayName.green}`);
41
+ } else {
42
+ Ec.waiting(`🔍 检测到可用工具: ${toolDisplayName}`);
43
+ }
44
+ await _openWithTool(tool.command);
45
+ Ec.askClose();
46
+ process.exit(0);
47
+ }
48
+
49
+ // 显示交互式选择菜单
50
+ Ec.waiting('🔍 检测到多个可用的AI工具,请选择要使用的工具:');
51
+ const choices = availableTools.map((tool, index) => {
52
+ let toolDisplayName = tool.name;
53
+ if (tool.name === 'Trae' || tool.name === 'Cursor') {
54
+ toolDisplayName = `${tool.name}(${'Multi-Agent'.cyan})`;
55
+ }
56
+ if (tool.name === 'Trae') {
57
+ return `${index + 1}. ${toolDisplayName} ${'推荐'.green}`;
58
+ } else {
59
+ return `${index + 1}. ${toolDisplayName}`;
60
+ }
61
+ });
62
+
63
+ // 添加退出选项
64
+ choices.push(`${choices.length + 1}. 退出`);
65
+
66
+ for (const choice of choices) {
67
+ Ec.waiting(choice);
68
+ }
69
+
70
+ // 获取用户选择
71
+ const answer = await Ec.ask('请输入选项编号: ');
72
+ const selectedIndex = parseInt(answer) - 1;
73
+
74
+ // 检查用户选择
75
+ if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= availableTools.length) {
76
+ if (selectedIndex === availableTools.length) {
77
+ Ec.waiting('👋 已取消操作');
78
+ Ec.askClose();
79
+ process.exit(0);
80
+ }
81
+ Ec.error('❌ 无效的选择');
82
+ Ec.askClose();
83
+ process.exit(1);
84
+ }
85
+
86
+ // 执行选择的工具
87
+ const selectedTool = availableTools[selectedIndex];
88
+ let toolDisplayName = selectedTool.name;
89
+ if (selectedTool.name === 'Trae' || selectedTool.name === 'Cursor') {
90
+ toolDisplayName = `${selectedTool.name}(${'Multi-Agent'.cyan})`;
91
+ }
92
+ Ec.waiting(`🚀 正在使用 ${toolDisplayName} 打开项目...`);
93
+ await _openWithTool(selectedTool.command);
94
+ Ec.askClose();
95
+ process.exit(0);
96
+
97
+ } catch (error) {
98
+ Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
99
+ Ec.askClose();
100
+ process.exit(1);
101
+ }
102
+ };
103
+
104
+ /**
105
+ * 检查命令是否可用
106
+ * @param {string} command 命令名称
107
+ * @returns {Promise<boolean>} 命令是否可用
108
+ */
109
+ const _isCommandAvailable = async (command) => {
110
+ return new Promise((resolve) => {
111
+ const process = spawn('which', [command]);
112
+ process.on('close', (code) => {
113
+ resolve(code === 0);
114
+ });
115
+ });
116
+ };
117
+
118
+ /**
119
+ * 使用指定工具打开当前目录
120
+ * @param {string} tool 工具命令
121
+ */
122
+ const _openWithTool = async (tool) => {
123
+ return new Promise((resolve, reject) => {
124
+ // 使用工具命令打开当前目录
125
+ const child = spawn(tool, ['.'], {
126
+ stdio: 'inherit',
127
+ cwd: process.cwd()
128
+ });
129
+
130
+ child.on('close', (code) => {
131
+ if (code === 0) {
132
+ Ec.info(`✅ 项目已成功在 ${tool} 中打开`);
133
+ resolve();
134
+ } else {
135
+ Ec.error(`❌ ${tool} 执行失败,退出码: ${code}`);
136
+ reject(new Error(`${tool} 执行失败,退出码: ${code}`));
137
+ }
138
+ });
139
+
140
+ child.on('error', (error) => {
141
+ if (error.code === 'ENOENT') {
142
+ Ec.error(`❌ 未找到命令: ${tool},请确保已正确安装`);
143
+ reject(new Error(`未找到命令: ${tool},请确保已正确安装`));
144
+ } else {
145
+ Ec.error(`❌ 执行 ${tool} 时发生错误: ${error.message}`);
146
+ reject(error);
147
+ }
148
+ });
149
+ });
150
+ };
@@ -0,0 +1,134 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+ const Ec = require('../epic');
5
+ const fsAsync = require('fs').promises;
6
+
7
+ /**
8
+ * 读取模板文件并提取 <!-- BEGIN --> 到 <!-- END --> 之间的内容
9
+ * @param {string} templatePath 模板文件路径
10
+ * @returns {Promise<string>} 提取的模板内容
11
+ */
12
+ const _readTemplate = async (templatePath) => {
13
+ try {
14
+ const content = await fsAsync.readFile(templatePath, 'utf8');
15
+ const lines = content.split('\n');
16
+ let beginIndex = -1;
17
+ let endIndex = -1;
18
+
19
+ for (let i = 0; i < lines.length; i++) {
20
+ if (lines[i].includes('<!-- BEGIN -->')) {
21
+ beginIndex = i;
22
+ } else if (lines[i].includes('<!-- END -->')) {
23
+ endIndex = i;
24
+ break;
25
+ }
26
+ }
27
+
28
+ if (beginIndex !== -1 && endIndex !== -1) {
29
+ // 提取 BEGIN 和 END 之间的内容(不包括 BEGIN 和 END 行)
30
+ return lines.slice(beginIndex + 1, endIndex).join('\n');
31
+ }
32
+
33
+ return content;
34
+ } catch (error) {
35
+ Ec.waiting(`读取模板文件失败: ${error.message}`);
36
+ throw error;
37
+ }
38
+ };
39
+
40
+ /**
41
+ * 渲染 EJS 模板
42
+ * @param {string} template 模板内容
43
+ * @param {Object} data 数据对象
44
+ * @returns {string} 渲染后的内容
45
+ */
46
+ const _renderTemplate = (template, data) => {
47
+ let result = template;
48
+ for (const key in data) {
49
+ if (data.hasOwnProperty(key)) {
50
+ const regex = new RegExp(`<%=\\s*${key}\\s*%>`, 'g');
51
+ result = result.replace(regex, data[key]);
52
+ }
53
+ }
54
+ return result;
55
+ };
56
+
57
+ /**
58
+ * 检查需求目录是否存在
59
+ * @param {string} changesDir changes目录路径
60
+ * @param {string} requirementName 需求名称
61
+ * @returns {boolean} 是否存在
62
+ */
63
+ const _isRequirementExists = (changesDir, requirementName) => {
64
+ const requirementDir = path.join(changesDir, requirementName);
65
+ return fs.existsSync(requirementDir);
66
+ };
67
+
68
+ /**
69
+ * 将内容复制到剪贴板(去除换行符)
70
+ * @param {string} content 要复制的内容
71
+ */
72
+ const _copyToClipboard = async (content) => {
73
+ try {
74
+ // 去除换行符,将内容合并为一行后再复制到剪贴板
75
+ const contentWithoutNewlines = content.replace(/\r?\n|\r/g, ' ');
76
+ const proc = spawn('pbcopy', { stdio: 'pipe' });
77
+ proc.stdin.write(contentWithoutNewlines);
78
+ proc.stdin.end();
79
+ Ec.waiting('✅ 计划提示词已复制到剪贴板');
80
+ } catch (error) {
81
+ Ec.waiting(`复制到剪贴板失败: ${error.message}`);
82
+ // 在非 macOS 系统上可能没有 pbcopy,提供备选方案
83
+ Ec.waiting('计划提示词内容:');
84
+ console.log(content);
85
+ }
86
+ };
87
+
88
+ module.exports = async (options) => {
89
+ // 参数提取
90
+ const parsed = Ec.parseArgument(options);
91
+
92
+ // 获取需求名称
93
+ const requirementName = parsed.name || parsed.n;
94
+
95
+ // 验证参数
96
+ if (!requirementName) {
97
+ Ec.error("❌ 请提供需求名称 (-n, --name)");
98
+ process.exit(1);
99
+ }
100
+
101
+ // 检查需求名称是否包含点号(扩展名)
102
+ if (requirementName.includes('.')) {
103
+ Ec.error("❌ 需求名称不能包含点号(.),以防止与文件扩展名混淆");
104
+ process.exit(1);
105
+ }
106
+
107
+ Ec.waiting(`准备生成需求计划: ${requirementName}`);
108
+
109
+ try {
110
+ // 获取项目路径
111
+ const projectDir = process.cwd();
112
+ const changesDir = path.join(projectDir, 'specification', 'changes');
113
+ const promptTemplatePath = path.join(__dirname, '../_template/PROMPT/plan.md.ejs');
114
+
115
+ // 检查需求是否存在
116
+ if (!_isRequirementExists(changesDir, requirementName)) {
117
+ Ec.error(`❌ 需求 "${requirementName}" 不存在`);
118
+ process.exit(1);
119
+ }
120
+
121
+ // 读取并处理提示词模板
122
+ const templateContent = await _readTemplate(promptTemplatePath);
123
+ const renderedContent = _renderTemplate(templateContent, { NAME: requirementName });
124
+
125
+ // 将提示词复制到剪贴板
126
+ await _copyToClipboard(renderedContent);
127
+
128
+ Ec.info(`✅ 成功生成需求 "${requirementName}" 的计划提示词`);
129
+ process.exit(0);
130
+ } catch (error) {
131
+ Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
132
+ process.exit(1);
133
+ }
134
+ };
@@ -0,0 +1,234 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const {exec} = require('child_process');
4
+ const util = require('util');
5
+ const Ec = require('../epic');
6
+
7
+ // 将 exec 转换为 Promise 版本
8
+ const execAsync = util.promisify(exec);
9
+
10
+ /**
11
+ * 检查当前目录是否为 Git 仓库
12
+ * @returns {Promise<boolean>} 是否为 Git 仓库
13
+ */
14
+ const _isGitRepo = async () => {
15
+ try {
16
+ await execAsync('git rev-parse --git-dir');
17
+ return true;
18
+ } catch (error) {
19
+ return false;
20
+ }
21
+ };
22
+
23
+ /**
24
+ * 初始化当前目录为 Git 仓库
25
+ */
26
+ const _initGitRepo = async () => {
27
+ try {
28
+ Ec.waiting('正在初始化 Git 仓库');
29
+ const command = 'git init -b master';
30
+ await execAsync(command);
31
+ Ec.waiting('✅ 成功初始化 Git 仓库');
32
+ } catch (error) {
33
+ Ec.error(`❌ 初始化 Git 仓库失败: ${error.message}`);
34
+ throw error;
35
+ }
36
+ };
37
+
38
+ /**
39
+ * 添加远程仓库作为 submodule
40
+ * @param {string} repoUrl 远程仓库地址
41
+ * @param {string} targetDir 目标目录(相对路径)
42
+ */
43
+ const _addSubmodule = async (repoUrl, targetDir) => {
44
+ try {
45
+ Ec.waiting(`正在添加 Submodule ${repoUrl} 到 ${targetDir}`);
46
+ // 使用相对路径添加 submodule
47
+ const command = `git submodule add ${repoUrl} ${targetDir}`;
48
+
49
+ // 使用 spawn 实现实时输出
50
+ const {spawn} = require('child_process');
51
+ const gitProcess = spawn('git', ['submodule', 'add', repoUrl, targetDir]);
52
+
53
+ // 实时输出 stdout
54
+ gitProcess.stdout.on('data', (data) => {
55
+ Ec.waiting(`Submodule 添加输出: ${data.toString().trim()}`);
56
+ });
57
+
58
+ // 实时输出 stderr
59
+ gitProcess.stderr.on('data', (data) => {
60
+ Ec.waiting(`Submodule 添加进度: ${data.toString().trim()}`);
61
+ });
62
+
63
+ // 等待进程完成
64
+ await new Promise((resolve, reject) => {
65
+ gitProcess.on('close', (code) => {
66
+ if (code === 0) {
67
+ Ec.waiting(`✅ 成功添加 Submodule 到 ${targetDir}`);
68
+ resolve();
69
+ } else {
70
+ Ec.error(`❌ git submodule add 命令退出码: ${code}`);
71
+ reject(new Error(`git submodule add 命令退出码: ${code}`));
72
+ }
73
+ });
74
+
75
+ gitProcess.on('error', (error) => {
76
+ Ec.error(`❌ 添加 Submodule 失败: ${error.message}`);
77
+ reject(error);
78
+ });
79
+ });
80
+ } catch (error) {
81
+ Ec.error(`❌ 添加 Submodule 失败: ${error.message}`);
82
+ throw error;
83
+ }
84
+ };
85
+
86
+ /**
87
+ * 初始化并更新 submodule
88
+ */
89
+ const _initAndUpdateSubmodules = async () => {
90
+ try {
91
+ Ec.waiting('正在初始化并更新所有 Submodule');
92
+ const command = 'git submodule update --init --recursive';
93
+
94
+ // 使用 spawn 实现实时输出
95
+ const {spawn} = require('child_process');
96
+ const gitProcess = spawn('git', ['submodule', 'update', '--init', '--recursive']);
97
+
98
+ // 实时输出 stdout
99
+ gitProcess.stdout.on('data', (data) => {
100
+ Ec.waiting(`Submodule 初始化输出: ${data.toString().trim()}`);
101
+ });
102
+
103
+ // 实时输出 stderr
104
+ gitProcess.stderr.on('data', (data) => {
105
+ Ec.waiting(`Submodule 初始化进度: ${data.toString().trim()}`);
106
+ });
107
+
108
+ // 等待进程完成
109
+ await new Promise((resolve, reject) => {
110
+ gitProcess.on('close', (code) => {
111
+ if (code === 0) {
112
+ Ec.waiting('✅ 成功初始化并更新所有 Submodule');
113
+ resolve();
114
+ } else {
115
+ Ec.error(`❌ git submodule update 命令退出码: ${code}`);
116
+ reject(new Error(`git submodule update 命令退出码: ${code}`));
117
+ }
118
+ });
119
+
120
+ gitProcess.on('error', (error) => {
121
+ Ec.error(`❌ 初始化并更新 Submodule 失败: ${error.message}`);
122
+ reject(error);
123
+ });
124
+ });
125
+ } catch (error) {
126
+ Ec.error(`❌ 初始化并更新 Submodule 失败: ${error.message}`);
127
+ throw error;
128
+ }
129
+ };
130
+
131
+ /**
132
+ * 创建目录(如果不存在)
133
+ * @param {string} dirPath 目录路径
134
+ */
135
+ const _createDirIfNotExists = async (dirPath) => {
136
+ if (!fs.existsSync(dirPath)) {
137
+ await fs.promises.mkdir(dirPath, {recursive: true});
138
+ }
139
+ };
140
+
141
+ /**
142
+ * 获取下一个可用的 develop 目录编号
143
+ * @param {string} sourceDir source 目录路径
144
+ * @returns {number} 下一个可用的编号
145
+ */
146
+ const _getNextDevelopNumber = (sourceDir) => {
147
+ if (!fs.existsSync(sourceDir)) {
148
+ return 1;
149
+ }
150
+
151
+ const files = fs.readdirSync(sourceDir);
152
+ const developDirs = files.filter(file =>
153
+ fs.statSync(path.join(sourceDir, file)).isDirectory() &&
154
+ file.startsWith('develop-')
155
+ );
156
+
157
+ if (developDirs.length === 0) {
158
+ return 1;
159
+ }
160
+
161
+ // 提取编号并找出最大值
162
+ const numbers = developDirs.map(dir => {
163
+ const match = dir.match(/^develop-(\d+)$/);
164
+ return match ? parseInt(match[1], 10) : 0;
165
+ });
166
+
167
+ return Math.max(...numbers) + 1;
168
+ };
169
+
170
+ module.exports = async (options) => {
171
+ // 参数提取
172
+ const parsed = Ec.parseArgument(options);
173
+
174
+ // 获取参数
175
+ const repoUrl = parsed.address || parsed.a;
176
+ const instanceCount = parsed.i || parsed.instance || 10; // 默认值为10
177
+
178
+ // 验证参数
179
+ if (!repoUrl) {
180
+ Ec.error("❌ 请提供远程仓库地址 (-a, --address)");
181
+ process.exit(1);
182
+ }
183
+
184
+ // 验证 instanceCount 范围(只能是2位数)
185
+ if (instanceCount < 1 || instanceCount > 99) {
186
+ Ec.error("❌ 实例数量必须在 1-99 之间");
187
+ process.exit(1);
188
+ }
189
+
190
+ Ec.waiting(`准备添加 Submodule ${repoUrl},创建 ${instanceCount} 个实例副本...`);
191
+
192
+ try {
193
+ // 检查当前目录是否为 Git 仓库,如果不是则初始化
194
+ const isGitRepo = await _isGitRepo();
195
+ if (!isGitRepo) {
196
+ await _initGitRepo();
197
+ }
198
+
199
+ // 确保 source 目录存在
200
+ const sourceDir = path.resolve(process.cwd(), 'source');
201
+ await _createDirIfNotExists(sourceDir);
202
+
203
+ // 获取起始编号
204
+ let startNumber = _getNextDevelopNumber(sourceDir);
205
+
206
+ // 添加指定数量的 submodule 实例
207
+ for (let i = 0; i < instanceCount; i++) {
208
+ const developNumber = startNumber + i;
209
+ // 格式化为两位数
210
+ const formattedNumber = developNumber.toString().padStart(2, '0');
211
+ // 使用相对路径
212
+ const targetDir = path.join('source', `develop-${formattedNumber}`);
213
+
214
+ // 如果目录已存在,跳过
215
+ const fullPath = path.join(process.cwd(), targetDir);
216
+ if (fs.existsSync(fullPath)) {
217
+ Ec.waiting(`⚠️ 目录 ${targetDir} 已存在,跳过`);
218
+ continue;
219
+ }
220
+
221
+ // 添加 submodule
222
+ await _addSubmodule(repoUrl, targetDir);
223
+ }
224
+
225
+ // 初始化并更新所有 submodule
226
+ await _initAndUpdateSubmodules();
227
+
228
+ Ec.info('✅ 所有 Submodule 添加完成!');
229
+ process.exit(0);
230
+ } catch (error) {
231
+ Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
232
+ process.exit(1);
233
+ }
234
+ };