tha-cli 0.0.0-stage → 0.1.0

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 (52) hide show
  1. package/ARCHITECTURE.md +193 -0
  2. package/LICENSE +21 -0
  3. package/README.md +408 -2
  4. package/package.json +42 -4
  5. package/tha/cli.mjs +116 -0
  6. package/tha/commands/adopt.mjs +119 -0
  7. package/tha/commands/check.mjs +226 -0
  8. package/tha/commands/diff.mjs +70 -0
  9. package/tha/commands/init.mjs +262 -0
  10. package/tha/commands/remove.mjs +328 -0
  11. package/tha/commands/sync.mjs +826 -0
  12. package/tha/config/field.mjs +178 -0
  13. package/tha/config/load.mjs +188 -0
  14. package/tha/config/manifest.mjs +75 -0
  15. package/tha/config/paths.mjs +124 -0
  16. package/tha/config/project.mjs +52 -0
  17. package/tha/config/rules.mjs +85 -0
  18. package/tha/config/schema.mjs +306 -0
  19. package/tha/config/template.mjs +78 -0
  20. package/tha/generate/domain/api.mjs +67 -0
  21. package/tha/generate/domain/context.mjs +76 -0
  22. package/tha/generate/domain/detail.mjs +35 -0
  23. package/tha/generate/domain/entry.mjs +91 -0
  24. package/tha/generate/domain/executors.mjs +93 -0
  25. package/tha/generate/domain/form.mjs +34 -0
  26. package/tha/generate/domain/index.mjs +72 -0
  27. package/tha/generate/domain/permissions.mjs +35 -0
  28. package/tha/generate/domain/store.mjs +68 -0
  29. package/tha/generate/domain/view.mjs +29 -0
  30. package/tha/generate/global.mjs +111 -0
  31. package/tha/generate/header.mjs +29 -0
  32. package/tha/generate/names.mjs +45 -0
  33. package/tha/generate/plan.mjs +41 -0
  34. package/tha/kit/ThaCrudPage.vue +344 -0
  35. package/tha/kit/ThaDetail.vue +146 -0
  36. package/tha/kit/ThaForm.vue +194 -0
  37. package/tha/kit/request.ts +30 -0
  38. package/tha/kit/transport.ts +11 -0
  39. package/tha/utils/args.mjs +59 -0
  40. package/tha/utils/compare.mjs +38 -0
  41. package/tha/utils/files.mjs +175 -0
  42. package/tha/utils/format.mjs +23 -0
  43. package/tha/utils/output.mjs +79 -0
  44. package/tha/utils/style.mjs +147 -0
  45. package/tha/utils/ts.mjs +73 -0
  46. package/tha/verify/ast.mjs +143 -0
  47. package/tha/verify/boundary.mjs +219 -0
  48. package/tha/verify/contract.mjs +186 -0
  49. package/tha/verify/openapi.mjs +345 -0
  50. package/tha/verify/rules.mjs +91 -0
  51. package/tha/verify/scan.mjs +59 -0
  52. package/tha.mjs +22 -0
