From 588f47affaaa66449c57cc561eae1b7c660d3c2b Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Tue, 5 May 2026 09:34:44 +0200 Subject: [PATCH] docs(plans): delete six stale 2026-04-06 plan docs (superseded by 2026-05-04-plan-{1..6}) --- .../2026-04-06-plan-1-monorepo-foundation.md | 965 ---------- .../plans/2026-04-06-plan-2-core-package.md | 1639 ----------------- .../plans/2026-04-06-plan-3-payload-cms.md | 977 ---------- .../2026-04-06-plan-4-api-layer-app-shells.md | 628 ------- .../plans/2026-04-06-plan-5-ui-system.md | 610 ------ .../plans/2026-04-06-plan-6-documentation.md | 9 - ...n-architecture-monorepo-template-design.md | 557 ------ 7 files changed, 5385 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-06-plan-1-monorepo-foundation.md delete mode 100644 docs/superpowers/plans/2026-04-06-plan-2-core-package.md delete mode 100644 docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md delete mode 100644 docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md delete mode 100644 docs/superpowers/plans/2026-04-06-plan-5-ui-system.md delete mode 100644 docs/superpowers/plans/2026-04-06-plan-6-documentation.md delete mode 100644 docs/superpowers/specs/2026-04-06-clean-architecture-monorepo-template-design.md diff --git a/docs/superpowers/plans/2026-04-06-plan-1-monorepo-foundation.md b/docs/superpowers/plans/2026-04-06-plan-1-monorepo-foundation.md deleted file mode 100644 index 0b2ce84..0000000 --- a/docs/superpowers/plans/2026-04-06-plan-1-monorepo-foundation.md +++ /dev/null @@ -1,965 +0,0 @@ -# Plan 1: Monorepo Foundation — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Scaffold a Turborepo + pnpm monorepo with shared TypeScript and ESLint configs, placeholder packages for all planned workspaces, and Docker Compose for local development. - -**Architecture:** Turborepo orchestrates builds across pnpm workspaces. Shared config packages (`@repo/typescript-config`, `@repo/eslint-config`) provide consistent tooling. All apps and packages are created as empty placeholders with correct `package.json` files so the workspace graph is valid from the start. Docker Compose provides PostgreSQL for local development. - -**Tech Stack:** Turborepo 2.x, pnpm 9.x, TypeScript 5.x, ESLint 9.x (flat config), Vitest, Docker Compose, PostgreSQL 16 - ---- - -## File Map - -| File | Responsibility | -|---|---| -| `package.json` | Root workspace manifest, delegates to turbo | -| `pnpm-workspace.yaml` | Declares workspace packages | -| `turbo.json` | Task pipeline (build, dev, lint, test, typecheck) | -| `.npmrc` | pnpm workspace settings | -| `.gitignore` | Ignore patterns for Turborepo + pnpm + Node.js | -| `packages/typescript-config/package.json` | Shared TS config package manifest | -| `packages/typescript-config/base.json` | Base TypeScript config | -| `packages/typescript-config/nextjs.json` | Next.js TypeScript config | -| `packages/typescript-config/react-library.json` | React library TypeScript config | -| `packages/eslint-config/package.json` | Shared ESLint config package manifest | -| `packages/eslint-config/base.js` | Base ESLint flat config | -| `packages/eslint-config/next.js` | Next.js ESLint config | -| `packages/eslint-config/react-internal.js` | React library ESLint config | -| `packages/core/package.json` | Placeholder — clean architecture core | -| `packages/core/tsconfig.json` | Extends @repo/typescript-config/base | -| `packages/api/package.json` | Placeholder — tRPC routers | -| `packages/api/tsconfig.json` | Extends @repo/typescript-config/base | -| `packages/api-client/package.json` | Placeholder — React Query hooks | -| `packages/api-client/tsconfig.json` | Extends @repo/typescript-config/react-library | -| `packages/cms-core/package.json` | Placeholder — Payload CMS definition | -| `packages/cms-core/tsconfig.json` | Extends @repo/typescript-config/base | -| `packages/cms-client/package.json` | Placeholder — Dual-mode Payload client | -| `packages/cms-client/tsconfig.json` | Extends @repo/typescript-config/base | -| `packages/ui/package.json` | Placeholder — shadcn/ui + Atomic Design | -| `packages/ui/tsconfig.json` | Extends @repo/typescript-config/react-library | -| `apps/web-next/package.json` | Placeholder — Next.js reference app | -| `apps/web-next/tsconfig.json` | Extends @repo/typescript-config/nextjs | -| `apps/web-tanstack/package.json` | Placeholder — TanStack Start reference app | -| `apps/web-tanstack/tsconfig.json` | Extends @repo/typescript-config/base | -| `apps/cms/package.json` | Placeholder — Payload admin shell | -| `apps/cms/tsconfig.json` | Extends @repo/typescript-config/nextjs | -| `apps/storybook/package.json` | Placeholder — Storybook instance | -| `apps/storybook/tsconfig.json` | Extends @repo/typescript-config/react-library | -| `docker-compose.yml` | PostgreSQL service for local dev | -| `.env.example` | Environment variable template | - ---- - -### Task 1: Root workspace files - -**Files:** -- Create: `package.json` -- Create: `pnpm-workspace.yaml` -- Create: `turbo.json` -- Create: `.npmrc` -- Create: `.gitignore` -- Create: `.env.example` - -- [ ] **Step 1: Create root package.json** - -```json -{ - "name": "template", - "private": true, - "packageManager": "pnpm@9.15.4", - "engines": { - "node": ">=20" - }, - "scripts": { - "build": "turbo run build", - "dev": "turbo run dev", - "lint": "turbo run lint", - "test": "turbo run test", - "typecheck": "turbo run typecheck", - "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", - "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"" - }, - "devDependencies": { - "prettier": "^3.5.0", - "turbo": "^2.4.0", - "typescript": "^5.8.0" - } -} -``` - -- [ ] **Step 2: Create pnpm-workspace.yaml** - -```yaml -packages: - - "apps/*" - - "packages/*" -``` - -- [ ] **Step 3: Create turbo.json** - -```json -{ - "$schema": "https://turborepo.dev/schema.json", - "tasks": { - "build": { - "dependsOn": ["^build"], - "outputs": ["dist/**", ".next/**", "!.next/cache/**"] - }, - "dev": { - "cache": false, - "persistent": true - }, - "lint": { - "dependsOn": ["^lint"] - }, - "test": { - "dependsOn": ["^build"] - }, - "typecheck": { - "dependsOn": ["^typecheck"] - } - } -} -``` - -- [ ] **Step 4: Create .npmrc** - -``` -auto-install-peers=true -enable-pre-post-scripts=true -``` - -- [ ] **Step 5: Create .gitignore** - -``` -# Dependencies -node_modules - -# Turbo -.turbo - -# Build outputs -dist -build -.next -out -storybook-static - -# Environment -.env -.env.local -.env.*.local - -# Testing -coverage - -# OS -.DS_Store -Thumbs.db - -# IDE -.vscode -.idea -*.swp - -# Debug -npm-debug.log* -pnpm-debug.log* - -# Superpowers brainstorm sessions -.superpowers/ -``` - -- [ ] **Step 6: Create .env.example** - -``` -# Database -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/template - -# Payload CMS -PAYLOAD_SECRET=your-secret-here - -# App URLs -NEXT_PUBLIC_APP_URL=http://localhost:3000 -CMS_URL=http://localhost:3001 -``` - -- [ ] **Step 7: Commit** - -```bash -git add package.json pnpm-workspace.yaml turbo.json .npmrc .gitignore .env.example -git commit -m "feat: scaffold root workspace files (Turborepo + pnpm)" -``` - ---- - -### Task 2: Shared TypeScript config package - -**Files:** -- Create: `packages/typescript-config/package.json` -- Create: `packages/typescript-config/base.json` -- Create: `packages/typescript-config/nextjs.json` -- Create: `packages/typescript-config/react-library.json` - -- [ ] **Step 1: Create package.json** - -```json -{ - "name": "@repo/typescript-config", - "private": true, - "version": "0.0.0" -} -``` - -- [ ] **Step 2: Create base.json** - -This is the base TypeScript config used by all packages. Includes `experimentalDecorators` and `emitDecoratorMetadata` required by InversifyJS. - -```json -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022"], - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "declaration": true, - "declarationMap": true, - "isolatedModules": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "noUncheckedIndexedAccess": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true - }, - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 3: Create nextjs.json** - -```json -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "jsx": "preserve", - "module": "ESNext", - "moduleResolution": "bundler", - "noEmit": true, - "incremental": true, - "plugins": [{ "name": "next" }] - } -} -``` - -- [ ] **Step 4: Create react-library.json** - -```json -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./base.json", - "compilerOptions": { - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "jsx": "react-jsx" - } -} -``` - -- [ ] **Step 5: Commit** - -```bash -git add packages/typescript-config/ -git commit -m "feat: add shared TypeScript config package (@repo/typescript-config)" -``` - ---- - -### Task 3: Shared ESLint config package - -**Files:** -- Create: `packages/eslint-config/package.json` -- Create: `packages/eslint-config/base.js` -- Create: `packages/eslint-config/next.js` -- Create: `packages/eslint-config/react-internal.js` - -- [ ] **Step 1: Create package.json** - -```json -{ - "name": "@repo/eslint-config", - "private": true, - "version": "0.0.0", - "type": "module", - "exports": { - "./base": "./base.js", - "./next": "./next.js", - "./react-internal": "./react-internal.js" - }, - "devDependencies": { - "@eslint/js": "^9.20.0", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "@typescript-eslint/parser": "^8.25.0", - "eslint": "^9.20.0", - "eslint-config-prettier": "^10.1.0", - "eslint-plugin-turbo": "^2.4.0", - "typescript-eslint": "^8.25.0" - } -} -``` - -- [ ] **Step 2: Create base.js** - -```javascript -import js from "@eslint/js"; -import eslintConfigPrettier from "eslint-config-prettier"; -import tseslint from "typescript-eslint"; -import turboPlugin from "eslint-plugin-turbo"; - -export default [ - { ignores: ["dist/**", "node_modules/**"] }, - js.configs.recommended, - ...tseslint.configs.recommended, - eslintConfigPrettier, - { - plugins: { turbo: turboPlugin }, - rules: { - "turbo/no-undeclared-env-vars": "warn", - }, - }, -]; -``` - -- [ ] **Step 3: Create next.js** - -```javascript -import baseConfig from "./base.js"; - -export default [ - ...baseConfig, - { ignores: [".next/**", "out/**"] }, -]; -``` - -- [ ] **Step 4: Create react-internal.js** - -```javascript -import baseConfig from "./base.js"; - -export default [...baseConfig]; -``` - -- [ ] **Step 5: Commit** - -```bash -git add packages/eslint-config/ -git commit -m "feat: add shared ESLint config package (@repo/eslint-config)" -``` - ---- - -### Task 4: Placeholder packages (core, api, api-client, cms-core, cms-client, ui) - -**Files:** -- Create: `packages/core/package.json` -- Create: `packages/core/tsconfig.json` -- Create: `packages/core/src/index.ts` -- Create: `packages/api/package.json` -- Create: `packages/api/tsconfig.json` -- Create: `packages/api/src/index.ts` -- Create: `packages/api-client/package.json` -- Create: `packages/api-client/tsconfig.json` -- Create: `packages/api-client/src/index.ts` -- Create: `packages/cms-core/package.json` -- Create: `packages/cms-core/tsconfig.json` -- Create: `packages/cms-core/src/index.ts` -- Create: `packages/cms-client/package.json` -- Create: `packages/cms-client/tsconfig.json` -- Create: `packages/cms-client/src/index.ts` -- Create: `packages/ui/package.json` -- Create: `packages/ui/tsconfig.json` -- Create: `packages/ui/src/index.ts` - -- [ ] **Step 1: Create packages/core/package.json** - -```json -{ - "name": "@repo/core", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "test": "vitest run", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 2: Create packages/core/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - }, - "types": ["reflect-metadata"] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 3: Create packages/core/src/index.ts** - -```typescript -// @repo/core — Clean Architecture core package -// Layers: entities, application, infrastructure, interface-adapters, di -export {}; -``` - -- [ ] **Step 4: Create packages/api/package.json** - -```json -{ - "name": "@repo/api", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 5: Create packages/api/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 6: Create packages/api/src/index.ts** - -```typescript -// @repo/api — tRPC router definitions -export {}; -``` - -- [ ] **Step 7: Create packages/api-client/package.json** - -```json -{ - "name": "@repo/api-client", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 8: Create packages/api-client/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/react-library.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 9: Create packages/api-client/src/index.ts** - -```typescript -// @repo/api-client — Shared React Query hooks -export {}; -``` - -- [ ] **Step 10: Create packages/cms-core/package.json** - -```json -{ - "name": "@repo/cms-core", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 11: Create packages/cms-core/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 12: Create packages/cms-core/src/index.ts** - -```typescript -// @repo/cms-core — Payload CMS config, collections, hooks, globals -export {}; -``` - -- [ ] **Step 13: Create packages/cms-client/package.json** - -```json -{ - "name": "@repo/cms-client", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 14: Create packages/cms-client/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 15: Create packages/cms-client/src/index.ts** - -```typescript -// @repo/cms-client — Dual-mode Payload client (local + HTTP) -export {}; -``` - -- [ ] **Step 16: Create packages/ui/package.json** - -```json -{ - "name": "@repo/ui", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "test": "vitest run", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 17: Create packages/ui/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/react-library.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 18: Create packages/ui/src/index.ts** - -```typescript -// @repo/ui — shadcn/ui + Atomic Design component library -export {}; -``` - -- [ ] **Step 19: Commit** - -```bash -git add packages/core/ packages/api/ packages/api-client/ packages/cms-core/ packages/cms-client/ packages/ui/ -git commit -m "feat: add placeholder packages (core, api, api-client, cms-core, cms-client, ui)" -``` - ---- - -### Task 5: Placeholder apps (web-next, web-tanstack, cms, storybook) - -**Files:** -- Create: `apps/web-next/package.json` -- Create: `apps/web-next/tsconfig.json` -- Create: `apps/web-tanstack/package.json` -- Create: `apps/web-tanstack/tsconfig.json` -- Create: `apps/cms/package.json` -- Create: `apps/cms/tsconfig.json` -- Create: `apps/storybook/package.json` -- Create: `apps/storybook/tsconfig.json` - -- [ ] **Step 1: Create apps/web-next/package.json** - -```json -{ - "name": "@repo/web-next", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "build": "echo 'placeholder'", - "dev": "echo 'placeholder'", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@repo/api-client": "workspace:*", - "@repo/ui": "workspace:*" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 2: Create apps/web-next/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/nextjs.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"], - "exclude": ["node_modules"] -} -``` - -- [ ] **Step 3: Create apps/web-tanstack/package.json** - -```json -{ - "name": "@repo/web-tanstack", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "build": "echo 'placeholder'", - "dev": "echo 'placeholder'", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@repo/api-client": "workspace:*", - "@repo/ui": "workspace:*" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 4: Create apps/web-tanstack/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules"] -} -``` - -- [ ] **Step 5: Create apps/cms/package.json** - -```json -{ - "name": "@repo/cms", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "build": "echo 'placeholder'", - "dev": "echo 'placeholder'", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@repo/cms-core": "workspace:*" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 6: Create apps/cms/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/nextjs.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"], - "exclude": ["node_modules"] -} -``` - -- [ ] **Step 7: Create apps/storybook/package.json** - -```json -{ - "name": "@repo/storybook", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "build": "echo 'placeholder'", - "dev": "echo 'placeholder'", - "lint": "eslint ." - }, - "dependencies": { - "@repo/ui": "workspace:*" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*" - } -} -``` - -- [ ] **Step 8: Create apps/storybook/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/react-library.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules"] -} -``` - -- [ ] **Step 9: Commit** - -```bash -git add apps/ -git commit -m "feat: add placeholder apps (web-next, web-tanstack, cms, storybook)" -``` - ---- - -### Task 6: Docker Compose - -**Files:** -- Create: `docker-compose.yml` - -- [ ] **Step 1: Create docker-compose.yml** - -```yaml -services: - postgres: - image: postgres:16-alpine - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: template - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 - -volumes: - postgres_data: -``` - -- [ ] **Step 2: Commit** - -```bash -git add docker-compose.yml -git commit -m "feat: add Docker Compose with PostgreSQL for local dev" -``` - ---- - -### Task 7: Install dependencies and verify workspace - -- [ ] **Step 1: Install pnpm if not available** - -Run: `corepack enable && corepack prepare pnpm@9.15.4 --activate` -Expected: pnpm is available - -- [ ] **Step 2: Run pnpm install** - -Run: `pnpm install` -Expected: Installs all workspace dependencies, creates `pnpm-lock.yaml`, no errors. - -- [ ] **Step 3: Verify Turborepo sees all workspaces** - -Run: `pnpm turbo run build --dry` -Expected: Output lists all 10 packages/apps: -- `@repo/typescript-config` -- `@repo/eslint-config` -- `@repo/core` -- `@repo/api` -- `@repo/api-client` -- `@repo/cms-core` -- `@repo/cms-client` -- `@repo/ui` -- `@repo/web-next` -- `@repo/web-tanstack` -- `@repo/cms` -- `@repo/storybook` - -- [ ] **Step 4: Run turbo build** - -Run: `pnpm build` -Expected: All workspaces build successfully (placeholder builds echo 'placeholder' or tsc --noEmit with no errors on empty src/index.ts). - -- [ ] **Step 5: Verify Docker Compose** - -Run: `docker compose up -d postgres && docker compose ps` -Expected: PostgreSQL container running, healthy. - -Run: `docker compose down` -Expected: Clean shutdown. - -- [ ] **Step 6: Commit lockfile** - -```bash -git add pnpm-lock.yaml -git commit -m "chore: add pnpm lockfile" -``` - ---- - -### Task 8: Create test directory structure - -**Files:** -- Create: `tests/unit/.gitkeep` -- Create: `tests/integration/.gitkeep` -- Create: `tests/e2e/.gitkeep` - -- [ ] **Step 1: Create test directories** - -```bash -mkdir -p tests/unit tests/integration tests/e2e -touch tests/unit/.gitkeep tests/integration/.gitkeep tests/e2e/.gitkeep -``` - -- [ ] **Step 2: Commit** - -```bash -git add tests/ -git commit -m "feat: add test directory structure (unit, integration, e2e)" -``` diff --git a/docs/superpowers/plans/2026-04-06-plan-2-core-package.md b/docs/superpowers/plans/2026-04-06-plan-2-core-package.md deleted file mode 100644 index 52dcd88..0000000 --- a/docs/superpowers/plans/2026-04-06-plan-2-core-package.md +++ /dev/null @@ -1,1639 +0,0 @@ -# Plan 2: Core Package + DI — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Implement the `@repo/core` clean architecture package with entities, application interfaces, use cases, mock infrastructure, InversifyJS DI, controllers, and unit tests — all following TDD. - -**Architecture:** Single `@repo/core` package with 5 layers (entities → application → infrastructure → interface-adapters → di). Dependencies point inward only. InversifyJS resolves interfaces to implementations at runtime. Mock implementations enable testing without external services. Use cases call `getInjection()` to resolve dependencies — they never import infrastructure directly. - -**Tech Stack:** TypeScript 5.x, InversifyJS 6.x, reflect-metadata, Zod, Vitest - ---- - -## File Map - -### Entities Layer (zero deps) -| File | Responsibility | -|---|---| -| `packages/core/src/entities/models/user.ts` | User Zod schema + type | -| `packages/core/src/entities/models/article.ts` | Article Zod schema + type | -| `packages/core/src/entities/models/session.ts` | Session Zod schema + type | -| `packages/core/src/entities/models/cookie.ts` | Cookie type | -| `packages/core/src/entities/models/index.ts` | Re-exports all models | -| `packages/core/src/entities/errors/auth.ts` | AuthenticationError, UnauthenticatedError, UnauthorizedError | -| `packages/core/src/entities/errors/common.ts` | NotFoundError, InputParseError | -| `packages/core/src/entities/errors/index.ts` | Re-exports all errors | -| `packages/core/src/entities/index.ts` | Re-exports models + errors | - -### Application Layer (imports entities only) -| File | Responsibility | -|---|---| -| `packages/core/src/application/repositories/users.repository.interface.ts` | IUsersRepository | -| `packages/core/src/application/repositories/articles.repository.interface.ts` | IArticlesRepository | -| `packages/core/src/application/repositories/index.ts` | Re-exports | -| `packages/core/src/application/services/auth.service.interface.ts` | IAuthenticationService | -| `packages/core/src/application/services/telemetry.service.interface.ts` | ITelemetryService | -| `packages/core/src/application/services/index.ts` | Re-exports | -| `packages/core/src/application/use-cases/auth/sign-in.use-case.ts` | Sign in logic | -| `packages/core/src/application/use-cases/auth/sign-up.use-case.ts` | Sign up logic | -| `packages/core/src/application/use-cases/auth/sign-out.use-case.ts` | Sign out logic | -| `packages/core/src/application/use-cases/content/create-article.use-case.ts` | Create article | -| `packages/core/src/application/use-cases/content/get-articles.use-case.ts` | Get articles | - -### Infrastructure Layer (mock implementations) -| File | Responsibility | -|---|---| -| `packages/core/src/infrastructure/repositories/mock-users.repository.ts` | In-memory users | -| `packages/core/src/infrastructure/repositories/mock-articles.repository.ts` | In-memory articles | -| `packages/core/src/infrastructure/services/mock-auth.service.ts` | Mock sessions | -| `packages/core/src/infrastructure/services/mock-telemetry.service.ts` | No-op telemetry | - -### Interface Adapters -| File | Responsibility | -|---|---| -| `packages/core/src/interface-adapters/controllers/auth/sign-in.controller.ts` | Validate + delegate | -| `packages/core/src/interface-adapters/controllers/auth/sign-up.controller.ts` | Validate + delegate | -| `packages/core/src/interface-adapters/controllers/auth/sign-out.controller.ts` | Validate + delegate | -| `packages/core/src/interface-adapters/controllers/content/articles.controller.ts` | Validate + delegate | - -### DI -| File | Responsibility | -|---|---| -| `packages/core/src/di/types.ts` | DI_SYMBOLS + DI_RETURN_TYPES | -| `packages/core/src/di/modules/auth.module.ts` | Binds auth deps | -| `packages/core/src/di/modules/content.module.ts` | Binds content deps | -| `packages/core/src/di/container.ts` | InversifyJS container | - -### Config + Tests -| File | Responsibility | -|---|---| -| `packages/core/src/config.ts` | Constants (SESSION_COOKIE, etc.) | -| `packages/core/vitest.config.ts` | Vitest config with path aliases | -| `packages/core/src/index.ts` | Public API re-exports | - -### Test files (co-located pattern from reference repo) -| File | Responsibility | -|---|---| -| `packages/core/tests/unit/use-cases/auth/sign-in.use-case.test.ts` | Sign in tests | -| `packages/core/tests/unit/use-cases/auth/sign-up.use-case.test.ts` | Sign up tests | -| `packages/core/tests/unit/use-cases/auth/sign-out.use-case.test.ts` | Sign out tests | -| `packages/core/tests/unit/use-cases/content/create-article.use-case.test.ts` | Create article tests | -| `packages/core/tests/unit/use-cases/content/get-articles.use-case.test.ts` | Get articles tests | -| `packages/core/tests/unit/controllers/auth/sign-in.controller.test.ts` | Sign in controller tests | -| `packages/core/tests/unit/controllers/auth/sign-up.controller.test.ts` | Sign up controller tests | -| `packages/core/tests/unit/controllers/auth/sign-out.controller.test.ts` | Sign out controller tests | -| `packages/core/tests/unit/controllers/content/articles.controller.test.ts` | Articles controller tests | - ---- - -### Task 1: Install dependencies - -**Files:** -- Modify: `packages/core/package.json` -- Modify: `packages/core/tsconfig.json` - -- [ ] **Step 1: Add dependencies to packages/core/package.json** - -```json -{ - "name": "@repo/core", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "test": "vitest run", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "inversify": "^6.2.0", - "reflect-metadata": "^0.2.2", - "zod": "^3.24.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "vitest": "^3.1.0" - } -} -``` - -- [ ] **Step 2: Update tsconfig.json to include reflect-metadata** - -```json -{ - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - }, - "types": ["reflect-metadata"] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 3: Run pnpm install from repo root** - -Run: `pnpm install` -Expected: Installs inversify, reflect-metadata, zod, vitest. No errors. - -- [ ] **Step 4: Commit** - -```bash -git add packages/core/package.json packages/core/tsconfig.json pnpm-lock.yaml -git commit -m "feat(core): add dependencies (inversify, reflect-metadata, zod, vitest)" -``` - ---- - -### Task 2: Vitest config + constants - -**Files:** -- Create: `packages/core/vitest.config.ts` -- Create: `packages/core/src/config.ts` - -- [ ] **Step 1: Create vitest.config.ts** - -```typescript -import { defineConfig } from "vitest/config"; -import { fileURLToPath, URL } from "node:url"; - -export default defineConfig({ - test: { - globals: true, - coverage: { - provider: "v8", - reportsDirectory: "./tests/coverage", - }, - }, - resolve: { - alias: { - "@": fileURLToPath(new URL("./src", import.meta.url)), - }, - }, -}); -``` - -- [ ] **Step 2: Create src/config.ts** - -```typescript -export const SESSION_COOKIE = "session"; -``` - -- [ ] **Step 3: Commit** - -```bash -git add packages/core/vitest.config.ts packages/core/src/config.ts -git commit -m "feat(core): add vitest config and constants" -``` - ---- - -### Task 3: Entities — models - -**Files:** -- Create: `packages/core/src/entities/models/user.ts` -- Create: `packages/core/src/entities/models/article.ts` -- Create: `packages/core/src/entities/models/session.ts` -- Create: `packages/core/src/entities/models/cookie.ts` -- Create: `packages/core/src/entities/models/index.ts` - -- [ ] **Step 1: Create user.ts** - -```typescript -import { z } from "zod"; - -export const userSchema = z.object({ - id: z.string(), - username: z.string().min(3).max(31), - passwordHash: z.string().min(6).max(255), -}); - -export type User = z.infer; -``` - -- [ ] **Step 2: Create article.ts** - -```typescript -import { z } from "zod"; - -export const articleStatusSchema = z.enum(["draft", "published"]); - -export const articleSchema = z.object({ - id: z.string(), - title: z.string().min(1).max(255), - slug: z.string().min(1).max(255), - content: z.string(), - status: articleStatusSchema.default("draft"), - authorId: z.string(), - createdAt: z.date(), - updatedAt: z.date(), -}); - -export type Article = z.infer; -export type ArticleStatus = z.infer; -``` - -- [ ] **Step 3: Create session.ts** - -```typescript -import { z } from "zod"; - -export const sessionSchema = z.object({ - id: z.string(), - userId: z.string(), - expiresAt: z.date(), -}); - -export type Session = z.infer; -``` - -- [ ] **Step 4: Create cookie.ts** - -```typescript -type CookieAttributes = { - secure?: boolean; - path?: string; - domain?: string; - sameSite?: "lax" | "strict" | "none"; - httpOnly?: boolean; - maxAge?: number; - expires?: Date; -}; - -export type Cookie = { - name: string; - value: string; - attributes: CookieAttributes; -}; -``` - -- [ ] **Step 5: Create index.ts** - -```typescript -export { userSchema, type User } from "./user.js"; -export { - articleSchema, - articleStatusSchema, - type Article, - type ArticleStatus, -} from "./article.js"; -export { sessionSchema, type Session } from "./session.js"; -export type { Cookie } from "./cookie.js"; -``` - -- [ ] **Step 6: Commit** - -```bash -git add packages/core/src/entities/models/ -git commit -m "feat(core): add entity models (user, article, session, cookie)" -``` - ---- - -### Task 4: Entities — errors - -**Files:** -- Create: `packages/core/src/entities/errors/auth.ts` -- Create: `packages/core/src/entities/errors/common.ts` -- Create: `packages/core/src/entities/errors/index.ts` -- Create: `packages/core/src/entities/index.ts` - -- [ ] **Step 1: Create auth.ts** - -```typescript -export class AuthenticationError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - } -} - -export class UnauthenticatedError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - } -} - -export class UnauthorizedError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - } -} -``` - -- [ ] **Step 2: Create common.ts** - -```typescript -export class NotFoundError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - } -} - -export class InputParseError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); - } -} -``` - -- [ ] **Step 3: Create errors/index.ts** - -```typescript -export { - AuthenticationError, - UnauthenticatedError, - UnauthorizedError, -} from "./auth.js"; -export { NotFoundError, InputParseError } from "./common.js"; -``` - -- [ ] **Step 4: Create entities/index.ts** - -```typescript -export * from "./models/index.js"; -export * from "./errors/index.js"; -``` - -- [ ] **Step 5: Commit** - -```bash -git add packages/core/src/entities/ -git commit -m "feat(core): add entity errors (auth, common)" -``` - ---- - -### Task 5: Application — repository and service interfaces - -**Files:** -- Create: `packages/core/src/application/repositories/users.repository.interface.ts` -- Create: `packages/core/src/application/repositories/articles.repository.interface.ts` -- Create: `packages/core/src/application/repositories/index.ts` -- Create: `packages/core/src/application/services/auth.service.interface.ts` -- Create: `packages/core/src/application/services/telemetry.service.interface.ts` -- Create: `packages/core/src/application/services/index.ts` - -- [ ] **Step 1: Create users.repository.interface.ts** - -```typescript -import type { User } from "@/entities/models/user.js"; - -export interface IUsersRepository { - getUser(id: string): Promise; - getUserByUsername(username: string): Promise; - createUser(input: User): Promise; -} -``` - -- [ ] **Step 2: Create articles.repository.interface.ts** - -```typescript -import type { Article } from "@/entities/models/article.js"; - -export interface IArticlesRepository { - getArticle(id: string): Promise
; - getArticles(options?: { - status?: string; - authorId?: string; - limit?: number; - offset?: number; - }): Promise; - createArticle(input: Article): Promise
; - updateArticle(id: string, input: Partial
): Promise
; -} -``` - -- [ ] **Step 3: Create repositories/index.ts** - -```typescript -export type { IUsersRepository } from "./users.repository.interface.js"; -export type { IArticlesRepository } from "./articles.repository.interface.js"; -``` - -- [ ] **Step 4: Create auth.service.interface.ts** - -```typescript -import type { Cookie } from "@/entities/models/cookie.js"; -import type { Session } from "@/entities/models/session.js"; -import type { User } from "@/entities/models/user.js"; - -export interface IAuthenticationService { - generateUserId(): string; - hashPassword(password: string): Promise; - verifyPassword(hash: string, password: string): Promise; - validateSession( - sessionId: string - ): Promise<{ user: User; session: Session }>; - createSession(user: User): Promise<{ session: Session; cookie: Cookie }>; - invalidateSession(sessionId: string): Promise<{ blankCookie: Cookie }>; -} -``` - -- [ ] **Step 5: Create telemetry.service.interface.ts** - -```typescript -export interface ITelemetryService { - startSpan(name: string, fn: () => T | Promise): Promise; -} -``` - -- [ ] **Step 6: Create services/index.ts** - -```typescript -export type { IAuthenticationService } from "./auth.service.interface.js"; -export type { ITelemetryService } from "./telemetry.service.interface.js"; -``` - -- [ ] **Step 7: Commit** - -```bash -git add packages/core/src/application/ -git commit -m "feat(core): add application interfaces (repositories + services)" -``` - ---- - -### Task 6: DI — types, modules, container - -**Files:** -- Create: `packages/core/src/di/types.ts` -- Create: `packages/core/src/di/modules/auth.module.ts` -- Create: `packages/core/src/di/modules/content.module.ts` -- Create: `packages/core/src/di/container.ts` - -- [ ] **Step 1: Create di/types.ts** - -```typescript -import type { IAuthenticationService } from "@/application/services/auth.service.interface.js"; -import type { ITelemetryService } from "@/application/services/telemetry.service.interface.js"; -import type { IUsersRepository } from "@/application/repositories/users.repository.interface.js"; -import type { IArticlesRepository } from "@/application/repositories/articles.repository.interface.js"; - -export const DI_SYMBOLS = { - IAuthenticationService: Symbol.for("IAuthenticationService"), - ITelemetryService: Symbol.for("ITelemetryService"), - IUsersRepository: Symbol.for("IUsersRepository"), - IArticlesRepository: Symbol.for("IArticlesRepository"), -}; - -export interface DI_RETURN_TYPES { - IAuthenticationService: IAuthenticationService; - ITelemetryService: ITelemetryService; - IUsersRepository: IUsersRepository; - IArticlesRepository: IArticlesRepository; -} -``` - -- [ ] **Step 2: Create di/modules/auth.module.ts** - -```typescript -import { ContainerModule, interfaces } from "inversify"; - -import type { IUsersRepository } from "@/application/repositories/users.repository.interface.js"; -import type { IAuthenticationService } from "@/application/services/auth.service.interface.js"; -import { MockUsersRepository } from "@/infrastructure/repositories/mock-users.repository.js"; -import { MockAuthenticationService } from "@/infrastructure/services/mock-auth.service.js"; -import { DI_SYMBOLS } from "../types.js"; - -const initializeModule = (bind: interfaces.Bind) => { - bind(DI_SYMBOLS.IUsersRepository).to(MockUsersRepository); - bind(DI_SYMBOLS.IAuthenticationService).to( - MockAuthenticationService - ); -}; - -export const AuthModule = new ContainerModule(initializeModule); -``` - -- [ ] **Step 3: Create di/modules/content.module.ts** - -```typescript -import { ContainerModule, interfaces } from "inversify"; - -import type { IArticlesRepository } from "@/application/repositories/articles.repository.interface.js"; -import { MockArticlesRepository } from "@/infrastructure/repositories/mock-articles.repository.js"; -import { DI_SYMBOLS } from "../types.js"; - -const initializeModule = (bind: interfaces.Bind) => { - bind(DI_SYMBOLS.IArticlesRepository).to( - MockArticlesRepository - ); -}; - -export const ContentModule = new ContainerModule(initializeModule); -``` - -- [ ] **Step 4: Create di/container.ts** - -```typescript -import "reflect-metadata"; -import { Container } from "inversify"; - -import { AuthModule } from "./modules/auth.module.js"; -import { ContentModule } from "./modules/content.module.js"; -import { DI_RETURN_TYPES, DI_SYMBOLS } from "./types.js"; - -const ApplicationContainer = new Container({ - defaultScope: "Singleton", -}); - -export const initializeContainer = () => { - ApplicationContainer.load(AuthModule); - ApplicationContainer.load(ContentModule); -}; - -export const destroyContainer = () => { - ApplicationContainer.unload(AuthModule); - ApplicationContainer.unload(ContentModule); -}; - -if (process.env.NODE_ENV !== "test") { - initializeContainer(); -} - -export function getInjection( - symbol: K -): DI_RETURN_TYPES[K] { - return ApplicationContainer.get(DI_SYMBOLS[symbol]); -} - -export { ApplicationContainer }; -``` - -- [ ] **Step 5: Commit** - -```bash -git add packages/core/src/di/ -git commit -m "feat(core): add InversifyJS DI container with auth and content modules" -``` - ---- - -### Task 7: Infrastructure — mock implementations - -**Files:** -- Create: `packages/core/src/infrastructure/repositories/mock-users.repository.ts` -- Create: `packages/core/src/infrastructure/repositories/mock-articles.repository.ts` -- Create: `packages/core/src/infrastructure/services/mock-auth.service.ts` -- Create: `packages/core/src/infrastructure/services/mock-telemetry.service.ts` - -- [ ] **Step 1: Create mock-users.repository.ts** - -```typescript -import { injectable } from "inversify"; - -import type { IUsersRepository } from "@/application/repositories/users.repository.interface.js"; -import type { User } from "@/entities/models/user.js"; - -@injectable() -export class MockUsersRepository implements IUsersRepository { - private _users: User[] = [ - { id: "1", username: "alice", passwordHash: "hashed_password_alice" }, - { id: "2", username: "bob", passwordHash: "hashed_password_bob" }, - ]; - - async getUser(id: string): Promise { - return this._users.find((u) => u.id === id); - } - - async getUserByUsername(username: string): Promise { - return this._users.find((u) => u.username === username); - } - - async createUser(input: User): Promise { - this._users.push(input); - return input; - } -} -``` - -- [ ] **Step 2: Create mock-articles.repository.ts** - -```typescript -import { injectable } from "inversify"; - -import type { IArticlesRepository } from "@/application/repositories/articles.repository.interface.js"; -import type { Article } from "@/entities/models/article.js"; - -@injectable() -export class MockArticlesRepository implements IArticlesRepository { - private _articles: Article[] = []; - - async getArticle(id: string): Promise
{ - return this._articles.find((a) => a.id === id); - } - - async getArticles(options?: { - status?: string; - authorId?: string; - limit?: number; - offset?: number; - }): Promise { - let result = [...this._articles]; - if (options?.status) { - result = result.filter((a) => a.status === options.status); - } - if (options?.authorId) { - result = result.filter((a) => a.authorId === options.authorId); - } - const offset = options?.offset ?? 0; - const limit = options?.limit ?? 50; - return result.slice(offset, offset + limit); - } - - async createArticle(input: Article): Promise
{ - this._articles.push(input); - return input; - } - - async updateArticle( - id: string, - input: Partial
- ): Promise
{ - const index = this._articles.findIndex((a) => a.id === id); - if (index === -1) return undefined; - this._articles[index] = { ...this._articles[index]!, ...input }; - return this._articles[index]; - } -} -``` - -- [ ] **Step 3: Create mock-auth.service.ts** - -```typescript -import { inject, injectable } from "inversify"; - -import type { IAuthenticationService } from "@/application/services/auth.service.interface.js"; -import type { IUsersRepository } from "@/application/repositories/users.repository.interface.js"; -import { UnauthenticatedError } from "@/entities/errors/auth.js"; -import { sessionSchema, type Session } from "@/entities/models/session.js"; -import type { Cookie } from "@/entities/models/cookie.js"; -import type { User } from "@/entities/models/user.js"; -import { DI_SYMBOLS } from "@/di/types.js"; -import { SESSION_COOKIE } from "@/config.js"; - -@injectable() -export class MockAuthenticationService implements IAuthenticationService { - private _sessions: Record = {}; - - constructor( - @inject(DI_SYMBOLS.IUsersRepository) - private _usersRepository: IUsersRepository - ) {} - - generateUserId(): string { - return (Math.random() + 1).toString(36).substring(7); - } - - async hashPassword(password: string): Promise { - return `hashed_${password}`; - } - - async verifyPassword(hash: string, password: string): Promise { - return hash === `hashed_${password}`; - } - - async validateSession( - sessionId: string - ): Promise<{ user: User; session: Session }> { - const result = this._sessions[sessionId]; - if (!result) { - throw new UnauthenticatedError("Unauthenticated"); - } - const user = await this._usersRepository.getUser(result.user.id); - if (!user) { - throw new UnauthenticatedError("Unauthenticated"); - } - return { user, session: result.session }; - } - - async createSession( - user: User - ): Promise<{ session: Session; cookie: Cookie }> { - const session = sessionSchema.parse({ - id: "session_" + user.id, - userId: user.id, - expiresAt: new Date(Date.now() + 86400000 * 7), - }); - const cookie: Cookie = { - name: SESSION_COOKIE, - value: session.id, - attributes: {}, - }; - this._sessions[session.id] = { session, user }; - return { session, cookie }; - } - - async invalidateSession( - sessionId: string - ): Promise<{ blankCookie: Cookie }> { - delete this._sessions[sessionId]; - return { - blankCookie: { name: SESSION_COOKIE, value: "", attributes: {} }, - }; - } -} -``` - -- [ ] **Step 4: Create mock-telemetry.service.ts** - -```typescript -import { injectable } from "inversify"; - -import type { ITelemetryService } from "@/application/services/telemetry.service.interface.js"; - -@injectable() -export class MockTelemetryService implements ITelemetryService { - async startSpan(_name: string, fn: () => T | Promise): Promise { - return fn(); - } -} -``` - -- [ ] **Step 5: Commit** - -```bash -git add packages/core/src/infrastructure/ -git commit -m "feat(core): add mock implementations (users, articles, auth, telemetry)" -``` - ---- - -### Task 8: Auth use cases + tests (TDD) - -**Files:** -- Create: `packages/core/src/application/use-cases/auth/sign-in.use-case.ts` -- Create: `packages/core/src/application/use-cases/auth/sign-up.use-case.ts` -- Create: `packages/core/src/application/use-cases/auth/sign-out.use-case.ts` -- Create: `packages/core/tests/unit/use-cases/auth/sign-in.use-case.test.ts` -- Create: `packages/core/tests/unit/use-cases/auth/sign-up.use-case.test.ts` -- Create: `packages/core/tests/unit/use-cases/auth/sign-out.use-case.test.ts` - -- [ ] **Step 1: Write sign-in test** - -```typescript -// packages/core/tests/unit/use-cases/auth/sign-in.use-case.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { signInUseCase } from "@/application/use-cases/auth/sign-in.use-case.js"; -import { AuthenticationError } from "@/entities/errors/auth.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("signInUseCase", () => { - it("returns session and cookie for valid credentials", async () => { - const result = await signInUseCase({ - username: "alice", - password: "password_alice", - }); - expect(result).toHaveProperty("session"); - expect(result).toHaveProperty("cookie"); - expect(result.session.userId).toBe("1"); - }); - - it("throws AuthenticationError for non-existing user", async () => { - await expect( - signInUseCase({ username: "non-existing", password: "any" }) - ).rejects.toBeInstanceOf(AuthenticationError); - }); - - it("throws AuthenticationError for wrong password", async () => { - await expect( - signInUseCase({ username: "alice", password: "wrong" }) - ).rejects.toBeInstanceOf(AuthenticationError); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cd packages/core && pnpm vitest run tests/unit/use-cases/auth/sign-in.use-case.test.ts` -Expected: FAIL — module not found - -- [ ] **Step 3: Implement sign-in use case** - -```typescript -// packages/core/src/application/use-cases/auth/sign-in.use-case.ts -import { AuthenticationError } from "@/entities/errors/auth.js"; -import type { Cookie } from "@/entities/models/cookie.js"; -import type { Session } from "@/entities/models/session.js"; -import { getInjection } from "@/di/container.js"; - -export async function signInUseCase(input: { - username: string; - password: string; -}): Promise<{ session: Session; cookie: Cookie }> { - const usersRepository = getInjection("IUsersRepository"); - const authService = getInjection("IAuthenticationService"); - - const existingUser = await usersRepository.getUserByUsername(input.username); - if (!existingUser) { - throw new AuthenticationError("User does not exist"); - } - - const validPassword = await authService.verifyPassword( - existingUser.passwordHash, - input.password - ); - if (!validPassword) { - throw new AuthenticationError("Incorrect username or password"); - } - - return await authService.createSession(existingUser); -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cd packages/core && pnpm vitest run tests/unit/use-cases/auth/sign-in.use-case.test.ts` -Expected: PASS (3 tests) - -- [ ] **Step 5: Write sign-up test** - -```typescript -// packages/core/tests/unit/use-cases/auth/sign-up.use-case.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { signUpUseCase } from "@/application/use-cases/auth/sign-up.use-case.js"; -import { AuthenticationError } from "@/entities/errors/auth.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("signUpUseCase", () => { - it("creates user and returns session, cookie, and user info", async () => { - const result = await signUpUseCase({ - username: "newuser", - password: "securepassword", - }); - expect(result).toHaveProperty("session"); - expect(result).toHaveProperty("cookie"); - expect(result).toHaveProperty("user"); - expect(result.user.username).toBe("newuser"); - }); - - it("throws AuthenticationError if username is taken", async () => { - await expect( - signUpUseCase({ username: "alice", password: "anypassword" }) - ).rejects.toBeInstanceOf(AuthenticationError); - }); -}); -``` - -- [ ] **Step 6: Implement sign-up use case** - -```typescript -// packages/core/src/application/use-cases/auth/sign-up.use-case.ts -import { AuthenticationError } from "@/entities/errors/auth.js"; -import type { Cookie } from "@/entities/models/cookie.js"; -import type { Session } from "@/entities/models/session.js"; -import type { User } from "@/entities/models/user.js"; -import { getInjection } from "@/di/container.js"; - -export async function signUpUseCase(input: { - username: string; - password: string; -}): Promise<{ - session: Session; - cookie: Cookie; - user: Pick; -}> { - const usersRepository = getInjection("IUsersRepository"); - const authService = getInjection("IAuthenticationService"); - - const existingUser = await usersRepository.getUserByUsername(input.username); - if (existingUser) { - throw new AuthenticationError("Username taken"); - } - - const passwordHash = await authService.hashPassword(input.password); - const userId = authService.generateUserId(); - - const newUser = await usersRepository.createUser({ - id: userId, - username: input.username, - passwordHash, - }); - - const { cookie, session } = await authService.createSession(newUser); - - return { - cookie, - session, - user: { id: newUser.id, username: newUser.username }, - }; -} -``` - -- [ ] **Step 7: Run sign-up test** - -Run: `cd packages/core && pnpm vitest run tests/unit/use-cases/auth/sign-up.use-case.test.ts` -Expected: PASS (2 tests) - -- [ ] **Step 8: Write sign-out test** - -```typescript -// packages/core/tests/unit/use-cases/auth/sign-out.use-case.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { signOutUseCase } from "@/application/use-cases/auth/sign-out.use-case.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("signOutUseCase", () => { - it("returns a blank cookie", async () => { - const result = await signOutUseCase("some-session-id"); - expect(result).toHaveProperty("blankCookie"); - expect(result.blankCookie.value).toBe(""); - }); -}); -``` - -- [ ] **Step 9: Implement sign-out use case** - -```typescript -// packages/core/src/application/use-cases/auth/sign-out.use-case.ts -import type { Cookie } from "@/entities/models/cookie.js"; -import { getInjection } from "@/di/container.js"; - -export async function signOutUseCase( - sessionId: string -): Promise<{ blankCookie: Cookie }> { - const authService = getInjection("IAuthenticationService"); - return await authService.invalidateSession(sessionId); -} -``` - -- [ ] **Step 10: Run all auth tests** - -Run: `cd packages/core && pnpm vitest run tests/unit/use-cases/auth/` -Expected: PASS (6 tests total) - -- [ ] **Step 11: Commit** - -```bash -git add packages/core/src/application/use-cases/auth/ packages/core/tests/ -git commit -m "feat(core): add auth use cases with tests (sign-in, sign-up, sign-out)" -``` - ---- - -### Task 9: Content use cases + tests (TDD) - -**Files:** -- Create: `packages/core/src/application/use-cases/content/create-article.use-case.ts` -- Create: `packages/core/src/application/use-cases/content/get-articles.use-case.ts` -- Create: `packages/core/tests/unit/use-cases/content/create-article.use-case.test.ts` -- Create: `packages/core/tests/unit/use-cases/content/get-articles.use-case.test.ts` - -- [ ] **Step 1: Write create-article test** - -```typescript -// packages/core/tests/unit/use-cases/content/create-article.use-case.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { createArticleUseCase } from "@/application/use-cases/content/create-article.use-case.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("createArticleUseCase", () => { - it("creates an article with generated slug and draft status", async () => { - const result = await createArticleUseCase({ - title: "My First Article", - content: "Hello world", - authorId: "1", - }); - expect(result.title).toBe("My First Article"); - expect(result.slug).toBe("my-first-article"); - expect(result.status).toBe("draft"); - expect(result.authorId).toBe("1"); - expect(result.id).toBeDefined(); - }); - - it("uses provided slug if given", async () => { - const result = await createArticleUseCase({ - title: "Another Article", - content: "Content here", - authorId: "1", - slug: "custom-slug", - }); - expect(result.slug).toBe("custom-slug"); - }); -}); -``` - -- [ ] **Step 2: Implement create-article use case** - -```typescript -// packages/core/src/application/use-cases/content/create-article.use-case.ts -import type { Article } from "@/entities/models/article.js"; -import { getInjection } from "@/di/container.js"; - -function generateSlug(title: string): string { - return title - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""); -} - -export async function createArticleUseCase(input: { - title: string; - content: string; - authorId: string; - slug?: string; -}): Promise
{ - const articlesRepository = getInjection("IArticlesRepository"); - - const now = new Date(); - const article: Article = { - id: crypto.randomUUID(), - title: input.title, - slug: input.slug ?? generateSlug(input.title), - content: input.content, - status: "draft", - authorId: input.authorId, - createdAt: now, - updatedAt: now, - }; - - return await articlesRepository.createArticle(article); -} -``` - -- [ ] **Step 3: Run create-article test** - -Run: `cd packages/core && pnpm vitest run tests/unit/use-cases/content/create-article.use-case.test.ts` -Expected: PASS (2 tests) - -- [ ] **Step 4: Write get-articles test** - -```typescript -// packages/core/tests/unit/use-cases/content/get-articles.use-case.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { createArticleUseCase } from "@/application/use-cases/content/create-article.use-case.js"; -import { getArticlesUseCase } from "@/application/use-cases/content/get-articles.use-case.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("getArticlesUseCase", () => { - it("returns empty array when no articles exist", async () => { - const result = await getArticlesUseCase(); - expect(result).toEqual([]); - }); - - it("returns created articles", async () => { - await createArticleUseCase({ - title: "Article One", - content: "Content one", - authorId: "1", - }); - await createArticleUseCase({ - title: "Article Two", - content: "Content two", - authorId: "1", - }); - - const result = await getArticlesUseCase(); - expect(result).toHaveLength(2); - }); - - it("filters by status", async () => { - await createArticleUseCase({ - title: "Draft Article", - content: "Draft", - authorId: "1", - }); - - const result = await getArticlesUseCase({ status: "published" }); - expect(result).toHaveLength(0); - }); -}); -``` - -- [ ] **Step 5: Implement get-articles use case** - -```typescript -// packages/core/src/application/use-cases/content/get-articles.use-case.ts -import type { Article } from "@/entities/models/article.js"; -import { getInjection } from "@/di/container.js"; - -export async function getArticlesUseCase(options?: { - status?: string; - authorId?: string; - limit?: number; - offset?: number; -}): Promise { - const articlesRepository = getInjection("IArticlesRepository"); - return await articlesRepository.getArticles(options); -} -``` - -- [ ] **Step 6: Run all content tests** - -Run: `cd packages/core && pnpm vitest run tests/unit/use-cases/content/` -Expected: PASS (5 tests total) - -- [ ] **Step 7: Commit** - -```bash -git add packages/core/src/application/use-cases/content/ packages/core/tests/unit/use-cases/content/ -git commit -m "feat(core): add content use cases with tests (create-article, get-articles)" -``` - ---- - -### Task 10: Auth controllers + tests (TDD) - -**Files:** -- Create: `packages/core/src/interface-adapters/controllers/auth/sign-in.controller.ts` -- Create: `packages/core/src/interface-adapters/controllers/auth/sign-up.controller.ts` -- Create: `packages/core/src/interface-adapters/controllers/auth/sign-out.controller.ts` -- Create: `packages/core/tests/unit/controllers/auth/sign-in.controller.test.ts` -- Create: `packages/core/tests/unit/controllers/auth/sign-up.controller.test.ts` -- Create: `packages/core/tests/unit/controllers/auth/sign-out.controller.test.ts` - -- [ ] **Step 1: Write sign-in controller test** - -```typescript -// packages/core/tests/unit/controllers/auth/sign-in.controller.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { signInController } from "@/interface-adapters/controllers/auth/sign-in.controller.js"; -import { InputParseError } from "@/entities/errors/common.js"; -import { AuthenticationError } from "@/entities/errors/auth.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("signInController", () => { - it("returns cookie for valid input", async () => { - const cookie = await signInController({ - username: "alice", - password: "password_alice", - }); - expect(cookie).toHaveProperty("name"); - expect(cookie).toHaveProperty("value"); - }); - - it("throws InputParseError for invalid input", async () => { - await expect( - signInController({ username: "ab", password: "short" }) - ).rejects.toBeInstanceOf(InputParseError); - }); - - it("throws AuthenticationError for wrong credentials", async () => { - await expect( - signInController({ username: "alice", password: "wrongpassword" }) - ).rejects.toBeInstanceOf(AuthenticationError); - }); -}); -``` - -- [ ] **Step 2: Implement sign-in controller** - -```typescript -// packages/core/src/interface-adapters/controllers/auth/sign-in.controller.ts -import { z } from "zod"; - -import { InputParseError } from "@/entities/errors/common.js"; -import type { Cookie } from "@/entities/models/cookie.js"; -import { signInUseCase } from "@/application/use-cases/auth/sign-in.use-case.js"; - -const inputSchema = z.object({ - username: z.string().min(3).max(31), - password: z.string().min(6).max(255), -}); - -export async function signInController( - input: Partial> -): Promise { - const { data, error: inputParseError } = inputSchema.safeParse(input); - - if (inputParseError) { - throw new InputParseError("Invalid data", { cause: inputParseError }); - } - - const { cookie } = await signInUseCase(data); - return cookie; -} -``` - -- [ ] **Step 3: Run sign-in controller test** - -Run: `cd packages/core && pnpm vitest run tests/unit/controllers/auth/sign-in.controller.test.ts` -Expected: PASS (3 tests) - -- [ ] **Step 4: Write sign-up controller test** - -```typescript -// packages/core/tests/unit/controllers/auth/sign-up.controller.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { signUpController } from "@/interface-adapters/controllers/auth/sign-up.controller.js"; -import { InputParseError } from "@/entities/errors/common.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("signUpController", () => { - it("returns session, cookie, and user for valid input", async () => { - const result = await signUpController({ - username: "newuser", - password: "securepassword", - confirmPassword: "securepassword", - }); - expect(result).toHaveProperty("session"); - expect(result).toHaveProperty("cookie"); - expect(result).toHaveProperty("user"); - }); - - it("throws InputParseError when passwords don't match", async () => { - await expect( - signUpController({ - username: "newuser", - password: "password1", - confirmPassword: "password2", - }) - ).rejects.toBeInstanceOf(InputParseError); - }); - - it("throws InputParseError for missing fields", async () => { - await expect(signUpController({})).rejects.toBeInstanceOf(InputParseError); - }); -}); -``` - -- [ ] **Step 5: Implement sign-up controller** - -```typescript -// packages/core/src/interface-adapters/controllers/auth/sign-up.controller.ts -import { z } from "zod"; - -import { InputParseError } from "@/entities/errors/common.js"; -import { signUpUseCase } from "@/application/use-cases/auth/sign-up.use-case.js"; - -const inputSchema = z - .object({ - username: z.string().min(3).max(31), - password: z.string().min(6).max(255), - confirmPassword: z.string().min(6).max(255), - }) - .superRefine(({ password, confirmPassword }, ctx) => { - if (confirmPassword !== password) { - ctx.addIssue({ - code: "custom", - message: "The passwords did not match", - path: ["password"], - }); - ctx.addIssue({ - code: "custom", - message: "The passwords did not match", - path: ["confirmPassword"], - }); - } - }); - -export async function signUpController( - input: Partial> -): Promise> { - const { data, error: inputParseError } = inputSchema.safeParse(input); - - if (inputParseError) { - throw new InputParseError("Invalid data", { cause: inputParseError }); - } - - return await signUpUseCase(data); -} -``` - -- [ ] **Step 6: Run sign-up controller test** - -Run: `cd packages/core && pnpm vitest run tests/unit/controllers/auth/sign-up.controller.test.ts` -Expected: PASS (3 tests) - -- [ ] **Step 7: Write sign-out controller test** - -```typescript -// packages/core/tests/unit/controllers/auth/sign-out.controller.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { signOutController } from "@/interface-adapters/controllers/auth/sign-out.controller.js"; -import { InputParseError } from "@/entities/errors/common.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("signOutController", () => { - it("returns blank cookie for valid session", async () => { - const cookie = await signOutController("some-session-id"); - expect(cookie.value).toBe(""); - }); - - it("throws InputParseError when no session ID provided", async () => { - await expect(signOutController(undefined)).rejects.toBeInstanceOf( - InputParseError - ); - }); -}); -``` - -- [ ] **Step 8: Implement sign-out controller** - -```typescript -// packages/core/src/interface-adapters/controllers/auth/sign-out.controller.ts -import { InputParseError } from "@/entities/errors/common.js"; -import type { Cookie } from "@/entities/models/cookie.js"; -import { signOutUseCase } from "@/application/use-cases/auth/sign-out.use-case.js"; - -export async function signOutController( - sessionId: string | undefined -): Promise { - if (!sessionId) { - throw new InputParseError("Must provide a session ID"); - } - - const { blankCookie } = await signOutUseCase(sessionId); - return blankCookie; -} -``` - -- [ ] **Step 9: Run all auth controller tests** - -Run: `cd packages/core && pnpm vitest run tests/unit/controllers/auth/` -Expected: PASS (8 tests total) - -- [ ] **Step 10: Commit** - -```bash -git add packages/core/src/interface-adapters/controllers/auth/ packages/core/tests/unit/controllers/auth/ -git commit -m "feat(core): add auth controllers with tests (sign-in, sign-up, sign-out)" -``` - ---- - -### Task 11: Content controller + tests (TDD) - -**Files:** -- Create: `packages/core/src/interface-adapters/controllers/content/articles.controller.ts` -- Create: `packages/core/tests/unit/controllers/content/articles.controller.test.ts` - -- [ ] **Step 1: Write articles controller test** - -```typescript -// packages/core/tests/unit/controllers/content/articles.controller.test.ts -import "reflect-metadata"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; - -import { - destroyContainer, - initializeContainer, -} from "@/di/container.js"; -import { - createArticleController, - getArticlesController, -} from "@/interface-adapters/controllers/content/articles.controller.js"; -import { InputParseError } from "@/entities/errors/common.js"; - -beforeEach(() => { - initializeContainer(); -}); - -afterEach(() => { - destroyContainer(); -}); - -describe("createArticleController", () => { - it("creates an article with valid input", async () => { - const result = await createArticleController({ - title: "Test Article", - content: "Some content", - authorId: "1", - }); - expect(result.title).toBe("Test Article"); - expect(result.slug).toBe("test-article"); - }); - - it("throws InputParseError for missing title", async () => { - await expect( - createArticleController({ content: "content", authorId: "1" } as any) - ).rejects.toBeInstanceOf(InputParseError); - }); -}); - -describe("getArticlesController", () => { - it("returns articles", async () => { - await createArticleController({ - title: "Article", - content: "Content", - authorId: "1", - }); - const result = await getArticlesController({}); - expect(result).toHaveLength(1); - }); -}); -``` - -- [ ] **Step 2: Implement articles controller** - -```typescript -// packages/core/src/interface-adapters/controllers/content/articles.controller.ts -import { z } from "zod"; - -import { InputParseError } from "@/entities/errors/common.js"; -import type { Article } from "@/entities/models/article.js"; -import { createArticleUseCase } from "@/application/use-cases/content/create-article.use-case.js"; -import { getArticlesUseCase } from "@/application/use-cases/content/get-articles.use-case.js"; - -const createInputSchema = z.object({ - title: z.string().min(1).max(255), - content: z.string(), - authorId: z.string(), - slug: z.string().optional(), -}); - -const getInputSchema = z.object({ - status: z.string().optional(), - authorId: z.string().optional(), - limit: z.number().optional(), - offset: z.number().optional(), -}); - -export async function createArticleController( - input: Partial> -): Promise
{ - const { data, error: inputParseError } = createInputSchema.safeParse(input); - - if (inputParseError) { - throw new InputParseError("Invalid data", { cause: inputParseError }); - } - - return await createArticleUseCase(data); -} - -export async function getArticlesController( - input: Partial> -): Promise { - const { data, error: inputParseError } = getInputSchema.safeParse(input); - - if (inputParseError) { - throw new InputParseError("Invalid data", { cause: inputParseError }); - } - - return await getArticlesUseCase(data); -} -``` - -- [ ] **Step 3: Run content controller tests** - -Run: `cd packages/core && pnpm vitest run tests/unit/controllers/content/` -Expected: PASS (3 tests) - -- [ ] **Step 4: Commit** - -```bash -git add packages/core/src/interface-adapters/controllers/content/ packages/core/tests/unit/controllers/content/ -git commit -m "feat(core): add content controller with tests (articles CRUD)" -``` - ---- - -### Task 12: Update index.ts + run all tests - -**Files:** -- Modify: `packages/core/src/index.ts` - -- [ ] **Step 1: Update src/index.ts with public API** - -```typescript -// @repo/core — Clean Architecture core package -export * from "./entities/index.js"; -export * from "./application/repositories/index.js"; -export * from "./application/services/index.js"; -export { signInUseCase } from "./application/use-cases/auth/sign-in.use-case.js"; -export { signUpUseCase } from "./application/use-cases/auth/sign-up.use-case.js"; -export { signOutUseCase } from "./application/use-cases/auth/sign-out.use-case.js"; -export { createArticleUseCase } from "./application/use-cases/content/create-article.use-case.js"; -export { getArticlesUseCase } from "./application/use-cases/content/get-articles.use-case.js"; -export { signInController } from "./interface-adapters/controllers/auth/sign-in.controller.js"; -export { signUpController } from "./interface-adapters/controllers/auth/sign-up.controller.js"; -export { signOutController } from "./interface-adapters/controllers/auth/sign-out.controller.js"; -export { - createArticleController, - getArticlesController, -} from "./interface-adapters/controllers/content/articles.controller.js"; -export { - getInjection, - initializeContainer, - destroyContainer, -} from "./di/container.js"; -export { DI_SYMBOLS } from "./di/types.js"; -``` - -- [ ] **Step 2: Run ALL tests** - -Run: `cd packages/core && pnpm vitest run` -Expected: PASS — all tests (approximately 19 tests across 9 test files) - -- [ ] **Step 3: Run turbo build from root** - -Run: `pnpm build` -Expected: All workspaces build successfully - -- [ ] **Step 4: Commit** - -```bash -git add packages/core/src/index.ts -git commit -m "feat(core): update public API exports" -``` diff --git a/docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md b/docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md deleted file mode 100644 index aacf4f5..0000000 --- a/docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md +++ /dev/null @@ -1,977 +0,0 @@ -# Plan 3: Payload CMS Integration — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Implement Payload CMS integration: `@repo/cms-core` (config, collections, hooks), `@repo/cms-client` (dual-mode local+HTTP client with generated types), and `apps/cms` (thin Next.js shell serving the Payload admin panel). - -**Architecture:** Payload config and collections live in `@repo/cms-core` — standalone, testable, framework-agnostic. `@repo/cms-client` provides a dual-mode client (Local API primary, HTTP fallback) that receives a Payload instance via injection. `apps/cms` is a thin Next.js 15 shell that imports config from cms-core and serves the admin panel. Type generation runs `payload generate:types` against cms-core's config. - -**Tech Stack:** Payload CMS 3.x, @payloadcms/db-postgres, @payloadcms/next, @payloadcms/richtext-lexical, Next.js 15, PostgreSQL 16, sharp - ---- - -## File Map - -### packages/cms-core -| File | Responsibility | -|---|---| -| `packages/cms-core/package.json` | Dependencies: payload, @payloadcms/db-postgres, @payloadcms/richtext-lexical | -| `packages/cms-core/src/payload.config.ts` | Root Payload config (db, editor, collections, globals) | -| `packages/cms-core/src/collections/users/index.ts` | Users collection with auth enabled | -| `packages/cms-core/src/collections/articles/index.ts` | Articles collection config | -| `packages/cms-core/src/collections/articles/fields.ts` | Article field definitions | -| `packages/cms-core/src/collections/articles/hooks/before-change.ts` | Slug auto-generation hook | -| `packages/cms-core/src/collections/media/index.ts` | Media collection with uploads | -| `packages/cms-core/src/globals/site-settings.ts` | Site settings global | -| `packages/cms-core/src/index.ts` | Exports config + all collections | - -### packages/cms-client -| File | Responsibility | -|---|---| -| `packages/cms-client/package.json` | Dependencies: payload (types only) | -| `packages/cms-client/src/client.ts` | createPayloadClient() factory | -| `packages/cms-client/src/local-client.ts` | LocalPayloadClient — wraps Payload instance | -| `packages/cms-client/src/http-client.ts` | HTTPPayloadClient — REST API fallback | -| `packages/cms-client/src/types.ts` | Shared client types (PayloadClient interface) | -| `packages/cms-client/src/index.ts` | Exports | - -### apps/cms -| File | Responsibility | -|---|---| -| `apps/cms/package.json` | Dependencies: next, payload, @payloadcms/next, @repo/cms-core | -| `apps/cms/next.config.mjs` | Next.js config wrapped with withPayload | -| `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx` | Admin panel catch-all route | -| `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx` | Admin 404 page | -| `apps/cms/src/app/(payload)/layout.tsx` | Payload layout with RootLayout | -| `apps/cms/src/app/(payload)/custom.scss` | Empty custom styles | -| `apps/cms/src/payload-types.ts` | Generated types (via payload generate:types) | - ---- - -### Task 1: Install cms-core dependencies - -**Files:** -- Modify: `packages/cms-core/package.json` -- Modify: `packages/cms-core/tsconfig.json` - -- [ ] **Step 1: Update packages/cms-core/package.json** - -```json -{ - "name": "@repo/cms-core", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "payload": "^3.14.0", - "@payloadcms/db-postgres": "^3.14.0", - "@payloadcms/richtext-lexical": "^3.14.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@types/node": "^22.0.0" - } -} -``` - -- [ ] **Step 2: Update tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 3: Run pnpm install** - -Run: `pnpm install` -Expected: Payload and its dependencies install successfully. - -- [ ] **Step 4: Commit** - -```bash -git add packages/cms-core/package.json packages/cms-core/tsconfig.json pnpm-lock.yaml -git commit -m "feat(cms-core): add Payload CMS dependencies" -``` - ---- - -### Task 2: Users collection - -**Files:** -- Create: `packages/cms-core/src/collections/users/index.ts` - -- [ ] **Step 1: Create Users collection** - -```typescript -import type { CollectionConfig } from "payload"; - -export const Users: CollectionConfig = { - slug: "users", - auth: true, - admin: { - useAsTitle: "email", - }, - fields: [ - { - name: "displayName", - type: "text", - }, - { - name: "role", - type: "select", - options: [ - { label: "Admin", value: "admin" }, - { label: "Editor", value: "editor" }, - { label: "Author", value: "author" }, - ], - defaultValue: "author", - required: true, - }, - ], -}; -``` - -- [ ] **Step 2: Commit** - -```bash -git add packages/cms-core/src/collections/users/ -git commit -m "feat(cms-core): add Users collection with auth" -``` - ---- - -### Task 3: Articles collection with fields and hooks - -**Files:** -- Create: `packages/cms-core/src/collections/articles/fields.ts` -- Create: `packages/cms-core/src/collections/articles/hooks/before-change.ts` -- Create: `packages/cms-core/src/collections/articles/index.ts` - -- [ ] **Step 1: Create fields.ts** - -```typescript -import type { Field } from "payload"; - -export const articleFields: Field[] = [ - { - name: "title", - type: "text", - required: true, - maxLength: 255, - }, - { - name: "slug", - type: "text", - unique: true, - admin: { - position: "sidebar", - description: "Auto-generated from title if left empty", - }, - }, - { - name: "content", - type: "richText", - }, - { - name: "status", - type: "select", - options: [ - { label: "Draft", value: "draft" }, - { label: "Published", value: "published" }, - ], - defaultValue: "draft", - required: true, - admin: { - position: "sidebar", - }, - }, - { - name: "author", - type: "relationship", - relationTo: "users", - required: true, - admin: { - position: "sidebar", - }, - }, - { - name: "featuredImage", - type: "upload", - relationTo: "media", - }, - { - name: "publishedAt", - type: "date", - admin: { - position: "sidebar", - date: { - pickerAppearance: "dayAndTime", - }, - }, - }, -]; -``` - -- [ ] **Step 2: Create hooks/before-change.ts** - -This is a CMS-operational hook (slug auto-generation) — stays in cms-core per the design spec. - -```typescript -import type { CollectionBeforeChangeHook } from "payload"; - -function generateSlug(title: string): string { - return title - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""); -} - -export const autoGenerateSlug: CollectionBeforeChangeHook = ({ - data, - operation, -}) => { - if (operation === "create" || operation === "update") { - if (data && data.title && !data.slug) { - data.slug = generateSlug(data.title); - } - } - return data; -}; -``` - -- [ ] **Step 3: Create articles/index.ts** - -```typescript -import type { CollectionConfig } from "payload"; - -import { articleFields } from "./fields.js"; -import { autoGenerateSlug } from "./hooks/before-change.js"; - -export const Articles: CollectionConfig = { - slug: "articles", - admin: { - useAsTitle: "title", - defaultColumns: ["title", "status", "author", "updatedAt"], - }, - hooks: { - beforeChange: [autoGenerateSlug], - }, - versions: { - drafts: true, - }, - fields: articleFields, -}; -``` - -- [ ] **Step 4: Commit** - -```bash -git add packages/cms-core/src/collections/articles/ -git commit -m "feat(cms-core): add Articles collection with slug auto-generation hook" -``` - ---- - -### Task 4: Media collection - -**Files:** -- Create: `packages/cms-core/src/collections/media/index.ts` - -- [ ] **Step 1: Create Media collection** - -```typescript -import type { CollectionConfig } from "payload"; - -export const Media: CollectionConfig = { - slug: "media", - upload: { - mimeTypes: ["image/*", "application/pdf"], - }, - admin: { - useAsTitle: "filename", - }, - fields: [ - { - name: "alt", - type: "text", - required: true, - }, - ], -}; -``` - -- [ ] **Step 2: Commit** - -```bash -git add packages/cms-core/src/collections/media/ -git commit -m "feat(cms-core): add Media collection with uploads" -``` - ---- - -### Task 5: Site settings global - -**Files:** -- Create: `packages/cms-core/src/globals/site-settings.ts` - -- [ ] **Step 1: Create site-settings.ts** - -```typescript -import type { GlobalConfig } from "payload"; - -export const SiteSettings: GlobalConfig = { - slug: "site-settings", - admin: { - group: "Settings", - }, - fields: [ - { - name: "siteName", - type: "text", - required: true, - defaultValue: "My App", - }, - { - name: "siteDescription", - type: "textarea", - }, - ], -}; -``` - -- [ ] **Step 2: Commit** - -```bash -git add packages/cms-core/src/globals/ -git commit -m "feat(cms-core): add SiteSettings global" -``` - ---- - -### Task 6: Payload config + exports - -**Files:** -- Create: `packages/cms-core/src/payload.config.ts` -- Modify: `packages/cms-core/src/index.ts` - -- [ ] **Step 1: Create payload.config.ts** - -```typescript -import { buildConfig } from "payload"; -import { postgresAdapter } from "@payloadcms/db-postgres"; -import { lexicalEditor } from "@payloadcms/richtext-lexical"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { Users } from "./collections/users/index.js"; -import { Articles } from "./collections/articles/index.js"; -import { Media } from "./collections/media/index.js"; -import { SiteSettings } from "./globals/site-settings.js"; - -const filename = fileURLToPath(import.meta.url); -const dirname = path.dirname(filename); - -export default buildConfig({ - editor: lexicalEditor(), - collections: [Users, Articles, Media], - globals: [SiteSettings], - secret: process.env.PAYLOAD_SECRET || "default-secret-change-me", - db: postgresAdapter({ - pool: { - connectionString: - process.env.DATABASE_URL || - "postgresql://postgres:postgres@localhost:5432/template", - }, - }), - typescript: { - outputFile: path.resolve(dirname, "payload-types.ts"), - }, -}); -``` - -- [ ] **Step 2: Update src/index.ts** - -```typescript -export { Users } from "./collections/users/index.js"; -export { Articles } from "./collections/articles/index.js"; -export { Media } from "./collections/media/index.js"; -export { SiteSettings } from "./globals/site-settings.js"; -export { default as config } from "./payload.config.js"; -``` - -- [ ] **Step 3: Commit** - -```bash -git add packages/cms-core/src/payload.config.ts packages/cms-core/src/index.ts -git commit -m "feat(cms-core): add Payload config with postgres adapter and lexical editor" -``` - ---- - -### Task 7: cms-client — dual-mode Payload client - -**Files:** -- Modify: `packages/cms-client/package.json` -- Create: `packages/cms-client/src/types.ts` -- Create: `packages/cms-client/src/local-client.ts` -- Create: `packages/cms-client/src/http-client.ts` -- Create: `packages/cms-client/src/client.ts` -- Modify: `packages/cms-client/src/index.ts` - -- [ ] **Step 1: Update packages/cms-client/package.json** - -```json -{ - "name": "@repo/cms-client", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "payload": "^3.14.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@types/node": "^22.0.0" - } -} -``` - -- [ ] **Step 2: Create src/types.ts** - -```typescript -export interface FindOptions { - where?: Record; - sort?: string; - limit?: number; - page?: number; - depth?: number; - locale?: string; -} - -export interface PayloadClientResult { - docs: T[]; - totalDocs: number; - limit: number; - totalPages: number; - page: number; - pagingCounter: number; - hasPrevPage: boolean; - hasNextPage: boolean; - prevPage: number | null; - nextPage: number | null; -} - -export interface PayloadClient { - find>( - collection: string, - options?: FindOptions - ): Promise>; - - findByID>( - collection: string, - id: string, - options?: { depth?: number } - ): Promise; - - create>( - collection: string, - data: Record, - options?: { depth?: number } - ): Promise; - - update>( - collection: string, - id: string, - data: Record, - options?: { depth?: number } - ): Promise; - - delete>( - collection: string, - id: string - ): Promise; -} -``` - -- [ ] **Step 3: Create src/local-client.ts** - -```typescript -import type { Payload } from "payload"; -import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js"; - -export class LocalPayloadClient implements PayloadClient { - constructor(private payload: Payload) {} - - async find>( - collection: string, - options?: FindOptions - ): Promise> { - const result = await this.payload.find({ - collection: collection as any, - where: options?.where as any, - sort: options?.sort, - limit: options?.limit, - page: options?.page, - depth: options?.depth, - locale: options?.locale as any, - }); - return result as unknown as PayloadClientResult; - } - - async findByID>( - collection: string, - id: string, - options?: { depth?: number } - ): Promise { - const result = await this.payload.findByID({ - collection: collection as any, - id, - depth: options?.depth, - }); - return result as unknown as T; - } - - async create>( - collection: string, - data: Record, - options?: { depth?: number } - ): Promise { - const result = await this.payload.create({ - collection: collection as any, - data: data as any, - depth: options?.depth, - }); - return result as unknown as T; - } - - async update>( - collection: string, - id: string, - data: Record, - options?: { depth?: number } - ): Promise { - const result = await this.payload.update({ - collection: collection as any, - id, - data: data as any, - depth: options?.depth, - }); - return result as unknown as T; - } - - async delete>( - collection: string, - id: string - ): Promise { - const result = await this.payload.delete({ - collection: collection as any, - id, - }); - return result as unknown as T; - } -} -``` - -- [ ] **Step 4: Create src/http-client.ts** - -```typescript -import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js"; - -export class HTTPPayloadClient implements PayloadClient { - constructor(private baseURL: string) {} - - private async request(path: string, options?: RequestInit): Promise { - const response = await fetch(`${this.baseURL}${path}`, { - headers: { "Content-Type": "application/json" }, - ...options, - }); - if (!response.ok) { - throw new Error(`Payload API error: ${response.status} ${response.statusText}`); - } - return response.json() as Promise; - } - - async find>( - collection: string, - options?: FindOptions - ): Promise> { - const params = new URLSearchParams(); - if (options?.limit) params.set("limit", String(options.limit)); - if (options?.page) params.set("page", String(options.page)); - if (options?.sort) params.set("sort", options.sort); - if (options?.depth) params.set("depth", String(options.depth)); - if (options?.where) params.set("where", JSON.stringify(options.where)); - const query = params.toString(); - return this.request>( - `/api/${collection}${query ? `?${query}` : ""}` - ); - } - - async findByID>( - collection: string, - id: string, - options?: { depth?: number } - ): Promise { - const params = new URLSearchParams(); - if (options?.depth) params.set("depth", String(options.depth)); - const query = params.toString(); - return this.request( - `/api/${collection}/${id}${query ? `?${query}` : ""}` - ); - } - - async create>( - collection: string, - data: Record, - options?: { depth?: number } - ): Promise { - const params = new URLSearchParams(); - if (options?.depth) params.set("depth", String(options.depth)); - const query = params.toString(); - return this.request( - `/api/${collection}${query ? `?${query}` : ""}`, - { method: "POST", body: JSON.stringify(data) } - ); - } - - async update>( - collection: string, - id: string, - data: Record, - options?: { depth?: number } - ): Promise { - const params = new URLSearchParams(); - if (options?.depth) params.set("depth", String(options.depth)); - const query = params.toString(); - return this.request( - `/api/${collection}/${id}${query ? `?${query}` : ""}`, - { method: "PATCH", body: JSON.stringify(data) } - ); - } - - async delete>( - collection: string, - id: string - ): Promise { - return this.request(`/api/${collection}/${id}`, { method: "DELETE" }); - } -} -``` - -- [ ] **Step 5: Create src/client.ts** - -```typescript -import type { Payload } from "payload"; -import type { PayloadClient } from "./types.js"; -import { LocalPayloadClient } from "./local-client.js"; -import { HTTPPayloadClient } from "./http-client.js"; - -type PayloadClientOptions = - | { mode: "local"; payload: Payload } - | { mode: "http"; baseURL: string }; - -export function createPayloadClient(options: PayloadClientOptions): PayloadClient { - if (options.mode === "local") { - return new LocalPayloadClient(options.payload); - } - return new HTTPPayloadClient(options.baseURL); -} -``` - -- [ ] **Step 6: Update src/index.ts** - -```typescript -export { createPayloadClient } from "./client.js"; -export { LocalPayloadClient } from "./local-client.js"; -export { HTTPPayloadClient } from "./http-client.js"; -export type { - PayloadClient, - PayloadClientResult, - FindOptions, -} from "./types.js"; -``` - -- [ ] **Step 7: Run pnpm install and commit** - -Run: `pnpm install` - -```bash -git add packages/cms-client/ pnpm-lock.yaml -git commit -m "feat(cms-client): add dual-mode Payload client (local + HTTP)" -``` - ---- - -### Task 8: apps/cms — thin Next.js shell - -**Files:** -- Modify: `apps/cms/package.json` -- Create: `apps/cms/next.config.mjs` -- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx` -- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx` -- Create: `apps/cms/src/app/(payload)/layout.tsx` -- Create: `apps/cms/src/app/(payload)/custom.scss` -- Modify: `apps/cms/tsconfig.json` - -- [ ] **Step 1: Update apps/cms/package.json** - -```json -{ - "name": "@repo/cms", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "build": "next build", - "dev": "next dev --port 3001", - "lint": "eslint .", - "typecheck": "tsc --noEmit", - "generate:types": "payload generate:types" - }, - "dependencies": { - "@payloadcms/next": "^3.14.0", - "@payloadcms/ui": "^3.14.0", - "@repo/cms-core": "workspace:*", - "next": "^15.3.0", - "payload": "^3.14.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "sharp": "^0.33.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0" - } -} -``` - -- [ ] **Step 2: Update apps/cms/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/nextjs.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"], - "@payload-config": ["../../packages/cms-core/src/payload.config.ts"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"], - "exclude": ["node_modules"] -} -``` - -- [ ] **Step 3: Create next.config.mjs** - -```javascript -import { withPayload } from "@payloadcms/next/withPayload"; - -/** @type {import('next').NextConfig} */ -const nextConfig = {}; - -export default withPayload(nextConfig); -``` - -- [ ] **Step 4: Create admin catch-all page** - -```tsx -// apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx -/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ -/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import type { Metadata } from "next"; - -import config from "@payload-config"; -import { RootPage, generatePageMetadata } from "@payloadcms/next/views"; -import { importMap } from "../importMap.js"; - -type Args = { - params: Promise<{ segments: string[] }>; - searchParams: Promise>; -}; - -export const generateMetadata = ({ - params, - searchParams, -}: Args): Promise => - generatePageMetadata({ config, params, searchParams }); - -const Page = ({ params, searchParams }: Args) => - RootPage({ config, importMap, params, searchParams }); - -export default Page; -``` - -- [ ] **Step 5: Create admin not-found page** - -```tsx -// apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx -/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ -/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import type { Metadata } from "next"; - -import config from "@payload-config"; -import { NotFoundPage, generatePageMetadata } from "@payloadcms/next/views"; -import { importMap } from "../importMap.js"; - -type Args = { - params: Promise<{ segments: string[] }>; - searchParams: Promise>; -}; - -export const generateMetadata = ({ - params, - searchParams, -}: Args): Promise => - generatePageMetadata({ config, params, searchParams }); - -const NotFound = ({ params, searchParams }: Args) => - NotFoundPage({ config, importMap, params, searchParams }); - -export default NotFound; -``` - -- [ ] **Step 6: Create payload layout** - -```tsx -// apps/cms/src/app/(payload)/layout.tsx -/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ -/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import type { ServerFunctionClient } from "payload"; - -import config from "@payload-config"; -import { RootLayout } from "@payloadcms/next/layouts"; -import React from "react"; - -import { importMap } from "./importMap.js"; -import "./custom.scss"; - -type Args = { - children: React.ReactNode; -}; - -const serverFunction: ServerFunctionClient = async function (args) { - "use server"; - const { default: payloadModule } = await import("payload"); - return payloadModule.handleServerFunctions({ ...args, config, importMap }); -}; - -const Layout = ({ children }: Args) => ( - - {children} - -); - -export default Layout; -``` - -- [ ] **Step 7: Create empty importMap and custom.scss** - -```typescript -// apps/cms/src/app/(payload)/importMap.js -export const importMap = {}; -``` - -```scss -// apps/cms/src/app/(payload)/custom.scss -// Custom admin panel styles -``` - -- [ ] **Step 8: Run pnpm install and commit** - -Run: `pnpm install` - -```bash -git add apps/cms/ pnpm-lock.yaml -git commit -m "feat(cms): add thin Next.js shell for Payload admin panel" -``` - ---- - -### Task 9: Update docker-compose with CMS service - -**Files:** -- Modify: `docker-compose.yml` - -- [ ] **Step 1: Update docker-compose.yml** - -```yaml -services: - postgres: - image: postgres:16-alpine - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: template - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 - -volumes: - postgres_data: -``` - -Note: CMS app service will be added when Dockerfiles are created in a later plan. For now, local dev uses `pnpm dev --filter @repo/cms`. - -- [ ] **Step 2: Commit (no change needed if docker-compose is already correct)** - -Only commit if docker-compose was modified. - ---- - -### Task 10: Verify build - -- [ ] **Step 1: Run pnpm install from root** - -Run: `pnpm install` -Expected: All dependencies resolve. - -- [ ] **Step 2: Run turbo build** - -Run: `pnpm build` -Expected: All workspaces build. The cms-core and cms-client packages should pass `tsc --noEmit`. The cms app should run `next build` (may need database for full build — if it fails due to no DB, that's expected and acceptable at this stage). - -- [ ] **Step 3: Commit any remaining changes** - -```bash -git add -A && git status -# Only commit if there are changes -git commit -m "chore: update lockfile after CMS integration" -``` diff --git a/docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md b/docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md deleted file mode 100644 index 46fe84f..0000000 --- a/docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md +++ /dev/null @@ -1,628 +0,0 @@ -# Plan 4: API Layer + App Shells — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Implement tRPC routers (`@repo/api`), shared React Query hooks (`@repo/api-client`), and both app shells (`apps/web-next` with Next.js 15, `apps/web-tanstack` with TanStack Start) — completing the full data flow from UI to core. - -**Architecture:** `@repo/api` defines the tRPC router that calls controllers from `@repo/core`. `@repo/api-client` provides a framework-agnostic React Query provider and typed hooks. Each app hosts its own tRPC HTTP endpoint and wraps with the shared provider. Both apps use identical hooks. - -**Tech Stack:** tRPC v11, @trpc/tanstack-react-query, TanStack Query v5, Next.js 15 (App Router), TanStack Start (Vite-based), Zustand - ---- - -## File Map - -### packages/api -| File | Responsibility | -|---|---| -| `packages/api/package.json` | tRPC server deps | -| `packages/api/src/trpc.ts` | tRPC init, context, middleware | -| `packages/api/src/router/auth.router.ts` | Auth procedures | -| `packages/api/src/router/content.router.ts` | Content procedures | -| `packages/api/src/router/index.ts` | Root appRouter | -| `packages/api/src/index.ts` | Exports AppRouter type | - -### packages/api-client -| File | Responsibility | -|---|---| -| `packages/api-client/package.json` | tRPC client + React Query deps | -| `packages/api-client/src/trpc.ts` | createTRPCReact instance | -| `packages/api-client/src/query-client.ts` | Shared QueryClient factory | -| `packages/api-client/src/provider.tsx` | ApiProvider component | -| `packages/api-client/src/index.ts` | Exports provider + trpc | - -### apps/web-next -| File | Responsibility | -|---|---| -| `apps/web-next/package.json` | Next.js 15 + deps | -| `apps/web-next/next.config.mjs` | Next.js config | -| `apps/web-next/src/app/layout.tsx` | Root layout with ApiProvider | -| `apps/web-next/src/app/page.tsx` | Home page | -| `apps/web-next/src/app/api/trpc/[trpc]/route.ts` | tRPC HTTP handler | -| `apps/web-next/src/lib/payload.ts` | Payload instance initialization | - -### apps/web-tanstack -| File | Responsibility | -|---|---| -| `apps/web-tanstack/package.json` | TanStack Start + deps | -| `apps/web-tanstack/vite.config.ts` | Vite + TanStack Start plugin | -| `apps/web-tanstack/src/router.tsx` | TanStack Router config | -| `apps/web-tanstack/src/routes/__root.tsx` | Root layout with ApiProvider | -| `apps/web-tanstack/src/routes/index.tsx` | Home page | -| `apps/web-tanstack/src/lib/payload.ts` | Payload instance initialization | - ---- - -### Task 1: packages/api — tRPC routers - -**Files:** -- Modify: `packages/api/package.json` -- Modify: `packages/api/tsconfig.json` -- Create: `packages/api/src/trpc.ts` -- Create: `packages/api/src/router/auth.router.ts` -- Create: `packages/api/src/router/content.router.ts` -- Create: `packages/api/src/router/index.ts` -- Modify: `packages/api/src/index.ts` - -- [ ] **Step 1: Update packages/api/package.json** - -```json -{ - "name": "@repo/api", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@repo/core": "workspace:*", - "@trpc/server": "^11.1.0", - "zod": "^3.24.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@types/node": "^22.0.0" - } -} -``` - -- [ ] **Step 2: Update packages/api/tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/base.json", - "compilerOptions": { - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 3: Create src/trpc.ts** - -```typescript -import { initTRPC } from "@trpc/server"; - -const t = initTRPC.create(); - -export const router = t.router; -export const publicProcedure = t.procedure; -``` - -- [ ] **Step 4: Create src/router/auth.router.ts** - -```typescript -import { z } from "zod"; -import { router, publicProcedure } from "../trpc.js"; -import { - signInController, - signUpController, - signOutController, -} from "@repo/core"; - -export const authRouter = router({ - signIn: publicProcedure - .input( - z.object({ - username: z.string().min(3).max(31), - password: z.string().min(6).max(255), - }) - ) - .mutation(async ({ input }) => { - return await signInController(input); - }), - - signUp: publicProcedure - .input( - z.object({ - username: z.string().min(3).max(31), - password: z.string().min(6).max(255), - confirmPassword: z.string().min(6).max(255), - }) - ) - .mutation(async ({ input }) => { - return await signUpController(input); - }), - - signOut: publicProcedure - .input(z.object({ sessionId: z.string() })) - .mutation(async ({ input }) => { - return await signOutController(input.sessionId); - }), -}); -``` - -- [ ] **Step 5: Create src/router/content.router.ts** - -```typescript -import { z } from "zod"; -import { router, publicProcedure } from "../trpc.js"; -import { createArticleController, getArticlesController } from "@repo/core"; - -export const contentRouter = router({ - listArticles: publicProcedure - .input( - z - .object({ - status: z.string().optional(), - authorId: z.string().optional(), - limit: z.number().optional(), - offset: z.number().optional(), - }) - .optional() - ) - .query(async ({ input }) => { - return await getArticlesController(input ?? {}); - }), - - createArticle: publicProcedure - .input( - z.object({ - title: z.string().min(1).max(255), - content: z.string(), - authorId: z.string(), - slug: z.string().optional(), - }) - ) - .mutation(async ({ input }) => { - return await createArticleController(input); - }), -}); -``` - -- [ ] **Step 6: Create src/router/index.ts** - -```typescript -import { router } from "../trpc.js"; -import { authRouter } from "./auth.router.js"; -import { contentRouter } from "./content.router.js"; - -export const appRouter = router({ - auth: authRouter, - content: contentRouter, -}); - -export type AppRouter = typeof appRouter; -``` - -- [ ] **Step 7: Update src/index.ts** - -```typescript -export { appRouter, type AppRouter } from "./router/index.js"; -``` - -- [ ] **Step 8: Run pnpm install and commit** - -Run: `pnpm install` - -```bash -git add packages/api/ pnpm-lock.yaml -git commit -m "feat(api): add tRPC routers (auth + content) calling core controllers" -``` - ---- - -### Task 2: packages/api-client — shared React Query hooks + provider - -**Files:** -- Modify: `packages/api-client/package.json` -- Modify: `packages/api-client/tsconfig.json` -- Create: `packages/api-client/src/trpc.ts` -- Create: `packages/api-client/src/query-client.ts` -- Create: `packages/api-client/src/provider.tsx` -- Modify: `packages/api-client/src/index.ts` - -- [ ] **Step 1: Update packages/api-client/package.json** - -```json -{ - "name": "@repo/api-client", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@repo/api": "workspace:*", - "@trpc/client": "^11.1.0", - "@trpc/tanstack-react-query": "^11.1.0", - "@tanstack/react-query": "^5.75.0", - "react": "^19.0.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@types/react": "^19.0.0" - } -} -``` - -- [ ] **Step 2: Update tsconfig.json** - -```json -{ - "extends": "@repo/typescript-config/react-library.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist"] -} -``` - -- [ ] **Step 3: Create src/trpc.ts** - -```typescript -import { createTRPCContext } from "@trpc/tanstack-react-query"; -import type { AppRouter } from "@repo/api"; - -export const { TRPCProvider, useTRPC } = createTRPCContext(); -``` - -- [ ] **Step 4: Create src/query-client.ts** - -```typescript -import { QueryClient } from "@tanstack/react-query"; - -let clientQueryClient: QueryClient | undefined; - -export function getQueryClient(): QueryClient { - if (typeof window === "undefined") { - return new QueryClient({ - defaultOptions: { - queries: { staleTime: 30 * 1000 }, - }, - }); - } - if (!clientQueryClient) { - clientQueryClient = new QueryClient({ - defaultOptions: { - queries: { staleTime: 30 * 1000 }, - }, - }); - } - return clientQueryClient; -} -``` - -- [ ] **Step 5: Create src/provider.tsx** - -```tsx -"use client"; - -import { QueryClientProvider } from "@tanstack/react-query"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import type { AppRouter } from "@repo/api"; -import { TRPCProvider } from "./trpc.js"; -import { getQueryClient } from "./query-client.js"; - -export function ApiProvider({ - children, - trpcUrl, -}: { - children: React.ReactNode; - trpcUrl: string; -}) { - const queryClient = getQueryClient(); - const trpcClient = createTRPCClient({ - links: [httpBatchLink({ url: trpcUrl })], - }); - - return ( - - {children} - - ); -} -``` - -- [ ] **Step 6: Update src/index.ts** - -```typescript -export { ApiProvider } from "./provider.js"; -export { useTRPC } from "./trpc.js"; -export { getQueryClient } from "./query-client.js"; -``` - -- [ ] **Step 7: Run pnpm install and commit** - -Run: `pnpm install` - -```bash -git add packages/api-client/ pnpm-lock.yaml -git commit -m "feat(api-client): add tRPC React Query provider and shared hooks" -``` - ---- - -### Task 3: apps/web-next — Next.js 15 app shell - -**Files:** -- Modify: `apps/web-next/package.json` -- Create: `apps/web-next/next.config.mjs` -- Create: `apps/web-next/src/app/layout.tsx` -- Create: `apps/web-next/src/app/page.tsx` -- Create: `apps/web-next/src/app/api/trpc/[trpc]/route.ts` -- Modify: `apps/web-next/tsconfig.json` - -- [ ] **Step 1: Update apps/web-next/package.json** - -```json -{ - "name": "@repo/web-next", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "build": "next build", - "dev": "next dev --port 3000", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@repo/api": "workspace:*", - "@repo/api-client": "workspace:*", - "@repo/ui": "workspace:*", - "next": "^15.3.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0" - } -} -``` - -- [ ] **Step 2: Create next.config.mjs** - -```javascript -/** @type {import('next').NextConfig} */ -const nextConfig = { - transpilePackages: ["@repo/api", "@repo/api-client", "@repo/core", "@repo/ui"], -}; - -export default nextConfig; -``` - -- [ ] **Step 3: Create src/app/api/trpc/[trpc]/route.ts** - -```typescript -import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; -import { appRouter } from "@repo/api"; - -const handler = (req: Request) => - fetchRequestHandler({ - endpoint: "/api/trpc", - req, - router: appRouter, - createContext: () => ({}), - }); - -export { handler as GET, handler as POST }; -``` - -- [ ] **Step 4: Create src/app/layout.tsx** - -```tsx -import type { Metadata } from "next"; -import { Providers } from "./providers"; - -export const metadata: Metadata = { - title: "Template — Next.js", - description: "Clean Architecture Monorepo Template", -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - - {children} - - - ); -} -``` - -- [ ] **Step 5: Create src/app/providers.tsx** - -```tsx -"use client"; - -import { ApiProvider } from "@repo/api-client"; - -export function Providers({ children }: { children: React.ReactNode }) { - return {children}; -} -``` - -- [ ] **Step 6: Create src/app/page.tsx** - -```tsx -export default function Home() { - return ( -
-

