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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Task: <title>
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
<one sentence: what changes for the API consumer>
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
- Files: <src/modules/…, test/…, openapi.yaml>
|
|
10
|
+
- Related rules: <docs/ai-rules/… §…>
|
|
11
|
+
|
|
12
|
+
## Increments
|
|
13
|
+
|
|
14
|
+
| # | What | Files | Verify | Commit |
|
|
15
|
+
|---|---|---|---|---|
|
|
16
|
+
| 1 | | | `bunx vitest run test/….test.ts` | `feat(scope): …` |
|
|
17
|
+
| 2 | | | | |
|
|
18
|
+
|
|
19
|
+
## Definition of done
|
|
20
|
+
|
|
21
|
+
- [ ] `bun run verify` green on every commit
|
|
22
|
+
- [ ] `openapi.yaml` updated
|
|
23
|
+
- [ ] New routes covered: happy path, 422, 404
|
|
24
|
+
- [ ] `git log --oneline` shows one commit per increment
|
|
25
|
+
|
|
26
|
+
## Deviations from ai-rules
|
|
27
|
+
|
|
28
|
+
| Rule § | Deviation | Why |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| | | |
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# __NAME__
|
|
2
|
+
|
|
3
|
+
Mock REST API (Bun + Express 5 + Zod) used as the backend for `simone-fe-template`
|
|
4
|
+
and as the `api/base` pilot of the `create-simone-app` generator. Standalone: no auth,
|
|
5
|
+
no database, no OIDC (not included on purpose).
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
| Command | What |
|
|
10
|
+
| ------------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `bun run dev` | watch mode on :4000 |
|
|
12
|
+
| `bun run verify` | typecheck + lint + format + AGENTS check + tests (coverage ≥80%) — must be green before every commit |
|
|
13
|
+
| `bun run test:watch` | Vitest watch |
|
|
14
|
+
| `bun run seed:regen` | regenerate `src/data/*.json` (faker, seed 42, deterministic) |
|
|
15
|
+
| `bun run rules:sync` / `rules:check` | mirror ai-rules into `docs/ai-rules/` + `MANIFEST.json` / fail on drift |
|
|
16
|
+
| `bun run agents:build` | regenerate this file from `docs/agents/*.md` (never edit AGENTS.md by hand) |
|
|
17
|
+
|
|
18
|
+
## Structure
|
|
19
|
+
|
|
20
|
+
| Path | Role |
|
|
21
|
+
| --------------------- | ------------------------------------------------------------------------- |
|
|
22
|
+
| `src/app.ts` | `createApp()`: middleware + routers, no listen |
|
|
23
|
+
| `src/server.ts` | listen on `PORT` (4000) |
|
|
24
|
+
| `src/lib/` | `http-error.ts`, `pagination.ts` (`{ data, page, limit, total }`) |
|
|
25
|
+
| `src/middleware/` | `validate.ts` (zod → 422), `errorHandler.ts` (`{ error, details? }`) |
|
|
26
|
+
| `src/modules/<name>/` | `*.schema.ts`, `*.repository.ts`, `*.router.ts` per module |
|
|
27
|
+
| `src/data/` | `schemas.ts` (entity zod types), committed JSON fixtures, `repository.ts` |
|
|
28
|
+
| `test/` | one supertest file per module |
|
|
29
|
+
| `openapi.yaml` | hand-written spec, served at `/docs.json` |
|
|
30
|
+
| `docs/ai-rules/` | read-only mirror of company ai-rules (+ `MANIFEST.json`) |
|
|
31
|
+
| `docs/agents/` | fragments concatenated into `AGENTS.md` |
|
|
32
|
+
| `.agents/skills/` | skills shared by Claude Code (`.claude/skills` symlink) and Codex |
|
|
33
|
+
|
|
34
|
+
## Workflow
|
|
35
|
+
|
|
36
|
+
1. Copy `TASK.md.template` → `TASK.md`, split the work into small increments.
|
|
37
|
+
2. Per increment: failing test → minimal code → `bun run verify` green → one Conventional Commit whose body explains _why_ (Italian body, English subject).
|
|
38
|
+
3. New route = schema + repository + router + test + `openapi.yaml` entry, same commit.
|
|
39
|
+
4. Never edit `docs/ai-rules/*`, `AGENTS.md`, `src/data/*.json` by hand (sync / build / regen instead).
|
|
40
|
+
|
|
41
|
+
## Non-negotiables
|
|
42
|
+
|
|
43
|
+
- Bun for everything (runtime, package manager, scripts). Vitest for tests (runs on Node → `src/` uses `node:*` only).
|
|
44
|
+
- TypeScript strict + `noUncheckedIndexedAccess`. No `any`, `@ts-ignore`, `enum`, non-null `!`; use `unknown` + guards, `as const`.
|
|
45
|
+
- ESLint 9 flat config with `no-duplicate-imports` + `consistent-type-imports` only; import order belongs to Prettier (`@trivago/prettier-plugin-sort-imports`).
|
|
46
|
+
- Every list endpoint returns `{ data, page, limit, total }`; `limit` ≤ 100; `sort=-field` for descending, whitelisted fields.
|
|
47
|
+
- Errors always `{ error, details? }` via `HttpError`; never leak internals on 500.
|
|
48
|
+
- `data-testid` naming `feature-element-qualifier` (see `docs/ai-rules/testing-selectors.md`) applies to any HTML this API ever serves.
|
|
49
|
+
|
|
50
|
+
## ai-rules
|
|
51
|
+
|
|
52
|
+
| Topic | File |
|
|
53
|
+
| ------------------------------ | ------------------------------------ |
|
|
54
|
+
| test selectors (`data-testid`) | `docs/ai-rules/testing-selectors.md` |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
3
|
+
import tseslint from 'typescript-eslint'
|
|
4
|
+
|
|
5
|
+
export default defineConfig(
|
|
6
|
+
globalIgnores(['node_modules', 'dist', 'coverage', 'docs/ai-rules']),
|
|
7
|
+
js.configs.recommended,
|
|
8
|
+
tseslint.configs.strict,
|
|
9
|
+
tseslint.configs.stylistic,
|
|
10
|
+
{
|
|
11
|
+
// core-rules §10.2: only these two import rules; ordering is Prettier's job (§10.1)
|
|
12
|
+
rules: {
|
|
13
|
+
'no-duplicate-imports': 'error',
|
|
14
|
+
'@typescript-eslint/consistent-type-imports': 'error',
|
|
15
|
+
'@typescript-eslint/no-unused-vars': [
|
|
16
|
+
'error',
|
|
17
|
+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
openapi: 3.1.0
|
|
2
|
+
info:
|
|
3
|
+
title: __NAME__
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
description: Mock REST API (Bun + Express 5 + Zod). List endpoints share the `{ data, page, limit, total }` contract.
|
|
6
|
+
servers:
|
|
7
|
+
- url: http://localhost:4000
|
|
8
|
+
paths:
|
|
9
|
+
/api/health:
|
|
10
|
+
get:
|
|
11
|
+
summary: Liveness
|
|
12
|
+
responses:
|
|
13
|
+
'200':
|
|
14
|
+
description: OK
|
|
15
|
+
content:
|
|
16
|
+
application/json:
|
|
17
|
+
schema:
|
|
18
|
+
type: object
|
|
19
|
+
required: [status, uptime]
|
|
20
|
+
properties:
|
|
21
|
+
status: { type: string, const: ok }
|
|
22
|
+
uptime: { type: number }
|
|
23
|
+
/api/users:
|
|
24
|
+
get:
|
|
25
|
+
summary: List users
|
|
26
|
+
parameters:
|
|
27
|
+
- $ref: '#/components/parameters/page'
|
|
28
|
+
- $ref: '#/components/parameters/limit'
|
|
29
|
+
- name: q
|
|
30
|
+
in: query
|
|
31
|
+
description: Case-insensitive match on name or email
|
|
32
|
+
schema: { type: string }
|
|
33
|
+
- name: sort
|
|
34
|
+
in: query
|
|
35
|
+
description: One of name, email, role, createdAt; prefix with - for descending
|
|
36
|
+
schema: { type: string, pattern: '^-?(name|email|role|createdAt)$' }
|
|
37
|
+
responses:
|
|
38
|
+
'200':
|
|
39
|
+
description: Page of users
|
|
40
|
+
content:
|
|
41
|
+
application/json:
|
|
42
|
+
schema:
|
|
43
|
+
allOf:
|
|
44
|
+
- $ref: '#/components/schemas/Page'
|
|
45
|
+
- type: object
|
|
46
|
+
properties:
|
|
47
|
+
data:
|
|
48
|
+
{
|
|
49
|
+
type: array,
|
|
50
|
+
items: { $ref: '#/components/schemas/User' },
|
|
51
|
+
}
|
|
52
|
+
'422': { $ref: '#/components/responses/Validation' }
|
|
53
|
+
/api/users/{id}:
|
|
54
|
+
get:
|
|
55
|
+
summary: Get user
|
|
56
|
+
parameters: [{ $ref: '#/components/parameters/id' }]
|
|
57
|
+
responses:
|
|
58
|
+
'200':
|
|
59
|
+
description: User
|
|
60
|
+
content:
|
|
61
|
+
{
|
|
62
|
+
application/json:
|
|
63
|
+
{ schema: { $ref: '#/components/schemas/User' } },
|
|
64
|
+
}
|
|
65
|
+
'404': { $ref: '#/components/responses/NotFound' }
|
|
66
|
+
/api/products:
|
|
67
|
+
get:
|
|
68
|
+
summary: List products
|
|
69
|
+
parameters:
|
|
70
|
+
- $ref: '#/components/parameters/page'
|
|
71
|
+
- $ref: '#/components/parameters/limit'
|
|
72
|
+
- name: category
|
|
73
|
+
in: query
|
|
74
|
+
schema: { $ref: '#/components/schemas/Category' }
|
|
75
|
+
- name: sort
|
|
76
|
+
in: query
|
|
77
|
+
schema: { type: string, pattern: '^-?(name|category|price|stock)$' }
|
|
78
|
+
responses:
|
|
79
|
+
'200':
|
|
80
|
+
description: Page of products
|
|
81
|
+
content:
|
|
82
|
+
application/json:
|
|
83
|
+
schema:
|
|
84
|
+
allOf:
|
|
85
|
+
- $ref: '#/components/schemas/Page'
|
|
86
|
+
- type: object
|
|
87
|
+
properties:
|
|
88
|
+
data:
|
|
89
|
+
{
|
|
90
|
+
type: array,
|
|
91
|
+
items: { $ref: '#/components/schemas/Product' },
|
|
92
|
+
}
|
|
93
|
+
'422': { $ref: '#/components/responses/Validation' }
|
|
94
|
+
/api/products/{id}:
|
|
95
|
+
get:
|
|
96
|
+
summary: Get product
|
|
97
|
+
parameters: [{ $ref: '#/components/parameters/id' }]
|
|
98
|
+
responses:
|
|
99
|
+
'200':
|
|
100
|
+
description: Product
|
|
101
|
+
content:
|
|
102
|
+
{
|
|
103
|
+
application/json:
|
|
104
|
+
{ schema: { $ref: '#/components/schemas/Product' } },
|
|
105
|
+
}
|
|
106
|
+
'404': { $ref: '#/components/responses/NotFound' }
|
|
107
|
+
/api/orders:
|
|
108
|
+
post:
|
|
109
|
+
summary: Create order (prices resolved server-side)
|
|
110
|
+
requestBody:
|
|
111
|
+
required: true
|
|
112
|
+
content:
|
|
113
|
+
application/json:
|
|
114
|
+
schema:
|
|
115
|
+
type: object
|
|
116
|
+
required: [userId, items]
|
|
117
|
+
properties:
|
|
118
|
+
userId: { type: string }
|
|
119
|
+
items:
|
|
120
|
+
type: array
|
|
121
|
+
minItems: 1
|
|
122
|
+
items:
|
|
123
|
+
type: object
|
|
124
|
+
required: [productId, qty]
|
|
125
|
+
properties:
|
|
126
|
+
productId: { type: string }
|
|
127
|
+
qty: { type: integer, minimum: 1 }
|
|
128
|
+
responses:
|
|
129
|
+
'201':
|
|
130
|
+
description: Created order
|
|
131
|
+
content:
|
|
132
|
+
{
|
|
133
|
+
application/json:
|
|
134
|
+
{ schema: { $ref: '#/components/schemas/Order' } },
|
|
135
|
+
}
|
|
136
|
+
'404': { $ref: '#/components/responses/NotFound' }
|
|
137
|
+
'422': { $ref: '#/components/responses/Validation' }
|
|
138
|
+
/api/stats:
|
|
139
|
+
get:
|
|
140
|
+
summary: Aggregates
|
|
141
|
+
responses:
|
|
142
|
+
'200':
|
|
143
|
+
description: Counts, revenue and top category by revenue
|
|
144
|
+
content:
|
|
145
|
+
application/json:
|
|
146
|
+
schema:
|
|
147
|
+
type: object
|
|
148
|
+
required: [users, products, orders, revenue, topCategory]
|
|
149
|
+
properties:
|
|
150
|
+
users: { type: integer }
|
|
151
|
+
products: { type: integer }
|
|
152
|
+
orders: { type: integer }
|
|
153
|
+
revenue: { type: number }
|
|
154
|
+
topCategory: { type: [string, 'null'] }
|
|
155
|
+
components:
|
|
156
|
+
parameters:
|
|
157
|
+
id: { name: id, in: path, required: true, schema: { type: string } }
|
|
158
|
+
page:
|
|
159
|
+
{
|
|
160
|
+
name: page,
|
|
161
|
+
in: query,
|
|
162
|
+
schema: { type: integer, minimum: 1, default: 1 },
|
|
163
|
+
}
|
|
164
|
+
limit:
|
|
165
|
+
{
|
|
166
|
+
name: limit,
|
|
167
|
+
in: query,
|
|
168
|
+
schema: { type: integer, minimum: 1, maximum: 100, default: 20 },
|
|
169
|
+
}
|
|
170
|
+
responses:
|
|
171
|
+
NotFound:
|
|
172
|
+
description: Resource not found
|
|
173
|
+
content:
|
|
174
|
+
{ application/json: { schema: { $ref: '#/components/schemas/Error' } } }
|
|
175
|
+
Validation:
|
|
176
|
+
description: Invalid input
|
|
177
|
+
content:
|
|
178
|
+
{ application/json: { schema: { $ref: '#/components/schemas/Error' } } }
|
|
179
|
+
schemas:
|
|
180
|
+
Error:
|
|
181
|
+
type: object
|
|
182
|
+
required: [error]
|
|
183
|
+
properties:
|
|
184
|
+
error: { type: string }
|
|
185
|
+
details:
|
|
186
|
+
type: array
|
|
187
|
+
items:
|
|
188
|
+
type: object
|
|
189
|
+
properties: { path: { type: string }, message: { type: string } }
|
|
190
|
+
Page:
|
|
191
|
+
type: object
|
|
192
|
+
required: [data, page, limit, total]
|
|
193
|
+
properties:
|
|
194
|
+
data: { type: array }
|
|
195
|
+
page: { type: integer }
|
|
196
|
+
limit: { type: integer }
|
|
197
|
+
total: { type: integer }
|
|
198
|
+
Category: { type: string, enum: [books, electronics, garden, toys, food] }
|
|
199
|
+
User:
|
|
200
|
+
type: object
|
|
201
|
+
required: [id, name, email, role, createdAt]
|
|
202
|
+
properties:
|
|
203
|
+
id: { type: string, format: uuid }
|
|
204
|
+
name: { type: string }
|
|
205
|
+
email: { type: string, format: email }
|
|
206
|
+
role: { type: string, enum: [admin, editor, viewer] }
|
|
207
|
+
createdAt: { type: string, format: date-time }
|
|
208
|
+
Product:
|
|
209
|
+
type: object
|
|
210
|
+
required: [id, name, category, price, stock]
|
|
211
|
+
properties:
|
|
212
|
+
id: { type: string, format: uuid }
|
|
213
|
+
name: { type: string }
|
|
214
|
+
category: { $ref: '#/components/schemas/Category' }
|
|
215
|
+
price: { type: number }
|
|
216
|
+
stock: { type: integer }
|
|
217
|
+
Order:
|
|
218
|
+
type: object
|
|
219
|
+
required: [id, userId, items, total, createdAt]
|
|
220
|
+
properties:
|
|
221
|
+
id: { type: string, format: uuid }
|
|
222
|
+
userId: { type: string, format: uuid }
|
|
223
|
+
items:
|
|
224
|
+
type: array
|
|
225
|
+
items:
|
|
226
|
+
type: object
|
|
227
|
+
required: [productId, qty, unitPrice]
|
|
228
|
+
properties:
|
|
229
|
+
productId: { type: string, format: uuid }
|
|
230
|
+
qty: { type: integer }
|
|
231
|
+
unitPrice: { type: number }
|
|
232
|
+
total: { type: number }
|
|
233
|
+
createdAt: { type: string, format: date-time }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "bun --watch src/server.ts",
|
|
8
|
+
"build": "bun build src/server.ts --outdir dist --target bun",
|
|
9
|
+
"test": "vitest run --coverage",
|
|
10
|
+
"test:watch": "vitest",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"lint:fix": "eslint . --fix",
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"format:check": "prettier --check .",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"verify": "bun run typecheck && bun run lint && bun run format:check && bun run agents:check && bun run test",
|
|
17
|
+
"rules:sync": "bun scripts/rules-sync.ts",
|
|
18
|
+
"rules:check": "bun scripts/rules-check.ts",
|
|
19
|
+
"seed:regen": "bun scripts/seed-regen.ts",
|
|
20
|
+
"agents:build": "bun scripts/agents-build.ts",
|
|
21
|
+
"agents:check": "bun scripts/agents-build.ts --check"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"yaml": "^2.9.1",
|
|
25
|
+
"zod": "^4.6.5"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@eslint/js": "^10.0.1",
|
|
29
|
+
"@faker-js/faker": "^10.6.0",
|
|
30
|
+
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
31
|
+
"@types/bun": "^1.4.2",
|
|
32
|
+
"@types/supertest": "^7.2.1",
|
|
33
|
+
"@vitest/coverage-v8": "^5.0.1",
|
|
34
|
+
"eslint": "^9",
|
|
35
|
+
"prettier": "^3.9.8",
|
|
36
|
+
"supertest": "^7.2.2",
|
|
37
|
+
"typescript": "^5",
|
|
38
|
+
"typescript-eslint": "^8.70.0",
|
|
39
|
+
"vitest": "^5.0.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
/** Fragment order, written by create-simone-app for the chosen variants. */
|
|
4
|
+
const FRAGMENTS: string[] = await Bun.file('docs/agents/fragments.json').json()
|
|
5
|
+
const OUT = 'AGENTS.md'
|
|
6
|
+
const HEADER =
|
|
7
|
+
'<!-- Generated by `bun run agents:build` from docs/agents/*.md. Edit the fragments, not this file. -->\n\n'
|
|
8
|
+
|
|
9
|
+
const parts = await Promise.all(
|
|
10
|
+
FRAGMENTS.map((name) => Bun.file(join('docs/agents', `${name}.md`)).text()),
|
|
11
|
+
)
|
|
12
|
+
const built = HEADER + parts.map((p) => p.trim()).join('\n\n') + '\n'
|
|
13
|
+
|
|
14
|
+
if (process.argv.includes('--check')) {
|
|
15
|
+
const current = (await Bun.file(OUT).exists())
|
|
16
|
+
? await Bun.file(OUT).text()
|
|
17
|
+
: ''
|
|
18
|
+
if (current !== built) {
|
|
19
|
+
console.error(`${OUT} is stale: run \`bun run agents:build\``)
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
console.log(`${OUT} up to date (${built.split('\n').length} lines)`)
|
|
23
|
+
} else {
|
|
24
|
+
await Bun.write(OUT, built)
|
|
25
|
+
console.log(`wrote ${OUT} (${built.split('\n').length} lines)`)
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MANIFEST,
|
|
5
|
+
type Manifest,
|
|
6
|
+
RULES_DIR,
|
|
7
|
+
rulesSource,
|
|
8
|
+
sha256,
|
|
9
|
+
} from './rules.config'
|
|
10
|
+
|
|
11
|
+
const manifestFile = Bun.file(MANIFEST)
|
|
12
|
+
if (!(await manifestFile.exists())) {
|
|
13
|
+
console.error(`${MANIFEST} missing: run \`bun run rules:sync\``)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
const manifest: Manifest = await manifestFile.json()
|
|
17
|
+
const source = rulesSource()
|
|
18
|
+
const upstreamAvailable = await Bun.file(join(source, 'README.md')).exists()
|
|
19
|
+
let failed = false
|
|
20
|
+
|
|
21
|
+
for (const entry of manifest.files) {
|
|
22
|
+
const local = join(RULES_DIR, entry.file)
|
|
23
|
+
if (!(await Bun.file(local).exists())) {
|
|
24
|
+
console.error(`missing ${local}`)
|
|
25
|
+
failed = true
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if ((await sha256(local)) !== entry.sha256) {
|
|
29
|
+
console.error(`${local} differs from MANIFEST (edited by hand?)`)
|
|
30
|
+
failed = true
|
|
31
|
+
}
|
|
32
|
+
if (upstreamAvailable) {
|
|
33
|
+
const upstream = join(source, entry.source)
|
|
34
|
+
if ((await sha256(upstream)) !== entry.sha256) {
|
|
35
|
+
console.error(
|
|
36
|
+
`${entry.source} changed upstream: run \`bun run rules:sync\``,
|
|
37
|
+
)
|
|
38
|
+
failed = true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!upstreamAvailable)
|
|
44
|
+
console.warn(`ai-rules not found at ${source}: checked local integrity only`)
|
|
45
|
+
if (failed) process.exit(1)
|
|
46
|
+
console.log(
|
|
47
|
+
`ai-rules ok (${manifest.files.length} files, synced ${manifest.syncedAt})`,
|
|
48
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MANIFEST,
|
|
6
|
+
type Manifest,
|
|
7
|
+
RULES,
|
|
8
|
+
RULES_DIR,
|
|
9
|
+
rulesSource,
|
|
10
|
+
sha256,
|
|
11
|
+
} from './rules.config'
|
|
12
|
+
|
|
13
|
+
const source = rulesSource()
|
|
14
|
+
if (!(await Bun.file(join(source, 'README.md')).exists())) {
|
|
15
|
+
console.error(`ai-rules not found at ${source} (set AI_RULES_PATH)`)
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await mkdir(RULES_DIR, { recursive: true })
|
|
20
|
+
const manifest: Manifest = { syncedAt: new Date().toISOString(), files: [] }
|
|
21
|
+
|
|
22
|
+
for (const rule of RULES) {
|
|
23
|
+
const from = join(source, rule)
|
|
24
|
+
const to = join(RULES_DIR, basename(rule))
|
|
25
|
+
await Bun.write(to, Bun.file(from))
|
|
26
|
+
manifest.files.push({
|
|
27
|
+
file: basename(rule),
|
|
28
|
+
source: rule,
|
|
29
|
+
sha256: await sha256(to),
|
|
30
|
+
})
|
|
31
|
+
console.log(`synced ${rule}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await Bun.write(MANIFEST, JSON.stringify(manifest, null, 2) + '\n')
|
|
35
|
+
console.log(`wrote ${MANIFEST}`)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/** ai-rules files (relative to the ai-rules repo root) mirrored into docs/ai-rules. */
|
|
5
|
+
export const RULES = ['frontend/testing-selectors.md'] as const
|
|
6
|
+
|
|
7
|
+
export const RULES_DIR = 'docs/ai-rules'
|
|
8
|
+
export const MANIFEST = join(RULES_DIR, 'MANIFEST.json')
|
|
9
|
+
|
|
10
|
+
export const rulesSource = (): string =>
|
|
11
|
+
process.env.AI_RULES_PATH ?? join(homedir(), 'Progetti', 'ai-rules')
|
|
12
|
+
|
|
13
|
+
export interface ManifestEntry {
|
|
14
|
+
file: string
|
|
15
|
+
source: string
|
|
16
|
+
sha256: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Manifest {
|
|
20
|
+
syncedAt: string
|
|
21
|
+
files: ManifestEntry[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const sha256 = async (path: string): Promise<string> => {
|
|
25
|
+
const hasher = new Bun.CryptoHasher('sha256')
|
|
26
|
+
hasher.update(await Bun.file(path).arrayBuffer())
|
|
27
|
+
return hasher.digest('hex')
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Order,
|
|
5
|
+
PRODUCT_CATEGORIES,
|
|
6
|
+
type Product,
|
|
7
|
+
USER_ROLES,
|
|
8
|
+
type User,
|
|
9
|
+
} from '../src/data/schemas'
|
|
10
|
+
|
|
11
|
+
// Fixed seed + fixed ref date → identical fixtures on every run (fixtures are committed).
|
|
12
|
+
faker.seed(42)
|
|
13
|
+
faker.setDefaultRefDate('2026-01-01T00:00:00.000Z')
|
|
14
|
+
|
|
15
|
+
const money = (n: number): number => Math.round(n * 100) / 100
|
|
16
|
+
const pastDate = (): string =>
|
|
17
|
+
faker.date
|
|
18
|
+
.between({
|
|
19
|
+
from: '2024-01-01T00:00:00.000Z',
|
|
20
|
+
to: '2025-12-31T23:59:59.000Z',
|
|
21
|
+
})
|
|
22
|
+
.toISOString()
|
|
23
|
+
|
|
24
|
+
const users: User[] = Array.from({ length: 50 }, () => {
|
|
25
|
+
const firstName = faker.person.firstName()
|
|
26
|
+
const lastName = faker.person.lastName()
|
|
27
|
+
return {
|
|
28
|
+
id: faker.string.uuid(),
|
|
29
|
+
name: `${firstName} ${lastName}`,
|
|
30
|
+
email: faker.internet.email({ firstName, lastName }).toLowerCase(),
|
|
31
|
+
role: faker.helpers.arrayElement(USER_ROLES),
|
|
32
|
+
createdAt: pastDate(),
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const products: Product[] = Array.from({ length: 20 }, () => ({
|
|
37
|
+
id: faker.string.uuid(),
|
|
38
|
+
name: faker.commerce.productName(),
|
|
39
|
+
category: faker.helpers.arrayElement(PRODUCT_CATEGORIES),
|
|
40
|
+
price: money(Number(faker.commerce.price({ min: 5, max: 500 }))),
|
|
41
|
+
stock: faker.number.int({ min: 0, max: 200 }),
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
const orders: Order[] = Array.from({ length: 10 }, () => {
|
|
45
|
+
const items = faker.helpers
|
|
46
|
+
.arrayElements(products, { min: 1, max: 4 })
|
|
47
|
+
.map((p) => ({
|
|
48
|
+
productId: p.id,
|
|
49
|
+
qty: faker.number.int({ min: 1, max: 5 }),
|
|
50
|
+
unitPrice: p.price,
|
|
51
|
+
}))
|
|
52
|
+
return {
|
|
53
|
+
id: faker.string.uuid(),
|
|
54
|
+
userId: faker.helpers.arrayElement(users).id,
|
|
55
|
+
items,
|
|
56
|
+
total: money(items.reduce((sum, i) => sum + i.qty * i.unitPrice, 0)),
|
|
57
|
+
createdAt: pastDate(),
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
for (const [name, rows] of Object.entries({ users, products, orders })) {
|
|
62
|
+
await Bun.write(`src/data/${name}.json`, JSON.stringify(rows, null, 2) + '\n')
|
|
63
|
+
console.log(`wrote src/data/${name}.json (${rows.length})`)
|
|
64
|
+
}
|