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
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # create-simone-app
2
+
3
+ Scaffold a React/TypeScript frontend, a Bun/Express (or Hono) mock API, or both, that follow the H&D **ai-rules** out of the box. Bun-first, no templating language: the templates are plain, lintable projects composed as **base + variants**.
4
+
5
+ ```bash
6
+ bunx create-simone-app my-app # wizard
7
+ bun create simone-app my-app # same thing
8
+ bunx create-simone-app my-app --preset fe --state jotai --ui base-ui --yes
9
+ bunx create-simone-app my-app --preset fullstack --framework hono --features msw,github-ci --yes --dry-run
10
+ ```
11
+
12
+ ## What you get
13
+
14
+ | Preset | Content |
15
+ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | `fe` | Vite 7 + React 19 + TanStack Router/Query/Table + Tailwind v4 + shadcn, `/users` server-side table, MSW tests, Playwright e2e, ESLint that enforces the dogmas |
17
+ | `api` | Bun + Express 5 (or Hono) + Zod mock REST API with deterministic fixtures, supertest, hand-written OpenAPI at `/docs.json` |
18
+ | `fullstack` | both as Bun workspaces under `apps/`, one `bun run verify` |
19
+
20
+ Every project ships the AI harness: generated `AGENTS.md` (from `docs/agents/*.md` fragments), `docs/ai-rules/` mirror + `MANIFEST.json` (synced at scaffold when `AI_RULES_PATH` or `~/Progetti/ai-rules` exists), skills for Claude Code and Codex, a PostToolUse format hook, `TASK.md.template`, CI.
21
+
22
+ ## Wizard
23
+
24
+ The wizard asks **only** where the ai-rules leave a choice, or where we deliberately deviate (and then writes the row in `## Deviations from ai-rules`):
25
+
26
+ | Dimension | Values | Default |
27
+ | ------------- | ------------------------------------------------------- | --------- |
28
+ | `--preset` | `fe` `api` `fullstack` | `fe` |
29
+ | `--state` | `zustand` `jotai` (⚠ deviation core-rules §7.1) | `zustand` |
30
+ | `--ui` | `radix` `base-ui` `react-aria` | `radix` |
31
+ | `--framework` | `express` `hono` | `express` |
32
+ | `--features` | comma list of `msw` `playwright` `github-ci`, or `none` | all |
33
+
34
+ Fixed and never asked: Bun, TS strict, Vite 7, React 19, TanStack, Tailwind v4, RHF + Zod, `apiClient`, `data-testid`, port 3000, Vitest, ESLint 9.
35
+
36
+ Flags: `--yes` (defaults for anything missing), `--dry-run` (print the file list, write nothing), `--no-install`, `--no-git`, `--dir <path>`.
37
+
38
+ ## How templates work
39
+
40
+ ```
41
+ templates/
42
+ fe/base/ everything fixed for the FE
43
+ fe/variants/state/{zustand,jotai} src/stores/* + tests + AGENTS fragment (+ deviation)
44
+ fe/variants/ui/{base-ui,radix,react-aria}
45
+ fe/variants/features/no-{msw,playwright,github-ci} removals + overlays
46
+ api/base/
47
+ api/variants/framework/{express,hono}
48
+ api/variants/features/no-github-ci
49
+ fullstack/ root package.json (workspaces), README, AGENTS.md, CI
50
+ ```
51
+
52
+ - Layers are copied in order (later wins); each `variant.json` declares deps to add/remove, files to remove, AGENTS fragments, Deviations rows and the libraries its seam allows (`eslint.variants.json` → the template self-defends: pick Zustand and `import 'jotai'` is an ESLint error).
53
+ - Only token: `__NAME__` (a `src/lib/appName.ts` constant in code). `_gitignore` → `.gitignore`.
54
+ - `AGENTS.md` = `docs/agents/fragments.json` order; the generated project rebuilds it with `bun run agents:build`.
55
+
56
+ ## Quality gates
57
+
58
+ - `bun run verify`: typecheck + lint + format + unit tests, including a **file-tree snapshot** per matrix combo.
59
+ - `bun run matrix [combo…]`: scaffolds every pairwise combination (`src/matrix.ts`) into `.tmp/matrix/`, runs `bun install && bun run verify` and the mocked Playwright e2e where enabled. CI runs one job per combo.
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ bun install
65
+ bun run dev my-app --preset fe --yes --no-install --no-git --dir .tmp/my-app
66
+ bun run verify
67
+ bun run matrix fe-zustand-all
68
+ ```
69
+
70
+ Decisions: [`docs/decisions/ui-libraries.md`](docs/decisions/ui-libraries.md). Phase-1 pilots: `simone-fe-template`, `simone-api-template` (frozen; `templates/` is the source of truth).
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bun
2
+ import { runMain } from 'citty'
3
+
4
+ import { main } from '../src/cli'
5
+
6
+ runMain(main)
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "create-simone-app",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold React/TS frontends and Bun/Express APIs that follow the H&D ai-rules: base + variants, no templating language.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-simone-app": "bin/create-simone-app.ts"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "templates"
14
+ ],
15
+ "engines": {
16
+ "bun": ">=1.2"
17
+ },
18
+ "scripts": {
19
+ "dev": "bun bin/create-simone-app.ts",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "lint": "eslint .",
23
+ "lint:fix": "eslint . --fix",
24
+ "format": "prettier --write .",
25
+ "format:check": "prettier --check .",
26
+ "typecheck": "tsc --noEmit",
27
+ "verify": "bun run typecheck && bun run lint && bun run format:check && bun run test",
28
+ "matrix": "bun scripts/matrix.ts"
29
+ },
30
+ "dependencies": {
31
+ "@clack/prompts": "^1.8.1",
32
+ "citty": "^0.2.2"
33
+ },
34
+ "devDependencies": {
35
+ "@eslint/js": "^10.0.1",
36
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
37
+ "@types/bun": "^1.4.2",
38
+ "eslint": "^9",
39
+ "prettier": "^3.9.8",
40
+ "typescript": "^5",
41
+ "typescript-eslint": "^8.70.0",
42
+ "vitest": "^5"
43
+ },
44
+ "keywords": [
45
+ "create",
46
+ "scaffold",
47
+ "bun",
48
+ "react",
49
+ "tanstack",
50
+ "shadcn",
51
+ "express",
52
+ "hono",
53
+ "ai-rules"
54
+ ],
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/simiriva95/create-simone-app.git"
58
+ },
59
+ "homepage": "https://github.com/simiriva95/create-simone-app#readme",
60
+ "author": "Simone Riva"
61
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,97 @@
1
+ import { defineCommand } from 'citty'
2
+
3
+ import {
4
+ OptionError,
5
+ type RawOptions,
6
+ deviationsFor,
7
+ resolveOptions,
8
+ } from './options'
9
+ import { log, note, outro, wizard } from './prompts'
10
+ import { scaffold } from './scaffold'
11
+
12
+ export const main = defineCommand({
13
+ meta: {
14
+ name: 'create-simone-app',
15
+ description:
16
+ 'Scaffold a project that follows the ai-rules (base + variants).',
17
+ },
18
+ args: {
19
+ name: {
20
+ type: 'positional',
21
+ description: 'project name (kebab-case)',
22
+ required: false,
23
+ },
24
+ preset: { type: 'string', description: 'fe | api | fullstack' },
25
+ state: { type: 'string', description: 'zustand | jotai (FE)' },
26
+ ui: { type: 'string', description: 'base-ui | radix | react-aria (FE)' },
27
+ framework: { type: 'string', description: 'express | hono (API)' },
28
+ features: {
29
+ type: 'string',
30
+ description:
31
+ 'comma list of msw,playwright,github-ci (default all; "none" for none)',
32
+ },
33
+ dir: { type: 'string', description: 'target directory (default ./<name>)' },
34
+ yes: {
35
+ type: 'boolean',
36
+ description: 'no prompts, defaults for anything missing',
37
+ default: false,
38
+ },
39
+ 'dry-run': {
40
+ type: 'boolean',
41
+ description: 'print the plan and the file list, write nothing',
42
+ default: false,
43
+ },
44
+ install: {
45
+ type: 'boolean',
46
+ description: 'run bun install (--no-install to skip)',
47
+ default: true,
48
+ },
49
+ git: {
50
+ type: 'boolean',
51
+ description: 'git init + first commit (--no-git to skip)',
52
+ default: true,
53
+ },
54
+ },
55
+ async run({ args }) {
56
+ const given: RawOptions = {
57
+ name: args.name,
58
+ preset: args.preset,
59
+ state: args.state,
60
+ ui: args.ui,
61
+ framework: args.framework,
62
+ features: args.features,
63
+ dir: args.dir,
64
+ install: args.install,
65
+ git: args.git,
66
+ dryRun: args['dry-run'],
67
+ }
68
+ try {
69
+ const raw =
70
+ args.yes || (given.name && given.preset) ? given : await wizard(given)
71
+ const options = resolveOptions(raw)
72
+ const deviations = deviationsFor(options)
73
+ if (deviations.length) {
74
+ note(
75
+ deviations.map((d) => `${d.choice} → deroga ${d.rule}`).join('\n'),
76
+ 'Deviations from ai-rules (scritte in AGENTS.md)',
77
+ )
78
+ }
79
+ const result = await scaffold(options)
80
+ if (options.dryRun) {
81
+ log.info(
82
+ `dry-run: ${result.files.length} files would be written to ${result.dir}`,
83
+ )
84
+ for (const file of result.files) console.log(` ${file}`)
85
+ } else {
86
+ for (const warning of result.warnings) log.warn(warning)
87
+ outro(`Done. cd ${options.dir} && bun run verify`)
88
+ }
89
+ } catch (error) {
90
+ if (error instanceof OptionError) {
91
+ log.error(error.message)
92
+ process.exit(2)
93
+ }
94
+ throw error
95
+ }
96
+ },
97
+ })
package/src/matrix.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { RawOptions } from './options'
2
+
3
+ /**
4
+ * Pairwise coverage of the wizard dimensions (preset × state × features).
5
+ * Every combination is scaffolded in CI and must pass `bun run verify` (+ e2e when Playwright is on).
6
+ * Add a row when a new variant lands; the snapshot test pins each file tree.
7
+ */
8
+ export const MATRIX: Record<string, RawOptions> = {
9
+ 'fe-zustand-all': { preset: 'fe', state: 'zustand' },
10
+ 'fe-jotai-none': { preset: 'fe', state: 'jotai', features: 'none' },
11
+ 'fe-zustand-no-playwright': {
12
+ preset: 'fe',
13
+ state: 'zustand',
14
+ features: 'msw,github-ci',
15
+ },
16
+ 'fe-jotai-no-msw': {
17
+ preset: 'fe',
18
+ state: 'jotai',
19
+ features: 'playwright,github-ci',
20
+ },
21
+ 'fe-baseui-jotai-no-msw': {
22
+ preset: 'fe',
23
+ state: 'jotai',
24
+ ui: 'base-ui',
25
+ features: 'playwright,github-ci',
26
+ },
27
+ 'api-express-all': { preset: 'api', framework: 'express' },
28
+ 'api-express-no-ci': {
29
+ preset: 'api',
30
+ framework: 'express',
31
+ features: 'none',
32
+ },
33
+ 'fullstack-jotai-all': { preset: 'fullstack', state: 'jotai' },
34
+ 'fullstack-zustand-none': {
35
+ preset: 'fullstack',
36
+ state: 'zustand',
37
+ features: 'none',
38
+ },
39
+ }
package/src/options.ts ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Everything the wizard may ask. Dogmas (Bun, TS strict, Vite 7, React 19, TanStack, Tailwind v4,
3
+ * RHF+Zod, apiClient, data-testid, port 3000, Vitest, ESLint 9) are fixed and never appear here.
4
+ */
5
+ export const PRESETS = ['fe', 'api', 'fullstack'] as const
6
+ export type Preset = (typeof PRESETS)[number]
7
+
8
+ export const STATES = ['zustand', 'jotai'] as const
9
+ export type State = (typeof STATES)[number]
10
+
11
+ export const UIS = ['base-ui', 'radix', 'react-aria'] as const
12
+ export type Ui = (typeof UIS)[number]
13
+
14
+ export const FRAMEWORKS = ['express', 'hono'] as const
15
+ export type Framework = (typeof FRAMEWORKS)[number]
16
+
17
+ export const FEATURES = ['msw', 'playwright', 'github-ci'] as const
18
+ export type Feature = (typeof FEATURES)[number]
19
+
20
+ export interface Options {
21
+ name: string
22
+ preset: Preset
23
+ state: State
24
+ ui: Ui
25
+ framework: Framework
26
+ features: Feature[]
27
+ install: boolean
28
+ git: boolean
29
+ dryRun: boolean
30
+ /** Target directory (absolute or relative to cwd). Defaults to `./<name>`. */
31
+ dir: string
32
+ }
33
+
34
+ export const DEFAULTS = {
35
+ preset: 'fe',
36
+ state: 'zustand',
37
+ ui: 'radix',
38
+ framework: 'express',
39
+ features: [...FEATURES],
40
+ } as const satisfies Pick<
41
+ Options,
42
+ 'preset' | 'state' | 'ui' | 'framework' | 'features'
43
+ >
44
+
45
+ /** Choices that deviate from the ai-rules: the wizard warns, the scaffold writes the Deviations row. */
46
+ export const DEVIATIONS: Partial<Record<State | Ui | Framework, string>> = {
47
+ jotai: 'core-rules §7.1',
48
+ }
49
+
50
+ export const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,39}$/
51
+
52
+ export class OptionError extends Error {}
53
+
54
+ const oneOf = <T extends string>(
55
+ field: string,
56
+ value: unknown,
57
+ allowed: readonly T[],
58
+ ): T => {
59
+ if (
60
+ typeof value !== 'string' ||
61
+ !(allowed as readonly string[]).includes(value)
62
+ ) {
63
+ throw new OptionError(
64
+ `--${field} must be one of ${allowed.join(', ')} (got ${String(value)})`,
65
+ )
66
+ }
67
+ return value as T
68
+ }
69
+
70
+ export const parseFeatures = (raw: unknown): Feature[] => {
71
+ if (raw === undefined || raw === true) return [...FEATURES]
72
+ if (raw === false || raw === 'none' || raw === '') return []
73
+ const list = (Array.isArray(raw) ? raw : String(raw).split(','))
74
+ .map((f) => String(f).trim())
75
+ .filter(Boolean)
76
+ return list.map((f) => oneOf('features', f, FEATURES))
77
+ }
78
+
79
+ export interface RawOptions {
80
+ name?: unknown
81
+ preset?: unknown
82
+ state?: unknown
83
+ ui?: unknown
84
+ framework?: unknown
85
+ features?: unknown
86
+ install?: unknown
87
+ git?: unknown
88
+ dryRun?: unknown
89
+ dir?: unknown
90
+ }
91
+
92
+ /** Validates CLI/wizard answers into Options; missing values fall back to DEFAULTS. */
93
+ export const resolveOptions = (raw: RawOptions): Options => {
94
+ if (typeof raw.name !== 'string' || !NAME_PATTERN.test(raw.name)) {
95
+ throw new OptionError(
96
+ `project name must match ${NAME_PATTERN} (got ${String(raw.name)})`,
97
+ )
98
+ }
99
+ const preset = oneOf('preset', raw.preset ?? DEFAULTS.preset, PRESETS)
100
+ return {
101
+ name: raw.name,
102
+ preset,
103
+ state: oneOf('state', raw.state ?? DEFAULTS.state, STATES),
104
+ ui: oneOf('ui', raw.ui ?? DEFAULTS.ui, UIS),
105
+ framework: oneOf(
106
+ 'framework',
107
+ raw.framework ?? DEFAULTS.framework,
108
+ FRAMEWORKS,
109
+ ),
110
+ features: parseFeatures(raw.features),
111
+ install: raw.install !== false,
112
+ git: raw.git !== false,
113
+ dryRun: raw.dryRun === true,
114
+ dir: typeof raw.dir === 'string' && raw.dir ? raw.dir : raw.name,
115
+ }
116
+ }
117
+
118
+ /** Which dimensions matter for a preset (the wizard skips the others). */
119
+ export const dimensionsFor = (preset: Preset) => ({
120
+ fe: preset !== 'api',
121
+ api: preset !== 'fe',
122
+ })
123
+
124
+ export const deviationsFor = (
125
+ options: Options,
126
+ ): { choice: string; rule: string }[] => {
127
+ const { fe, api } = dimensionsFor(options.preset)
128
+ const picks = [
129
+ fe && options.state,
130
+ fe && options.ui,
131
+ api && options.framework,
132
+ ].filter((p): p is State | Ui | Framework => typeof p === 'string')
133
+ return picks.flatMap((choice) => {
134
+ const rule = DEVIATIONS[choice]
135
+ return rule ? [{ choice, rule }] : []
136
+ })
137
+ }
package/src/prompts.ts ADDED
@@ -0,0 +1,108 @@
1
+ import * as p from '@clack/prompts'
2
+
3
+ import {
4
+ DEFAULTS,
5
+ DEVIATIONS,
6
+ FEATURES,
7
+ FRAMEWORKS,
8
+ type Feature,
9
+ NAME_PATTERN,
10
+ PRESETS,
11
+ type Preset,
12
+ type RawOptions,
13
+ STATES,
14
+ UIS,
15
+ dimensionsFor,
16
+ } from './options'
17
+
18
+ const bail = (value: unknown): never => {
19
+ if (p.isCancel(value)) {
20
+ p.cancel('Annullato.')
21
+ process.exit(0)
22
+ }
23
+ throw new Error('unexpected prompt value')
24
+ }
25
+
26
+ type SelectOptions<T> = Parameters<typeof p.select<T>>[0]['options']
27
+
28
+ const pick = async <T extends string>(
29
+ message: string,
30
+ values: readonly T[],
31
+ initial: T,
32
+ hint?: Partial<Record<T, string>>,
33
+ ): Promise<T> => {
34
+ // Option<T> is a deferred conditional type for a generic T: build concretely, cast once.
35
+ const options = values.map((v) => ({
36
+ value: v,
37
+ label: v,
38
+ hint: hint?.[v],
39
+ })) as unknown as SelectOptions<T>
40
+ const value = await p.select<T>({ message, initialValue: initial, options })
41
+ return p.isCancel(value) ? bail(value) : value
42
+ }
43
+
44
+ /** Asks only what the ai-rules leave open (or where we deliberately deviate). */
45
+ export const wizard = async (given: RawOptions): Promise<RawOptions> => {
46
+ p.intro('create-simone-app')
47
+ const answers: RawOptions = { ...given }
48
+
49
+ if (typeof answers.name !== 'string') {
50
+ const name = await p.text({
51
+ message: 'Nome progetto',
52
+ placeholder: 'my-app',
53
+ validate: (v) =>
54
+ NAME_PATTERN.test(v ?? '')
55
+ ? undefined
56
+ : 'kebab-case, inizia con lettera o numero',
57
+ })
58
+ answers.name = p.isCancel(name) ? bail(name) : name
59
+ }
60
+
61
+ if (answers.preset === undefined) {
62
+ answers.preset = await pick<Preset>('Preset', PRESETS, DEFAULTS.preset, {
63
+ fe: 'React 19 + TanStack + Tailwind',
64
+ api: 'Bun + Express + Zod mock API',
65
+ fullstack: 'entrambi in Bun workspaces',
66
+ })
67
+ }
68
+ const dims = dimensionsFor(answers.preset as Preset)
69
+
70
+ if (dims.fe && answers.state === undefined) {
71
+ answers.state = await pick('State', STATES, DEFAULTS.state, {
72
+ zustand: 'default (core-rules §7)',
73
+ jotai: `⚠ deroga ${DEVIATIONS.jotai}`,
74
+ })
75
+ }
76
+ if (dims.fe && answers.ui === undefined) {
77
+ answers.ui = await pick('UI primitives (shadcn)', UIS, DEFAULTS.ui, {
78
+ radix: 'default (ui-shadcn §1.1), ecosistema shadcn più ampio',
79
+ 'base-ui': 'nuove primitive degli autori Radix, meno componenti',
80
+ 'react-aria': 'a11y/i18n, bundle più pesante',
81
+ })
82
+ }
83
+ if (dims.api && answers.framework === undefined) {
84
+ answers.framework = await pick(
85
+ 'API framework',
86
+ FRAMEWORKS,
87
+ DEFAULTS.framework,
88
+ {
89
+ express: 'default',
90
+ hono: 'fetch-native, più leggero',
91
+ },
92
+ )
93
+ }
94
+ if (answers.features === undefined) {
95
+ const features = await p.multiselect<Feature>({
96
+ message: 'Extra',
97
+ initialValues: [...DEFAULTS.features],
98
+ required: false,
99
+ options: FEATURES.map((f) => ({ value: f, label: f })),
100
+ })
101
+ answers.features = p.isCancel(features) ? bail(features) : features
102
+ }
103
+ return answers
104
+ }
105
+
106
+ export const log = p.log
107
+ export const outro = p.outro
108
+ export const note = p.note