momo-ai 1.0.49 → 1.0.50

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,307 @@
1
+ # AI Agents Integration (Momo CLI)
2
+
3
+ This document helps AI coding assistants understand the project structure and workflows. Following these conventions improves accuracy when assisting development.
4
+
5
+ ## Project Workflow
6
+
7
+ 1. **Requirements** – Define and analyze business needs
8
+ 2. **Task planning** – Break requirements into executable tasks
9
+ 3. **Role assignment** – Assign AI or human roles per task
10
+ 4. **Implementation** – Implement tasks and generate code
11
+ 5. **Verification** – Validate that outcomes meet requirements
12
+ 6. **Archive & commit** – Archive and commit completed work
13
+
14
+ ## Momo CLI Tools (Reference)
15
+
16
+ All tools are invoked as `momo <command> [options]`. Descriptions and options are derived from `src/commander/*.json`.
17
+
18
+ ---
19
+
20
+ ### Basic commands
21
+
22
+ #### `momo help [-c <command>]`
23
+ Show help. With `-c`, show detailed help for a specific command.
24
+
25
+ **Options:**
26
+ - `-c, --command`: Command name
27
+
28
+ **Examples:**
29
+ ```bash
30
+ momo help
31
+ momo help -c init
32
+ ```
33
+
34
+ #### `momo env`
35
+ Check environment (Node.js, Python, and other system dependencies).
36
+
37
+ **Examples:**
38
+ ```bash
39
+ momo env
40
+ ```
41
+
42
+ #### `momo init [-d <dir>]`
43
+ Initialize R2MO specification directory structure.
44
+
45
+ **Options:**
46
+ - `-d, --dir`: Target directory (default: current directory)
47
+
48
+ **Examples:**
49
+ ```bash
50
+ momo init
51
+ momo init -d ./my-project
52
+ ```
53
+
54
+ ---
55
+
56
+ ### Development & project commands
57
+
58
+ #### `momo app -n <name>`
59
+ Create a new R2MO/Spring or ZERO/Vertx application.
60
+
61
+ **Options:**
62
+ - `-n, --name`: Application name (required)
63
+
64
+ **Examples:**
65
+ ```bash
66
+ momo app -n my-app
67
+ ```
68
+
69
+ #### `momo open [-d <dir>]`
70
+ Open the project with a chosen AI tool (Antigravity, Trae, Cursor).
71
+
72
+ **Options:**
73
+ - `-d, --dir`: Directory to open (default: current directory)
74
+
75
+ **Examples:**
76
+ ```bash
77
+ momo open
78
+ momo open -d ./src
79
+ ```
80
+
81
+ #### `momo domain [-d <dir>] [-e]`
82
+ Run `r2mo_proto` to process Maven project domain model (Protobuf / DB).
83
+
84
+ **Options:**
85
+ - `-d, --dir`: Target directory (default: current directory); must be Maven root with `pom.xml`
86
+ - `-e, --entity`: Generate from Entity (true) or from SQL (false); default true
87
+
88
+ **Examples:**
89
+ ```bash
90
+ momo domain
91
+ momo domain -d ./my-maven-project
92
+ ```
93
+
94
+ #### `momo ui -n <name> [-d <dir>] [-u]`
95
+ Create or update a UI subproject from the r2mo-ui template (Rust/WASM + Tauri).
96
+
97
+ **Options:**
98
+ - `-n, --name`: Project name (required for create; DPA suggests `xxx-ui`)
99
+ - `-d, --dir`: Parent directory (default: current directory)
100
+ - `-u, --update`: Update mode: sync root MD, `src/pages/components`, `src/pages/utils`, and optionally other changed files via multi-select; target defaults to current directory when `-u` is used without `-n`
101
+
102
+ **Examples:**
103
+ ```bash
104
+ momo ui -n my-app-ui -d .
105
+ momo ui -u
106
+ ```
107
+
108
+ #### `momo admin [-d <dir>]`
109
+ Generate front-end page structure from project requirements (`.r2mo/requirements/project.md`). Writes module/personal pages under `src/pages/`; skips overwriting existing files (metadata, requirement, page yaml).
110
+
111
+ **Options:**
112
+ - `-d, --dir`: Target directory (default: current directory)
113
+
114
+ **Examples:**
115
+ ```bash
116
+ momo admin
117
+ momo admin -d .
118
+ ```
119
+
120
+ ---
121
+
122
+ ### Specification & API commands
123
+
124
+ #### `momo mod [-d <dir>]`
125
+ Pull r2mo-spec into `.r2mo/repo` and copy project/OpenAPI artifacts to `.r2mo/api/`.
126
+
127
+ **Options:**
128
+ - `-d, --dir`: Project root (default: current directory)
129
+
130
+ **Examples:**
131
+ ```bash
132
+ momo mod
133
+ momo mod -d .
134
+ ```
135
+
136
+ #### `momo openapi [-d <dir>]`
137
+ Extract Operation/Schema markdown from subprojects’ `src/main/resources/openapi` and copy to `-ui/.r2mo/api/` preserving structure.
138
+
139
+ **Options:**
140
+ - `-d, --dir`: Project root (default: current directory)
141
+
142
+ **Examples:**
143
+ ```bash
144
+ momo openapi
145
+ momo openapi -d .
146
+ ```
147
+
148
+ #### `momo docs [-d <dir>]`
149
+ Open the docs directory with Obsidian.
150
+
151
+ **Options:**
152
+ - `-d, --dir`: Target directory (default: current directory)
153
+
154
+ **Examples:**
155
+ ```bash
156
+ momo docs
157
+ momo docs -d ./specification
158
+ ```
159
+
160
+ #### `momo menu [-d <dir>]`
161
+ Scan `src/pages` for `menu.yaml` and print the full tree menu (name, text, icon).
162
+
163
+ **Options:**
164
+ - `-d, --dir`: Project root (default: current directory)
165
+
166
+ **Examples:**
167
+ ```bash
168
+ momo menu
169
+ momo menu -d .
170
+ ```
171
+
172
+ ---
173
+
174
+ ### Dictionary & Flyway commands
175
+
176
+ #### `momo dict [-d <dir>] [-r]`
177
+ **Forward:** Read `.r2mo/api/components/schemas` (XTabular/XCategory), connect via `app.env`, export `X_TABULAR`/`X_CATEGORY` by TYPE to `targetDir/.r2mo/data/dbdict/` as `dict.{type}.yaml` and `tree.{type}.yaml`. When matching Flyway SQL exists, prepend metadata (sqlFile, sqlPath) in YAML front-matter.
178
+
179
+ **Reverse (`-r`):** Read `.r2mo/data/dbdict` YAML (DPA: from `-ui` dir). Only process files with valid metadata (sqlPath). Ask once to overwrite; then overwrite the SQL files at recorded paths.
180
+
181
+ **Options:**
182
+ - `-d, --dir`: Project root (default: current directory)
183
+ - `-r, --reverse`: Reverse mode: YAML as input, generate/overwrite Flyway SQL using metadata
184
+
185
+ **Examples:**
186
+ ```bash
187
+ momo dict
188
+ momo dict -d .
189
+ momo dict -r
190
+ ```
191
+
192
+ ---
193
+
194
+ ### Spec repository codegen commands
195
+
196
+ #### `momo mmr0`
197
+ Download from r2mo-spec repository and generate Flyway SQL files (e.g. into `-domain` Flyway dir).
198
+
199
+ **Examples:**
200
+ ```bash
201
+ momo mmr0
202
+ ```
203
+
204
+ #### `momo mmr2`
205
+ Download from r2mo-spec repository and generate Entity classes.
206
+
207
+ **Examples:**
208
+ ```bash
209
+ momo mmr2
210
+ ```
211
+
212
+ ---
213
+
214
+ ### Skills & MCP commands
215
+
216
+ #### `momo apply [-r [repo_name]]`
217
+ Install skills from a remote repository into the local project. Choose target path interactively.
218
+
219
+ **Options:**
220
+ - `-r, --remote`: Install from remote (optional repo name)
221
+
222
+ **Install targets:**
223
+ - Cursor (`.claude/skills/`)
224
+ - Antigravity (`.agent/skills/`)
225
+ - Trae CN / Trae (`.trae/skills/`)
226
+ - Lingma (`.lingma/skills/`)
227
+
228
+ **Examples:**
229
+ ```bash
230
+ momo apply -r
231
+ momo apply -r anthropics/skills
232
+ ```
233
+
234
+ #### `momo mcp [-c]`
235
+ Configure MCP Skills Server: merge project and global skills and generate Cursor `mcp.json`.
236
+
237
+ **Options:**
238
+ - `-c, --check`: Only check dependencies; do not configure
239
+
240
+ **Behavior:**
241
+ - Install MCP deps under `.r2mo/mcpserver`
242
+ - Write `.cursor/mcp.json`
243
+ - Copy config to clipboard
244
+
245
+ **Examples:**
246
+ ```bash
247
+ momo mcp
248
+ momo mcp -c
249
+ ```
250
+
251
+ ---
252
+
253
+ ### Prompt & template commands
254
+
255
+ #### `momo ask`
256
+ Pick a prompt template from `src/_template/R2MO/`, extract content between `--- BEGIN` and `--- END`, copy to clipboard, and show template details (file, title, version, skills, commands). If the prompt contains a “模块:ID,NAME,PATH” placeholder, scan `src/pages/*/requirement.module.md` (with valid front-matter, no `{}` placeholders), let the user choose a module, and replace the placeholder with that module’s ID, name, and path.
257
+
258
+ **Examples:**
259
+ ```bash
260
+ momo ask
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Tool summary (by command)
266
+
267
+ | Command | Description (short) |
268
+ |----------|----------------------|
269
+ | `help` | Show help; optional `-c` for command-specific help |
270
+ | `env` | Environment check |
271
+ | `init` | Initialize R2MO spec directory |
272
+ | `app` | Create R2MO/Spring or ZERO/Vertx app |
273
+ | `open` | Open project in AI tool (Antigravity/Trae/Cursor) |
274
+ | `domain` | Run r2mo_proto for domain/Protobuf |
275
+ | `ui` | Create/update UI subproject from r2mo-ui; `-u` update mode |
276
+ | `admin` | Generate front-end page structure from requirements (skip existing) |
277
+ | `mod` | Pull r2mo-spec, copy OpenAPI to `.r2mo/api/` |
278
+ | `openapi`| Extract OpenAPI md to `-ui/.r2mo/api/` |
279
+ | `docs` | Open docs in Obsidian |
280
+ | `menu` | Print tree menu from `src/pages` menu.yaml |
281
+ | `dict` | Export dict to `.r2mo/data/dbdict` (with metadata); `-r` reverse to Flyway SQL |
282
+ | `mmr0` | Generate Flyway SQL from r2mo-spec |
283
+ | `mmr2` | Generate Entity classes from r2mo-spec |
284
+ | `apply` | Install skills from remote (Cursor/Agent/Trae/Lingma) |
285
+ | `mcp` | Configure MCP Skills Server and `mcp.json` |
286
+ | `ask` | Select prompt template, optional module substitution, copy to clipboard |
287
+
288
+ ---
289
+
290
+ ## Key paths
291
+
292
+ - **Spec / requirements:** `.r2mo/requirements/`, `src/_template/R2MO/`
293
+ - **Schemas / API:** `.r2mo/api/components/schemas`, `-ui/.r2mo/api/`
294
+ - **Dictionary:** `.r2mo/data/dbdict/` (dict.*.yaml, tree.*.yaml; optional front-matter sqlFile/sqlPath)
295
+ - **Flyway SQL:** `-domain/src/main/resources/plugins/<artifactId>/flyway/MYSQL/`
296
+ - **Skills:** `.claude/skills/`, `.agent/skills/`, `.trae/skills/`, `.lingma/skills/`
297
+ - **MCP:** `.r2mo/mcpserver/`, `.cursor/mcp.json`
298
+
299
+ ---
300
+
301
+ ## Conventions
302
+
303
+ 1. **DPA vs ONE:** DPA = Domain + Provider + Api (+ `-ui`); target dir for UI/dbdict is `-ui`; app.env from `-api`. ONE = single project; target dir is project root.
304
+ 2. **momo dict:** Forward writes YAML with optional metadata from matching Flyway SQL; reverse only processes YAML with valid metadata and overwrites SQL after user confirmation.
305
+ 3. **momo admin:** Does not overwrite existing metadata, requirement, or page yaml files.
306
+ 4. **momo ask:** Modules with `{}` placeholders in front-matter are excluded from the module list.
307
+ 5. **momo ui -u:** Without `-n`, target is current directory; Rust/Tauri and listed root files are not updated in “other files” multi-select.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "momo-ai",
3
- "version": "1.0.49",
3
+ "version": "1.0.50",
4
4
  "description": "Rachel Momo ( OpenSpec )",
5
5
  "main": "src/momo.js",
6
6
  "bin": {
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: mo-dev-login
3
+ description: Leptos+Rust login flow: Z_ENDPOINT/Z_APP, /app/name, /auth/login, storage keys, WASM pitfalls
4
+ version: 1.0.0
5
+ tags: [r2mo, dev, login, rust, leptos, wasm, auth, storage, gloo-net]
6
+ repository: https://gitee.com/silentbalanceyh/r2mo-lain.git
7
+ ---
8
+
9
+ # mo-dev-login (concise)
10
+
11
+ Leptos (CSR) + Trunk + gloo-net: login via GET `/app/name/{Z_APP}` and POST `/auth/login`; session keys (LocalStorage appId, SessionStorage token+userId); auth guard in layout; session restore on load. Align with `.r2mo/api`.
12
+
13
+ ## Scope
14
+
15
+ **In**: `Z_ENDPOINT`/`Z_APP` (config, `option_env!`); GET `/app/name/{Z_APP}` on login mount → `data.id`; POST `/auth/login` → id/username/token; storage keys below; layout token check + redirect; main restore from `Ut::load_user()`.
16
+
17
+ **Out**: Backend, OAuth/SMS/email, register, MFA.
18
+
19
+ ## Storage keys
20
+
21
+ | Where | Key | Value |
22
+ |-------|-----|--------|
23
+ | LocalStorage | `app_id_{Z_APP}` | app id from `/app/name` |
24
+ | SessionStorage | `current_user_{appId}` | userId |
25
+ | SessionStorage | `user_data_{appId}_{userId}` | UserData (id, username, token, role, login_time) |
26
+
27
+ `UserData` must include `id` (from login response).
28
+
29
+ ## Files (minimal)
30
+
31
+ | File | Role |
32
+ |------|------|
33
+ | `src/config.rs` | `z_endpoint()`, `z_app()` via `option_env!`, defaults 6200 / r2-cloud.app-admin |
34
+ | `src/api/auth.rs` | `fetch_app_by_name()`, `login(user, pass)`; base from config |
35
+ | `src/models/auth.rs` | XApp, AppByNameResponse, RequestLoginCommon, ResponseLoginCommon, LoginResponse |
36
+ | `src/utils/storage.rs` | set/get_app_id (LS), save/load/clear_user (SS, keys above) |
37
+ | `src/pages/login.rs` | Mount: Effect + spawn_local fetch app → set_app_id; submit: login → save_user → nav /home |
38
+ | `src/components/layout.rs` | Effect: no token → clear_user, clear_app_id, nav /; logout same |
39
+ | `src/main.rs` | If `Ut::load_user().is_some()` → set is_logged_in, user_name |
40
+
41
+ ## WASM pitfalls & fixes
42
+
43
+ | Error / warning | Fix |
44
+ |-----------------|-----|
45
+ | `no method send for Result` | `Request::body()` returns Result → `.body(body_str).map_err(...)?` then `.send()` |
46
+ | `future cannot be sent` / not `Send` | Don't use `Resource::new` for fetch; use `Effect::new` + `spawn_local(async { fetch_app_by_name().await })` |
47
+ | `borrow of moved value: navigate` | Before Effect: `let navigate_auth = navigate.clone();` use `navigate_auth` in Effect |
48
+ | `?` can't convert serde_json::Error to JsValue | `.map_err(\|e\| JsValue::from(e.to_string()))?` in storage |
49
+ | unused imports (models), unused AppData (utils) | Drop re-exports or remove from `pub use` |
50
+ | `clear_app` never used | `#[allow(dead_code)]` on fn |
51
+
52
+ ## Snippets
53
+
54
+ **App fetch (no Resource):**
55
+ ```rust
56
+ Effect::new(move |_| {
57
+ let set_app_ready = set_app_ready.clone();
58
+ let set_error_message = set_error_message.clone();
59
+ spawn_local(async move {
60
+ match fetch_app_by_name().await {
61
+ Ok(app) => { let _ = Ut::set_app_id(&app.id); set_app_ready.set(true); }
62
+ Err(e) => set_error_message.set(e),
63
+ }
64
+ });
65
+ });
66
+ ```
67
+
68
+ **POST body:**
69
+ ```rust
70
+ let body_str = serde_json::to_string(&body).map_err(|e| e.to_string())?;
71
+ Request::post(&url).header("Content-Type", "application/json").body(body_str).map_err(...)? .send().await
72
+ ```
73
+
74
+ **Storage JSON error:**
75
+ ```rust
76
+ serde_json::to_string(data).map_err(|e| JsValue::from(e.to_string()))?
77
+ ```
78
+
79
+ ## Checklist
80
+
81
+ - [ ] Login page: GET app/name → set_app_id (LS); POST login → save_user (SS) → nav /home
82
+ - [ ] Layout: no token → clear + nav /; logout → clear_user, clear_app_id, nav /
83
+ - [ ] Main: load_user() → restore context
84
+ - [ ] cargo check / clippy clean
85
+
86
+ Ref: `.r2mo/api/operations/app.name.$name.get/`, `.r2mo/api/metadata.yaml` (/auth/login, @RequestLoginCommon, @ResponseLoginCommon), `r2-frontend-rust.mdc`, `.claude/skills/r2-dev-login`.
@@ -7,13 +7,15 @@ version: 1.0.0
7
7
  使用 `r2-req-page` 技能分析模块需求,生成对应模块下的页面需求,执行时先做一个 Plan,若无特殊说明采用最小需求的模式执行分析,保证模块的业务闭环。
