create-ailk 0.7.0 → 0.8.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 (169) hide show
  1. package/README.md +44 -10
  2. package/component-catalog.json +520 -5
  3. package/component-catalog.md +9 -6
  4. package/dist/cli.js +22 -24
  5. package/dist/lib/extract-module-bundle.js +5 -1
  6. package/dist/lib/module-license-gate.d.ts +79 -0
  7. package/dist/lib/module-license-gate.js +160 -12
  8. package/dist/lib/module-tier.d.ts +45 -16
  9. package/dist/lib/module-tier.js +76 -0
  10. package/dist/lib/pro-callout.js +10 -2
  11. package/dist/parse-args.d.ts +78 -10
  12. package/dist/parse-args.js +204 -14
  13. package/dist/programmatic.d.ts +39 -9
  14. package/dist/programmatic.js +163 -25
  15. package/dist/site-roster.js +11 -6
  16. package/dist/starter-grid.d.ts +216 -0
  17. package/dist/starter-grid.js +449 -0
  18. package/dist/surfaces.d.ts +4 -4
  19. package/dist/surfaces.js +102 -43
  20. package/dist/template-manifest.d.ts +36 -17
  21. package/dist/template-manifest.js +121 -367
  22. package/dist/template-names.d.ts +14 -12
  23. package/dist/template-names.js +13 -17
  24. package/package.json +3 -3
  25. package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
  26. package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
  27. package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
  28. package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
  29. package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
  30. package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
  31. package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
  32. package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
  33. package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
  34. package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
  35. package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
  36. package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
  37. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
  38. package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
  39. package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
  40. package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
  41. package/site-templates/designs/standard/DESIGN.md +5 -0
  42. package/site-templates/marketing-site/DESIGN.md +5 -0
  43. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
  44. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
  45. package/site-templates/marketing-site-free/DESIGN.md +5 -0
  46. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
  47. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
  48. package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
  49. package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
  50. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
  51. package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
  52. package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  53. package/site-templates/saas-product/DESIGN.md +5 -0
  54. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
  55. package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
  56. package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
  57. package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
  58. package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
  59. package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
  60. package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  61. package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
  62. package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
  63. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
  64. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
  65. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
  66. package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
  67. package/site-templates/shapes/local-business-service/provenance.json +4 -0
  68. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
  69. package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
  70. package/site-templates/shapes/organization-service/ailk-template.json +10 -0
  71. package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
  72. package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
  73. package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  74. package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
  75. package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  76. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
  77. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
  78. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
  79. package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
  80. package/site-templates/shapes/organization-service/provenance.json +4 -0
  81. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
  82. package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
  83. package/site-templates/shapes/person-service/ailk-template.json +10 -0
  84. package/site-templates/shapes/person-service/content/_site.mdx +85 -0
  85. package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
  86. package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
  87. package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
  88. package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
  89. package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
  90. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
  91. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
  92. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
  93. package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
  94. package/site-templates/shapes/person-service/provenance.json +4 -0
  95. package/templates/.claude/skills/README.md +3 -2
  96. package/templates/.claude/skills/add-page/references/page-types.md +4 -0
  97. package/templates/.claude/skills/setup-database/SKILL.md +156 -0
  98. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
  99. package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
  100. package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
  101. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
  102. package/templates/DESIGN.md +5 -0
  103. package/templates/README.md +46 -38
  104. package/templates/apps/api/CLAUDE.md +3 -2
  105. package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
  106. package/templates/apps/api/src/config/index.ts +35 -16
  107. package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
  108. package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
  109. package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
  110. package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
  111. package/templates/apps/api/src/openapi/spec.ts +63 -12
  112. package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
  113. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
  114. package/templates/apps/api/src/routes/leads/index.ts +46 -1
  115. package/templates/apps/api/src/server.ts +7 -0
  116. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
  117. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
  118. package/templates/apps/api/src/services/lead-routing.ts +140 -30
  119. package/templates/apps/api/src/services/leads-inbox.ts +48 -11
  120. package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
  121. package/templates/apps/mcp/src/tools/create_project.ts +21 -6
  122. package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
  123. package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
  124. package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
  125. package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
  126. package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
  127. package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
  128. package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
  129. package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
  130. package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
  131. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
  132. package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
  133. package/templates/apps/web/app/[locale]/layout.tsx +37 -5
  134. package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
  135. package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
  136. package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
  137. package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
  138. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
  139. package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
  140. package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
  141. package/templates/apps/web/lib/nav-config.ts +48 -36
  142. package/templates/apps/web/lib/site-identity.ts +14 -0
  143. package/templates/apps/web/messages/en.json +28 -2
  144. package/templates/content/_site.mdx +4 -2
  145. package/templates/content/en/BlogList/blog.mdx +37 -16
  146. package/templates/content/en/ContactPage/contact.mdx +14 -44
  147. package/templates/content/en/HomePage/home.mdx +8 -16
  148. package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
  149. package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
  150. package/templates/database/CHANGELOG.md +7 -0
  151. package/templates/database/package.json +1 -1
  152. package/versions.json +33 -33
  153. package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
  154. package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
  155. package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
  156. package/site-templates/simple-service/DESIGN.md +0 -162
  157. package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
  158. package/site-templates/simple-service/ailk-template.json +0 -11
  159. package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
  160. package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
  161. package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
  162. package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
  163. package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
  164. package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
  165. package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
  166. package/site-templates/simple-service/provenance.json +0 -4
  167. package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
  168. package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
  169. package/templates/content/en/SoftwareProduct/product.mdx +0 -175
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: setup-database
3
+ description: Take this site from no database to a working lead inbox — provision Postgres, set the connection variables, create the tables, pin the owner, and prove it with one test message that must appear in the inbox. Use when the user asks "set up the database", "I want a lead inbox", "keep my leads", "collect newsletter signups", "connect Postgres", or when setup-deploy points here. Confirms before every write and never prints a connection string.
4
+ ---
5
+
6
+ # setup-database
7
+
8
+ A new site needs no database: contact-form messages are emailed to the
9
+ owner (when the email key is set). A database turns on the **lead inbox**
10
+ — every message kept at `/leads`, reviewed signed in — and newsletter
11
+ signups. This skill takes the site from no database to that inbox, and
12
+ does not call it done until one test message is in it.
13
+
14
+ ## When to use
15
+
16
+ - "set up the database" / "connect Postgres"
17
+ - "I want a lead inbox" / "keep every message"
18
+ - "collect newsletter signups"
19
+ - `setup-deploy` pointed here from its lead-delivery step
20
+
21
+ Skip it when the site only needs messages by email — that is
22
+ `setup-deploy`'s email key, no database required.
23
+
24
+ ## Rules for every step
25
+
26
+ - **Confirm before every write.** Say what file or command is next and
27
+ wait for a yes.
28
+ - **Never echo a connection string.** A database URL contains a
29
+ password. Refer to it as "your `DATABASE_URL`", never print it, never
30
+ commit it. It goes in `.env.local` (gitignored) and the hosting
31
+ dashboards only.
32
+ - **Stop on the first failure** and say which step failed and why
33
+ (see "When a step fails"). Never skip ahead.
34
+
35
+ ## The mechanic
36
+
37
+ ### Step 1 — Detect
38
+
39
+ Look for `DATABASE_URL` in `.env.local`, then `.env`. If it is already
40
+ set, say so (without printing it) and go to Step 4.
41
+
42
+ ### Step 2 — Provision Postgres
43
+
44
+ Ask which Postgres the owner wants. Any provider that gives a **pooled**
45
+ and a **direct** connection URL works (Neon, Supabase, Railway, RDS, …).
46
+ For local work, the one command in `README.md` § "When you need a
47
+ database":
48
+
49
+ ```bash
50
+ docker run --name ailk-postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:17
51
+ ```
52
+
53
+ The owner pastes the two URLs. Write them to `.env.local` only:
54
+
55
+ ```bash
56
+ DATABASE_URL=<pooled URL>
57
+ DATABASE_URL_NON_POOLED=<direct URL>
58
+ ```
59
+
60
+ (Local Docker: the same URL for both.) Then tell the owner to add the
61
+ same two variables to **both** hosting projects — the API's (it stores
62
+ the leads) and the web app's (sign-in reads the database). Placeholders
63
+ only in any file you show; the real values go in each dashboard.
64
+
65
+ ### Step 3 — Create the tables, then the default workspace
66
+
67
+ ```bash
68
+ pnpm db:migrate:deploy
69
+ ```
70
+
71
+ ```bash
72
+ pnpm db:seed
73
+ ```
74
+
75
+ The seed creates the default Organization and Workspace every lead is
76
+ filed under. Without it the API answers `503 tenant_unresolved` to every
77
+ submission.
78
+
79
+ ### Step 4 — Pin the owner, and make sure they can sign in
80
+
81
+ The inbox shows leads only to the site's pinned owner. Ask for the
82
+ email address the owner signs in with and set it — in `.env.local`, and
83
+ on the API project's dashboard:
84
+
85
+ ```bash
86
+ AILK_OPERATOR_EMAIL=you@your-domain.com
87
+ ```
88
+
89
+ The owner must sign in with that address, and it must be verified by
90
+ the sign-in provider. If no sign-in provider is configured yet, hand off
91
+ to the `add-auth` skill now — the inbox is behind sign-in — and come
92
+ back to Step 5 when it is done.
93
+
94
+ ### Step 5 — Send one test message
95
+
96
+ Make a nonce (for example `setup-database 3f9c2a`, a few random
97
+ characters) and submit a lead to the running API (`pnpm dev` locally,
98
+ or the deployed API's URL):
99
+
100
+ ```bash
101
+ curl -sS -X POST <api>/v1/leads/submit \
102
+ -H 'content-type: application/json' \
103
+ -d '{"source":"web","email":"<owner email>","payload":{"name":"setup-database test","message":"<nonce>"}}'
104
+ ```
105
+
106
+ It must answer **`201`** with `"persisted": "db"`. Anything else is a
107
+ failed step (see below).
108
+
109
+ ### Step 6 — Verify it is in the inbox
110
+
111
+ Build the database package once (its `./tenancy` export resolves to
112
+ `dist/`), then read the inbox with the inbox's own rule — the default
113
+ workspace, newest first, the first page of 50:
114
+
115
+ ```bash
116
+ pnpm --filter @working-theory/database build
117
+ pnpm --filter @working-theory/database exec tsx ../.claude/skills/setup-database/scripts/verify-inbox.ts "<nonce>"
118
+ ```
119
+
120
+ It prints `found in inbox` and exits 0 only when the test lead is on
121
+ that page. Then ask the owner to open `/leads`, signed in as the pinned
122
+ email, and confirm they see the "setup-database test" row.
123
+
124
+ Do not report the database as set up until verify-inbox.ts exits 0.
125
+
126
+ ## When a step fails
127
+
128
+ Say which step failed and the likely cause — never "done":
129
+
130
+ | Symptom | Likely cause |
131
+ | ------------------------------------------- | ---------------------------------------------------------------------------------------------- |
132
+ | `db:migrate:deploy` cannot connect | wrong `DATABASE_URL_NON_POOLED`, or the database is not reachable from here |
133
+ | submit answers `503 tenant_unresolved` (or, with `CONTENT_ADAPTER=database`, the API refuses to start) | Step 3's `pnpm db:seed` did not run |
134
+ | submit answers `201` with `persisted` ≠ `db` | the API process does not see `DATABASE_URL` — restart it, or set it on the API project |
135
+ | verify-inbox exits 1 | the lead landed under another workspace, or 50 newer leads push it off the first page |
136
+ | `/leads` is empty but verify passed | `AILK_OPERATOR_EMAIL` unset, a different or unverified sign-in email, or more than one workspace |
137
+
138
+ ## What this skill does not do
139
+
140
+ - Call a database vendor's API. The owner provisions; the skill walks
141
+ them through it.
142
+ - Print, log or commit a connection string.
143
+ - Change application code. The API switches to the inbox by itself once
144
+ `DATABASE_URL` is set.
145
+
146
+ ## Coordination
147
+
148
+ - **`setup-deploy`** — its lead-delivery step points here, and its
149
+ `RUNBOOK.md` names this skill as the **Lead inbox** path.
150
+ - **`add-auth`** — the inbox is behind sign-in; Step 4 hands off to it
151
+ when no provider is configured.
152
+
153
+ ## Files
154
+
155
+ - `scripts/verify-inbox.ts` — Step 6's read. Exports `findInInbox`
156
+ (pure, takes an injected Prisma-shaped client) and a thin `main()`.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * verify-inbox — the last step of the `setup-database` skill.
3
+ *
4
+ * Proves the test lead the skill just submitted is in the lead inbox, by
5
+ * reading with the inbox's own predicate: `Lead.siteId` = the default
6
+ * workspace id, newest first, the first page of 50 — exactly what `/leads`
7
+ * shows. A lead that exists somewhere else in the table does not count.
8
+ *
9
+ * Run inside the scaffold's `database` workspace, which owns the Prisma client
10
+ * and its driver adapter. The script's own imports resolve from the WORKING
11
+ * directory (`resolveFromWorkspace`), not from where this file sits — the
12
+ * scaffold root depends on neither package. Build it first — the `./tenancy`
13
+ * export resolves to `dist/`:
14
+ *
15
+ * pnpm --filter @working-theory/database build
16
+ * pnpm --filter @working-theory/database exec tsx ../.claude/skills/setup-database/scripts/verify-inbox.ts <nonce>
17
+ *
18
+ * Exit 0, printing `found in inbox`, only when the nonce's lead is on that
19
+ * page. Exit 1 when it is not; exit 2 on a usage error. Reads only — writes
20
+ * nothing, and prints no lead field but the match.
21
+ */
22
+
23
+ import { createRequire } from "node:module";
24
+ import { join } from "node:path";
25
+ import { pathToFileURL } from "node:url";
26
+
27
+ /**
28
+ * Import `specifier` as the package at `cwd` sees it. A bare `import()` here
29
+ * would resolve from this file's own directory (`.claude/skills/…`), where
30
+ * the scaffold installs neither `@working-theory/database` nor its adapter;
31
+ * resolving against `<cwd>/package.json` finds the `database` workspace's own
32
+ * dependencies and its self-reference.
33
+ */
34
+ export function resolveFromWorkspace(specifier: string, cwd: string = process.cwd()): string {
35
+ return createRequire(join(cwd, "package.json")).resolve(specifier);
36
+ }
37
+
38
+ async function importFromWorkspace<T>(specifier: string): Promise<T> {
39
+ return (await import(pathToFileURL(resolveFromWorkspace(specifier)).href)) as T;
40
+ }
41
+
42
+ /** The inbox's first page (`GET /v1/leads`'s default `limit`). */
43
+ export const INBOX_PAGE_SIZE = 50;
44
+
45
+ interface LeadRow {
46
+ id: string;
47
+ payload: unknown;
48
+ }
49
+
50
+ /** The one Prisma call this script makes — injected, so it is testable without a database. */
51
+ export interface InboxReader {
52
+ lead: {
53
+ findMany(args: {
54
+ where: { siteId: string };
55
+ orderBy: Array<Record<string, "desc">>;
56
+ take: number;
57
+ select: { id: true; payload: true };
58
+ }): Promise<LeadRow[]>;
59
+ };
60
+ }
61
+
62
+ function carriesNonce(payload: unknown, nonce: string): boolean {
63
+ if (typeof payload !== "object" || payload === null) return false;
64
+ const message = (payload as Record<string, unknown>)["message"];
65
+ return typeof message === "string" && message.includes(nonce);
66
+ }
67
+
68
+ /** Whether the lead whose message carries `nonce` is on the inbox's first page. */
69
+ export async function findInInbox(
70
+ db: InboxReader,
71
+ { workspaceId, nonce }: { workspaceId: string; nonce: string },
72
+ ): Promise<boolean> {
73
+ const page = await db.lead.findMany({
74
+ where: { siteId: workspaceId },
75
+ orderBy: [{ createdAt: "desc" }, { id: "desc" }],
76
+ take: INBOX_PAGE_SIZE,
77
+ select: { id: true, payload: true },
78
+ });
79
+ return page.some((row) => carriesNonce(row.payload, nonce));
80
+ }
81
+
82
+ /** `findInInbox` as a process result: 0 + `found in inbox`, or 1 + the reason. */
83
+ export async function verifyInbox(
84
+ db: InboxReader,
85
+ args: { workspaceId: string; nonce: string },
86
+ out: (line: string) => void = (line) => process.stdout.write(`${line}\n`),
87
+ ): Promise<0 | 1> {
88
+ if (await findInInbox(db, args)) {
89
+ out("found in inbox");
90
+ return 0;
91
+ }
92
+ out(
93
+ "not in inbox: no lead carrying this nonce is on the default workspace's first page. " +
94
+ "Check that `pnpm db:seed` ran (else the API answers tenant_unresolved) and that " +
95
+ "the test submission returned 201 with persisted \"db\".",
96
+ );
97
+ return 1;
98
+ }
99
+
100
+ async function main(argv: string[]): Promise<number> {
101
+ const nonce = argv[0];
102
+ if (!nonce) {
103
+ process.stderr.write("usage: verify-inbox.ts <nonce>\n");
104
+ return 2;
105
+ }
106
+ // DATABASE_URL lives in the scaffold root's `.env.local` (setup-database
107
+ // writes it there); this runs from `database/`, one level down.
108
+ for (const file of ["../.env.local", "../.env"]) {
109
+ if (process.env.DATABASE_URL) break;
110
+ try {
111
+ process.loadEnvFile(file);
112
+ } catch {
113
+ // absent — try the next one
114
+ }
115
+ }
116
+ if (!process.env.DATABASE_URL) {
117
+ process.stderr.write("verify-inbox: DATABASE_URL is not set (.env.local)\n");
118
+ return 1;
119
+ }
120
+ // Resolved inside the scaffold's `database` workspace (see the header).
121
+ // Prisma 7 needs an explicit driver adapter, as `database/seed.ts` does.
122
+ const [{ PrismaClient }, { PrismaPg }, { DEFAULT_WORKSPACE_ID }] = await Promise.all([
123
+ importFromWorkspace<{
124
+ PrismaClient: new (opts: { adapter: unknown }) => InboxReader & {
125
+ $disconnect(): Promise<void>;
126
+ };
127
+ }>("@working-theory/database"),
128
+ importFromWorkspace<{ PrismaPg: new (opts: { connectionString: string }) => unknown }>(
129
+ "@prisma/adapter-pg",
130
+ ),
131
+ importFromWorkspace<{ DEFAULT_WORKSPACE_ID: string }>("@working-theory/database/tenancy"),
132
+ ]);
133
+ const prisma = new PrismaClient({
134
+ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
135
+ });
136
+ try {
137
+ return await verifyInbox(prisma, {
138
+ workspaceId: DEFAULT_WORKSPACE_ID,
139
+ nonce,
140
+ });
141
+ } finally {
142
+ await prisma.$disconnect();
143
+ }
144
+ }
145
+
146
+ if (/verify-inbox\.ts$/.test(process.argv[1] ?? "")) {
147
+ main(process.argv.slice(2)).then(
148
+ (code) => process.exit(code),
149
+ (err: unknown) => {
150
+ process.stderr.write(`verify-inbox: ${err instanceof Error ? err.message : String(err)}\n`);
151
+ process.exit(1);
152
+ },
153
+ );
154
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup-deploy
3
- description: Emit deploy configuration for an AILK consumer site — `vercel.json`, `.env.example`, and `RUNBOOK.md`. Vercel-only at v1; other platforms reject with a clear "not yet supported" message. Use when the user asks "set up Vercel", "provision deploy config", "what env vars do I need for production?", "I'm ready to ship to Vercel", or otherwise asks about pre-deploy file setup. The RUNBOOK includes the three named persistence-upgrade paths for leads.jsonl (Vercel KV / Supabase / Postgres-via-`@working-theory/database`).
3
+ description: Emit deploy configuration for an AILK consumer site — `vercel.json`, `.env.example`, and `RUNBOOK.md`. Vercel-only at v1; other platforms reject with a clear "not yet supported" message. Use when the user asks "set up Vercel", "provision deploy config", "what env vars do I need for production?", "I'm ready to ship to Vercel", or otherwise asks about pre-deploy file setup. Asks where leads should go — the email key (`RESEND_API_KEY` + the two notification addresses) or a database (`setup-database`) — and warns when neither is set. The RUNBOOK documents the two lead destinations: Email (default) and Lead inbox (setup-database).
4
4
  ---
5
5
 
6
6
  # setup-deploy
@@ -42,9 +42,11 @@ Skip this skill when:
42
42
  (merge / overwrite / skip) before writing.
43
43
  - Emit the three artifacts at the consumer's project root, derived
44
44
  from templates in `references/`.
45
+ - Ask where leads go (Step 5a) — the email key, or a database via
46
+ `setup-database` — and warn, verbatim, when neither is set.
45
47
  - Validate the emitted output (parse `vercel.json` as JSON, check
46
48
  `.env.example` covers required vars, confirm `RUNBOOK.md` has the
47
- three named persistence-upgrade headings).
49
+ two lead-destination headings).
48
50
 
49
51
  **Does not:**
50
52
 
@@ -70,9 +72,7 @@ platform, respond:
70
72
  > if your platform is missing.
71
73
  >
72
74
  > Cloudflare is deferred entirely (deploy target + KV/D1/R2 data
73
- > layer) as a coherent follow-up. CF requires different default
74
- > persistence per platform because Workers lack the Node `fs` API
75
- > the JSONL fallback depends on."
75
+ > layer) as a coherent follow-up."
76
76
 