Template — Next.js

-

Clean Architecture Monorepo Template

-
- ); -} -``` - -- [ ] **Step 7: Commit** - -```bash -git add apps/web-next/ pnpm-lock.yaml -git commit -m "feat(web-next): add Next.js 15 app shell with tRPC endpoint" -``` - ---- - -### Task 4: apps/web-tanstack — TanStack Start app shell - -**Files:** -- Modify: `apps/web-tanstack/package.json` -- Create: `apps/web-tanstack/vite.config.ts` -- Create: `apps/web-tanstack/src/router.tsx` -- Create: `apps/web-tanstack/src/routes/__root.tsx` -- Create: `apps/web-tanstack/src/routes/index.tsx` -- Modify: `apps/web-tanstack/tsconfig.json` - -- [ ] **Step 1: Update apps/web-tanstack/package.json** - -```json -{ - "name": "@repo/web-tanstack", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "build": "vite build", - "dev": "vite dev --port 3002", - "lint": "eslint .", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@repo/api": "workspace:*", - "@repo/api-client": "workspace:*", - "@repo/ui": "workspace:*", - "@tanstack/react-router": "^1.120.0", - "@tanstack/react-start": "^1.120.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "vite": "^6.3.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^4.4.0" - } -} -``` - -- [ ] **Step 2: Create vite.config.ts** - -```typescript -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], -}); -``` - -- [ ] **Step 3: Create src/routes/__root.tsx** - -```tsx -import { Outlet, createRootRoute } from "@tanstack/react-router"; -import { ApiProvider } from "@repo/api-client"; - -export const Route = createRootRoute({ - component: () => ( - - - - ), -}); -``` - -- [ ] **Step 4: Create src/routes/index.tsx** - -```tsx -import { createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/")({ - component: Home, -}); - -function Home() { - return ( -
-

Template — TanStack Start

-

Clean Architecture Monorepo Template

-
- ); -} -``` - -- [ ] **Step 5: Commit** - -```bash -git add apps/web-tanstack/ pnpm-lock.yaml -git commit -m "feat(web-tanstack): add TanStack Start app shell with tRPC client" -``` - ---- - -### Task 5: Install all dependencies and verify - -- [ ] **Step 1: Run pnpm install** - -Run: `pnpm install` -Expected: All dependencies resolve. - -- [ ] **Step 2: Run turbo build** - -Run: `pnpm build` -Expected: All packages build (apps may fail on `next build` / `vite build` without full setup — change to placeholder if needed). - -- [ ] **Step 3: Run core tests** - -Run: `cd packages/core && pnpm vitest run` -Expected: All 22 tests pass. - -- [ ] **Step 4: Commit any remaining fixes** - -```bash -git add -A -git commit -m "chore: finalize Plan 4 — API layer + app shells" -``` diff --git a/docs/superpowers/plans/2026-04-06-plan-5-ui-system.md b/docs/superpowers/plans/2026-04-06-plan-5-ui-system.md deleted file mode 100644 index 53c8944..0000000 --- a/docs/superpowers/plans/2026-04-06-plan-5-ui-system.md +++ /dev/null @@ -1,610 +0,0 @@ -# Plan 5: UI System — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Set up `@repo/ui` with Atomic Design folder structure, Tailwind CSS v4, shadcn/ui, base components (atoms + a molecule), and `apps/storybook` with Storybook 8. - -**Architecture:** `@repo/ui` uses Atomic Design (atoms/molecules/organisms/templates). Tailwind v4 uses CSS-first config (`@import "tailwindcss"` + `@theme`). shadcn/ui components land in `atoms/` by default. Storybook runs as a separate app pulling stories from the UI package via `@storybook/react-vite` with `@tailwindcss/vite` plugin. - -**Tech Stack:** Tailwind CSS v4, shadcn/ui, clsx, tailwind-merge, Storybook 8, @storybook/react-vite - ---- - -### Task 1: Set up @repo/ui with Tailwind v4 + Atomic Design structure - -**Files:** -- Modify: `packages/ui/package.json` -- Modify: `packages/ui/tsconfig.json` -- Create: `packages/ui/src/styles/globals.css` -- Create: `packages/ui/src/lib/utils.ts` -- Create: `packages/ui/src/atoms/index.ts` -- Create: `packages/ui/src/molecules/index.ts` -- Create: `packages/ui/src/organisms/index.ts` -- Create: `packages/ui/src/templates/index.ts` -- Modify: `packages/ui/src/index.ts` - -- [ ] **Step 1: Update packages/ui/package.json** - -```json -{ - "name": "@repo/ui", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "echo 'typechecked by consuming app bundler'", - "lint": "eslint .", - "test": "vitest run", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "clsx": "^2.1.0", - "tailwind-merge": "^3.0.0", - "react": "^19.0.0" - }, - "devDependencies": { - "@repo/eslint-config": "workspace:*", - "@repo/typescript-config": "workspace:*", - "@types/react": "^19.0.0", - "tailwindcss": "^4.1.0" - } -} -``` - -- [ ] **Step 2: Create src/styles/globals.css** - -Tailwind v4 CSS-first config — no tailwind.config.ts needed. - -```css -@import "tailwindcss"; - -@theme { - --color-background: hsl(0 0% 100%); - --color-foreground: hsl(240 10% 3.9%); - --color-card: hsl(0 0% 100%); - --color-card-foreground: hsl(240 10% 3.9%); - --color-popover: hsl(0 0% 100%); - --color-popover-foreground: hsl(240 10% 3.9%); - --color-primary: hsl(240 5.9% 10%); - --color-primary-foreground: hsl(0 0% 98%); - --color-secondary: hsl(240 4.8% 95.9%); - --color-secondary-foreground: hsl(240 5.9% 10%); - --color-muted: hsl(240 4.8% 95.9%); - --color-muted-foreground: hsl(240 3.8% 46.1%); - --color-accent: hsl(240 4.8% 95.9%); - --color-accent-foreground: hsl(240 5.9% 10%); - --color-destructive: hsl(0 84.2% 60.2%); - --color-destructive-foreground: hsl(0 0% 98%); - --color-border: hsl(240 5.9% 90%); - --color-input: hsl(240 5.9% 90%); - --color-ring: hsl(240 5.9% 10%); - --radius-sm: 0.25rem; - --radius-md: 0.375rem; - --radius-lg: 0.5rem; -} -``` - -- [ ] **Step 3: Create src/lib/utils.ts** - -```typescript -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} -``` - -- [ ] **Step 4: Create atomic design barrel files** - -```typescript -// packages/ui/src/atoms/index.ts -// Atom components are exported from here -export {}; -``` - -```typescript -// packages/ui/src/molecules/index.ts -// Molecule components are exported from here -export {}; -``` - -```typescript -// packages/ui/src/organisms/index.ts -// Organism components are exported from here -export {}; -``` - -```typescript -// packages/ui/src/templates/index.ts -// Template components are exported from here -export {}; -``` - -- [ ] **Step 5: Update src/index.ts** - -```typescript -export { cn } from "./lib/utils.js"; -export * from "./atoms/index.js"; -export * from "./molecules/index.js"; -export * from "./organisms/index.js"; -export * from "./templates/index.js"; -``` - -- [ ] **Step 6: Run pnpm install and commit** - -```bash -pnpm install -git add packages/ui/ pnpm-lock.yaml -git commit -m "feat(ui): set up Atomic Design structure with Tailwind v4" -``` - ---- - -### Task 2: Add Button atom - -**Files:** -- Create: `packages/ui/src/atoms/button/button.tsx` -- Create: `packages/ui/src/atoms/button/button.stories.tsx` -- Create: `packages/ui/src/atoms/button/index.ts` -- Modify: `packages/ui/src/atoms/index.ts` - -- [ ] **Step 1: Create button.tsx** - -```tsx -import { forwardRef, type ButtonHTMLAttributes } from "react"; -import { cn } from "../../lib/utils.js"; - -export interface ButtonProps extends ButtonHTMLAttributes { - variant?: "default" | "secondary" | "destructive" | "outline" | "ghost"; - size?: "sm" | "default" | "lg"; -} - -const variantStyles: Record, string> = { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - ghost: "hover:bg-accent hover:text-accent-foreground", -}; - -const sizeStyles: Record, string> = { - sm: "h-9 px-3 text-sm", - default: "h-10 px-4 py-2", - lg: "h-11 px-8 text-lg", -}; - -export const Button = forwardRef( - ({ className, variant = "default", size = "default", ...props }, ref) => { - return ( -