@@ -0,0 +1,193 @@
1
+ # THA 架构设计
2
+
3
+ > 本文讲**为什么这么设计**:要解决什么、靠什么结构解决、代价是什么。
4
+ > 怎么安装、`config.ts` 怎么写、命令怎么用,见 [README.md](./README.md)。
5
+ > 每条约束的原话在 `tha/verify/rules.mjs`,本文引用的都是那里的 `basis`。
6
+
7
+ ---
8
+
9
+ ## 1. 要解决的问题
10
+
11
+ 前端业务模块的腐化不是某一处写错,而是**每一处单看都合理、堆起来就不可维护**。典型的五种症状:
12
+
13
+ | 症状 | 后果 |
14
+ |---|---|
15
+ | 页面直接调接口 | 接口一改,页面跟着改 |
16
+ | 状态层里发请求 | 状态和 IO 搅在一起,没法单独测 |
17
+ | 执行器互相 import | 改一个用例,炸三个 |
18
+ | 执行器绕开上下文自己拿依赖 | 依赖关系散落各处,换实现要全局搜 |
19
+ | 一个模块被创建多份 | 「私人大脑」,状态不同步 |
20
+
21
+ 共同点是:靠 code review 抓不住,只能靠结构。所以 THA 不从「写代码的规范」入手,而是**先把一个业务模块的形状定死**。
22
+
23
+ ---
24
+
25
+ ## 2. 一个模块的形状
26
+
27
+ ```
28
+ view 表达意图,不碰实现
29
+ ↓
30
+ hub 单例,只分发,不写逻辑
31
+ ↓
32
+ executor 用例,依赖全部从 ctx 来
33
+ ↓
34
+ step 被第二个用例复用时才下沉
35
+ ```
36
+
37
+ | 层 | 职责 | 不做什么 |
38
+ |---|---|---|
39
+ | view 页面 | 调中枢的动作、读状态 | 不调接口、不建实例、不碰执行器内部 |
40
+ | hub 中枢 | 模块级单例;把动作挂上去 | 不写业务逻辑,不 import `steps/` |
41
+ | executor 执行器 | 写用例,依赖全部走 ctx | 不 import 网络层/状态层/路由,不互相 import |
42
+ | step 零件 | 被多个执行器复用的函数 | 不预判未来,只有一个使用者时不下沉 |
43
+ | context | 提供 state / api / ui / run / can | 不写业务判断 |
44
+
45
+ 形状固定之后,**「这段逻辑该放哪」就不再需要讨论**:有副作用的 IO 进执行器,被复用的纯步骤进步骤,页面只表达意图。
46
+
47
+ ---
48
+
49
+ ## 3. 四条结构决定
50
+
51
+ ### 3.1 中枢只分发
52
+
53
+ > 「中枢只做分发:把意图交给执行器;禁止直接依赖执行器内部步骤」(HUB-01)
54
+
55
+ 入口一旦开始写逻辑,就会长成上帝对象:先是一个 `if`,然后是一次请求,最后整个模块的业务都堆在 `index.ts` 里。所以中枢只做两件事:**单例缓存、挂载动作**。控制类逻辑(比如中断当前任务)也必须放进执行器,不许写在入口。
56
+
57
+ ### 3.2 执行器只认 ctx
58
+
59
+ > 「执行器只认 ctx:网络走 ctx.api,状态走 ctx.state」(EXE-02)
60
+
61
+ 生成器的 `context.ts` 提供五样东西:
62
+
63
+ ```ts
64
+ interface Context {
65
+ state // 状态
66
+ api // 网络
67
+ ui // toast
68
+ run // 统一的 pending / error / finally 包装
69
+ can // 权限判定,入口和页面按钮共用
70
+ }
71
+ ```
72
+
73
+ 这是**最小集**,不够就往 `context.ts` 里加——加在上下文,不加在执行器里。好处是:换实现只改一处;写用例测试时塞一个假 ctx 就能跑,不用起 store 和网络。
74
+
75
+ ### 3.3 执行器互不引用
76
+
77
+ > 「执行器之间互不引用:共用逻辑下沉成零件,用例顺序留在各自执行器里」(EXE-01)
78
+
79
+ 两个用例要共用一段逻辑,正确做法是抽到 `steps/`,不是 A import B。执行器之间的横向引用会形成网,而不是树——网的改动影响范围算不出来。
80
+
81
+ ### 3.4 零件按需下沉,不预判
82
+
83
+ > 「零件是「同一段步骤被第二个用例用到」才下沉的;只有一个使用者是过度设计」(STEP-01)
84
+
85
+ 这是全套规则里唯一**反对抽象**的一条,而且是 warn 不是 error。它防的是「我先抽个工具函数,以后肯定用得上」——过度设计比重复更贵,因为重复看得见,过度设计看不见。
86
+
87
+ ---
88
+
89
+ ## 4. 依赖方向必须单向
90
+
91
+ ```
92
+ view → hub → executor → step
93
+ ↑
94
+ ctx 提供一切外部能力
95
+ ```
96
+
97
+ 配套两条边界:
98
+
99
+ - **状态层只存状态,不发请求**(STORE-01)。请求属于用例,状态只负责被写入。
100
+ - **传输层只上报不越权**(UTIL-01)。错误总线只管把错误抛上来,不决定怎么展示、不感知业务模块。
101
+
102
+ 单向依赖的价值不是「好看」,是**改动影响范围可预测**:改 `steps/` 只影响调用它的执行器,改 `utils/` 不影响任何业务判断。
103
+
104
+ ---
105
+
106
+ ## 5. 边界不靠人记,靠工具查
107
+
108
+ 架构约束写在文档里,没人会读;写成可执行的检查,才有约束力。三个机制:
109
+
110
+ ### 5.1 规则表与判断逻辑分离
111
+
112
+ `tha/verify/rules.mjs` 只放「说给人听的话」——`id` / `level` / `title` / `basis` / `fix`;
113
+ 判断逻辑在 `tha/verify/boundary.mjs`。改文案不动逻辑,改逻辑不动文案。
114
+
115
+ ### 5.2 每条规则带 `basis`
116
+
117
+ `basis` 直接引用架构原话。输出违规时,能回溯到「哪条约束被破了」,而不是「工具说不行」。
118
+
119
+ 违规条目按这个形状输出:
120
+
121
+ ```
122
+ src/domains/doc/index.ts:1 HUB-03
123
+ 问题:模块入口不是单例,被重复创建
124
+ 没找到缓存的实例(每次调用都会新建一个)
125
+ 应该:在模块顶层用 let 缓存:if (!module) module = createXxxModule()
126
+ 依据:每个领域全局唯一实例,视图不能各造一个「私人大脑」
127
+ ```
128
+
129
+ ### 5.3 `import type` 一律放行
130
+
131
+ 类型不构成运行时依赖。只查值导入,规则才不至于因噎废食。
132
+
133
+ ---
134
+
135
+ ## 6. 生成与检查分离
136
+
137
+ | | 管什么 | 不管什么 |
138
+ |---|---|---|
139
+ | 产线(sync) | 标准形态:`config.ts` → 5 个文件 + 注册表 | 不生成业务逻辑 |
140
+ | 护栏(check) | 所有代码,包括完全手写的模块 | 不替你改代码 |
141
+
142
+ `adopt` 的语义最能说明这个分工:**放弃「写」,不放弃「管」**。文件交给人之后,生成器退出,但分层规则照样查。这样「生成」和「约束」就是两件可以独立演进的事——生成器可以不管某个文件,但架构约束不能有盲区。
143
+
144
+ ---
145
+
146
+ ## 7. 配置面收窄
147
+
148
+ `config.ts` 顶层只有 9 个键,多写一个直接报错,当次一个文件都不生成。
149
+
150
+ 逻辑:**配置项每多一个,生成器要处理的组合就翻一倍,边界就越难守**。宁可让人在代码里写,也不让配置去表达——代码里的复杂度是局部的,配置里的复杂度是全局的。
151
+
152
+ ---
153
+
154
+ ## 8. 代价
155
+
156
+ 说清楚取舍的另一面:
157
+
158
+ 1. **只覆盖标准 CRUD**。工作流、嵌套表单、自定义布局仍然人写,工具只保证不覆盖、不误报。
159
+ 2. **小模块显得重**。一个简单模块也要 5~6 个文件,抽象成本前置了。
160
+ 3. **有学习曲线**。得先理解 hub / executor / step / ctx 四层语义才能顺畅写。
161
+ 4. **原型阶段会变慢**。强约束要求先想清楚形状再动手。
162
+ 5. **生成物是复制不是引用**。CLI 升级后,项目里已有的基线组件不会自动更新。
163
+
164
+ 所以它的定位不是「通用代码生成器」,而是:**让一个人写业务的时候,不需要记架构**。约束由工具承担,人只负责业务本身。
165
+
166
+ ---
167
+
168
+ ## 9. 规则索引
169
+
170
+ | id | 级别 | 约束原话(`basis`) |
171
+ |---|---|---|
172
+ | HUB-01 | error | 中枢只做分发:把意图交给执行器;禁止直接依赖执行器内部步骤 |
173
+ | HUB-03 | warn | 每个领域全局唯一实例,视图不能各造一个「私人大脑」 |
174
+ | EXE-01 | error | 执行器之间互不引用:共用逻辑下沉成零件,用例顺序留在各自执行器里 |
175
+ | EXE-02 | error | 执行器只认 ctx:网络走 ctx.api,状态走 ctx.state |
176
+ | EXE-03 | error | 执行器只认 ctx:日志 / 时钟 / 流 / 鉴权走 ctx.ports,判定走 ctx.errors |
177
+ | STEP-01 | warn | 零件是「同一段步骤被第二个用例用到」才下沉的;只有一个使用者是过度设计 |
178
+ | VIEW-01 | error | 视图只表达意图:动作走域中枢,数据直读状态层 |
179
+ | VIEW-02 | error | 每个领域全局唯一实例:视图只能经 useXxxModule() 取,不能自己 create |
180
+ | STORE-01 | error | 状态层只存状态,不发请求 |
181
+ | UTIL-01 | error | 传输层只上报不越权:不感知业务模块,不做展示 |
182
+
183
+ 规则真源:[`tha/verify/rules.mjs`](./tha/verify/rules.mjs) 判断逻辑:[`tha/verify/boundary.mjs`](./tha/verify/boundary.mjs)
184
+
185
+ `EXE-03` 提到的 `ctx.ports` / `ctx.errors` 是架构原话里的完整形态;生成器当前的 `context.ts` 只给最小集(`state` / `api` / `ui` / `run` / `can`),需要时按 3.2 往上下文里加。
186
+
187
+ ---
188
+
189
+ ## 10. 相关文档
190
+
191
+ - [README.md](./README.md)——安装、命令、`config.ts` 全量配置项、契约与退出码
192
+ - `tha/generate/domain/`——每一类生成物的模板,是「形状」的可执行版本
193
+ - `tha/verify/rules.mjs`——规则真源,改架构先改这里
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 温强
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,409 @@
1
- # Temporary Holding Version
1
+ # tha-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ THA 架构的构建期工具。两副面孔:**产线负责生成,护栏负责守边界。**
4
+
5
+ - **产线**:读 `domains/<域>/config.ts`,生成标准文件(基线 5 个,按 `pages` 追加详情 / 表单)和全局注册表。
6
+ - **护栏**:没有 `config.ts` 的手写模块,不生成,只按层查边界。
7
+ - **自带基线**:`init` 顺手补齐生成物要 import 的组件和工具,空项目也能直接跑。
8
+
9
+ 产物不依赖 `tha`:上线后可以把它从 `devDependencies` 删掉。
10
+
11
+ 设计思想、取舍与代价见 [ARCHITECTURE.md](./ARCHITECTURE.md);本文只讲怎么用。
12
+
13
+ ---
14
+
15
+ ## THA 架构
16
+
17
+ 分层,依赖只能向下:
18
+
19
+ ```
20
+ views 页面 只调中枢,不碰网络层和执行器内部
21
+ domains 领域 中枢(单例,只挂动作)→ 执行器(用例)→ 零件(被复用的函数)
22
+ stores 状态 只管状态和原子更新,不发请求
23
+ api 网络 请求和类型
24
+ utils 基础 纯函数、远端传输
25
+ ```
26
+
27
+ 一个领域只有一个入口:
28
+
29
+ - **中枢** `index.ts` 对外只暴露 `useXxxModule()`,模块级单例,重复创建即违规。
30
+ - **执行器** 写用例,依赖全部走 `context.ts` 注入,不直接 import 网络层、状态层、路由。
31
+ - **零件** 是被多个执行器复用的函数,只被一个执行器用时不算零件。
32
+
33
+ 这套约束的目的很单一:业务写在一处,边界由工具守,不靠人记。
34
+
35
+ 为什么这么切分、每条约束的原话和代价,见 [ARCHITECTURE.md](./ARCHITECTURE.md)。
36
+
37
+ ---
38
+
39
+ ## tha-cli 做什么
40
+
41
+ | 模式 | 输入 | 产物 |
42
+ |---|---|---|
43
+ | 产线 | `config.ts` | 标准 CRUD 的 api / store / context / 执行器 / 页面 / 路由 / 菜单 |
44
+ | 护栏 | 任意源码 | 违规清单(10 条边界规则 + 契约比对) |
45
+
46
+ 它不生成业务逻辑。`config.ts` 声明「这个模块有哪些字段、开哪些动作」,工具把标准部分铺好;复杂逻辑仍然人写,写在自定义执行器里,工具不覆盖。
47
+
48
+ ---
49
+
50
+ ## 安装
51
+
52
+ ```bash
53
+ npm i -D tha-cli
54
+ npx tha-cli check
55
+ ```
56
+
57
+ 前置条件:
58
+
59
+ - **TypeScript 项目**(`.ts` / `.vue`)。纯 JS 项目不支持——模块靠 `config.ts` 识别,生成物也全是 TS。
60
+ - 不需要自己装 `typescript`。CLI 自带一份 5.x,只用于解析源码(读 `config.ts`、遍历 AST),不参与你项目的构建——你项目里用 5.x 还是 7.x 都不影响它。
61
+
62
+ ---
63
+
64
+ ## 快速开始
65
+
66
+ ```bash
67
+ tha init doc # 建 config 模板,不生成代码
68
+ # 编辑 src/domains/doc/config.ts
69
+ tha sync # 生成 5 个文件;中枢缺失时补一次骨架
70
+ tha check # 检查:配置 + 契约 + 护栏 + 漂移
71
+ ```
72
+
73
+ CI:
74
+
75
+ ```bash
76
+ tha check # 有 error 即非零退出
77
+ tha check --strict # 警告也算失败
78
+ tha check --json # 机器可消费
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 命令
84
+
85
+ | 命令 | 作用 |
86
+ |---|---|
87
+ | `tha init <域>` | 建 config 模板 + 补齐基线(`--bare` 只写 config) |
88
+ | `tha sync [<域>]` | 生成 / 更新标准 CRUD 与注册表;补一次中枢骨架 |
89
+ | `tha check` | 只检查不写:配置 + 契约(响应与请求体)+ 护栏 + 漂移 |
90
+ | `tha diff` | 显示将要发生的文件变化 |
91
+ | `tha remove <域>` | 删模块:生成物清掉,手写文件保留;保留的文件若还引用已删生成物会明确报出来 |
92
+ | `tha adopt <文件...>` | 这个文件以后你自己维护:不再自动生成,但分层规则照样查 |
93
+ | `tha adopt <文件...> --release` | 交回生成管理 |
94
+
95
+ 参数按用途分组:
96
+
97
+ - 初始化:`--bare`(不补基线)、`--config`(写目录映射示例)
98
+ - 写盘:`--all`、`--force`、`--yes`(非交互确认;`adopt` 接受但忽略,它只改清单)、`--prune`(含清空目录)、`--dry-run`
99
+ - 检查:`--strict`、`--json`
100
+ - 定位:`--root`
101
+ - 交接:`--release`
102
+
103
+ 非交互环境(脚本 / CI)不加 `--yes` 不会写盘。
104
+
105
+ ---
106
+
107
+ ## 一个标准域的文件
108
+
109
+ ```
110
+ <domains>/<域>/
111
+ ├── config.ts 手写:唯一真源
112
+ ├── index.ts 手写:中枢(单例 + 只挂载动作;缺失时补骨架)
113
+ ├── permissions.ts 按需生成:权限码 + canXxx 骨架(声明了权限码才有)
114
+ ├── context.ts 生成:DI + can
115
+ ├── executors/
116
+ │ ├── crud.generated.ts 生成:refresh / load / create / update / remove(按 crud 裁剪)
117
+ │ └── <用例>.ts 手写:自定义用例(可选)
118
+ └── steps/ 手写:被多个用例复用的零件(可选)
119
+
120
+ <api>/<域>.ts 生成:类型 + CRUD 接口(按 crud 裁剪)
121
+ <stores>/<域>/index.ts 生成:状态 + 原子更新(按 crud 裁剪)
122
+ <views>/<域>/Index.vue 生成:列表页
123
+ <views>/<域>/Detail.vue pages 含 detail 时生成
124
+ <views>/<域>/Form.vue pages 含 form 时生成
125
+
126
+ <router>/routes.generated.ts 全局生成
127
+ <layouts>/menu.generated.ts 全局生成
128
+ ```
129
+
130
+ `<domains>` / `<api>` / `<views>` 是目录映射的键,默认 `src/domains`、`src/api`、`src/views`(见「目录映射」)。
131
+
132
+ 中枢骨架带单例,视图只调 `useXxxModule()`:
133
+
134
+ ```ts
135
+ let docModule: DocModule | null = null
136
+
137
+ export function useDocModule(): DocModule {
138
+ if (!docModule) docModule = createDocModule()
139
+ return docModule
140
+ }
141
+
142
+ // 内部工厂不导出:视图拿不到第二份
143
+ function createDocModule(ctx = createDocContext()): DocModule { ... }
144
+ ```
145
+
146
+ ---
147
+
148
+ ## 配置项:一个 config 的完整形状
149
+
150
+ `tha init doc` 生成的骨架只开最基本的:
151
+
152
+ ```ts
153
+ export const docConfig = {
154
+ name: 'doc', // 内部名:目录、action、变量名都从它推
155
+ title: '文档', // 显示名:菜单、页面标题
156
+ path: '/doc', // 浏览器地址
157
+ api: { base: '/docs' }, // 接口前缀
158
+ fields: {
159
+ title: { label: '标题', type: 'string', required: true, search: true }
160
+ },
161
+ pages: ['list', 'detail'],
162
+ list: {},
163
+ crud: {},
164
+ permissions: {}
165
+ }
166
+ ```
167
+
168
+ 可选项都填上:
169
+
170
+ ```ts
171
+ export const docConfig = {
172
+ name: 'doc',
173
+ title: '文档',
174
+ path: '/doc',
175
+
176
+ api: {
177
+ base: '/docs',
178
+ contract: 'docs/openapi.json' // 填了才做字段比对
179
+ },
180
+
181
+ fields: {
182
+ title: { label: '标题', type: 'string', required: true, search: true },
183
+ summary: { label: '摘要', type: 'text' },
184
+ wordCount: { label: '字数', type: 'number', list: true, search: true },
185
+ published: { label: '已发布', type: 'boolean', default: false },
186
+ status: {
187
+ label: '状态',
188
+ type: 'enum',
189
+ default: 'draft',
190
+ search: true,
191
+ options: [
192
+ { label: '草稿', value: 'draft', color: 'gray' },
193
+ { label: '已发布', value: 'published', color: 'ok' },
194
+ { label: '已下线', value: 'offline', color: 'warn' }
195
+ ]
196
+ },
197
+ publishedAt: { label: '发布时间', type: 'datetime', list: true },
198
+ createdAt: { label: '创建日期', type: 'date', readonly: true }
199
+ },
200
+
201
+ pages: ['list', 'detail', 'form'],
202
+ list: { pageSize: 50, orderBy: 'createdAt', order: 'desc' },
203
+ crud: { create: true, update: true, remove: false },
204
+ permissions: { view: 'doc:view', create: 'doc:create', update: 'doc:update' }
205
+ }
206
+ ```
207
+
208
+ ### 顶层配置
209
+
210
+ | 键 | 必填 | 取值 | 默认 | 说明 |
211
+ |---|---|---|---|---|
212
+ | `name` | 是 | 小驼峰,如 `doc` / `userProfile` | — | 内部名,决定目录、action 名 |
213
+ | `title` | 是 | 非空字符串 | — | 显示名 |
214
+ | `path` | 是 | `/doc` 或 `/sys/doc`,不能带尾斜杠 | — | 页面地址 |
215
+ | `api.base` | 是 | `/docs`,不能带尾斜杠 | — | 接口前缀 |
216
+ | `api.contract` | 否 | 项目内 `.json` / `.yaml` / `.yml` 相对路径 | 无 | OpenAPI 文件,填了才比对字段 |
217
+ | `fields` | 是 | 对象,至少一个字段 | — | 字段声明 |
218
+ | `pages` | 否 | `list` / `detail` / `form` 的数组 | `['list','detail']` | 要生成哪几个页面 |
219
+ | `list.pageSize` | 否 | 1–100 的整数 | `20` | 每页条数 |
220
+ | `list.orderBy` | 否 | `fields` 里声明过的字段名 | 无 | 列表默认按这个字段排序 |
221
+ | `list.order` | 否 | `asc` / `desc` | `desc` | 和 `orderBy` 一起写 |
222
+ | `crud` | 否 | `create` / `update` / `remove` 布尔 | 全 `true` | 关掉的动作整条链都不生成 |
223
+ | `permissions` | 否 | `view` / `create` / `update` / `remove` 权限码 | `{}` | 非空才生成 `permissions.ts` |
224
+
225
+ 顶层**只有这 9 个键**,多写一个直接报「不认识的配置项」,一个文件都不会写。
226
+
227
+ ### 字段配置
228
+
229
+ | 键 | 必填 | 取值 | 默认 | 说明 |
230
+ |---|---|---|---|---|
231
+ | `label` | 是 | 非空字符串 | — | 列表列头、表单项标签 |
232
+ | `type` | 是 | `string` / `text` / `number` / `boolean` / `enum` / `date` / `datetime` | — | `text` 是长文本 |
233
+ | `required` | 否 | 布尔 | `false` | 必填校验 + 契约比对用 |
234
+ | `list` | 否 | 布尔 | `true` | 是否出现在列表列(写 `false` 才关) |
235
+ | `form` | 否 | 布尔 | `true` | 是否出现在表单(写 `false` 才关) |
236
+ | `search` | 否 | 布尔 | `false` | 是否可搜索 |
237
+ | `readonly` | 否 | 布尔 | `false` | 只读:不出现在表单,只在详情/列表显示 |
238
+ | `options` | `enum` 时必填 | `[{ label, value, color }]` | — | `color` 取 `gray` / `ok` / `warn` / `red` / `blue` |
239
+ | `default` | 否 | 与类型匹配的值 | 无 | `enum` 的 `default` 必须在 `options` 的 value 里 |
240
+
241
+ `id` 由 CLI 生成,**不要在 `fields` 里写 `id`**。
242
+
243
+ ### 会直接报错的写法
244
+
245
+ | 写法 | 报什么 |
246
+ |---|---|
247
+ | 顶层多一个键(如 `filters`、`templates`) | 不认识的配置项;该次 `sync` 一个文件都不写 |
248
+ | 字段名叫 `id`,或 `Title` 这种大写开头 | 字段名必须小写开头 |
249
+ | `readonly: true` 同时 `form: true` | 只读字段不能进表单 |
250
+ | `search: true` 用在 `text` / `boolean` | 只有 string / enum / number / date / datetime 可搜索 |
251
+ | `enum` 没写 `options`,或 `default` 不在 `options` 里 | 枚举字段必须给非空 options;default 必须在其中 |
252
+ | `pages` 里没有 `list`,或同一页写两遍 | list 必须保留(列表页是入口);重复的页面要删掉 |
253
+ | `pages` 含 `form` 但 `crud` 全关 | form 页需要 create 或 update 至少开一个 |
254
+ | `name: 'views'` 这种撞目录名的 | 不能使用保留名(api / stores / domains / views / router / layouts / components / utils) |
255
+ | `list.pageSize: 0` 或 `500` | 必须是 1–100 的整数 |
256
+ | `list.orderBy` 指向 `fields` 里没有的字段,或 enum / boolean | 排序字段必须已声明,且类型可排序 |
257
+ | 只写 `list.order` 不写 `orderBy`,或 `order` 不是 asc / desc | order 要和 orderBy 一起写;只支持 asc、desc |
258
+
259
+ ### 常见组合
260
+
261
+ ```ts
262
+ pages: ['list'] // 只要列表
263
+ crud: { create: false, update: false, remove: false } // 只读模块
264
+ pages: ['list', 'form'] // 不要详情,只在列表 + 表单页
265
+ permissions: { view: 'doc:view', update: 'doc:update' } // 只声明用得上的权限码
266
+ api: { base: '/docs', contract: 'docs/openapi.json' } // 接后端契约比对
267
+ ```
268
+
269
+ ---
270
+
271
+ ## 目录映射:不想放 src/ 就改这里
272
+
273
+ 项目根建 `tha.config.json`:
274
+
275
+ ```json
276
+ { "dirs": { "views": "src/pages", "stores": "src/model", "api": "src/services", "utils": "src/shared" } }
277
+ ```
278
+
279
+ 可用键:`domains` / `views` / `stores` / `api` / `router` / `layouts` / `components` / `utils`。
280
+ 不写就用默认值;写错(绝对路径、`..`、未知键)直接报错,一个文件都不生成。
281
+ 生成代码里的 import 会自动跟着改(`src/services` → `@/services`)。
282
+
283
+ `tha init <域> --config` 会写一份全默认值的示例,改完删掉即回默认。
284
+
285
+ ---
286
+
287
+ ## 所有权:四种文件,四种命运
288
+
289
+ | 所有权 | 怎么识别 | sync | check |
290
+ |---|---|---|---|
291
+ | generated | 有 `@tha-generated` 生成头,hash 对得上 | 可以更新 | 照常查边界 |
292
+ | handwritten | 没有生成头,且不在交接清单 | 永不覆盖 | 提示,照常查边界 |
293
+ | adopted | 在 `.tha/manifest.json` 的 `manual` 里 | 永不覆盖、不比对漂移 | 不算失败,照常查边界 |
294
+ | edited | 有生成头但 hash 对不上 | 拒绝,除非 `--force` | 报 drift |
295
+
296
+ `adopt` 只接受构建期会生成的文件(用 `tha diff` 查看)。清单写在
297
+ `.tha/manifest.json`,建议提交到版本库。
298
+
299
+ **adopt 的语义**:工具放弃「写」,不放弃「管」。
300
+
301
+ ---
302
+
303
+ ## 护栏:10 条边界规则
304
+
305
+ 规则唯一真源在 `tha/verify/rules.mjs`,每条带 `id` / `level` / `basis`(架构原话)/ `fix`。
306
+
307
+ | id | 级别 | 判据 |
308
+ |---|---|---|
309
+ | HUB-01 | error | 中枢直接 import `./steps/*` |
310
+ | HUB-03 | warn | 中枢不是模块级单例 |
311
+ | EXE-01 | error | 执行器之间互相 import |
312
+ | EXE-02 | error | 执行器 / 零件值导入网络层、状态层、路由 |
313
+ | EXE-03 | error | 执行器 / 零件值导入 `@/utils/*`(纯函数白名单除外) |
314
+ | STEP-01 | warn | 零件只被 0 或 1 个执行器使用 |
315
+ | VIEW-01 | error | 视图值导入网络层 / 传输层 / 执行器内部 |
316
+ | VIEW-02 | error | 视图直接 `createXxxModule()`,绕开单例 |
317
+ | STORE-01 | error | 状态层 import 网络层 |
318
+ | UTIL-01 | error | 基础设施 import UI 库或业务模块 |
319
+
320
+ 约定:
321
+
322
+ - `import type` 一律放行(类型不构成运行时依赖)。
323
+ - 检查**按层扫**:`views/`、`stores/`、`utils/` 全局一遍,`domains/<域>/` 按模块。
324
+ - 手写模块靠目录形状识别:有 `index.ts` / `executors/` / `context.ts`,但没有 `config.ts`。
325
+ - `PURE_UTILS` 白名单在 `rules.mjs` 里声明,不再靠 eslint 注释豁免。
326
+
327
+ ---
328
+
329
+ ## 契约与权限:声明了就得真生效
330
+
331
+ 这两项最容易出现「配置写了、产物有、但那件事没发生」,所以检查口径要明确。
332
+
333
+ **字段契约**对两个方向都比(`api.contract` 指向的 OpenAPI,`.json` / `.yaml` / `.yml` 都认):
334
+
335
+ | 方向 | 取自 | 级别 |
336
+ |---|---|---|
337
+ | 响应:config 字段后端没有 | GET 200 / POST 201 的 schema(`{code,data}` 包装自动解一层) | error |
338
+ | 响应:类型不一致 | 同上 | error |
339
+ | 响应:`format` / 必填 / 后端多字段 | 同上 | warn |
340
+ | 请求体:表单会提交的字段(`form !== false`)后端不收 | POST 请求体 → `create`;PUT / PATCH → `update` | error |
341
+ | 请求体:类型不一致 | 同上 | error |
342
+ | 请求体:后端必填、config 里没有可提交字段 | 同上 | error |
343
+ | 请求体:后端有可选字段 config 没声明 | 同上 | warn |
344
+ | 契约里没写请求体 | — | 一条 warn,跳过,不逐字段误报 |
345
+
346
+ **权限守卫**:`permissions` 非空的模块,执行器里必须至少有**一处** `can(...)` 判断(注释不算)。一处都没有 → error,提示「声明了 permissions,但执行器里没有任何 can(...) 判断」。
347
+
348
+ 这条专门兜 `adopt`:生成模板里本来每个动作前都有 `ctx.can('view')`,一旦这个文件交给手写,生成器不再管它,只有这条检查还能发现「权限被悄悄删了」。
349
+
350
+ ---
351
+
352
+ ## 退出码
353
+
354
+ `drift` / 手写冲突 / 契约失败(响应或请求体)/ 权限守卫缺失 / 护栏 error 任一存在 → `1`;
355
+ `--strict` 时契约与护栏的 warn 也算失败。
356
+
357
+ ---
358
+
359
+ ## 注意事项
360
+
361
+ 实际用起来会踩到的几条:
362
+
363
+ 1. **只覆盖标准 CRUD**。列表 / 详情 / 表单、增删改查、搜索分页是标准形态;工作流、嵌套表单、自定义布局这类复杂模块仍然人写,工具只保证不覆盖、不误报。
364
+ 2. **自带组件是复制,不是引用**。`init` / `sync` 会把 `ThaCrudPage.vue` 等基线复制进项目。之后 CLI 升级,项目里那份**不会自动更新**:新能力(比如 `list.orderBy`)在老项目里不生效。要么删掉该文件重新 `sync`,要么手动把改动抄过去。
365
+ 3. **`list.orderBy` 只排前端**。它是 `ThaCrudPage` 的本地排序,不会给后端接口加 query 参数。要后端排序,那是接口契约的事。
366
+ 4. **`api.contract` 的 YAML 解析依赖 `yaml` 包**,随 CLI 一起装,不需要单独安装。
367
+ 5. **`config.ts` 顶层只有 9 个键**。多写一个直接报错,该次 `sync` 一个文件都不写。收窄配置面是有意的,否则守不住边界。
368
+ 6. **非交互不写盘**。脚本里不加 `--yes`,`sync` / `remove` 只打印计划。
369
+ 7. **adopt 是放弃写、不放弃管**。文件交给你之后,工具不再生成它,但分层规则照样查。
370
+
371
+ ---
372
+
373
+ ## 开发
374
+
375
+ ```bash
376
+ node test/guard.test.mjs # 造 10 种违规,断言 10/10 命中 + 退出码 1
377
+ node test/snapshot.test.mjs # 固定配置 → 与 test/snapshots/ 逐字节比对(--update 重记)
378
+ node test/openapi.test.mjs # 契约比对:方言、YAML、请求体方向
379
+ ```
380
+
381
+ 目录:
382
+
383
+ ```
384
+ tha/
385
+ ├── cli.mjs 入口分发
386
+ ├── commands/ init / sync / check / diff / remove / adopt
387
+ ├── kit/ 自带页面与基线(ThaCrudPage / ThaDetail / ThaForm / request / transport)
388
+ ├── config/ paths(目录映射)、project(config + 手写模块)、manifest(交接清单)
389
+ ├── generate/ plan + 模板(api、store、context、executors、entry、detail、form、permissions)
390
+ ├── verify/ rules(规则真源)、ast、boundary、contract、openapi、scan
391
+ └── utils/ files / compare / output / style / format
392
+ test/ guard.test.mjs、snapshot.test.mjs、openapi.test.mjs、snapshots/
393
+ ```
394
+
395
+ ---
396
+
397
+ ## 发布
398
+
399
+ ```bash
400
+ npm test # guard + snapshot + openapi 三套
401
+ npm pack --dry-run # 看会打进包里哪些文件
402
+ npm publish
403
+ ```
404
+
405
+ 包里只带 `tha.mjs` + `tha/` + `README.md` + `ARCHITECTURE.md`(`LICENSE` 由 npm 自动带上);`test/` 与 `snapshots/` 不进包,`typescript` 也不进包——它写在 `dependencies` 里,安装时由 npm 单独下载。
406
+
407
+ `typescript` 是普通依赖(`^5`),不参与你项目的构建。运行时先找项目里的那份(API 齐全才用),不齐就退回 CLI 自带的那份;两边都不能用时才报错,不会静默跳过检查。
408
+
409
+ 包名 `tha-cli` 若在 npm 上已被占用,改 `package.json` 的 `name` 再发;命令名由 `bin` 决定,仍然是 `tha`。
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "tha-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "THA 架构的构建期工具:产线生成标准增删改查,护栏守住分层边界。",
5
+ "type": "module",
6
+ "author": "温强 <373601967@qq.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://gitee.com/JonnyWen6699/tha-cli.git"
10
+ },
11
+ "homepage": "https://gitee.com/JonnyWen6699/tha-cli#readme",
12
+ "bugs": {
13
+ "url": "https://gitee.com/JonnyWen6699/tha-cli/issues"
14
+ },
15
+ "bin": {
16
+ "tha": "tha.mjs"
17
+ },
18
+ "files": [
19
+ "tha.mjs",
20
+ "tha",
21
+ "README.md",
22
+ "ARCHITECTURE.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18.17"
26
+ },
27
+ "scripts": {
28
+ "test": "node test/guard.test.mjs && node test/snapshot.test.mjs && node test/openapi.test.mjs",
29
+ "snapshot": "node test/snapshot.test.mjs --update"
30
+ },
31
+ "keywords": [
32
+ "tha",
33
+ "codegen",
34
+ "architecture",
35
+ "boundary",
36
+ "vue",
37
+ "cli"
38
+ ],
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "typescript": "^5",
42
+ "yaml": "^2.9.1"
43
+ }
44
+ }