create-fullstack-scaffold 0.4.25 → 0.5.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 (135) hide show
  1. package/dist/cli/index.js +432 -15
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +10 -13
  4. package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
  5. package/template/eslint-rules/no-cross-module-service-import.js +4 -0
  6. package/template/eslint-rules/no-merged-api-type-export.js +190 -0
  7. package/template/eslint.config.js +3 -0
  8. package/template/package.json +9 -6
  9. package/template/scripts/sync-agent-hooks.mjs +189 -0
  10. package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
  11. package/template/src/admin/pages/ContentPage.tsx +1 -1
  12. package/template/src/admin/pages/DisputesPage.tsx +1 -1
  13. package/template/src/admin/pages/OrdersPage.tsx +1 -1
  14. package/template/src/admin/pages/TicketsPage.tsx +1 -1
  15. package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
  16. package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
  17. package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
  18. package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
  19. package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
  20. package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
  21. package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
  22. package/template/src/admin/services/apiClient.ts +2 -3
  23. package/template/src/cli/modules/content/index.ts +3 -3
  24. package/template/src/cli/modules/dispute/index.ts +3 -3
  25. package/template/src/cli/modules/ticket/index.ts +3 -3
  26. package/template/src/cli/modules/todo/index.ts +1 -1
  27. package/template/src/cli/rpc/client.ts +3 -4
  28. package/template/src/cli/rpc/index.ts +1 -1
  29. package/template/src/client/App.tsx +3 -39
  30. package/template/src/client/AppRoutes.tsx +53 -0
  31. package/template/src/client/entry-server.tsx +75 -0
  32. package/template/src/client/main.tsx +3 -1
  33. package/template/src/client/pages/SearchPage.tsx +1 -1
  34. package/template/src/client/services/apiClient.ts +12 -4
  35. package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
  36. package/template/src/client/stores/entry-stores.ts +36 -0
  37. package/template/src/client/stores/notificationStore.ts +4 -4
  38. package/template/src/client/stores/todoStore.ts +2 -2
  39. package/template/src/merchant/pages/DisputesPage.tsx +3 -3
  40. package/template/src/merchant/pages/OrdersPage.tsx +1 -1
  41. package/template/src/merchant/pages/ProductsPage.tsx +1 -1
  42. package/template/src/merchant/pages/SettingsPage.tsx +1 -1
  43. package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
  44. package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
  45. package/template/src/server/app.ts +6 -4
  46. package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
  47. package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
  48. package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
  49. package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
  50. package/template/src/server/core/isr-cache.ts +6 -12
  51. package/template/src/server/core/isr-invalidation.ts +6 -12
  52. package/template/src/server/core/isr-registry.ts +104 -0
  53. package/template/src/server/core/isr-renderer.ts +75 -0
  54. package/template/src/server/db/schema/contents.ts +28 -20
  55. package/template/src/server/db/schema/disputes.ts +30 -22
  56. package/template/src/server/db/schema/notifications.ts +20 -14
  57. package/template/src/server/db/schema/orders.ts +23 -16
  58. package/template/src/server/db/schema/plugins.ts +56 -38
  59. package/template/src/server/db/schema/products.ts +24 -17
  60. package/template/src/server/db/schema/tickets.ts +44 -31
  61. package/template/src/server/db/schema/todos.ts +26 -18
  62. package/template/src/server/entries/cloudflare.ts +82 -19
  63. package/template/src/server/entries/node.ts +0 -2
  64. package/template/src/server/index.ts +0 -1
  65. package/template/src/server/isr-modules.ts +10 -0
  66. package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
  67. package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
  68. package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
  69. package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
  70. package/template/src/server/module-admin/services/admin-service.ts +57 -13
  71. package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
  72. package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
  73. package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
  74. package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
  75. package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
  76. package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
  77. package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
  78. package/template/src/server/module-content/isr.ts +63 -0
  79. package/template/src/server/module-content/routes/content-routes.ts +10 -10
  80. package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
  81. package/template/src/server/module-content/routes/topics-routes.ts +3 -0
  82. package/template/src/server/module-content/services/content-service.ts +23 -10
  83. package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
  84. package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
  85. package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
  86. package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
  87. package/template/src/server/module-file/routes/file-routes.ts +3 -0
  88. package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
  89. package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
  90. package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
  91. package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
  92. package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
  93. package/template/src/server/module-order/routes/cart-routes.ts +3 -0
  94. package/template/src/server/module-order/routes/order-routes.ts +9 -4
  95. package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
  96. package/template/src/server/module-order/services/order-service.ts +25 -13
  97. package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
  98. package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
  99. package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
  100. package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
  101. package/template/src/server/module-permission/routes/role-routes.ts +3 -0
  102. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
  103. package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
  104. package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
  105. package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
  106. package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
  107. package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
  108. package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
  109. package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
  110. package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
  111. package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
  112. package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
  113. package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
  114. package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
  115. package/template/src/server/module-todos/isr.ts +41 -0
  116. package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
  117. package/template/src/server/module-todos/services/todo-service.ts +31 -10
  118. package/template/src/server/route-registry.ts +0 -4
  119. package/template/src/server/rpc-merge.ts +27 -0
  120. package/template/src/server/rpc-surface.ts +117 -0
  121. package/template/src/server/rpc-type-canary.ts +80 -0
  122. package/template/src/server/test-utils/test-client.ts +7 -9
  123. package/template/src/server/test-utils/test-isr-helper.ts +140 -0
  124. package/template/src/shared/modules/content/schemas.ts +14 -0
  125. package/template/src/shared/modules/dispute/schemas.ts +14 -0
  126. package/template/src/shared/modules/order/schemas.ts +9 -1
  127. package/template/src/shared/modules/ticket/schemas.ts +14 -0
  128. package/template/src/shared/modules/todos/index.ts +4 -0
  129. package/template/src/shared/modules/todos/schemas.ts +14 -0
  130. package/template/src/shared/schemas/index.ts +18 -0
  131. package/template/tsup.config.ts +48 -1
  132. package/template/vitest.config.ts +19 -3
  133. package/template/vitest.setup.ts +68 -5
  134. package/template/patches/typescript+5.9.3.patch +0 -24
  135. /package/template/patches/{hono+4.12.16.patch → hono+4.12.34.patch} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fullstack-scaffold",
