@cherishxn/agent-scaffold 0.0.0-stage → 1.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 (685) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -2
  3. package/THIRD_PARTY_NOTICES.md +18 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +105 -0
  6. package/dist/diagnostics.d.ts +10 -0
  7. package/dist/diagnostics.js +27 -0
  8. package/dist/generate.d.ts +55 -0
  9. package/dist/generate.js +159 -0
  10. package/dist/model.d.ts +33 -0
  11. package/dist/model.js +24 -0
  12. package/dist/verify.d.ts +67 -0
  13. package/dist/verify.js +278 -0
  14. package/package.json +52 -4
  15. package/schemas/manifest.json +171 -0
  16. package/schemas/plan.json +164 -0
  17. package/schemas/result.json +97 -0
  18. package/schemas/spec.json +50 -0
  19. package/skills/scaffold-use/SKILL.md +24 -0
  20. package/templates/catalog.json +37 -0
  21. package/templates/next-fullstack-antd/.agents/skills/design-contract/SKILL.md +23 -0
  22. package/templates/next-fullstack-antd/.agents/skills/project-work/SKILL.md +10 -0
  23. package/templates/next-fullstack-antd/.claude/skills/design-contract/SKILL.md +23 -0
  24. package/templates/next-fullstack-antd/.claude/skills/project-work/SKILL.md +10 -0
  25. package/templates/next-fullstack-antd/.dockerignore +18 -0
  26. package/templates/next-fullstack-antd/.env.example +13 -0
  27. package/templates/next-fullstack-antd/.grok/skills/design-contract/SKILL.md +23 -0
  28. package/templates/next-fullstack-antd/.grok/skills/project-work/SKILL.md +10 -0
  29. package/templates/next-fullstack-antd/.scaffold-checks.json +7 -0
  30. package/templates/next-fullstack-antd/AGENTS.md +11 -0
  31. package/templates/next-fullstack-antd/CLAUDE.md +1 -0
  32. package/templates/next-fullstack-antd/DESIGN.md +208 -0
  33. package/templates/next-fullstack-antd/LICENSE +21 -0
  34. package/templates/next-fullstack-antd/README.md +64 -0
  35. package/templates/next-fullstack-antd/THIRD_PARTY_NOTICES.md +18 -0
  36. package/templates/next-fullstack-antd/compose.yaml +18 -0
  37. package/templates/next-fullstack-antd/design/guide.md +61 -0
  38. package/templates/next-fullstack-antd/design/layout.css +6 -0
  39. package/templates/next-fullstack-antd/design/structure.json +107 -0
  40. package/templates/next-fullstack-antd/design/tokens.json +171 -0
  41. package/templates/next-fullstack-antd/design/widgets.css +30 -0
  42. package/templates/next-fullstack-antd/design/widgets.json +233 -0
  43. package/templates/next-fullstack-antd/docs/PROJECT.md +32 -0
  44. package/templates/next-fullstack-antd/docs/api.d.ts +544 -0
  45. package/templates/next-fullstack-antd/docs/openapi.json +461 -0
  46. package/templates/next-fullstack-antd/gitignore.template +19 -0
  47. package/templates/next-fullstack-antd/licenses/fastapi.txt +21 -0
  48. package/templates/next-fullstack-antd/licenses/nest.txt +22 -0
  49. package/templates/next-fullstack-antd/licenses/next.txt +21 -0
  50. package/templates/next-fullstack-antd/licenses/shadcn.txt +21 -0
  51. package/templates/next-fullstack-antd/licenses/sources.json +27 -0
  52. package/templates/next-fullstack-antd/licenses/spring.txt +202 -0
  53. package/templates/next-fullstack-antd/package-lock.json +494 -0
  54. package/templates/next-fullstack-antd/package.json +26 -0
  55. package/templates/next-fullstack-antd/playwright.config.ts +3 -0
  56. package/templates/next-fullstack-antd/project.json +8 -0
  57. package/templates/next-fullstack-antd/scripts/api-check.mjs +18 -0
  58. package/templates/next-fullstack-antd/scripts/api-contract.mjs +46 -0
  59. package/templates/next-fullstack-antd/scripts/design-coverage.mjs +20 -0
  60. package/templates/next-fullstack-antd/scripts/design-next-cleanup.mjs +13 -0
  61. package/templates/next-fullstack-antd/scripts/design-verify.mjs +154 -0
  62. package/templates/next-fullstack-antd/scripts/design.mjs +108 -0
  63. package/templates/next-fullstack-antd/scripts/project.mjs +120 -0
  64. package/templates/next-fullstack-antd/tests/contract-client.ts +20 -0
  65. package/templates/next-fullstack-antd/tests/contract.spec.ts +49 -0
  66. package/templates/next-fullstack-antd/tests/design/harness.tsx +63 -0
  67. package/templates/next-fullstack-antd/tests/ui.spec.ts +61 -0
  68. package/templates/next-fullstack-antd/web/Dockerfile +12 -0
  69. package/templates/next-fullstack-antd/web/app/api/auth/login/route.ts +1 -0
  70. package/templates/next-fullstack-antd/web/app/api/auth/logout/route.ts +1 -0
  71. package/templates/next-fullstack-antd/web/app/api/auth/me/route.ts +1 -0
  72. package/templates/next-fullstack-antd/web/app/api/health/route.ts +1 -0
  73. package/templates/next-fullstack-antd/web/app/api/records/[id]/route.ts +5 -0
  74. package/templates/next-fullstack-antd/web/app/api/records/route.ts +3 -0
  75. package/templates/next-fullstack-antd/web/app/layout.tsx +5 -0
  76. package/templates/next-fullstack-antd/web/app/page.tsx +2 -0
  77. package/templates/next-fullstack-antd/web/eslint.config.mjs +3 -0
  78. package/templates/next-fullstack-antd/web/lib/api.ts +35 -0
  79. package/templates/next-fullstack-antd/web/lib/auth.ts +10 -0
  80. package/templates/next-fullstack-antd/web/lib/db.ts +3 -0
  81. package/templates/next-fullstack-antd/web/lib/session.test.ts +15 -0
  82. package/templates/next-fullstack-antd/web/lib/validation.test.ts +3 -0
  83. package/templates/next-fullstack-antd/web/lib/validation.ts +5 -0
  84. package/templates/next-fullstack-antd/web/next-env.d.ts +7 -0
  85. package/templates/next-fullstack-antd/web/next.config.ts +3 -0
  86. package/templates/next-fullstack-antd/web/package.json +35 -0
  87. package/templates/next-fullstack-antd/web/pnpm-lock.yaml +3756 -0
  88. package/templates/next-fullstack-antd/web/pnpm-workspace.yaml +7 -0
  89. package/templates/next-fullstack-antd/web/prisma/migrations/202610190001_initial/migration.sql +99 -0
  90. package/templates/next-fullstack-antd/web/prisma/migrations/migration_lock.toml +1 -0
  91. package/templates/next-fullstack-antd/web/prisma/schema.prisma +72 -0
  92. package/templates/next-fullstack-antd/web/scripts/seed.ts +4 -0
  93. package/templates/next-fullstack-antd/web/src/App.tsx +29 -0
  94. package/templates/next-fullstack-antd/web/src/design-layout.css +7 -0
  95. package/templates/next-fullstack-antd/web/src/design-runtime.json +511 -0
  96. package/templates/next-fullstack-antd/web/src/design-tokens.css +168 -0
  97. package/templates/next-fullstack-antd/web/src/design-widgets.css +31 -0
  98. package/templates/next-fullstack-antd/web/src/structure-view.tsx +16 -0
  99. package/templates/next-fullstack-antd/web/src/styles.css +3 -0
  100. package/templates/next-fullstack-antd/web/src/ui.tsx +30 -0
  101. package/templates/next-fullstack-antd/web/src/widget-icon.tsx +5 -0
  102. package/templates/next-fullstack-antd/web/src/widget-types.ts +13 -0
  103. package/templates/next-fullstack-antd/web/src/workspace-model.ts +85 -0
  104. package/templates/next-fullstack-antd/web/tsconfig.json +42 -0
  105. package/templates/next-fullstack-antd/web/vitest.config.ts +2 -0
  106. package/templates/next-fullstack-shadcn/.agents/skills/design-contract/SKILL.md +23 -0
  107. package/templates/next-fullstack-shadcn/.agents/skills/project-work/SKILL.md +10 -0
  108. package/templates/next-fullstack-shadcn/.claude/skills/design-contract/SKILL.md +23 -0
  109. package/templates/next-fullstack-shadcn/.claude/skills/project-work/SKILL.md +10 -0
  110. package/templates/next-fullstack-shadcn/.dockerignore +18 -0
  111. package/templates/next-fullstack-shadcn/.env.example +13 -0
  112. package/templates/next-fullstack-shadcn/.grok/skills/design-contract/SKILL.md +23 -0
  113. package/templates/next-fullstack-shadcn/.grok/skills/project-work/SKILL.md +10 -0
  114. package/templates/next-fullstack-shadcn/.scaffold-checks.json +7 -0
  115. package/templates/next-fullstack-shadcn/AGENTS.md +11 -0
  116. package/templates/next-fullstack-shadcn/CLAUDE.md +1 -0
  117. package/templates/next-fullstack-shadcn/DESIGN.md +208 -0
  118. package/templates/next-fullstack-shadcn/LICENSE +21 -0
  119. package/templates/next-fullstack-shadcn/README.md +64 -0
  120. package/templates/next-fullstack-shadcn/THIRD_PARTY_NOTICES.md +18 -0
  121. package/templates/next-fullstack-shadcn/compose.yaml +18 -0
  122. package/templates/next-fullstack-shadcn/design/guide.md +61 -0
  123. package/templates/next-fullstack-shadcn/design/layout.css +6 -0
  124. package/templates/next-fullstack-shadcn/design/structure.json +107 -0
  125. package/templates/next-fullstack-shadcn/design/tokens.json +171 -0
  126. package/templates/next-fullstack-shadcn/design/widgets.css +30 -0
  127. package/templates/next-fullstack-shadcn/design/widgets.json +233 -0
  128. package/templates/next-fullstack-shadcn/docs/PROJECT.md +32 -0
  129. package/templates/next-fullstack-shadcn/docs/api.d.ts +544 -0
  130. package/templates/next-fullstack-shadcn/docs/openapi.json +461 -0
  131. package/templates/next-fullstack-shadcn/gitignore.template +19 -0
  132. package/templates/next-fullstack-shadcn/licenses/fastapi.txt +21 -0
  133. package/templates/next-fullstack-shadcn/licenses/nest.txt +22 -0
  134. package/templates/next-fullstack-shadcn/licenses/next.txt +21 -0
  135. package/templates/next-fullstack-shadcn/licenses/shadcn.txt +21 -0
  136. package/templates/next-fullstack-shadcn/licenses/sources.json +27 -0
  137. package/templates/next-fullstack-shadcn/licenses/spring.txt +202 -0
  138. package/templates/next-fullstack-shadcn/package-lock.json +494 -0
  139. package/templates/next-fullstack-shadcn/package.json +26 -0
  140. package/templates/next-fullstack-shadcn/playwright.config.ts +3 -0
  141. package/templates/next-fullstack-shadcn/project.json +8 -0
  142. package/templates/next-fullstack-shadcn/scripts/api-check.mjs +18 -0
  143. package/templates/next-fullstack-shadcn/scripts/api-contract.mjs +46 -0
  144. package/templates/next-fullstack-shadcn/scripts/design-coverage.mjs +20 -0
  145. package/templates/next-fullstack-shadcn/scripts/design-next-cleanup.mjs +13 -0
  146. package/templates/next-fullstack-shadcn/scripts/design-verify.mjs +154 -0
  147. package/templates/next-fullstack-shadcn/scripts/design.mjs +108 -0
  148. package/templates/next-fullstack-shadcn/scripts/project.mjs +120 -0
  149. package/templates/next-fullstack-shadcn/tests/contract-client.ts +20 -0
  150. package/templates/next-fullstack-shadcn/tests/contract.spec.ts +49 -0
  151. package/templates/next-fullstack-shadcn/tests/design/harness.tsx +63 -0
  152. package/templates/next-fullstack-shadcn/tests/ui.spec.ts +61 -0
  153. package/templates/next-fullstack-shadcn/web/Dockerfile +12 -0
  154. package/templates/next-fullstack-shadcn/web/app/api/auth/login/route.ts +1 -0
  155. package/templates/next-fullstack-shadcn/web/app/api/auth/logout/route.ts +1 -0
  156. package/templates/next-fullstack-shadcn/web/app/api/auth/me/route.ts +1 -0
  157. package/templates/next-fullstack-shadcn/web/app/api/health/route.ts +1 -0
  158. package/templates/next-fullstack-shadcn/web/app/api/records/[id]/route.ts +5 -0
  159. package/templates/next-fullstack-shadcn/web/app/api/records/route.ts +3 -0
  160. package/templates/next-fullstack-shadcn/web/app/layout.tsx +4 -0
  161. package/templates/next-fullstack-shadcn/web/app/page.tsx +2 -0
  162. package/templates/next-fullstack-shadcn/web/eslint.config.mjs +3 -0
  163. package/templates/next-fullstack-shadcn/web/lib/api.ts +35 -0
  164. package/templates/next-fullstack-shadcn/web/lib/auth.ts +10 -0
  165. package/templates/next-fullstack-shadcn/web/lib/db.ts +3 -0
  166. package/templates/next-fullstack-shadcn/web/lib/session.test.ts +15 -0
  167. package/templates/next-fullstack-shadcn/web/lib/validation.test.ts +3 -0
  168. package/templates/next-fullstack-shadcn/web/lib/validation.ts +5 -0
  169. package/templates/next-fullstack-shadcn/web/next-env.d.ts +7 -0
  170. package/templates/next-fullstack-shadcn/web/next.config.ts +3 -0
  171. package/templates/next-fullstack-shadcn/web/package.json +39 -0
  172. package/templates/next-fullstack-shadcn/web/pnpm-lock.yaml +3286 -0
  173. package/templates/next-fullstack-shadcn/web/pnpm-workspace.yaml +7 -0
  174. package/templates/next-fullstack-shadcn/web/postcss.config.mjs +1 -0
  175. package/templates/next-fullstack-shadcn/web/prisma/migrations/202610190001_initial/migration.sql +99 -0
  176. package/templates/next-fullstack-shadcn/web/prisma/migrations/migration_lock.toml +1 -0
  177. package/templates/next-fullstack-shadcn/web/prisma/schema.prisma +72 -0
  178. package/templates/next-fullstack-shadcn/web/scripts/seed.ts +4 -0
  179. package/templates/next-fullstack-shadcn/web/src/App.tsx +30 -0
  180. package/templates/next-fullstack-shadcn/web/src/design-layout.css +7 -0
  181. package/templates/next-fullstack-shadcn/web/src/design-runtime.json +511 -0
  182. package/templates/next-fullstack-shadcn/web/src/design-tokens.css +168 -0
  183. package/templates/next-fullstack-shadcn/web/src/design-widgets.css +31 -0
  184. package/templates/next-fullstack-shadcn/web/src/structure-view.tsx +16 -0
  185. package/templates/next-fullstack-shadcn/web/src/styles.css +3 -0
  186. package/templates/next-fullstack-shadcn/web/src/tailwind.css +2 -0
  187. package/templates/next-fullstack-shadcn/web/src/ui.tsx +13 -0
  188. package/templates/next-fullstack-shadcn/web/src/widget-icon.tsx +5 -0
  189. package/templates/next-fullstack-shadcn/web/src/widget-types.ts +13 -0
  190. package/templates/next-fullstack-shadcn/web/src/workspace-model.ts +85 -0
  191. package/templates/next-fullstack-shadcn/web/tsconfig.json +42 -0
  192. package/templates/next-fullstack-shadcn/web/vitest.config.ts +2 -0
  193. package/templates/react-fastapi-antd/.agents/skills/design-contract/SKILL.md +23 -0
  194. package/templates/react-fastapi-antd/.agents/skills/project-work/SKILL.md +10 -0
  195. package/templates/react-fastapi-antd/.claude/skills/design-contract/SKILL.md +23 -0
  196. package/templates/react-fastapi-antd/.claude/skills/project-work/SKILL.md +10 -0
  197. package/templates/react-fastapi-antd/.dockerignore +18 -0
  198. package/templates/react-fastapi-antd/.env.example +13 -0
  199. package/templates/react-fastapi-antd/.grok/skills/design-contract/SKILL.md +23 -0
  200. package/templates/react-fastapi-antd/.grok/skills/project-work/SKILL.md +10 -0
  201. package/templates/react-fastapi-antd/.scaffold-checks.json +7 -0
  202. package/templates/react-fastapi-antd/AGENTS.md +11 -0
  203. package/templates/react-fastapi-antd/CLAUDE.md +1 -0
  204. package/templates/react-fastapi-antd/DESIGN.md +208 -0
  205. package/templates/react-fastapi-antd/LICENSE +21 -0
  206. package/templates/react-fastapi-antd/README.md +64 -0
  207. package/templates/react-fastapi-antd/THIRD_PARTY_NOTICES.md +18 -0
  208. package/templates/react-fastapi-antd/backend/.python-version +1 -0
  209. package/templates/react-fastapi-antd/backend/Dockerfile +10 -0
  210. package/templates/react-fastapi-antd/backend/alembic.ini +3 -0
  211. package/templates/react-fastapi-antd/backend/app/__init__.py +0 -0
  212. package/templates/react-fastapi-antd/backend/app/core/__init__.py +0 -0
  213. package/templates/react-fastapi-antd/backend/app/core/security.py +25 -0
  214. package/templates/react-fastapi-antd/backend/app/db.py +11 -0
  215. package/templates/react-fastapi-antd/backend/app/main.py +146 -0
  216. package/templates/react-fastapi-antd/backend/app/models.py +21 -0
  217. package/templates/react-fastapi-antd/backend/app/seed.py +21 -0
  218. package/templates/react-fastapi-antd/backend/migrations/env.py +9 -0
  219. package/templates/react-fastapi-antd/backend/migrations/versions/0001_initial.py +18 -0
  220. package/templates/react-fastapi-antd/backend/pyproject.toml +16 -0
  221. package/templates/react-fastapi-antd/backend/tests/test_api.py +27 -0
  222. package/templates/react-fastapi-antd/backend/uv.lock +1085 -0
  223. package/templates/react-fastapi-antd/compose.yaml +18 -0
  224. package/templates/react-fastapi-antd/design/guide.md +61 -0
  225. package/templates/react-fastapi-antd/design/layout.css +6 -0
  226. package/templates/react-fastapi-antd/design/structure.json +107 -0
  227. package/templates/react-fastapi-antd/design/tokens.json +171 -0
  228. package/templates/react-fastapi-antd/design/widgets.css +30 -0
  229. package/templates/react-fastapi-antd/design/widgets.json +233 -0
  230. package/templates/react-fastapi-antd/docs/PROJECT.md +32 -0
  231. package/templates/react-fastapi-antd/docs/api.d.ts +544 -0
  232. package/templates/react-fastapi-antd/docs/openapi.json +461 -0
  233. package/templates/react-fastapi-antd/gitignore.template +19 -0
  234. package/templates/react-fastapi-antd/licenses/fastapi.txt +21 -0
  235. package/templates/react-fastapi-antd/licenses/nest.txt +22 -0
  236. package/templates/react-fastapi-antd/licenses/next.txt +21 -0
  237. package/templates/react-fastapi-antd/licenses/shadcn.txt +21 -0
  238. package/templates/react-fastapi-antd/licenses/sources.json +27 -0
  239. package/templates/react-fastapi-antd/licenses/spring.txt +202 -0
  240. package/templates/react-fastapi-antd/package-lock.json +494 -0
  241. package/templates/react-fastapi-antd/package.json +26 -0
  242. package/templates/react-fastapi-antd/playwright.config.ts +3 -0
  243. package/templates/react-fastapi-antd/project.json +8 -0
  244. package/templates/react-fastapi-antd/scripts/api-check.mjs +18 -0
  245. package/templates/react-fastapi-antd/scripts/api-contract.mjs +46 -0
  246. package/templates/react-fastapi-antd/scripts/design-coverage.mjs +20 -0
  247. package/templates/react-fastapi-antd/scripts/design-next-cleanup.mjs +13 -0
  248. package/templates/react-fastapi-antd/scripts/design-verify.mjs +154 -0
  249. package/templates/react-fastapi-antd/scripts/design.mjs +108 -0
  250. package/templates/react-fastapi-antd/scripts/project.mjs +120 -0
  251. package/templates/react-fastapi-antd/tests/contract-client.ts +20 -0
  252. package/templates/react-fastapi-antd/tests/contract.spec.ts +49 -0
  253. package/templates/react-fastapi-antd/tests/design/harness.tsx +63 -0
  254. package/templates/react-fastapi-antd/tests/ui.spec.ts +61 -0
  255. package/templates/react-fastapi-antd/web/bun.lock +442 -0
  256. package/templates/react-fastapi-antd/web/eslint.config.mjs +3 -0
  257. package/templates/react-fastapi-antd/web/index.html +1 -0
  258. package/templates/react-fastapi-antd/web/package.json +28 -0
  259. package/templates/react-fastapi-antd/web/src/App.tsx +29 -0
  260. package/templates/react-fastapi-antd/web/src/design-layout.css +7 -0
  261. package/templates/react-fastapi-antd/web/src/design-runtime.json +511 -0
  262. package/templates/react-fastapi-antd/web/src/design-tokens.css +168 -0
  263. package/templates/react-fastapi-antd/web/src/design-widgets.css +31 -0
  264. package/templates/react-fastapi-antd/web/src/main.tsx +6 -0
  265. package/templates/react-fastapi-antd/web/src/structure-view.tsx +16 -0
  266. package/templates/react-fastapi-antd/web/src/styles.css +3 -0
  267. package/templates/react-fastapi-antd/web/src/ui.tsx +30 -0
  268. package/templates/react-fastapi-antd/web/src/widget-icon.tsx +5 -0
  269. package/templates/react-fastapi-antd/web/src/widget-types.ts +13 -0
  270. package/templates/react-fastapi-antd/web/src/workspace-model.ts +85 -0
  271. package/templates/react-fastapi-antd/web/tsconfig.json +22 -0
  272. package/templates/react-fastapi-antd/web/vite.config.ts +3 -0
  273. package/templates/react-fastapi-shadcn/.agents/skills/design-contract/SKILL.md +23 -0
  274. package/templates/react-fastapi-shadcn/.agents/skills/project-work/SKILL.md +10 -0
  275. package/templates/react-fastapi-shadcn/.claude/skills/design-contract/SKILL.md +23 -0
  276. package/templates/react-fastapi-shadcn/.claude/skills/project-work/SKILL.md +10 -0
  277. package/templates/react-fastapi-shadcn/.dockerignore +18 -0
  278. package/templates/react-fastapi-shadcn/.env.example +13 -0
  279. package/templates/react-fastapi-shadcn/.grok/skills/design-contract/SKILL.md +23 -0
  280. package/templates/react-fastapi-shadcn/.grok/skills/project-work/SKILL.md +10 -0
  281. package/templates/react-fastapi-shadcn/.scaffold-checks.json +7 -0
  282. package/templates/react-fastapi-shadcn/AGENTS.md +11 -0
  283. package/templates/react-fastapi-shadcn/CLAUDE.md +1 -0
  284. package/templates/react-fastapi-shadcn/DESIGN.md +208 -0
  285. package/templates/react-fastapi-shadcn/LICENSE +21 -0
  286. package/templates/react-fastapi-shadcn/README.md +64 -0
  287. package/templates/react-fastapi-shadcn/THIRD_PARTY_NOTICES.md +18 -0
  288. package/templates/react-fastapi-shadcn/backend/.python-version +1 -0
  289. package/templates/react-fastapi-shadcn/backend/Dockerfile +10 -0
  290. package/templates/react-fastapi-shadcn/backend/alembic.ini +3 -0
  291. package/templates/react-fastapi-shadcn/backend/app/__init__.py +0 -0
  292. package/templates/react-fastapi-shadcn/backend/app/core/__init__.py +0 -0
  293. package/templates/react-fastapi-shadcn/backend/app/core/security.py +25 -0
  294. package/templates/react-fastapi-shadcn/backend/app/db.py +11 -0
  295. package/templates/react-fastapi-shadcn/backend/app/main.py +146 -0
  296. package/templates/react-fastapi-shadcn/backend/app/models.py +21 -0
  297. package/templates/react-fastapi-shadcn/backend/app/seed.py +21 -0
  298. package/templates/react-fastapi-shadcn/backend/migrations/env.py +9 -0
  299. package/templates/react-fastapi-shadcn/backend/migrations/versions/0001_initial.py +18 -0
  300. package/templates/react-fastapi-shadcn/backend/pyproject.toml +16 -0
  301. package/templates/react-fastapi-shadcn/backend/tests/test_api.py +27 -0
  302. package/templates/react-fastapi-shadcn/backend/uv.lock +1085 -0
  303. package/templates/react-fastapi-shadcn/compose.yaml +18 -0
  304. package/templates/react-fastapi-shadcn/design/guide.md +61 -0
  305. package/templates/react-fastapi-shadcn/design/layout.css +6 -0
  306. package/templates/react-fastapi-shadcn/design/structure.json +107 -0
  307. package/templates/react-fastapi-shadcn/design/tokens.json +171 -0
  308. package/templates/react-fastapi-shadcn/design/widgets.css +30 -0
  309. package/templates/react-fastapi-shadcn/design/widgets.json +233 -0
  310. package/templates/react-fastapi-shadcn/docs/PROJECT.md +32 -0
  311. package/templates/react-fastapi-shadcn/docs/api.d.ts +544 -0
  312. package/templates/react-fastapi-shadcn/docs/openapi.json +461 -0
  313. package/templates/react-fastapi-shadcn/gitignore.template +19 -0
  314. package/templates/react-fastapi-shadcn/licenses/fastapi.txt +21 -0
  315. package/templates/react-fastapi-shadcn/licenses/nest.txt +22 -0
  316. package/templates/react-fastapi-shadcn/licenses/next.txt +21 -0
  317. package/templates/react-fastapi-shadcn/licenses/shadcn.txt +21 -0
  318. package/templates/react-fastapi-shadcn/licenses/sources.json +27 -0
  319. package/templates/react-fastapi-shadcn/licenses/spring.txt +202 -0
  320. package/templates/react-fastapi-shadcn/package-lock.json +494 -0
  321. package/templates/react-fastapi-shadcn/package.json +26 -0
  322. package/templates/react-fastapi-shadcn/playwright.config.ts +3 -0
  323. package/templates/react-fastapi-shadcn/project.json +8 -0
  324. package/templates/react-fastapi-shadcn/scripts/api-check.mjs +18 -0
  325. package/templates/react-fastapi-shadcn/scripts/api-contract.mjs +46 -0
  326. package/templates/react-fastapi-shadcn/scripts/design-coverage.mjs +20 -0
  327. package/templates/react-fastapi-shadcn/scripts/design-next-cleanup.mjs +13 -0
  328. package/templates/react-fastapi-shadcn/scripts/design-verify.mjs +154 -0
  329. package/templates/react-fastapi-shadcn/scripts/design.mjs +108 -0
  330. package/templates/react-fastapi-shadcn/scripts/project.mjs +120 -0
  331. package/templates/react-fastapi-shadcn/tests/contract-client.ts +20 -0
  332. package/templates/react-fastapi-shadcn/tests/contract.spec.ts +49 -0
  333. package/templates/react-fastapi-shadcn/tests/design/harness.tsx +63 -0
  334. package/templates/react-fastapi-shadcn/tests/ui.spec.ts +61 -0
  335. package/templates/react-fastapi-shadcn/web/bun.lock +413 -0
  336. package/templates/react-fastapi-shadcn/web/eslint.config.mjs +3 -0
  337. package/templates/react-fastapi-shadcn/web/index.html +1 -0
  338. package/templates/react-fastapi-shadcn/web/package.json +33 -0
  339. package/templates/react-fastapi-shadcn/web/src/App.tsx +30 -0
  340. package/templates/react-fastapi-shadcn/web/src/design-layout.css +7 -0
  341. package/templates/react-fastapi-shadcn/web/src/design-runtime.json +511 -0
  342. package/templates/react-fastapi-shadcn/web/src/design-tokens.css +168 -0
  343. package/templates/react-fastapi-shadcn/web/src/design-widgets.css +31 -0
  344. package/templates/react-fastapi-shadcn/web/src/main.tsx +6 -0
  345. package/templates/react-fastapi-shadcn/web/src/structure-view.tsx +16 -0
  346. package/templates/react-fastapi-shadcn/web/src/styles.css +3 -0
  347. package/templates/react-fastapi-shadcn/web/src/tailwind.css +2 -0
  348. package/templates/react-fastapi-shadcn/web/src/ui.tsx +13 -0
  349. package/templates/react-fastapi-shadcn/web/src/widget-icon.tsx +5 -0
  350. package/templates/react-fastapi-shadcn/web/src/widget-types.ts +13 -0
  351. package/templates/react-fastapi-shadcn/web/src/workspace-model.ts +85 -0
  352. package/templates/react-fastapi-shadcn/web/tsconfig.json +22 -0
  353. package/templates/react-fastapi-shadcn/web/vite.config.ts +4 -0
  354. package/templates/react-nest-antd/.agents/skills/design-contract/SKILL.md +23 -0
  355. package/templates/react-nest-antd/.agents/skills/project-work/SKILL.md +10 -0
  356. package/templates/react-nest-antd/.claude/skills/design-contract/SKILL.md +23 -0
  357. package/templates/react-nest-antd/.claude/skills/project-work/SKILL.md +10 -0
  358. package/templates/react-nest-antd/.dockerignore +18 -0
  359. package/templates/react-nest-antd/.env.example +13 -0
  360. package/templates/react-nest-antd/.grok/skills/design-contract/SKILL.md +23 -0
  361. package/templates/react-nest-antd/.grok/skills/project-work/SKILL.md +10 -0
  362. package/templates/react-nest-antd/.scaffold-checks.json +7 -0
  363. package/templates/react-nest-antd/AGENTS.md +11 -0
  364. package/templates/react-nest-antd/CLAUDE.md +1 -0
  365. package/templates/react-nest-antd/DESIGN.md +208 -0
  366. package/templates/react-nest-antd/LICENSE +21 -0
  367. package/templates/react-nest-antd/README.md +64 -0
  368. package/templates/react-nest-antd/THIRD_PARTY_NOTICES.md +18 -0
  369. package/templates/react-nest-antd/backend/Dockerfile +12 -0
  370. package/templates/react-nest-antd/backend/eslint.config.mjs +3 -0
  371. package/templates/react-nest-antd/backend/jest.config.cjs +1 -0
  372. package/templates/react-nest-antd/backend/package.json +40 -0
  373. package/templates/react-nest-antd/backend/pnpm-lock.yaml +5165 -0
  374. package/templates/react-nest-antd/backend/pnpm-workspace.yaml +6 -0
  375. package/templates/react-nest-antd/backend/prisma/migrations/202610190001_initial/migration.sql +3 -0
  376. package/templates/react-nest-antd/backend/prisma/migrations/migration_lock.toml +1 -0
  377. package/templates/react-nest-antd/backend/prisma/schema.prisma +25 -0
  378. package/templates/react-nest-antd/backend/scripts/seed.ts +4 -0
  379. package/templates/react-nest-antd/backend/src/app.module.ts +43 -0
  380. package/templates/react-nest-antd/backend/src/main.ts +6 -0
  381. package/templates/react-nest-antd/backend/src/password.ts +3 -0
  382. package/templates/react-nest-antd/backend/src/validation.ts +7 -0
  383. package/templates/react-nest-antd/backend/test/api.test.ts +25 -0
  384. package/templates/react-nest-antd/backend/tsconfig.build.json +15 -0
  385. package/templates/react-nest-antd/backend/tsconfig.json +17 -0
  386. package/templates/react-nest-antd/compose.yaml +18 -0
  387. package/templates/react-nest-antd/design/guide.md +61 -0
  388. package/templates/react-nest-antd/design/layout.css +6 -0
  389. package/templates/react-nest-antd/design/structure.json +107 -0
  390. package/templates/react-nest-antd/design/tokens.json +171 -0
  391. package/templates/react-nest-antd/design/widgets.css +30 -0
  392. package/templates/react-nest-antd/design/widgets.json +233 -0
  393. package/templates/react-nest-antd/docs/PROJECT.md +32 -0
  394. package/templates/react-nest-antd/docs/api.d.ts +544 -0
  395. package/templates/react-nest-antd/docs/openapi.json +461 -0
  396. package/templates/react-nest-antd/gitignore.template +19 -0
  397. package/templates/react-nest-antd/licenses/fastapi.txt +21 -0
  398. package/templates/react-nest-antd/licenses/nest.txt +22 -0
  399. package/templates/react-nest-antd/licenses/next.txt +21 -0
  400. package/templates/react-nest-antd/licenses/shadcn.txt +21 -0
  401. package/templates/react-nest-antd/licenses/sources.json +27 -0
  402. package/templates/react-nest-antd/licenses/spring.txt +202 -0
  403. package/templates/react-nest-antd/package-lock.json +494 -0
  404. package/templates/react-nest-antd/package.json +26 -0
  405. package/templates/react-nest-antd/playwright.config.ts +3 -0
  406. package/templates/react-nest-antd/project.json +8 -0
  407. package/templates/react-nest-antd/scripts/api-check.mjs +18 -0
  408. package/templates/react-nest-antd/scripts/api-contract.mjs +46 -0
  409. package/templates/react-nest-antd/scripts/design-coverage.mjs +20 -0
  410. package/templates/react-nest-antd/scripts/design-next-cleanup.mjs +13 -0
  411. package/templates/react-nest-antd/scripts/design-verify.mjs +154 -0
  412. package/templates/react-nest-antd/scripts/design.mjs +108 -0
  413. package/templates/react-nest-antd/scripts/project.mjs +120 -0
  414. package/templates/react-nest-antd/tests/contract-client.ts +20 -0
  415. package/templates/react-nest-antd/tests/contract.spec.ts +49 -0
  416. package/templates/react-nest-antd/tests/design/harness.tsx +63 -0
  417. package/templates/react-nest-antd/tests/ui.spec.ts +61 -0
  418. package/templates/react-nest-antd/web/eslint.config.mjs +3 -0
  419. package/templates/react-nest-antd/web/index.html +1 -0
  420. package/templates/react-nest-antd/web/package.json +28 -0
  421. package/templates/react-nest-antd/web/pnpm-lock.yaml +2215 -0
  422. package/templates/react-nest-antd/web/src/App.tsx +29 -0
  423. package/templates/react-nest-antd/web/src/design-layout.css +7 -0
  424. package/templates/react-nest-antd/web/src/design-runtime.json +511 -0
  425. package/templates/react-nest-antd/web/src/design-tokens.css +168 -0
  426. package/templates/react-nest-antd/web/src/design-widgets.css +31 -0
  427. package/templates/react-nest-antd/web/src/main.tsx +6 -0
  428. package/templates/react-nest-antd/web/src/structure-view.tsx +16 -0
  429. package/templates/react-nest-antd/web/src/styles.css +3 -0
  430. package/templates/react-nest-antd/web/src/ui.tsx +30 -0
  431. package/templates/react-nest-antd/web/src/widget-icon.tsx +5 -0
  432. package/templates/react-nest-antd/web/src/widget-types.ts +13 -0
  433. package/templates/react-nest-antd/web/src/workspace-model.ts +85 -0
  434. package/templates/react-nest-antd/web/tsconfig.json +22 -0
  435. package/templates/react-nest-antd/web/vite.config.ts +3 -0
  436. package/templates/react-nest-shadcn/.agents/skills/design-contract/SKILL.md +23 -0
  437. package/templates/react-nest-shadcn/.agents/skills/project-work/SKILL.md +10 -0
  438. package/templates/react-nest-shadcn/.claude/skills/design-contract/SKILL.md +23 -0
  439. package/templates/react-nest-shadcn/.claude/skills/project-work/SKILL.md +10 -0
  440. package/templates/react-nest-shadcn/.dockerignore +18 -0
  441. package/templates/react-nest-shadcn/.env.example +13 -0
  442. package/templates/react-nest-shadcn/.grok/skills/design-contract/SKILL.md +23 -0
  443. package/templates/react-nest-shadcn/.grok/skills/project-work/SKILL.md +10 -0
  444. package/templates/react-nest-shadcn/.scaffold-checks.json +7 -0
  445. package/templates/react-nest-shadcn/AGENTS.md +11 -0
  446. package/templates/react-nest-shadcn/CLAUDE.md +1 -0
  447. package/templates/react-nest-shadcn/DESIGN.md +208 -0
  448. package/templates/react-nest-shadcn/LICENSE +21 -0
  449. package/templates/react-nest-shadcn/README.md +64 -0
  450. package/templates/react-nest-shadcn/THIRD_PARTY_NOTICES.md +18 -0
  451. package/templates/react-nest-shadcn/backend/Dockerfile +12 -0
  452. package/templates/react-nest-shadcn/backend/eslint.config.mjs +3 -0
  453. package/templates/react-nest-shadcn/backend/jest.config.cjs +1 -0
  454. package/templates/react-nest-shadcn/backend/package.json +40 -0
  455. package/templates/react-nest-shadcn/backend/pnpm-lock.yaml +5165 -0
  456. package/templates/react-nest-shadcn/backend/pnpm-workspace.yaml +6 -0
  457. package/templates/react-nest-shadcn/backend/prisma/migrations/202610190001_initial/migration.sql +3 -0
  458. package/templates/react-nest-shadcn/backend/prisma/migrations/migration_lock.toml +1 -0
  459. package/templates/react-nest-shadcn/backend/prisma/schema.prisma +25 -0
  460. package/templates/react-nest-shadcn/backend/scripts/seed.ts +4 -0
  461. package/templates/react-nest-shadcn/backend/src/app.module.ts +43 -0
  462. package/templates/react-nest-shadcn/backend/src/main.ts +6 -0
  463. package/templates/react-nest-shadcn/backend/src/password.ts +3 -0
  464. package/templates/react-nest-shadcn/backend/src/validation.ts +7 -0
  465. package/templates/react-nest-shadcn/backend/test/api.test.ts +25 -0
  466. package/templates/react-nest-shadcn/backend/tsconfig.build.json +15 -0
  467. package/templates/react-nest-shadcn/backend/tsconfig.json +17 -0
  468. package/templates/react-nest-shadcn/compose.yaml +18 -0
  469. package/templates/react-nest-shadcn/design/guide.md +61 -0
  470. package/templates/react-nest-shadcn/design/layout.css +6 -0
  471. package/templates/react-nest-shadcn/design/structure.json +107 -0
  472. package/templates/react-nest-shadcn/design/tokens.json +171 -0
  473. package/templates/react-nest-shadcn/design/widgets.css +30 -0
  474. package/templates/react-nest-shadcn/design/widgets.json +233 -0
  475. package/templates/react-nest-shadcn/docs/PROJECT.md +32 -0
  476. package/templates/react-nest-shadcn/docs/api.d.ts +544 -0
  477. package/templates/react-nest-shadcn/docs/openapi.json +461 -0
  478. package/templates/react-nest-shadcn/gitignore.template +19 -0
  479. package/templates/react-nest-shadcn/licenses/fastapi.txt +21 -0
  480. package/templates/react-nest-shadcn/licenses/nest.txt +22 -0
  481. package/templates/react-nest-shadcn/licenses/next.txt +21 -0
  482. package/templates/react-nest-shadcn/licenses/shadcn.txt +21 -0
  483. package/templates/react-nest-shadcn/licenses/sources.json +27 -0
  484. package/templates/react-nest-shadcn/licenses/spring.txt +202 -0
  485. package/templates/react-nest-shadcn/package-lock.json +494 -0
  486. package/templates/react-nest-shadcn/package.json +26 -0
  487. package/templates/react-nest-shadcn/playwright.config.ts +3 -0
  488. package/templates/react-nest-shadcn/project.json +8 -0
  489. package/templates/react-nest-shadcn/scripts/api-check.mjs +18 -0
  490. package/templates/react-nest-shadcn/scripts/api-contract.mjs +46 -0
  491. package/templates/react-nest-shadcn/scripts/design-coverage.mjs +20 -0
  492. package/templates/react-nest-shadcn/scripts/design-next-cleanup.mjs +13 -0
  493. package/templates/react-nest-shadcn/scripts/design-verify.mjs +154 -0
  494. package/templates/react-nest-shadcn/scripts/design.mjs +108 -0
  495. package/templates/react-nest-shadcn/scripts/project.mjs +120 -0
  496. package/templates/react-nest-shadcn/tests/contract-client.ts +20 -0
  497. package/templates/react-nest-shadcn/tests/contract.spec.ts +49 -0
  498. package/templates/react-nest-shadcn/tests/design/harness.tsx +63 -0
  499. package/templates/react-nest-shadcn/tests/ui.spec.ts +61 -0
  500. package/templates/react-nest-shadcn/web/eslint.config.mjs +3 -0
  501. package/templates/react-nest-shadcn/web/index.html +1 -0
  502. package/templates/react-nest-shadcn/web/package.json +33 -0
  503. package/templates/react-nest-shadcn/web/pnpm-lock.yaml +1788 -0
  504. package/templates/react-nest-shadcn/web/src/App.tsx +30 -0
  505. package/templates/react-nest-shadcn/web/src/design-layout.css +7 -0
  506. package/templates/react-nest-shadcn/web/src/design-runtime.json +511 -0
  507. package/templates/react-nest-shadcn/web/src/design-tokens.css +168 -0
  508. package/templates/react-nest-shadcn/web/src/design-widgets.css +31 -0
  509. package/templates/react-nest-shadcn/web/src/main.tsx +6 -0
  510. package/templates/react-nest-shadcn/web/src/structure-view.tsx +16 -0
  511. package/templates/react-nest-shadcn/web/src/styles.css +3 -0
  512. package/templates/react-nest-shadcn/web/src/tailwind.css +2 -0
  513. package/templates/react-nest-shadcn/web/src/ui.tsx +13 -0
  514. package/templates/react-nest-shadcn/web/src/widget-icon.tsx +5 -0
  515. package/templates/react-nest-shadcn/web/src/widget-types.ts +13 -0
  516. package/templates/react-nest-shadcn/web/src/workspace-model.ts +85 -0
  517. package/templates/react-nest-shadcn/web/tsconfig.json +22 -0
  518. package/templates/react-nest-shadcn/web/vite.config.ts +4 -0
  519. package/templates/react-spring-antd/.agents/skills/design-contract/SKILL.md +23 -0
  520. package/templates/react-spring-antd/.agents/skills/project-work/SKILL.md +10 -0
  521. package/templates/react-spring-antd/.claude/skills/design-contract/SKILL.md +23 -0
  522. package/templates/react-spring-antd/.claude/skills/project-work/SKILL.md +10 -0
  523. package/templates/react-spring-antd/.dockerignore +18 -0
  524. package/templates/react-spring-antd/.env.example +13 -0
  525. package/templates/react-spring-antd/.grok/skills/design-contract/SKILL.md +23 -0
  526. package/templates/react-spring-antd/.grok/skills/project-work/SKILL.md +10 -0
  527. package/templates/react-spring-antd/.scaffold-checks.json +7 -0
  528. package/templates/react-spring-antd/AGENTS.md +11 -0
  529. package/templates/react-spring-antd/CLAUDE.md +1 -0
  530. package/templates/react-spring-antd/DESIGN.md +208 -0
  531. package/templates/react-spring-antd/LICENSE +21 -0
  532. package/templates/react-spring-antd/README.md +64 -0
  533. package/templates/react-spring-antd/THIRD_PARTY_NOTICES.md +18 -0
  534. package/templates/react-spring-antd/backend/.mvn/wrapper/maven-wrapper.properties +3 -0
  535. package/templates/react-spring-antd/backend/Dockerfile +11 -0
  536. package/templates/react-spring-antd/backend/mvnw +295 -0
  537. package/templates/react-spring-antd/backend/pom.xml +15 -0
  538. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/Api.java +38 -0
  539. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/AppUser.java +11 -0
  540. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/Application.java +7 -0
  541. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/Errors.java +13 -0
  542. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/PersonalRecord.java +15 -0
  543. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/Records.java +14 -0
  544. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/Security.java +28 -0
  545. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/Seed.java +13 -0
  546. package/templates/react-spring-antd/backend/src/main/java/dev/scaffold/app/Users.java +5 -0
  547. package/templates/react-spring-antd/backend/src/main/resources/application.properties +17 -0
  548. package/templates/react-spring-antd/backend/src/main/resources/db/migration/V1__initial.sql +3 -0
  549. package/templates/react-spring-antd/backend/src/main/resources/openapi.json +461 -0
  550. package/templates/react-spring-antd/backend/src/test/java/dev/scaffold/app/ApiTest.java +28 -0
  551. package/templates/react-spring-antd/backend/src/test/java/dev/scaffold/app/SessionExpiryTest.java +37 -0
  552. package/templates/react-spring-antd/compose.yaml +18 -0
  553. package/templates/react-spring-antd/design/guide.md +61 -0
  554. package/templates/react-spring-antd/design/layout.css +6 -0
  555. package/templates/react-spring-antd/design/structure.json +107 -0
  556. package/templates/react-spring-antd/design/tokens.json +171 -0
  557. package/templates/react-spring-antd/design/widgets.css +30 -0
  558. package/templates/react-spring-antd/design/widgets.json +233 -0
  559. package/templates/react-spring-antd/docs/PROJECT.md +32 -0
  560. package/templates/react-spring-antd/docs/api.d.ts +544 -0
  561. package/templates/react-spring-antd/docs/openapi.json +461 -0
  562. package/templates/react-spring-antd/gitignore.template +19 -0
  563. package/templates/react-spring-antd/licenses/fastapi.txt +21 -0
  564. package/templates/react-spring-antd/licenses/nest.txt +22 -0
  565. package/templates/react-spring-antd/licenses/next.txt +21 -0
  566. package/templates/react-spring-antd/licenses/shadcn.txt +21 -0
  567. package/templates/react-spring-antd/licenses/sources.json +27 -0
  568. package/templates/react-spring-antd/licenses/spring.txt +202 -0
  569. package/templates/react-spring-antd/package-lock.json +494 -0
  570. package/templates/react-spring-antd/package.json +26 -0
  571. package/templates/react-spring-antd/playwright.config.ts +3 -0
  572. package/templates/react-spring-antd/project.json +8 -0
  573. package/templates/react-spring-antd/scripts/api-check.mjs +18 -0
  574. package/templates/react-spring-antd/scripts/api-contract.mjs +46 -0
  575. package/templates/react-spring-antd/scripts/design-coverage.mjs +20 -0
  576. package/templates/react-spring-antd/scripts/design-next-cleanup.mjs +13 -0
  577. package/templates/react-spring-antd/scripts/design-verify.mjs +154 -0
  578. package/templates/react-spring-antd/scripts/design.mjs +108 -0
  579. package/templates/react-spring-antd/scripts/project.mjs +120 -0
  580. package/templates/react-spring-antd/tests/contract-client.ts +20 -0
  581. package/templates/react-spring-antd/tests/contract.spec.ts +49 -0
  582. package/templates/react-spring-antd/tests/design/harness.tsx +63 -0
  583. package/templates/react-spring-antd/tests/ui.spec.ts +61 -0
  584. package/templates/react-spring-antd/web/eslint.config.mjs +3 -0
  585. package/templates/react-spring-antd/web/index.html +1 -0
  586. package/templates/react-spring-antd/web/package.json +28 -0
  587. package/templates/react-spring-antd/web/pnpm-lock.yaml +2215 -0
  588. package/templates/react-spring-antd/web/src/App.tsx +29 -0
  589. package/templates/react-spring-antd/web/src/design-layout.css +7 -0
  590. package/templates/react-spring-antd/web/src/design-runtime.json +511 -0
  591. package/templates/react-spring-antd/web/src/design-tokens.css +168 -0
  592. package/templates/react-spring-antd/web/src/design-widgets.css +31 -0
  593. package/templates/react-spring-antd/web/src/main.tsx +6 -0
  594. package/templates/react-spring-antd/web/src/structure-view.tsx +16 -0
  595. package/templates/react-spring-antd/web/src/styles.css +3 -0
  596. package/templates/react-spring-antd/web/src/ui.tsx +30 -0
  597. package/templates/react-spring-antd/web/src/widget-icon.tsx +5 -0
  598. package/templates/react-spring-antd/web/src/widget-types.ts +13 -0
  599. package/templates/react-spring-antd/web/src/workspace-model.ts +85 -0
  600. package/templates/react-spring-antd/web/tsconfig.json +22 -0
  601. package/templates/react-spring-antd/web/vite.config.ts +3 -0
  602. package/templates/react-spring-shadcn/.agents/skills/design-contract/SKILL.md +23 -0
  603. package/templates/react-spring-shadcn/.agents/skills/project-work/SKILL.md +10 -0
  604. package/templates/react-spring-shadcn/.claude/skills/design-contract/SKILL.md +23 -0
  605. package/templates/react-spring-shadcn/.claude/skills/project-work/SKILL.md +10 -0
  606. package/templates/react-spring-shadcn/.dockerignore +18 -0
  607. package/templates/react-spring-shadcn/.env.example +13 -0
  608. package/templates/react-spring-shadcn/.grok/skills/design-contract/SKILL.md +23 -0
  609. package/templates/react-spring-shadcn/.grok/skills/project-work/SKILL.md +10 -0
  610. package/templates/react-spring-shadcn/.scaffold-checks.json +7 -0
  611. package/templates/react-spring-shadcn/AGENTS.md +11 -0
  612. package/templates/react-spring-shadcn/CLAUDE.md +1 -0
  613. package/templates/react-spring-shadcn/DESIGN.md +208 -0
  614. package/templates/react-spring-shadcn/LICENSE +21 -0
  615. package/templates/react-spring-shadcn/README.md +64 -0
  616. package/templates/react-spring-shadcn/THIRD_PARTY_NOTICES.md +18 -0
  617. package/templates/react-spring-shadcn/backend/.mvn/wrapper/maven-wrapper.properties +3 -0
  618. package/templates/react-spring-shadcn/backend/Dockerfile +11 -0
  619. package/templates/react-spring-shadcn/backend/mvnw +295 -0
  620. package/templates/react-spring-shadcn/backend/pom.xml +15 -0
  621. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/Api.java +38 -0
  622. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/AppUser.java +11 -0
  623. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/Application.java +7 -0
  624. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/Errors.java +13 -0
  625. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/PersonalRecord.java +15 -0
  626. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/Records.java +14 -0
  627. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/Security.java +28 -0
  628. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/Seed.java +13 -0
  629. package/templates/react-spring-shadcn/backend/src/main/java/dev/scaffold/app/Users.java +5 -0
  630. package/templates/react-spring-shadcn/backend/src/main/resources/application.properties +17 -0
  631. package/templates/react-spring-shadcn/backend/src/main/resources/db/migration/V1__initial.sql +3 -0
  632. package/templates/react-spring-shadcn/backend/src/main/resources/openapi.json +461 -0
  633. package/templates/react-spring-shadcn/backend/src/test/java/dev/scaffold/app/ApiTest.java +28 -0
  634. package/templates/react-spring-shadcn/backend/src/test/java/dev/scaffold/app/SessionExpiryTest.java +37 -0
  635. package/templates/react-spring-shadcn/compose.yaml +18 -0
  636. package/templates/react-spring-shadcn/design/guide.md +61 -0
  637. package/templates/react-spring-shadcn/design/layout.css +6 -0
  638. package/templates/react-spring-shadcn/design/structure.json +107 -0
  639. package/templates/react-spring-shadcn/design/tokens.json +171 -0
  640. package/templates/react-spring-shadcn/design/widgets.css +30 -0
  641. package/templates/react-spring-shadcn/design/widgets.json +233 -0
  642. package/templates/react-spring-shadcn/docs/PROJECT.md +32 -0
  643. package/templates/react-spring-shadcn/docs/api.d.ts +544 -0
  644. package/templates/react-spring-shadcn/docs/openapi.json +461 -0
  645. package/templates/react-spring-shadcn/gitignore.template +19 -0
  646. package/templates/react-spring-shadcn/licenses/fastapi.txt +21 -0
  647. package/templates/react-spring-shadcn/licenses/nest.txt +22 -0
  648. package/templates/react-spring-shadcn/licenses/next.txt +21 -0
  649. package/templates/react-spring-shadcn/licenses/shadcn.txt +21 -0
  650. package/templates/react-spring-shadcn/licenses/sources.json +27 -0
  651. package/templates/react-spring-shadcn/licenses/spring.txt +202 -0
  652. package/templates/react-spring-shadcn/package-lock.json +494 -0
  653. package/templates/react-spring-shadcn/package.json +26 -0
  654. package/templates/react-spring-shadcn/playwright.config.ts +3 -0
  655. package/templates/react-spring-shadcn/project.json +8 -0
  656. package/templates/react-spring-shadcn/scripts/api-check.mjs +18 -0
  657. package/templates/react-spring-shadcn/scripts/api-contract.mjs +46 -0
  658. package/templates/react-spring-shadcn/scripts/design-coverage.mjs +20 -0
  659. package/templates/react-spring-shadcn/scripts/design-next-cleanup.mjs +13 -0
  660. package/templates/react-spring-shadcn/scripts/design-verify.mjs +154 -0
  661. package/templates/react-spring-shadcn/scripts/design.mjs +108 -0
  662. package/templates/react-spring-shadcn/scripts/project.mjs +120 -0
  663. package/templates/react-spring-shadcn/tests/contract-client.ts +20 -0
  664. package/templates/react-spring-shadcn/tests/contract.spec.ts +49 -0
  665. package/templates/react-spring-shadcn/tests/design/harness.tsx +63 -0
  666. package/templates/react-spring-shadcn/tests/ui.spec.ts +61 -0
  667. package/templates/react-spring-shadcn/web/eslint.config.mjs +3 -0
  668. package/templates/react-spring-shadcn/web/index.html +1 -0
  669. package/templates/react-spring-shadcn/web/package.json +33 -0
  670. package/templates/react-spring-shadcn/web/pnpm-lock.yaml +1788 -0
  671. package/templates/react-spring-shadcn/web/src/App.tsx +30 -0
  672. package/templates/react-spring-shadcn/web/src/design-layout.css +7 -0
  673. package/templates/react-spring-shadcn/web/src/design-runtime.json +511 -0
  674. package/templates/react-spring-shadcn/web/src/design-tokens.css +168 -0
  675. package/templates/react-spring-shadcn/web/src/design-widgets.css +31 -0
  676. package/templates/react-spring-shadcn/web/src/main.tsx +6 -0
  677. package/templates/react-spring-shadcn/web/src/structure-view.tsx +16 -0
  678. package/templates/react-spring-shadcn/web/src/styles.css +3 -0
  679. package/templates/react-spring-shadcn/web/src/tailwind.css +2 -0
  680. package/templates/react-spring-shadcn/web/src/ui.tsx +13 -0
  681. package/templates/react-spring-shadcn/web/src/widget-icon.tsx +5 -0
  682. package/templates/react-spring-shadcn/web/src/widget-types.ts +13 -0
  683. package/templates/react-spring-shadcn/web/src/workspace-model.ts +85 -0
  684. package/templates/react-spring-shadcn/web/tsconfig.json +22 -0
  685. package/templates/react-spring-shadcn/web/vite.config.ts +4 -0