77
77
  Then stop. Do not emit any files.
78
78
 
@@ -102,9 +102,9 @@ Wait for the consumer's choice per file. Do not blindly overwrite.
102
102
  file, append it (with comment) at the bottom under a "## Added by
103
103
  setup-deploy" section. Vars already present are not modified.
104
104
 
105
- **Merge logic — `RUNBOOK.md`:** if any of the three named
106
- persistence sections (Vercel KV, Supabase,
107
- Postgres-via-`@working-theory/database`) is missing from the existing file,
105
+ **Merge logic — `RUNBOOK.md`:** if the "Leads — where messages go"
106
+ section (with its two headings, `Email (default)` and
107
+ `Lead inbox (setup-database)`) is missing from the existing file,
108
108
  append it at the end. Existing sections are not modified.
109
109
 
110
110
  **Merge logic — `vercel.json`:** **not supported.** `vercel.json` is
@@ -147,6 +147,34 @@ the template's expected count. If a var the consumer's site uses
147
147
  template, add it under "## Added by setup-deploy" — never silently
148
148
  omit a var the consumer needs.
149
149
 
150
+ ### Step 5a — Lead delivery
151
+
152
+ Where a contact-form message goes is decided by the API project's
153
+ environment (RUNBOOK § "Leads — where messages go"). Ask the consumer
154
+ two questions:
155
+
156
+ 1. **Will this site use a database?** If yes, point them to the
157
+ `setup-database` skill — it provisions Postgres, sets
158
+ `DATABASE_URL`, runs migrations and verifies a test lead reaches
159
+ the inbox. Do not collect a connection string here.
160
+ 2. **What email should messages go to?** Ask for
161
+ `RESEND_API_KEY`, `AILK_LEAD_NOTIFICATION_TO` (their address) and
162
+ `AILK_LEAD_NOTIFICATION_FROM` (a sender on their Resend-verified
163
+ domain). All three are needed — a key alone sends nothing. Write
164
+ **placeholders only** into `.env.example`; tell the consumer to set
165
+ the real values in the **API** project's dashboard. Never write a
166
+ key into a file.
167
+
168
+ If the consumer provides neither a database nor all three email
169
+ values, print this, verbatim:
170
+
171
+ > ⚠ No lead destination is set. In production your contact page will show
172
+ > your contact email instead of the form, and no messages will be collected.
173
+ > Run setup-database, or set RESEND_API_KEY, AILK_LEAD_NOTIFICATION_TO and
174
+ > AILK_LEAD_NOTIFICATION_FROM on the API project.
175
+
176
+ Then continue — the warning does not stop the deploy config.
177
+
150
178
  ### Step 6 — Emit `RUNBOOK.md`
