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 +307 -0
- package/package.json +1 -1
- package/skills/mo-dev-login/SKILL.md +86 -0
- package/src/_template/R2MO/03.req-page.md +4 -2
- package/src/_template/R2MO/09.util-system.md +1 -1
- package/src/_template/R2MO/10.util-route.md +11 -4
- package/src/_template/R2MO/11.util-api-missed.md +19 -0
- package/src/commander/apply.json +8 -2
- package/src/commander/dict.json +1 -1
- package/src/executor/executeApply.js +58 -16
- package/src/executor/executeDict.js +193 -8
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
|
@@ -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
|
|
@@ -8,12 +8,19 @@ version: 1.0.0
|
|
|
8
8
|
|
|
9
9
|
输入:
|
|
10
10
|
- {MOD}/menu.yaml,模块目录下的菜单配置
|
|
11
|
-
- {MOD}/
|
|
11
|
+
- {MOD}/metadata.yaml,模块需求中的页面清单
|
|
12
|
+
- {MOD}/{PAGE-DIRECTORY},模块目录下的页面目录清单
|
|
12
13
|
|
|
13
|
-
> `MOD` 表示模块目录,位于当前项目的 `src/pages/`
|
|
14
|
+
> `MOD` 表示模块目录,位于当前项目的 `src/pages/` 下
|
|
14
15
|
|
|
15
16
|
输出:
|
|
16
|
-
- 以 `
|
|
17
|
-
-
|
|
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
|
package/src/commander/apply.json
CHANGED
|
@@ -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
|
}
|
package/src/commander/dict.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"executor":"executeDict","description":"从 .r2mo/api/components/schemas
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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.
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|