3
- "version": "0.4.25",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-fullstack-scaffold": "dist/cli/index.js"
@@ -30,10 +30,7 @@
30
30
  "template/public"
31
31
  ],
32
32
  "workspaces": [
33
- "template",
34
- "packages/core",
35
- "packages/infrastructure",
36
- "packages/domain-plugin"
33
+ "template"
37
34
  ],
38
35
  "publishConfig": {
39
36
  "access": "public"
@@ -104,10 +101,12 @@
104
101
  "turbo:test": "turbo test",
105
102
  "turbo:lint": "turbo lint",
106
103
  "turbo:typecheck": "turbo typecheck",
107
- "turbo:all": "turbo lint typecheck test build"
104
+ "turbo:all": "turbo lint typecheck test build",
105
+ "hooks:sync": "node scripts/sync-agent-hooks.mjs",
106
+ "hooks:check": "node scripts/sync-agent-hooks.mjs --check"
108
107
  },
109
108
  "dependencies": {
110
- "@hono/node-server": "^1.14.4",
109
+ "@hono/node-server": "^1.19.15",
111
110
  "@hono/zod-openapi": "1.2.2",
112
111
  "@hono/zod-validator": "0.7.6",
113
112
  "@inquirer/prompts": "^8.4.2",
@@ -117,7 +116,7 @@
117
116
  "dotenv": "^16.5.0",
118
117
  "drizzle-orm": "^0.45.1",
119
118
  "fs-extra": "^11.3.5",
120
- "hono": "4.12.16",
119
+ "hono": "4.12.34",
121
120
  "jsonwebtoken": "^9.0.3",
122
121
  "lucide-react": "^0.576.0",
123
122
  "ora": "^9.4.0",
@@ -125,8 +124,8 @@
125
124
  "pino-pretty": "^13.0.0",
126
125
  "react": "^18.3.1",
127
126
  "react-dom": "^18.3.1",
128
- "react-router-dom": "^7.13.1",
129
- "ws": "^8.18.3",
127
+ "react-router-dom": "^7.18.3",
128
+ "ws": "^8.21.3",
130
129
  "tsx": "^4.21.0",
131
130
  "zod": "^4.4.3",
132
131
  "zustand": "^5.0.2"
@@ -183,9 +182,7 @@
183
182
  "eslint --fix",
184
183
  "eslint --max-warnings=0 --no-warn-ignored"
185
184
  ],
186
- "*.{json,md,css}": [
187
- "prettier --write"
188
- ]
185
+ "*.{json,md,css}": "prettier --write"
189
186
  },
