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.
- package/README.md +70 -0
- package/bin/create-simone-app.ts +6 -0
- package/package.json +61 -0
- package/src/cli.ts +97 -0
- package/src/matrix.ts +39 -0
- package/src/options.ts +137 -0
- package/src/prompts.ts +108 -0
- package/src/scaffold.ts +304 -0
- package/src/templates.ts +77 -0
- package/templates/api/base/.agents/skills/ai-rules/SKILL.md +12 -0
- package/templates/api/base/.agents/skills/plan-feature/SKILL.md +13 -0
- package/templates/api/base/.agents/skills/review-diff/SKILL.md +12 -0
- package/templates/api/base/.agents/skills/tdd-step/SKILL.md +14 -0
- package/templates/api/base/.claude/agents/reviewer.md +20 -0
- package/templates/api/base/.claude/hooks/format.sh +22 -0
- package/templates/api/base/.claude/settings.json +38 -0
- package/templates/api/base/.codex/agents/reviewer.toml +7 -0
- package/templates/api/base/.codex/config.toml +10 -0
- package/templates/api/base/.codex/hooks.json +12 -0
- package/templates/api/base/.env.example +2 -0
- package/templates/api/base/.github/workflows/ci.yml +18 -0
- package/templates/api/base/.prettierignore +10 -0
- package/templates/api/base/.prettierrc +20 -0
- package/templates/api/base/CLAUDE.md +1 -0
- package/templates/api/base/README.md +43 -0
- package/templates/api/base/TASK.md.template +30 -0
- package/templates/api/base/_gitignore +8 -0
- package/templates/api/base/docs/agents/base.md +54 -0
- package/templates/api/base/eslint.config.js +21 -0
- package/templates/api/base/openapi.yaml +233 -0
- package/templates/api/base/package.json +41 -0
- package/templates/api/base/scripts/agents-build.ts +26 -0
- package/templates/api/base/scripts/rules-check.ts +48 -0
- package/templates/api/base/scripts/rules-sync.ts +35 -0
- package/templates/api/base/scripts/rules.config.ts +28 -0
- package/templates/api/base/scripts/seed-regen.ts +64 -0
- package/templates/api/base/src/data/orders.json +187 -0
- package/templates/api/base/src/data/products.json +142 -0
- package/templates/api/base/src/data/repository.ts +55 -0
- package/templates/api/base/src/data/schemas.ts +44 -0
- package/templates/api/base/src/data/users.json +352 -0
- package/templates/api/base/src/lib/appName.ts +2 -0
- package/templates/api/base/src/lib/http-error.ts +14 -0
- package/templates/api/base/src/lib/pagination.ts +55 -0
- package/templates/api/base/src/modules/orders/orders.repository.ts +26 -0
- package/templates/api/base/src/modules/orders/orders.schema.ts +11 -0
- package/templates/api/base/src/modules/products/products.repository.ts +17 -0
- package/templates/api/base/src/modules/products/products.schema.ts +21 -0
- package/templates/api/base/src/modules/stats/stats.repository.ts +37 -0
- package/templates/api/base/src/modules/users/users.repository.ts +26 -0
- package/templates/api/base/src/modules/users/users.schema.ts +15 -0
- package/templates/api/base/test/docs.test.ts +23 -0
- package/templates/api/base/test/health.test.ts +19 -0
- package/templates/api/base/test/orders.test.ts +82 -0
- package/templates/api/base/test/products.test.ts +54 -0
- package/templates/api/base/test/repository.test.ts +45 -0
- package/templates/api/base/test/smoke.test.ts +7 -0
- package/templates/api/base/test/stats.test.ts +40 -0
- package/templates/api/base/test/users.test.ts +84 -0
- package/templates/api/base/tsconfig.json +22 -0
- package/templates/api/base/vitest.config.ts +16 -0
- package/templates/api/variants/features/no-github-ci/variant.json +1 -0
- package/templates/api/variants/framework/express/docs/agents/framework-express.md +9 -0
- package/templates/api/variants/framework/express/src/app.ts +37 -0
- package/templates/api/variants/framework/express/src/middleware/errorHandler.ts +13 -0
- package/templates/api/variants/framework/express/src/middleware/validate.ts +59 -0
- package/templates/api/variants/framework/express/src/modules/docs/docs.router.ts +15 -0
- package/templates/api/variants/framework/express/src/modules/health/health.router.ts +7 -0
- package/templates/api/variants/framework/express/src/modules/orders/orders.router.ts +14 -0
- package/templates/api/variants/framework/express/src/modules/products/products.router.ts +24 -0
- package/templates/api/variants/framework/express/src/modules/stats/stats.router.ts +9 -0
- package/templates/api/variants/framework/express/src/modules/users/users.router.ts +24 -0
- package/templates/api/variants/framework/express/src/server.ts +8 -0
- package/templates/api/variants/framework/express/test/errorHandler.test.ts +89 -0
- package/templates/api/variants/framework/express/variant.json +15 -0
- package/templates/fe/base/.agents/skills/ai-rules/SKILL.md +12 -0
- package/templates/fe/base/.agents/skills/plan-feature/SKILL.md +13 -0
- package/templates/fe/base/.agents/skills/review-diff/SKILL.md +12 -0
- package/templates/fe/base/.agents/skills/tdd-step/SKILL.md +14 -0
- package/templates/fe/base/.claude/agents/reviewer.md +21 -0
- package/templates/fe/base/.claude/hooks/format.sh +22 -0
- package/templates/fe/base/.claude/settings.json +41 -0
- package/templates/fe/base/.codex/agents/reviewer.toml +7 -0
- package/templates/fe/base/.codex/config.toml +10 -0
- package/templates/fe/base/.codex/hooks.json +12 -0
- package/templates/fe/base/.env.example +2 -0
- package/templates/fe/base/.github/workflows/ci.yml +37 -0
- package/templates/fe/base/.prettierignore +14 -0
- package/templates/fe/base/.prettierrc +20 -0
- package/templates/fe/base/CLAUDE.md +1 -0
- package/templates/fe/base/README.md +37 -0
- package/templates/fe/base/TASK.md.template +31 -0
- package/templates/fe/base/_gitignore +11 -0
- package/templates/fe/base/docs/agents/base.md +69 -0
- package/templates/fe/base/e2e/users.spec.ts +82 -0
- package/templates/fe/base/eslint.config.js +169 -0
- package/templates/fe/base/index.html +13 -0
- package/templates/fe/base/package.json +63 -0
- package/templates/fe/base/playwright.config.ts +17 -0
- package/templates/fe/base/public/favicon.svg +1 -0
- package/templates/fe/base/public/icons.svg +24 -0
- package/templates/fe/base/scripts/agents-build.ts +26 -0
- package/templates/fe/base/scripts/rules-check.ts +48 -0
- package/templates/fe/base/scripts/rules-sync.ts +35 -0
- package/templates/fe/base/scripts/rules.config.ts +35 -0
- package/templates/fe/base/src/components/shared/EmptyState.tsx +23 -0
- package/templates/fe/base/src/components/shared/ErrorState.tsx +37 -0
- package/templates/fe/base/src/components/shared/PageHeader.tsx +21 -0
- package/templates/fe/base/src/features/users/UserSearch.tsx +48 -0
- package/templates/fe/base/src/features/users/UsersPage.tsx +58 -0
- package/templates/fe/base/src/features/users/UsersTable.tsx +245 -0
- package/templates/fe/base/src/features/users/UsersTableSkeleton.tsx +16 -0
- package/templates/fe/base/src/features/users/__tests__/UserSearch.test.tsx +30 -0
- package/templates/fe/base/src/features/users/__tests__/UsersPage.test.tsx +69 -0
- package/templates/fe/base/src/features/users/__tests__/UsersTable.test.tsx +120 -0
- package/templates/fe/base/src/features/users/__tests__/useUsers.test.tsx +62 -0
- package/templates/fe/base/src/features/users/useUsers.ts +22 -0
- package/templates/fe/base/src/features/users/users.api.ts +22 -0
- package/templates/fe/base/src/features/users/users.schema.ts +43 -0
- package/templates/fe/base/src/lib/apiClient.test.ts +56 -0
- package/templates/fe/base/src/lib/apiClient.ts +42 -0
- package/templates/fe/base/src/lib/appName.ts +2 -0
- package/templates/fe/base/src/lib/env.test.ts +21 -0
- package/templates/fe/base/src/lib/env.ts +26 -0
- package/templates/fe/base/src/lib/queryClient.ts +15 -0
- package/templates/fe/base/src/lib/router.ts +11 -0
- package/templates/fe/base/src/main.tsx +21 -0
- package/templates/fe/base/src/routeTree.gen.ts +77 -0
- package/templates/fe/base/src/routes/__root.tsx +83 -0
- package/templates/fe/base/src/routes/index.tsx +26 -0
- package/templates/fe/base/src/routes/users/index.tsx +24 -0
- package/templates/fe/base/src/stores/seam.test.ts +21 -0
- package/templates/fe/base/src/test/api-mock.ts +22 -0
- package/templates/fe/base/src/test/fixtures/users.ts +39 -0
- package/templates/fe/base/src/test/msw/handlers.ts +9 -0
- package/templates/fe/base/src/test/msw/server.ts +5 -0
- package/templates/fe/base/src/test/render.tsx +45 -0
- package/templates/fe/base/src/test/setup.ts +15 -0
- package/templates/fe/base/tsconfig.json +25 -0
- package/templates/fe/base/vite.config.ts +22 -0
- package/templates/fe/base/vitest.config.ts +21 -0
- package/templates/fe/variants/features/no-github-ci/variant.json +1 -0
- package/templates/fe/variants/features/no-msw/src/test/api-mock.ts +52 -0
- package/templates/fe/variants/features/no-msw/src/test/setup.ts +13 -0
- package/templates/fe/variants/features/no-msw/variant.json +4 -0
- package/templates/fe/variants/features/no-playwright/.github/workflows/ci.yml +19 -0
- package/templates/fe/variants/features/no-playwright/variant.json +5 -0
- package/templates/fe/variants/state/jotai/docs/agents/state-jotai.md +5 -0
- package/templates/fe/variants/state/jotai/src/stores/index.ts +15 -0
- package/templates/fe/variants/state/jotai/src/stores/ui.atoms.test.tsx +29 -0
- package/templates/fe/variants/state/jotai/src/stores/ui.atoms.ts +39 -0
- package/templates/fe/variants/state/jotai/variant.json +12 -0
- package/templates/fe/variants/state/zustand/docs/agents/state-zustand.md +5 -0
- package/templates/fe/variants/state/zustand/src/stores/index.ts +12 -0
- package/templates/fe/variants/state/zustand/src/stores/ui.store.test.ts +34 -0
- package/templates/fe/variants/state/zustand/src/stores/ui.store.ts +62 -0
- package/templates/fe/variants/state/zustand/variant.json +5 -0
- package/templates/fe/variants/ui/base-ui/components.json +25 -0
- package/templates/fe/variants/ui/base-ui/docs/agents/ui-base-ui.md +7 -0
- package/templates/fe/variants/ui/base-ui/src/components/ui/alert.tsx +75 -0
- package/templates/fe/variants/ui/base-ui/src/components/ui/button.tsx +57 -0
- package/templates/fe/variants/ui/base-ui/src/components/ui/input.tsx +19 -0
- package/templates/fe/variants/ui/base-ui/src/components/ui/skeleton.tsx +13 -0
- package/templates/fe/variants/ui/base-ui/src/components/ui/table.tsx +113 -0
- package/templates/fe/variants/ui/base-ui/src/index.css +130 -0
- package/templates/fe/variants/ui/base-ui/src/lib/utils.test.ts +10 -0
- package/templates/fe/variants/ui/base-ui/src/lib/utils.ts +1 -0
- package/templates/fe/variants/ui/base-ui/variant.json +23 -0
- package/templates/fe/variants/ui/radix/components.json +25 -0
- package/templates/fe/variants/ui/radix/docs/agents/ui-radix.md +7 -0
- package/templates/fe/variants/ui/radix/src/components/ui/alert.tsx +75 -0
- package/templates/fe/variants/ui/radix/src/components/ui/button.tsx +66 -0
- package/templates/fe/variants/ui/radix/src/components/ui/input.tsx +18 -0
- package/templates/fe/variants/ui/radix/src/components/ui/skeleton.tsx +13 -0
- package/templates/fe/variants/ui/radix/src/components/ui/table.tsx +113 -0
- package/templates/fe/variants/ui/radix/src/index.css +130 -0
- package/templates/fe/variants/ui/radix/src/lib/utils.test.ts +10 -0
- package/templates/fe/variants/ui/radix/src/lib/utils.ts +1 -0
- package/templates/fe/variants/ui/radix/variant.json +23 -0
- package/templates/fullstack/.github/workflows/ci.yml +18 -0
- package/templates/fullstack/AGENTS.md +7 -0
- package/templates/fullstack/CLAUDE.md +1 -0
- package/templates/fullstack/README.md +16 -0
- package/templates/fullstack/_gitignore +11 -0
- 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).
|
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
|