151
179
 
152
180
  Use the template at `references/runbook-template.md`. The emitted
@@ -154,26 +182,24 @@ runbook has these sections:
154
182
 
155
183
  1. **Pre-deploy checklist** — what to set in Vercel's project
156
184
  dashboard before clicking Deploy.
157
- 2. **Operations — leads.jsonl persistence** — the **critical**
158
- section. AILK's lead-capture writes JSONL to
159
- `apps/api/leads.jsonl`, which **does not persist** on Vercel's
160
- ephemeral filesystem. Consumer must pick a persistence backend
161
- before going live or leads disappear on cold-start.
162
- 3. **Persistence upgrade — Vercel KV** — drop-in, no new infra,
163
- cheapest fix. When to pick.
164
- 4. **Persistence upgrade — Supabase** — Postgres + auth + storage
165
- in one provider. When to pick.
166
- 5. **Persistence upgrade — Postgres via `@working-theory/database`** — for
167
- consumers who already have Postgres elsewhere (Neon, Railway,
168
- RDS). When to pick.
169
- 6. **Custom domain + DNS** — pointer at Vercel's domain docs.
170
- 7. **Production callbacks for OAuth** — for consumers who used
185
+ 2. **Leads — where messages go** — the two lead destinations, in
186
+ plain words:
187
+ - **Email (default)** — no database; each message is emailed to
188
+ the owner (`RESEND_API_KEY`, `AILK_LEAD_NOTIFICATION_TO`,
189
+ `AILK_LEAD_NOTIFICATION_FROM` on the API project).
190
+ - **Lead inbox (setup-database)** — `DATABASE_URL` set; messages
191
+ land in `/leads` and newsletter signups are collected.
192
+ With neither, production `/contact` shows the site's contact email
193
+ instead of the form. On a non-Vercel host, the API process needs
194
+ `NODE_ENV=production`.
195
+ 3. **Custom domain + DNS** — pointer at Vercel's domain docs.
196
+ 4. **Production callbacks for OAuth** — for consumers who used
171
197
  `add-auth` and need to register prod redirect URIs.
