@horka/app-forge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +99 -0
  3. package/bin/cli.js +371 -0
  4. package/bin/cli.test.js +91 -0
  5. package/package.json +43 -0
  6. package/templates/core/CLAUDE.md +36 -0
  7. package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
  8. package/templates/core/claude/memory/COMMANDS.md +13 -0
  9. package/templates/core/claude/memory/DECISIONS.md +5 -0
  10. package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
  11. package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
  12. package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
  13. package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
  14. package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
  15. package/templates/core/claude/skills/save-context/SKILL.md +35 -0
  16. package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
  17. package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
  18. package/templates/core/docs-architecture/DELIVERY.md +68 -0
  19. package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
  20. package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
  21. package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
  22. package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
  23. package/templates/core/gitignore +15 -0
  24. package/templates/core/mcp.json +8 -0
  25. package/templates/packs/nuxt-web/CLAUDE.md +74 -0
  26. package/templates/packs/nuxt-web/app/app.vue +5 -0
  27. package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
  28. package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
  29. package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
  30. package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
  31. package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
  32. package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
  33. package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
  34. package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
  35. package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
  36. package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
  37. package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
  38. package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
  39. package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
  40. package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
  41. package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
  42. package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
  43. package/templates/packs/nuxt-web/gitignore +18 -0
  44. package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
  45. package/templates/packs/nuxt-web/pack.json +11 -0
  46. package/templates/packs/nuxt-web/package.json +31 -0
  47. package/templates/packs/nuxt-web/playwright.config.ts +39 -0
  48. package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
  49. package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
  50. package/templates/packs/nuxt-web/tsconfig.json +4 -0
  51. package/templates/packs/nuxt-web/vitest.config.ts +23 -0
  52. package/templates/packs/swift-ios/CLAUDE.md +64 -0
  53. package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
  54. package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
  55. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
  56. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
  57. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
  58. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
  59. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
  60. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
  61. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
  62. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
  63. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
  64. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
  65. package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
  66. package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
  67. package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
  68. package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
  69. package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
  70. package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
  71. package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
  72. package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
  73. package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
  74. package/templates/packs/swift-ios/gitignore +5 -0
  75. package/templates/packs/swift-ios/mcp.json +8 -0
  76. package/templates/packs/swift-ios/pack.json +11 -0
  77. package/templates/packs/swift-ios/project.yml +33 -0
  78. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
  79. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
  80. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
  81. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
  82. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
  83. package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
  84. package/templates/packs/ts-sdk/CLAUDE.md +72 -0
  85. package/templates/packs/ts-sdk/MIGRATION.md +28 -0
  86. package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
  87. package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
  88. package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
  89. package/templates/packs/ts-sdk/gitignore +6 -0
  90. package/templates/packs/ts-sdk/pack.json +11 -0
  91. package/templates/packs/ts-sdk/package.json +55 -0
  92. package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
  93. package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
  94. package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
  95. package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
  96. package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
  97. package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
  98. package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
  99. package/templates/packs/ts-sdk/src/index.ts +62 -0
  100. package/templates/packs/ts-sdk/src/types/index.ts +33 -0
  101. package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
  102. package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
  103. package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
  104. package/templates/packs/ts-sdk/tsconfig.json +15 -0
  105. package/templates/packs/ts-sdk/tsup.config.ts +22 -0
  106. package/templates/packs/ts-sdk/vitest.config.ts +8 -0
  107. package/templates/packs/vapor-api/CLAUDE.md +73 -0
  108. package/templates/packs/vapor-api/Dockerfile +80 -0
  109. package/templates/packs/vapor-api/Package.swift +68 -0
  110. package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
  111. package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
  112. package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
  113. package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
  114. package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
  115. package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
  116. package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
  117. package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
  118. package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
  119. package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
  120. package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
  121. package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
  122. package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
  123. package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
  124. package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
  125. package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
  126. package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
  127. package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
  128. package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
  129. package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
  130. package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
  131. package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
  132. package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
  133. package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
  134. package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
  135. package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
  136. package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
  137. package/templates/packs/vapor-api/env_dist +29 -0
  138. package/templates/packs/vapor-api/gitignore +7 -0
  139. package/templates/packs/vapor-api/pack.json +11 -0
  140. package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
  141. package/templates/packs/vapor-api/scripts/validate-env-vars.sh +72 -0
@@ -0,0 +1,118 @@
1
+ # SEO & ROUTING — mixed public/private apps on SSR
2
+
3
+ The default product shape this pack assumes: public marketing pages (home, pricing, features,
4
+ changelog…) and a private app behind auth — in ONE Nuxt codebase. This file is the recipe
5
+ that keeps both worlds correct.
6
+
7
+ ## 1. SSR is on and stays on
8
+
9
+ `ssr: true` is the pack default. Public pages must deliver their content **in the server
10
+ response** — crawlers and link previews don't run your client fetches reliably, and users
11
+ shouldn't watch spinners for static copy.
12
+
13
+ Proof command (run it on every public page before calling it done):
14
+
15
+ ```bash
16
+ curl -s http://localhost:3000/pricing | grep -i "your headline text"
17
+ ```
18
+
19
+ If the content isn't in the HTML, the page is fetching client-side — fix the data fetching
20
+ (CONVENTIONS.md §4: `useAsyncData` in setup, `onMounted` only with a `CLIENT-ONLY:` rationale).
21
+
22
+ ## 2. Auth policy lives ON the route — default deny
23
+
24
+ Public pages declare their own policy, next to their own code:
25
+
26
+ ```ts
27
+ // app/pages/pricing.vue
28
+ definePageMeta({ auth: false })
29
+ ```
30
+
31
+ The global middleware reads route meta and **default-denies** everything else:
32
+
33
+ ```ts
34
+ // app/middleware/auth.global.ts — add the day auth lands; the meta convention starts day one
35
+ export default defineNuxtRouteMiddleware((to) => {
36
+ if (to.meta.auth === false) return // page opted out, explicitly
37
+ const { isAuthenticated } = useSession() // your auth provider's session check
38
+ if (!isAuthenticated.value) {
39
+ return navigateTo({ path: '/', query: { redirect: to.fullPath } })
40
+ }
41
+ })
42
+ ```
43
+
44
+ Absolute rules:
45
+ - **Never a name allowlist** (`['index', 'pricing', …]`) — it's a second route table that
46
+ silently drifts from the real one (ANTI_PATTERNS.md #2).
47
+ - New pages are **private by default**; making one public is a one-line, reviewable, greppable
48
+ diff on the page itself: `grep -rn "auth: false" app/pages` lists the public surface.
49
+ - The redirect target preserves `redirect=` so login returns the user where they were headed.
50
+
51
+ > 📖 **War story:** a production team used a hand-maintained array of public page names in the
52
+ > auth middleware. A public changelog page shipped but was never added to the list — anonymous
53
+ > visitors (and crawlers) were silently 302'd to the homepage **for weeks**. No error, no log,
54
+ > no failing test; the sitemap even advertised the URL. Route-meta + default-deny makes that
55
+ > failure impossible: the page carries its own policy, and forgetting it fails *closed* (a
56
+ > private page stays private) instead of breaking the public site invisibly.
57
+
58
+ ## 3. Sitemap & robots — explicit allowlist, never the route table
59
+
60
+ - The sitemap lists **only an explicit array of public URLs**, maintained next to the sitemap
61
+ config. Never auto-generate from the route table — your route table contains the private app.
62
+ With `@nuxtjs/sitemap`: `excludeAppSources: true` + an explicit `urls` list.
63
+ - Adding a public page = page file + `auth: false` + sitemap entry + i18n module. Put this
64
+ 4-item checklist in the PR template.
65
+ - Private sections also send `X-Robots-Tag` via route rules (belt and braces with the auth wall):
66
+
67
+ ```ts
68
+ routeRules: {
69
+ '/': { prerender: true }, // pure-static marketing
70
+ '/pricing': { prerender: true },
71
+ '/changelog': { swr: 3600 }, // public but updated often
72
+ '/app/**': { headers: { 'X-Robots-Tag': 'noindex' } } // private app surface
73
+ },
74
+ ```
75
+
76
+ - **Staging must never be indexed**: gate a global `robots: noindex, nofollow` meta on the
77
+ environment from runtime config — not on `NODE_ENV`, which is `production` on staging builds.
78
+
79
+ ## 4. Per-page meta
80
+
81
+ Every public page owns its meta in setup — no global defaults pretending to be content:
82
+
83
+ ```ts
84
+ useSeoMeta({
85
+ title: t('pricing.meta.title'),
86
+ description: t('pricing.meta.description'),
87
+ ogTitle: t('pricing.meta.title'),
88
+ ogImage: `${config.public.baseUrl}/og/pricing.png`, // ABSOLUTE URL — relative og:image is ignored
89
+ twitterCard: 'summary_large_image',
90
+ })
91
+ ```
92
+
93
+ - `og:image` and canonical URLs must be **absolute**, built from `runtimeConfig.public.baseUrl`
94
+ — never hardcoded hostnames (they rot across environments).
95
+ - One `<h1>` per page; headings follow document order — crawlers and screen readers share it.
96
+ - Dynamic public pages (blog posts…) set meta from the same `useAsyncData` payload that renders
97
+ the content, so meta and body can't diverge.
98
+
99
+ ## 5. Routing hygiene
100
+
101
+ - File-based routes only; no `hashMode`. Deep links into the private app must survive the
102
+ login round-trip (the `redirect=` query above).
103
+ - Route params are validated in the page (`validate`) — a malformed id is a 404, not a blank
104
+ page with a console error.
105
+ - Error states route through `error.vue` with correct status codes:
106
+ `throw createError({ statusCode: 404, statusMessage: 'Not found' })` during SSR returns a
107
+ real 404 to crawlers — a soft-404 (200 + "not found" text) poisons indexing.
108
+ - i18n URL strategy: app-only products may hide locale (`no_prefix`); indexable multilingual
109
+ marketing needs per-locale URLs + `hreflang` (I18N.md §5). Record the choice in DECISIONS.md.
110
+
111
+ ## 6. Pre-ship checklist (public pages)
112
+
113
+ - [ ] `definePageMeta({ auth: false })` present
114
+ - [ ] Content visible in `curl` output (SSR-rendered, no client fetch for primary content)
115
+ - [ ] `useSeoMeta` with title/description/absolute ogImage
116
+ - [ ] Sitemap entry added; staging still noindex
117
+ - [ ] i18n module exists in every locale (parity check green)
118
+ - [ ] Status codes correct for the page's error paths (404 via `createError`, not a soft-404)
@@ -0,0 +1,18 @@
1
+ # Nuxt
2
+ .nuxt/
3
+ .output/
4
+ dist/
5
+ node_modules/
6
+
7
+ # env — never commit real env files
8
+ .env
9
+ .env.*
10
+
11
+ # test artifacts — generated reports are claims, not proof (ANTI_PATTERNS.md #10)
12
+ coverage/
13
+ playwright-report/
14
+ test-results/
15
+
16
+ # misc
17
+ *.log
18
+ .cache/
@@ -0,0 +1,49 @@
1
+ // Layer mapping and the reasoning behind every block: docs-architecture/ARCHITECTURE.md
2
+ export default defineNuxtConfig({
3
+ compatibilityDate: '2025-07-15',
4
+
5
+ // SSR is ON and stays on. Public pages must render their content server-side;
6
+ // data fetching happens in setup (useAsyncData), never in onMounted without a
7
+ // written CLIENT-ONLY rationale. See docs-architecture/SEO_AND_ROUTING.md §1.
8
+ ssr: true,
9
+
10
+ devtools: { enabled: true },
11
+
12
+ css: [
13
+ '~/assets/css/tokens.css', // L0 — the ONLY place visual values are defined
14
+ '~/assets/css/main.css',
15
+ ],
16
+
17
+ // Module auto-registration: DS modules (L3, domain-blind) and feature modules (L4)
18
+ // expose their components/ subfolder. Adding a module requires zero config here.
19
+ components: [
20
+ // NOTE: glob goes in `pattern`, not `path` — a glob inside `path` is silently
21
+ // ignored by the scanner (zero components registered, no warning).
22
+ { path: '~/designSystem', pattern: '**/components/**', pathPrefix: false },
23
+ { path: '~/features', pattern: '**/components/**', pathPrefix: false },
24
+ ],
25
+
26
+ imports: {
27
+ dirs: [
28
+ '~/designSystem/**/composables',
29
+ '~/features/**/composables',
30
+ ],
31
+ },
32
+
33
+ runtimeConfig: {
34
+ // Server-only values (override at runtime: NUXT_<KEY>). Secrets live HERE.
35
+ public: {
36
+ // Client-visible values (override at runtime: NUXT_PUBLIC_<KEY>) — never secrets.
37
+ apiBaseUrl: '',
38
+ baseUrl: 'http://localhost:3000',
39
+ environment: 'development',
40
+ },
41
+ },
42
+
43
+ app: {
44
+ head: {
45
+ htmlAttrs: { lang: 'en' },
46
+ title: '{{PROJECT_NAME}}',
47
+ },
48
+ },
49
+ })
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "nuxt-web",
3
+ "label": "Web app — Nuxt 4 / Vue 3 / TypeScript",
4
+ "languages": ["typescript", "vue"],
5
+ "idPrompt": "App identifier (reverse-DNS)",
6
+ "requirements": [
7
+ "Node.js 20+ (LTS) and npm",
8
+ "Playwright browsers for e2e (one-time): npx playwright install"
9
+ ],
10
+ "notes": "Skeleton builds and tests day one: npm install && npm run test && npm run build"
11
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "{{BUNDLE_ID}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=20"
7
+ },
8
+ "scripts": {
9
+ "dev": "nuxt dev",
10
+ "build": "nuxt build",
11
+ "generate": "nuxt generate",
12
+ "preview": "nuxt preview",
13
+ "postinstall": "nuxt prepare",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:e2e": "playwright test"
17
+ },
18
+ "dependencies": {
19
+ "nuxt": "^4.0.0",
20
+ "vue": "^3.5.0",
21
+ "vue-router": "^4.5.0"
22
+ },
23
+ "devDependencies": {
24
+ "@playwright/test": "^1.49.0",
25
+ "@vitejs/plugin-vue": "^5.2.0",
26
+ "@vue/test-utils": "^2.4.6",
27
+ "happy-dom": "^20.0.0",
28
+ "typescript": "^5.6.0",
29
+ "vitest": "^3.0.0"
30
+ }
31
+ }
@@ -0,0 +1,39 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+
3
+ // E2E suite — real dependency, real config, runs from a clean checkout
4
+ // (one-time per machine: `npx playwright install`). Specs live in tests/e2e ONLY;
5
+ // vitest owns app/** and tests/unit/**. Reports are gitignored — a committed
6
+ // report is a claim, a green run is proof (ANTI_PATTERNS.md #10).
7
+
8
+ // One source of truth for the e2e origin, so `use.baseURL` and `webServer.url` can never
9
+ // drift apart. Override with PLAYWRIGHT_BASE_URL to point at a deployed preview.
10
+ //
11
+ // ⚠️ Port-collision trap: locally `reuseExistingServer` is true, so if ANYTHING is already
12
+ // listening on this port (a stale `nuxt dev`, another app, a previous crashed run) Playwright
13
+ // silently tests THAT process instead of a fresh build — a false green or false red that has
14
+ // nothing to do with your code. Port 3000 is heavily contended; this skeleton uses a less
15
+ // common 4173. If a run looks wrong, kill stragglers first: `lsof -ti:4173 | xargs kill`,
16
+ // or set PLAYWRIGHT_BASE_URL to a clean port. In CI, reuseExistingServer is false (always fresh).
17
+ const PORT = Number(process.env.PLAYWRIGHT_PORT ?? 4173)
18
+ const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${PORT}`
19
+
20
+ export default defineConfig({
21
+ testDir: 'tests/e2e',
22
+ fullyParallel: true,
23
+ forbidOnly: !!process.env.CI,
24
+ retries: process.env.CI ? 2 : 0,
25
+ reporter: [['html', { open: 'never' }]],
26
+ use: {
27
+ baseURL: BASE_URL,
28
+ trace: 'on-first-retry',
29
+ },
30
+ projects: [
31
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
32
+ ],
33
+ webServer: {
34
+ command: `npm run dev -- --port ${PORT}`,
35
+ url: BASE_URL,
36
+ reuseExistingServer: !process.env.CI,
37
+ timeout: 120_000,
38
+ },
39
+ })
@@ -0,0 +1,7 @@
1
+ // The SINGLE health endpoint — Docker HEALTHCHECK, orchestrator checks, deploy
2
+ // gates and uptime monitoring all point HERE and nowhere else (OPS_WEB.md §7).
3
+ export default defineEventHandler(() => ({
4
+ status: 'ok',
5
+ service: '{{BUNDLE_ID}}',
6
+ time: new Date().toISOString(),
7
+ }))
@@ -0,0 +1,19 @@
1
+ import { expect, test } from '@playwright/test'
2
+
3
+ // Example e2e spec — proves the wiring is real (server boots, page SSRs, DS button reacts).
4
+ // One-time per machine: `npx playwright install`. Then: `npm run test:e2e`.
5
+ test('home page renders server-side and the DS button responds', async ({ page }) => {
6
+ await page.goto('/')
7
+ await expect(page.getByRole('heading', { level: 1 })).toContainText('{{PROJECT_NAME}}')
8
+
9
+ const button = page.getByRole('button', { name: /clicked/i })
10
+ await expect(button).toContainText('0')
11
+ await button.click()
12
+ await expect(button).toContainText('1')
13
+ })
14
+
15
+ test('health endpoint answers', async ({ request }) => {
16
+ const response = await request.get('/api/health')
17
+ expect(response.ok()).toBeTruthy()
18
+ expect((await response.json()).status).toBe('ok')
19
+ })
@@ -0,0 +1,4 @@
1
+ {
2
+ // https://nuxt.com/docs/guide/concepts/typescript
3
+ "extends": "./.nuxt/tsconfig.json"
4
+ }
@@ -0,0 +1,23 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import { defineConfig } from 'vitest/config'
4
+
5
+ // Unit tests run WITHOUT booting Nuxt: pure domain code and DS components mount
6
+ // standalone. Keep it that way — this is the fast feedback loop (ARCHITECTURE.md §7).
7
+ export default defineConfig({
8
+ plugins: [vue()],
9
+ resolve: {
10
+ alias: {
11
+ '~': fileURLToPath(new URL('./app', import.meta.url)),
12
+ '@': fileURLToPath(new URL('./app', import.meta.url)),
13
+ },
14
+ },
15
+ test: {
16
+ environment: 'happy-dom',
17
+ // tests/e2e belongs to Playwright — vitest must never pick it up
18
+ include: [
19
+ 'app/**/*.{test,spec}.ts',
20
+ 'tests/unit/**/*.{test,spec}.ts',
21
+ ],
22
+ },
23
+ })
@@ -0,0 +1,64 @@
1
+ # {{PROJECT_NAME}} — Claude Code Operating Manual
2
+
3
+ This project was scaffolded by **AppForge** (pack: iOS/Swift): a Claude-Code-first
4
+ architecture extracted from production apps. You (Claude) are the team lead AND the
5
+ primary developer. Follow this manual exactly — it encodes hard-won lessons, not preferences.
6
+
7
+ ## Identity
8
+ - App: **{{PROJECT_NAME}}** · Bundle id: `{{BUNDLE_ID}}` · iOS 26+ · Swift 6.2 (strict concurrency)
9
+ - Backend: CloudKit-only by default (container `iCloud.{{BUNDLE_ID}}`) — adapt if the PRD says otherwise.
10
+
11
+ ## Session protocol (MANDATORY)
12
+ 1. **Session start**: run the `restore-context` skill — read `.claude/memory/*.md` before doing anything. Never invent project facts.
13
+ 2. **Empty project / new idea**: run the `kickoff` skill — it interviews the user, writes the PRD, plans slices, then builds autonomously.
14
+ 3. **After significant work**: update `.claude/memory/PROJECT_STATE.md` (and DECISIONS/NEXT_STEPS when relevant) — `save-context` skill.
15
+
16
+ ## Architecture (read the docs before coding)
17
+ The knowledge base lives in `docs-architecture/`. Read the relevant doc BEFORE touching that area:
18
+
19
+ | You are about to… | Read first |
20
+ |---|---|
21
+ | understand the layer model (stack-agnostic) | `ARCHITECTURE_PRINCIPLES.md` |
22
+ | plan/deliver slices, validate, update memory | `DELIVERY.md` |
23
+ | product spans repos (API + SDK + clients) | `MULTI_REPO_CONTRACT.md` |
24
+ | ship/consume a typed SDK | `SDK_CONTRACT.md` |
25
+ | accept user-supplied URLs (webhooks…) | `SECURITY_USER_URLS.md` |
26
+ | write any documentation | `DOCS_PLACEMENT.md` |
27
+ | add caching/feature flags/auth shortcuts | `ANTI_PATTERNS.md` |
28
+ | add/move any file, create a feature | `ARCHITECTURE.md` |
29
+ | write any Swift code | `CONVENTIONS.md` |
30
+ | add a screen, sheet, deeplink, push routing | `NAVIGATION.md` |
31
+ | touch CloudKit, CKShare, sync, subscriptions | `CLOUDKIT_GUIDE.md` |
32
+ | style anything (colors, fonts, spacing) | `DESIGN_SYSTEM.md` |
33
+ | write or modify domain logic | `TESTING.md` |
34
+ | build, run, validate, debug on device | `WORKFLOW.md` |
35
+
36
+ Layer summary (universal contract in ARCHITECTURE_PRINCIPLES.md, Swift mapping in ARCHITECTURE.md):
37
+ L0 `{{PROJECT_NAME}}DS` tokens · L1 Ops (create when needed) · L2 `DataLayer` (implements L3 contracts) ·
38
+ L3 `{{PROJECT_NAME}}Core` (pure domain — **never imports SwiftUI**) + DS `Components/` (Core UI) ·
39
+ L4 app `Module/` (shared feature bricks) · L5 app `App/` + `Store/` + `Tools/`. Imports point downward only.
40
+
41
+ ## Non-negotiable rules
42
+ - **Packages first**: `swift build`/`swift test --package-path Packages/<X>` before any `xcodebuild`. Fast, precise errors.
43
+ - **Never claim done without proof**: package tests green + app build green + (for UI) a simulator screenshot you actually looked at.
44
+ - **Design tokens only**: no hardcoded colors/fonts/spacing in app code — everything through `{{PROJECT_NAME}}DS`.
45
+ - **Pure domain logic**: engines are `nonisolated enum`s with injected `Calendar`/dates. Every rule ships with a test.
46
+ - **Logging**: `os.Logger` with `privacy: .public` interpolation (print() is invisible on device).
47
+ - **Memory is law**: contradictions between memory files and code → code wins, then fix the memory file.
48
+
49
+ ## Build commands
50
+ ```bash
51
+ # per-package loop (seconds)
52
+ swift build --package-path Packages/{{PROJECT_NAME}}Core
53
+ swift test --package-path Packages/{{PROJECT_NAME}}Core
54
+
55
+ # app target (after xcodegen generate, only when packages are green)
56
+ xcodebuild -project {{PROJECT_NAME}}.xcodeproj -scheme {{PROJECT_NAME}} \
57
+ -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
58
+ -derivedDataPath /tmp/{{PROJECT_NAME}}_dd build 2>&1 | grep -E "error:|BUILD"
59
+ ```
60
+ Simulator validation: use the `ios-simulator` MCP (install_app → launch_app → screenshot → look at it).
61
+
62
+ ## Git
63
+ - Never push without explicit user approval. Feature branches; commit format `add/update/fix(scope) - description`.
64
+ - No AI attribution in commits or file headers.
@@ -0,0 +1,21 @@
1
+ // swift-tools-version: 6.2
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "DataLayer",
6
+ platforms: [.iOS(.v26), .macOS(.v15)],
7
+ products: [.library(name: "DataLayer", targets: ["DataLayer"])],
8
+ dependencies: [
9
+ .package(path: "../{{PROJECT_NAME}}Core"),
10
+ ],
11
+ targets: [
12
+ // REAL repository implementations only (CloudKit, URLSession, …). Protocols AND their
13
+ // InMemory variant (tests/previews/offline) live in Core — DataLayer ships IO-backed
14
+ // impls of those contracts. See docs-architecture/ARCHITECTURE.md §2.
15
+ .target(
16
+ name: "DataLayer",
17
+ dependencies: [.product(name: "{{PROJECT_NAME}}Core", package: "{{PROJECT_NAME}}Core")],
18
+ swiftSettings: [.swiftLanguageMode(.v6)]
19
+ ),
20
+ ]
21
+ )
@@ -0,0 +1,11 @@
1
+ import {{PROJECT_NAME}}Core
2
+
3
+ /// PERMANENT file — do not delete (an SPM target with zero sources does not build).
4
+ /// DataLayer hosts the REAL repository implementations (CloudKit, URLSession, database…),
5
+ /// each conforming to a contract declared in {{PROJECT_NAME}}Core (ports & adapters).
6
+ /// Real implementations are added by slices that need them; until then this marker keeps
7
+ /// the target alive. See docs-architecture/CLOUDKIT_GUIDE.md before writing a CloudKit impl.
8
+ public enum DataLayer {
9
+ /// Bump when a real implementation lands, so the placeholder's job is visible in reviews.
10
+ public static let implementations: [String] = []
11
+ }
@@ -0,0 +1,20 @@
1
+ // swift-tools-version: 6.2
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "{{PROJECT_NAME}}Core",
6
+ platforms: [.iOS(.v26), .macOS(.v15)],
7
+ products: [.library(name: "{{PROJECT_NAME}}Core", targets: ["{{PROJECT_NAME}}Core"])],
8
+ targets: [
9
+ // Pure domain logic. NEVER imports SwiftUI/UIKit. Fully testable with `swift test`.
10
+ .target(
11
+ name: "{{PROJECT_NAME}}Core",
12
+ swiftSettings: [.swiftLanguageMode(.v6)]
13
+ ),
14
+ .testTarget(
15
+ name: "{{PROJECT_NAME}}CoreTests",
16
+ dependencies: ["{{PROJECT_NAME}}Core"],
17
+ swiftSettings: [.swiftLanguageMode(.v6)]
18
+ ),
19
+ ]
20
+ )
@@ -0,0 +1,15 @@
1
+ import Foundation
2
+
3
+ /// Sample domain entity — replace at kickoff. Shows the conventions:
4
+ /// value type, Identifiable + Sendable, explicit dates injected (never read the clock here).
5
+ public struct SampleItem: Identifiable, Hashable, Sendable {
6
+ public let id: UUID
7
+ public var title: String
8
+ public var createdAt: Date
9
+
10
+ public init(id: UUID = UUID(), title: String, createdAt: Date) {
11
+ self.id = id
12
+ self.title = title
13
+ self.createdAt = createdAt
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ import Foundation
2
+
3
+ /// Sample pure engine — replace at kickoff. Conventions on display:
4
+ /// `enum` (no instances), nonisolated pure functions, `Calendar` injected for determinism.
5
+ public enum SampleEngine {
6
+ /// Items created on the same calendar day as `reference`.
7
+ public static func items(
8
+ _ items: [SampleItem],
9
+ onSameDayAs reference: Date,
10
+ calendar: Calendar = .current
11
+ ) -> [SampleItem] {
12
+ items.filter { calendar.isDate($0.createdAt, inSameDayAs: reference) }
13
+ }
14
+ }
@@ -0,0 +1,27 @@
1
+ import Foundation
2
+
3
+ /// Sample repository pair — replace at kickoff. The pattern (see docs-architecture/ARCHITECTURE.md §2):
4
+ /// the CONTRACT and the InMemory reference implementation live HERE in Core (testable without IO
5
+ /// frameworks); the real backend implementation (CloudKit/network) lives in DataLayer and imports
6
+ /// this protocol — the one sanctioned upward arrow (ports & adapters).
7
+ public protocol SampleItemRepository: Sendable {
8
+ func loadItems() async throws -> [SampleItem]
9
+ func save(_ item: SampleItem) async throws
10
+ }
11
+
12
+ /// Backs tests, previews and offline-first development.
13
+ public actor InMemorySampleItemRepository: SampleItemRepository {
14
+ private var items: [SampleItem] = []
15
+
16
+ public init() {}
17
+
18
+ public func loadItems() async throws -> [SampleItem] { items }
19
+
20
+ public func save(_ item: SampleItem) async throws {
21
+ if let index = items.firstIndex(where: { $0.id == item.id }) {
22
+ items[index] = item // update in place — keeps stable order
23
+ } else {
24
+ items.append(item)
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,32 @@
1
+ import Foundation
2
+ import Testing
3
+ @testable import {{PROJECT_NAME}}Core
4
+
5
+ /// Swift Testing (@Test/#expect), deterministic dates via a Fixture helper. This is the pattern
6
+ /// every engine test follows. Delete alongside SampleEngine at kickoff.
7
+ @Suite struct SampleEngineTests {
8
+ /// Fixed Gregorian/UTC calendar — never `Calendar.current` (its timezone moves day boundaries
9
+ /// between machines/CI and makes hour/day-boundary tests flaky). Inject it everywhere.
10
+ private static let calendar: Calendar = {
11
+ var calendar = Calendar(identifier: .gregorian)
12
+ calendar.timeZone = TimeZone(identifier: "UTC")!
13
+ return calendar
14
+ }()
15
+
16
+ private static func date(_ year: Int, _ month: Int, _ day: Int) -> Date {
17
+ var components = DateComponents()
18
+ (components.year, components.month, components.day) = (year, month, day)
19
+ return calendar.date(from: components)!
20
+ }
21
+
22
+ @Test func sameDayFilterKeepsOnlyMatchingItems() {
23
+ let monday = Self.date(2026, 6, 1)
24
+ let tuesday = Self.date(2026, 6, 2)
25
+ let items = [
26
+ SampleItem(title: "a", createdAt: monday),
27
+ SampleItem(title: "b", createdAt: tuesday),
28
+ ]
29
+ let result = SampleEngine.items(items, onSameDayAs: monday, calendar: Self.calendar)
30
+ #expect(result.map(\.title) == ["a"])
31
+ }
32
+ }
@@ -0,0 +1,17 @@
1
+ // swift-tools-version: 6.2
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "{{PROJECT_NAME}}DS",
6
+ platforms: [.iOS(.v26), .macOS(.v15)],
7
+ products: [.library(name: "{{PROJECT_NAME}}DS", targets: ["{{PROJECT_NAME}}DS"])],
8
+ targets: [
9
+ .target(
10
+ name: "{{PROJECT_NAME}}DS",
11
+ swiftSettings: [
12
+ .swiftLanguageMode(.v6),
13
+ .defaultIsolation(MainActor.self),
14
+ ]
15
+ ),
16
+ ]
17
+ )
@@ -0,0 +1,18 @@
1
+ import SwiftUI
2
+
3
+ /// Semantic palette. Dark-first. Rename/extend at kickoff — never bypass with raw values.
4
+ public extension Color {
5
+ enum DS {
6
+ public static let accent = Color(red: 0.35, green: 0.45, blue: 1.00)
7
+ public static let accentSecondary = Color(red: 0.55, green: 0.30, blue: 0.95)
8
+ public static let background = Color(red: 0.039, green: 0.039, blue: 0.059)
9
+ public static let surface = Color.white.opacity(0.06)
10
+ public static let surfaceStrong = Color.white.opacity(0.10)
11
+ public static let stroke = Color.white.opacity(0.12)
12
+ public static let textPrimary = Color.white
13
+ public static let textSecondary = Color.white.opacity(0.6)
14
+ public static let textTertiary = Color.white.opacity(0.4)
15
+ public static let success = Color(red: 0.20, green: 0.85, blue: 0.45)
16
+ public static let danger = Color(red: 1.00, green: 0.27, blue: 0.36)
17
+ }
18
+ }
@@ -0,0 +1,22 @@
1
+ import SwiftUI
2
+
3
+ /// Starter Core-UI component (L3 — domain-blind). The Components/ folder grows at kickoff;
4
+ /// every component here uses tokens only and knows nothing about the domain.
5
+ public struct DSCard<Content: View>: View {
6
+ private let content: Content
7
+
8
+ public init(@ViewBuilder content: () -> Content) {
9
+ self.content = content()
10
+ }
11
+
12
+ public var body: some View {
13
+ content
14
+ .padding(DS.Padding.m)
15
+ .frame(maxWidth: .infinity, alignment: .leading)
16
+ .background(Color.DS.surface, in: RoundedRectangle(cornerRadius: DS.Radius.m))
17
+ .overlay(
18
+ RoundedRectangle(cornerRadius: DS.Radius.m)
19
+ .stroke(Color.DS.stroke, lineWidth: DS.Size.hairline)
20
+ )
21
+ }
22
+ }
@@ -0,0 +1,36 @@
1
+ import SwiftUI
2
+
3
+ /// Design system namespace. ALL visual tokens live here — app code never hardcodes values.
4
+ public enum DS {
5
+ /// Responsive scaling hook (1.0 for now; derive from screen size later if needed).
6
+ public static let ratio: CGFloat = 1.0
7
+
8
+ public enum Padding {
9
+ public static let xs: CGFloat = 4 * DS.ratio
10
+ public static let s: CGFloat = 8 * DS.ratio
11
+ public static let m: CGFloat = 16 * DS.ratio
12
+ public static let l: CGFloat = 24 * DS.ratio
13
+ public static let xl: CGFloat = 40 * DS.ratio
14
+ }
15
+
16
+ public enum Radius {
17
+ public static let s: CGFloat = 10 * DS.ratio
18
+ public static let m: CGFloat = 16 * DS.ratio
19
+ public static let l: CGFloat = 24 * DS.ratio
20
+ public static let pill: CGFloat = 999
21
+ }
22
+
23
+ public enum Size {
24
+ public static let hairline: CGFloat = 1
25
+ public static let icon: CGFloat = 24 * DS.ratio
26
+ }
27
+
28
+ /// Signature gradients — customize at kickoff to match the app's brand.
29
+ public enum Gradients {
30
+ public static let brand = LinearGradient(
31
+ colors: [Color.DS.accent, Color.DS.accentSecondary],
32
+ startPoint: .topLeading,
33
+ endPoint: .bottomTrailing
34
+ )
35
+ }
36
+ }