8
8
 
9
9
  输入(模块目录中):
10
- - `metadata.yaml` 中包含了页面配置,若页面有缺失请直接创建新目录
10
+ - `metadata.yaml` 中包含了页面配置
11
11
  - `metadata.md` (可选)中包含了部分补充说明,虽然不完善,但依旧可遵循
12
12
  - `requirement.module.md` 中包含了模块需求
13
+ - API部分的内容参考:`.r2mo/api/metadata.yaml` ,`.r2mo/api/missed/*.yaml` 的定义
13
14
 
14
15
  输出:
15
16
  - 严格遵循 rules ( `*.mdc` ) 规范和 skills 中的规范
16
- - 输出内容精简,让AI可以更容易理解
17
+ - 输出内容精简,让AI可以更容易理解,但要保证页面开发就绪
18
+ - 按分析结果更新 `requirement.page.md` 和 `page.yaml`
17
19
 
18
20
  模块:ID,NAME,PATH
19
21
  --- END
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: 09 - 辅助系统 / 全系统整合
2
+ title: 09 - 辅助工具 / 全系统整合
3
3
  skill: r2-sys-integrate
4
4
  version: 1.1.0
5
5
  ---
@@ -8,12 +8,19 @@ version: 1.0.0
8
8
 
9
9
  输入:
