momo-ai 1.0.0 → 1.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.
Files changed (107) hide show
  1. package/README.md +116 -0
  2. package/package.json +2 -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 +130 -0
  92. package/src/executor/executePlan.js +134 -0
  93. package/src/executor/executeRepo.js +234 -0
  94. package/src/executor/executeRun.js +321 -0
  95. package/src/executor/executeShow.js +164 -0
  96. package/src/executor/executeTasks.js +260 -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,74 @@
1
+ const U = require("underscore");
2
+
3
+ // Import
4
+ const __IT = require("./momo.fn.it");
5
+
6
+ const __CX = require("./momo.fn.cx");
7
+ const __FX = require("./momo.fn.fx");
8
+
9
+ const __LOG = require("./momo.fn.log");
10
+ // Import Private
11
+ const __U = require("./momo.fn.find");
12
+
13
+ const __csvJObject = (object = {}, keysData) => {
14
+ const values = [];
15
+ const keys = keysData ? keysData : Object.keys(object);
16
+ keys.forEach(key => values.push(undefined !== object[key] ? object[key] : ""));
17
+ return values;
18
+ };
19
+ const toJArray = (content = "") => {
20
+ __CX.cxJString(content);
21
+ return JSON.parse(content);
22
+ };
23
+
24
+ const toJObject = (content = "") => {
25
+ __CX.cxJString(content);
26
+ return JSON.parse(content);
27
+ };
28
+
29
+ const toCsv = (array = [], mapping = {}, seperator) => {
30
+ let lines = [];
31
+ if (U.isArray(array) && 0 < array.length) {
32
+ const keys = __U.findMaxFields(array);
33
+ // 转换字段信息
34
+ const formatted = {};
35
+ keys.forEach(key => formatted[key] = key);
36
+ __IT.itObject(mapping, (from, to) => {
37
+ if (formatted.hasOwnProperty(from)) {
38
+ __LOG.info(`字段执行转换:${from.red} -> ${to.blue}`);
39
+ formatted[to] = to;
40
+ delete formatted[from];
41
+ }
42
+ });
43
+ const header = Object.keys(formatted);
44
+ lines.push(header.join(seperator));
45
+ array.forEach(each => {
46
+ __IT.itObject(mapping, (from, to) => {
47
+ if (each.hasOwnProperty(from)) {
48
+ each[to] = each[from];
49
+ delete each[from];
50
+ }
51
+ });
52
+ const line = __csvJObject(each, Object.keys(formatted));
53
+ lines.push(line.join(seperator));
54
+ });
55
+ return lines;
56
+ } else {
57
+ __FX.fxError(10001, `value = ${JSON.stringify(array)}, type = ${typeof array}`, "Array")
58
+ }
59
+ return lines;
60
+ };
61
+ const toTable = (record = {}) => {
62
+ const content = [];
63
+ content.push(`参数名\t\t|\t参数值\t\t|`.blue);
64
+ content.push(`-----------------------------------------`.yellow);
65
+ Object.keys(record).forEach(field => content.push(`${field}\t\t|\t${record[field]}\t\t|`));
66
+ content.push(`-----------------------------------------`.yellow);
67
+ return content.join("\n");
68
+ }
69
+ module.exports = {
70
+ toCsv,
71
+ toJArray,
72
+ toJObject,
73
+ toTable,
74
+ }
@@ -0,0 +1,5 @@
1
+ const path = require('path');
2
+
3
+ module.exports = {
4
+ FILE_DELIMITER: path.sep,
5
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ## `Ec.E`
3
+ *
4
+ * ### 1. 基本介绍
5
+ *
6
+ * 该类为错误信息定义专用类,方法名采用`fn<Code>`的方式执行最终输出的错误信息内容。通常使用下边代码:
7
+ *
8
+ * ```js
9
+ * // 第一种调用方法:第一参直接是错误代码
10
+ * Ec.fxError(10001, arg1, arg2);
11
+ *
12
+ * // 第二种调用方法:第一参是Boolean值,true就输出,第二参是错误代码
13
+ * const checked = true;
14
+ * Ec.fxError(checked, 10001, arg1, arg2);
15
+ *
16
+ * // 第三种调用方法:第一参是Function,执行后结果为true就输出,第二参是错误代码
17
+ * const fnChecked = () => true;
18
+ * Ec.fxError(fnChecked, 10001, arg1, arg2);
19
+ * ```
20
+ *
21
+ * ### 2. 错误代码表
22
+ *
23
+ * |代码|参数表|含义|
24
+ * |---|---|:---|
25
+ * |10001|`arg,type`|输入参数类型不匹配。|
26
+ * |10002|`arg,type,expected`|「带期望」输入参数和期望参数不匹配。|
27
+ * |10003|`fileType`|文件类型无法被解析。|
28
+ * |10004|`command`|无法识别命令名,不在可解析的命令中。|
29
+ * |10005|`command,expected`|「带期望」输入的命令不在期望的命令列表中。|
30
+ * |10006|`arg`|命令执行中丢失了必须的参数。|
31
+ * |10007|`path`|输入的目录不存在,或者输入路径并不是一个目录。|
32
+ * |10008|`path`|目录不存在,或输入路径是一个文件。|
33
+ * |10009|`path`|路径直接不存在,不论目录还是文件都不存在。|
34
+ * |10010|`projects`|系统检测到两个或两个以上的项目目录,系统无法定位操作项目环境。|
35
+ * |10011|`config`|「后端」配置数据中丢失了`api`属性值。|
36
+ * |10012|`member,clazz`|「后端」在查找的`clazz`类名中无法找到(成员变量/成员函数)`member`。|
37
+ * |10013|`lineType`|工具无法分析行类型,输入的文件内容不符合Zero Ai的基本规范,无法解析源代码。|
38
+ * |10014|`pkg`|「后端」系统找到了超过两个以上的`package`语句,这个在定义过程中是非法的,不可连续执行。|
39
+ * |10015|`method,clazz`|「后端」在查找的`clazz`类名中找到了重复的(成员函数/成员变量)`method`,所以非法。|
40
+ * |10016|`command`|「前端」Zero UI规范错误,不可执行当前命令。|
41
+ * |10017|`root`|「前端」Zero项目的目录并非一个合法的项目目录,请定位到合法的项目目录中。|
42
+ * |10018|`resource`|「前端」资源文件绑定过程中出现了资源错误,请检查环境或执行命令。|
43
+ * |10019|`root`|「前端」当前命令只能在项目根目录中执行(带有package.json文件),其他目录不可执行该方法。|
44
+ * |10020|`menuData`|「前端」当前菜单数据必须是一个合法的Array类型,当前类型不对。|
45
+ * |10021|`field,value`|条件`field=value`引起了重复数据记录,导致不匹配UK规范,检查重复数据专用错误。|
46
+ * |10022|`root`|「前端/后端」无法定位项目的根目录,不可执行项目专用类命令。|
47
+ * |10023|`folder`|「前端」初始化项目时检测到输入的文件路径是一个非空目录,不可执行Zero AI的初始化。|
48
+ * |10024|`path`|当前操作和输入的路径冲突,不可在路径中执行操作指令。|
49
+ * |10025|`configKey`|「前端」配置项主键丢失了核心配置,在生成前端Web组件时出现了规范冲突。|
50
+ * |10026|`path`|「前端」输入路径非法,不在支持的Zero Ui专用路径规范中。|
51
+ * |10027|`modulePath`|「前端」输入路径必须是`<module>/<page>`格式,当前路径并非该格式,和规范冲突。|
52
+ * |10028|`arg`|「前端」模块参数不在枚举值中,必须是四者之一:`FORM, FILTER, HALF, EDIT`。|
53
+ * |10029|`zt`|「带期望」环境变量缺失或者格式不对,必须是`<module>/<page>`格式。|
54
+ * |10030|`arg, key`|「开发专用」方法要求资源文件中必须包含`key`属性,当前`key`属性值不对。|
55
+ * |10031|`id`|「开发专用」当前HTML按钮元素要求`btn`前缀,输入前缀不合法。|
56
+ * |10032|`platform`|操作系统不支持当前命令,或者该操作系统平台中还未实现该命令的执行逻辑。|
57
+ * |10033|`path`|当前路径不符合Zero专用规范(基础Java/Maven规范)。|
58
+ * |10034|`path`|当前路径不符合Ox平台专用规范。|
59
+ *
60
+ * @class E
61
+ */
62
+ module.exports = {
63
+ // 基础
64
+ fn10001: (arg, type) => `[AI-10001] Argument must be '${type}', but now it's '${arg}'`,
65
+ fn10002: (arg, type, expected) => `[AI-10002] Argument must be in (${expected}), but now it's '${arg}' of '${type}'`,
66
+
67
+ // 命令
68
+ fn10003: (fileType) => `[AI-10003] The fileType='${fileType}' is unknown and could not found parser`,
69
+ fn10004: (command) => `[AI-10004] The executor of command '${command}' could not be found(执行器丢失)`,
70
+ fn10005: (command = "未输入", expected) => `[AI-10005] The command '${command}' could not be found, expected '${expected}'`,
71
+ fn10006: (arg) => `[AI-10006] The command missed required arguments: '${arg}'`,
72
+
73
+ // 路径
74
+ fn10007: (path) => `[AI-10007] The file '${path}' does not exist or it's a directory.`,
75
+ fn10008: (path) => `[AI-10008] The directory '${path}' does not exist or it's a file.`,
76
+ fn10009: (path) => `[AI-10009] The path '${path}' does not exist.`,
77
+
78
+ // 后端
79
+ fn10010: (projects = []) => `[AI-10010] The tool detect more than one project folders, found '${projects.length}', please switch.`,
80
+ fn10011: (config) => `[AI-10011] 'api' attribute has been missed in current config ${JSON.stringify(config)}`,
81
+ fn10012: (member, clazz) => `[AI-10012] Duplicated member '${member}' found in class '${clazz}', please check.`,
82
+ fn10013: (lineType) => `[AI-10013] Zero system could not analyze the code line type, type = ${lineType} is Unknown`,
83
+ fn10014: (pkg) => `[AI-10014] More than one 'package' sentence found.${JSON.stringify(pkg)}`,
84
+ fn10015: (method, clazz) => `[AI-10015] Duplicated method '${method}' found in class '${clazz}', please check.`,
85
+
86
+ // 前端
87
+ fn10016: (command) => `[AI-10016] Zero UI specification wrong, you could not execute in '${command}'`,
88
+ fn10017: (root) => `[AI-10017] Current folder '${root}' is not project folder, please switch to project root directory`,
89
+ fn10018: (resource) => `[AI-10018] Zero Resource config file missing '${resource}', please check.`,
90
+ fn10019: (root) => `[AI-10019] This command could run in project root folder only, current folder = ${root}`,
91
+ fn10020: (menuData) => `[AI-10020] The menu data file must be json format with 'data' (Array) node, current = ${menuData}`,
92
+ fn10021: (field, value) => `[AI-10021] The condition '${field} = ${value}' hit duplicated record in your file.`,
93
+ fn10022: (root) => `[AI-10022] Could not find root folder of current project. ${root}`,
94
+ fn10023: (folder) => `[AI-10023] Initialized folder must be empty, current ${folder} is invalid`,
95
+ fn10024: (path) => `[AI-10024] This operation is not allowed for current path ${path}`,
96
+ fn10025: (configKey) => `[AI-10025] Web control initialized require "actual.data" must be string, current:${configKey}`,
97
+ fn10026: (path) => `[AI-10026] The path is invalid, ${path}, system support one of ".", "src/components/xxx", "xxx".`,
98
+ fn10027: (modulePath) => `[AI-10027] The path must be formatted with "<module>/<page>", could not be others, current = ${modulePath}`,
99
+ fn10028: (arg) => `[AI-10028] The "module" parameter must be one of "FORM", "FILTER", "HALF", "EDIT" values. current = ${arg}`,
100
+ fn10029: (zt, name) => `[AI-10029] [DEV] You'll use development command, please set "${name}" environment first, current = ${zt}. React format = "<module>/<page>", Java for other.`,
101
+ fn10030: (arg, key) => `[AI-10030] [DEV] This method require "${key}" in your resource file, but current "${key}" = ${arg}`,
102
+ fn10031: (id) => `[AI-10031] [DEV] Button key must start with "btn", current ${id}`,
103
+ fn10032: (platform) => `[AI-10032] This api is not implemented in current platform os.platform() -> '${platform}'`,
104
+ fn10033: (path) => `[AI-10033] (Zero) Current path '${path}' is not correct Zero work folder under specification`,
105
+ fn10034: (path) => `[AI-10034] (Ox) Current path '${path}' is not correct Ox work folder`,
106
+ fn10035: (path) => `[AI-10035] (Mod) The "initialize.json" configuration file has been missed in '${path}'`,
107
+ fn10036: (mode) => `[AI-10036] (Mod) The mode must be in "REPLACE, APPEND", could not been parsed: ${mode}`,
108
+ };
@@ -0,0 +1,113 @@
1
+ const Ec = require('../epic');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * 交互式选择LLM
7
+ */
8
+ const _selectLLM = async (actorName) => {
9
+ Ec.waiting('🤖 请选择要使用的LLM:');
10
+
11
+ const llmOptions = [
12
+ {name: 'OpenSpec', value: 'openspec'},
13
+ {name: 'SpecKit', value: 'spec-kit'},
14
+ {name: 'Kiro', value: 'kiro'},
15
+ {name: 'Trea', value: 'trea'},
16
+ {name: 'Cursor', value: 'cursor'},
17
+ {name: 'Lingma', value: 'lingma'},
18
+ {name: 'Qoder', value: 'qoder'},
19
+ {name: 'WindSurf', value: 'windsurf'},
20
+ {name: 'GitHub Copilot', value: 'github'},
21
+ {name: 'Claude Code', value: 'claude-code'},
22
+ {name: 'ChatGPT', value: 'chatgpt'}
23
+ ];
24
+
25
+ // 显示选项
26
+ llmOptions.forEach((option, index) => {
27
+ Ec.waiting(`${index + 1}. ${option.name}`);
28
+ });
29
+
30
+ // 获取用户选择
31
+ const answer = await Ec.ask('请输入选项编号: ');
32
+ const selectedIndex = parseInt(answer) - 1;
33
+
34
+ // 验证选择
35
+ if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= llmOptions.length) {
36
+ Ec.error('❌ 无效的选择');
37
+ return _selectLLM(actorName); // 重新选择
38
+ }
39
+
40
+ const selectedLLM = llmOptions[selectedIndex];
41
+
42
+ // 获取 token 配置信息
43
+ const token = await Ec.ask(`请输入 ${selectedLLM.name} 的 Token (可选): `);
44
+
45
+ return {
46
+ llm: selectedLLM.value,
47
+ name: actorName,
48
+ token: token || ''
49
+ };
50
+ };
51
+
52
+ module.exports = (options) => {
53
+ // 参数提取
54
+ const parsed = Ec.parseArgument(options);
55
+
56
+ const actorName = parsed.name;
57
+ Ec.waiting(`🔍 准备创建角色: ${actorName}`);
58
+
59
+ // 创建角色目录
60
+ const actorDir = path.resolve(process.cwd(), 'specification', 'actor', actorName);
61
+ if (!fs.existsSync(actorDir)) {
62
+ Ec.waiting(`📁 创建角色目录: ${actorDir}`);
63
+ fs.mkdirSync(actorDir, {recursive: true});
64
+ } else {
65
+ Ec.waiting(`✅ 角色目录已存在: ${actorDir}`);
66
+ }
67
+
68
+ // 从模板复制内容到stack.md和limit.md
69
+ const templatePath = path.resolve(__dirname, '../_template/LAIN/.momo/advanced/actor.md');
70
+ if (fs.existsSync(templatePath)) {
71
+ const templateContent = fs.readFileSync(templatePath, 'utf8');
72
+
73
+ // 提取技能说明部分作为stack.md的内容
74
+ const stackMatch = templateContent.match(/## 技能说明\s*([\s\S]*?)\s*## 角色限制/);
75
+ if (stackMatch && stackMatch[1]) {
76
+ const stackContent = `# 技术栈信息\n\n${stackMatch[1].trim()}\n`;
77
+ const stackPath = path.resolve(actorDir, 'stack.md');
78
+ fs.writeFileSync(stackPath, stackContent);
79
+ Ec.waiting(`📄 创建技术栈文件: ${stackPath}`);
80
+ }
81
+
82
+ // 提取角色限制部分作为limit.md的内容
83
+ const limitMatch = templateContent.match(/## 角色限制\s*([\s\S]*)/);
84
+ if (limitMatch && limitMatch[1]) {
85
+ const limitContent = `# 角色限制\n\n${limitMatch[1].trim()}\n`;
86
+ const limitPath = path.resolve(actorDir, 'limit.md');
87
+ fs.writeFileSync(limitPath, limitContent);
88
+ Ec.waiting(`📄 创建限制文件: ${limitPath}`);
89
+ }
90
+ }
91
+
92
+ // 交互式选择LLM
93
+ _selectLLM(actorName)
94
+ .then((llmConfig) => {
95
+ // 创建config.json文件
96
+ const configPath = path.resolve(actorDir, 'config.json');
97
+ fs.writeFileSync(configPath, JSON.stringify(llmConfig, null, 4));
98
+ Ec.waiting(`⚙️ 创建配置文件: ${configPath}`);
99
+
100
+ Ec.info(`✅ 角色 "${actorName}" 创建完成`);
101
+ // 关闭 readline 接口
102
+ Ec.askClose();
103
+ // 退出程序
104
+ process.exit(0);
105
+ })
106
+ .catch((error) => {
107
+ Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
108
+ // 关闭 readline 接口
109
+ Ec.askClose();
110
+ // 退出程序
111
+ process.exit(1);
112
+ });
113
+ };
@@ -0,0 +1,58 @@
1
+ const Ec = require('../epic');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ module.exports = (options) => {
6
+ // 参数提取
7
+ const parsed = Ec.parseArgument(options);
8
+
9
+ try {
10
+ // 获取 actor 目录路径
11
+ const actorsDir = path.resolve(process.cwd(), 'specification', 'actor');
12
+
13
+ // 检查目录是否存在
14
+ if (!fs.existsSync(actorsDir)) {
15
+ Ec.error("❌ 未找到 actor 目录,请先初始化项目或创建角色");
16
+ Ec.askClose();
17
+ process.exit(1);
18
+ }
19
+
20
+ // 读取所有 actor 目录
21
+ const actors = fs.readdirSync(actorsDir).filter(file =>
22
+ fs.statSync(path.join(actorsDir, file)).isDirectory()
23
+ );
24
+
25
+ if (actors.length === 0) {
26
+ Ec.waiting("🔍 未找到任何角色");
27
+ Ec.askClose();
28
+ process.exit(0);
29
+ }
30
+
31
+ // 遍历所有 actor 并读取配置
32
+ actors.forEach((actorName, index) => {
33
+ const configPath = path.resolve(actorsDir, actorName, 'config.json');
34
+ let llm = '-';
35
+ let token = '-';
36
+
37
+ if (fs.existsSync(configPath)) {
38
+ try {
39
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
40
+ llm = config.llm || '-';
41
+ token = config.token || '-';
42
+ } catch (e) {
43
+ // 解析配置文件出错,保持默认值
44
+ }
45
+ }
46
+
47
+ // 输出格式:序号. 名称 ( LLM = ??, Token = ?? )
48
+ Ec.waiting(`${index + 1}. ${actorName} ( LLM = ${llm}, Token = ${token} )`);
49
+ });
50
+
51
+ Ec.askClose();
52
+ process.exit(0);
53
+ } catch (error) {
54
+ Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
55
+ Ec.askClose();
56
+ process.exit(1);
57
+ }
58
+ };
@@ -0,0 +1,248 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const util = require('util');
4
+ const { spawn } = require('child_process');
5
+ const Ec = require('../epic');
6
+ const fsAsync = require('fs').promises;
7
+
8
+ /**
9
+ * 读取模板文件并提取 <!-- BEGIN --> 到 <!-- END --> 之间的内容
10
+ * @param {string} templatePath 模板文件路径
11
+ * @returns {Promise<string>} 提取的模板内容
12
+ */
13
+ const _readTemplate = async (templatePath) => {
14
+ try {
15
+ const content = await fsAsync.readFile(templatePath, 'utf8');
16
+ const lines = content.split('\n');
17
+ let beginIndex = -1;
18
+ let endIndex = -1;
19
+
20
+ for (let i = 0; i < lines.length; i++) {
21
+ if (lines[i].includes('<!-- BEGIN -->')) {
22
+ beginIndex = i;
23
+ } else if (lines[i].includes('<!-- END -->')) {
24
+ endIndex = i;
25
+ break;
26
+ }
27
+ }
28
+
29
+ if (beginIndex !== -1 && endIndex !== -1) {
30
+ // 提取 BEGIN 和 END 之间的内容(不包括 BEGIN 和 END 行)
31
+ return lines.slice(beginIndex + 1, endIndex).join('\n');
32
+ }
33
+
34
+ return content;
35
+ } catch (error) {
36
+ Ec.waiting(`读取模板文件失败: ${error.message}`);
37
+ throw error;
38
+ }
39
+ };
40
+
41
+ /**
42
+ * 渲染 EJS 模板
43
+ * @param {string} template 模板内容
44
+ * @param {Object} data 数据对象
45
+ * @returns {string} 渲染后的内容
46
+ */
47
+ const _renderTemplate = (template, data) => {
48
+ let result = template;
49
+ for (const key in data) {
50
+ if (data.hasOwnProperty(key)) {
51
+ const regex = new RegExp(`<%=\\s*${key}\\s*%>`, 'g');
52
+ result = result.replace(regex, data[key]);
53
+ }
54
+ }
55
+ return result;
56
+ };
57
+
58
+ /**
59
+ * 检查需求目录是否已存在
60
+ * @param {string} changesDir changes目录路径
61
+ * @param {string} requirementName 需求名称
62
+ * @returns {boolean} 是否存在
63
+ */
64
+ const _isRequirementExists = (changesDir, requirementName) => {
65
+ const requirementDir = path.join(changesDir, requirementName);
66
+ return fs.existsSync(requirementDir);
67
+ };
68
+
69
+ /**
70
+ * 从 Markdown 文件中提取第一个标题作为需求名称
71
+ * @param {string} filePath 文件路径
72
+ * @returns {Promise<string>} 第一个标题内容
73
+ */
74
+ const _extractTitleFromMarkdown = async (filePath) => {
75
+ try {
76
+ const content = await fsAsync.readFile(filePath, 'utf8');
77
+ const lines = content.split('\n');
78
+
79
+ for (const line of lines) {
80
+ // 匹配 # 开头的标题行
81
+ const titleMatch = line.match(/^#\s+(.+)$/);
82
+ if (titleMatch) {
83
+ return titleMatch[1].trim();
84
+ }
85
+ }
86
+
87
+ // 如果没有找到标题,使用文件名(不含扩展名)作为需求名称
88
+ const fileName = path.basename(filePath);
89
+ return fileName.replace(/\.[^/.]+$/, ""); // 移除扩展名
90
+ } catch (error) {
91
+ Ec.waiting(`读取文件失败: ${error.message}`);
92
+ throw error;
93
+ }
94
+ };
95
+
96
+ /**
97
+ * 创建需求目录并拷贝模板文件
98
+ * @param {string} templateDir 模板目录
99
+ * @param {string} targetDir 目标目录
100
+ * @param {string} sourceFilePath 源文件路径(可选)
101
+ */
102
+ const _copyTemplateFiles = async (templateDir, targetDir, sourceFilePath = null) => {
103
+ try {
104
+ // 确保目标目录存在
105
+ if (!fs.existsSync(targetDir)) {
106
+ await fsAsync.mkdir(targetDir, { recursive: true });
107
+ }
108
+
109
+ let proposalSource = path.join(templateDir, 'proposal.md');
110
+ const proposalTarget = path.join(targetDir, 'proposal.md');
111
+
112
+ // 如果提供了源文件路径,则使用源文件内容覆盖 proposal.md
113
+ if (sourceFilePath && fs.existsSync(sourceFilePath)) {
114
+ proposalSource = sourceFilePath;
115
+ Ec.waiting(`从文件 "${sourceFilePath}" 拷贝内容到 proposal.md`);
116
+ await fsAsync.copyFile(sourceFilePath, proposalTarget);
117
+ } else if (fs.existsSync(proposalSource)) {
118
+ Ec.waiting(`拷贝文件: ${proposalTarget}`);
119
+ await fsAsync.copyFile(proposalSource, proposalTarget);
120
+ }
121
+
122
+ // 拷贝 tasks.md
123
+ const tasksSource = path.join(templateDir, 'tasks.md');
124
+ const tasksTarget = path.join(targetDir, 'tasks.md');
125
+ if (fs.existsSync(tasksSource)) {
126
+ Ec.waiting(`拷贝文件: ${tasksTarget}`);
127
+ await fsAsync.copyFile(tasksSource, tasksTarget);
128
+ }
129
+
130
+ // 创建 tasks 目录
131
+ const tasksDir = path.join(targetDir, 'tasks');
132
+ if (!fs.existsSync(tasksDir)) {
133
+ Ec.waiting(`创建目录: ${tasksDir}`);
134
+ await fsAsync.mkdir(tasksDir, { recursive: true });
135
+ }
136
+
137
+ // 拷贝 task-detail.md 到 tasks 目录
138
+ const taskDetailSource = path.join(templateDir, 'tasks', 'task-detail.md');
139
+ const taskDetailTarget = path.join(tasksDir, 'M01-T001-01.md'); // 示例任务文件名
140
+ if (fs.existsSync(taskDetailSource)) {
141
+ Ec.waiting(`拷贝文件: ${taskDetailTarget}`);
142
+ await fsAsync.copyFile(taskDetailSource, taskDetailTarget);
143
+ }
144
+ } catch (error) {
145
+ Ec.waiting(`拷贝模板文件失败: ${error.message}`);
146
+ throw error;
147
+ }
148
+ };
149
+
150
+ /**
151
+ * 将内容复制到剪贴板(去除换行符)
152
+ * @param {string} content 要复制的内容
153
+ */
154
+ const _copyToClipboard = async (content) => {
155
+ try {
156
+ // 去除换行符,将内容合并为一行
157
+ const contentWithoutNewlines = content.replace(/\r?\n|\r/g, ' ');
158
+ const proc = spawn('pbcopy', { stdio: 'pipe' });
159
+ proc.stdin.write(contentWithoutNewlines);
160
+ proc.stdin.end();
161
+ Ec.waiting('✅ 提示词已复制到剪贴板');
162
+ } catch (error) {
163
+ Ec.waiting(`复制到剪贴板失败: ${error.message}`);
164
+ // 在非 macOS 系统上可能没有 pbcopy,提供备选方案
165
+ Ec.waiting('提示词内容:');
166
+ console.log(content);
167
+ }
168
+ };
169
+
170
+ module.exports = async (options) => {
171
+ // 参数提取
172
+ const parsed = Ec.parseArgument(options);
173
+
174
+ // 获取需求名称
175
+ let requirementName = parsed.name || parsed.n;
176
+
177
+ // 验证参数
178
+ if (!requirementName) {
179
+ Ec.error("❌ 请提供需求名称或需求文件路径 (-n, --name)");
180
+ process.exit(1);
181
+ }
182
+
183
+ // 检查是否包含扩展名
184
+ const hasExtension = path.extname(requirementName) !== '';
185
+
186
+ // 如果包含扩展名,视为文件路径处理
187
+ let sourceFilePath = null;
188
+ if (hasExtension) {
189
+ // 检查文件是否存在
190
+ if (!fs.existsSync(requirementName)) {
191
+ // 检查是否在当前目录下存在
192
+ const fullPath = path.resolve(process.cwd(), requirementName);
193
+ if (!fs.existsSync(fullPath)) {
194
+ Ec.error(`❌ 文件 "${requirementName}" 不存在`);
195
+ process.exit(1);
196
+ }
197
+ sourceFilePath = fullPath;
198
+ } else {
199
+ sourceFilePath = requirementName;
200
+ }
201
+
202
+ // 从文件中提取标题作为需求名称
203
+ requirementName = await _extractTitleFromMarkdown(sourceFilePath);
204
+ }
205
+
206
+ // 检查需求名称是否包含点号(除了允许的独立需求)
207
+ if (!hasExtension && requirementName.includes('.')) {
208
+ Ec.error("❌ 独立需求名称不能包含点号(.),以防止与文件扩展名混淆");
209
+ process.exit(1);
210
+ }
211
+
212
+ Ec.waiting(`准备添加新需求: ${requirementName}`);
213
+
214
+ try {
215
+ // 获取项目路径
216
+ const projectDir = process.cwd();
217
+ const changesDir = path.join(projectDir, 'specification', 'changes');
218
+ const templateDir = path.join(__dirname, '../_template/LAIN/changes');
219
+ const promptTemplatePath = path.join(__dirname, '../_template/PROMPT/add.md.ejs');
220
+
221
+ // 检查需求是否已存在
222
+ if (_isRequirementExists(changesDir, requirementName)) {
223
+ Ec.error(`❌ 需求 "${requirementName}" 已存在`);
224
+ process.exit(1);
225
+ }
226
+
227
+ // 创建需求目录
228
+ const requirementDir = path.join(changesDir, requirementName);
229
+ Ec.waiting(`创建需求目录: ${requirementDir}`);
230
+ await fsAsync.mkdir(requirementDir, { recursive: true });
231
+
232
+ // 拷贝模板文件
233
+ await _copyTemplateFiles(templateDir, requirementDir, sourceFilePath);
234
+
235
+ // 读取并处理提示词模板
236
+ const templateContent = await _readTemplate(promptTemplatePath);
237
+ const renderedContent = _renderTemplate(templateContent, { NAME: requirementName });
238
+
239
+ // 将提示词复制到剪贴板
240
+ await _copyToClipboard(renderedContent);
241
+
242
+ Ec.info(`✅ 成功添加新需求 "${requirementName}"`);
243
+ process.exit(0);
244
+ } catch (error) {
245
+ Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
246
+ process.exit(1);
247
+ }
248
+ };