@@ -0,0 +1,154 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {createServer} from 'node:net';
3
+ import {createRequire} from 'node:module';
4
+ import {mkdir,readFile,writeFile,rm} from 'node:fs/promises';
5
+ import {dirname,resolve,join} from 'node:path';
6
+ import {fileURLToPath} from 'node:url';
7
+ import {createHash,randomUUID} from 'node:crypto';
8
+ import {cleanNextFixtureConfig} from './design-next-cleanup.mjs';
9
+ import {syncDesign} from './design.mjs';
10
+ import {validateFixtureCoverage} from './design-coverage.mjs';
11
+ const root=resolve(dirname(fileURLToPath(import.meta.url)),'..');
12
+ const require=createRequire(join(root,'package.json'));
13
+ const {chromium}=require('@playwright/test');
14
+ const project=JSON.parse(await readFile(join(root,'project.json'),'utf8'));
15
+ const design=JSON.parse(await readFile(join(root,'web/src/design-runtime.json'),'utf8'));
16
+ const output=join(root,'reports/design-check');await mkdir(output,{recursive:true});
17
+ const nonce=randomUUID().slice(0,8),next=project.template==='next-fullstack';
18
+ const nextDist=`.next/scaffold-design-cache-${nonce}`,nextFiles=new Map();
19
+ const harness=join(root,`web/src/scaffold-design-check-${nonce}.tsx`);
20
+ const entry=next?join(root,`web/app/scaffold-design-check-${nonce}/page.tsx`):join(root,`web/scaffold-design-check-${nonce}.html`);
21
+ let server,browser,serverLog='',interrupted=false,createdHarness=false,createdEntry=false,createdDirectory=false;
22
+ const signals=()=>{interrupted=true;server?.kill('SIGTERM');};
23
+ process.once('SIGINT',signals);process.once('SIGTERM',signals);
24
+ const report={schemaVersion:1,status:'FAIL',template:project.template,ui:project.ui,platform:process.platform,startedAt:new Date().toISOString(),cases:[],failures:[]};
25
+ function compare(expected,actual,path='') {
26
+ if(typeof expected==='number'&&typeof actual==='number')return Math.abs(expected-actual)<=design.widgets.acceptance.positionTolerance?[]:[`${path}: ${expected} != ${actual}`];
27
+ if(expected===null||actual===null||typeof expected!=='object'||typeof actual!=='object')return expected===actual?[]:[`${path}: ${JSON.stringify(expected)} != ${JSON.stringify(actual)}`];
28
+ const keys=new Set([...Object.keys(expected),...Object.keys(actual)]);
29
+ return [...keys].flatMap(key=>compare(expected[key],actual[key],path?path+'.'+key:key));
30
+ }
31
+ async function collect(page){return page.evaluate(()=>{
32
+ const selectors='[data-widget],[data-part],[data-region],h1,h2,h3,p,label,button,input,textarea,a';
33
+ const elements=[...document.querySelectorAll(selectors)].filter(el=>el.getClientRects().length&&getComputedStyle(el).display!=='none');
34
+ const styles=['color','backgroundColor','borderTopColor','borderRightColor','borderBottomColor','borderLeftColor','borderTopLeftRadius','borderTopRightRadius','borderBottomLeftRadius','borderBottomRightRadius','borderTopWidth','borderRightWidth','borderBottomWidth','borderLeftWidth','fontFamily','fontSize','fontWeight','lineHeight','letterSpacing','paddingTop','paddingRight','paddingBottom','paddingLeft','opacity','outlineColor','outlineWidth','outlineStyle','outlineOffset','boxShadow'];
35
+ const box=rect=>({x:rect.x,y:rect.y,width:rect.width,height:rect.height});
36
+ return elements.map(el=>{
37
+ const css=getComputedStyle(el),walker=document.createTreeWalker(el,NodeFilter.SHOW_TEXT),text=[];
38
+ let node;while((node=walker.nextNode()))if(node.textContent.trim()){const range=document.createRange();range.selectNodeContents(node);text.push({value:node.textContent,box:box(range.getBoundingClientRect())});}
39
+ return {role:el.getAttribute('data-widget')||el.getAttribute('data-part')||el.getAttribute('data-region')||el.tagName.toLowerCase(),box:box(el.getBoundingClientRect()),styles:Object.fromEntries(styles.map(name=>[name,css[name]])),text,icon:el.tagName.toLowerCase()==='svg'?{viewBox:el.getAttribute('viewBox'),stroke:el.getAttribute('stroke'),strokeWidth:el.getAttribute('stroke-width'),linecap:el.getAttribute('stroke-linecap'),linejoin:el.getAttribute('stroke-linejoin'),paths:[...el.querySelectorAll('path')].map(path=>path.getAttribute('d'))}:null};
40
+ });
41
+ });}
42
+ async function checkSpecification(page){
43
+ const failures=await page.evaluate(design=>{
44
+ const failures=[],colors=design.tokens.colors;
45
+ const normal=(property,value)=>{const probe=document.createElement('span');probe.style.borderStyle='solid';probe.style[property]=String(value);document.body.append(probe);const result=getComputedStyle(probe)[property];probe.remove();return result;};
46
+ for(const element of document.querySelectorAll('[data-widget]')){
47
+ const name=element.getAttribute('data-widget'),spec=design.widgets.widgets[name];
48
+ if(!spec){failures.push('Undeclared rendered widget '+name);continue;}
49
+ for(const part of spec.parts)if(part!=='root'&&!element.querySelector(`[data-part="${part}"]`))failures.push(name+' missing part '+part);
50
+ const target=name==='field'?element.querySelector('[data-part="label"]'):element,css=getComputedStyle(target);
51
+ const map={height:'height',fontSize:'fontSize',fontWeight:'fontWeight',lineHeight:'lineHeight',radius:'borderTopLeftRadius',borderWidth:'borderTopWidth',paddingX:'paddingLeft',paddingY:'paddingTop'};
52
+ for(const [key,property] of Object.entries(map))if(spec.styles[key]!==undefined){const expected=normal(property,spec.styles[key]);if(css[property]!==expected)failures.push(name+'.'+property+': expected '+expected+', got '+css[property]);}
53
+ const assertColor=(property,value)=>{const expected=normal(property,value);if(css[property]!==expected)failures.push(name+'.'+property+': expected '+expected+', got '+css[property]);};
54
+ if(name==='button'){
55
+ const variant=element.getAttribute('data-variant'),hover=element.matches(':hover'),active=element.matches(':active');
56
+ const key=variant==='primary'?(active?'primary-active':hover?'primary-hover':'primary'):variant==='secondary'?(hover||active?'secondary-hover':'control-background'):(hover||active?'danger-hover':'danger-background');
57
+ assertColor('backgroundColor',colors[key]);assertColor('color',colors[variant+'-text']);
58
+ }
59
+ if(name==='input'||name==='textarea'){assertColor('backgroundColor',colors['control-background']);assertColor('color',colors.text);}
60
+ if(name==='alert'){const variant=element.getAttribute('data-variant');assertColor('backgroundColor',colors[variant+'-background']);assertColor('color',colors[variant+'-text']);
61
+ const icon=element.querySelector('[data-part="icon"]'),expected=design.widgets.icons[variant];
62
+ if(icon?.getAttribute('viewBox')!==expected.viewBox||icon?.getAttribute('stroke-width')!==String(expected.strokeWidth)||JSON.stringify([...icon.querySelectorAll('path')].map(path=>path.getAttribute('d')))!==JSON.stringify(expected.paths))failures.push('Icon geometry differs from declared '+variant);
63
+ }
64
+ if(name==='panel')assertColor('backgroundColor',colors.surface);
65
+ const rootCss=getComputedStyle(element);
66
+ const dimension=(node,property,value)=>{const actual=getComputedStyle(node)[property],expected=normal(property,value);if(actual!==expected)failures.push(name+'.'+property+': expected '+expected+', got '+actual);};
67
+ if(spec.styles.gap)dimension(element,'gap',spec.styles.gap);
68
+ if(spec.styles.marginY){dimension(element,'marginTop',spec.styles.marginY);dimension(element,'marginBottom',spec.styles.marginY);}
69
+ if(spec.styles.padding)for(const side of ['Top','Right','Bottom','Left'])dimension(element,'padding'+side,innerWidth<=parseFloat(design.tokens.spacing['mobile-breakpoint'])?design.tokens.spacing.s20:spec.styles.padding);
70
+ if(spec.styles.iconSize){const icon=element.querySelector('[data-part="icon"]');dimension(icon,'width',spec.styles.iconSize);dimension(icon,'height',spec.styles.iconSize);}
71
+ if(element.matches(':focus-visible')&&name==='button'||element.matches(':focus')&&['input','textarea'].includes(name)){
72
+ dimension(element,'outlineWidth','2px');if(rootCss.outlineStyle!=='solid')failures.push(name+' missing focus outline');assertColor('outlineColor',colors['control-focus']);
73
+ }
74
+ if(name==='input'||name==='textarea')assertColor('borderTopColor',colors[element.getAttribute('aria-invalid')==='true'?'error-border':element.matches(':hover')?'control-focus':'control-border']);
75
+ if(element.matches(':disabled')&&rootCss.opacity!=='0.5')failures.push(name+' missing disabled appearance');
76
+ }
77
+ return failures;
78
+ },design);
79
+ if(failures.length)throw new Error(failures.slice(0,12).join('\n'));
80
+ }
81
+
82
+ async function interact(page,id){
83
+ const [widget,,state]=id.split('.');if(widget==='page')return;
84
+ const control=page.locator(`[data-widget="${widget}"]`).first();
85
+ if(state==='hover'||state==='active'){await control.hover();if(state==='active')await page.mouse.down();}
86
+ if(state==='focus'){await page.keyboard.press('Tab');if(!await control.evaluate(el=>el===document.activeElement))throw new Error(id+' did not receive focus');}
87
+ if(state==='disabled'||state==='loading'){if(!await control.isDisabled())throw new Error(id+' must disable interaction');}
88
+ if(state==='loading'&&await control.getAttribute('aria-busy')!=='true')throw new Error('Loading state is not exposed');
89
+ if(state==='invalid'&&await control.getAttribute('aria-invalid')!=='true')throw new Error('Invalid state is not exposed');
90
+ if(widget==='input'&&id.split('.')[1]==='password'&&await control.getAttribute('type')!=='password')throw new Error('Password must stay masked');
91
+ }
92
+ async function checkKeyboard(page,id){
93
+ const [widget,,state]=id.split('.'),control=page.locator(`[data-widget="${widget}"]`).first();
94
+ if(widget==='button'&&state==='default'){
95
+ await control.focus();await page.keyboard.press('Enter');await page.keyboard.press('Space');
96
+ if(await page.locator('[data-events]').getAttribute('data-events')!=='2')throw new Error('Button must activate once per Enter/Space');
97
+ }
98
+ if((widget==='input'||widget==='textarea')&&state==='readonly'){
99
+ const before=await control.inputValue();await control.focus();await page.keyboard.type('x');if(await control.inputValue()!==before)throw new Error('Readonly value changed');
100
+ }
101
+ }
102
+ try {
103
+ await syncDesign(root,true);
104
+ if(next)for(const file of ['tsconfig.json','next-env.d.ts'])nextFiles.set(file,await readFile(join(root,'web',file)));
105
+ validateFixtureCoverage(design);
106
+ const digest=createHash('sha256');for(const file of ['design/tokens.json','design/widgets.json','design/structure.json','design/layout.css','design/widgets.css','web/src/ui.tsx','web/src/App.tsx','web/src/workspace-model.ts','tests/design/harness.tsx','scripts/design-verify.mjs','scripts/design-coverage.mjs','scripts/design-next-cleanup.mjs','scripts/design.mjs','web/src/structure-view.tsx','web/src/widget-icon.tsx','web/src/widget-types.ts','web/src/styles.css','web/src/design-tokens.css','web/src/design-layout.css','web/src/design-widgets.css','web/src/design-runtime.json','project.json',...(next?['web/next.config.ts']:[]),...(project.ui==='shadcn'?['web/src/tailwind.css']:[])])digest.update(file).update(await readFile(join(root,file)));report.sourceDigest=digest.digest('hex');
107
+ await writeFile(harness,await readFile(join(root,'tests/design/harness.tsx')),{flag:'wx'});createdHarness=true;
108
+ if(next){await mkdir(dirname(entry));createdDirectory=true;await writeFile(entry,`export {default} from '../../src/scaffold-design-check-${nonce}';\n`,{flag:'wx'});}
109
+ else await writeFile(entry,`<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body><div id="root"></div><script type="module">import React from 'react';import{createRoot}from'react-dom/client';import Harness from './src/scaffold-design-check-${nonce}.tsx';createRoot(document.getElementById('root')).render(React.createElement(Harness));</script></body></html>`,{flag:'wx'});
110
+ createdEntry=true;
111
+ const port=await new Promise((yes,no)=>{const socket=createServer();socket.on('error',no);socket.listen(0,'127.0.0.1',()=>{const port=socket.address().port;socket.close(()=>yes(port));});});
112
+ const web=join(root,'web'),webRequire=createRequire(join(web,'package.json'));
113
+ const executable=next?webRequire.resolve('next/dist/bin/next'):resolve(dirname(webRequire.resolve('vite/package.json')),'bin/vite.js');
114
+ server=spawn(process.execPath,[executable,...(next?['dev','--webpack','--hostname','127.0.0.1','--port',String(port)]:['--host','127.0.0.1','--port',String(port),'--strictPort'])],{cwd:web,env:{...process.env,NEXT_TELEMETRY_DISABLED:'1',...(next?{SCAFFOLD_DESIGN_DIST_DIR:nextDist}:{})},stdio:['ignore','pipe','pipe']});
115
+ for(const stream of [server.stdout,server.stderr])stream.on('data',data=>{serverLog=(serverLog+data).slice(-50000);});
116
+ server.on('error',error=>{serverLog+=error.message;});
117
+ const base=`http://127.0.0.1:${port}/scaffold-design-check-${nonce}${next?'':'.html'}`;
118
+ let ready=false;for(let attempt=0;attempt<120;attempt++){if(interrupted)throw new Error('Interrupted');if(server.exitCode!==null)throw new Error('Fixture server exited');try{const response=await fetch(base);if(response.ok){ready=true;break;}}catch{}await new Promise(resolve=>setTimeout(resolve,500));}
119
+ if(!ready)throw new Error('Fixture server did not become ready');
120
+ browser=await chromium.launch({headless:true});report.browser=browser.version();
121
+ const cases=Object.entries(design.widgets.widgets).flatMap(([name,spec])=>spec.variants.flatMap(variant=>spec.states.map(state=>`${name}.${variant}.${state}`))).concat(design.structure.scenarios.map(name=>'page.'+name));
122
+ report.expectedCases=cases.length*design.widgets.acceptance.viewports.length;
123
+ for(const viewport of design.widgets.acceptance.viewports){
124
+ const context=await browser.newContext({viewport,deviceScaleFactor:1,reducedMotion:'reduce',locale:'zh-CN',timezoneId:'UTC'});
125
+ const page=await context.newPage();
126
+ for(const id of cases){
127
+ if(interrupted)throw new Error('Interrupted');
128
+ const measurements={};let failure;
129
+ try{
130
+ for(const mode of ['reference','actual']){
131
+ await page.mouse.move(viewport.width-1,viewport.height-1);await page.goto(base+`?case=${encodeURIComponent(id)}&mode=${mode}`,{waitUntil:'networkidle'});
132
+ await page.locator(`[data-design-ready="${id}"]`).waitFor();await page.evaluate(()=>document.fonts.ready);
133
+ await interact(page,id);await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));await checkSpecification(page);measurements[mode]=await collect(page);await page.mouse.up();await checkKeyboard(page,id);
134
+ }
135
+ const differences=compare(measurements.reference,measurements.actual);if(differences.length)throw new Error(differences.slice(0,12).join('\n'));
136
+ if(!measurements.actual.length)throw new Error('No measurable contract elements');
137
+ }catch(error){failure=error.message;report.failures.push({id,viewport,failure});await page.screenshot({path:join(output,`${viewport.width}-${id}.png`),fullPage:true}).catch(()=>{});}
138
+ report.cases.push({id,viewport,status:failure?'FAIL':'PASS',...(failure?{failure}:{}),measurements});
139
+ }
140
+ await context.close();
141
+ }
142
+ if(report.cases.length!==report.expectedCases)throw new Error('Incomplete declared coverage');
143
+ report.status=report.failures.length?'FAIL':'PASS';
144
+ }catch(error){report.failures.push({failure:error.message});}
145
+ finally{
146
+ await browser?.close();server?.kill('SIGTERM');
147
+ if(server&&server.exitCode===null)await Promise.race([new Promise(resolve=>server.once('exit',resolve)),new Promise(resolve=>setTimeout(()=>{server.kill('SIGKILL');resolve();},3000))]);
148
+ if(createdHarness)await rm(harness,{force:true});if(createdDirectory)await rm(dirname(entry),{recursive:true,force:true});else if(createdEntry)await rm(entry,{force:true});
149
+ if(next){await rm(join(root,'web',nextDist),{recursive:true,force:true});for(const [file,content] of nextFiles){const path=join(root,'web',file),current=await readFile(path,'utf8');const cleaned=cleanNextFixtureConfig(file,current,content.toString(),nextDist);if(cleaned!==current)await writeFile(path,cleaned);}}
150
+ report.finishedAt=new Date().toISOString();await writeFile(join(output,'result.json'),JSON.stringify(report,null,2)+'\n');await writeFile(join(output,'server.log'),serverLog);
151
+ process.removeListener('SIGINT',signals);process.removeListener('SIGTERM',signals);
152
+ }
153
+ console.log(JSON.stringify({status:report.status,template:project.template,ui:project.ui,expectedCases:report.expectedCases,completedCases:report.cases.length,failures:report.failures.slice(0,8),report:'reports/design-check/result.json'},null,2));
154
+ if(report.status!=='PASS')process.exitCode=1;
@@ -0,0 +1,108 @@
1
+ import {readFile, writeFile, mkdir, lstat, realpath} from 'node:fs/promises';
2
+ import {realpathSync} from 'node:fs';
3
+ import {resolve, dirname, join, relative, sep} from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+
6
+ const sections=['Overview','Colors','Typography','Layout','Elevation & Depth','Shapes','Components',"Do's and Don'ts"];
7
+ const generated=['DESIGN.md','web/src/design-tokens.css','web/src/design-layout.css','web/src/design-widgets.css','web/src/design-runtime.json'];
8
+ const variable=path=>'--design-'+path.split('.').map(part=>part.replace(/[A-Z]/g,c=>'-'+c.toLowerCase())).join('-');
9
+ function leaves(value,path='') {
10
+ return Object.entries(value).flatMap(([key,child])=>{
11
+ if(!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(key))throw new Error('Invalid design token key: '+key);
12
+ const at=path?path+'.'+key:key;
13
+ if(child && typeof child==='object' && !Array.isArray(child))return leaves(child,at);
14
+ if(typeof child!=='string' && (typeof child!=='number'||!Number.isFinite(child)))throw new Error('Invalid token value: '+at);
15
+ return [[at,child]];
16
+ });
17
+ }
18
+ function resolver(tokens) {
19
+ const flat=new Map(leaves(tokens));
20
+ function get(path,seen=[]) {
21
+ if(!flat.has(path))throw new Error('Unknown design token reference: '+path);
22
+ if(seen.includes(path))throw new Error('Cyclic design token reference: '+path);
23
+ const value=flat.get(path);
24
+ const match=typeof value==='string' && /^\{([\w.-]+)\}$/.exec(value);
25
+ if(match)return get(match[1],[...seen,path]);
26
+ if(typeof value==='string' && /[{};\n\r<>]/.test(value))throw new Error('Unsafe or partial design token reference: '+path);
27
+ return value;
28
+ }
29
+ for(const path of flat.keys())get(path);
30
+ return {flat,get};
31
+ }
32
+ function yaml(value,indent=0) {
33
+ return Object.entries(value).map(([key,child])=>' '.repeat(indent)+key+':'+(child && typeof child==='object'
34
+ ? '\n'+yaml(child,indent+2) : ' '+JSON.stringify(child))).join('\n');
35
+ }
36
+ function resolveTree(value,get) {
37
+ if(Array.isArray(value))return value.map(child=>resolveTree(child,get));
38
+ if(value && typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,child])=>[key,resolveTree(child,get)]));
39
+ const match=typeof value==='string' && /^\{([\w.-]+)\}$/.exec(value);
40
+ return match?get(match[1]):value;
41
+ }
42
+ export async function designFiles(root) {
43
+ const source=JSON.parse(await readFile(join(root,'design/tokens.json'),'utf8'));
44
+ const widgets=JSON.parse(await readFile(join(root,'design/widgets.json'),'utf8'));
45
+ const structure=JSON.parse(await readFile(join(root,'design/structure.json'),'utf8'));
46
+ if(widgets.schemaVersion!==1||structure.schemaVersion!==1)throw new Error('Unsupported design specification version');
47
+ for(const [name,spec] of Object.entries(widgets.widgets)) {
48
+ for(const key of ['parts','variants','states'])if(!Array.isArray(spec[key])||!spec[key].length||new Set(spec[key]).size!==spec[key].length)throw new Error('Invalid widget '+name+'.'+key);
49
+ }
50
+ const tokens=source.tokens;
51
+ const {flat,get}=resolver({...tokens,effects:source.effects,widgets:Object.fromEntries(Object.entries(widgets.widgets).map(([name,spec])=>[name,spec.styles]))});
52
+ const substitute=text=>text.replace(/\{\{([\w.-]+)\}\}/g,(_,path)=>String(get(path)));
53
+ const guide=substitute(await readFile(join(root,'design/guide.md'),'utf8'));
54
+ if(JSON.stringify([...guide.matchAll(/^## (.+)$/gm)].map(m=>m[1]))!==JSON.stringify(sections))throw new Error('Design guide must contain the eight ordered sections');
55
+ const cssEntries=[...flat].filter(([path])=>!['version','name','description'].includes(path));
56
+ const variables=new Set(cssEntries.map(([path])=>variable(path)));
57
+ if(variables.size!==cssEntries.length)throw new Error('Design token names produce colliding CSS variables');
58
+ const css='/* Generated by design:sync. Edit design/tokens.json or design/widgets.json. */\n:root {\n'+cssEntries.map(([path])=>` ${variable(path)}: ${get(path)};`).join('\n')+'\n}\n';
59
+ const layout=substitute(await readFile(join(root,'design/layout.css'),'utf8'));
60
+ const widgetCss=substitute(await readFile(join(root,'design/widgets.css'),'utf8'));
61
+ for(const match of (layout+widgetCss).matchAll(/var\(\s*(--design-[\w-]+)\s*(?:,|\))/g))if(!variables.has(match[1]))throw new Error('Unknown CSS design variable: '+match[1]);
62
+ return new Map([
63
+ [generated[0],'---\n'+yaml(tokens)+'\n---\n\n'+guide],
64
+ [generated[1],css],
65
+ [generated[2],'/* Generated by design:sync. Edit design/layout.css. */\n'+layout],
66
+ [generated[3],'/* Generated by design:sync. Edit design/widgets.css. */\n'+widgetCss],
67
+ [generated[4],JSON.stringify({tokens:resolveTree(tokens,get),effects:resolveTree(source.effects,get),widgets:resolveTree(widgets,get),structure},null,2)+'\n'],
68
+ ]);
69
+ }
70
+ async function validateOutputPaths(root,paths) {
71
+ for(const path of paths) {
72
+ const file=join(root,path);
73
+ if(relative(root,file).split(sep).includes('..'))throw new Error('Design output escapes project');
74
+ let current=root;
75
+ for(const segment of path.split('/')) {
76
+ current=join(current,segment);
77
+ try {
78
+ const info=await lstat(current);
79
+ if(info.isSymbolicLink())throw new Error('Refusing design symlink: '+relative(root,current));
80
+ if(current!==file&&!info.isDirectory())throw new Error('Design output parent is not a directory');
81
+ }catch(error){if(error.code!=='ENOENT')throw error;}
82
+ }
83
+ }
84
+ }
85
+ export async function syncDesign(root,check=false) {
86
+ root=await realpath(root);
87
+ const files=await designFiles(root);
88
+ // Check every ancestor before the first write so failure cannot partly overwrite outputs.
89
+ await validateOutputPaths(root,files.keys());
90
+ for(const [path,content] of files) {
91
+ const file=join(root,path);
92
+ if(check) {
93
+ let actual;try{actual=await readFile(file,'utf8');}catch{throw new Error('Missing generated design file: '+path);}
94
+ if(actual!==content)throw new Error('Design drift: '+path+'; run npm run design:sync');
95
+ }else{
96
+ await mkdir(dirname(file),{recursive:true});
97
+ await writeFile(file,content);
98
+ }
99
+ }
100
+ }
101
+ if(process.argv[1] && realpathSync(process.argv[1])===fileURLToPath(import.meta.url)) {
102
+ try {
103
+ const command=process.argv[2];
104
+ if(!['sync','check'].includes(command))throw new Error('Use: node scripts/design.mjs sync|check');
105
+ await syncDesign(resolve(dirname(fileURLToPath(import.meta.url)),'..'),command==='check');
106
+ console.log(command==='check'?'Design documentation and UI outputs match their source.':'Generated DESIGN.md and UI design outputs.');
107
+ }catch(error){console.error(error.message);process.exitCode=1;}
108
+ }
@@ -0,0 +1,120 @@
1
+ import { spawn, execFileSync } from 'node:child_process';
2
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { randomBytes } from 'node:crypto';
6
+ const root=resolve(dirname(fileURLToPath(import.meta.url)),'..');
7
+ process.chdir(root);
8
+ const project=JSON.parse(readFileSync('project.json','utf8'));
9
+ const stack=project.template, next=stack==='next-fullstack', fast=stack==='react-fastapi';
10
+ if(process.env.SCAFFOLD_TEST!=='1' && existsSync('.env')) process.loadEnvFile('.env');
11
+ const children=new Set();
12
+ let stopping=false;
13
+ function stop(signal='SIGTERM') { stopping=true;for(const c of children)c.kill(signal); }
14
+ for(const sig of ['SIGTERM','SIGINT'])process.on(sig,()=>{stop(sig);process.exitCode=sig==='SIGINT'?130:143;});
15
+ async function run(program,args,cwd='.',extra={}) {
16
+ if(stopping)throw new Error('Interrupted');
17
+ return new Promise((yes,no)=>{
18
+ const c=spawn(program,args,{cwd:resolve(root,cwd),stdio:'inherit',env:{...process.env,...extra}});children.add(c);
19
+ c.on('error',e=>{children.delete(c);no(e);});
20
+ c.on('exit',(code,signal)=>{children.delete(c);code===0?yes():no(new Error(`${program} failed (${code??signal})`));});
21
+ });
22
+ }
23
+ function env() {
24
+ for(const key of ['DATABASE_URL','AUTH_SECRET','APP_ORIGIN']) if(!process.env[key])throw new Error(`Missing ${key}; run dev:setup or provide an isolated test environment.`);
25
+ if(process.env.AUTH_SECRET.length<32)throw new Error('AUTH_SECRET must contain at least 32 characters.');
26
+ }
27
+ async function migrationGuard() {
28
+ env();
29
+ if(process.env.SCAFFOLD_TEST==='1') {
30
+ const context=JSON.parse(readFileSync(process.env.SCAFFOLD_TEST_CONTEXT,'utf8'));
31
+ const u=new URL(process.env.DATABASE_URL);
32
+ if(context.databaseUrl!==process.env.DATABASE_URL || u.hostname!=='127.0.0.1' || !/^[a-f0-9]{64}$/.test(context.containerId||'') || !/^[a-f0-9-]{36}$/.test(context.runId||''))throw new Error('Test database identity mismatch.');
33
+ // Inspect privately: Docker configuration contains the ephemeral database password.
34
+ const actual=JSON.parse(execFileSync('docker',['inspect','--format','{{json .}}',context.containerId],{encoding:'utf8',stdio:['ignore','pipe','pipe']}));
35
+ const ports=actual.NetworkSettings?.Ports?.['5432/tcp']||[];
36
+ const variables=actual.Config?.Env||[];
37
+ if(actual.Id!==context.containerId || actual.Config?.Labels?.['agent-scaffold.run']!==context.runId || actual.State?.Running!==true || ports.length!==1 || ports[0].HostIp!=='127.0.0.1' || ports[0].HostPort!==u.port || u.pathname!=='/app' || decodeURIComponent(u.username)!=='app' || !variables.includes('POSTGRES_USER=app') || !variables.includes('POSTGRES_DB=app') || !variables.includes('POSTGRES_PASSWORD='+decodeURIComponent(u.password)))throw new Error('Test database identity mismatch.');
38
+ }
39
+ }
40
+ async function migrate() {
41
+ await migrationGuard();
42
+ if(fast)await run('uv',['run','--frozen','alembic','upgrade','head'],'backend');
43
+ else if(stack==='react-spring')await run('./mvnw',['-q','-DskipTests','spring-boot:run','-Dspring-boot.run.arguments=--scaffold.migrate-only=true'],'backend',jdbc());
44
+ else await run('pnpm',['exec','prisma','migrate','deploy'],next?'web':'backend');
45
+ }
46
+ function jdbc() {
47
+ const u=new URL(process.env.DATABASE_URL);
48
+ return {SESSION_COOKIE_SECURE:String(process.env.APP_ORIGIN.startsWith('https://')),SPRING_DATASOURCE_URL:`jdbc:postgresql://${u.hostname}:${u.port||5432}${u.pathname}`,SPRING_DATASOURCE_USERNAME:decodeURIComponent(u.username),SPRING_DATASOURCE_PASSWORD:decodeURIComponent(u.password)};
49
+ }
50
+ async function seed() {
51
+ await migrationGuard();
52
+ for(const k of ['SEED_A_EMAIL','SEED_B_EMAIL','SEED_PASSWORD']) if(!process.env[k])throw new Error(`Missing explicit ${k}`);
53
+ if(fast)await run('uv',['run','--frozen','python','-m','app.seed'],'backend');
54
+ else if(stack==='react-spring')await run('./mvnw',['-q','-DskipTests','spring-boot:run','-Dspring-boot.run.arguments=--scaffold.seed-only=true'],'backend',jdbc());
55
+ else await run('pnpm',['exec','tsx','scripts/seed.ts'],next?'web':'backend',{SCAFFOLD_SEED:'1'});
56
+ }
57
+ async function serve() {
58
+ env();
59
+ const tasks=[];
60
+ if(fast)tasks.push(run('uv',['run','--frozen','uvicorn','app.main:app','--host','127.0.0.1','--port',process.env.PORT||'8000'],'backend'));
61
+ if(stack==='react-spring')tasks.push(run('java',['-jar','target/backend.jar'],'backend',jdbc()));
62
+ if(stack==='react-nest')tasks.push(run('node',['dist/main.js'],'backend'));
63
+ if(next)tasks.push(run('pnpm',['exec','next','start','--hostname','127.0.0.1','--port',process.env.WEB_PORT||'3000'],'web'));
64
+ else tasks.push(run(fast?'bun':'pnpm',['run','preview','--host','127.0.0.1','--port',process.env.WEB_PORT||'5173','--strictPort'],'web'));
65
+ try {await Promise.all(tasks);} finally {stop();}
66
+ }
67
+ async function install() {
68
+ await run('npm',['ci']);
69
+ await run(fast?'bun':'pnpm',fast?['install','--frozen-lockfile']:['install','--frozen-lockfile'],'web');
70
+ if(fast)await run('uv',['sync','--frozen'],'backend');
71
+ else if(stack==='react-nest')await run('pnpm',['install','--frozen-lockfile'],'backend');
72
+ else if(stack==='react-spring')await run('./mvnw',['-q','-DskipTests','compile'],'backend');
73
+ if(next||stack==='react-nest')await run('pnpm',['exec','prisma','generate'],next?'web':'backend');
74
+ await run('npx',['--no-install','playwright','install','chromium']);
75
+ }
76
+ async function setup() {
77
+ if(process.env.SCAFFOLD_TEST==='1')throw new Error('dev:setup cannot run in test mode.');
78
+ if(!existsSync('.env')) {
79
+ const db=randomBytes(24).toString('hex'), secret=randomBytes(32).toString('hex'), pass=randomBytes(16).toString('hex');
80
+ const web=process.env.WEB_PORT||String(next?3000:5173), dbPort=process.env.POSTGRES_PORT||'54329';
81
+ const vars={POSTGRES_USER:'app',POSTGRES_DB:'app',POSTGRES_PASSWORD:db,POSTGRES_PORT:dbPort,DATABASE_URL:`postgresql://app:${db}@127.0.0.1:${dbPort}/app`,AUTH_SECRET:secret,APP_ORIGIN:`http://localhost:${web}`,PORT:process.env.PORT||'8000',WEB_PORT:String(web),SEED_A_EMAIL:'alice@example.test',SEED_B_EMAIL:'bob@example.test',SEED_PASSWORD:pass};
82
+ writeFileSync('.env',Object.entries(vars).map(([k,v])=>`${k}=${v}`).join('\n')+'\n',{flag:'wx',mode:0o600});
83
+ writeFileSync('.scaffold-local-credentials',`Local development only\nEmail: ${vars.SEED_A_EMAIL}\nPassword: ${pass}\n`,{flag:'wx',mode:0o600});
84
+ process.loadEnvFile('.env');
85
+ }
86
+ await run('docker',['compose','up','-d','--wait','db']);await migrate();await seed();
87
+ console.log('Development database ready. Credentials are in .scaffold-local-credentials (ignored by Git).');
88
+ }
89
+ const command=process.argv[2];
90
+ try {
91
+ if(command!=='api:check')mkdirSync('reports',{recursive:true});
92
+ if(command==='install')await install();
93
+ else if(command==='setup'){await install();await setup();}
94
+ else if(command==='check'){for(const task of ['api:check','typecheck','lint','build'])await run('node',['scripts/project.mjs',task]);}
95
+ else if(command==='dev:setup')await setup();
96
+ else if(command==='migrate')await migrate();
97
+ else if(command==='seed')await seed();
98
+ else if(command==='serve')await serve();
99
+ else if(command==='dev') {await run('node',['scripts/project.mjs','build']);await serve();}
100
+ else if(command==='build'||command==='typecheck'||command==='lint') {
101
+ await run('node',['scripts/design.mjs','check']);
102
+ if(next||stack==='react-nest')await run('pnpm',['exec','prisma','generate'],next?'web':'backend');
103
+ await run(fast?'bun':'pnpm',['run',command],'web');
104
+ if(fast) {
105
+ if(command==='build')await run('uv',['run','--frozen','python','-m','compileall','-q','app'],'backend');
106
+ if(command==='typecheck')await run('uv',['run','--frozen','mypy','app'],'backend');
107
+ if(command==='lint')await run('uv',['run','--frozen','ruff','check','app','tests'],'backend');
108
+ } else if(stack==='react-nest')await run('pnpm',['run',command],'backend');
109
+ else if(stack==='react-spring')await run('./mvnw',['-q','-DskipTests',command==='build'?'package':'compile'],'backend');
110
+ } else if(command==='test:native') {
111
+ await migrationGuard();
112
+ if(fast)await run('uv',['run','--frozen','python','-m','pytest','--junitxml='+resolve(process.env.SCAFFOLD_REPORT_DIR||'reports','native.xml'),'-q'],'backend');
113
+ else if(stack==='react-spring')await run('./mvnw',['-q','test','-Dscaffold.reports='+resolve(process.env.SCAFFOLD_REPORT_DIR||'reports','spring')],'backend',jdbc());
114
+ else if(stack==='react-nest')await run('pnpm',['run','test'],'backend');
115
+ else await run('pnpm',['run','test'],'web');
116
+ } else if(command==='test:api'||command==='test:e2e')await run('npx',['--no-install','playwright','test',command==='test:api'?'tests/contract.spec.ts':'tests/ui.spec.ts']);
117
+ else if(command==='api:check')await run('node',['scripts/api-check.mjs']);
118
+ else if(command==='api:generate')await run('npx',['--no-install','openapi-typescript','docs/openapi.json','-o','docs/api.d.ts']);
119
+ else throw new Error('Use setup, install, dev:setup, dev, check, build, typecheck, lint, migrate, seed, test:native, test:api, test:e2e, api:check, api:generate.');
120
+ } catch(e) {stop();console.error(e instanceof Error?e.message:'Project command failed');process.exitCode=process.exitCode||1;}
@@ -0,0 +1,20 @@
1
+ import {request as raw,type APIRequestContext,type APIResponse} from '@playwright/test';
2
+ import document from '../docs/openapi.json' with {type:'json'};
3
+ import {responseValidator} from '../scripts/api-contract.mjs';
4
+ const validate=responseValidator(document);
5
+ export const requests={
6
+ async newContext(options?:Parameters<typeof raw.newContext>[0]):Promise<APIRequestContext>{
7
+ const context=await raw.newContext(options);
8
+ return new Proxy(context,{
9
+ get(target,key){
10
+ const value=Reflect.get(target,key,target);
11
+ if(['get','post','put','delete','patch','head'].includes(String(key)))return async(...args:unknown[])=>{
12
+ const response:APIResponse=await value.apply(target,args);
13
+ validate(String(key),response.url(),response.status(),response.headers(),(await response.body()).toString('utf8'));
14
+ return response;
15
+ };
16
+ return typeof value==='function'?value.bind(target):value;
17
+ }
18
+ });
19
+ }
20
+ };
@@ -0,0 +1,49 @@
1
+ import {requests} from './contract-client';
2
+ import {test,expect,type APIRequestContext} from '@playwright/test';
3
+ import project from '../project.json' with {type:'json'};
4
+ const baseURL=`http://127.0.0.1:${process.env.WEB_PORT||5173}`;
5
+ const origin=process.env.APP_ORIGIN||baseURL;
6
+ async function login(email:string){const context=await requests.newContext({baseURL,extraHTTPHeaders:{Origin:origin}});const res=await context.post('/api/auth/login',{data:{email,password:process.env.SEED_PASSWORD}});expect(res.status()).toBe(200);const auth=await res.json();if(auth.token)await context.dispose();const client:APIRequestContext=auth.token?await requests.newContext({baseURL,extraHTTPHeaders:{Origin:origin,Authorization:`Bearer ${auth.token}`}}):context;return {client,auth};}
7
+ test('HTTP identity, persistent CRUD, pagination and owner isolation',async()=>{
8
+ const health=await requests.newContext({baseURL});expect((await health.get('/api/health')).status()).toBe(200);await health.dispose();
9
+ const anonymous=await requests.newContext({baseURL,extraHTTPHeaders:{Origin:origin}});
10
+ expect((await anonymous.get('/api/records')).status()).toBe(401);
11
+ expect((await anonymous.post('/api/records',{data:{title:'x',content:''}})).status()).toBe(401);
12
+ expect((await anonymous.post('/api/auth/login',{data:{email:process.env.SEED_A_EMAIL,password:'invalid-password'}})).status()).toBe(401);
13
+ const {client:a}=await login(process.env.SEED_A_EMAIL!);const {client:b}=await login(process.env.SEED_B_EMAIL!);expect((await a.get('/api/auth/me')).status()).toBe(200);
14
+ const body=process.env.TEST_BODY_MARKER||'synthetic-content';
15
+ const res=await a.post('/api/records',{data:{title:' Contract record ',content:body}});expect(res.status()).toBe(201);const row=await res.json();const path=`/api/records/${row.id}`;
16
+ expect(row.title).toBe('Contract record');expect((await a.get(path)).status()).toBe(200);
17
+ expect((await (await a.get(path)).json()).content).toBe(body);
18
+ expect((await b.get(path)).status()).toBe(404);expect((await b.put(path,{data:{title:'hijacked',content:''}})).status()).toBe(404);expect((await b.delete(path)).status()).toBe(404);
19
+ const other=await (await b.get('/api/records?page=1&pageSize=100')).json();expect(other.items.some((item:{id:string})=>item.id===row.id)).toBe(false);
20
+ const page=await (await a.get('/api/records?page=1&pageSize=1')).json();expect(page.items.length).toBeLessThanOrEqual(1);expect(page.total).toBeGreaterThan(0);expect(page.pageSize).toBe(1);
21
+ const changed=await a.put(path,{data:{title:'Updated title',content:body}});expect(changed.status()).toBe(200);expect(new Date((await changed.json()).updated_at).getTime()).toBeGreaterThanOrEqual(new Date(row.created_at).getTime());
22
+ const relogin=await login(process.env.SEED_A_EMAIL!);expect((await (await relogin.client.get(path)).json()).title).toBe('Updated title');await relogin.client.dispose();
23
+ expect((await a.delete(path)).status()).toBe(204);expect((await a.get(path)).status()).toBe(404);
24
+ const after=await (await a.get('/api/records?page=1&pageSize=100')).json();expect(after.items.some((item:{id:string})=>item.id===row.id)).toBe(false);
25
+ await Promise.all([a.dispose(),b.dispose(),anonymous.dispose()]);
26
+ });
27
+ test('rejects invalid and protected fields without writing data',async()=>{
28
+ const {client:a,auth}=await login(process.env.SEED_A_EMAIL!);
29
+ const count=(await (await a.get('/api/records')).json()).total;
30
+ for(const data of [{title:'',content:''},{title:' ',content:''},{title:'x'.repeat(121),content:''},{title:'x',content:'x'.repeat(10001)},{title:'x',content:'',owner_id:auth.user.id},{title:'x',content:'',created_at:new Date().toISOString()},{title:42,content:''}])expect((await a.post('/api/records',{data})).status()).toBe(422);
31
+ expect((await (await a.get('/api/records')).json()).total).toBe(count);
32
+ for(const query of ['page=0','pageSize=101','page=bad'])expect((await a.get('/api/records?'+query)).status()).toBe(422);
33
+ expect((await a.get('/api/records/not-a-uuid')).status()).toBe(422);
34
+ const unicode=await a.post('/api/records',{data:{title:'😀'.repeat(120),content:''}});expect(unicode.status()).toBe(201);const row=await unicode.json();
35
+ expect((await a.put(`/api/records/${row.id}`,{data:{title:'bad',content:'',owner_id:auth.user.id}})).status()).toBe(422);
36
+ expect((await (await a.get(`/api/records/${row.id}`)).json()).title).toBe('😀'.repeat(120));
37
+ await a.delete(`/api/records/${row.id}`);await a.dispose();
38
+ });
39
+ test('logout and same-origin session protection',async()=>{
40
+ const {client,auth}=await login(process.env.SEED_A_EMAIL!);
41
+ if(auth.mode==='session'){
42
+ const state=await client.storageState();const replay=await requests.newContext({baseURL,storageState:state,extraHTTPHeaders:{Origin:origin}});
43
+ const cross=await requests.newContext({baseURL,storageState:state,extraHTTPHeaders:{Origin:'https://untrusted.example'}});
44
+ expect((await cross.post('/api/records',{data:{title:'csrf',content:''}})).status()).toBe(403);await cross.dispose();
45
+ expect((await client.post('/api/auth/logout',{data:{}})).status()).toBe(204);
46
+ expect((await replay.get('/api/auth/me')).status()).toBe(401);await replay.dispose();
47
+ }else{expect(['react-fastapi','react-nest']).toContain(project.template);expect((await client.post('/api/auth/logout',{data:{}})).status()).toBe(204);}
48
+ await client.dispose();
49
+ });
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import {useEffect,useState} from 'react';
3
+ import type * as React from 'react';
4
+ import {WorkspaceView} from './App';
5
+ import * as actualWidgets from './ui';
6
+ import {WidgetIcon} from './widget-icon';
7
+ import type {WidgetSet} from './widget-types';
8
+ import {createWorkspaceModel,initialWorkspaceState,type WorkspaceState} from './workspace-model';
9
+ import design from './design-runtime.json';
10
+ // Independent semantic HTML oracle; it imports no component library.
11
+ const reference:WidgetSet={
12
+ Button:({children,variant='primary',loading=false,disabled,type='button',...props})=><button {...props} type={type} data-widget="button" data-variant={variant} aria-busy={loading||undefined} disabled={disabled||loading}><span data-part="label">{children}</span></button>,
13
+ Input:({invalid,...props})=><input {...props} data-widget="input" aria-invalid={invalid||undefined}/>,
14
+ TextArea:({invalid,...props})=><textarea {...props} data-widget="textarea" aria-invalid={invalid||undefined}/>,
15
+ Alert:({children,variant='error'})=><div data-widget="alert" data-variant={variant} role={variant==='error'?'alert':'status'}><WidgetIcon kind={variant}/><div data-part="message">{children}</div></div>,
16
+ Badge:({children})=><span data-widget="badge" className="badge"><span data-part="label">{children}</span></span>,
17
+ Panel:({children,className='',...props})=><section {...props} data-widget="panel" className={`panel ${className}`}>{children}</section>,
18
+ Field:({children,label,htmlFor})=><div data-widget="field" className="field"><label data-part="label" htmlFor={htmlFor}>{label}</label><div data-part="control">{children}</div></div>,
19
+ Theme:({children})=><>{children}</>
20
+ };
21
+ const record={id:'record-1',title:'一条用于对齐的记录',content:'相同内容,相同交互。\nSecond line.',created_at:'2026-01-01T00:00:00Z',updated_at:'2026-01-01T00:00:00Z'};
22
+ function scenario(name:string):WorkspaceState {
23
+ const state={...initialWorkspaceState(),checking:false};
24
+ if(name==='checking')return {...state,checking:true};
25
+ if(name==='login')return state;
26
+ if(name==='login-error')return {...state,error:'账号或密码不正确,请重试。'};
27
+ if(name==='login-busy')return {...state,busy:true,email:'user@example.test',password:'sample-password'};
28
+ state.user={id:'user-1',email:'user@example.test'};
29
+ if(name==='workspace-empty')return state;
30
+ if(name==='workspace-loading')return {...state,loading:true};
31
+ state.list={items:[record],total:11,page:1,pageSize:10};
32
+ if(name==='workspace-records')return state;
33
+ if(name==='editor-new')return {...state,editing:true};
34
+ if(name==='editor-existing')return {...state,editing:true,selected:record.id,title:record.title,content:record.content,notice:'已保存'};
35
+ if(name==='editor-error')return {...state,editing:true,selected:record.id,title:record.title,content:record.content,error:'请求失败或超时,输入已保留。'};
36
+ throw new Error('Unimplemented declared page scenario: '+name);
37
+ }
38
+ export default function Harness(){
39
+ const [query,setQuery]=useState<URLSearchParams|null>(null),[events,setEvents]=useState(0);
40
+ useEffect(()=>{setQuery(new URLSearchParams(window.location.search));},[]);
41
+ if(!query)return null;
42
+ const mode=query.get('mode'),id=query.get('case')||'';
43
+ const widgets=mode==='reference'?reference:actualWidgets;
44
+ const {Button,Input,TextArea,Alert,Badge,Panel,Field,Theme}=widgets;
45
+ let content:React.ReactNode;
46
+ if(id.startsWith('page.'))content=<WorkspaceView state={scenario(id.slice(5))} model={createWorkspaceModel(async()=>{throw new Error('Unexpected network call in deterministic fixture');})} widgets={widgets}/>;
47
+ else {
48
+ const [name,variant,state]=id.split('.');
49
+ const spec=design.widgets.widgets[name as keyof typeof design.widgets.widgets];
50
+ if(!spec||!(spec.variants as readonly string[]).includes(variant)||!(spec.states as readonly string[]).includes(state))throw new Error('Undeclared widget fixture: '+id);
51
+ const controlProps={disabled:state==='disabled',readOnly:state==='readonly',invalid:state==='invalid',onChange:()=>setEvents(value=>value+1)};
52
+ if(name==='button')content=<Button variant={variant as 'primary'|'secondary'|'danger'} disabled={state==='disabled'} loading={state==='loading'} onClick={()=>setEvents(value=>value+1)}>{state==='loading'?'正在处理…':'保存记录'}</Button>;
53
+ else if(name==='input')content=<Input {...controlProps} id="control" aria-label="测试输入" type={variant} defaultValue={variant==='email'?'user@example.test':'示例内容 Sample'} autoComplete="off"/>;
54
+ else if(name==='textarea')content=<TextArea {...controlProps} aria-label="测试输入" rows={3} defaultValue={'第一行内容\nSecond line'}/>;
55
+ else if(name==='alert')content=<Alert variant={variant as 'error'|'success'}>操作结果说明,请检查后继续。</Alert>;
56
+ else if(name==='badge')content=<Badge>个人工作空间</Badge>;
57
+ else if(name==='panel')content=<Panel><h2>记录面板</h2><p>内容可读,层级明确。</p></Panel>;
58
+ else if(name==='field')content=<Field label="邮箱" htmlFor="field-control"><Input id="field-control" type="email" defaultValue="user@example.test"/></Field>;
59
+ else throw new Error('Unimplemented declared widget: '+name);
60
+ content=<Theme><div style={{width:320,margin:24}}>{content}</div></Theme>;
61
+ }
62
+ return <div data-design-ready={id} data-design-mode={mode} data-events={events}>{content}</div>;
63
+ }
@@ -0,0 +1,61 @@
1
+ import {test,expect,type Page} from '@playwright/test';
2
+ import project from '../project.json' with {type:'json'};
3
+ async function login(page:Page){await page.goto('/');await expect(page.getByRole('button',{name:'登录',exact:true}).or(page.getByRole('heading',{name:'我的记录',exact:true}))).toBeVisible();if(await page.getByRole('button',{name:'登录',exact:true}).isVisible()){await page.getByLabel('邮箱').fill(process.env.SEED_A_EMAIL!);await page.getByLabel('密码').fill(process.env.SEED_PASSWORD!);await page.getByRole('button',{name:'登录',exact:true}).click();}await expect(page.getByRole('heading',{name:'我的记录',exact:true})).toBeVisible();}
4
+ test('browser CRUD persists, renders text, and keeps input on failures',async({page})=>{
5
+ await login(page);await expect(page).toHaveTitle(project.nameZh);await page.getByRole('button',{name:'新建记录',exact:true}).click();
6
+ const title=`Browser record ${Date.now()}`;const body=process.env.TEST_BODY_MARKER||'browser-content';
7
+ await page.getByLabel('标题',{exact:true}).fill(title);await page.getByLabel('正文',{exact:true}).fill(body+' <script>window.injected=true</script>');
8
+ await page.getByRole('button',{name:'保存记录',exact:true}).click();await expect(page.getByRole('status').filter({hasText:'已保存'})).toContainText('已保存');
9
+ await page.reload();await login(page);await page.getByRole('button',{name:new RegExp(title)}).click();await expect(page.getByLabel('正文',{exact:true})).toHaveValue(body+' <script>window.injected=true</script>');expect(await page.evaluate(()=>Object.hasOwn(window,'injected'))).toBe(false);
10
+ await page.getByLabel('标题',{exact:true}).fill(title+' edited');
11
+ await page.route('**/api/records/*',route=>route.request().method()==='PUT'?route.abort('failed'):route.continue());
12
+ await page.getByRole('button',{name:'保存记录',exact:true}).click();await expect(page.getByRole('alert').filter({hasText:'输入已保留'})).toContainText('输入已保留');await expect(page.getByLabel('标题',{exact:true})).toHaveValue(title+' edited');await expect(page.getByText('已保存',{exact:true})).toHaveCount(0);
13
+ await page.unroute('**/api/records/*');await page.getByRole('button',{name:'保存记录',exact:true}).click();await expect(page.getByRole('status').filter({hasText:'已保存'})).toContainText('已保存');
14
+ page.once('dialog',dialog=>dialog.accept());await page.getByRole('button',{name:'删除记录',exact:true}).click();await expect(page.getByRole('status').filter({hasText:'已删除'})).toContainText('已删除');await expect(page.getByRole('button',{name:new RegExp(title)})).toHaveCount(0);
15
+ await page.getByRole('button',{name:'退出登录',exact:true}).click();await expect(page.getByRole('button',{name:'登录',exact:true})).toBeVisible();
16
+ });
17
+ test('small viewport remains usable',async({page})=>{await page.setViewportSize({width:375,height:812});await login(page);expect(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth)).toBe(true);await page.getByRole('button',{name:'新建记录',exact:true}).click();await expect(page.getByLabel('标题',{exact:true})).toBeVisible();});
18
+ test('expired identity clears the previous account editor before another login',async({page})=>{
19
+ await login(page);
20
+ const title=`Private A ${Date.now()}`;
21
+ await page.getByRole('button',{name:'新建记录',exact:true}).click();
22
+ await page.getByLabel('标题',{exact:true}).fill(title);
23
+ await page.getByLabel('正文',{exact:true}).fill('Private content for account A');
24
+ await page.getByRole('button',{name:'保存记录',exact:true}).click();
25
+ await expect(page.getByRole('status').filter({hasText:'已保存'})).toBeVisible();
26
+ await page.route('**/api/records/*',route=>route.request().method()==='GET'?route.fulfill({status:401,contentType:'application/json',body:JSON.stringify({error:{message:'请重新登录。'}})}):route.continue());
27
+ await page.getByRole('button',{name:title,exact:false}).click();
28
+ await expect(page.getByRole('button',{name:'登录',exact:true})).toBeVisible();
29
+ await page.unroute('**/api/records/*');
30
+ await page.getByLabel('邮箱').fill(process.env.SEED_B_EMAIL!);
31
+ await page.getByLabel('密码').fill(process.env.SEED_PASSWORD!);
32
+ await page.getByRole('button',{name:'登录',exact:true}).click();
33
+ await expect(page.getByRole('heading',{name:'我的记录',exact:true})).toBeVisible();
34
+ await expect(page.getByLabel('正文',{exact:true})).toHaveCount(0);
35
+ await expect(page.getByRole('button',{name:title,exact:false})).toHaveCount(0);
36
+ await expect(page.getByText('Private content for account A',{exact:true})).toHaveCount(0);
37
+ });
38
+ test('rendered design follows tokens across desktop, focus, hover and mobile',async({page})=>{
39
+ await page.goto('/');const button=page.getByRole('button',{name:'登录',exact:true});await expect(button).toBeVisible();
40
+ const token=async(name:string)=>page.evaluate(name=>getComputedStyle(document.documentElement).getPropertyValue('--design-'+name).trim(),name);
41
+ const color=async(name:string)=>page.evaluate(name=>{const probe=document.createElement('span');probe.style.color='var(--design-'+name+')';document.body.append(probe);const value=getComputedStyle(probe).color;probe.remove();return value;},name);
42
+ await expect(page.locator('body')).toHaveCSS('background-color',await color('colors-background'));
43
+ await expect(page.locator('body')).toHaveCSS('font-size',await token('typography-body-font-size'));
44
+ await expect(page.locator('.panel').first()).toHaveCSS('border-radius',await token('components-panel-rounded'));
45
+ await expect(button).toHaveCSS('background-color',await color('components-button-primary-background-color'));
46
+ await expect(button).toHaveCSS('border-radius',await token('components-button-primary-rounded'));
47
+ await expect(button).toHaveCSS('color',await color('components-button-primary-text-color'));
48
+ const input=page.getByLabel('邮箱');
49
+ await expect(input).toHaveCSS('background-color',await color('colors-control-background'));
50
+ await expect(input).toHaveCSS('color',await color('colors-text'));
51
+ await expect(input).toHaveCSS('border-radius',await token('components-input-rounded'));
52
+ const height=(await button.boundingBox())!.height;expect(height).toBeGreaterThanOrEqual(Number.parseFloat(await token('spacing-control-min')));
53
+ await button.hover();await expect(button).toHaveCSS('background-color',await color('colors-primary-hover'));
54
+ await page.getByLabel('邮箱').focus();await expect(page.getByLabel('邮箱')).toHaveCSS('outline-color',await color('colors-control-focus'));await expect(page.getByLabel('邮箱')).toHaveCSS('outline-style','solid');
55
+ await page.emulateMedia({reducedMotion:'reduce'});
56
+ expect(await button.evaluate(el=>getComputedStyle(el).transitionDuration.split(',').every(value=>Number.parseFloat(value)<=0.001))).toBe(true);
57
+ const breakpoint=Number.parseFloat(await token('spacing-mobile-breakpoint'));
58
+ await page.setViewportSize({width:breakpoint+1,height:900});expect((await page.locator('.login-layout').evaluate(el=>getComputedStyle(el).gridTemplateColumns)).split(' ')).toHaveLength(2);
59
+ await page.setViewportSize({width:breakpoint,height:900});expect((await page.locator('.login-layout').evaluate(el=>getComputedStyle(el).gridTemplateColumns)).split(' ')).toHaveLength(1);
60
+ await page.setViewportSize({width:375,height:812});expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true);
61
+ });