10
10
  - {MOD}/menu.yaml,模块目录下的菜单配置
11
- - {MOD}/{PAGE},模块目录下的页面目录清单
11
+ - {MOD}/metadata.yaml,模块需求中的页面清单
12
+ - {MOD}/{PAGE-DIRECTORY},模块目录下的页面目录清单
12
13
 
13
- > `MOD` 表示模块目录,位于当前项目的 `src/pages/` 下。
14
+ > `MOD` 表示模块目录,位于当前项目的 `src/pages/`
14
15
 
15
16
  输出:
16
- - 以 `menu.yaml` 为基础,追加缺失的页面信息
17
- - `menu.yaml` 为基础,将多余的页面信息全部删除
17
+ - 以 `metadata.yaml` 中的页面为第一优先级,为了能开发**业务流程**完整的模块
18
+ - 创建缺失的页面(包括内部的 `page.yaml`、`requirement.page.md`
19
+ - 删除不再使用的页面(没有存在于 `menu.yaml` 和 `metadata.yaml` 中)
20
+ - 以 `menu.yaml` 为基础,检查是否有页面和它对齐,产生最终的报告,未对齐的部分再次**创建缺失页面**(按步骤一对齐),报告输出位置:`docs/pages/` 目录
21
+ - 严格限制:
22
+ - 整个过程中不要更新 `menu.yaml` 文件
23
+ - `menu.yaml` 和 `metadata.yaml` 中出现过的页面确认有目录与之对应
24
+ - 没有多余页面目录存在于 `menu.yaml` 和 `metadata.yaml` 中
18
25
 
19
26
  --- END
@@ -0,0 +1,19 @@
1
+ ---
2
+ title: 11 - 辅助工具 / 缺失接口汇总
3
+ skill:
4
+ version: 1.0.0
5
+ ---
6
+ --- BEGIN
7
+ 分析所有模块根目录( `src/pages/{MOD}/`)生成的 `miss-api.md` 缺失集合,在 `src/pages` 中生成两份缺失接口文档。基于现有的业务需求,参考三类型原则
8
+ - 最小版本
9
+ - 标准版本
10
+ - 完整版本
11
+ 但缺失接口只考虑边界版本:**最小版本**和**完整版本**。
12
+
13
+ 输出:
14
+ - 生成最小缺失接口版本:`.r2mo/api/missed/min.yaml`,最小版本可以保证业务的基本闭环,剪裁掉一些无关紧要的接口。
15
+ - 生成全缺失接口版本:`.r2mo/api/missed/full.yaml`,全版本基于现有需求生成的所有 `miss-api.md` 整合。
16
+ - `min.yaml` 和 `full.yaml` 两份格式采用 OpenAPI 3.1 的规范。
17
+ - 生成的 OpenAPI 中接口不可重复,相同路径下的接口合并到一起。
18
+
19
+ --- END
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "executor": "executeApply",
3
- "description": "从远程仓库安装技能到本地项目(仅支持远程)",
3
+ "description": "从远程仓库安装技能到当前项目(默认);-i 将当前项目 skills/ 反馈到 Z_LAIN_SKILL/skills",
4
4
  "command": "apply",
5
5
  "options": [
6
6
  {
7
7
  "name": "remote",
8
8
  "alias": "r",
9
- "description": "从远程仓库安装技能(可选指定仓库名)",
9
+ "description": "远程仓库名(可选,默认仍从远程安装)",
10
10
  "type": "string"
11
+ },
12
+ {
13
+ "name": "import",
14
+ "alias": "i",
15
+ "description": "反馈模式:将当前项目 skills/ 拷贝到 Z_LAIN_SKILL/skills",
16
+ "type": "boolean"
11
17
  }
12
18
  ]
13
19
  }
