create-simone-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +70 -0
  2. package/bin/create-simone-app.ts +6 -0
  3. package/package.json +61 -0
  4. package/src/cli.ts +97 -0
  5. package/src/matrix.ts +39 -0
  6. package/src/options.ts +137 -0
  7. package/src/prompts.ts +108 -0
  8. package/src/scaffold.ts +304 -0
  9. package/src/templates.ts +77 -0
  10. package/templates/api/base/.agents/skills/ai-rules/SKILL.md +12 -0
  11. package/templates/api/base/.agents/skills/plan-feature/SKILL.md +13 -0
  12. package/templates/api/base/.agents/skills/review-diff/SKILL.md +12 -0
  13. package/templates/api/base/.agents/skills/tdd-step/SKILL.md +14 -0
  14. package/templates/api/base/.claude/agents/reviewer.md +20 -0
  15. package/templates/api/base/.claude/hooks/format.sh +22 -0
  16. package/templates/api/base/.claude/settings.json +38 -0
  17. package/templates/api/base/.codex/agents/reviewer.toml +7 -0
  18. package/templates/api/base/.codex/config.toml +10 -0
  19. package/templates/api/base/.codex/hooks.json +12 -0
  20. package/templates/api/base/.env.example +2 -0
  21. package/templates/api/base/.github/workflows/ci.yml +18 -0
  22. package/templates/api/base/.prettierignore +10 -0
  23. package/templates/api/base/.prettierrc +20 -0
  24. package/templates/api/base/CLAUDE.md +1 -0
  25. package/templates/api/base/README.md +43 -0
  26. package/templates/api/base/TASK.md.template +30 -0
  27. package/templates/api/base/_gitignore +8 -0
  28. package/templates/api/base/docs/agents/base.md +54 -0
  29. package/templates/api/base/eslint.config.js +21 -0
  30. package/templates/api/base/openapi.yaml +233 -0
  31. package/templates/api/base/package.json +41 -0
  32. package/templates/api/base/scripts/agents-build.ts +26 -0
  33. package/templates/api/base/scripts/rules-check.ts +48 -0
  34. package/templates/api/base/scripts/rules-sync.ts +35 -0
  35. package/templates/api/base/scripts/rules.config.ts +28 -0
  36. package/templates/api/base/scripts/seed-regen.ts +64 -0
  37. package/templates/api/base/src/data/orders.json +187 -0
  38. package/templates/api/base/src/data/products.json +142 -0
  39. package/templates/api/base/src/data/repository.ts +55 -0
  40. package/templates/api/base/src/data/schemas.ts +44 -0
  41. package/templates/api/base/src/data/users.json +352 -0
  42. package/templates/api/base/src/lib/appName.ts +2 -0
  43. package/templates/api/base/src/lib/http-error.ts +14 -0
  44. package/templates/api/base/src/lib/pagination.ts +55 -0
  45. package/templates/api/base/src/modules/orders/orders.repository.ts +26 -0
  46. package/templates/api/base/src/modules/orders/orders.schema.ts +11 -0
  47. package/templates/api/base/src/modules/products/products.repository.ts +17 -0
  48. package/templates/api/base/src/modules/products/products.schema.ts +21 -0
  49. package/templates/api/base/src/modules/stats/stats.repository.ts +37 -0
  50. package/templates/api/base/src/modules/users/users.repository.ts +26 -0
  51. package/templates/api/base/src/modules/users/users.schema.ts +15 -0
  52. package/templates/api/base/test/docs.test.ts +23 -0
  53. package/templates/api/base/test/health.test.ts +19 -0
  54. package/templates/api/base/test/orders.test.ts +82 -0
  55. package/templates/api/base/test/products.test.ts +54 -0
  56. package/templates/api/base/test/repository.test.ts +45 -0
  57. package/templates/api/base/test/smoke.test.ts +7 -0
  58. package/templates/api/base/test/stats.test.ts +40 -0
  59. package/templates/api/base/test/users.test.ts +84 -0
  60. package/templates/api/base/tsconfig.json +22 -0
  61. package/templates/api/base/vitest.config.ts +16 -0
  62. package/templates/api/variants/features/no-github-ci/variant.json +1 -0
  63. package/templates/api/variants/framework/express/docs/agents/framework-express.md +9 -0
  64. package/templates/api/variants/framework/express/src/app.ts +37 -0
  65. package/templates/api/variants/framework/express/src/middleware/errorHandler.ts +13 -0
  66. package/templates/api/variants/framework/express/src/middleware/validate.ts +59 -0
  67. package/templates/api/variants/framework/express/src/modules/docs/docs.router.ts +15 -0
  68. package/templates/api/variants/framework/express/src/modules/health/health.router.ts +7 -0
  69. package/templates/api/variants/framework/express/src/modules/orders/orders.router.ts +14 -0
  70. package/templates/api/variants/framework/express/src/modules/products/products.router.ts +24 -0
  71. package/templates/api/variants/framework/express/src/modules/stats/stats.router.ts +9 -0
  72. package/templates/api/variants/framework/express/src/modules/users/users.router.ts +24 -0
  73. package/templates/api/variants/framework/express/src/server.ts +8 -0
  74. package/templates/api/variants/framework/express/test/errorHandler.test.ts +89 -0
  75. package/templates/api/variants/framework/express/variant.json +15 -0
  76. package/templates/fe/base/.agents/skills/ai-rules/SKILL.md +12 -0
  77. package/templates/fe/base/.agents/skills/plan-feature/SKILL.md +13 -0
  78. package/templates/fe/base/.agents/skills/review-diff/SKILL.md +12 -0
  79. package/templates/fe/base/.agents/skills/tdd-step/SKILL.md +14 -0
  80. package/templates/fe/base/.claude/agents/reviewer.md +21 -0
  81. package/templates/fe/base/.claude/hooks/format.sh +22 -0
  82. package/templates/fe/base/.claude/settings.json +41 -0
  83. package/templates/fe/base/.codex/agents/reviewer.toml +7 -0
  84. package/templates/fe/base/.codex/config.toml +10 -0
  85. package/templates/fe/base/.codex/hooks.json +12 -0
  86. package/templates/fe/base/.env.example +2 -0
  87. package/templates/fe/base/.github/workflows/ci.yml +37 -0
  88. package/templates/fe/base/.prettierignore +14 -0
  89. package/templates/fe/base/.prettierrc +20 -0
  90. package/templates/fe/base/CLAUDE.md +1 -0
  91. package/templates/fe/base/README.md +37 -0
  92. package/templates/fe/base/TASK.md.template +31 -0
  93. package/templates/fe/base/_gitignore +11 -0
  94. package/templates/fe/base/docs/agents/base.md +69 -0
  95. package/templates/fe/base/e2e/users.spec.ts +82 -0
  96. package/templates/fe/base/eslint.config.js +169 -0
  97. package/templates/fe/base/index.html +13 -0
  98. package/templates/fe/base/package.json +63 -0
  99. package/templates/fe/base/playwright.config.ts +17 -0
  100. package/templates/fe/base/public/favicon.svg +1 -0
  101. package/templates/fe/base/public/icons.svg +24 -0
  102. package/templates/fe/base/scripts/agents-build.ts +26 -0
  103. package/templates/fe/base/scripts/rules-check.ts +48 -0
  104. package/templates/fe/base/scripts/rules-sync.ts +35 -0
  105. package/templates/fe/base/scripts/rules.config.ts +35 -0
  106. package/templates/fe/base/src/components/shared/EmptyState.tsx +23 -0
  107. package/templates/fe/base/src/components/shared/ErrorState.tsx +37 -0
  108. package/templates/fe/base/src/components/shared/PageHeader.tsx +21 -0
  109. package/templates/fe/base/src/features/users/UserSearch.tsx +48 -0
  110. package/templates/fe/base/src/features/users/UsersPage.tsx +58 -0
  111. package/templates/fe/base/src/features/users/UsersTable.tsx +245 -0
  112. package/templates/fe/base/src/features/users/UsersTableSkeleton.tsx +16 -0
  113. package/templates/fe/base/src/features/users/__tests__/UserSearch.test.tsx +30 -0
  114. package/templates/fe/base/src/features/users/__tests__/UsersPage.test.tsx +69 -0
  115. package/templates/fe/base/src/features/users/__tests__/UsersTable.test.tsx +120 -0
  116. package/templates/fe/base/src/features/users/__tests__/useUsers.test.tsx +62 -0
  117. package/templates/fe/base/src/features/users/useUsers.ts +22 -0
  118. package/templates/fe/base/src/features/users/users.api.ts +22 -0
  119. package/templates/fe/base/src/features/users/users.schema.ts +43 -0
  120. package/templates/fe/base/src/lib/apiClient.test.ts +56 -0
  121. package/templates/fe/base/src/lib/apiClient.ts +42 -0
  122. package/templates/fe/base/src/lib/appName.ts +2 -0
  123. package/templates/fe/base/src/lib/env.test.ts +21 -0
  124. package/templates/fe/base/src/lib/env.ts +26 -0
  125. package/templates/fe/base/src/lib/queryClient.ts +15 -0
  126. package/templates/fe/base/src/lib/router.ts +11 -0
  127. package/templates/fe/base/src/main.tsx +21 -0
  128. package/templates/fe/base/src/routeTree.gen.ts +77 -0
  129. package/templates/fe/base/src/routes/__root.tsx +83 -0
  130. package/templates/fe/base/src/routes/index.tsx +26 -0
  131. package/templates/fe/base/src/routes/users/index.tsx +24 -0
  132. package/templates/fe/base/src/stores/seam.test.ts +21 -0
  133. package/templates/fe/base/src/test/api-mock.ts +22 -0
  134. package/templates/fe/base/src/test/fixtures/users.ts +39 -0
  135. package/templates/fe/base/src/test/msw/handlers.ts +9 -0
  136. package/templates/fe/base/src/test/msw/server.ts +5 -0
  137. package/templates/fe/base/src/test/render.tsx +45 -0
  138. package/templates/fe/base/src/test/setup.ts +15 -0
  139. package/templates/fe/base/tsconfig.json +25 -0
  140. package/templates/fe/base/vite.config.ts +22 -0
  141. package/templates/fe/base/vitest.config.ts +21 -0
  142. package/templates/fe/variants/features/no-github-ci/variant.json +1 -0
  143. package/templates/fe/variants/features/no-msw/src/test/api-mock.ts +52 -0
  144. package/templates/fe/variants/features/no-msw/src/test/setup.ts +13 -0
  145. package/templates/fe/variants/features/no-msw/variant.json +4 -0
  146. package/templates/fe/variants/features/no-playwright/.github/workflows/ci.yml +19 -0
  147. package/templates/fe/variants/features/no-playwright/variant.json +5 -0
  148. package/templates/fe/variants/state/jotai/docs/agents/state-jotai.md +5 -0
  149. package/templates/fe/variants/state/jotai/src/stores/index.ts +15 -0
  150. package/templates/fe/variants/state/jotai/src/stores/ui.atoms.test.tsx +29 -0
  151. package/templates/fe/variants/state/jotai/src/stores/ui.atoms.ts +39 -0
  152. package/templates/fe/variants/state/jotai/variant.json +12 -0
  153. package/templates/fe/variants/state/zustand/docs/agents/state-zustand.md +5 -0
  154. package/templates/fe/variants/state/zustand/src/stores/index.ts +12 -0
  155. package/templates/fe/variants/state/zustand/src/stores/ui.store.test.ts +34 -0
  156. package/templates/fe/variants/state/zustand/src/stores/ui.store.ts +62 -0
  157. package/templates/fe/variants/state/zustand/variant.json +5 -0
  158. package/templates/fe/variants/ui/base-ui/components.json +25 -0
  159. package/templates/fe/variants/ui/base-ui/docs/agents/ui-base-ui.md +7 -0
  160. package/templates/fe/variants/ui/base-ui/src/components/ui/alert.tsx +75 -0
  161. package/templates/fe/variants/ui/base-ui/src/components/ui/button.tsx +57 -0
  162. package/templates/fe/variants/ui/base-ui/src/components/ui/input.tsx +19 -0
  163. package/templates/fe/variants/ui/base-ui/src/components/ui/skeleton.tsx +13 -0
  164. package/templates/fe/variants/ui/base-ui/src/components/ui/table.tsx +113 -0
  165. package/templates/fe/variants/ui/base-ui/src/index.css +130 -0
  166. package/templates/fe/variants/ui/base-ui/src/lib/utils.test.ts +10 -0
  167. package/templates/fe/variants/ui/base-ui/src/lib/utils.ts +1 -0
  168. package/templates/fe/variants/ui/base-ui/variant.json +23 -0
  169. package/templates/fe/variants/ui/radix/components.json +25 -0
  170. package/templates/fe/variants/ui/radix/docs/agents/ui-radix.md +7 -0
  171. package/templates/fe/variants/ui/radix/src/components/ui/alert.tsx +75 -0
  172. package/templates/fe/variants/ui/radix/src/components/ui/button.tsx +66 -0
  173. package/templates/fe/variants/ui/radix/src/components/ui/input.tsx +18 -0
  174. package/templates/fe/variants/ui/radix/src/components/ui/skeleton.tsx +13 -0
  175. package/templates/fe/variants/ui/radix/src/components/ui/table.tsx +113 -0
  176. package/templates/fe/variants/ui/radix/src/index.css +130 -0
  177. package/templates/fe/variants/ui/radix/src/lib/utils.test.ts +10 -0
  178. package/templates/fe/variants/ui/radix/src/lib/utils.ts +1 -0
  179. package/templates/fe/variants/ui/radix/variant.json +23 -0
  180. package/templates/fullstack/.github/workflows/ci.yml +18 -0
  181. package/templates/fullstack/AGENTS.md +7 -0
  182. package/templates/fullstack/CLAUDE.md +1 -0
  183. package/templates/fullstack/README.md +16 -0
  184. package/templates/fullstack/_gitignore +11 -0
  185. package/templates/fullstack/package.json +16 -0