172
198
 
173
- After write, grep the RUNBOOK for the three named persistence
174
- section headings (Vercel KV, Supabase,
175
- Postgres-via-`@working-theory/database`). All three must be present. If any
176
- is missing, the skill has a bug — flag with the literal error.
199
+ After write, grep the RUNBOOK for the two lead-destination headings
200
+ (`Email (default)`, `Lead inbox (setup-database)`). Both must be
201
+ present. If either is missing, the skill has a bug — flag with the
202
+ literal error.
177
203
 
178
204
  ### Step 7 — Closing summary
179
205
 
@@ -181,8 +207,9 @@ Print:
181
207
 
182
208
  - Which artifacts were emitted vs skipped (per consumer's Step 3
183
209
  choices).
184
- - A pointer to `RUNBOOK.md`'s leads-persistence section (the
185
- highest-stakes operational caveat).
210
+ - Where leads will go (email, the lead inbox, or — with the Step 5a
211
+ warning repeated — nowhere), with a pointer to `RUNBOOK.md`'s
212
+ "Leads — where messages go" section.
186
213
  - A reminder that Vercel project-level env-vars need to be set in
187
214
  the Vercel dashboard before deploy — `.env.example` is the
188
215
  contract, not the values.
@@ -203,8 +230,10 @@ Before reporting "done":
203
230
 
