create-fullstack-scaffold 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 (530) hide show
  1. package/README.md +154 -0
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +439 -0
  4. package/dist/cli/index.js.map +1 -0
  5. package/package.json +171 -0
  6. package/template/.agents/skills/writing-hookify-rules/SKILL.md +408 -0
  7. package/template/.ai-context.md +368 -0
  8. package/template/.claude/hookify.block-dangerous-commands.local.md +41 -0
  9. package/template/.claude/hookify.check-api-types.local.md +111 -0
  10. package/template/.claude/hookify.check-duplicate-types.local.md +287 -0
  11. package/template/.claude/hookify.check-test-assertions.local.md +93 -0
  12. package/template/.claude/hookify.remind-add-tests.local.md +209 -0
  13. package/template/.claude/hookify.remind-db-migration.local.md +58 -0
  14. package/template/.claude/hookify.remind-run-tests.local.md +52 -0
  15. package/template/.claude/hookify.warn-any-type.local.md +203 -0
  16. package/template/.claude/hookify.warn-console-log.local.md +73 -0
  17. package/template/.claude/rules/00-project-config.md +147 -0
  18. package/template/.claude/rules/01-file-types.md +127 -0
  19. package/template/.claude/rules/02-git-workflow.md +167 -0
  20. package/template/.claude/rules/10-api-type-inference.md +555 -0
  21. package/template/.claude/rules/20-server-api.md +349 -0
  22. package/template/.claude/rules/21-server-entrypoint.md +257 -0
  23. package/template/.claude/rules/30-client-components.md +314 -0
  24. package/template/.claude/rules/31-client-services.md +768 -0
  25. package/template/.claude/rules/32-client-state-zustand.md +324 -0
  26. package/template/.claude/rules/33-client-app-entry.md +276 -0
  27. package/template/.claude/rules/34-client-admin-module.md +445 -0
  28. package/template/.claude/rules/35-cli-module.md +259 -0
  29. package/template/.claude/rules/40-admin-module.md +495 -0
  30. package/template/.claude/rules/40-shared-types.md +450 -0
  31. package/template/.claude/rules/50-websocket.md +245 -0
  32. package/template/.claude/rules/51-sse.md +372 -0
  33. package/template/.claude/rules/52-cloudflare-realtime.md +384 -0
  34. package/template/.claude/rules/60-testing-standards.md +245 -0
  35. package/template/.claude/rules/61-hono-testing.md +200 -0
  36. package/template/.claude/scripts/README.md +187 -0
  37. package/template/.claude/scripts/check-duplicate-types.ts +188 -0
  38. package/template/.claude/scripts/post-edit-check-incremental.sh +61 -0
  39. package/template/.claude/scripts/post-edit-check.sh +22 -0
  40. package/template/.claude/scripts/pre-tool-use-check.sh +86 -0
  41. package/template/.claude/scripts/test-pre-tool-use.sh +73 -0
  42. package/template/.claude/scripts/validate-git-changes.sh +38 -0
  43. package/template/.claude/settings.json +32 -0
  44. package/template/.claude/skills/writing-hookify-rules/SKILL.md +408 -0
  45. package/template/.dev-context-compact.md +54 -0
  46. package/template/.dev-context.md +621 -0
  47. package/template/.dev-guide.md +243 -0
  48. package/template/.env.example +60 -0
  49. package/template/.eslintrc.routes.json +39 -0
  50. package/template/.husky/_/husky.sh +27 -0
  51. package/template/.husky/commit-msg +4 -0
  52. package/template/.husky/post-commit +4 -0
  53. package/template/.husky/pre-commit +19 -0
  54. package/template/.husky/pre-push +12 -0
  55. package/template/.prettierignore +22 -0
  56. package/template/.prettierrc +8 -0
  57. package/template/.sessions/3/2026-03-26T05-41-09-154Z_03bb597f-3898-4a2e-8c08-5746bbe8d95e.jsonl +18 -0
  58. package/template/.sessions/3/2026-03-26T05-48-30-052Z_12de4774-9cb3-494c-9852-bd1e6b0003c3.jsonl +6 -0
  59. package/template/.vscode/extensions.json +8 -0
  60. package/template/.vscode/settings.json +4 -0
  61. package/template/.workspaces/3/hello.py +1 -0
  62. package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite +0 -0
  63. package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite-shm +0 -0
  64. package/template/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/803c2e37182b913f8857edf6d8776515cef4af6d48fcb25913e457f4b14ffa97.sqlite-wal +0 -0
  65. package/template/CLAUDE.md +296 -0
  66. package/template/DESIGN.md +260 -0
  67. package/template/DEV_CONTEXT_GUIDE.md +366 -0
  68. package/template/DEV_TOOLS_README.md +396 -0
  69. package/template/FRAMEWORK_HISTORY.md +34 -0
  70. package/template/PROJECT_SUMMARY.md +325 -0
  71. package/template/QUICKSTART.md +305 -0
  72. package/template/README.md +154 -0
  73. package/template/STRUCTURE.txt +81 -0
  74. package/template/admin.html +13 -0
  75. package/template/auth-inject.html +33 -0
  76. package/template/commitlint.config.js +12 -0
  77. package/template/docs/PERMISSION_ARCHITECTURE.md +279 -0
  78. package/template/docs/PERMISSION_EXAMPLES.md +230 -0
  79. package/template/docs/PERMISSION_SYSTEM.md +301 -0
  80. package/template/docs/SMART_TEST.md +171 -0
  81. package/template/docs/runtime-abstract.md +253 -0
  82. package/template/drizzle/0000_rainy_boomer.sql +101 -0
  83. package/template/drizzle/0001_add_todo_attachments.sql +12 -0
  84. package/template/drizzle/0002_chilly_magneto.sql +89 -0
  85. package/template/drizzle/meta/0000_snapshot.json +652 -0
  86. package/template/drizzle/meta/0001_snapshot.json +735 -0
  87. package/template/drizzle/meta/0002_snapshot.json +1198 -0
  88. package/template/drizzle/meta/_journal.json +27 -0
  89. package/template/drizzle.config.ts +13 -0
  90. package/template/eslint-rules/README-no-middleware-in-routes.md +154 -0
  91. package/template/eslint-rules/__tests__/enforce-valid-method.test.ts +251 -0
  92. package/template/eslint-rules/__tests__/layer-boundary.test.ts +102 -0
  93. package/template/eslint-rules/__tests__/module-boundary.test.ts +77 -0
  94. package/template/eslint-rules/__tests__/no-direct-zod-import-in-file-routes.test.ts +45 -0
  95. package/template/eslint-rules/__tests__/no-new-old-service-naming.test.ts +76 -0
  96. package/template/eslint-rules/e2e-test-location.js +153 -0
  97. package/template/eslint-rules/enforce-valid-method.js +217 -0
  98. package/template/eslint-rules/flat-routes-services.js +69 -0
  99. package/template/eslint-rules/framework-protect.js +186 -0
  100. package/template/eslint-rules/layer-boundary.js +164 -0
  101. package/template/eslint-rules/limit-type-complexity.js +130 -0
  102. package/template/eslint-rules/middleware-location.js +162 -0
  103. package/template/eslint-rules/module-boundary.js +125 -0
  104. package/template/eslint-rules/no-ambiguous-file-paths.js +64 -0
  105. package/template/eslint-rules/no-any-on-apiclient.js +173 -0
  106. package/template/eslint-rules/no-boolean-success.js +43 -0
  107. package/template/eslint-rules/no-deep-relative-imports.js +69 -0
  108. package/template/eslint-rules/no-direct-fetch.js +122 -0
  109. package/template/eslint-rules/no-direct-ws-sse.js +57 -0
  110. package/template/eslint-rules/no-direct-zod-import-in-file-routes.js +54 -0
  111. package/template/eslint-rules/no-disable-direct-fetch.js +57 -0
  112. package/template/eslint-rules/no-disable-type-safe-client.js +58 -0
  113. package/template/eslint-rules/no-inline-schema.js +220 -0
  114. package/template/eslint-rules/no-middleware-in-routes.js +81 -0
  115. package/template/eslint-rules/no-new-old-service-naming.js +123 -0
  116. package/template/eslint-rules/no-type-assertion-in-rpc.js +99 -0
  117. package/template/eslint-rules/no-type-assertion-on-shared-types.js +109 -0
  118. package/template/eslint-rules/no-util-functions-in-service.js +128 -0
  119. package/template/eslint-rules/prefer-shared-types.js +352 -0
  120. package/template/eslint-rules/protect-ws-sse-interface.js +66 -0
  121. package/template/eslint-rules/require-antd-generic-types.js +73 -0
  122. package/template/eslint-rules/require-file-openapi-props.js +114 -0
  123. package/template/eslint-rules/require-hono-chain-syntax.js +129 -0
  124. package/template/eslint-rules/require-nullable-for-optional.js +99 -0
  125. package/template/eslint-rules/require-response-helpers.js +140 -0
  126. package/template/eslint-rules/require-type-safe-test-client.js +106 -0
  127. package/template/eslint-rules/route-location.js +72 -0
  128. package/template/eslint.config.js +231 -0
  129. package/template/index.html +17 -0
  130. package/template/lint-scripts/DIRECTORY_RULES.md +335 -0
  131. package/template/lint-scripts/README.md +299 -0
  132. package/template/lint-scripts/TESTING.md +273 -0
  133. package/template/lint-scripts/ai-context-generator.ts +159 -0
  134. package/template/lint-scripts/check-framework-modify.sh +8 -0
  135. package/template/lint-scripts/check-menu-permissions.js +242 -0
  136. package/template/lint-scripts/check-refs.ts +165 -0
  137. package/template/lint-scripts/check-route-auth-complete.js +260 -0
  138. package/template/lint-scripts/check-route-auth.js +212 -0
  139. package/template/lint-scripts/check-route-permissions.js +210 -0
  140. package/template/lint-scripts/config/project.config.ts +358 -0
  141. package/template/lint-scripts/dev-context-analyzer.ts +707 -0
  142. package/template/lint-scripts/dev-guide-generator.ts +830 -0
  143. package/template/lint-scripts/dev-server.ts +58 -0
  144. package/template/lint-scripts/eslint-route-auth.js +158 -0
  145. package/template/lint-scripts/eslint-routes-permission.js +147 -0
  146. package/template/lint-scripts/fix-route-auth.js +245 -0
  147. package/template/lint-scripts/framework/check-modify.ts +152 -0
  148. package/template/lint-scripts/framework/hash-utils.ts +180 -0
  149. package/template/lint-scripts/framework/init-baseline.ts +84 -0
  150. package/template/lint-scripts/framework/update-baseline.ts +148 -0
  151. package/template/lint-scripts/post-commit-track.ts +66 -0
  152. package/template/lint-scripts/quick-test.sh +27 -0
  153. package/template/lint-scripts/smart-test.ts +268 -0
  154. package/template/lint-scripts/test-history.ts +360 -0
  155. package/template/lint-scripts/test-tracker.ts +350 -0
  156. package/template/lint-scripts/validate-all.ts +250 -0
  157. package/template/lint-scripts/validators/api-coverage.validator.ts +355 -0
  158. package/template/lint-scripts/validators/client-rpc.validator.ts +190 -0
  159. package/template/lint-scripts/validators/client-tests.validator.ts +155 -0
  160. package/template/lint-scripts/validators/console-log.validator.ts +122 -0
  161. package/template/lint-scripts/validators/directory-structure.validator.ts +181 -0
  162. package/template/lint-scripts/validators/imports.validator.ts +180 -0
  163. package/template/lint-scripts/validators/index.ts +271 -0
  164. package/template/lint-scripts/validators/md-refs.validator.ts +165 -0
  165. package/template/lint-scripts/validators/module-tests.validator.ts +112 -0
  166. package/template/lint-scripts/validators/sensitive.validator.ts +185 -0
  167. package/template/lint-scripts/validators/server-rpc.validator.ts +141 -0
  168. package/template/lint-scripts/validators/test-quality.validator.ts +257 -0
  169. package/template/lint-scripts/validators/todos.validator.ts +126 -0
  170. package/template/lint-scripts/watch-validator.ts +131 -0
  171. package/template/modules.config.ts +71 -0
  172. package/template/package-lock.json +14554 -0
  173. package/template/package.json +158 -0
  174. package/template/patches/@hono+zod-openapi+1.2.2.patch +67 -0
  175. package/template/patches/@hono+zod-validator+0.7.6.patch +13 -0
  176. package/template/patches/hono+4.12.16.patch +246 -0
  177. package/template/patches/typescript+5.8.3.patch +26 -0
  178. package/template/playwright.config.ts +60 -0
  179. package/template/pnpm-lock.yaml +7137 -0
  180. package/template/postcss.config.js +6 -0
  181. package/template/scripts/create-template.ts +1724 -0
  182. package/template/src/admin/App.tsx +59 -0
  183. package/template/src/admin/components/AccountSwitcher.tsx +104 -0
  184. package/template/src/admin/components/CaptchaModal.tsx +150 -0
  185. package/template/src/admin/components/NotificationDrawer.tsx +213 -0
  186. package/template/src/admin/components/PageHeader.tsx +45 -0
  187. package/template/src/admin/components/PermissionConfigEditor.tsx +237 -0
  188. package/template/src/admin/components/PermissionGuard.tsx +108 -0
  189. package/template/src/admin/components/PermissionTree.tsx +193 -0
  190. package/template/src/admin/components/ProtectedRoute.tsx +33 -0
  191. package/template/src/admin/components/StatsCard.tsx +44 -0
  192. package/template/src/admin/components/UserFormModal.tsx +67 -0
  193. package/template/src/admin/components/UserTable.tsx +72 -0
  194. package/template/src/admin/components/__tests__/PageHeader.test.tsx +48 -0
  195. package/template/src/admin/components/__tests__/PermissionConfigEditor.test.tsx +147 -0
  196. package/template/src/admin/components/__tests__/PermissionGuard.test.tsx +221 -0
  197. package/template/src/admin/components/__tests__/PermissionTree.test.tsx +187 -0
  198. package/template/src/admin/components/__tests__/ProtectedRoute.test.tsx +129 -0
  199. package/template/src/admin/components/__tests__/StatsCard.test.tsx +44 -0
  200. package/template/src/admin/components/__tests__/UserFormModal.test.tsx +125 -0
  201. package/template/src/admin/components/__tests__/UserTable.test.tsx +89 -0
  202. package/template/src/admin/components/index.ts +7 -0
  203. package/template/src/admin/hooks/__tests__/useAuditLogs.test.ts +109 -0
  204. package/template/src/admin/hooks/__tests__/usePermissions.test.tsx +327 -0
  205. package/template/src/admin/hooks/__tests__/usePermissionsBranches.test.ts +198 -0
  206. package/template/src/admin/hooks/__tests__/useRoles.test.ts +261 -0
  207. package/template/src/admin/hooks/useAdminNotifications.ts +156 -0
  208. package/template/src/admin/hooks/useAuditLogs.ts +44 -0
  209. package/template/src/admin/hooks/useConfig.ts +168 -0
  210. package/template/src/admin/hooks/usePermissions.ts +156 -0
  211. package/template/src/admin/hooks/useRoles.ts +120 -0
  212. package/template/src/admin/layouts/Header.tsx +93 -0
  213. package/template/src/admin/layouts/Layout.tsx +23 -0
  214. package/template/src/admin/layouts/Sidebar.tsx +120 -0
  215. package/template/src/admin/main.tsx +18 -0
  216. package/template/src/admin/pages/ContentPage.tsx +252 -0
  217. package/template/src/admin/pages/DashboardPage.tsx +141 -0
  218. package/template/src/admin/pages/DisputesPage.tsx +261 -0
  219. package/template/src/admin/pages/LoginPage.tsx +121 -0
  220. package/template/src/admin/pages/MediaTestPage.tsx +524 -0
  221. package/template/src/admin/pages/OrdersPage.tsx +302 -0
  222. package/template/src/admin/pages/PermissionsPage.tsx +95 -0
  223. package/template/src/admin/pages/RegisterPage.tsx +103 -0
  224. package/template/src/admin/pages/RolesPage.tsx +330 -0
  225. package/template/src/admin/pages/SettingsPage.tsx +73 -0
  226. package/template/src/admin/pages/SystemLogsPage.tsx +218 -0
  227. package/template/src/admin/pages/TestCaptchaPage.tsx +120 -0
  228. package/template/src/admin/pages/TicketsPage.tsx +292 -0
  229. package/template/src/admin/pages/UsersPage.tsx +282 -0
  230. package/template/src/admin/pages/__tests__/ContentPage.test.tsx +213 -0
  231. package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +153 -0
  232. package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +196 -0
  233. package/template/src/admin/pages/__tests__/LoginPage.test.tsx +112 -0
  234. package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +217 -0
  235. package/template/src/admin/pages/__tests__/PermissionsPage.test.tsx +119 -0
  236. package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +179 -0
  237. package/template/src/admin/pages/__tests__/RolesPage.test.tsx +256 -0
  238. package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +66 -0
  239. package/template/src/admin/pages/__tests__/SystemLogsPage.test.tsx +125 -0
  240. package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +202 -0
  241. package/template/src/admin/pages/__tests__/UsersPage.test.tsx +229 -0
  242. package/template/src/admin/services/README.md +263 -0
  243. package/template/src/admin/services/TESTING.md +323 -0
  244. package/template/src/admin/services/__tests__/skipLoading.test.ts +146 -0
  245. package/template/src/admin/services/apiClient.ts +66 -0
  246. package/template/src/admin/services/requestInterceptor.ts +127 -0
  247. package/template/src/admin/services/types.ts +5 -0
  248. package/template/src/admin/stores/__tests__/adminStore.test.ts +184 -0
  249. package/template/src/admin/stores/__tests__/adminStoreBranches.test.ts +148 -0
  250. package/template/src/admin/stores/__tests__/captchaStore.test.ts +75 -0
  251. package/template/src/admin/stores/__tests__/captchaStoreBranches.test.ts +37 -0
  252. package/template/src/admin/stores/__tests__/loadingStore.test.ts +68 -0
  253. package/template/src/admin/stores/adminStore.ts +88 -0
  254. package/template/src/admin/stores/captchaStore.ts +47 -0
  255. package/template/src/admin/stores/loadingStore.ts +27 -0
  256. package/template/src/cli/index.ts +22 -0
  257. package/template/src/cli/modules/config/index.ts +132 -0
  258. package/template/src/cli/modules/index.ts +12 -0
  259. package/template/src/cli/modules/notification/index.ts +86 -0
  260. package/template/src/cli/modules/todo/index.ts +60 -0
  261. package/template/src/cli/rpc/client.ts +23 -0
  262. package/template/src/cli/rpc/index.ts +1 -0
  263. package/template/src/cli/utils/api.ts +17 -0
  264. package/template/src/cli/utils/auto-command.ts +229 -0
  265. package/template/src/cli/utils/index.ts +8 -0
  266. package/template/src/cli/utils/logger.ts +79 -0
  267. package/template/src/client/App.tsx +24 -0
  268. package/template/src/client/Layout.tsx +27 -0
  269. package/template/src/client/components/AuthButton.tsx +41 -0
  270. package/template/src/client/components/ConnectionStatus.tsx +75 -0
  271. package/template/src/client/components/EmptyState.tsx +26 -0
  272. package/template/src/client/components/Footer.tsx +17 -0
  273. package/template/src/client/components/LoadingSpinner.tsx +26 -0
  274. package/template/src/client/components/MessageCard.tsx +61 -0
  275. package/template/src/client/components/Navigation.tsx +65 -0
  276. package/template/src/client/components/StatusBadge.tsx +50 -0
  277. package/template/src/client/components/__tests__/App.test.tsx +79 -0
  278. package/template/src/client/components/__tests__/AuthButton.test.tsx +119 -0
  279. package/template/src/client/components/__tests__/ConnectionStatus.test.tsx +188 -0
  280. package/template/src/client/components/__tests__/EmptyState.test.tsx +70 -0
  281. package/template/src/client/components/__tests__/Footer.test.tsx +22 -0
  282. package/template/src/client/components/__tests__/LoadingSpinner.test.tsx +75 -0
  283. package/template/src/client/components/__tests__/MessageCard.test.tsx +142 -0
  284. package/template/src/client/components/__tests__/Navigation.test.tsx +40 -0
  285. package/template/src/client/components/__tests__/StatusBadge.test.tsx +107 -0
  286. package/template/src/client/components/index.ts +6 -0
  287. package/template/src/client/index.css +27 -0
  288. package/template/src/client/main.tsx +25 -0
  289. package/template/src/client/pages/ContentDetailPage.tsx +142 -0
  290. package/template/src/client/pages/ContentListPage.tsx +169 -0
  291. package/template/src/client/pages/NotificationPage.tsx +288 -0
  292. package/template/src/client/pages/TodoPage.tsx +389 -0
  293. package/template/src/client/pages/WebSocketPage.tsx +239 -0
  294. package/template/src/client/pages/__tests__/ContentDetailPage.test.tsx +176 -0
  295. package/template/src/client/pages/__tests__/ContentListPage.test.tsx +202 -0
  296. package/template/src/client/pages/__tests__/NotificationPage.test.tsx +246 -0
  297. package/template/src/client/pages/__tests__/TodoPage.test.tsx +433 -0
  298. package/template/src/client/pages/__tests__/WebSocketPage.test.tsx +378 -0
  299. package/template/src/client/services/apiClient.ts +66 -0
  300. package/template/src/client/stores/__tests__/authStore.test.ts +139 -0
  301. package/template/src/client/stores/__tests__/chatWSStore.test.ts +416 -0
  302. package/template/src/client/stores/__tests__/notificationStore.test.ts +453 -0
  303. package/template/src/client/stores/__tests__/todoStore.test.ts +509 -0
  304. package/template/src/client/stores/authStore.ts +51 -0
  305. package/template/src/client/stores/chatWSStore.ts +122 -0
  306. package/template/src/client/stores/notificationStore.ts +199 -0
  307. package/template/src/client/stores/todoStore.ts +207 -0
  308. package/template/src/server/__tests__/integration/todos-api.test.ts +138 -0
  309. package/template/src/server/app.ts +105 -0
  310. package/template/src/server/config.ts +86 -0
  311. package/template/src/server/core/__tests__/realtime-core.test.ts +329 -0
  312. package/template/src/server/core/__tests__/realtime-scanner.test.ts +210 -0
  313. package/template/src/server/core/__tests__/runtime-node.test.ts +330 -0
  314. package/template/src/server/core/__tests__/runtime.test.ts +219 -0
  315. package/template/src/server/core/__tests__/typed-runtime.test.ts +178 -0
  316. package/template/src/server/core/durable-objects/RealtimeDO.ts +206 -0
  317. package/template/src/server/core/index.ts +83 -0
  318. package/template/src/server/core/module-loader.ts +281 -0
  319. package/template/src/server/core/realtime-core.ts +152 -0
  320. package/template/src/server/core/realtime-scanner.ts +99 -0
  321. package/template/src/server/core/runtime-cloudflare.ts +207 -0
  322. package/template/src/server/core/runtime-node.ts +170 -0
  323. package/template/src/server/core/runtime.ts +129 -0
  324. package/template/src/server/core/typed-runtime.ts +80 -0
  325. package/template/src/server/db/__tests__/driver.test.ts +203 -0
  326. package/template/src/server/db/config.ts +1 -0
  327. package/template/src/server/db/driver-cloudflare.ts +50 -0
  328. package/template/src/server/db/driver.ts +116 -0
  329. package/template/src/server/db/index.ts +3 -0
  330. package/template/src/server/db/init.ts +412 -0
  331. package/template/src/server/db/schema/api-endpoints.ts +23 -0
  332. package/template/src/server/db/schema/contents.ts +31 -0
  333. package/template/src/server/db/schema/disputes.ts +39 -0
  334. package/template/src/server/db/schema/index.ts +14 -0
  335. package/template/src/server/db/schema/notifications.ts +16 -0
  336. package/template/src/server/db/schema/orders.ts +30 -0
  337. package/template/src/server/db/schema/permission-audit-logs.ts +17 -0
  338. package/template/src/server/db/schema/permission-route-mappings.ts +22 -0
  339. package/template/src/server/db/schema/permissions.ts +17 -0
  340. package/template/src/server/db/schema/role-permissions.ts +22 -0
  341. package/template/src/server/db/schema/roles.ts +17 -0
  342. package/template/src/server/db/schema/tickets.ts +62 -0
  343. package/template/src/server/db/schema/todo-attachments.ts +22 -0
  344. package/template/src/server/db/schema/todos.ts +21 -0
  345. package/template/src/server/db/schema/user-roles.ts +17 -0
  346. package/template/src/server/db/seeds/index.ts +1 -0
  347. package/template/src/server/db/seeds/permission-data.ts +354 -0
  348. package/template/src/server/db/test-setup.ts +647 -0
  349. package/template/src/server/entries/cloudflare.ts +71 -0
  350. package/template/src/server/entries/node.ts +190 -0
  351. package/template/src/server/index.ts +62 -0
  352. package/template/src/server/middleware/__tests__/auth-simple.test.ts +147 -0
  353. package/template/src/server/middleware/__tests__/auth.test.ts +276 -0
  354. package/template/src/server/middleware/__tests__/captcha.test.ts +258 -0
  355. package/template/src/server/middleware/__tests__/error-handler.test.ts +364 -0
  356. package/template/src/server/middleware/__tests__/error-response-format.test.ts +80 -0
  357. package/template/src/server/middleware/__tests__/permission.test.ts +312 -0
  358. package/template/src/server/middleware/audit-log.ts +101 -0
  359. package/template/src/server/middleware/auth.ts +235 -0
  360. package/template/src/server/middleware/captcha.ts +166 -0
  361. package/template/src/server/middleware/cors.ts +30 -0
  362. package/template/src/server/middleware/error-handler.ts +135 -0
  363. package/template/src/server/middleware/index.ts +24 -0
  364. package/template/src/server/middleware/logger.ts +73 -0
  365. package/template/src/server/middleware/permission.ts +127 -0
  366. package/template/src/server/middleware/rate-limit.ts +69 -0
  367. package/template/src/server/middleware/realtime-env.ts +12 -0
  368. package/template/src/server/module-admin/__tests__/admin-routes.test.ts +360 -0
  369. package/template/src/server/module-admin/__tests__/admin-service.test.ts +222 -0
  370. package/template/src/server/module-admin/__tests__/audit-log-routes.test.ts +261 -0
  371. package/template/src/server/module-admin/__tests__/export-routes.test.ts +160 -0
  372. package/template/src/server/module-admin/__tests__/media-routes.test.ts +122 -0
  373. package/template/src/server/module-admin/module.ts +40 -0
  374. package/template/src/server/module-admin/routes/admin-notification-routes.ts +197 -0
  375. package/template/src/server/module-admin/routes/admin-routes.ts +20 -0
  376. package/template/src/server/module-admin/routes/auth-routes.ts +86 -0
  377. package/template/src/server/module-admin/routes/export-routes.ts +207 -0
  378. package/template/src/server/module-admin/routes/media-routes.ts +78 -0
  379. package/template/src/server/module-admin/routes/system-routes.ts +89 -0
  380. package/template/src/server/module-admin/routes/user-management-routes.ts +143 -0
  381. package/template/src/server/module-admin/services/admin-service.ts +268 -0
  382. package/template/src/server/module-captcha/__tests__/captcha-route.test.ts +35 -0
  383. package/template/src/server/module-captcha/__tests__/captcha-service.test.ts +53 -0
  384. package/template/src/server/module-captcha/module.ts +35 -0
  385. package/template/src/server/module-captcha/routes/captcha-routes.ts +68 -0
  386. package/template/src/server/module-captcha/services/captcha-service.ts +3 -0
  387. package/template/src/server/module-chat/__tests__/chat-routes.test.ts +39 -0
  388. package/template/src/server/module-chat/__tests__/chat-rpc.test.ts +185 -0
  389. package/template/src/server/module-chat/__tests__/chat-service.test.ts +64 -0
  390. package/template/src/server/module-chat/module.ts +27 -0
  391. package/template/src/server/module-chat/routes/chat-routes.ts +46 -0
  392. package/template/src/server/module-chat/services/chat-service.ts +38 -0
  393. package/template/src/server/module-content/__tests__/content-route.test.ts +40 -0
  394. package/template/src/server/module-content/__tests__/content-service.test.ts +107 -0
  395. package/template/src/server/module-content/module.ts +39 -0
  396. package/template/src/server/module-content/routes/content-routes.ts +182 -0
  397. package/template/src/server/module-content/routes/public-content-routes.ts +58 -0
  398. package/template/src/server/module-content/services/content-service.ts +224 -0
  399. package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +43 -0
  400. package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +135 -0
  401. package/template/src/server/module-dispute/module.ts +30 -0
  402. package/template/src/server/module-dispute/routes/dispute-routes.ts +153 -0
  403. package/template/src/server/module-dispute/services/dispute-service.ts +255 -0
  404. package/template/src/server/module-file/__tests__/file-routes.test.ts +284 -0
  405. package/template/src/server/module-file/__tests__/file-storage-service.test.ts +323 -0
  406. package/template/src/server/module-file/module.ts +28 -0
  407. package/template/src/server/module-file/routes/file-routes.ts +288 -0
  408. package/template/src/server/module-notifications/__tests__/notification-route-rpc.test.ts +375 -0
  409. package/template/src/server/module-notifications/__tests__/notification-service.test.ts +241 -0
  410. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +289 -0
  411. package/template/src/server/module-notifications/module.ts +32 -0
  412. package/template/src/server/module-notifications/routes/notification-routes.ts +190 -0
  413. package/template/src/server/module-notifications/services/notification-service.ts +104 -0
  414. package/template/src/server/module-order/__tests__/order-route.test.ts +258 -0
  415. package/template/src/server/module-order/__tests__/order-service.test.ts +222 -0
  416. package/template/src/server/module-order/module.ts +30 -0
  417. package/template/src/server/module-order/routes/order-routes.ts +202 -0
  418. package/template/src/server/module-order/services/order-service.ts +268 -0
  419. package/template/src/server/module-permission/__tests__/permission-middleware.test.ts +90 -0
  420. package/template/src/server/module-permission/__tests__/permission-routes.test.ts +330 -0
  421. package/template/src/server/module-permission/__tests__/permission-service-impl.test.ts +388 -0
  422. package/template/src/server/module-permission/__tests__/permission-service.test.ts +190 -0
  423. package/template/src/server/module-permission/__tests__/role-routes.test.ts +215 -0
  424. package/template/src/server/module-permission/module.ts +68 -0
  425. package/template/src/server/module-permission/routes/audit-log-routes.ts +76 -0
  426. package/template/src/server/module-permission/routes/permission-routes.ts +184 -0
  427. package/template/src/server/module-permission/routes/role-routes.ts +263 -0
  428. package/template/src/server/module-permission/services/audit-log-service.ts +108 -0
  429. package/template/src/server/module-permission/services/permission-service-impl.ts +233 -0
  430. package/template/src/server/module-permission/services/permission-service.ts +256 -0
  431. package/template/src/server/module-permission/services/role-service.ts +122 -0
  432. package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +173 -0
  433. package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +201 -0
  434. package/template/src/server/module-ticket/module.ts +30 -0
  435. package/template/src/server/module-ticket/routes/ticket-routes.ts +210 -0
  436. package/template/src/server/module-ticket/services/ticket-service.ts +297 -0
  437. package/template/src/server/module-todos/__tests__/todo-service.test.ts +196 -0
  438. package/template/src/server/module-todos/__tests__/todos-file-upload.test.ts +439 -0
  439. package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +575 -0
  440. package/template/src/server/module-todos/module.ts +30 -0
  441. package/template/src/server/module-todos/routes/todos-routes.ts +260 -0
  442. package/template/src/server/module-todos/services/todo-service.ts +202 -0
  443. package/template/src/server/route-registry.ts +47 -0
  444. package/template/src/server/test-utils/test-client.ts +66 -0
  445. package/template/src/server/test-utils/test-server.ts +131 -0
  446. package/template/src/server/types/bindings.ts +10 -0
  447. package/template/src/server/utils/__tests__/app-error.test.ts +374 -0
  448. package/template/src/server/utils/__tests__/auth.test.ts +76 -0
  449. package/template/src/server/utils/__tests__/captcha.test.ts +80 -0
  450. package/template/src/server/utils/__tests__/file-storage.test.ts +472 -0
  451. package/template/src/server/utils/__tests__/logger.test.ts +221 -0
  452. package/template/src/server/utils/__tests__/route-helpers.test.ts +264 -0
  453. package/template/src/server/utils/app-error.ts +355 -0
  454. package/template/src/server/utils/auth.ts +65 -0
  455. package/template/src/server/utils/captcha.ts +119 -0
  456. package/template/src/server/utils/date.ts +50 -0
  457. package/template/src/server/utils/env.ts +4 -0
  458. package/template/src/server/utils/file-storage.ts +462 -0
  459. package/template/src/server/utils/generate.ts +26 -0
  460. package/template/src/server/utils/id-helpers.ts +5 -0
  461. package/template/src/server/utils/logger.ts +139 -0
  462. package/template/src/server/utils/permission-utils.ts +6 -0
  463. package/template/src/server/utils/response.ts +40 -0
  464. package/template/src/server/utils/route-helpers.ts +200 -0
  465. package/template/src/server/utils/uuid.ts +8 -0
  466. package/template/src/server/utils/ws-helper.ts +30 -0
  467. package/template/src/shared/constants/index.ts +1 -0
  468. package/template/src/shared/constants/resource-types.ts +72 -0
  469. package/template/src/shared/core/__tests__/sse-client.test.ts +376 -0
  470. package/template/src/shared/core/__tests__/ws-client.test.ts +468 -0
  471. package/template/src/shared/core/api-request.ts +192 -0
  472. package/template/src/shared/core/api-schemas.ts +41 -0
  473. package/template/src/shared/core/index.ts +37 -0
  474. package/template/src/shared/core/module-manifest.ts +120 -0
  475. package/template/src/shared/core/protocol-types.ts +26 -0
  476. package/template/src/shared/core/sse-client.ts +185 -0
  477. package/template/src/shared/core/ws-client.ts +236 -0
  478. package/template/src/shared/hooks/index.ts +2 -0
  479. package/template/src/shared/hooks/useSSE.ts +57 -0
  480. package/template/src/shared/hooks/useWebSocket.ts +66 -0
  481. package/template/src/shared/index.ts +5 -0
  482. package/template/src/shared/modules/admin/index.ts +1 -0
  483. package/template/src/shared/modules/admin/schemas.ts +100 -0
  484. package/template/src/shared/modules/audit/index.ts +1 -0
  485. package/template/src/shared/modules/audit/schemas.ts +24 -0
  486. package/template/src/shared/modules/captcha/index.ts +1 -0
  487. package/template/src/shared/modules/captcha/schemas.ts +19 -0
  488. package/template/src/shared/modules/chat/index.ts +38 -0
  489. package/template/src/shared/modules/content/index.ts +1 -0
  490. package/template/src/shared/modules/content/schemas.ts +53 -0
  491. package/template/src/shared/modules/dispute/index.ts +1 -0
  492. package/template/src/shared/modules/dispute/schemas.ts +62 -0
  493. package/template/src/shared/modules/files/index.ts +11 -0
  494. package/template/src/shared/modules/files/schemas.ts +44 -0
  495. package/template/src/shared/modules/index.ts +70 -0
  496. package/template/src/shared/modules/notifications/index.ts +26 -0
  497. package/template/src/shared/modules/notifications/schemas.ts +73 -0
  498. package/template/src/shared/modules/order/index.ts +1 -0
  499. package/template/src/shared/modules/order/schemas.ts +63 -0
  500. package/template/src/shared/modules/permission/index.ts +42 -0
  501. package/template/src/shared/modules/permission/permission-dependencies.ts +50 -0
  502. package/template/src/shared/modules/permission/permissions.ts +275 -0
  503. package/template/src/shared/modules/permission/schemas.ts +92 -0
  504. package/template/src/shared/modules/permission/types.ts +23 -0
  505. package/template/src/shared/modules/role/index.ts +1 -0
  506. package/template/src/shared/modules/role/schemas.ts +46 -0
  507. package/template/src/shared/modules/ticket/index.ts +1 -0
  508. package/template/src/shared/modules/ticket/schemas.ts +77 -0
  509. package/template/src/shared/modules/todos/index.ts +20 -0
  510. package/template/src/shared/modules/todos/schemas.ts +65 -0
  511. package/template/src/shared/schemas/index.ts +79 -0
  512. package/template/src/types/global.d.ts +14 -0
  513. package/template/src/vite-env.d.ts +21 -0
  514. package/template/tailwind.config.js +11 -0
  515. package/template/tests/e2e/admin-crud.spec.ts +81 -0
  516. package/template/tests/e2e/case-o1.spec.ts +108 -0
  517. package/template/tests/e2e/global-setup.ts +73 -0
  518. package/template/tests/e2e/global-teardown.ts +28 -0
  519. package/template/tests/e2e/notification.spec.ts +458 -0
  520. package/template/tests/e2e/todo.spec.ts +623 -0
  521. package/template/tests/e2e/websocket.spec.ts +254 -0
  522. package/template/tsconfig.json +35 -0
  523. package/template/tsup.config.ts +95 -0
  524. package/template/vite-plugins.ts +113 -0
  525. package/template/vite.config.ts +96 -0
  526. package/template/vitest.config.ts +53 -0
  527. package/template/vitest.integration.config.ts +24 -0
  528. package/template/vitest.integration.setup.ts +22 -0
  529. package/template/vitest.setup.ts +74 -0
  530. package/template/wrangler.toml +33 -0
@@ -0,0 +1,1724 @@
1
+ /**
2
+ * 模块模板生成器
3
+ *
4
+ * 用法:
5
+ * npm run create:module <name> [options]
6
+ *
7
+ * 选项:
8
+ * --with-db 包含数据库操作
9
+ * --sse SSE (Server-Sent Events) 模板
10
+ * --ws WebSocket 模板
11
+ * --admin 创建管理后台模块 (module-admin-xxx)
12
+ *
13
+ * 示例:
14
+ * npm run create:module product # 基础模板(无数据库)
15
+ * npm run create:module product --with-db # 数据库模板
16
+ * npm run create:module notifications --sse # SSE 模板
17
+ * npm run create:module chat --ws # WebSocket 模板
18
+ * npm run create:module users --admin # 管理后台模块 (module-admin-users)
19
+ * npm run create:module reports --admin --with-db # 管理后台数据库模块
20
+ *
21
+ * 自动操作:
22
+ * 1. 创建模块目录结构
23
+ * 2. 创建 schema 文件 (shared/modules/{name}/schemas.ts)
24
+ * 3. 更新 app.ts 导入和注册路由
25
+ * 4. 更新 shared/modules/index.ts 导出
26
+ */
27
+
28
+ import fs from 'fs'
29
+ import path from 'path'
30
+ import { fileURLToPath } from 'url'
31
+
32
+ const __filename = fileURLToPath(import.meta.url)
33
+ const __dirname = path.dirname(__filename)
34
+ const templateDir = path.resolve(__dirname, '..')
35
+
36
+ interface CreateOptions {
37
+ name: string
38
+ withDatabase: boolean
39
+ withSSE: boolean
40
+ withWebSocket: boolean
41
+ prefix: string
42
+ }
43
+
44
+ interface ModifiedFileResult {
45
+ success: boolean
46
+ diff?: string
47
+ addedLines?: number
48
+ }
49
+
50
+ interface CreatedFile {
51
+ path: string
52
+ type: 'created' | 'modified'
53
+ diff?: string
54
+ lineCount?: number
55
+ }
56
+
57
+ function generateClientUsageExample(name: string, options: CreateOptions): string {
58
+ const pascalName = toPascalCase(name)
59
+ const camelName = toCamelCase(name)
60
+ const kebabName = toKebabCase(name)
61
+
62
+ if (options.withSSE) {
63
+ return `
64
+ // 📡 SSE 客户端使用示例
65
+
66
+ // 1️⃣ React Hook 方式(推荐)
67
+ import { useSSE } from '@shared/hooks'
68
+ import { apiClient } from '@client/services/apiClient'
69
+ import type { ${pascalName}SSEProtocol, ${pascalName}Event } from '@shared/modules/${kebabName}'
70
+
71
+ function ${pascalName}Component() {
72
+ const { status, connect, disconnect, client } = useSSE<${pascalName}SSEProtocol>(
73
+ () => apiClient.api['${kebabName}s'].stream.$sse()
74
+ )
75
+
76
+ useEffect(() => {
77
+ connect()
78
+ return () => disconnect()
79
+ }, [connect, disconnect])
80
+
81
+ useEffect(() => {
82
+ if (!client) return
83
+
84
+ // 🎯 类型安全的事件监听
85
+ const unsubscribe = client.on('message', (event: ${pascalName}Event) => {
86
+ console.log('收到事件:', event)
87
+ })
88
+
89
+ return unsubscribe
90
+ }, [client])
91
+
92
+ return <div>SSE 状态: {status}</div>
93
+ }
94
+
95
+ // 2️⃣ 直接使用 apiClient
96
+ import { apiClient } from '@client/services/apiClient'
97
+
98
+ // ========== 基础 SSE 连接 ==========
99
+
100
+ const sseClient = apiClient.api['${kebabName}s'].stream.$sse()
101
+
102
+ // 监听连接状态
103
+ sseClient.onStatusChange(status => {
104
+ console.log('SSE 状态:', status)
105
+ })
106
+
107
+ // 监听事件(带类型推导)
108
+ sseClient.on('message', (event: ${pascalName}Event) => {
109
+ console.log('收到事件:', event)
110
+ })
111
+
112
+ // 关闭连接
113
+ sseClient.abort()
114
+ `
115
+ } else if (options.withWebSocket) {
116
+ return `
117
+ // 🔌 WebSocket 客户端使用示例
118
+
119
+ // 1️⃣ React Hook 方式(推荐)
120
+ import { useWebSocket } from '@shared/hooks'
121
+ import { apiClient } from '@client/services/apiClient'
122
+ import type { ${pascalName}Message } from '@shared/modules/${kebabName}'
123
+
124
+ function ${pascalName}Component() {
125
+ const { status, connect, disconnect, call, emit, on } = useWebSocket(
126
+ apiClient.api['${kebabName}s'].ws
127
+ )
128
+
129
+ useEffect(() => {
130
+ connect()
131
+ return () => disconnect()
132
+ }, [connect, disconnect])
133
+
134
+ useEffect(() => {
135
+ // 🎯 类型安全的事件监听 - 根据 type 自动推导 payload 类型
136
+ const unsubscribe = on('message', (message: ${pascalName}Message) => {
137
+ // TypeScript 能够根据 message.type 自动推导 payload 类型
138
+ switch (message.type) {
139
+ case 'chat':
140
+ // message.payload 类型自动推导为 ChatPayload
141
+ console.log('聊天消息:', message.payload)
142
+ break
143
+ case 'notification':
144
+ // message.payload 类型自动推导为 NotificationPayload
145
+ console.log('通知:', message.payload)
146
+ break
147
+ case 'system':
148
+ // message.payload 类型自动推导为 SystemPayload
149
+ console.log('系统消息:', message.payload)
150
+ break
151
+ }
152
+ })
153
+
154
+ return unsubscribe
155
+ }, [on])
156
+
157
+ // 🎯 类型安全的 RPC 调用
158
+ const handleEcho = async () => {
159
+ // 参数和返回值都有类型检查
160
+ const result = await call('echo', { message: 'Hello' })
161
+ console.log('Echo 结果:', result)
162
+ }
163
+
164
+ // 🎯 类型安全的事件发送
165
+ const handleSend = () => {
166
+ emit('message', { id: '1', type: 'chat', payload: { text: 'Hello' } })
167
+ }
168
+
169
+ return (
170
+ <div>
171
+ <p>WebSocket 状态: {status}</p>
172
+ <button onClick={handleEcho}>Echo</button>
173
+ <button onClick={handleSend}>发送消息</button>
174
+ </div>
175
+ )
176
+ }
177
+
178
+ // 2️⃣ 直接使用 apiClient
179
+ import { apiClient } from '@client/services/apiClient'
180
+
181
+ // ========== 基础 WebSocket 连接 ==========
182
+
183
+ const wsClient = apiClient.api['${kebabName}s'].ws.$ws()
184
+
185
+ // 监听连接状态
186
+ wsClient.onStatusChange(status => {
187
+ console.log('WebSocket 状态:', status)
188
+ })
189
+
190
+ // 发送消息(带类型检查)
191
+ wsClient.emit('message', { id: '1', type: 'chat', payload: { text: 'Hello' } })
192
+
193
+ // 监听消息(带类型推导)
194
+ wsClient.on('message', (message: ${pascalName}Message) => {
195
+ if (message.type === 'chat') {
196
+ // TypeScript 知道 message.payload 是 ChatPayload 类型
197
+ console.log('聊天:', message.payload)
198
+ }
199
+ })
200
+
201
+ // RPC 调用(带类型检查)
202
+ const result = await wsClient.call('echo', { message: 'Hello' })
203
+
204
+ // 关闭连接
205
+ wsClient.close()
206
+
207
+ // ========== 🔐 带认证的 WebSocket 连接 ==========
208
+
209
+ // 方式 1: 使用 header 选项传递认证 token
210
+ const authWsClient = apiClient.api['${kebabName}s'].ws.$ws({
211
+ header: {
212
+ Authorization: 'Bearer your-token-here',
213
+ },
214
+ })
215
+
216
+ // 方式 2: 在创建 apiClient 时配置默认 headers
217
+ // 参考 src/client/services/apiClient.ts
218
+
219
+ // ========== 🛡️ 需要登录的 WebSocket 路由示例 ==========
220
+
221
+ // 假设路由配置了 authMiddleware 中间件:
222
+ // middleware: [authMiddleware()]
223
+
224
+ // 带认证的 WebSocket 连接会自动验证 token
225
+ // - 连接时验证 token
226
+ // - 无效 token 会导致连接被拒绝
227
+
228
+ // 示例:聊天室 WebSocket(需要登录)
229
+ const chatWs = apiClient.api.chat.ws.$ws({
230
+ header: {
231
+ Authorization: 'Bearer user-token',
232
+ },
233
+ })
234
+
235
+ chatWs.on('message', (message) => {
236
+ console.log('收到消息:', message)
237
+ })
238
+
239
+ chatWs.emit('message', { id: '1', type: 'chat', payload: { text: 'Hello' } })
240
+ `
241
+ } else {
242
+ return `
243
+ // 📦 REST API 客户端使用示例
244
+
245
+ // 1️⃣ React Hook 方式(推荐使用 SWR 或 React Query)
246
+ import useSWR from 'swr'
247
+ import { apiClient } from '@client/services/apiClient'
248
+ import type { ${pascalName}, Create${pascalName}Input, Update${pascalName}Input } from '@shared/modules/${kebabName}'
249
+
250
+ function ${pascalName}Component() {
251
+ // 🎯 类型安全的列表查询
252
+ const { data: listData, mutate } = useSWR(
253
+ '${kebabName}s-list',
254
+ async () => {
255
+ const res = await apiClient.api['${kebabName}s'].$get()
256
+ const data = await res.json()
257
+ return data.success ? data.data : []
258
+ }
259
+ )
260
+
261
+ // 🎯 类型安全的创建操作
262
+ const handleCreate = async (input: Create${pascalName}Input) => {
263
+ const res = await apiClient.api['${kebabName}s'].$post({ json: input })
264
+ const data = await res.json()
265
+ if (data.success) {
266
+ mutate() // 刷新列表
267
+ }
268
+ }
269
+
270
+ // 🎯 类型安全的更新操作
271
+ const handleUpdate = async (id: string, input: Update${pascalName}Input) => {
272
+ const res = await apiClient.api['${kebabName}s'][':id'].$put({
273
+ param: { id },
274
+ json: input,
275
+ })
276
+ const data = await res.json()
277
+ if (data.success) {
278
+ mutate()
279
+ }
280
+ }
281
+
282
+ // 🎯 类型安全的删除操作
283
+ const handleDelete = async (id: string) => {
284
+ const res = await apiClient.api['${kebabName}s'][':id'].$delete({
285
+ param: { id },
286
+ })
287
+ const data = await res.json()
288
+ if (data.success) {
289
+ mutate()
290
+ }
291
+ }
292
+
293
+ return (
294
+ <div>
295
+ {listData?.map(item => (
296
+ <div key={item.id}>
297
+ {item.name}
298
+ <button onClick={() => handleUpdate(item.id, { name: 'Updated' })}>更新</button>
299
+ <button onClick={() => handleDelete(item.id)}>删除</button>
300
+ </div>
301
+ ))}
302
+ </div>
303
+ )
304
+ }
305
+
306
+ // 2️⃣ 直接使用 apiClient
307
+ import { apiClient } from '@client/services/apiClient'
308
+ import type { ${pascalName}, Create${pascalName}Input } from '@shared/modules/${kebabName}'
309
+
310
+ // ========== 基础 CRUD 操作 ==========
311
+
312
+ // 获取列表
313
+ const listRes = await apiClient.api['${kebabName}s'].$get()
314
+ const listData = await listRes.json()
315
+ if (listData.success) {
316
+ console.log('列表:', listData.data) // data 类型为 ${pascalName}[]
317
+ }
318
+
319
+ // 获取单个
320
+ const getRes = await apiClient.api['${kebabName}s'][':id'].$get({
321
+ param: { id: '1' },
322
+ })
323
+ const getData = await getRes.json()
324
+ if (getData.success) {
325
+ console.log('详情:', getData.data) // data 类型为 ${pascalName}
326
+ }
327
+
328
+ // 创建
329
+ const createRes = await apiClient.api['${kebabName}s'].$post({
330
+ json: { name: 'New ${pascalName}' } as Create${pascalName}Input,
331
+ })
332
+ const createData = await createRes.json()
333
+ if (createData.success) {
334
+ console.log('创建成功:', createData.data)
335
+ }
336
+
337
+ // 更新
338
+ const updateRes = await apiClient.api['${kebabName}s'][':id'].$put({
339
+ param: { id: '1' },
340
+ json: { name: 'Updated ${pascalName}' },
341
+ })
342
+ const updateData = await updateRes.json()
343
+ if (updateData.success) {
344
+ console.log('更新成功:', updateData.data)
345
+ }
346
+
347
+ // 删除
348
+ const deleteRes = await apiClient.api['${kebabName}s'][':id'].$delete({
349
+ param: { id: '1' },
350
+ })
351
+ const deleteData = await deleteRes.json()
352
+ if (deleteData.success) {
353
+ console.log('删除成功')
354
+ }
355
+
356
+ // ========== 🔐 带认证的请求(Header 示例) ==========
357
+
358
+ // 方式 1: 使用 header 选项传递认证 token
359
+ const authRes = await apiClient.api['${kebabName}s'].$get({
360
+ header: {
361
+ Authorization: 'Bearer your-token-here',
362
+ },
363
+ })
364
+
365
+ // 方式 2: 在创建 apiClient 时配置默认 headers
366
+ // 参考 src/client/services/apiClient.ts
367
+
368
+ // ========== 🛡️ 需要登录的路由示例 ==========
369
+
370
+ // 假设路由配置了 authMiddleware 中间件:
371
+ // middleware: [authMiddleware({ requiredRole: 'admin' })]
372
+
373
+ // 带认证的请求会自动验证 token
374
+ // - 401: 未提供有效的认证 token
375
+ // - 403: 权限不足(如需要 admin 角色但用户是普通用户)
376
+
377
+ // 示例:获取当前用户信息(需要登录)
378
+ const meRes = await apiClient.api.admin.me.$get({
379
+ header: {
380
+ Authorization: 'Bearer admin-token', // 或 'Bearer user-token'
381
+ },
382
+ })
383
+ const meData = await meRes.json()
384
+ if (meData.success) {
385
+ console.log('当前用户:', meData.data)
386
+ }
387
+
388
+ // 示例:管理员操作(需要 admin 角色)
389
+ const adminRes = await apiClient.api.admin.stats.$get({
390
+ header: {
391
+ Authorization: 'Bearer admin-token', // 必须是 admin token
392
+ },
393
+ })
394
+ const adminData = await adminRes.json()
395
+ if (adminData.success) {
396
+ console.log('系统统计:', adminData.data)
397
+ }
398
+
399
+ // ========== ⚠️ 错误处理示例 ==========
400
+
401
+ try {
402
+ const res = await apiClient.api.admin.stats.$get({
403
+ header: {
404
+ Authorization: 'Bearer invalid-token',
405
+ },
406
+ })
407
+ const data = await res.json()
408
+ if (!data.success) {
409
+ console.log('错误:', data.error) // "Unauthorized: Invalid authentication token"
410
+ }
411
+ } catch (error) {
412
+ console.error('请求失败:', error)
413
+ }
414
+ `
415
+ }
416
+ }
417
+
418
+ function toPascalCase(str: string): string {
419
+ return str
420
+ .split(/[-_\s]+/)
421
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
422
+ .join('')
423
+ }
424
+
425
+ function toCamelCase(str: string): string {
426
+ const pascal = toPascalCase(str)
427
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1)
428
+ }
429
+
430
+ function toKebabCase(str: string): string {
431
+ return str
432
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
433
+ .replace(/[\s_]+/g, '-')
434
+ .toLowerCase()
435
+ }
436
+
437
+ // ============================================
438
+ // Schema 模板
439
+ // ============================================
440
+
441
+ function generateBasicSchema(name: string): string {
442
+ const pascalName = toPascalCase(name)
443
+
444
+ return `import { z } from '@hono/zod-openapi'
445
+
446
+ export const ${pascalName}Schema = z.object({
447
+ id: z.string(),
448
+ name: z.string(),
449
+ createdAt: z.string().optional(),
450
+ updatedAt: z.string().optional(),
451
+ })
452
+
453
+ export const Create${pascalName}Schema = ${pascalName}Schema.omit({ id: true, createdAt: true, updatedAt: true })
454
+
455
+ export const Update${pascalName}Schema = ${pascalName}Schema.partial().omit({ id: true, createdAt: true, updatedAt: true })
456
+
457
+ export const ${pascalName}ListSchema = z.array(${pascalName}Schema)
458
+
459
+ export const DeleteResultSchema = z.object({ message: z.string() })
460
+
461
+ export type ${pascalName} = z.infer<typeof ${pascalName}Schema>
462
+ export type Create${pascalName}Input = z.infer<typeof Create${pascalName}Schema>
463
+ export type Update${pascalName}Input = z.infer<typeof Update${pascalName}Schema>
464
+ export type DeleteResult = z.infer<typeof DeleteResultSchema>
465
+ `
466
+ }
467
+
468
+ function generateSSESchema(name: string): string {
469
+ const pascalName = toPascalCase(name)
470
+
471
+ return `import { z } from '@hono/zod-openapi'
472
+
473
+ export const ${pascalName}EventSchema = z.object({
474
+ id: z.string(),
475
+ type: z.enum(['created', 'updated', 'deleted']),
476
+ data: z.unknown(),
477
+ timestamp: z.string(),
478
+ })
479
+
480
+ export const ${pascalName}SSEProtocolSchema = z.object({
481
+ events: z.object({
482
+ message: ${pascalName}EventSchema,
483
+ ping: z.object({ timestamp: z.number() }),
484
+ connected: z.object({ timestamp: z.number() }),
485
+ }),
486
+ })
487
+
488
+ export type ${pascalName}Event = z.infer<typeof ${pascalName}EventSchema>
489
+ export type ${pascalName}SSEProtocol = z.infer<typeof ${pascalName}SSEProtocolSchema>
490
+ `
491
+ }
492
+
493
+ function generateWebSocketSchema(name: string): string {
494
+ const pascalName = toPascalCase(name)
495
+
496
+ return `import { z } from '@hono/zod-openapi'
497
+
498
+ export const ${pascalName}MessageSchema = z.object({
499
+ id: z.string(),
500
+ type: z.string(),
501
+ payload: z.unknown(),
502
+ timestamp: z.string(),
503
+ senderId: z.string().optional(),
504
+ })
505
+
506
+ export const ${pascalName}ConnectionSchema = z.object({
507
+ userId: z.string().optional(),
508
+ roomId: z.string().optional(),
509
+ })
510
+
511
+ export const WebSocketStatusSchema = z.object({
512
+ connectedClients: z.number(),
513
+ })
514
+
515
+ export type ${pascalName}Message = z.infer<typeof ${pascalName}MessageSchema>
516
+ export type ${pascalName}Connection = z.infer<typeof ${pascalName}ConnectionSchema>
517
+ export type WebSocketStatus = z.infer<typeof WebSocketStatusSchema>
518
+ `
519
+ }
520
+
521
+ // ============================================
522
+ // 路由模板
523
+ // ============================================
524
+
525
+ function generateBasicRouteTemplate(name: string): string {
526
+ const pascalName = toPascalCase(name)
527
+ const camelName = toCamelCase(name)
528
+ const kebabName = toKebabCase(name)
529
+
530
+ return `import { createRoute } from '@hono/zod-openapi'
531
+ import { OpenAPIHono } from '@hono/zod-openapi'
532
+ import * as ${camelName}Service from '../services/${kebabName}-service'
533
+ import { successResponse, errorResponse } from '../../utils/route-helpers'
534
+ import {
535
+ ${pascalName}Schema,
536
+ Create${pascalName}Schema,
537
+ Update${pascalName}Schema,
538
+ ${pascalName}ListSchema,
539
+ DeleteResultSchema,
540
+ } from '@shared/modules/${kebabName}'
541
+
542
+ const listRoute = createRoute({
543
+ method: 'get',
544
+ path: '/${kebabName}s',
545
+ tags: ['${kebabName}s'],
546
+ responses: {
547
+ 200: successResponse(${pascalName}ListSchema, 'List all ${kebabName}s'),
548
+ 500: errorResponse('Internal server error'),
549
+ },
550
+ })
551
+
552
+ const getRoute = createRoute({
553
+ method: 'get',
554
+ path: '/${kebabName}s/{id}',
555
+ tags: ['${kebabName}s'],
556
+ request: {
557
+ params: ${pascalName}Schema.pick({ id: true }),
558
+ },
559
+ responses: {
560
+ 200: successResponse(${pascalName}Schema, 'Get ${kebabName} by id'),
561
+ 404: errorResponse('${pascalName} not found'),
562
+ 500: errorResponse('Internal server error'),
563
+ },
564
+ })
565
+
566
+ const createRouteDef = createRoute({
567
+ method: 'post',
568
+ path: '/${kebabName}s',
569
+ tags: ['${kebabName}s'],
570
+ request: {
571
+ body: {
572
+ content: {
573
+ 'application/json': {
574
+ schema: Create${pascalName}Schema,
575
+ },
576
+ },
577
+ },
578
+ },
579
+ responses: {
580
+ 201: successResponse(${pascalName}Schema, 'Create ${kebabName}'),
581
+ 400: errorResponse('Invalid input'),
582
+ 500: errorResponse('Internal server error'),
583
+ },
584
+ })
585
+
586
+ const updateRoute = createRoute({
587
+ method: 'put',
588
+ path: '/${kebabName}s/{id}',
589
+ tags: ['${kebabName}s'],
590
+ request: {
591
+ params: ${pascalName}Schema.pick({ id: true }),
592
+ body: {
593
+ content: {
594
+ 'application/json': {
595
+ schema: Update${pascalName}Schema,
596
+ },
597
+ },
598
+ },
599
+ },
600
+ responses: {
601
+ 200: successResponse(${pascalName}Schema, 'Update ${kebabName}'),
602
+ 404: errorResponse('${pascalName} not found'),
603
+ 400: errorResponse('Invalid input'),
604
+ 500: errorResponse('Internal server error'),
605
+ },
606
+ })
607
+
608
+ const deleteRoute = createRoute({
609
+ method: 'delete',
610
+ path: '/${kebabName}s/{id}',
611
+ tags: ['${kebabName}s'],
612
+ request: {
613
+ params: ${pascalName}Schema.pick({ id: true }),
614
+ },
615
+ responses: {
616
+ 200: successResponse(DeleteResultSchema, '${pascalName} deleted'),
617
+ 404: errorResponse('${pascalName} not found'),
618
+ 500: errorResponse('Internal server error'),
619
+ },
620
+ })
621
+
622
+ export const ${camelName}Routes = new OpenAPIHono()
623
+ .openapi(listRoute, async c => {
624
+ const result = await ${camelName}Service.getAll()
625
+ return c.json({ success: true, data: result })
626
+ })
627
+ .openapi(getRoute, async c => {
628
+ const { id } = c.req.valid('param')
629
+ const result = await ${camelName}Service.getById(id)
630
+ if (!result) {
631
+ return c.json({ success: false, error: '${pascalName} not found' }, 404)
632
+ }
633
+ return c.json({ success: true, data: result })
634
+ })
635
+ .openapi(createRouteDef, async c => {
636
+ const body = c.req.valid('json')
637
+ const result = await ${camelName}Service.create(body)
638
+ return c.json({ success: true, data: result }, 201)
639
+ })
640
+ .openapi(updateRoute, async c => {
641
+ const { id } = c.req.valid('param')
642
+ const body = c.req.valid('json')
643
+ const result = await ${camelName}Service.update(id, body)
644
+ if (!result) {
645
+ return c.json({ success: false, error: '${pascalName} not found' }, 404)
646
+ }
647
+ return c.json({ success: true, data: result })
648
+ })
649
+ .openapi(deleteRoute, async c => {
650
+ const { id } = c.req.valid('param')
651
+ const result = await ${camelName}Service.delete${pascalName}(id)
652
+ if (!result) {
653
+ return c.json({ success: false, error: '${pascalName} not found' }, 404)
654
+ }
655
+ return c.json({ success: true, data: { message: 'Deleted successfully' } })
656
+ })
657
+ `
658
+ }
659
+
660
+ function generateDatabaseRouteTemplate(name: string): string {
661
+ const pascalName = toPascalCase(name)
662
+ const camelName = toCamelCase(name)
663
+ const kebabName = toKebabCase(name)
664
+
665
+ return `import { createRoute } from '@hono/zod-openapi'
666
+ import { OpenAPIHono } from '@hono/zod-openapi'
667
+ import * as ${camelName}Service from '../services/${kebabName}-service'
668
+ import { successResponse, errorResponse } from '../../utils/route-helpers'
669
+ import {
670
+ ${pascalName}Schema,
671
+ Create${pascalName}Schema,
672
+ Update${pascalName}Schema,
673
+ ${pascalName}ListSchema,
674
+ DeleteResultSchema,
675
+ } from '@shared/modules/${kebabName}'
676
+
677
+ const listRoute = createRoute({
678
+ method: 'get',
679
+ path: '/${kebabName}s',
680
+ tags: ['${kebabName}s'],
681
+ responses: {
682
+ 200: successResponse(${pascalName}ListSchema, 'List all ${kebabName}s'),
683
+ 500: errorResponse('Internal server error'),
684
+ },
685
+ })
686
+
687
+ const getRoute = createRoute({
688
+ method: 'get',
689
+ path: '/${kebabName}s/{id}',
690
+ tags: ['${kebabName}s'],
691
+ request: {
692
+ params: ${pascalName}Schema.pick({ id: true }),
693
+ },
694
+ responses: {
695
+ 200: successResponse(${pascalName}Schema, 'Get ${kebabName} by id'),
696
+ 404: errorResponse('${pascalName} not found'),
697
+ 500: errorResponse('Internal server error'),
698
+ },
699
+ })
700
+
701
+ const createRouteDef = createRoute({
702
+ method: 'post',
703
+ path: '/${kebabName}s',
704
+ tags: ['${kebabName}s'],
705
+ request: {
706
+ body: {
707
+ content: {
708
+ 'application/json': {
709
+ schema: Create${pascalName}Schema,
710
+ },
711
+ },
712
+ },
713
+ },
714
+ responses: {
715
+ 201: successResponse(${pascalName}Schema, 'Create ${kebabName}'),
716
+ 400: errorResponse('Invalid input'),
717
+ 500: errorResponse('Internal server error'),
718
+ },
719
+ })
720
+
721
+ const updateRoute = createRoute({
722
+ method: 'put',
723
+ path: '/${kebabName}s/{id}',
724
+ tags: ['${kebabName}s'],
725
+ request: {
726
+ params: ${pascalName}Schema.pick({ id: true }),
727
+ body: {
728
+ content: {
729
+ 'application/json': {
730
+ schema: Update${pascalName}Schema,
731
+ },
732
+ },
733
+ },
734
+ },
735
+ responses: {
736
+ 200: successResponse(${pascalName}Schema, 'Update ${kebabName}'),
737
+ 404: errorResponse('${pascalName} not found'),
738
+ 400: errorResponse('Invalid input'),
739
+ 500: errorResponse('Internal server error'),
740
+ },
741
+ })
742
+
743
+ const deleteRoute = createRoute({
744
+ method: 'delete',
745
+ path: '/${kebabName}s/{id}',
746
+ tags: ['${kebabName}s'],
747
+ request: {
748
+ params: ${pascalName}Schema.pick({ id: true }),
749
+ },
750
+ responses: {
751
+ 200: successResponse(DeleteResultSchema, '${pascalName} deleted'),
752
+ 404: errorResponse('${pascalName} not found'),
753
+ 500: errorResponse('Internal server error'),
754
+ },
755
+ })
756
+
757
+ export const ${camelName}Routes = new OpenAPIHono()
758
+ .openapi(listRoute, async c => {
759
+ const result = await ${camelName}Service.getAll()
760
+ return c.json({ success: true, data: result })
761
+ })
762
+ .openapi(getRoute, async c => {
763
+ const { id } = c.req.valid('param')
764
+ const result = await ${camelName}Service.getById(id)
765
+ if (!result) {
766
+ return c.json({ success: false, error: '${pascalName} not found' }, 404)
767
+ }
768
+ return c.json({ success: true, data: result })
769
+ })
770
+ .openapi(createRouteDef, async c => {
771
+ const body = c.req.valid('json')
772
+ const result = await ${camelName}Service.create(body)
773
+ return c.json({ success: true, data: result }, 201)
774
+ })
775
+ .openapi(updateRoute, async c => {
776
+ const { id } = c.req.valid('param')
777
+ const body = c.req.valid('json')
778
+ const result = await ${camelName}Service.update(id, body)
779
+ if (!result) {
780
+ return c.json({ success: false, error: '${pascalName} not found' }, 404)
781
+ }
782
+ return c.json({ success: true, data: result })
783
+ })
784
+ .openapi(deleteRoute, async c => {
785
+ const { id } = c.req.valid('param')
786
+ const result = await ${camelName}Service.delete${pascalName}(id)
787
+ if (!result) {
788
+ return c.json({ success: false, error: '${pascalName} not found' }, 404)
789
+ }
790
+ return c.json({ success: true, data: { message: 'Deleted successfully' } })
791
+ })
792
+ `
793
+ }
794
+
795
+ function generateSSERouteTemplate(name: string): string {
796
+ const pascalName = toPascalCase(name)
797
+ const camelName = toCamelCase(name)
798
+ const kebabName = toKebabCase(name)
799
+
800
+ return `import { createRoute } from '@hono/zod-openapi'
801
+ import { OpenAPIHono } from '@hono/zod-openapi'
802
+ import * as ${camelName}Service from '../services/${kebabName}-service'
803
+ import { ${pascalName}SSEProtocolSchema } from '@shared/modules/${kebabName}'
804
+ import { getRuntimeAdapter } from '@server/core/runtime'
805
+
806
+ const streamRoute = createRoute({
807
+ method: 'get',
808
+ path: '/${kebabName}s/stream',
809
+ tags: ['${kebabName}s'],
810
+ responses: {
811
+ 200: {
812
+ description: 'SSE stream for ${kebabName} events',
813
+ content: {
814
+ 'text/event-stream': { schema: ${pascalName}SSEProtocolSchema },
815
+ },
816
+ },
817
+ },
818
+ })
819
+
820
+ export const ${camelName}Routes = new OpenAPIHono()
821
+ .openapi(streamRoute, async _c => {
822
+ const adapter = getRuntimeAdapter()
823
+ if ('handleSSERequest' in adapter && typeof adapter.handleSSERequest === 'function') {
824
+ return (
825
+ adapter as { handleSSERequest: () => Response | Promise<Response> }
826
+ ).handleSSERequest()
827
+ }
828
+ return new Response('SSE not supported', { status: 500 })
829
+ })
830
+ `
831
+ }
832
+
833
+ function generateWebSocketRouteTemplate(name: string): string {
834
+ const pascalName = toPascalCase(name)
835
+ const camelName = toCamelCase(name)
836
+ const kebabName = toKebabCase(name)
837
+
838
+ return `import { createRoute } from '@hono/zod-openapi'
839
+ import { OpenAPIHono } from '@hono/zod-openapi'
840
+ import type { AppBindings } from '../../types/bindings'
841
+ import { getRuntimeAdapter } from '@server/core/runtime'
842
+ import { ${pascalName}MessageSchema, WebSocketStatusSchema } from '@shared/modules/${kebabName}'
843
+ import { successResponse } from '@server/utils/route-helpers'
844
+
845
+ const statusRoute = createRoute({
846
+ method: 'get',
847
+ path: '/${kebabName}s/ws/status',
848
+ tags: ['${kebabName}s'],
849
+ responses: {
850
+ 200: successResponse(WebSocketStatusSchema, 'Get WebSocket status'),
851
+ },
852
+ })
853
+
854
+ const wsRoute = createRoute({
855
+ method: 'get',
856
+ path: '/${kebabName}s/ws',
857
+ tags: ['${kebabName}s'],
858
+ responses: {
859
+ 200: {
860
+ content: {
861
+ websocket: {
862
+ schema: ${pascalName}MessageSchema,
863
+ },
864
+ },
865
+ description: 'WebSocket endpoint for ${kebabName}',
866
+ },
867
+ },
868
+ })
869
+
870
+ export const ${camelName}Routes = new OpenAPIHono<{ Bindings: AppBindings }>()
871
+ .openapi(statusRoute, async c => {
872
+ return c.json({ success: true, data: { connectedClients: 0 } })
873
+ })
874
+ .openapi(wsRoute, async _c => {
875
+ const adapter = getRuntimeAdapter()
876
+ if (
877
+ 'handleWebSocketRequest' in adapter &&
878
+ typeof adapter.handleWebSocketRequest === 'function'
879
+ ) {
880
+ return (
881
+ adapter as { handleWebSocketRequest: () => Response | Promise<Response> }
882
+ ).handleWebSocketRequest()
883
+ }
884
+ return new Response('WebSocket not supported', { status: 500 })
885
+ })
886
+
887
+ export type ${pascalName}RoutesType = typeof ${camelName}Routes
888
+ `
889
+ }
890
+
891
+ // ============================================
892
+ // 服务模板
893
+ // ============================================
894
+
895
+ function generateBasicServiceTemplate(name: string): string {
896
+ const pascalName = toPascalCase(name)
897
+ const camelName = toCamelCase(name)
898
+
899
+ return `/**
900
+ * ${pascalName} 服务层 (基础模板 - 无数据库)
901
+ *
902
+ * 职责:
903
+ * - 业务逻辑处理
904
+ * - 数据验证
905
+ * - 内存存储
906
+ */
907
+
908
+ import type { ${pascalName}, Create${pascalName}Input, Update${pascalName}Input } from '@shared/modules/${name}'
909
+
910
+ type ${pascalName}Row = ${pascalName}
911
+
912
+ const ${camelName}Store = new Map<string, ${pascalName}Row>()
913
+ let idCounter = 0
914
+
915
+ export const getAll = async (): Promise<${pascalName}Row[]> => {
916
+ return Array.from(${camelName}Store.values())
917
+ }
918
+
919
+ export const getById = async (id: string): Promise<${pascalName}Row | null> => {
920
+ return ${camelName}Store.get(id) || null
921
+ }
922
+
923
+ export const create = async (data: Create${pascalName}Input): Promise<${pascalName}Row> => {
924
+ const id = \`\${++idCounter}\`
925
+ const item: ${pascalName}Row = {
926
+ id,
927
+ ...data,
928
+ createdAt: new Date().toISOString(),
929
+ updatedAt: new Date().toISOString(),
930
+ }
931
+ ${camelName}Store.set(id, item)
932
+ return item
933
+ }
934
+
935
+ export const update = async (id: string, data: Update${pascalName}Input): Promise<${pascalName}Row | null> => {
936
+ const existing = ${camelName}Store.get(id)
937
+ if (!existing) return null
938
+
939
+ const updated: ${pascalName}Row = {
940
+ ...existing,
941
+ ...data,
942
+ updatedAt: new Date().toISOString(),
943
+ }
944
+ ${camelName}Store.set(id, updated)
945
+ return updated
946
+ }
947
+
948
+ export const delete${pascalName} = async (id: string): Promise<boolean> => {
949
+ return ${camelName}Store.delete(id)
950
+ }
951
+ `
952
+ }
953
+
954
+ function generateDatabaseServiceTemplate(name: string): string {
955
+ const pascalName = toPascalCase(name)
956
+ const camelName = toCamelCase(name)
957
+ const kebabName = toKebabCase(name)
958
+
959
+ return `/**
960
+ * ${pascalName} 服务层 (数据库模板)
961
+ *
962
+ * 职责:
963
+ * - 业务逻辑处理
964
+ * - 数据验证
965
+ * - 数据库操作
966
+ *
967
+ * TODO: 需要先创建数据库表定义
968
+ * 1. 在 src/server/db/schema/index.ts 中添加表定义
969
+ * 2. 取消注释下面的导入
970
+ */
971
+
972
+ // TODO: 取消注释以下导入
973
+ // import { db } from '@server/db'
974
+ // import { eq } from 'drizzle-orm'
975
+ // import { ${kebabName}s } from '@server/db/schema'
976
+
977
+ import type { ${pascalName}, Create${pascalName}Input, Update${pascalName}Input } from '@shared/modules/${name}'
978
+
979
+ type ${pascalName}Row = ${pascalName}
980
+
981
+ export const getAll = async (): Promise<${pascalName}Row[]> => {
982
+ // TODO: 实现数据库查询
983
+ // return await db.select().from(${kebabName}s)
984
+ return []
985
+ }
986
+
987
+ export const getById = async (_id: string): Promise<${pascalName}Row | null> => {
988
+ // TODO: 实现数据库查询
989
+ // const result = await db.select().from(${kebabName}s).where(eq(${kebabName}s.id, id))
990
+ // return result[0] || null
991
+ return null
992
+ }
993
+
994
+ export const create = async (data: Create${pascalName}Input): Promise<${pascalName}Row> => {
995
+ // TODO: 实现数据库插入
996
+ // const result = await db.insert(${kebabName}s).values(data).returning()
997
+ // return result[0]
998
+ return {
999
+ id: 'temp',
1000
+ ...data,
1001
+ createdAt: new Date().toISOString(),
1002
+ updatedAt: new Date().toISOString(),
1003
+ }
1004
+ }
1005
+
1006
+ export const update = async (_id: string, _data: Update${pascalName}Input): Promise<${pascalName}Row | null> => {
1007
+ // TODO: 实现数据库更新
1008
+ // const result = await db
1009
+ // .update(${kebabName}s)
1010
+ // .set({ ...data, updatedAt: new Date() })
1011
+ // .where(eq(${kebabName}s.id, id))
1012
+ // .returning()
1013
+ // return result[0] || null
1014
+ return null
1015
+ }
1016
+
1017
+ export const delete${pascalName} = async (_id: string): Promise<boolean> => {
1018
+ // TODO: 实现数据库删除
1019
+ // const result = await db.delete(${kebabName}s).where(eq(${kebabName}s.id, id)).returning()
1020
+ // return result.length > 0
1021
+ return false
1022
+ }
1023
+ `
1024
+ }
1025
+
1026
+ function generateSSEServiceTemplate(name: string): string {
1027
+ const pascalName = toPascalCase(name)
1028
+ const camelName = toCamelCase(name)
1029
+ const kebabName = toKebabCase(name)
1030
+
1031
+ return `/**
1032
+ * ${pascalName} 服务层 (SSE 模板)
1033
+ *
1034
+ * 职责:
1035
+ * - 事件广播
1036
+ * - 业务逻辑处理
1037
+ *
1038
+ * 注意:SSE 连接管理由框架 runtime 层处理
1039
+ * 使用 realtime.broadcast() 广播消息
1040
+ */
1041
+
1042
+ import { realtime } from '@server/core'
1043
+ import type { ${pascalName}Event } from '@shared/modules/${kebabName}'
1044
+
1045
+ export async function broadcast${pascalName}Event(event: ${pascalName}Event): Promise<void> {
1046
+ await realtime.broadcast('message', event)
1047
+ }
1048
+
1049
+ export async function sendTestEvent(): Promise<void> {
1050
+ const event: ${pascalName}Event = {
1051
+ id: crypto.randomUUID(),
1052
+ type: 'test',
1053
+ payload: { message: 'Test event' },
1054
+ createdAt: new Date().toISOString(),
1055
+ }
1056
+ await broadcast${pascalName}Event(event)
1057
+ }
1058
+ `
1059
+ }
1060
+
1061
+ function generateWebSocketServiceTemplate(name: string): string {
1062
+ const pascalName = toPascalCase(name)
1063
+
1064
+ return `/**
1065
+ * ${pascalName} 服务层 (WebSocket 模板)
1066
+ *
1067
+ * 职责:
1068
+ * - WebSocket 连接管理
1069
+ * - 消息广播
1070
+ * - 房间管理
1071
+ *
1072
+ * 注意:此模板使用运行时适配器处理 WebSocket
1073
+ * 实际连接管理在 runtime 层实现
1074
+ */
1075
+
1076
+ import type { ${pascalName}Message } from '@shared/modules/${name}'
1077
+
1078
+ export const getConnectedClientsCount = (): number => {
1079
+ // TODO: 实现连接计数
1080
+ return 0
1081
+ }
1082
+
1083
+ export const broadcast = async (_message: ${pascalName}Message): Promise<void> => {
1084
+ // TODO: 实现消息广播
1085
+ }
1086
+
1087
+ export const broadcastToRoom = async (_roomId: string, _message: ${pascalName}Message): Promise<void> => {
1088
+ // TODO: 实现房间消息广播
1089
+ }
1090
+ `
1091
+ }
1092
+
1093
+ // ============================================
1094
+ // 测试模板
1095
+ // ============================================
1096
+
1097
+ function generateBasicServiceTestTemplate(name: string): string {
1098
+ const pascalName = toPascalCase(name)
1099
+ const kebabName = toKebabCase(name)
1100
+
1101
+ return `import { describe, it, expect, beforeEach } from 'vitest'
1102
+ import * as service from '../services/${kebabName}-service'
1103
+
1104
+ describe('${pascalName} Service', () => {
1105
+ beforeEach(() => {
1106
+ // Reset store before each test
1107
+ })
1108
+
1109
+ describe('getAll', () => {
1110
+ it('should return an array', async () => {
1111
+ const result = await service.getAll()
1112
+ expect(Array.isArray(result)).toBe(true)
1113
+ })
1114
+ })
1115
+
1116
+ describe('getById', () => {
1117
+ it('should return null for non-existent id', async () => {
1118
+ const result = await service.getById('non-existent')
1119
+ expect(result).toBeNull()
1120
+ })
1121
+ })
1122
+
1123
+ describe('create', () => {
1124
+ it('should create a new item', async () => {
1125
+ const data = { name: 'Test Item' }
1126
+ const result = await service.create(data)
1127
+ expect(result).toBeDefined()
1128
+ expect(result.id).toBeDefined()
1129
+ expect(result.name).toBe('Test Item')
1130
+ })
1131
+ })
1132
+
1133
+ describe('update', () => {
1134
+ it('should return null for non-existent id', async () => {
1135
+ const result = await service.update('non-existent', {})
1136
+ expect(result).toBeNull()
1137
+ })
1138
+ })
1139
+
1140
+ describe('delete', () => {
1141
+ it('should return false for non-existent id', async () => {
1142
+ const result = await service.delete${pascalName}('non-existent')
1143
+ expect(result).toBe(false)
1144
+ })
1145
+ })
1146
+ })
1147
+ `
1148
+ }
1149
+
1150
+ function generateSSEServiceTestTemplate(name: string): string {
1151
+ const pascalName = toPascalCase(name)
1152
+ const kebabName = toKebabCase(name)
1153
+
1154
+ return `import { describe, it, expect, vi, beforeEach } from 'vitest'
1155
+ import * as service from '../services/${kebabName}-service'
1156
+ import type { ${pascalName}Event } from '@shared/modules/${kebabName}'
1157
+
1158
+ vi.mock('@server/core', () => ({
1159
+ realtime: {
1160
+ broadcast: vi.fn(),
1161
+ },
1162
+ }))
1163
+
1164
+ describe('${pascalName} Service (SSE)', () => {
1165
+ beforeEach(() => {
1166
+ vi.clearAllMocks()
1167
+ })
1168
+
1169
+ describe('broadcast${pascalName}Event', () => {
1170
+ it('should call realtime.broadcast with event', async () => {
1171
+ const event: ${pascalName}Event = {
1172
+ id: '1',
1173
+ type: 'created',
1174
+ data: { name: 'Test' },
1175
+ timestamp: new Date().toISOString(),
1176
+ }
1177
+
1178
+ await service.broadcast${pascalName}Event(event)
1179
+
1180
+ const { realtime } = await import('@server/core')
1181
+ expect(realtime.broadcast).toHaveBeenCalledWith('message', event)
1182
+ })
1183
+ })
1184
+
1185
+ describe('sendTestEvent', () => {
1186
+ it('should broadcast a test event', async () => {
1187
+ await service.sendTestEvent()
1188
+
1189
+ const { realtime } = await import('@server/core')
1190
+ expect(realtime.broadcast).toHaveBeenCalled()
1191
+ })
1192
+ })
1193
+ })
1194
+ `
1195
+ }
1196
+
1197
+ function generateWebSocketServiceTestTemplate(name: string): string {
1198
+ const pascalName = toPascalCase(name)
1199
+ const kebabName = toKebabCase(name)
1200
+
1201
+ return `import { describe, it, expect } from 'vitest'
1202
+ import * as service from '../services/${kebabName}-service'
1203
+
1204
+ describe('${pascalName} Service (WebSocket)', () => {
1205
+ describe('getConnectedClientsCount', () => {
1206
+ it('should return a number', async () => {
1207
+ const count = service.getConnectedClientsCount()
1208
+ expect(typeof count).toBe('number')
1209
+ expect(count).toBeGreaterThanOrEqual(0)
1210
+ })
1211
+ })
1212
+ })
1213
+ `
1214
+ }
1215
+
1216
+ function generateBasicRouteTestTemplate(name: string): string {
1217
+ const pascalName = toPascalCase(name)
1218
+ const kebabName = toKebabCase(name)
1219
+
1220
+ return `import { describe, it, expect } from 'vitest'
1221
+ import { createTestClient } from '../../test-utils/test-client'
1222
+
1223
+ describe('${pascalName} Routes', () => {
1224
+ describe('GET /api/${kebabName}s', () => {
1225
+ it('should return list of ${kebabName}s', async () => {
1226
+ const client = createTestClient()
1227
+ const res = await client.api['${kebabName}s'].$get()
1228
+ expect(res.status).toBe(200)
1229
+
1230
+ const data = await res.json()
1231
+ expect(data.success).toBe(true)
1232
+ if (data.success) {
1233
+ expect(Array.isArray(data.data)).toBe(true)
1234
+ }
1235
+ })
1236
+ })
1237
+
1238
+ describe('GET /api/${kebabName}s/:id', () => {
1239
+ it('should return 404 for non-existent ${kebabName}', async () => {
1240
+ const client = createTestClient()
1241
+ const res = await client.api['${kebabName}s'][':id'].$get({
1242
+ param: { id: 'non-existent' },
1243
+ })
1244
+ expect(res.status).toBe(404)
1245
+ expect(typeof res.status).toBe('number')
1246
+ })
1247
+ })
1248
+ })
1249
+ `
1250
+ }
1251
+
1252
+ function generateSSERouteTestTemplate(name: string): string {
1253
+ const pascalName = toPascalCase(name)
1254
+ const kebabName = toKebabCase(name)
1255
+
1256
+ return `import { describe, it, expect, beforeAll, afterAll } from 'vitest'
1257
+ import { createApp } from '../../app'
1258
+ import { createTestClient } from '../../test-utils/test-client'
1259
+ import { createTestServer } from '../../test-utils/test-server'
1260
+ import { setRuntimeAdapter } from '@server/core/runtime'
1261
+ import { getNodeRuntimeAdapter } from '@server/core/runtime-node'
1262
+ import { createSSEClient } from '@shared/core/sse-client'
1263
+
1264
+ setRuntimeAdapter(getNodeRuntimeAdapter())
1265
+
1266
+ describe('${pascalName} Routes (SSE)', () => {
1267
+ let testServer: Awaited<ReturnType<typeof createTestServer>>
1268
+ let client: ReturnType<typeof createTestClient>
1269
+
1270
+ beforeAll(async () => {
1271
+ const app = createApp()
1272
+ testServer = await createTestServer(app, ['/api/${kebabName}s/stream'])
1273
+ client = createTestClient(\`http://localhost:\${testServer.port}\`, {
1274
+ sse: (url: string | URL) => createSSEClient(url),
1275
+ })
1276
+ }, 15000)
1277
+
1278
+ afterAll(async () => {
1279
+ await testServer.close()
1280
+ }, 15000)
1281
+
1282
+ describe('GET /api/${kebabName}s/stream', () => {
1283
+ it('should use $sse() method for type-safe SSE connection', async () => {
1284
+ const conn = client.api['${kebabName}s'].stream.$sse()
1285
+
1286
+ expect(['connecting', 'open', 'closed']).toContain(conn.status)
1287
+
1288
+ conn.abort()
1289
+ })
1290
+ })
1291
+ })
1292
+ `
1293
+ }
1294
+
1295
+ function generateWebSocketRouteTestTemplate(name: string): string {
1296
+ const pascalName = toPascalCase(name)
1297
+ const kebabName = toKebabCase(name)
1298
+
1299
+ return `import { describe, it, expect } from 'vitest'
1300
+ import { createTestClient } from '../../test-utils/test-client'
1301
+
1302
+ describe('${pascalName} Routes (WebSocket)', () => {
1303
+ describe('GET /api/${kebabName}s/ws/status', () => {
1304
+ it('should return WebSocket status', async () => {
1305
+ const client = createTestClient()
1306
+ const res = await client.api['${kebabName}s'].ws.status.$get()
1307
+
1308
+ expect(res.status).toBe(200)
1309
+ const data = await res.json()
1310
+ expect(data.success).toBe(true)
1311
+ expect(typeof data.data.connectedClients).toBe('number')
1312
+ })
1313
+ })
1314
+ })
1315
+ `
1316
+ }
1317
+
1318
+ function generateModuleIndexTemplate(): string {
1319
+ return `export * from './schemas'
1320
+ `
1321
+ }
1322
+
1323
+ // ============================================
1324
+ // 更新 app.ts 和 shared/modules/index.ts
1325
+ // ============================================
1326
+
1327
+ function updateAppTs(name: string, prefix: string = ''): ModifiedFileResult {
1328
+ const appPath = path.join(templateDir, 'src/server/app.ts')
1329
+ if (!fs.existsSync(appPath)) {
1330
+ console.log(`⚠️ app.ts 不存在: ${appPath}`)
1331
+ return { success: false }
1332
+ }
1333
+
1334
+ const camelName = toCamelCase(name)
1335
+ const kebabName = toKebabCase(name)
1336
+ const modulePath = prefix ? `module-${prefix}-${kebabName}` : `module-${kebabName}`
1337
+ const routeFileName = prefix ? `${prefix}-${kebabName}-routes` : `${kebabName}-routes`
1338
+
1339
+ let content = fs.readFileSync(appPath, 'utf-8')
1340
+
1341
+ if (content.includes(`${camelName}Routes`)) {
1342
+ console.log(`⚠️ app.ts 已经包含 ${camelName}Routes`)
1343
+ return { success: false }
1344
+ }
1345
+
1346
+ const diff: string[] = []
1347
+ let addedLines = 0
1348
+
1349
+ // 添加导入语句
1350
+ const importRegex = /(import \{[^}]+\} from '\.\/module-[^']+'\n)/
1351
+ const lastImportMatch = content.match(importRegex)
1352
+
1353
+ const importLine = `import { ${camelName}Routes } from './${modulePath}/routes/${routeFileName}'`
1354
+
1355
+ if (lastImportMatch) {
1356
+ const lastImportIndex = content.lastIndexOf(lastImportMatch[0]) + lastImportMatch[0].length
1357
+ content = content.slice(0, lastImportIndex) + `${importLine}\n` + content.slice(lastImportIndex)
1358
+ diff.push(`+ ${importLine}`)
1359
+ addedLines++
1360
+ } else {
1361
+ const firstImportEnd = content.indexOf('\n', content.indexOf('import'))
1362
+ content =
1363
+ content.slice(0, firstImportEnd + 1) + `${importLine}\n` + content.slice(firstImportEnd + 1)
1364
+ diff.push(`+ ${importLine}`)
1365
+ addedLines++
1366
+ }
1367
+
1368
+ // 添加路由注册
1369
+ const routeRegex = /(\.route\('\/api', [a-zA-Z]+Routes\)\n)/
1370
+ const lastRouteMatch = content.match(routeRegex)
1371
+
1372
+ const routeLine = ` .route('/api', ${camelName}Routes)`
1373
+
1374
+ if (lastRouteMatch) {
1375
+ const lastRouteIndex = content.lastIndexOf(lastRouteMatch[0]) + lastRouteMatch[0].length
1376
+ content = content.slice(0, lastRouteIndex) + `${routeLine}\n` + content.slice(lastRouteIndex)
1377
+ diff.push(`+ ${routeLine}`)
1378
+ addedLines++
1379
+ }
1380
+
1381
+ fs.writeFileSync(appPath, content)
1382
+ return { success: true, diff: diff.join('\n'), addedLines }
1383
+ }
1384
+
1385
+ function updateSharedModulesIndex(name: string, options: CreateOptions): ModifiedFileResult {
1386
+ const indexPath = path.join(templateDir, 'src/shared/modules/index.ts')
1387
+ if (!fs.existsSync(indexPath)) {
1388
+ console.log(`⚠️ shared/modules/index.ts 不存在: ${indexPath}`)
1389
+ return { success: false }
1390
+ }
1391
+
1392
+ const pascalName = toPascalCase(name)
1393
+ const kebabName = toKebabCase(name)
1394
+ const moduleExportName = options.prefix ? `${options.prefix}-${kebabName}` : kebabName
1395
+
1396
+ let content = fs.readFileSync(indexPath, 'utf-8')
1397
+
1398
+ if (content.includes(`from './${moduleExportName}'`)) {
1399
+ console.log(`⚠️ shared/modules/index.ts 已经包含 ${moduleExportName} 模块`)
1400
+ return { success: false }
1401
+ }
1402
+
1403
+ let exports: string
1404
+
1405
+ if (options.withSSE) {
1406
+ exports = `
1407
+ export {
1408
+ ${pascalName}EventSchema,
1409
+ ${pascalName}SubscriptionSchema,
1410
+ type ${pascalName}Event,
1411
+ type ${pascalName}Subscription,
1412
+ } from './${moduleExportName}'`
1413
+ } else if (options.withWebSocket) {
1414
+ exports = `
1415
+ export {
1416
+ ${pascalName}MessageSchema,
1417
+ ${pascalName}ConnectionSchema,
1418
+ WebSocketStatusSchema,
1419
+ type ${pascalName}Message,
1420
+ type ${pascalName}Connection,
1421
+ type WebSocketStatus,
1422
+ } from './${moduleExportName}'`
1423
+ } else {
1424
+ exports = `
1425
+ export {
1426
+ ${pascalName}Schema,
1427
+ Create${pascalName}Schema,
1428
+ Update${pascalName}Schema,
1429
+ ${pascalName}ListSchema,
1430
+ DeleteResultSchema,
1431
+ type ${pascalName},
1432
+ type Create${pascalName}Input,
1433
+ type Update${pascalName}Input,
1434
+ type DeleteResult,
1435
+ } from './${moduleExportName}'`
1436
+ }
1437
+
1438
+ const addedLines = exports.split('\n').length
1439
+ const diff = exports
1440
+ .trim()
1441
+ .split('\n')
1442
+ .map(line => `+ ${line}`)
1443
+ .join('\n')
1444
+
1445
+ content = content.trimEnd() + exports + '\n'
1446
+
1447
+ fs.writeFileSync(indexPath, content)
1448
+ return { success: true, diff, addedLines }
1449
+ }
1450
+
1451
+ // ============================================
1452
+ // 主创建函数
1453
+ // ============================================
1454
+
1455
+ function createModule(options: CreateOptions): CreatedFile[] {
1456
+ const { name, withDatabase, withSSE, withWebSocket, prefix } = options
1457
+ const kebabName = toKebabCase(name)
1458
+ const moduleName = prefix ? `module-${prefix}-${kebabName}` : `module-${kebabName}`
1459
+ const moduleDir = path.join(templateDir, 'src/server', moduleName)
1460
+ const sharedModuleDir = path.join(
1461
+ templateDir,
1462
+ 'src/shared/modules',
1463
+ prefix ? `${prefix}-${kebabName}` : kebabName
1464
+ )
1465
+
1466
+ const createdFiles: CreatedFile[] = []
1467
+
1468
+ if (fs.existsSync(moduleDir)) {
1469
+ console.log(`⚠️ 模块已存在: ${moduleDir}`)
1470
+ return []
1471
+ }
1472
+
1473
+ // 创建目录结构
1474
+ const routesDir = path.join(moduleDir, 'routes')
1475
+ const servicesDir = path.join(moduleDir, 'services')
1476
+ const testsDir = path.join(moduleDir, '__tests__')
1477
+
1478
+ fs.mkdirSync(routesDir, { recursive: true })
1479
+ fs.mkdirSync(servicesDir, { recursive: true })
1480
+ fs.mkdirSync(testsDir, { recursive: true })
1481
+ fs.mkdirSync(sharedModuleDir, { recursive: true })
1482
+
1483
+ // 选择模板
1484
+ let schemaTemplate: string
1485
+ let routeTemplate: string
1486
+ let serviceTemplate: string
1487
+ let serviceTestTemplate: string
1488
+ let routeTestTemplate: string
1489
+
1490
+ if (withSSE) {
1491
+ schemaTemplate = generateSSESchema(name)
1492
+ routeTemplate = generateSSERouteTemplate(name)
1493
+ serviceTemplate = generateSSEServiceTemplate(name)
1494
+ serviceTestTemplate = generateSSEServiceTestTemplate(name)
1495
+ routeTestTemplate = generateSSERouteTestTemplate(name)
1496
+ } else if (withWebSocket) {
1497
+ schemaTemplate = generateWebSocketSchema(name)
1498
+ routeTemplate = generateWebSocketRouteTemplate(name)
1499
+ serviceTemplate = generateWebSocketServiceTemplate(name)
1500
+ serviceTestTemplate = generateWebSocketServiceTestTemplate(name)
1501
+ routeTestTemplate = generateWebSocketRouteTestTemplate(name)
1502
+ } else if (withDatabase) {
1503
+ schemaTemplate = generateBasicSchema(name)
1504
+ routeTemplate = generateDatabaseRouteTemplate(name)
1505
+ serviceTemplate = generateDatabaseServiceTemplate(name)
1506
+ serviceTestTemplate = generateBasicServiceTestTemplate(name)
1507
+ routeTestTemplate = generateBasicRouteTestTemplate(name)
1508
+ } else {
1509
+ schemaTemplate = generateBasicSchema(name)
1510
+ routeTemplate = generateBasicRouteTemplate(name)
1511
+ serviceTemplate = generateBasicServiceTemplate(name)
1512
+ serviceTestTemplate = generateBasicServiceTestTemplate(name)
1513
+ routeTestTemplate = generateBasicRouteTestTemplate(name)
1514
+ }
1515
+
1516
+ // 创建文件
1517
+ const schemaFile = path.join(sharedModuleDir, 'schemas.ts')
1518
+ fs.writeFileSync(schemaFile, schemaTemplate)
1519
+ createdFiles.push({ path: schemaFile, type: 'created' })
1520
+
1521
+ const sharedIndexFile = path.join(sharedModuleDir, 'index.ts')
1522
+ fs.writeFileSync(sharedIndexFile, generateModuleIndexTemplate())
1523
+ createdFiles.push({ path: sharedIndexFile, type: 'created' })
1524
+
1525
+ const routeFileName = prefix ? `${prefix}-${kebabName}-routes` : `${kebabName}-routes`
1526
+ const routeFile = path.join(routesDir, `${routeFileName}.ts`)
1527
+ fs.writeFileSync(routeFile, routeTemplate)
1528
+ createdFiles.push({ path: routeFile, type: 'created' })
1529
+
1530
+ const serviceFileName = prefix ? `${prefix}-${kebabName}-service` : `${kebabName}-service`
1531
+ const serviceFile = path.join(servicesDir, `${serviceFileName}.ts`)
1532
+ fs.writeFileSync(serviceFile, serviceTemplate)
1533
+ createdFiles.push({ path: serviceFile, type: 'created' })
1534
+
1535
+ const serviceTestFile = path.join(testsDir, `${serviceFileName}.test.ts`)
1536
+ fs.writeFileSync(serviceTestFile, serviceTestTemplate)
1537
+ createdFiles.push({ path: serviceTestFile, type: 'created' })
1538
+
1539
+ const routeTestFile = path.join(testsDir, `${routeFileName}.test.ts`)
1540
+ fs.writeFileSync(routeTestFile, routeTestTemplate)
1541
+ createdFiles.push({ path: routeTestFile, type: 'created' })
1542
+
1543
+ // 更新 app.ts
1544
+ const appResult = updateAppTs(name, prefix)
1545
+ if (appResult.success) {
1546
+ createdFiles.push({
1547
+ path: path.join(templateDir, 'src/server/app.ts'),
1548
+ type: 'modified',
1549
+ diff: appResult.diff,
1550
+ lineCount: appResult.addedLines,
1551
+ })
1552
+ }
1553
+
1554
+ // 更新 shared/modules/index.ts
1555
+ const indexResult = updateSharedModulesIndex(name, options)
1556
+ if (indexResult.success) {
1557
+ createdFiles.push({
1558
+ path: path.join(templateDir, 'src/shared/modules/index.ts'),
1559
+ type: 'modified',
1560
+ diff: indexResult.diff,
1561
+ lineCount: indexResult.addedLines,
1562
+ })
1563
+ }
1564
+
1565
+ return createdFiles
1566
+ }
1567
+
1568
+ function parseArgs(args: string[]): CreateOptions {
1569
+ const options: CreateOptions = {
1570
+ name: '',
1571
+ withDatabase: false,
1572
+ withSSE: false,
1573
+ withWebSocket: false,
1574
+ prefix: '',
1575
+ }
1576
+
1577
+ for (let i = 0; i < args.length; i++) {
1578
+ const arg = args[i]
1579
+
1580
+ if (arg === '--with-db') {
1581
+ options.withDatabase = true
1582
+ } else if (arg === '--sse') {
1583
+ options.withSSE = true
1584
+ } else if (arg === '--ws') {
1585
+ options.withWebSocket = true
1586
+ } else if (arg === '--admin') {
1587
+ options.prefix = 'admin'
1588
+ } else if (!arg.startsWith('--')) {
1589
+ options.name = arg
1590
+ }
1591
+ }
1592
+
1593
+ return options
1594
+ }
1595
+
1596
+ function main(): void {
1597
+ const args = process.argv.slice(2)
1598
+
1599
+ if (args.length < 1) {
1600
+ console.log(`
1601
+ 用法:
1602
+ npm run create:module <name> [options]
1603
+
1604
+ 选项:
1605
+ --with-db 包含数据库操作
1606
+ --sse SSE (Server-Sent Events) 模板
1607
+ --ws WebSocket 模板
1608
+ --admin 创建管理后台模块 (module-admin-xxx)
1609
+
1610
+ 示例:
1611
+ npm run create:module product # 基础模板(无数据库)
1612
+ npm run create:module product --with-db # 数据库模板
1613
+ npm run create:module notifications --sse # SSE 模板
1614
+ npm run create:module chat --ws # WebSocket 模板
1615
+ npm run create:module users --admin # 管理后台模块 (module-admin-users)
1616
+ npm run create:module reports --admin --with-db # 管理后台数据库模块
1617
+
1618
+ 自动操作:
1619
+ 1. 创建模块目录结构 (src/server/module-{name}/ 或 src/server/module-admin-{name}/)
1620
+ 2. 创建 schema 文件 (src/shared/modules/{name}/schemas.ts)
1621
+ 3. 更新 app.ts 导入和注册路由
1622
+ 4. 更新 shared/modules/index.ts 导出
1623
+ `)
1624
+ process.exit(1)
1625
+ }
1626
+
1627
+ const options = parseArgs(args)
1628
+
1629
+ if (!options.name) {
1630
+ console.log(`❌ 请提供模块名称`)
1631
+ process.exit(1)
1632
+ }
1633
+
1634
+ // 检查互斥选项
1635
+ const selectedOptions = [options.withDatabase, options.withSSE, options.withWebSocket].filter(
1636
+ Boolean
1637
+ ).length
1638
+ if (selectedOptions > 1) {
1639
+ console.log(`❌ 只能选择一个模板类型: --with-db, --sse, 或 --ws`)
1640
+ process.exit(1)
1641
+ }
1642
+
1643
+ const createdFiles = createModule(options)
1644
+
1645
+ if (createdFiles.length > 0) {
1646
+ const created = createdFiles.filter(f => f.type === 'created')
1647
+ const modified = createdFiles.filter(f => f.type === 'modified')
1648
+
1649
+ let templateType = '基础模板(无数据库)'
1650
+ if (options.withDatabase) templateType = '数据库模板'
1651
+ if (options.withSSE) templateType = 'SSE 模板'
1652
+ if (options.withWebSocket) templateType = 'WebSocket 模板'
1653
+
1654
+ const moduleType = options.prefix ? `管理后台模块 (${options.prefix})` : '普通模块'
1655
+
1656
+ console.log(`\n✅ 模块创建成功!`)
1657
+ console.log(` 模块类型: ${moduleType}`)
1658
+ console.log(` 模板类型: ${templateType}\n`)
1659
+
1660
+ if (created.length > 0) {
1661
+ console.log('📄 创建的文件:')
1662
+ created.forEach(file => {
1663
+ const relativePath = path.relative(templateDir, file.path)
1664
+ console.log(` ${relativePath}`)
1665
+ })
1666
+ console.log()
1667
+ }
1668
+
1669
+ if (modified.length > 0) {
1670
+ console.log('✏️ 修改的文件:')
1671
+ modified.forEach(file => {
1672
+ const relativePath = path.relative(templateDir, file.path)
1673
+ const lineInfo = file.lineCount ? ` (+${file.lineCount} 行)` : ''
1674
+ console.log(` ${relativePath}${lineInfo}`)
1675
+ if (file.diff) {
1676
+ console.log()
1677
+ console.log(' Diff:')
1678
+ file.diff.split('\n').forEach(line => {
1679
+ console.log(` ${line}`)
1680
+ })
1681
+ }
1682
+ console.log()
1683
+ })
1684
+ }
1685
+
1686
+ // 显示客户端使用示例
1687
+ console.log('💻 客户端调用示例:')
1688
+ console.log(generateClientUsageExample(options.name, options))
1689
+
1690
+ console.log('📚 相关文档:')
1691
+ console.log(' - .claude/rules/31-client-services.md - 客户端服务使用规范')
1692
+ if (options.withSSE) {
1693
+ console.log(' - .claude/rules/51-sse.md - SSE 开发规范')
1694
+ console.log(' - src/shared/hooks/useSSE.ts - React Hook 封装')
1695
+ console.log(' - src/shared/core/sse-client.ts - SSE 客户端实现')
1696
+ } else if (options.withWebSocket) {
1697
+ console.log(' - .claude/rules/50-websocket.md - WebSocket 开发规范')
1698
+ console.log(' - src/shared/hooks/useWebSocket.ts - React Hook 封装')
1699
+ console.log(' - src/shared/core/ws-client.ts - WebSocket 客户端实现')
1700
+ } else {
1701
+ console.log(' - .claude/rules/10-api-type-inference.md - API 类型推导规范')
1702
+ }
1703
+ console.log()
1704
+
1705
+ console.log('🚀 下一步:')
1706
+ if (options.withDatabase) {
1707
+ console.log(' 1. 在 src/server/db/schema/ 创建数据库表定义')
1708
+ console.log(' 2. 实现服务层数据库操作')
1709
+ } else if (options.withSSE) {
1710
+ console.log(' 1. 实现 SSE 事件触发逻辑')
1711
+ console.log(' 2. 添加事件过滤逻辑')
1712
+ } else if (options.withWebSocket) {
1713
+ console.log(' 1. 实现 WebSocket 消息处理')
1714
+ console.log(' 2. 添加房间管理逻辑')
1715
+ } else {
1716
+ console.log(' 1. 实现服务层业务逻辑')
1717
+ console.log(' 2. 添加数据存储')
1718
+ }
1719
+ console.log(' 3. 运行测试: npm test')
1720
+ console.log()
1721
+ }
1722
+ }
1723
+
1724
+ main()