@@ -1 +1 @@
1
- {"executor":"executeDict","description":"从 .r2mo/api/components/schemas 读取 XTabular/XCategory 结构,按 app.env 连接业务库导出 X_TABULAR/X_CATEGORY .r2mo/dict/(dict.{type}.yaml / tree.{type}.yaml)","command":"dict","options":[{"name":"dir","alias":"d","description":"项目根目录(默认为当前目录)","default":"."}]}
1
+ {"executor":"executeDict","description":"从 .r2mo/api/components/schemas 读取结构并导出字典到 .r2mo/data/dbdict;-r 逆向从 dbdict yaml 生成 flyway SQL","command":"dict","options":[{"name":"dir","alias":"d","description":"项目根目录(默认为当前目录)","default":"."},{"name":"reverse","alias":"r","description":"逆向:以 .r2mo/data/dbdict 的 yaml 为输入,在 -domain 或当前项目 flyway 目录下生成 SQL 脚本","type":"boolean"}]}
@@ -5,8 +5,8 @@ const Ec = require('../epic');
5
5
  const fsAsync = require('fs').promises;
6
6
  const readline = require('readline');
7
7
  const { execSync } = require('child_process');
8
- const { parseOptional } = require('../utils/momo-args');
9
- const { copyDir, readJson, parseFile } = require('../utils/momo-file-utils');
8
+ const { parseOptional, parseBool } = require('../utils/momo-args');
9
+ const { copyDir, readJson, parseFile, ensureDir, exists } = require('../utils/momo-file-utils');
10
10
  const { selectSingle } = require('../utils/momo-menu');