204
231
  - `vercel.json` parses as JSON ✓
205
232
  - `.env.example` declares >= the count of vars in the template ✓
206
- - `RUNBOOK.md` contains all three named persistence section
207
- headings ✓
233
+ - `RUNBOOK.md` contains both lead-destination headings
234
+ (`Email (default)`, `Lead inbox (setup-database)`) ✓
235
+ - Step 5a ran: the consumer chose a lead destination, or saw the
236
+ no-destination warning ✓
208
237
 
209
238
  If any check fails, surface the failure to the consumer (not silent).
210
239
 
@@ -223,11 +252,14 @@ If any check fails, surface the failure to the consumer (not silent).
223
252
  - `references/vercel-json-template.md` — minimal viable `vercel.json`.
224
253
  - `references/env-vars-template.md` — `.env.example` structure with
225
254
  placeholder values per category.
226
- - `references/runbook-template.md` — `RUNBOOK.md` with the three
227
- named persistence-upgrade sections.
255
+ - `references/runbook-template.md` — `RUNBOOK.md` with the two lead
256
+ destinations (Email (default), Lead inbox (setup-database)).
228
257
 
229
258
  ## Coordination
230
259
 
260
+ - **`setup-database`** — takes a site from no database to a working
261
+ lead inbox. Step 5a hands off to it; this skill never collects a
262
+ connection string.
231
263
  - **`add-auth`** — produces its own `RUNBOOK.md`