190
187
  "engines": {
191
188
  "node": ">=18.0.0"
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @framework-baseline bd40e51c9ebe57ab
3
+ */
4
+
5
+ import { RuleTester } from 'eslint'
6
+ import tseslint from 'typescript-eslint'
7
+ import { noMergedApiTypeExport } from '../no-merged-api-type-export.js'
8
+
9
+ const ruleTester = new RuleTester({
10
+ languageOptions: {
11
+ parser: tseslint.parser,
12
+ ecmaVersion: 2022,
13
+ sourceType: 'module',
14
+ },
15
+ })
16
+
17
+ ruleTester.run('no-merged-api-type-export', noMergedApiTypeExport, {
18
+ valid: [
19
+ {
20
+ // 运行时链式挂载是合法的 —— 约束的是"类型出口",不是运行时
21
+ filename: 'src/server/route-registry.ts',
22
+ code: `
23
+ import { OpenAPIHono } from '@hono/zod-openapi'
24
+
25
+ export const clientApiRoutes = new OpenAPIHono()
26
+ .route('/api', moduleA)
27
+ .route('/api', moduleB)
28
+ .route('/api', moduleC)
29
+ `,
30
+ },
31
+ {
32
+ // 模块级类型导出(深度 = 1 个模块)是推荐做法
33
+ filename: 'src/server/module-todos/routes/todos-routes.ts',
34
+ code: `
35
+ import { OpenAPIHono } from '@hono/zod-openapi'
36
+
37
+ export const apiRoutes = new OpenAPIHono()
38
+ .openapi(listRoute, handler)
39
+
40
+ export type TodosApiType = typeof apiRoutes
41
+ `,
42
+ },
43
+ {
44
+ // typeof 引用的变量没有链式 .route —— 不是 merge 结果
45
+ filename: 'src/server/module-chat/routes/chat-routes.ts',
46
+ code: `
47
+ export const chatRoutes = new OpenAPIHono()
48
+
49
+ export type ChatApiType = typeof chatRoutes
50
+ `,
51
+ },
52
+ ],
53
+ invalid: [
54
+ {
55
+ // 出口 1: export type = typeof <链式 merge 实例>
56
+ filename: 'src/server/route-registry.ts',
57
+ code: `
58
+ import { OpenAPIHono } from '@hono/zod-openapi'
59
+
60
+ export const clientApiRoutes = new OpenAPIHono()
61
+ .route('/api', moduleA)
62
+ .route('/api', moduleB)
63
+
64
+ export type ClientApiRoutes = typeof clientApiRoutes
65
+ `,
66
+ errors: [{ messageId: 'noMergedTypeExport', data: { name: 'clientApiRoutes' } }],
67
+ },
68
+ {
69
+ // 出口 2: ReturnType<typeof createApp> —— app 工厂内部链式 merge
70
+ filename: 'src/server/app.ts',
71
+ code: `
72
+ import { OpenAPIHono } from '@hono/zod-openapi'
73
+
74
+ export function createApp() {
75
+ return new OpenAPIHono()
76
+ .route('/', clientApiRoutes)
77
+ .route('/', adminApiRoutes)
78
+ }
79
+
80
+ export type AppType = ReturnType<typeof createApp>
81
+ `,
82
+ errors: [{ messageId: 'noReturnTypeOfAppFactory', data: { name: 'createApp' } }],
83
+ },
84
+ {
85
+ // 出口 3: ReturnType<typeof hc<MegaType>>> —— 测试客户端的 mega 实例化
86
+ filename: 'src/server/test-utils/test-client.ts',
87
+ code: `
88
+ import { hc } from 'hono/client'
89
+ import type { AppType } from '@server/index'
90
+
91
+ export type TestClient = ReturnType<typeof hc<AppType>>
92
+ `,
93
+ errors: [{ messageId: 'noReturnTypeOfHcInstantiation' }],
94
+ },
95
+ ],
96
+ })
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @framework-baseline ae78703ba6753ec3
3
+ */
4
+
1
5
  /**
2
6
  * @fileoverview Prevent cross-module service internal imports
3
7
  * @author create-biomimic-app
@@ -0,0 +1,190 @@
1
+ /**
2
+ * @framework-baseline c934bac96702cb89
3
+ */
4
+
5
+ /**
6
+ * 禁止导出链式 merge 后的巨型 app 类型(TS2589 的根源治理)
7
+ *
8
+ * 背景:route-registry 把 N 个模块 .route() 链式合并,Hono 的类型系统在每一环
9
+ * 都携带前面所有环的完整累积。把这种 merge 结果导出为类型(typeof / ReturnType)
10
+ * 并被 hc<T> 实例化时,TypeScript 需要深度展开整个链条,超过 instantiationDepth
11
+ * 100 就报 TS2589。历史上曾用 typescript+5.9.3.patch 把上限改到 1000 来绕过,
12
+ * 代价是编译变慢、编辑器不稳定(该 patch 已被 no-ts-patch validator 禁止)。
13
+ *
14
+ * 正确模式:每个模块导出自己的窄类型(深度 = 1),客户端按模块实例化 hc。
15
+ * 运行时链式挂载(.route())本身合法——本规则只封"类型出口",不碰运行时。
16
+ *
17
+ * 检测三种出口形态:
18
+ * 1. export type X = typeof <初始化含 ≥2 个 .route() 的变量>
19
+ * 2. export type X = ReturnType<typeof <函数体内含 ≥2 个 .route() 的函数>>
20
+ * 3. export type X = ReturnType<typeof hc<MegaType>>>(测试客户端的 mega 实例化)
21
+ */
22
+
23
+ /** @param {import('eslint').Rule.RuleContext} context */
24
+ function create(context) {
25
+ const sourceCode = context.sourceCode || context.getSourceCode()
26
+
27
+ // 变量名 → 初始化子树内 .route() 调用 ≥2 次的声明
28
+ const chainedVars = new Map()
29
+ // 函数名 → 函数体子树内 .route() 调用 ≥2 次的声明
30
+ const appFactories = new Map()
31
+ // 待分析的 type alias(在 Program:exit 统一判定,避免依赖源码顺序)
32
+ const typeAliases = []
33
+
34
+ /**
35
+ * 统计子树内 .route() 调用次数(不依赖链根,嵌套闭包内的也算)
36
+ * @param {import('eslint').Rule.Node} node
37
+ */
38
+ function countRouteCalls(node) {
39
+ if (!node || typeof node.type !== 'string') return 0
40
+ let count = 0
41
+ if (
42
+ node.type === 'CallExpression' &&
43
+ node.callee &&
44
+ node.callee.type === 'MemberExpression' &&
45
+ node.callee.property &&
46
+ node.callee.property.type === 'Identifier' &&
47
+ node.callee.property.name === 'route'
48
+ ) {
49
+ count++
50
+ }
51
+ const keys = sourceCode.visitorKeys[node.type] || []
52
+ for (const key of keys) {
53
+ const child = node[key]
54
+ if (Array.isArray(child)) {
55
+ for (const c of child) count += countRouteCalls(c)
56
+ } else if (child && typeof child.type === 'string') {
57
+ count += countRouteCalls(child)
58
+ }
59
+ }
60
+ return count
61
+ }
62
+
63
+ return {
64
+ VariableDeclarator(node) {
65
+ if (!node.init || !node.id || node.id.type !== 'Identifier') return
66
+ if (countRouteCalls(node.init) >= 2) {
67
+ chainedVars.set(node.id.name, node)
68
+ }
69
+ },
70
+
71
+ FunctionDeclaration(node) {
72
+ if (!node.id || !node.body) return
73
+ if (countRouteCalls(node.body) >= 2) {
74
+ appFactories.set(node.id.name, node)
75
+ }
76
+ },
77
+
78
+ TSTypeAliasDeclaration(node) {
79
+ typeAliases.push(node)
80
+ },
81
+
82
+ 'Program:exit'() {
83
+ for (const alias of typeAliases) {
84
+ // 只约束导出的类型别名(规则名与语义都是 "export")
85
+ const isExported = alias.parent && alias.parent.type === 'ExportNamedDeclaration'
86
+
87
+ // typeAnnotation 直接就是类型节点(TSTypeQuery / TSTypeReference),没有中间包装
88
+ const typeRef = alias.typeAnnotation
89
+
90
+ // 形态 1: typeof <chainedVar>
91
+ if (typeRef && typeRef.type === 'TSTypeQuery') {
92
+ const exprName = typeRef.exprName
93
+ if (
94
+ isExported &&
95
+ exprName &&
96
+ exprName.type === 'Identifier' &&
97
+ chainedVars.has(exprName.name)
98
+ ) {
99
+ context.report({
100
+ node: alias,
101
+ messageId: 'noMergedTypeExport',
102
+ data: { name: exprName.name },
103
+ })
104
+ }
105
+ }
106
+
107
+ // 形态 2/3: ReturnType<...>
108
+ if (
109
+ typeRef &&
110
+ typeRef.type === 'TSTypeReference' &&
111
+ typeRef.typeName &&
112
+ typeRef.typeName.type === 'Identifier' &&
113
+ typeRef.typeName.name === 'ReturnType' &&
114
+ typeRef.typeArguments &&
115
+ typeRef.typeArguments.params &&
116
+ typeRef.typeArguments.params.length > 0
117
+ ) {
118
+ const arg = typeRef.typeArguments.params[0]
119
+
120
+ // 形态 3: typeof hc<AppType> —— instantiation expression
121
+ if (
122
+ isExported &&
123
+ arg.type === 'TSTypeQuery' &&
124
+ arg.typeArguments &&
125
+ arg.exprName &&
126
+ arg.exprName.type === 'Identifier' &&
127
+ arg.exprName.name === 'hc'
128
+ ) {
129
+ context.report({
130
+ node: alias,
131
+ messageId: 'noReturnTypeOfHcInstantiation',
132
+ })
133
+ continue
134
+ }
135
+
136
+ // 形态 2: typeof createApp
137
+ if (
138
+ isExported &&
139
+ arg.type === 'TSTypeQuery' &&
140
+ arg.exprName &&
141
+ arg.exprName.type === 'Identifier' &&
142
+ appFactories.has(arg.exprName.name)
143
+ ) {
144
+ context.report({
145
+ node: alias,
146
+ messageId: 'noReturnTypeOfAppFactory',
147
+ data: { name: arg.exprName.name },
148
+ })
149
+ }
150
+ }
151
+ }
152
+ },
153
+ }
154
+ }
155
+
156
+ export const noMergedApiTypeExport = {
157
+ meta: {
158
+ type: 'problem',
159
+ docs: {
160
+ description:
161
+ '禁止导出链式 merge 后的巨型 app 类型(typeof / ReturnType),防止 TS2589 类型爆炸',
162
+ recommended: true,
163
+ },
164
+ messages: {
165
+ noMergedTypeExport:
166
+ '禁止导出 `typeof {{{name}}}`:它的初始化链式合并了多个模块(≥2 个 .route()),' +
167
+ '导出此类型会让 hc<T> 深度实例化整条链,触发 TS2589。\n\n' +
168
+ '// ❌ 错误\n' +
169
+ 'export type ClientApiRoutes = typeof clientApiRoutes // 11 个模块的 merge\n\n' +
170
+ '// ✅ 正确 —— 每个模块导出自己的窄类型\n' +
171
+ 'export type TodosApiType = typeof apiRoutes // 模块 routes 文件里\n' +
172
+ 'export const todosClient = hc<TodosApiType>("/api") // 客户端按模块实例化',
173
+ noReturnTypeOfAppFactory:
174
+ '禁止导出 `ReturnType<typeof {{{name}}}>`:该工厂函数内部链式合并了多个模块(≥2 个 .route()),' +
175
+ '此类型会把整条 merge 链暴露给类型系统,触发 TS2589。\n\n' +
176
+ '// ❌ 错误\n' +
177
+ 'export type AppType = ReturnType<typeof createApp>\n\n' +
178
+ '// ✅ 正确 —— 运行时类型用 Hono<AppBindings>,RPC 类型按模块拆分',
179
+ noReturnTypeOfHcInstantiation:
180
+ '禁止导出 `ReturnType<typeof hc<MegaType>>>`:用 merge 后的巨型类型实例化 hc ' +
181
+ '正是 TS2589 的直接触发点(深度展开整条类型链)。\n\n' +
182
+ '// ❌ 错误\n' +
183
+ 'export type TestClient = ReturnType<typeof hc<AppType>>\n\n' +
184
+ '// ✅ 正确 —— 按模块组装的门面,每个属性是独立的窄客户端类型\n' +
185
+ 'export type TestClient = { api: { todos: TodosClient; chat: ChatClient } }',
186
+ },
187
+ schema: [],
188
+ },
189
+ create,
190
+ }
@@ -39,6 +39,7 @@ import { noDeepRelativeImports } from './eslint-rules/no-deep-relative-imports.j
39
39
  import { noCrossModuleServiceImport } from './eslint-rules/no-cross-module-service-import.js'
40
40
  import { noDisableTypeSafeClient } from './eslint-rules/no-disable-type-safe-client.js'
41
41
  import { routeLocation } from './eslint-rules/route-location.js'
42
+ import { noMergedApiTypeExport } from './eslint-rules/no-merged-api-type-export.js'
42
43
 
43
44
  const localRules = {
44
45
  rules: {
@@ -77,6 +78,7 @@ const localRules = {
77
78
  'no-cross-module-service-import': noCrossModuleServiceImport,
78
79
  'no-disable-type-safe-client': noDisableTypeSafeClient,
79
80
  'route-location': routeLocation,
81
+ 'no-merged-api-type-export': noMergedApiTypeExport,
80
82
  },
81
83
  }
82
84
 
@@ -127,6 +129,7 @@ export default tseslint.config(
127
129
  'local-rules/no-cross-module-service-import': 'error',
128
130
  'local-rules/route-location': 'error',
129
131
  'local-rules/limit-type-complexity': ['warn', { maxRouteChainLength: 15 }],
132
+ 'local-rules/no-merged-api-type-export': 'error',
130
133
  },
131
134
  },
132
135
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "biomimic-todo-app",
3
3
  "private": true,
4
- "version": "0.1.1",
4
+ "version": "0.5.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "biomimic": "./dist/cli/index.js"
@@ -67,11 +67,13 @@
67
67
  "ai:context": "node --import tsx/esm lint-scripts/ai-context-generator.ts",
68
68
  "postinstall": "node -e \"process.exit(require('fs').existsSync('node_modules/hono')?0:1)\" && patch-package || true",
69
69
  "prepare": "husky",
70
- "typecheck:full": "tsc --noEmit && eslint . --max-warnings=0"
70
+ "typecheck:full": "tsc --noEmit && eslint . --max-warnings=0",
71
+ "hooks:sync": "node scripts/sync-agent-hooks.mjs",
72
+ "hooks:check": "node scripts/sync-agent-hooks.mjs --check"
71
73
  },
72
74
  "dependencies": {
73
75
  "@dyyz1993/xcli-core": "^0.7.4",
74
- "@hono/node-server": "^1.14.4",
76
+ "@hono/node-server": "^1.19.15",
75
77
  "@hono/zod-openapi": "1.2.2",
76
78
  "@hono/zod-validator": "0.7.6",
77
79
  "@ant-design/icons": "^5.6.1",
@@ -81,7 +83,7 @@
81
83
  "dayjs": "^1.11.13",
82
84
  "dotenv": "^16.5.0",
83
85
  "drizzle-orm": "^0.45.1",
84
- "hono": "4.12.16",
86
+ "hono": "4.12.34",
85
87
  "jsonwebtoken": "^9.0.3",
86
88
  "lucide-react": "^0.576.0",
87
89
  "pino": "^9.7.0",
@@ -89,8 +91,8 @@
89
91
  "react": "^18.3.1",
90
92
  "react-dom": "^18.3.1",
91
93
  "react-helmet-async": "^3.0.0",
92
- "react-router-dom": "^7.13.1",
93
- "ws": "^8.18.3",
94
+ "react-router-dom": "^7.18.3",
95
+ "ws": "^8.21.3",
94
96
  "zod": "^4.4.3",
95
97
  "i18next": "^24.2.3",
96
98
  "react-i18next": "^15.5.3",
@@ -105,6 +107,7 @@
105
107
  "@playwright/test": "^1.60.0",
106
108
  "@prerenderer/renderer-jsdom": "^1.1.9",
107
109
  "@prerenderer/renderer-puppeteer": "^1.2.4",
110
+ "puppeteer": "^24.43.1",
108
111
  "@prerenderer/rollup-plugin": "^0.3.12",
109
112
  "@tailwindcss/postcss": "^4.2.1",
110
113
  "@testing-library/dom": "^10.4.1",
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent hooks 同步工具(规范源:仓库 .zcode/hooks/no-no-verify.mjs)
4
+ *
5
+ * 用法:
6
+ * node scripts/sync-agent-hooks.mjs # 探测环境并报告状态
7
+ * node scripts/sync-agent-hooks.mjs --global # 同步安装到各 agent 全局配置
8
+ * node scripts/sync-agent-hooks.mjs --check # 漂移检查(CI / validate:all 用)
9
+ *
10
+ * 两层模型:
11
+ * 工作区级(.zcode/config.json 随仓库走)——修改后 git pull 即生效,无需本工具
12
+ * 全局级(~/.zcode、~/.codex)——开发者机器私有,用 --global 显式同步;
13
+ * 修改规范源后需重跑 --global(--check 会提示过期)
14
+ */
15
+
16
+ import { readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync } from 'node:fs'
17
+ import { homedir } from 'node:os'
18
+ import path from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
22
+ const ROOT = path.resolve(__dirname, '..')
23
+ const CANONICAL = path.join(ROOT, '.zcode/hooks/no-no-verify.mjs')
24
+
25
+ const args = process.argv.slice(2)
26
+ const MODE = args.includes('--check') ? 'check' : args.includes('--global') ? 'global' : 'status'
27
+
28
+ // ---------- 环境探测 ----------
29
+
30
+ function detectAgents() {
31
+ const home = homedir()
32
+ return {
33
+ zcode: {
34
+ installed: existsSync(path.join(home, '.zcode')),
35
+ globalConfig: path.join(home, '.zcode/cli/config.json'),
36
+ globalHooksDir: path.join(home, '.zcode/cli/hooks'),
37
+ },
38
+ codex: {
39
+ installed: existsSync(path.join(home, '.codex')),
40
+ configToml: path.join(home, '.codex/config.toml'),
41
+ hooksDir: path.join(home, '.codex/hooks'),
42
+ },
43
+ claude: { installed: existsSync(path.join(home, '.claude')) },
44
+ cursor: { installed: existsSync(path.join(home, '.cursor')) },
45
+ }
46
+ }
47
+
48
+ // ---------- ZCode 全局(幂等注册) ----------
49
+
50
+ function syncZCodeGlobal(agents, apply) {
51
+ const target = path.join(agents.zcode.globalHooksDir, 'no-no-verify.mjs')
52
+ const cmd = `node ${JSON.stringify(target).replace(/"/g, '')}`
53
+ const cfgPath = agents.zcode.globalConfig
54
+ if (!existsSync(cfgPath)) return { status: 'skip', detail: `未找到 ${cfgPath}` }
55
+
56
+ const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'))
57
+ cfg.hooks ??= {}
58
+ cfg.hooks.enabled = true
59
+ cfg.hooks.events ??= {}
60
+ const list = (cfg.hooks.events.PreToolUse ??= [])
61
+ const command = `node ${target}`
62
+ const exists = list.some(g => (g.hooks || []).some(h => /no-verify/.test(h.command || '')))
63
+
64
+ // 无条件幂等归一:清掉所有 no-verify 家族条目(含旧 block-no-verify 命名),
65
+ // 再注册规范条目 —— 重复执行结果一致,也不会新旧双轨
66
+ for (const g of list) {
67
+ g.hooks = (g.hooks || []).filter(h => !/no-verify/.test(h.command || ''))
68
+ }
69
+ list.push({ matcher: 'Bash', hooks: [{ type: 'command', command, enabled: true, timeoutMs: 5000 }] })
70
+ const changed = true
71
+ const installedUpToDate = existsSync(target) && readFileSync(target, 'utf8') === readFileSync(CANONICAL, 'utf8')
72
+
73
+ if (MODE === 'check') {
74
+ // 未安装(如 CI runner / 新同事)不是漂移;只有"存在但过期"才是
75
+ if (!existsSync(target)) return { status: 'missing', detail: '本机未安装全局钩子(可选:npm run hooks:sync -- --global)' }
76
+ if (!installedUpToDate) return { status: 'drift', detail: '全局脚本与规范源不一致,运行 npm run hooks:sync -- --global' }
77
+ return { status: 'ok', detail: '已安装且为最新' }
78
+ }
79
+ if (MODE === 'global' && apply) {
80
+ if (!installedUpToDate) {
81
+ mkdirSync(agents.zcode.globalHooksDir, { recursive: true })
82
+ copyFileSync(CANONICAL, target)
83
+ }
84
+ if (changed || !installedUpToDate) writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
85
+ return { status: 'ok', detail: `已同步 → ${target}` }
86
+ }
87
+ return { status: exists ? 'ok' : 'missing', detail: exists ? '已注册' : '未注册' }
88
+ }
89
+
90
+ // ---------- Codex 全局(TOML 幂等追加) ----------
91
+
92
+ const CODEX_BLOCK = (scriptPath) => `# >>> scaffold agent-hooks(npm run hooks:sync 管理,勿手改) >>>
93
+ [[hooks.PreToolUse]]
94
+ matcher = "^Bash$"
95
+
96
+ [[hooks.PreToolUse.hooks]]
97
+ type = "command"
98
+ command = "node ${scriptPath}"
99
+ timeout = 5
100
+ statusMessage = "Checking for forbidden --no-verify"
101
+ # <<< scaffold agent-hooks <<<`
102
+
103
+ function syncCodexGlobal(agents, apply) {
104
+ if (!agents.codex.installed || !existsSync(agents.codex.configToml)) {
105
+ return { status: 'skip', detail: '未检测到 Codex' }
106
+ }
107
+ const target = path.join(agents.codex.hooksDir, 'no-no-verify.mjs')
108
+ const toml = readFileSync(agents.codex.configToml, 'utf8')
109
+ const hasBlock = toml.includes('scaffold agent-hooks')
110
+
111
+ if (MODE === 'check') {
112
+ if (!hasBlock && !existsSync(target)) {
113
+ return { status: 'missing', detail: '本机未检测到 Codex 钩子(可选:npm run hooks:sync -- --global)' }
114
+ }
115
+ const upToDate = hasBlock && existsSync(target) && readFileSync(target, 'utf8') === readFileSync(CANONICAL, 'utf8')
116
+ if (!upToDate) return { status: 'drift', detail: 'Codex 全局钩子过期,运行 npm run hooks:sync -- --global' }
117
+ return { status: 'ok', detail: '已安装且为最新' }
118
+ }
119
+ if (MODE === 'global' && apply) {
120
+ mkdirSync(agents.codex.hooksDir, { recursive: true })
121
+ copyFileSync(CANONICAL, target)
122
+ if (!hasBlock) {
123
+ writeFileSync(agents.codex.configToml, toml.trimEnd() + '\n\n' + CODEX_BLOCK(target) + '\n')
124
+ }
125
+ return {
126
+ status: 'ok',
127
+ detail: `已同步 → ${target}(config.toml 若有 trust-hash 机制需在 Codex 内重新信任)`,
128
+ }
129
+ }
130
+ return { status: hasBlock ? 'ok' : 'missing', detail: hasBlock ? '已注册' : '未注册(--global 安装)' }
131
+ }
132
+
133
+ // ---------- root ↔ template 副本对账 ----------
134
+
135
+ function checkTemplateMirror() {
136
+ const pairs = [
137
+ ['.zcode/hooks/no-no-verify.mjs', '.zcode/hooks/no-no-verify.mjs'],
138
+ ['.zcode/config.json', '.zcode/config.json'],
139
+ ['scripts/sync-agent-hooks.mjs', 'scripts/sync-agent-hooks.mjs'],
140
+ ]
141
+ const issues = []
142
+ for (const [rootRel, tplRel] of pairs) {
143
+ const a = path.join(ROOT, rootRel)
144
+ const b = path.join(ROOT, 'template', tplRel)
145
+ if (!existsSync(a) || !existsSync(b)) {
146
+ issues.push(`缺失:${rootRel} ↔ template/${tplRel}`)
147
+ } else if (readFileSync(a, 'utf8') !== readFileSync(b, 'utf8')) {
148
+ issues.push(`内容漂移:${rootRel} ↔ template/${tplRel}`)
149
+ }
150
+ }
151
+ return issues
152
+ }
153
+
154
+ // ---------- 主流程 ----------
155
+
156
+ const agents = detectAgents()
157
+ const results = []
158
+
159
+ if (MODE === 'check' || MODE === 'status') {
160
+ const mirrorIssues = checkTemplateMirror()
161
+ results.push({ agent: 'root↔template', r: mirrorIssues.length ? { status: 'drift', detail: mirrorIssues.join('; ') } : { status: 'ok', detail: '镜像一致' } })
162
+ }
163
+ results.push({ agent: 'ZCode 全局', r: syncZCodeGlobal(agents, MODE === 'global') })
164
+ results.push({ agent: 'Codex 全局', r: syncCodexGlobal(agents, MODE === 'global') })
165
+
166
+ if (MODE === 'status') {
167
+ console.log('🔍 Agent 环境探测:')
168
+ for (const [name, a] of Object.entries(agents)) console.log(` ${a.installed ? '✓' : '–'} ${name}`)
169
+ console.log('\n🪝 钩子状态(工作区级 .zcode/ 随仓库自动生效):')
170
+ }
171
+ for (const { agent, r } of results) {
172
+ const icon = r.status === 'ok' ? '✓' : r.status === 'drift' ? '❌' : r.status === 'missing' ? '⚠️' : '–'
173
+ console.log(` ${icon} ${agent}:${r.detail}`)
174
+ }
175
+
176
+ if (MODE === 'global') console.log('\n✅ 全局同步完成。工作区钩子(.zcode/)无需本工具,随 git 自动生效。')
177
+
178
+ if (MODE === 'check') {
179
+ // 退出码只由 root↔template 镜像决定;全局级是开发者机器私有(缺失/过期仅提示),
180
+ // 否则无 ~/.zcode 的 CI runner 或新同事会被误伤
181
+ const mirrorDrift = results.find(x => x.agent === 'root↔template' && x.r.status === 'drift')
182
+ if (mirrorDrift) {
183
+ console.error('\n❌ hooks 镜像漂移(root ↔ template),请同步副本后重试')
184
+ process.exit(1)
185
+ }
186
+ if (results.some(({ r }) => r.status === 'drift')) {
187
+ console.log('\n⚠️ 全局钩子过期:运行 npm run hooks:sync -- --global 更新本机配置')
188
+ }
189
+ }
@@ -27,7 +27,7 @@ describe('StatsCard', () => {
27
27
  render(<StatsCard title="Users" value={100} trend={{ value: 12, isUp: true }} />)
28
28
 
29
29
  expect(screen.getByText('12%')).toBeInTheDocument()
30
- expect(screen.getByText('vs last month')).toBeInTheDocument()
30
+ expect(screen.getByText('较上月')).toBeInTheDocument()
31
31
  })
32
32
 
33
33
  it('should render downward trend', () => {
@@ -39,6 +39,6 @@ describe('StatsCard', () => {
39
39
  it('should not render trend section when trend is undefined', () => {
40
40
  render(<StatsCard title="Users" value={100} />)
41
41
 
42
- expect(screen.queryByText('vs last month')).not.toBeInTheDocument()
42
+ expect(screen.queryByText('较上月')).not.toBeInTheDocument()
43
43
  })
44
44
  })
@@ -47,7 +47,7 @@ export const ContentPage: React.FC = () => {
47
47
  const response = await apiClient.api.contents.$get({ query: {} })
48
48
  const result = await response.json()
49
49
  if (result.success) {
50
- setContents(result.data)
50
+ setContents(result.data.contents)
51
51
  } else {
52
52
  message.error(t('content.loadFailed'))
53
53
  }
@@ -48,7 +48,7 @@ export const DisputesPage: React.FC = () => {
48
48
  const response = await apiClient.api.disputes.$get({ query: {} })
49
49
  const result = await response.json()
50
50
  if (result.success) {
51
- setDisputes(result.data)
51
+ setDisputes(result.data.disputes)
52
52
  } else {
53
53
  message.error(result.error || t('disputes.loadFailed'))
54
54
  }
@@ -45,7 +45,7 @@ export const OrdersPage: React.FC = () => {
45
45
  })
46
46
  const result = await response.json()
47
47
  if (result.success) {
48
- setOrders(result.data)
48
+ setOrders(result.data.orders)
49
49
  } else {
50
50
  message.error(result.error || t('orders.loadFailed'))
51
51
  }