11
11
 
12
12
  // 远程仓库配置文件路径
@@ -576,41 +576,83 @@ const _installFromRemote = async (repository, targetPath) => {
576
576
  Ec.info(`✅ 成功安装 ${selectedIndices.length} 个技能!`);
577
577
  };
578
578
 
579
- module.exports = async (options) => {
580
- // 检查 -r 参数
581
- const { hasFlag: hasRemote, value: remoteValue } = parseOptional('remote', 'r');
582
-
583
- if (!hasRemote) {
584
- Ec.error('❌ 此命令必须使用 -r 参数指定远程仓库');
585
- Ec.waiting('用法: momo apply -r [仓库名]');
579
+ /**
580
+ * -i 模式:将当前项目 skills/ 拷贝到 Z_LAIN_SKILL/skills,重名时逐个询问是否覆盖
581
+ */
582
+ const _importToLainSkills = async () => {
583
+ const projectDir = process.cwd();
584
+ const projectSkillsDir = path.join(projectDir, 'skills');
585
+ const lainRoot = process.env.Z_LAIN_SKILL;
586
+ if (!lainRoot || !String(lainRoot).trim()) {
587
+ Ec.error('❌ 环境变量 Z_LAIN_SKILL 未设置');
588
+ Ec.warn('请设置 Z_LAIN_SKILL 指向 Lain 技能根目录后再执行 momo apply -i');
589
+ process.exit(1);
590
+ }
591
+ const destDir = path.join(lainRoot.trim(), 'skills');
592
+ if (!exists(projectSkillsDir) || !fs.statSync(projectSkillsDir).isDirectory()) {
593
+ Ec.warn(`当前项目下未找到 skills/ 目录: ${projectSkillsDir}`);
586
594
  process.exit(1);
587
595
  }
596
+ const skills = _scanSkillsFromDir(projectSkillsDir);
597
+ if (skills.length === 0) {
598
+ Ec.warn('skills/ 目录下未找到任何有效技能');
599
+ process.exit(0);
600
+ }
601
+ Ec.waiting(`当前项目 skills/: ${projectSkillsDir}`);
602
+ Ec.waiting(`目标目录: ${destDir}`);
603
+ await ensureDir(destDir);
604
+ let copied = 0;
605
+ for (const skill of skills) {
606
+ const destPath = path.join(destDir, skill.dirname);
607
+ if (exists(destPath)) {
608
+ try {
609
+ const answer = await Ec.ask(`技能 "${skill.name}" 已存在,是否覆盖?(y/N): `);
610
+ if (!/^y|yes$/i.test((answer || '').trim())) {
611
+ Ec.waiting(` 跳过: ${skill.name}`);
612
+ continue;
613
+ }
614
+ } catch (e) {
615
+ Ec.waiting('已取消');
616
+ process.exit(0);
617
+ }
618
+ await fsAsync.rm(destPath, { recursive: true, force: true });
619
+ }
620
+ await _copyDirectory(skill.path, destPath);
621
+ Ec.waiting(` ✓ 已拷贝: ${skill.name}`);
622
+ copied++;
623
+ }
624
+ Ec.info(`✅ 已反馈 ${copied} 个技能到 ${destDir}`);
625
+ };
626
+
627
+ module.exports = async (options) => {
628
+ const isImport = parseBool('import', 'i');
629
+ const { value: remoteValue } = parseOptional('remote', 'r');
630
+
631
+ if (isImport) {
632
+ await _importToLainSkills();
633
+ process.exit(0);
634
+ }
588
635
 
589
- // 加载远程仓库配置
636
+ // 默认:从远程仓库安装技能到当前项目(原 momo apply -r)
590
637
  const repositories = _loadRepositories();
591
-
592
638
  if (repositories.length === 0) {
593
639
  Ec.error('❌ 未找到任何远程仓库配置');
594
- Ec.waiting(`请检查配置文件: ${REPOSITORIES_CONFIG}`);
640
+ Ec.warn(`请检查配置文件: ${REPOSITORIES_CONFIG}`);
595
641
  process.exit(1);
596
642
  }
597
643
 
598
- // 选择仓库
599
644
  const repository = await _selectRepository(repositories, remoteValue);
600
645
  if (!repository) {
601
646
  Ec.waiting('已取消选择仓库');
602
647
  process.exit(0);
603
648
  }
604
649
 
605
- // 选择目标路径
606
650
  const targetPathConfig = await _selectTargetPath();
607
651
  if (!targetPathConfig) {
608
652
  Ec.waiting('已取消选择目标路径');
609
653
  process.exit(0);
610
654
  }
611
655
 
612
- // 从远程仓库安装技能
613
656
  await _installFromRemote(repository, targetPathConfig.path);
614
-
615
657
  process.exit(0);
616
658
  };
@@ -1,15 +1,14 @@
1
1
  /**
2
- * momo dict [-d <dir>]
3
- * 1. 读取 targetDir/.r2mo/api/components/schemas 中的 XTabular、XCategory 作为字典结构依据
4
- * 2. DPA 时从 x-api/.r2mo/app.env 读取数据库配置,ONE 时从当前项目根 .r2mo/app.env 读取
5
- * 3. 从业务库 X_TABULAR、X_CATEGORY 按 TYPE 导出,写入 targetDir/.r2mo/data/dbdict/:dict.{type}.yaml、tree.{type}.yaml(YAML 使用 Schema 属性名,非数据库字段名)
2
+ * momo dict [-d <dir>] [-r]
3
+ * 正向:从 schemas + 业务库导出字典到 targetDir/.r2mo/data/dbdict/
4
+ * 逆向 -r:以 .r2mo/data/dbdict yaml 为输入,在 -domain 或当前项目 flyway 目录下生成 SQL 脚本
6
5
  */
7
6
  const path = require('path');
8
7
  const fs = require('fs');
9
8
  const fsAsync = require('fs').promises;
10
9
  const Ec = require('../epic');
11
- const { parseOptional } = require('../utils/momo-args');
12
- const { exists, ensureDir } = require('../utils/momo-file-utils');
10
+ const { parseOptional, parseBool } = require('../utils/momo-args');
11
+ const { exists, ensureDir, parseFile } = require('../utils/momo-file-utils');
13
12
 
14
13
  /** 懒加载可选依赖,缺失时自动执行 npm install -g 并重试,仍失败则返回 null */
15
14
  const _requireOptional = (moduleName, installHint) => {
@@ -58,6 +57,12 @@ const _snakeToCamel = (str) => {
58
57
  .join('');
59
58
  };
60
59
 
60
+ /** camelCase 转 UPPER_SNAKE(逆向写 SQL 列名用) */
61
+ const _camelToSnake = (str) => {
62
+ if (!str || typeof str !== 'string') return str;
63
+ return str.replace(/([A-Z])/g, '_$1').replace(/^_/, '').toUpperCase();
64
+ };
65
+
61
66
  /** 从 Schema .md 中解析 properties 下的属性名列表(顺序与 Schema 一致) */
62
67
  const _parseSchemaProperties = (schemasDir, entityName) => {
63
68
  const mdPath = path.join(schemasDir, `${entityName}.md`);
@@ -200,9 +205,95 @@ const _getDbConfigFromEnv = () => {
200
205
  };
201
206
  };
202
207
 
208
+ // ---------- 逆向 -r:dbdict yaml -> flyway SQL ----------
209
+
210
+ /** 递归查找包含 .sql 的 flyway 目录(优先 MYSQL,其次任意含 .sql 的目录) */
211
+ const _findFlywaySqlDir = (startDir, maxDepth = 6) => {
212
+ const search = (dir, depth) => {
213
+ if (depth > maxDepth) return null;
214
+ if (!exists(dir) || !fs.statSync(dir).isDirectory()) return null;
215
+ const lower = dir.toLowerCase();
216
+ if (lower.includes('flyway') && (lower.includes('mysql') || path.basename(dir) === 'MYSQL')) {
217
+ try {
218
+ const files = fs.readdirSync(dir);
219
+ if (files.some((f) => f.endsWith('.sql'))) return dir;
220
+ } catch (e) {}
221
+ }
222
+ try {
223
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
224
+ for (const e of entries) {
225
+ if (!e.isDirectory()) continue;
226
+ const next = path.join(dir, e.name);
227
+ const found = search(next, depth + 1);
228
+ if (found) return found;
229
+ }
230
+ } catch (e) {}
231
+ return null;
232
+ };
233
+ return search(startDir, 0);
234
+ };
235
+
236
+ /** 在 flyway 目录中查找与 tableName + typeLabel 匹配的 SQL 文件,返回 { sqlFile, sqlPath } 或 null */
237
+ const _findMatchingSqlFile = (flywaySqlDir, tableName, typeLabel) => {
238
+ if (!exists(flywaySqlDir)) return null;
239
+ const safeType = (typeLabel || '').replace(/\s+/g, '.');
240
+ const suffix = `.${tableName}.${safeType}.sql`;
241
+ const files = fs.readdirSync(flywaySqlDir).filter((f) => f.endsWith('.sql') && f.endsWith(suffix));
242
+ if (files.length === 0) return null;
243
+ const sqlFile = files[0];
244
+ const sqlPath = path.resolve(flywaySqlDir, sqlFile);
245
+ return { sqlFile, sqlPath };
246
+ };
247
+
248
+ /** 从 flyway 目录中解析已有 SQL 文件名(匹配表名+type),取最大版本号并返回下一段 */
249
+ const _nextSqlVersion = (flywaySqlDir, tableName, typeLabel) => {
250
+ if (!exists(flywaySqlDir)) return '001.000.001';
251
+ const files = fs.readdirSync(flywaySqlDir).filter((f) => f.endsWith('.sql'));
252
+ const safeType = (typeLabel || '').replace(/\s+/g, '.');
253
+ const versionRe = new RegExp(`^R__([\\d.]+)\\.${tableName}\\.${safeType.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.sql$`, 'i');
254
+ let maxThird = 0;
255
+ const fallbackRe = new RegExp(`^R__([\\d.]+)\\.${tableName}\\.`, 'i');
256
+ for (const f of files) {
257
+ let m = f.match(versionRe);
258
+ if (!m && safeType) m = f.match(fallbackRe);
259
+ if (m) {
260
+ const parts = m[1].split('.').map(Number);
261
+ const third = parts.length >= 3 ? parts[2] : 0;
262
+ if (third > maxThird) maxThird = third;
263
+ }
264
+ }
265
+ const nextThird = maxThird + 1;
266
+ return `211.001.${String(nextThird).padStart(3, '0')}`;
267
+ };
268
+
269
+ /** 将 yaml 行数组转为 INSERT IGNORE SQL(camelCase 键转 UPPER_SNAKE,占位符 ${SIGMA}/${TENANT_ID}/${APP_ID}) */
270
+ const _yamlRowsToInsertSql = (rows, tableName, typeLabel) => {
271
+ if (!Array.isArray(rows) || rows.length === 0) return '';
272
+ const escape = (v) => {
273
+ if (v == null) return 'NULL';
274
+ if (typeof v === 'boolean') return v ? '1' : '0';
275
+ if (typeof v === 'number') return String(v);
276
+ return "'" + String(v).replace(/'/g, "''") + "'";
277
+ };
278
+ const cols = ['ID', 'CODE', 'NAME', 'COMMENT', 'ICON', 'SORT', 'TYPE', 'SIGMA', 'TENANT_ID', 'APP_ID', 'ACTIVE', 'LANGUAGE', 'CREATED_AT', 'CREATED_BY', 'UPDATED_AT', 'UPDATED_BY'];
279
+ const header = `-- =============================================================\n-- 逆向生成: ${tableName} (${typeLabel})\n-- =============================================================\n\nINSERT IGNORE INTO \`${tableName}\` (\n \`${cols.join('`, `')}\`\n) VALUES\n`;
280
+ const placeholders = "'${SIGMA}', '${TENANT_ID}', '${APP_ID}', 1, 'zh_CN', NOW(), '9a0d5018-33ad-4c64-80bf-8ae7947c482f', NOW(), '9a0d5018-33ad-4c64-80bf-8ae7947c482f'";
281
+ const vals = rows.map((row) => {
282
+ const code = escape(row.code);
283
+ const name = escape(row.name);
284
+ const comment = escape(row.comment);
285
+ const icon = escape(row.icon);
286
+ const sort = row.sort != null ? Number(row.sort) : 0;
287
+ const type = escape(row.type || typeLabel);
288
+ return `(UUID(), ${code}, ${name}, ${comment}, ${icon}, ${sort}, ${type}, ${placeholders})`;
289
+ });
290
+ return header + vals.join(',\n') + ';';
291
+ };
292
+
203
293
  module.exports = async () => {
204
294
  try {
205
295
  const dirArg = parseOptional('dir', 'd');
296
+ const isReverse = parseBool('reverse', 'r');
206
297
  const directory = (dirArg.value && dirArg.value.trim()) || '.';
207
298
  const basePath = path.resolve(process.cwd(), directory);
208
299
 
@@ -212,6 +303,81 @@ module.exports = async () => {
212
303
  if (projectName) Ec.waiting(`检测到 Maven 项目: ${projectName}`);
213
304
 
214
305
  const { type, targetDir } = await _detectProjectType(basePath, projectName);
306
+
307
+ if (isReverse) {
308
+ const dbdictDir = path.join(targetDir, ...R2MO_DBDICT_REL);
309
+ if (!exists(dbdictDir) || !fs.statSync(dbdictDir).isDirectory()) {
310
+ Ec.warn(`未找到 dbdict 目录: ${dbdictDir}`);
311
+ Ec.warn('逆向 -r 需要 DPA 时 -ui 下或 ONE 时当前项目下存在 .r2mo/data/dbdict');
312
+ process.exit(1);
313
+ }
314
+ Ec.waiting(`dbdict 输入: ${dbdictDir}`);
315
+
316
+ const yaml = _requireOptional('js-yaml', 'js-yaml');
317
+ if (!yaml) {
318
+ Ec.warn('需要 js-yaml,请执行: npm install js-yaml');
319
+ process.exit(1);
320
+ }
321
+ const yamlFiles = fs.readdirSync(dbdictDir).filter((f) => f.endsWith('.yaml') && (f.startsWith('dict.') || f.startsWith('tree.')));
322
+ if (yamlFiles.length === 0) {
323
+ Ec.warn(`未找到 dict.*.yaml 或 tree.*.yaml: ${dbdictDir}`);
324
+ process.exit(1);
325
+ }
326
+ const withMetadata = [];
327
+ for (const file of yamlFiles) {
328
+ const fullPath = path.join(dbdictDir, file);
329
+ const parsed = parseFile(fullPath);
330
+ if (!parsed || !parsed.attributes || !parsed.attributes.sqlPath) {
331
+ Ec.waiting(`跳过(无 metadata): ${file}`);
332
+ continue;
333
+ }
334
+ const sqlPath = String(parsed.attributes.sqlPath || '').trim();
335
+ const sqlFile = String(parsed.attributes.sqlFile || path.basename(sqlPath)).trim();
336
+ if (!sqlPath) continue;
337
+ let rows;
338
+ try {
339
+ rows = yaml.load(parsed.body || '[]');
340
+ } catch (e) {
341
+ Ec.warn(`跳过 ${file}: 解析 body 失败 ${e.message}`);
342
+ continue;
343
+ }
344
+ rows = Array.isArray(rows) ? rows : (rows && rows.items ? rows.items : []);
345
+ if (!rows.length) continue;
346
+ const isTree = file.startsWith('tree.');
347
+ const tableName = isTree ? TABLE_CATEGORY : TABLE_TABULAR;
348
+ const typeLabel = file.replace(/^(dict|tree)\.|\.yaml$/gi, '').trim() || 'default';
349
+ withMetadata.push({ file, fullPath, sqlFile, sqlPath, rows, tableName, typeLabel });
350
+ }
351
+ if (withMetadata.length === 0) {
352
+ Ec.warn('未找到含有 sqlPath 元数据的 yaml 文件,逆向跳过');
353
+ process.exit(0);
354
+ }
355
+ Ec.info(`检测到 ${withMetadata.length} 个 yaml 含有 SQL 路径元数据`);
356
+ try {
357
+ const answer = await Ec.ask('是否全覆盖已存在的 SQL 文件?(y/N): ');
358
+ if (!/^y|yes$/i.test((answer || '').trim())) {
359
+ Ec.waiting('已取消覆盖');
360
+ process.exit(0);
361
+ }
362
+ } catch (e) {
363
+ Ec.waiting('已取消');
364
+ process.exit(0);
365
+ }
366
+ const written = [];
367
+ for (const item of withMetadata) {
368
+ const sqlContent = _yamlRowsToInsertSql(item.rows, item.tableName, item.typeLabel);
369
+ await fsAsync.writeFile(item.sqlPath, sqlContent, 'utf8');
370
+ written.push({ rel: item.sqlFile, full: item.sqlPath });
371
+ Ec.waiting(` ✓ 覆盖: ${item.sqlFile}`);
372
+ }
373
+ console.log('');
374
+ Ec.info('-------- 逆向已覆盖的 flyway SQL --------');
375
+ written.forEach(({ rel }) => Ec.info(' ' + rel));
376
+ Ec.info(`✅ 共覆盖 ${written.length} 个 SQL`);
377
+ console.log('');
378
+ process.exit(0);
379
+ }
380
+
215
381
  if (type === 'DPA') {
216
382
  Ec.waiting('项目类型: DPA / Domain, Provider, Api 经典架构');
217
383
  Ec.waiting(`目标目录(.r2mo/data/dbdict 落点): ${targetDir}`);
@@ -288,17 +454,36 @@ module.exports = async () => {
288
454
  if (!yaml) {
289
455
  process.exit(0);
290
456
  }
457
+ let flywaySqlDir = null;
458
+ if (type === 'DPA' && projectName) {
459
+ const domainPath = path.join(basePath, `${projectName}-domain`);
460
+ flywaySqlDir = _findFlywaySqlDir(domainPath);
461
+ }
462
+ if (!flywaySqlDir) flywaySqlDir = _findFlywaySqlDir(basePath);
463
+
291
464
  const written = [];
292
465
  for (const [typeVal, rows] of tabularByType) {
293
466
  const safeType = typeVal || 'default';
294
467
  const filePath = path.join(dbdictDir, `dict.${safeType}.yaml`);
295
- await fsAsync.writeFile(filePath, yaml.dump(rows, { lineWidth: -1 }), 'utf8');
468
+ let metadata = null;
469
+ if (flywaySqlDir) metadata = _findMatchingSqlFile(flywaySqlDir, TABLE_TABULAR, safeType);
470
+ const body = yaml.dump(rows, { lineWidth: -1 });
471
+ const content = metadata
472
+ ? '---\n' + yaml.dump({ sqlFile: metadata.sqlFile, sqlPath: metadata.sqlPath }) + '---\n' + body
473
+ : body;
474
+ await fsAsync.writeFile(filePath, content, 'utf8');
296
475
  written.push({ rel: `dict.${safeType}.yaml`, full: filePath });
297
476
  }
298
477
  for (const [typeVal, rows] of categoryByType) {
299
478
  const safeType = typeVal || 'default';
300
479
  const filePath = path.join(dbdictDir, `tree.${safeType}.yaml`);
301
- await fsAsync.writeFile(filePath, yaml.dump(rows, { lineWidth: -1 }), 'utf8');
480
+ let metadata = null;
481
+ if (flywaySqlDir) metadata = _findMatchingSqlFile(flywaySqlDir, TABLE_CATEGORY, safeType);
482
+ const body = yaml.dump(rows, { lineWidth: -1 });
483
+ const content = metadata
484
+ ? '---\n' + yaml.dump({ sqlFile: metadata.sqlFile, sqlPath: metadata.sqlPath }) + '---\n' + body
485
+ : body;
486
+ await fsAsync.writeFile(filePath, content, 'utf8');
302
487
  written.push({ rel: `tree.${safeType}.yaml`, full: filePath });
303
488
  }
304
489