232
264
  sections for OAuth setup (dev callback URLs). This skill's
233
265
  RUNBOOK adds production callback URLs and merges with any
@@ -28,11 +28,23 @@ CONTENT_BACKEND=mdx
28
28
  # Override the content directory. Default is <repo-root>/content.
29
29
  # AILK_CONTENT_ROOT=
30
30
 
31
- # ── Database (optional in standalone mode) ───────────────────────
32
- # Required only if you've installed @working-theory/database for persistent
33
- # leads, sessions, or other data. See RUNBOOK.md § "Persistence
34
- # upgrade — Postgres via @working-theory/database" for setup.
31
+ # ── Lead delivery ────────────────────────────────────────────────
32
+ # Where contact-form messages go (RUNBOOK.md § "Leads — where
33
+ # messages go"). Set these on the API project.
34
+ #
35
+ # Lead inbox — set by the `setup-database` skill. With it, messages
36
+ # land in /leads and newsletter signups are collected.
35
37
  # DATABASE_URL=postgres://user:pass@host:5432/db
38
+ # DATABASE_URL_NON_POOLED=postgres://user:pass@host:5432/db
39
+ #
40
+ # Email (default) — with no database, each message is emailed to you.
41
+ # All three are required; a key alone sends nothing.
42
+ # RESEND_API_KEY=
43
+ # AILK_LEAD_NOTIFICATION_TO=you@your-domain.com
44
+ # AILK_LEAD_NOTIFICATION_FROM=leads@your-domain.com
45
+ #
46
+ # With neither, /contact shows your contact email (content/_site.mdx
47
+ # `contactEmail`) instead of the form in production.
36
48
 
