tha-cli 0.1.2 → 0.1.4

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/ARCHITECTURE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # THA 架构设计
2
2
 
3
3
  > 本文讲**为什么这么设计**:要解决什么、靠什么结构解决、代价是什么。
4
- > 怎么安装、`config.ts` 怎么写、命令怎么用,见 [README.md](https://gitee.com/JonnyWen6699/tha-cli/blob/main/README.md)。
5
- > 每条约束的原话在 `tha/verify/rules.mjs`,本文引用的都是那里的 `basis`。
4
+ > 怎么安装、`config.ts` 怎么写、命令怎么用,见 README.md。
5
+ > 每条边界规则的实现见 `tha/eslint-rules/`,本文引用的都是规则的设计依据。
6
6
 
7
7
  ---
8
8
 
@@ -103,30 +103,53 @@ view → hub → executor → step
103
103
 
104
104
  ---
105
105
 
106
- ## 5. 边界不靠人记,靠工具查
106
+ ## 5. 边界不靠人记,靠 ESLint 实时查
107
107
 
108
- 架构约束写在文档里,没人会读;写成可执行的检查,才有约束力。三个机制:
108
+ 架构约束写在文档里,没人会读;写成可执行的检查,才有约束力。而且,**事后检查不如实时检查**——写错的那一刻编辑器就有红线,比提交前跑一遍 CLI 有效得多。
109
109
 
110
- ### 5.1 规则表与判断逻辑分离
110
+ 因此边界检查全部交给 ESLint,CLI 只负责自动装和写配置。
111
111
 
112
- `tha/verify/rules.mjs` 只保留规则的自然语言描述——`id` / `level` / `title` / `basis` / `fix`;
113
- 判断逻辑在 `tha/verify/boundary.mjs`。改文案不动逻辑,改逻辑不动文案。
112
+ ### 5.1 为什么选 ESLint,不自己写检查器
114
113
 
115
- ### 5.2 每条规则带 `basis`
114
+ | 方案 | 优点 | 缺点 |
115
+ |---|---|---|
116
+ | 自己写检查器 | 依赖少,完全可控 | 编辑器要自己写插件,VSCode / WebStorm 都得写一套;热更新要自己做 |
117
+ | ESLint | 编辑器原生支持(VSCode / WebStorm 都有扩展),实时红线;生态成熟 | 依赖包多,要手动装 |
116
118
 
117
- `basis` 直接引用架构原话。输出违规时,能回溯到「哪条约束被破了」,而不是「工具说不行」。
119
+ 选 ESLint 的原因:**不干扰开发者现有习惯**。开发者已经在用 ESLint,不需要再学新工具;VSCode / WebStorm 的 ESLint 扩展已经装好,打开项目就生效。
118
120
 
119
- 违规条目按这个形状输出:
121
+ 代价是依赖包多,但这个问题由 `tha init` 自动解决——一行命令装完所有依赖、写好配置,开发者不用动手。
120
122
 
121
- ```
122
- src/domains/doc/index.ts:1 HUB-03
123
- 问题:模块入口不是单例,被重复创建
124
- 没找到缓存的实例(每次调用都会新建一个)
125
- 应该:在模块顶层用 let 缓存:if (!module) module = createXxxModule()
126
- 依据:每个领域全局唯一实例,视图不能各造一个「私人大脑」
127
- ```
123
+ ### 5.2 8 条边界规则
124
+
125
+ 每条规则是一个独立的 ESLint 插件文件,放在 `tha/eslint-rules/` 目录下:
126
+
127
+ | 规则 ID | 层级 | 检查内容 |
128
+ |---|---|---|
129
+ | `tha/hub-no-import-steps` | HUB-01 | 中枢不许 import 执行器内部步骤 |
130
+ | `tha/executor-no-cross-import` | EXE-01 | 执行器之间不许互相 import |
131
+ | `tha/executor-no-import-api` | EXE-02 | 执行器不许 import 网络层、状态层、路由 |
132
+ | `tha/executor-no-import-utils` | EXE-03 | 执行器不许 import 传输工具,必须经 ctx 注入 |
133
+ | `tha/view-no-import-api` | VIEW-01 | 视图不许直接 import api 层、传输工具、执行器内部 |
134
+ | `tha/view-no-create-module` | VIEW-02 | 视图不许自己创建中枢实例 |
135
+ | `tha/store-no-import-api` | STORE-01 | 状态层不许发请求 |
136
+ | `tha/util-no-import-ui` | UTIL-01 | 基础工具不许 import UI 组件 |
137
+
138
+ 规则只检查单文件内的 import 语句,不做跨文件分析——ESLint 擅长这个,跨文件规则交给 `tha check` 做。
128
139
 
129
- ### 5.3 `import type` 一律放行
140
+ ### 5.3 `tha init` 自动完成一切
141
+
142
+ `tha init` 做五件事:
143
+
144
+ 1. **检测环境**:项目里有没有 ESLint、有没有 Vue
145
+ 2. **自动装依赖**:缺什么装什么(eslint、@typescript-eslint、eslint-plugin-vue)
146
+ 3. **自动写配置**:生成 `eslint.config.js`,把 8 条规则写进去
147
+ 4. **自动跑 sync**:生成标准代码
148
+ 5. **自动改 dev script**:把 `tha watch` 加进 `npm run dev`
149
+
150
+ 开发者不需要手动装任何包、不需要手动写任何配置。一行 `tha init` 之后,编辑器里就有实时红线了。
151
+
152
+ ### 5.4 `import type` 一律放行
130
153
 
131
154
  类型不构成运行时依赖。只查值导入,规则才不至于因噎废食。
132
155
 
@@ -167,20 +190,22 @@ src/domains/doc/index.ts:1 HUB-03
167
190
 
168
191
  ## 9. 规则索引
169
192
 
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`](https://gitee.com/JonnyWen6699/tha-cli/blob/main/tha/verify/rules.mjs) 判断逻辑:[`tha/verify/boundary.mjs`](https://gitee.com/JonnyWen6699/tha-cli/blob/main/tha/verify/boundary.mjs)
193
+ | id | 级别 | 约束原话(`basis`) | 实现位置 |
194
+ |---|---|---|---|
195
+ | HUB-01 | error | 中枢只做分发:把意图交给执行器;禁止直接依赖执行器内部步骤 | ESLint |
196
+ | HUB-03 | warn | 每个领域全局唯一实例,视图不能各造一个「私人大脑」 | 待实现 |
197
+ | EXE-01 | error | 执行器之间互不引用:共用逻辑下沉成零件,用例顺序留在各自执行器里 | ESLint |
198
+ | EXE-02 | error | 执行器只认 ctx:网络走 ctx.api,状态走 ctx.state | ESLint |
199
+ | EXE-03 | error | 执行器只认 ctx:日志 / 时钟 / 流 / 鉴权走 ctx.ports,判定走 ctx.errors | ESLint |
200
+ | STEP-01 | warn | 零件是「同一段步骤被第二个用例用到」才下沉的;只有一个使用者是过度设计 | 待实现 |
201
+ | VIEW-01 | error | 视图只表达意图:动作走域中枢,数据直读状态层 | ESLint |
202
+ | VIEW-02 | error | 每个领域全局唯一实例:视图只能经 useXxxModule() 取,不能自己 create | ESLint |
203
+ | STORE-01 | error | 状态层只存状态,不发请求 | ESLint |
204
+ | UTIL-01 | error | 传输层只上报不越权:不感知业务模块,不做展示 | ESLint |
205
+
206
+ ESLint 规则实现:`tha/eslint-rules/` 配置自动生成:`tha/utils/eslint.mjs`
207
+
208
+ `HUB-03` 和 `STEP-01` 是跨文件规则,ESLint 单文件检查不好做,后续版本会补回 `tha check` 里。
184
209
 
185
210
  `EXE-03` 提到的 `ctx.ports` / `ctx.errors` 是架构原话里的完整形态;生成器当前的 `context.ts` 只给最小集(`state` / `api` / `ui` / `run` / `can`),需要时按 3.2 往上下文里加。
186
211
 
@@ -188,6 +213,6 @@ src/domains/doc/index.ts:1 HUB-03
188
213
 
189
214
  ## 10. 相关文档
190
215
 
191
- - [README.md](https://gitee.com/JonnyWen6699/tha-cli/blob/main/README.md)——安装、命令、`config.ts` 全量配置项、契约与退出码
216
+ - README.md——安装、命令、`config.ts` 全量配置项、契约与退出码
192
217
  - `tha/generate/domain/`——每一类生成物的模板,是「形状」的可执行版本
193
218
  - `tha/verify/rules.mjs`——规则真源,改架构先改这里
package/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # tha-cli
2
2
 
3
- THA 架构的构建期工具。**生成器负责产出标准代码,检查器负责保障分层边界不被破坏。**
3
+ THA 架构的构建期工具。**一行 init 完成全部配置:自动装依赖、自动写 ESLint 规则、自动生成代码、自动接入开发流程。**
4
4
 
5
5
  - **生成器**:读 `domains/<域>/config.ts`,生成标准文件(基线 5 个,按 `pages` 追加详情 / 表单)和全局注册表。
6
- - **检查器**:没有 `config.ts` 的手写模块,不生成,只按层查边界。
6
+ - **边界检查**:内置 8 条 ESLint 规则,`init` 自动安装 ESLint 并写好配置,编辑器里实时红线提示。
7
+ - **热更新**:`tha watch` 监听配置文件变化,改完自动跑 sync,无需手动执行命令。
7
8
  - **自带基线**:`init` 自动补齐生成物所需的组件和工具,空项目也可直接使用。
8
9
 
9
- 设计思想、取舍与代价见 [ARCHITECTURE.md](https://gitee.com/JonnyWen6699/tha-cli/blob/main/ARCHITECTURE.md);本文只讲怎么用。
10
+ 设计思想、取舍与代价见 ARCHITECTURE.md;本文只讲怎么用。
10
11
 
11
12
  ---
12
13
 
@@ -30,7 +31,7 @@ utils 基础 纯函数、远端传输
30
31
 
31
32
  这套约束的目的很单一:业务逻辑集中维护,边界由工具保障,无需人工记忆。
32
33
 
33
- 为什么这么切分、每条约束的原话和代价,见 [ARCHITECTURE.md](https://gitee.com/JonnyWen6699/tha-cli/blob/main/ARCHITECTURE.md)。
34
+ 为什么这么切分、每条约束的原话和代价,见 ARCHITECTURE.md。
34
35
 
35
36
  ---
36
37
 
@@ -39,7 +40,8 @@ utils 基础 纯函数、远端传输
39
40
  | 模式 | 输入 | 产物 |
40
41
  |---|---|---|
41
42
  | 生成器 | `config.ts` | 标准 CRUD 的 api / store / context / 执行器 / 页面 / 路由 / 菜单 |
42
- | 检查器 | 任意源码 | 违规清单(10 条边界规则 + 契约比对) |
43
+ | ESLint 规则 | 内置 8 条边界规则 | `init` 自动写进 `eslint.config.js`,编辑器实时检查 |
44
+ | 配置检查 | 项目源码 | 契约比对 + 文件一致性 + ESLint 配置完整性 |
43
45
 
44
46
  它不生成业务逻辑。`config.ts` 声明模块的字段与动作范围,工具据此生成标准代码;复杂逻辑由开发者编写,置于自定义执行器中,工具不覆盖。
45
47
 
@@ -62,12 +64,21 @@ npx tha-cli check
62
64
  ## 快速开始
63
65
 
64
66
  ```bash
65
- tha init doc # 建 config 模板,不生成代码
67
+ tha init doc # 自动装 ESLint + 写配置 + 生成代码 + 改 dev script
66
68
  # 编辑 src/domains/doc/config.ts
67
- tha sync # 生成 5 个文件;中枢缺失时补一次骨架
68
- tha check # 检查:配置 + 契约 + 边界检查 + 文件一致性
69
+ # 保存后 tha watch 会自动跑 sync,无需手动执行
69
70
  ```
70
71
 
72
+ 一行 `tha init` 完成全部初始化:
73
+
74
+ 1. 检测项目环境(有没有 ESLint、有没有 Vue)
75
+ 2. 自动安装所需依赖(eslint、@typescript-eslint、eslint-plugin-vue)
76
+ 3. 自动写 `eslint.config.js`,内置 8 条边界规则
77
+ 4. 自动生成标准代码
78
+ 5. 自动改 `package.json` 的 dev script,把 `tha watch` 加进去
79
+
80
+ 编辑器打开项目后,VSCode / WebStorm 的 ESLint 扩展会自动识别配置,写错 import 立刻有红线提示。
81
+
71
82
  CI:
72
83
 
73
84
  ```bash
@@ -82,9 +93,10 @@ tha check --json # 机器可消费
82
93
 
83
94
  | 命令 | 作用 |
84
95
  |---|---|
85
- | `tha init <域>` | 建 config 模板 + 补齐基线(`--bare` 只写 config) |
96
+ | `tha init <域>` | 一键初始化:自动装 ESLint + 写配置 + 生成代码 + 改 dev script |
86
97
  | `tha sync [<域>]` | 生成 / 更新标准 CRUD 与注册表;补一次中枢骨架 |
87
- | `tha check` | 只检查不写:配置 + 契约(响应与请求体)+ 边界检查 + 文件一致性 |
98
+ | `tha watch` | 监听 `domains/*/config.ts` 变化,自动跑 sync(debounce 500ms) |
99
+ | `tha check` | 只检查不写:配置 + 契约(响应与请求体)+ 文件一致性 + ESLint 配置完整性 |
88
100
  | `tha diff` | 显示将要发生的文件变化 |
89
101
  | `tha remove <域>` | 删模块:生成物清掉,手写文件保留;保留的文件若还引用已删生成物会明确报出来 |
90
102
  | `tha adopt <文件...>` | 这个文件以后你自己维护:不再自动生成,但分层规则照样查 |
@@ -298,29 +310,31 @@ api: { base: '/docs', contract: 'docs/openapi.json' } // 接后端契约比对
298
310
 
299
311
  ---
300
312
 
301
- ## 护栏:10 条边界规则
313
+ ## 边界检查:ESLint 实时红线
302
314
 
303
- 规则唯一真源在 `tha/verify/rules.mjs`,每条带 `id` / `level` / `basis`(架构原话)/ `fix`。
315
+ 边界检查全部交给 ESLint,`tha init` 自动完成全部配置。编辑器打开项目后,写错 import 立刻有红线提示,不需要等提交前跑 CLI。
304
316
 
305
- | id | 级别 | 判据 |
317
+ 8 条单文件规则,每条对应一个 ESLint 插件文件:
318
+
319
+ | 规则 | 级别 | 检查内容 |
306
320
  |---|---|---|
307
- | HUB-01 | error | 中枢直接 import `./steps/*` |
308
- | HUB-03 | warn | 中枢不是模块级单例 |
309
- | EXE-01 | error | 执行器之间互相 import |
310
- | EXE-02 | error | 执行器 / 零件值导入网络层、状态层、路由 |
311
- | EXE-03 | error | 执行器 / 零件值导入 `@/utils/*`(纯函数白名单除外) |
312
- | STEP-01 | warn | 零件只被 0 或 1 个执行器使用 |
313
- | VIEW-01 | error | 视图值导入网络层 / 传输层 / 执行器内部 |
314
- | VIEW-02 | error | 视图直接 `createXxxModule()`,绕开单例 |
315
- | STORE-01 | error | 状态层 import 网络层 |
316
- | UTIL-01 | error | 基础设施 import UI 库或业务模块 |
321
+ | `tha/hub-no-import-steps` | error | 中枢直接 import `./steps/*` |
322
+ | `tha/executor-no-cross-import` | error | 执行器之间互相 import |
323
+ | `tha/executor-no-import-api` | error | 执行器值导入网络层、状态层、路由 |
324
+ | `tha/executor-no-import-utils` | error | 执行器值导入 `@/utils/*`(纯函数白名单除外) |
325
+ | `tha/view-no-import-api` | error | 视图值导入网络层 / 传输层 / 执行器内部 |
326
+ | `tha/view-no-create-module` | error | 视图直接 `createXxxModule()`,绕开单例 |
327
+ | `tha/store-no-import-api` | error | 状态层 import 网络层 |
328
+ | `tha/util-no-import-ui` | error | 基础设施 import UI 库或业务模块 |
317
329
 
318
330
  约定:
319
331
 
320
332
  - `import type` 一律放行(类型不构成运行时依赖)。
321
- - 检查**按层扫**:`views/`、`stores/`、`utils/` 全局一遍,`domains/<域>/` 按模块。
322
- - 手写模块靠目录形状识别:有 `index.ts` / `executors/` / `context.ts`,但没有 `config.ts`。
323
- - `PURE_UTILS` 白名单在 `rules.mjs` 里声明,不再靠 eslint 注释豁免。
333
+ - 规则实现:`tha/eslint-rules/` 目录下,每条规则一个文件。
334
+ - 配置自动生成:`tha init` 检测项目环境,自动装依赖、自动写 `eslint.config.js`。
335
+ - VSCode / WebStorm 的 ESLint 扩展原生支持,不需要额外装插件。
336
+
337
+ 跨文件规则(中枢单例检查、零件复用数检查)是 warn 级别,后续版本会补回 `tha check` 命令里。
324
338
 
325
339
  ---
326
340
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tha-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "THA 架构的构建期工具:产线生成标准增删改查,护栏守住分层边界。",
5
5
  "type": "module",
6
6
  "author": "温强 <373601967@qq.com>",
package/tha/cli.mjs CHANGED
@@ -6,6 +6,7 @@ import { runDiff } from './commands/diff.mjs'
6
6
  import { runSync } from './commands/sync.mjs'
7
7
  import { runRemove } from './commands/remove.mjs'
8
8
  import { runAdopt } from './commands/adopt.mjs'
9
+ import { runWatch } from './commands/watch.mjs'
9
10
  import { configureStyle, ui } from './utils/style.mjs'
10
11
 
11
12
  const COMMANDS = {
@@ -13,6 +14,7 @@ const COMMANDS = {
13
14
  check: runCheck,
14
15
  diff: runDiff,
15
16
  sync: runSync,
17
+ watch: runWatch,
16
18
  remove: runRemove,
17
19
  adopt: runAdopt
18
20
  }
@@ -4,10 +4,10 @@ import { buildPlan } from '../generate/plan.mjs'
4
4
  import { comparePlan } from '../utils/compare.mjs'
5
5
  import { parseArgs } from '../utils/args.mjs'
6
6
  import { printErrors, printHeader } from '../utils/output.mjs'
7
- import { checkBoundaries } from '../verify/boundary.mjs'
8
7
  import { checkContracts } from '../verify/contract.mjs'
9
8
  import { checkOpenApiContracts } from '../verify/openapi.mjs'
10
9
  import { describeConfigured } from '../verify/scan.mjs'
10
+ import { checkEslintConfig } from '../utils/eslint.mjs'
11
11
  import { glyph, ui } from '../utils/style.mjs'
12
12
 
13
13
  export function runCheck({ root, args }) {
@@ -49,17 +49,19 @@ export function runCheck({ root, args }) {
49
49
  ]
50
50
  const contractErrors = contracts.filter((item) => (item.level ?? 'error') === 'error')
51
51
  const contractWarns = contracts.filter((item) => item.level === 'warn')
52
- const guards = checkBoundaries(modules, root, project.dirs)
53
- const guardErrors = guards.filter((item) => item.level === 'error')
54
- const guardWarns = guards.filter((item) => item.level === 'warn')
55
52
 
56
- // 契约违约、漂移、手改、护栏 error,都让 CI 红;warn 只在 --strict 时红
53
+ // ESLint 配置完整性检查
54
+ const eslintProblems = checkEslintConfig(root)
55
+ const eslintErrors = eslintProblems.filter((item) => item.level === 'error')
56
+ const eslintWarns = eslintProblems.filter((item) => item.level === 'warn')
57
+
58
+ // 契约违约、漂移、手改、ESLint 配置错误,都让 CI 红;warn 只在 --strict 时红
57
59
  const failed =
58
60
  drift.length > 0 ||
59
61
  blocked > 0 ||
60
62
  contractErrors.length > 0 ||
61
- guardErrors.length > 0 ||
62
- (flags.strict && (contractWarns.length > 0 || guardWarns.length > 0))
63
+ eslintErrors.length > 0 ||
64
+ (flags.strict && (contractWarns.length > 0 || eslintWarns.length > 0))
63
65
 
64
66
  if (failed) process.exitCode = 1
65
67
 
@@ -118,21 +120,18 @@ export function runCheck({ root, args }) {
118
120
  }
119
121
  }
120
122
 
121
- if (guards.length > 0) {
123
+ if (eslintProblems.length > 0) {
122
124
  console.log('')
123
125
  console.log(
124
- ` ${ui.danger(glyph.err)} ${ui.label(
125
- `分层规则被破坏:${guardErrors.length} 处错误、${guardWarns.length} 处提醒`,
126
- `Layering rules broken: ${guardErrors.length} errors, ${guardWarns.length} warnings`
126
+ ` ${ui.warning(glyph.warn)} ${ui.label(
127
+ `ESLint 配置:${eslintErrors.length} 处错误、${eslintWarns.length} 处提醒`,
128
+ `ESLint config: ${eslintErrors.length} errors, ${eslintWarns.length} warnings`
127
129
  )}`
128
130
  )
129
- for (const item of guards) {
130
- const badge = item.level === 'error' ? ui.danger(item.rule) : ui.warning(item.rule)
131
- console.log(` ${ui.path(`${item.file}:${item.line}`)} ${badge}`)
132
- console.log(` 问题:${item.message}`)
133
- if (item.note) console.log(` ${ui.muted(item.note)}`)
134
- console.log(` 应该:${item.fix}`)
135
- console.log(` ${ui.muted(`依据:${item.basis}`)}`)
131
+ for (const item of eslintProblems) {
132
+ const badge = item.level === 'error' ? ui.danger(glyph.err) : ui.warning(glyph.warn)
133
+ console.log(` ${badge} ${ui.path(item.file)}`)
134
+ console.log(` ${ui.muted(item.message)}`)
136
135
  }
137
136
  }
138
137
 
@@ -9,6 +9,8 @@ import { parseArgs } from '../utils/args.mjs'
9
9
  import { absolute, exists, read, writeAtomic } from '../utils/files.mjs'
10
10
  import { glyph, ui } from '../utils/style.mjs'
11
11
  import { printNextSteps } from '../utils/output.mjs'
12
+ import { detectEslintEnv, installEslintDeps, writeEslintConfig, verifyEslintRules } from '../utils/eslint.mjs'
13
+ import { runSync } from './sync.mjs'
12
14
 
13
15
  // 生成物会 import 的基线文件:空项目里没有,init 时补上(不覆盖已有文件)
14
16
  const BASELINE = [
@@ -145,6 +147,48 @@ export async function runInit({ root, args }) {
145
147
  const baseline = flags.bare ? { created: [], skipped: [] } : ensureBaseline(root, dirs)
146
148
  const dirsConfig = flags.config ? ensureDirsConfig(root) : null
147
149
 
150
+ // ESLint 自动配置:检测 → 装依赖 → 写配置 → 验证
151
+ let eslintResult = null
152
+ if (!flags.bare && !flags['dry-run']) {
153
+ const env = detectEslintEnv(root)
154
+ let installed = []
155
+ let installFailed = []
156
+
157
+ if (!env.hasEslint) {
158
+ console.log('')
159
+ console.log(` ${ui.label('检测到项目没有 ESLint,自动安装...', 'Installing ESLint...')}`)
160
+ const result = installEslintDeps(root, env)
161
+ installed = result.installed
162
+ installFailed = result.failed
163
+ }
164
+
165
+ const configResult = writeEslintConfig(root, env)
166
+ const verifyResult = env.hasEslint || installed.length > 0
167
+ ? verifyEslintRules(root)
168
+ : { ok: false, message: '' }
169
+
170
+ eslintResult = { env, installed, installFailed, configResult, verifyResult }
171
+ }
172
+
173
+ // 自动跑一次 sync,直接生成全套文件
174
+ let syncResult = null
175
+ if (!flags.bare && !flags['dry-run'] && !flags.json) {
176
+ console.log('')
177
+ console.log(` ${ui.label('自动生成代码...', 'Generating code...')}`)
178
+ try {
179
+ await runSync({ root, args: [name, '--yes'] })
180
+ syncResult = { ok: true }
181
+ } catch (error) {
182
+ syncResult = { ok: false, message: error.message }
183
+ }
184
+ }
185
+
186
+ // 自动改 package.json 的 dev script,加上 tha watch
187
+ let packageJsonResult = null
188
+ if (!flags.bare && !flags['dry-run']) {
189
+ packageJsonResult = updateDevScript(root)
190
+ }
191
+
148
192
  if (flags.json) {
149
193
  console.log(
150
194
  JSON.stringify(
@@ -179,6 +223,46 @@ export async function runInit({ root, args }) {
179
223
  console.log(` ${ui.muted(`目录映射示例已写入 ${dirsConfig},改完删掉即回默认`)}`)
180
224
  }
181
225
 
226
+ // ESLint 配置结果
227
+ if (eslintResult) {
228
+ console.log('')
229
+ if (eslintResult.installed.length > 0) {
230
+ console.log(` ${ui.success(glyph.ok)} ${ui.label('ESLint 依赖已安装', 'ESLint installed')}`)
231
+ console.log(` ${ui.muted(eslintResult.installed.join(', '))}`)
232
+ }
233
+ if (eslintResult.installFailed.length > 0) {
234
+ console.log(` ${ui.warning(glyph.warn)} ${ui.label('ESLint 部分依赖安装失败', 'Some ESLint deps failed')}`)
235
+ console.log(` ${ui.muted('请手动安装:npm i -D ' + eslintResult.installFailed.join(' '))}`)
236
+ }
237
+ if (eslintResult.configResult.written) {
238
+ console.log(` ${ui.success(glyph.ok)} ${ui.label('ESLint 配置已生成', 'ESLint config created')}`)
239
+ console.log(` ${ui.path(eslintResult.configResult.written)}`)
240
+ }
241
+ if (eslintResult.configResult.skipped) {
242
+ console.log(` ${ui.muted('已有 ESLint 配置,未覆盖(可手动加 tha 规则)')}`)
243
+ }
244
+ if (eslintResult.verifyResult.ok) {
245
+ console.log(` ${ui.success(glyph.ok)} ${ui.label('THA 规则已生效', 'THA rules active')}`)
246
+ }
247
+ }
248
+
249
+ // sync 结果
250
+ if (syncResult) {
251
+ console.log('')
252
+ if (syncResult.ok) {
253
+ console.log(` ${ui.success(glyph.ok)} ${ui.label('代码已自动生成', 'Code generated')}`)
254
+ } else {
255
+ console.log(` ${ui.warning(glyph.warn)} ${ui.label('自动生成失败,可手动跑 tha sync', 'Auto sync failed')}`)
256
+ console.log(` ${ui.muted(syncResult.message)}`)
257
+ }
258
+ }
259
+
260
+ // package.json 结果
261
+ if (packageJsonResult && packageJsonResult.updated) {
262
+ console.log(` ${ui.success(glyph.ok)} ${ui.label('已加入 dev script', 'Added to dev script')}`)
263
+ console.log(` ${ui.muted('npm run dev 现在会自动跑 tha watch')}`)
264
+ }
265
+
182
266
  console.log('')
183
267
  printNextSteps()
184
268
  } finally {
@@ -221,6 +305,34 @@ function ensureDirsConfig(root) {
221
305
  return CONFIG_FILE
222
306
  }
223
307
 
308
+ /**
309
+ * 自动改 package.json 的 dev script,加上 tha watch
310
+ */
311
+ function updateDevScript(root) {
312
+ const pkgPath = path.join(root, 'package.json')
313
+ if (!exists(pkgPath)) return null
314
+
315
+ try {
316
+ const pkg = JSON.parse(read(pkgPath))
317
+ if (!pkg.scripts) pkg.scripts = {}
318
+
319
+ const currentDev = pkg.scripts.dev || ''
320
+ if (currentDev.includes('tha watch')) {
321
+ return { updated: false, reason: 'already has tha watch' }
322
+ }
323
+
324
+ // 把 tha watch 加到 dev 命令前面
325
+ pkg.scripts.dev = currentDev
326
+ ? `tha watch & ${currentDev}`
327
+ : 'tha watch'
328
+
329
+ writeAtomic(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
330
+ return { updated: true, script: pkg.scripts.dev }
331
+ } catch {
332
+ return { updated: false, reason: 'package.json parse failed' }
333
+ }
334
+ }
335
+
224
336
  async function ask(rl, { cn, en, hint, fallback }) {
225
337
  const suffix = fallback ? ` ${ui.muted(`[${fallback}]`)}` : ''
226
338
  const answer = (
@@ -0,0 +1,67 @@
1
+ // ============ tha watch:监听 config.ts 变化,自动跑 sync ============
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { loadDirs, relPath } from '../config/paths.mjs'
6
+ import { runSync } from './sync.mjs'
7
+ import { glyph, ui } from '../utils/style.mjs'
8
+
9
+ const DEBOUNCE_MS = 500
10
+ let timer = null
11
+ let running = false
12
+
13
+ export async function runWatch({ root, args }) {
14
+ const { dirs } = loadDirs(root)
15
+ const domainsDir = path.join(root, dirs.domains)
16
+
17
+ if (!fs.existsSync(domainsDir)) {
18
+ throw new Error(`没有 ${dirs.domains} 目录:先运行 tha init <模块>`)
19
+ }
20
+
21
+ console.log('')
22
+ console.log(` ${ui.heading('监听模式', 'watch · auto sync on config change')}`)
23
+ console.log(` ${ui.rule(40)}`)
24
+ console.log(` ${ui.muted('监听目录:')} ${ui.path(dirs.domains)}`)
25
+ console.log(` ${ui.muted('改 config.ts 会自动跑 sync,按 Ctrl+C 退出')}`)
26
+ console.log('')
27
+
28
+ // 监听整个 domains 目录
29
+ fs.watch(domainsDir, { recursive: true }, (eventType, filename) => {
30
+ if (!filename) return
31
+
32
+ // 只关心 config.ts 文件
33
+ if (!filename.endsWith('config.ts')) return
34
+
35
+ // debounce:500ms 内只跑一次
36
+ clearTimeout(timer)
37
+ timer = setTimeout(() => {
38
+ handleConfigChange(root, dirs, filename)
39
+ }, DEBOUNCE_MS)
40
+ })
41
+
42
+ // 保持进程运行
43
+ await new Promise(() => {})
44
+ }
45
+
46
+ async function handleConfigChange(root, dirs, filename) {
47
+ if (running) {
48
+ console.log(` ${ui.muted('sync 正在跑,等跑完再触发...')}`)
49
+ return
50
+ }
51
+
52
+ running = true
53
+ console.log(` ${ui.accent(glyph.dot)} config 变化:${ui.path(filename)}`)
54
+
55
+ try {
56
+ // 从路径里提取模块名
57
+ // filename 是相对 domainsDir 的路径,比如 user/config.ts
58
+ const moduleName = filename.split('/')[0]
59
+
60
+ await runSync({ root, args: [moduleName, '--yes'] })
61
+ console.log(` ${ui.success(glyph.ok)} 已同步\n`)
62
+ } catch (error) {
63
+ console.log(` ${ui.danger(glyph.err)} 同步失败:${error.message}\n`)
64
+ } finally {
65
+ running = false
66
+ }
67
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * EXE-01:执行器之间不许互相 import
3
+ * 共用逻辑应该下沉成零件(steps/),不应该横向依赖
4
+ */
5
+
6
+ import path from 'node:path'
7
+
8
+ const DEFAULT_DIRS = {
9
+ domains: 'src/domains'
10
+ }
11
+
12
+ function getDirs(context) {
13
+ return { ...DEFAULT_DIRS, ...(context.settings?.tha?.dirs || {}) }
14
+ }
15
+
16
+ function isExecutorFile(filename, dirs) {
17
+ const domainsDir = dirs.domains.replace(/\\/g, '/')
18
+ const normalized = filename.replace(/\\/g, '/')
19
+ // 文件在 domains/<模块>/executors/ 下
20
+ return new RegExp(`/${domainsDir}/[^/]+/executors/`).test(normalized)
21
+ }
22
+
23
+ export default {
24
+ meta: {
25
+ type: 'problem',
26
+ docs: {
27
+ description: '执行器之间不许互相 import,共用逻辑请下沉成零件',
28
+ recommended: true
29
+ },
30
+ schema: []
31
+ },
32
+
33
+ create(context) {
34
+ const dirs = getDirs(context)
35
+ const filename = context.filename
36
+
37
+ if (!isExecutorFile(filename, dirs)) return {}
38
+
39
+ return {
40
+ ImportDeclaration(node) {
41
+ const importPath = node.source.value
42
+
43
+ // import 了另一个执行器(相对路径指向 executors/)
44
+ if (/^(\.\.?\/)+executors\//.test(importPath) || importPath.startsWith('./')) {
45
+ context.report({
46
+ node,
47
+ message: '执行器之间不许互相 import,共用逻辑请下沉成零件(steps/)'
48
+ })
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }