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,304 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { mkdir, readFile, symlink, writeFile } from 'node:fs/promises'
4
+ import { homedir } from 'node:os'
5
+ import { basename, dirname, extname, join, resolve } from 'node:path'
6
+
7
+ import { type Options, deviationsFor } from './options'
8
+ import {
9
+ type App,
10
+ type Deviation,
11
+ type Layer,
12
+ TEMPLATES_DIR,
13
+ layersFor,
14
+ loadLayer,
15
+ walk,
16
+ } from './templates'
17
+
18
+ export interface ScaffoldResult {
19
+ dir: string
20
+ /** Relative paths that are (or would be) written, sorted. */
21
+ files: string[]
22
+ warnings: string[]
23
+ }
24
+
25
+ type Content = string | Uint8Array
26
+ type Plan = Map<string, Content>
27
+
28
+ const TEXT_EXT = new Set([
29
+ '.ts',
30
+ '.tsx',
31
+ '.js',
32
+ '.mjs',
33
+ '.json',
34
+ '.md',
35
+ '.html',
36
+ '.css',
37
+ '.yaml',
38
+ '.yml',
39
+ '.toml',
40
+ '.sh',
41
+ '.txt',
42
+ '.svg',
43
+ '',
44
+ ])
45
+ const isText = (file: string) =>
46
+ TEXT_EXT.has(extname(file)) || basename(file).startsWith('.')
47
+
48
+ const AGENTS_HEADER =
49
+ '<!-- Generated by `bun run agents:build` from docs/agents/*.md. Edit the fragments, not this file. -->\n\n'
50
+
51
+ const renderDeviations = (rows: Deviation[]): string => {
52
+ const body = rows.length
53
+ ? rows.map((d) => `| ${d.rule} | ${d.what} | ${d.why} |`).join('\n')
54
+ : '| — | none | every choice follows the ai-rules |'
55
+ return `## Deviations from ai-rules\n\n| Rule § | Deviation | Why |\n|---|---|---|\n${body}\n`
56
+ }
57
+
58
+ interface PackageJson {
59
+ name?: string
60
+ scripts?: Record<string, string>
61
+ dependencies?: Record<string, string>
62
+ devDependencies?: Record<string, string>
63
+ [key: string]: unknown
64
+ }
65
+
66
+ const sorted = (o: Record<string, string>) =>
67
+ Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)))
68
+
69
+ /** Base package.json + variant deps/removals, deps sorted for stable diffs. */
70
+ const mergePackageJson = (base: string, layers: Layer[]): string => {
71
+ const pkg = JSON.parse(base) as PackageJson
72
+ const drop = (o: Record<string, string>, keys: string[]) =>
73
+ Object.fromEntries(Object.entries(o).filter(([k]) => !keys.includes(k)))
74
+ let deps = { ...pkg.dependencies }
75
+ let dev = { ...pkg.devDependencies }
76
+ let scripts = { ...pkg.scripts }
77
+ for (const { manifest } of layers) {
78
+ deps = drop(
79
+ { ...deps, ...manifest.dependencies },
80
+ manifest.removeDependencies ?? [],
81
+ )
82
+ dev = drop(
83
+ { ...dev, ...manifest.devDependencies },
84
+ manifest.removeDevDependencies ?? [],
85
+ )
86
+ scripts = drop(scripts, manifest.removeScripts ?? [])
87
+ }
88
+ return (
89
+ JSON.stringify(
90
+ {
91
+ ...pkg,
92
+ scripts,
93
+ dependencies: sorted(deps),
94
+ devDependencies: sorted(dev),
95
+ },
96
+ null,
97
+ 2,
98
+ ) + '\n'
99
+ )
100
+ }
101
+
102
+ /** Builds one app (fe or api) into `plan` under `prefix` ('' or 'apps/fe/'). */
103
+ const planApp = async (
104
+ app: App,
105
+ options: Options,
106
+ plan: Plan,
107
+ prefix: string,
108
+ stripCi: boolean,
109
+ ) => {
110
+ const layers = await Promise.all(
111
+ layersFor(app, options).map((name) => loadLayer(app, name)),
112
+ )
113
+ const token = (s: string) => s.replaceAll('__NAME__', options.name)
114
+ const put = (rel: string, content: Content) => plan.set(prefix + rel, content)
115
+
116
+ for (const layer of layers) {
117
+ for (const rel of await walk(layer.dir)) {
118
+ if (rel === 'variant.json') continue
119
+ const target =
120
+ basename(rel) === '_gitignore' ? join(dirname(rel), '.gitignore') : rel
121
+ const source = join(layer.dir, rel)
122
+ put(
123
+ target,
124
+ isText(rel)
125
+ ? token(await readFile(source, 'utf8'))
126
+ : new Uint8Array(await readFile(source)),
127
+ )
128
+ }
129
+ }
130
+ const removed = layers
131
+ .flatMap((l) => l.manifest.remove ?? [])
132
+ .concat(stripCi ? ['.github'] : [])
133
+ for (const key of [...plan.keys()]) {
134
+ const rel = key.startsWith(prefix) ? key.slice(prefix.length) : undefined
135
+ if (rel && removed.some((r) => rel === r || rel.startsWith(r + '/')))
136
+ plan.delete(key)
137
+ }
138
+
139
+ const basePkg = plan.get(prefix + 'package.json')
140
+ if (typeof basePkg === 'string')
141
+ put('package.json', mergePackageJson(basePkg, layers))
142
+
143
+ // AGENTS.md from fragments: base + variant fragments + deviations.
144
+ const fragments = [
145
+ 'base',
146
+ ...layers.flatMap((l) => l.manifest.agents ?? []),
147
+ 'deviations',
148
+ ]
149
+ const deviations = layers.flatMap((l) => l.manifest.deviations ?? [])
150
+ put('docs/agents/fragments.json', JSON.stringify(fragments, null, 2) + '\n')
151
+ put('docs/agents/deviations.md', renderDeviations(deviations))
152
+ const parts = fragments.map((f) => {
153
+ const content = plan.get(`${prefix}docs/agents/${f}.md`)
154
+ if (typeof content !== 'string')
155
+ throw new Error(`missing AGENTS fragment ${f} for ${app}`)
156
+ return content.trim()
157
+ })
158
+ put('AGENTS.md', AGENTS_HEADER + parts.join('\n\n') + '\n')
159
+
160
+ if (app === 'fe') {
161
+ const stateLibs = layers.flatMap((l) => l.manifest.eslint?.stateLibs ?? [])
162
+ put('eslint.variants.json', JSON.stringify({ stateLibs }, null, 2) + '\n')
163
+ }
164
+ // Claude Code shares the skills folder with Codex through a symlink (recreated, never shipped).
165
+ put('.claude/skills', SYMLINK)
166
+ }
167
+
168
+ const SYMLINK = '__symlink__:../.agents/skills'
169
+
170
+ /** Computes every file of the project in memory. Writes nothing. */
171
+ export const plan = async (options: Options): Promise<Plan> => {
172
+ const files: Plan = new Map()
173
+ if (options.preset === 'fullstack') {
174
+ const root = join(TEMPLATES_DIR, 'fullstack')
175
+ for (const rel of await walk(root)) {
176
+ const target = basename(rel) === '_gitignore' ? '.gitignore' : rel
177
+ files.set(
178
+ target,
179
+ (await readFile(join(root, rel), 'utf8')).replaceAll(
180
+ '__NAME__',
181
+ options.name,
182
+ ),
183
+ )
184
+ }
185
+ if (!options.features.includes('github-ci'))
186
+ files.delete('.github/workflows/ci.yml')
187
+ await planApp(
188
+ 'api',
189
+ { ...options, name: `${options.name}-api` },
190
+ files,
191
+ 'apps/api/',
192
+ true,
193
+ )
194
+ await planApp(
195
+ 'fe',
196
+ { ...options, name: `${options.name}-fe` },
197
+ files,
198
+ 'apps/fe/',
199
+ true,
200
+ )
201
+ } else {
202
+ await planApp(options.preset, options, files, '', false)
203
+ }
204
+ return files
205
+ }
206
+
207
+ const write = async (dir: string, files: Plan) => {
208
+ for (const [rel, content] of files) {
209
+ const target = join(dir, rel)
210
+ await mkdir(dirname(target), { recursive: true })
211
+ if (content === SYMLINK) {
212
+ if (!existsSync(target)) await symlink('../.agents/skills', target)
213
+ } else {
214
+ await writeFile(target, content)
215
+ }
216
+ }
217
+ }
218
+
219
+ /** Where the company ai-rules live: `AI_RULES_PATH` or the conventional checkout. */
220
+ export const rulesSource = (): string =>
221
+ process.env.AI_RULES_PATH ?? join(homedir(), 'Progetti', 'ai-rules')
222
+
223
+ // node:child_process, not Bun.spawn: Vitest runs this module on Node.
224
+ const run = (
225
+ cmd: string[],
226
+ cwd: string,
227
+ env: Record<string, string> = {},
228
+ quiet = false,
229
+ ) =>
230
+ new Promise<void>((resolvePromise, reject) => {
231
+ const [bin, ...args] = cmd
232
+ if (!bin) throw new Error('empty command')
233
+ const child = spawn(bin, args, {
234
+ cwd,
235
+ env: { ...process.env, ...env },
236
+ stdio: ['ignore', quiet ? 'ignore' : 'inherit', 'inherit'],
237
+ })
238
+ child.on('error', reject)
239
+ child.on('exit', (code) =>
240
+ code === 0
241
+ ? resolvePromise()
242
+ : reject(new Error(`${cmd.join(' ')} exited with ${code}`)),
243
+ )
244
+ })
245
+
246
+ /** Mirrors the ai-rules into each app (docs/ai-rules + MANIFEST.json). Missing source = warning, never an error. */
247
+ const syncRules = async (dir: string, appDirs: string[]): Promise<string[]> => {
248
+ const source = rulesSource()
249
+ if (!existsSync(join(source, 'README.md'))) {
250
+ return [
251
+ `ai-rules not found at ${source}: docs/ai-rules/ left empty. Set AI_RULES_PATH and run \`bun run rules:sync\` before pushing (CI runs rules:check).`,
252
+ ]
253
+ }
254
+ for (const app of appDirs) {
255
+ await run(
256
+ ['bun', 'scripts/rules-sync.ts'],
257
+ join(dir, app),
258
+ { AI_RULES_PATH: source },
259
+ true,
260
+ )
261
+ }
262
+ return []
263
+ }
264
+
265
+ export const scaffold = async (options: Options): Promise<ScaffoldResult> => {
266
+ const dir = resolve(options.dir)
267
+ const files = await plan(options)
268
+ const list = [...files.keys()].sort()
269
+ const appDirs =
270
+ options.preset === 'fullstack' ? ['apps/api', 'apps/fe'] : ['.']
271
+ if (options.dryRun) return { dir, files: list, warnings: [] }
272
+
273
+ if (existsSync(dir) && (await walk(dir)).length > 0) {
274
+ throw new Error(`target directory is not empty: ${dir}`)
275
+ }
276
+ await write(dir, files)
277
+ const warnings = await syncRules(dir, appDirs)
278
+ if (options.install) {
279
+ await run(['bun', 'install'], dir)
280
+ // Token substitution can change line widths: normalise once so `format:check` is green from commit 1.
281
+ for (const app of appDirs)
282
+ await run(
283
+ ['bunx', 'prettier', '--write', '--log-level', 'warn', '.'],
284
+ join(dir, app),
285
+ )
286
+ }
287
+ if (options.git) {
288
+ await run(['git', 'init', '-q', '-b', 'main'], dir)
289
+ await run(['git', 'add', '-A'], dir)
290
+ await run(
291
+ [
292
+ 'git',
293
+ 'commit',
294
+ '-q',
295
+ '-m',
296
+ `chore: scaffold ${options.name} with create-simone-app`,
297
+ ],
298
+ dir,
299
+ )
300
+ }
301
+ return { dir, files: list, warnings }
302
+ }
303
+
304
+ export { deviationsFor }
@@ -0,0 +1,77 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readFile, readdir } from 'node:fs/promises'
3
+ import { join, relative, resolve } from 'node:path'
4
+
5
+ import type { Options } from './options'
6
+
7
+ export const TEMPLATES_DIR = resolve(import.meta.dirname, '../templates')
8
+
9
+ export type App = 'fe' | 'api'
10
+
11
+ export interface Deviation {
12
+ rule: string
13
+ what: string
14
+ why: string
15
+ }
16
+
17
+ /** variant.json: what a variant adds/removes on top of base. Files next to it are overlaid. */
18
+ export interface VariantManifest {
19
+ dependencies?: Record<string, string>
20
+ devDependencies?: Record<string, string>
21
+ remove?: string[]
22
+ removeDependencies?: string[]
23
+ removeDevDependencies?: string[]
24
+ removeScripts?: string[]
25
+ /** AGENTS.md fragment names shipped in this variant's docs/agents/. */
26
+ agents?: string[]
27
+ deviations?: Deviation[]
28
+ eslint?: { stateLibs?: string[] }
29
+ }
30
+
31
+ export interface Layer {
32
+ name: string
33
+ dir: string
34
+ manifest: VariantManifest
35
+ }
36
+
37
+ /** Ordered layers for an app: base first, later layers override earlier files. */
38
+ export const layersFor = (app: App, options: Options): string[] => {
39
+ const layers = ['base']
40
+ if (app === 'fe') {
41
+ layers.push(`variants/state/${options.state}`, `variants/ui/${options.ui}`)
42
+ for (const f of ['msw', 'playwright'] as const) {
43
+ if (!options.features.includes(f))
44
+ layers.push(`variants/features/no-${f}`)
45
+ }
46
+ } else {
47
+ layers.push(`variants/framework/${options.framework}`)
48
+ }
49
+ if (!options.features.includes('github-ci'))
50
+ layers.push('variants/features/no-github-ci')
51
+ return layers
52
+ }
53
+
54
+ export const loadLayer = async (app: App, name: string): Promise<Layer> => {
55
+ const dir = join(TEMPLATES_DIR, app, name)
56
+ if (!existsSync(dir))
57
+ throw new Error(`template layer not found: ${app}/${name}`)
58
+ const manifestPath = join(dir, 'variant.json')
59
+ const manifest: VariantManifest = existsSync(manifestPath)
60
+ ? (JSON.parse(await readFile(manifestPath, 'utf8')) as VariantManifest)
61
+ : {}
62
+ return { name, dir, manifest }
63
+ }
64
+
65
+ /** All files under `dir`, as paths relative to it. Symlinks are skipped (the scaffold recreates them). */
66
+ export const walk = async (dir: string, root = dir): Promise<string[]> => {
67
+ const entries = await readdir(dir, { withFileTypes: true })
68
+ const files = await Promise.all(
69
+ entries.map(async (e) => {
70
+ const full = join(dir, e.name)
71
+ if (e.isSymbolicLink()) return []
72
+ if (e.isDirectory()) return walk(full, root)
73
+ return [relative(root, full)]
74
+ }),
75
+ )
76
+ return files.flat().sort()
77
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: ai-rules
3
+ description: Consult the company ai-rules mirrored in docs/ai-rules before writing code that they cover (test selectors, and in the FE template also stack, TanStack, shadcn, a11y). Use whenever a rule might apply.
4
+ ---
5
+
6
+ # ai-rules
7
+
8
+ 1. `AGENTS.md` → "## ai-rules" table maps topic → `docs/ai-rules/<file>`. Read the relevant file section, not the whole file.
9
+ 2. The rules are company-wide and **read-only**: never edit `docs/ai-rules/*`. If a rule looks wrong, propose a change upstream and keep following it here.
10
+ 3. A deliberate deviation must be listed in `docs/agents/deviations.md` with the rule § and the reason, then `bun run agents:build`.
11
+ 4. `bun run rules:check` fails when the mirror drifted from upstream (`AI_RULES_PATH` or `~/Progetti/ai-rules`): run `bun run rules:sync` and commit `docs/ai-rules/` + `MANIFEST.json` in their own `chore(rules): sync ai-rules` commit.
12
+ 5. The "## Non-negotiables" digest in `AGENTS.md` must never contradict `docs/ai-rules/`; the full text wins.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: plan-feature
3
+ description: Turn a feature request into TASK.md with small verifiable increments (one commit each). Use before writing code for anything bigger than a one-line fix.
4
+ ---
5
+
6
+ # plan-feature
7
+
8
+ 1. Copy `TASK.md.template` to `TASK.md` (git-ignored is fine; it may also be committed with the feature).
9
+ 2. Fill **Goal** in one sentence and **Context** with the files that will change (read them first).
10
+ 3. Split into increments of ≤ 1 hour each. Each row: what, files, how to verify (a test name or a curl), commit subject in Conventional Commits.
11
+ 4. Ordering rule: schema → repository → router → openapi → test, or test-first when behaviour is clear (see `tdd-step`).
12
+ 5. Anything that would deviate from `docs/ai-rules/` goes into the **Deviations** section _before_ implementation.
13
+ 6. Show the plan and wait for confirmation only if a decision is not covered by `AGENTS.md`; otherwise start with increment 1.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: review-diff
3
+ description: Review the working-tree diff against AGENTS.md before committing. Use at the end of every increment or when asked "review".
4
+ ---
5
+
6
+ # review-diff
7
+
8
+ 1. `git diff` and `git diff --staged`; read `AGENTS.md`.
9
+ 2. Run `bun run verify`; a red verify is finding #1.
10
+ 3. Walk the checklist in `.claude/agents/reviewer.md` (contracts, types, runtime, tests, openapi, generated files untouched).
11
+ 4. Report only violations, as `path:line — problem — fix`. No praise, no restating the diff.
12
+ 5. End with APPROVE / REQUEST CHANGES.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: tdd-step
3
+ description: Execute one increment red → green → refactor → verify → commit. Use for every increment of TASK.md.
4
+ ---
5
+
6
+ # tdd-step
7
+
8
+ 1. **Red**: write the failing test first in `test/<module>.test.ts` (supertest on `createApp()`). Run `bunx vitest run test/<module>.test.ts` and confirm it fails for the right reason.
9
+ 2. **Green**: minimal code in `src/modules/<module>/`. No extra abstractions, no speculative options.
10
+ 3. **Refactor** only what the new code touched. Keep the response shapes from `AGENTS.md`.
11
+ 4. `openapi.yaml` updated if a route changed.
12
+ 5. `bun run verify` must be green (typecheck, lint, format, AGENTS check, coverage ≥80%).
13
+ 6. One commit: `type(scope): subject` in English, body in Italian explaining _why_ (not what). Show `git log --oneline -1`.
14
+ 7. Tick the increment in `TASK.md`.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: reviewer
3
+ description: Reviews a diff against AGENTS.md conventions and docs/ai-rules. Use after each increment, before committing.
4
+ tools: Read, Grep, Glob, Bash
5
+ model: inherit
6
+ ---
7
+
8
+ You review the current diff (`git diff` + `git diff --staged`) of __NAME__. Read `AGENTS.md` first.
9
+
10
+ Check, in this order, and report only violations with file:line and a one-line fix:
11
+
12
+ 1. `bun run verify` is green (run it).
13
+ 2. Conventions: `{ data, page, limit, total }` on lists, `HttpError` + `{ error, details? }`, `validate()` on every route with input, status codes 200/201/404/422/500.
14
+ 3. Types: no `any`, `@ts-ignore`, `enum`, `!`; `import type` where possible.
15
+ 4. Runtime: no `Bun.*` in `src/`.
16
+ 5. Tests: every new route has a supertest file covering happy path, 422 and 404.
17
+ 6. `openapi.yaml` updated for any route change.
18
+ 7. Nothing edited by hand in `AGENTS.md`, `docs/ai-rules/`, `src/data/*.json`.
19
+
20
+ Finish with "APPROVE" or "REQUEST CHANGES" and, if changes, the commit message subject you would use.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # PostToolUse hook: prettier + eslint --fix on the file just edited.
3
+ # Reads $CLAUDE_FILE_PATH, falls back to the hook JSON on stdin. Exit 0 on anything not lintable.
4
+ set -uo pipefail
5
+
6
+ file="${CLAUDE_FILE_PATH:-}"
7
+ if [ -z "$file" ] && [ ! -t 0 ]; then
8
+ file="$(bun -e 'const s = await Bun.stdin.text(); try { const j = JSON.parse(s); console.log(j.tool_input?.file_path ?? j.file_path ?? "") } catch { console.log("") }')"
9
+ fi
10
+ [ -n "$file" ] && [ -f "$file" ] || exit 0
11
+
12
+ case "$file" in
13
+ *.ts|*.tsx|*.js|*.jsx|*.json|*.md|*.yaml|*.yml|*.css) ;;
14
+ *) exit 0 ;;
15
+ esac
16
+
17
+ cd "$(dirname "${BASH_SOURCE[0]}")/../.." || exit 0
18
+ bunx prettier --write --log-level warn "$file" || true
19
+ case "$file" in
20
+ *.ts|*.tsx|*.js|*.jsx) bunx eslint --fix --no-warn-ignored "$file" || true ;;
21
+ esac
22
+ exit 0
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "permissions": {
4
+ "allow": [
5
+ "Bash(bun run:*)",
6
+ "Bash(bun install:*)",
7
+ "Bash(bun add:*)",
8
+ "Bash(bunx vitest:*)",
9
+ "Bash(bunx eslint:*)",
10
+ "Bash(bunx prettier:*)",
11
+ "Bash(bunx tsc:*)",
12
+ "Bash(git status:*)",
13
+ "Bash(git diff:*)",
14
+ "Bash(git log:*)",
15
+ "Bash(git add:*)",
16
+ "Bash(git commit:*)"
17
+ ],
18
+ "deny": [
19
+ "Bash(git push:*)",
20
+ "Bash(rm -rf:*)",
21
+ "Read(.env)",
22
+ "Read(.env.*)",
23
+ "Edit(AGENTS.md)",
24
+ "Edit(docs/ai-rules/**)",
25
+ "Edit(src/data/*.json)"
26
+ ]
27
+ },
28
+ "hooks": {
29
+ "PostToolUse": [
30
+ {
31
+ "matcher": "Edit|Write|MultiEdit",
32
+ "hooks": [
33
+ { "type": "command", "command": "bash .claude/hooks/format.sh" }
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+ }
@@ -0,0 +1,7 @@
1
+ name = "reviewer"
2
+ description = "Reviews the diff against AGENTS.md conventions and docs/ai-rules."
3
+ instructions = """
4
+ Read AGENTS.md, then `git diff` and `git diff --staged`. Run `bun run verify`.
5
+ Report only violations as `path:line — problem — fix`, following the checklist in .claude/agents/reviewer.md.
6
+ End with APPROVE or REQUEST CHANGES.
7
+ """
@@ -0,0 +1,10 @@
1
+ # Codex CLI project config. AGENTS.md is picked up natively; hooks in hooks.json mirror .claude/settings.json.
2
+ model = "gpt-5-codex"
3
+ approval_policy = "on-request"
4
+ sandbox_mode = "workspace-write"
5
+
6
+ [projects."."]
7
+ trust_level = "trusted"
8
+
9
+ [agents]
10
+ reviewer = "agents/reviewer.toml"
@@ -0,0 +1,12 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "apply_patch|write_file|edit_file",
6
+ "hooks": [
7
+ { "type": "command", "command": "bash .claude/hooks/format.sh" }
8
+ ]
9
+ }
10
+ ]
11
+ }
12
+ }
@@ -0,0 +1,2 @@
1
+ PORT=4000
2
+ LOG_LEVEL=info
@@ -0,0 +1,18 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ verify:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: oven-sh/setup-bun@v2
14
+ with:
15
+ bun-version: latest
16
+ - run: bun install --frozen-lockfile
17
+ - run: bun run rules:check
18
+ - run: bun run verify
@@ -0,0 +1,10 @@
1
+ node_modules
2
+ dist
3
+ coverage
4
+ bun.lock
5
+ docs/ai-rules
6
+ AGENTS.md
7
+
8
+ docs/agents/deviations.md
9
+ docs/agents/fragments.json
10
+ eslint.variants.json
@@ -0,0 +1,20 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "printWidth": 80,
5
+ "tabWidth": 2,
6
+ "trailingComma": "all",
7
+ "bracketSpacing": true,
8
+ "arrowParens": "always",
9
+ "endOfLine": "lf",
10
+ "plugins": ["@trivago/prettier-plugin-sort-imports"],
11
+ "importOrder": [
12
+ "^react",
13
+ "^@tanstack/(.*)$",
14
+ "<THIRD_PARTY_MODULES>",
15
+ "^@/(.*)$",
16
+ "^[./]"
17
+ ],
18
+ "importOrderSeparation": true,
19
+ "importOrderSortSpecifiers": true
20
+ }
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,43 @@
1
+ # __NAME__
2
+
3
+ Mock REST API built with **Bun + Express 5 + Zod**. Backend for [`simone-fe-template`](../simone-fe-template) and pilot for the `create-simone-app` generator (`api/base` + `framework/express` variant).
4
+
5
+ ## Quickstart
6
+
7
+ ```bash
8
+ bun install
9
+ cp .env.example .env
10
+ bun run dev # http://localhost:4000
11
+ bun run verify # typecheck + lint + format + AGENTS check + tests (coverage ≥ 80%)
12
+ ```
13
+
14
+ ## Endpoints
15
+
16
+ | Method | Path | Notes |
17
+ | ------ | ------------------------------ | -------------------------------------------------------------------------- |
18
+ | GET | `/api/health` | liveness |
19
+ | GET | `/api/users?page&limit&q&sort` | `{ data, page, limit, total }`, `sort=-name` for desc |
20
+ | GET | `/api/users/:id` | 404 `{ error }` |
21
+ | GET | `/api/products?category&sort` | same list contract |
22
+ | GET | `/api/products/:id` | |
23
+ | POST | `/api/orders` | `{ userId, items[{ productId, qty }] }` → 201, prices resolved server-side |
24
+ | GET | `/api/stats` | counts, revenue, top category |
25
+ | GET | `/docs.json` | parsed `openapi.yaml` |
26
+
27
+ Validation errors → `422 { error, details: [{ path, message }] }`.
28
+
29
+ ## Data
30
+
31
+ `src/data/*.json` are committed fixtures (50 users, 20 products, 10 orders) generated by `bun run seed:regen` with a fixed faker seed. The API keeps them in memory; `POST /api/orders` mutates memory only.
32
+
33
+ ## AI harness
34
+
35
+ - `AGENTS.md` (generated from `docs/agents/*.md` via `bun run agents:build`) is the single source of conventions; `CLAUDE.md` just includes it.
36
+ - Company **ai-rules** are mirrored read-only in `docs/ai-rules/` with `MANIFEST.json`; `bun run rules:check` fails on drift, `bun run rules:sync` refreshes from `$AI_RULES_PATH` (default `~/Progetti/ai-rules`).
37
+ - Skills in `.agents/skills/` (shared by Claude Code via the `.claude/skills` symlink and Codex): `ai-rules`, `plan-feature`, `tdd-step`, `review-diff`.
38
+ - `.claude/settings.json`: permissions + PostToolUse hook `format.sh` (prettier + eslint --fix). `.codex/` mirrors the same hook and reviewer agent.
39
+ - `TASK.md.template`: per-feature plan with one commit per increment.
40
+
41
+ ## Scripts
42
+
43
+ `dev` · `build` · `test` · `test:watch` · `lint` · `lint:fix` · `format` · `format:check` · `typecheck` · `verify` · `rules:sync` · `rules:check` · `seed:regen` · `agents:build` · `agents:check`