37
49
  # ── Auth — required env vars ─────────────────────────────────────
38
50
  # Set to "1" to require sign-in for protected routes; default is
@@ -78,11 +90,6 @@ CONTENT_BACKEND=mdx
78
90
  # Webhooks → <endpoint> → Signing secret. Format: whsec_*.
79
91
  # STRIPE_WEBHOOK_SECRET=
80
92
 
81
- # ── Email provider (Resend) ──────────────────────────────────────
82
- # Used by transactional senders outside auth (e.g., contact form).
83
- # Same Resend account as magic-link if both are configured.
84
- # RESEND_API_KEY=
85
-
86
93
  # ── Analytics — all off by default ───────────────────────────────
87
94
  # Each provider is independent. Set the env var(s) to enable.
88
95
  #
@@ -105,26 +112,11 @@ CONTENT_BACKEND=mdx
105
112
  # Get the token from search.google.com/search-console → Settings →
106
113
  # Ownership verification → HTML tag. Paste only the content="..." value.
107
114
  # GOOGLE_SITE_VERIFICATION=
108
-
109
- # ── Persistent leads (optional) ──────────────────────────────────
110
- # By default, leads.jsonl is written to apps/api/leads.jsonl. On
111
- # Vercel's ephemeral filesystem this does NOT persist across cold
112
- # starts. See RUNBOOK.md § "Persistence upgrade" for the three
113
- # named upgrade paths.
114
- #
115
- # Vercel KV (drop-in):
116
- # KV_REST_API_URL=
117
- # KV_REST_API_TOKEN=
118
- #
119
- # Supabase (Postgres + auth + storage):
120
- # NEXT_PUBLIC_SUPABASE_URL=
121
- # NEXT_PUBLIC_SUPABASE_ANON_KEY=
122
- # SUPABASE_SERVICE_ROLE_KEY=
123
115
  ```
124
116
 
125
117
  ## Categories the skill emits
126
118
 
127
- The template above groups vars into seven categories. Every emitted
119
+ The template above groups vars into six categories. Every emitted
128
120
  `.env.example` should hit each category at least with its header
129
121
  comment, even if the consumer hasn't activated any vars in that
130
122
  category — the comment block tells future-them what's available.
@@ -133,11 +125,10 @@ Categories:
133
125
 
134
126
  1. Marketing / canonical domain
135
127
  2. Content backend
136
- 3. Database (optional)
128
+ 3. Lead delivery (database or email)
137
129
  4. Auth — required + per-provider
138
130
  5. Stripe (sandbox-only)
139
- 6. Email + Analytics
140
- 7. Persistent leads (optional)
131
+ 6. Analytics
141
132
 
142
133
  ## What the skill never includes
143
134