@@ -0,0 +1,169 @@
1
+ import js from '@eslint/js'
2
+ import jsxA11y from 'eslint-plugin-jsx-a11y'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import { defineConfig, globalIgnores } from 'eslint/config'
5
+ import globals from 'globals'
6
+ import tseslint from 'typescript-eslint'
7
+
8
+ // Written by create-simone-app: which state library the chosen variant may import inside src/stores.
9
+ import variants from './eslint.variants.json' with { type: 'json' }
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Forbidden imports (dogmas + seams). `allow` lifts a keyword for a folder.
13
+ // ---------------------------------------------------------------------------
14
+ const RESTRICTED = {
15
+ paths: [
16
+ {
17
+ name: 'axios',
18
+ message:
19
+ 'HTTP only via `apiClient` from @/lib/apiClient (core-rules §8.1).',
20
+ },
21
+ { name: 'zustand', message: 'State seam: import from @/stores only.' },
22
+ { name: 'jotai', message: 'State seam: import from @/stores only.' },
23
+ { name: 'swr', message: 'TanStack Query only (tanstack-ecosystem §2).' },
24
+ { name: 'antd', message: 'shadcn/ui only (ui-shadcn §1.1).' },
25
+ {
26
+ name: 'styled-components',
27
+ message: 'Tailwind only, single index.css (ui-shadcn §2.1).',
28
+ },
29
+ ],
30
+ patterns: [
31
+ // regex, not group: gitignore-style groups would also match '@tanstack/react-router'
32
+ {
33
+ regex: '^react-router(-dom)?(/.*)?$',
34
+ message: 'TanStack Router only (tanstack-ecosystem §1.1).',
35
+ },
36
+ {
37
+ group: ['@mui/*', '@chakra-ui/*'],
38
+ message: 'shadcn/ui only (ui-shadcn §1.1).',
39
+ },
40
+ {
41
+ group: ['@emotion/*'],
42
+ message: 'Tailwind only, single index.css (ui-shadcn §2.1).',
43
+ },
44
+ {
45
+ group: ['redux', 'redux-*', '@reduxjs/*', 'react-redux'],
46
+ message: 'Zustand for client state (core-rules §7).',
47
+ },
48
+ {
49
+ group: ['zustand/*', 'jotai/*'],
50
+ message: 'State seam: import from @/stores only.',
51
+ },
52
+ {
53
+ group: ['@base-ui/*', '@radix-ui/*', 'radix-ui', 'react-aria-components'],
54
+ message: 'UI seam: UI primitives only inside src/components/ui.',
55
+ },
56
+ ],
57
+ }
58
+
59
+ const restrictedImports = (allow = []) => [
60
+ 'error',
61
+ {
62
+ paths: RESTRICTED.paths.filter(
63
+ (p) => !allow.some((a) => p.name.startsWith(a)),
64
+ ),
65
+ patterns: RESTRICTED.patterns.filter(
66
+ (p) =>
67
+ !(p.group ?? [p.regex]).some((g) => allow.some((a) => g.startsWith(a))),
68
+ ),
69
+ },
70
+ ]
71
+
72
+ // testing-selectors §1/§5: every interactive element carries data-testid.
73
+ // Native elements + the shadcn wrappers we compose; a spread may carry the attribute, so it is exempt.
74
+ const INTERACTIVE = 'button|a|input|select|textarea|Button|Input'
75
+ const MISSING_TESTID = {
76
+ selector: `JSXOpeningElement[name.name=/^(${INTERACTIVE})$/]:not(:has(JSXAttribute[name.name="data-testid"])):not(:has(JSXSpreadAttribute))`,
77
+ message:
78
+ 'Interactive element without data-testid="feature-element-qualifier" (testing-selectors §1).',
79
+ }
80
+
81
+ export default defineConfig(
82
+ // core-rules §10.2: shadcn output is generated, never linted. routeTree is generated too.
83
+ globalIgnores([
84
+ 'node_modules',
85
+ 'dist',
86
+ 'coverage',
87
+ 'docs/ai-rules',
88
+ 'src/components/ui/',
89
+ 'src/routeTree.gen.ts',
90
+ 'playwright-report',
91
+ 'test-results',
92
+ ]),
93
+ js.configs.recommended,
94
+ tseslint.configs.strict,
95
+ tseslint.configs.stylistic,
96
+ {
97
+ // core-rules §10.2: only these two import rules; ordering is Prettier's job (§10.1)
98
+ rules: {
99
+ 'no-duplicate-imports': 'error',
100
+ '@typescript-eslint/consistent-type-imports': 'error',
101
+ '@typescript-eslint/no-unused-vars': [
102
+ 'error',
103
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
104
+ ],
105
+ },
106
+ },
107
+ {
108
+ files: ['src/**/*.{ts,tsx}'],
109
+ extends: [
110
+ reactHooks.configs.flat.recommended,
111
+ jsxA11y.flatConfigs.recommended,
112
+ ],
113
+ languageOptions: { globals: globals.browser },
114
+ rules: {
115
+ 'no-restricted-imports': restrictedImports(),
116
+ 'no-restricted-syntax': ['error', MISSING_TESTID],
117
+ // React Compiler is not enabled; TanStack Table (a dogma) trips this heuristic.
118
+ 'react-hooks/incompatible-library': 'off',
119
+ // a11y §1.2: reinforced severity
120
+ 'jsx-a11y/alt-text': 'error',
121
+ 'jsx-a11y/anchor-has-content': 'error',
122
+ 'jsx-a11y/anchor-is-valid': [
123
+ 'error',
124
+ {
125
+ components: ['Link'],
126
+ specialLink: ['to'],
127
+ aspects: ['noHref', 'invalidHref', 'preferButton'],
128
+ },
129
+ ],
130
+ 'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
131
+ 'jsx-a11y/aria-props': 'error',
132
+ 'jsx-a11y/aria-proptypes': 'error',
133
+ 'jsx-a11y/aria-role': ['error', { ignoreNonDOM: true }],
134
+ 'jsx-a11y/aria-unsupported-elements': 'error',
135
+ 'jsx-a11y/autocomplete-valid': 'error',
136
+ 'jsx-a11y/click-events-have-key-events': 'error',
137
+ 'jsx-a11y/heading-has-content': 'error',
138
+ 'jsx-a11y/html-has-lang': 'error',
139
+ 'jsx-a11y/img-redundant-alt': 'error',
140
+ 'jsx-a11y/interactive-supports-focus': 'error',
141
+ 'jsx-a11y/label-has-associated-control': 'error',
142
+ 'jsx-a11y/lang': 'error',
143
+ 'jsx-a11y/media-has-caption': 'error',
144
+ 'jsx-a11y/mouse-events-have-key-events': 'error',
145
+ 'jsx-a11y/no-access-key': 'error',
146
+ 'jsx-a11y/no-autofocus': ['error', { ignoreNonDOM: true }],
147
+ 'jsx-a11y/no-distracting-elements': 'error',
148
+ 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
149
+ 'jsx-a11y/no-noninteractive-element-interactions': 'error',
150
+ 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error',
151
+ 'jsx-a11y/no-noninteractive-tabindex': 'error',
152
+ 'jsx-a11y/no-redundant-roles': 'error',
153
+ 'jsx-a11y/no-static-element-interactions': 'error',
154
+ 'jsx-a11y/role-has-required-aria-props': 'error',
155
+ 'jsx-a11y/role-supports-aria-props': 'error',
156
+ 'jsx-a11y/scope': 'error',
157
+ 'jsx-a11y/tabindex-no-positive': 'error',
158
+ },
159
+ },
160
+ // Seams: the only places allowed to touch the underlying libraries.
161
+ {
162
+ files: ['src/lib/apiClient.ts'],
163
+ rules: { 'no-restricted-imports': restrictedImports(['axios']) },
164
+ },
165
+ {
166
+ files: ['src/stores/**'],
167
+ rules: { 'no-restricted-imports': restrictedImports(variants.stateLibs) },
168
+ },
169
+ )
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="it">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>__NAME__</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "__NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc --noEmit && vite build",
9
+ "preview": "vite preview",
10
+ "test": "vitest run --coverage",
11
+ "test:watch": "vitest",
12
+ "e2e": "playwright test",
13
+ "lint": "eslint .",
14
+ "lint:fix": "eslint . --fix",
15
+ "format": "prettier --write .",
16
+ "format:check": "prettier --check .",
17
+ "typecheck": "tsc --noEmit",
18
+ "verify": "bun run typecheck && bun run lint && bun run format:check && bun run agents:check && bun run test",
19
+ "rules:sync": "bun scripts/rules-sync.ts",
20
+ "rules:check": "bun scripts/rules-check.ts",
21
+ "agents:build": "bun scripts/agents-build.ts",
22
+ "agents:check": "bun scripts/agents-build.ts --check"
23
+ },
24
+ "dependencies": {
25
+ "@tailwindcss/vite": "^4.3.3",
26
+ "@tanstack/react-query": "^5.103.1",
27
+ "@tanstack/react-router": "^1.170.38",
28
+ "@tanstack/react-table": "^8",
29
+ "axios": "^1.20.0",
30
+ "lucide-react": "^1.47.0",
31
+ "react": "^19",
32
+ "react-dom": "^19",
33
+ "tailwindcss": "^4.3.3",
34
+ "zod": "^4.6.5"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^10.0.1",
38
+ "@playwright/test": "^1.63.0",
39
+ "@tanstack/router-plugin": "^1.168.40",
40
+ "@testing-library/dom": "^10.4.2",
41
+ "@testing-library/jest-dom": "^7.0.1",
42
+ "@testing-library/react": "^16.3.3",
43
+ "@testing-library/user-event": "^14.6.7",
44
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
45
+ "@types/bun": "^1.4.2",
46
+ "@types/node": "^26.6.1",
47
+ "@types/react": "^19.3.0",
48
+ "@types/react-dom": "^19.3.0",
49
+ "@vitejs/plugin-react": "^5",
50
+ "@vitest/coverage-v8": "^5.0.1",
51
+ "eslint": "^9",
52
+ "eslint-plugin-jsx-a11y": "^6.10.2",
53
+ "eslint-plugin-react-hooks": "^7.1.1",
54
+ "globals": "^17.12.0",
55
+ "jsdom": "^30.1.0",
56
+ "msw": "^2.15.0",
57
+ "prettier": "^3.9.8",
58
+ "typescript": "^5",
59
+ "typescript-eslint": "^8.70.0",
60
+ "vite": "^7",
61
+ "vitest": "^5.0.1"
62
+ }
63
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+
3
+ export default defineConfig({
4
+ testDir: 'e2e',
5
+ fullyParallel: true,
6
+ forbidOnly: Boolean(process.env.CI),
7
+ retries: process.env.CI ? 1 : 0,
8
+ reporter: process.env.CI ? 'github' : 'list',
9
+ use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry' },
10
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
11
+ webServer: {
12
+ command: 'bun run dev',
13
+ url: 'http://localhost:3000',
14
+ reuseExistingServer: !process.env.CI,
15
+ timeout: 60_000,
16
+ },
17
+ })
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>
@@ -0,0 +1,26 @@
1
+ import { join } from 'node:path'
2
+
3
+ /** Fragment order, written by create-simone-app for the chosen variants. */
4
+ const FRAGMENTS: string[] = await Bun.file('docs/agents/fragments.json').json()
5
+ const OUT = 'AGENTS.md'
6
+ const HEADER =
7
+ '<!-- Generated by `bun run agents:build` from docs/agents/*.md. Edit the fragments, not this file. -->\n\n'
8
+
9
+ const parts = await Promise.all(
10
+ FRAGMENTS.map((name) => Bun.file(join('docs/agents', `${name}.md`)).text()),
11
+ )
12
+ const built = HEADER + parts.map((p) => p.trim()).join('\n\n') + '\n'
13
+
14
+ if (process.argv.includes('--check')) {
15
+ const current = (await Bun.file(OUT).exists())
16
+ ? await Bun.file(OUT).text()
17
+ : ''
18
+ if (current !== built) {
19
+ console.error(`${OUT} is stale: run \`bun run agents:build\``)
20
+ process.exit(1)
21
+ }
22
+ console.log(`${OUT} up to date (${built.split('\n').length} lines)`)
23
+ } else {
24
+ await Bun.write(OUT, built)
25
+ console.log(`wrote ${OUT} (${built.split('\n').length} lines)`)
26
+ }
@@ -0,0 +1,48 @@
1
+ import { join } from 'node:path'
2
+
3
+ import {
4
+ MANIFEST,
5
+ type Manifest,
6
+ RULES_DIR,
7
+ rulesSource,
8
+ sha256,
9
+ } from './rules.config'
10
+
11
+ const manifestFile = Bun.file(MANIFEST)
12
+ if (!(await manifestFile.exists())) {
13
+ console.error(`${MANIFEST} missing: run \`bun run rules:sync\``)
14
+ process.exit(1)
15
+ }
16
+ const manifest: Manifest = await manifestFile.json()
17
+ const source = rulesSource()
18
+ const upstreamAvailable = await Bun.file(join(source, 'README.md')).exists()
19
+ let failed = false
20
+
21
+ for (const entry of manifest.files) {
22
+ const local = join(RULES_DIR, entry.file)
23
+ if (!(await Bun.file(local).exists())) {
24
+ console.error(`missing ${local}`)
25
+ failed = true
26
+ continue
27
+ }
28
+ if ((await sha256(local)) !== entry.sha256) {
29
+ console.error(`${local} differs from MANIFEST (edited by hand?)`)
30
+ failed = true
31
+ }
32
+ if (upstreamAvailable) {
33
+ const upstream = join(source, entry.source)
34
+ if ((await sha256(upstream)) !== entry.sha256) {
35
+ console.error(
36
+ `${entry.source} changed upstream: run \`bun run rules:sync\``,
37
+ )
38
+ failed = true
39
+ }
40
+ }
41
+ }
42
+
43
+ if (!upstreamAvailable)
44
+ console.warn(`ai-rules not found at ${source}: checked local integrity only`)
45
+ if (failed) process.exit(1)
46
+ console.log(
47
+ `ai-rules ok (${manifest.files.length} files, synced ${manifest.syncedAt})`,
48
+ )
@@ -0,0 +1,35 @@
1
+ import { mkdir } from 'node:fs/promises'
2
+ import { basename, join } from 'node:path'
3
+
4
+ import {
5
+ MANIFEST,
6
+ type Manifest,
7
+ RULES,
8
+ RULES_DIR,
9
+ rulesSource,
10
+ sha256,
11
+ } from './rules.config'
12
+
13
+ const source = rulesSource()
14
+ if (!(await Bun.file(join(source, 'README.md')).exists())) {
15
+ console.error(`ai-rules not found at ${source} (set AI_RULES_PATH)`)
16
+ process.exit(1)
17
+ }
18
+
19
+ await mkdir(RULES_DIR, { recursive: true })
20
+ const manifest: Manifest = { syncedAt: new Date().toISOString(), files: [] }
21
+
22
+ for (const rule of RULES) {
23
+ const from = join(source, rule)
24
+ const to = join(RULES_DIR, basename(rule))
25
+ await Bun.write(to, Bun.file(from))
26
+ manifest.files.push({
27
+ file: basename(rule),
28
+ source: rule,
29
+ sha256: await sha256(to),
30
+ })
31
+ console.log(`synced ${rule}`)
32
+ }
33
+
34
+ await Bun.write(MANIFEST, JSON.stringify(manifest, null, 2) + '\n')
35
+ console.log(`wrote ${MANIFEST}`)
@@ -0,0 +1,35 @@
1
+ import { homedir } from 'node:os'
2
+ import { join } from 'node:path'
3
+
4
+ /** ai-rules files (relative to the ai-rules repo root) mirrored into docs/ai-rules. */
5
+ export const RULES = [
6
+ 'frontend/react-stack/core-rules.md',
7
+ 'frontend/react-stack/tanstack-ecosystem.md',
8
+ 'frontend/react-stack/ui-shadcn.md',
9
+ 'frontend/react-stack/a11y.md',
10
+ 'frontend/testing-selectors.md',
11
+ 'frontend/accessibility-legge-stanca.md',
12
+ ] as const
13
+
14
+ export const RULES_DIR = 'docs/ai-rules'
15
+ export const MANIFEST = join(RULES_DIR, 'MANIFEST.json')
16
+
17
+ export const rulesSource = (): string =>
18
+ process.env.AI_RULES_PATH ?? join(homedir(), 'Progetti', 'ai-rules')
19
+
20
+ export interface ManifestEntry {
21
+ file: string
22
+ source: string
23
+ sha256: string
24
+ }
25
+
26
+ export interface Manifest {
27
+ syncedAt: string
28
+ files: ManifestEntry[]
29
+ }
30
+
31
+ export const sha256 = async (path: string): Promise<string> => {
32
+ const hasher = new Bun.CryptoHasher('sha256')
33
+ hasher.update(await Bun.file(path).arrayBuffer())
34
+ return hasher.digest('hex')
35
+ }
@@ -0,0 +1,23 @@
1
+ import { SearchX } from 'lucide-react'
2
+
3
+ interface EmptyStateProps {
4
+ title: string
5
+ description?: string
6
+ testId: string
7
+ }
8
+
9
+ export function EmptyState({ title, description, testId }: EmptyStateProps) {
10
+ return (
11
+ <div
12
+ role="status"
13
+ className="flex flex-col items-center gap-2 rounded-lg border border-dashed px-6 py-12 text-center"
14
+ data-testid={testId}
15
+ >
16
+ <SearchX aria-hidden="true" className="size-8 text-muted-foreground" />
17
+ <p className="font-medium">{title}</p>
18
+ {description && (
19
+ <p className="text-sm text-muted-foreground">{description}</p>
20
+ )}
21
+ </div>
22
+ )
23
+ }
@@ -0,0 +1,37 @@
1
+ import { AlertCircle } from 'lucide-react'
2
+
3
+ import {
4
+ Alert,
5
+ AlertAction,
6
+ AlertDescription,
7
+ AlertTitle,
8
+ } from '@/components/ui/alert'
9
+ import { Button } from '@/components/ui/button'
10
+
11
+ interface ErrorStateProps {
12
+ message: string
13
+ onRetry?: () => void
14
+ testId: string
15
+ }
16
+
17
+ export function ErrorState({ message, onRetry, testId }: ErrorStateProps) {
18
+ return (
19
+ <Alert variant="destructive" role="alert" data-testid={testId}>
20
+ <AlertCircle aria-hidden="true" />
21
+ <AlertTitle>Si è verificato un errore</AlertTitle>
22
+ <AlertDescription>{message}</AlertDescription>
23
+ {onRetry && (
24
+ <AlertAction>
25
+ <Button
26
+ variant="outline"
27
+ size="sm"
28
+ onClick={onRetry}
29
+ data-testid={`${testId}-retry-button`}
30
+ >
31
+ Riprova
32
+ </Button>
33
+ </AlertAction>
34
+ )}
35
+ </Alert>
36
+ )
37
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ interface PageHeaderProps {
4
+ title: string
5
+ description?: string
6
+ children?: ReactNode
7
+ }
8
+
9
+ export function PageHeader({ title, description, children }: PageHeaderProps) {
10
+ return (
11
+ <div className="mb-6 flex flex-wrap items-end justify-between gap-4">
12
+ <div>
13
+ <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
14
+ {description && (
15
+ <p className="mt-1 text-muted-foreground">{description}</p>
16
+ )}
17
+ </div>
18
+ {children}
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,48 @@
1
+ import { type ChangeEvent, useEffect, useRef, useState } from 'react'
2
+
3
+ import { Input } from '@/components/ui/input'
4
+
5
+ interface UserSearchProps {
6
+ value: string
7
+ onChange: (q: string) => void
8
+ /** Debounce in ms before `onChange` fires. */
9
+ delay?: number
10
+ }
11
+
12
+ export function UserSearch({ value, onChange, delay = 300 }: UserSearchProps) {
13
+ const [draft, setDraft] = useState(value)
14
+ const [lastValue, setLastValue] = useState(value)
15
+ const timer = useRef<ReturnType<typeof setTimeout>>(undefined)
16
+
17
+ // External value changed (back/forward, reset): adopt it during render, no effect needed.
18
+ if (value !== lastValue) {
19
+ setLastValue(value)
20
+ setDraft(value)
21
+ }
22
+
23
+ useEffect(() => () => clearTimeout(timer.current), [])
24
+
25
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
26
+ const next = event.target.value
27
+ setDraft(next)
28
+ clearTimeout(timer.current)
29
+ timer.current = setTimeout(() => onChange(next.trim()), delay)
30
+ }
31
+
32
+ return (
33
+ <div className="mb-4 flex max-w-sm flex-col gap-1.5">
34
+ <label htmlFor="users-search-input" className="text-sm font-medium">
35
+ Cerca
36
+ </label>
37
+ <Input
38
+ id="users-search-input"
39
+ type="search"
40
+ placeholder="Nome o email"
41
+ autoComplete="off"
42
+ value={draft}
43
+ onChange={handleChange}
44
+ data-testid="users-search-input"
45
+ />
46
+ </div>
47
+ )
48
+ }