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.
- package/README.md +44 -10
- package/component-catalog.json +520 -5
- package/component-catalog.md +9 -6
- package/dist/cli.js +22 -24
- package/dist/lib/extract-module-bundle.js +5 -1
- package/dist/lib/module-license-gate.d.ts +79 -0
- package/dist/lib/module-license-gate.js +160 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- package/dist/parse-args.d.ts +78 -10
- package/dist/parse-args.js +204 -14
- package/dist/programmatic.d.ts +39 -9
- package/dist/programmatic.js +163 -25
- package/dist/site-roster.js +11 -6
- package/dist/starter-grid.d.ts +216 -0
- package/dist/starter-grid.js +449 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.js +102 -43
- package/dist/template-manifest.d.ts +36 -17
- package/dist/template-manifest.js +121 -367
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +3 -3
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -1
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +69 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +69 -0
- package/site-templates/designs/standard/DESIGN.md +5 -0
- package/site-templates/marketing-site/DESIGN.md +5 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +89 -25
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +35 -14
- package/site-templates/marketing-site-free/DESIGN.md +5 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +95 -29
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +74 -44
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +17 -29
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -1
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/site-templates/saas-product/DESIGN.md +5 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +96 -76
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +302 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +362 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +300 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +369 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +299 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/shapes/person-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +3 -2
- package/templates/.claude/skills/add-page/references/page-types.md +4 -0
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +19 -28
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- package/templates/DESIGN.md +5 -0
- package/templates/README.md +46 -38
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/server.ts +7 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/layout.tsx +37 -5
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +18 -3
- package/templates/apps/web/lib/nav-config.ts +48 -36
- package/templates/apps/web/lib/site-identity.ts +14 -0
- package/templates/apps/web/messages/en.json +28 -2
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- package/templates/content/en/ContactPage/contact.mdx +14 -44
- package/templates/content/en/HomePage/home.mdx +8 -16
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/use-cases.mdx +6 -6
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/versions.json +33 -33
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- 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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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. **
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
-
|
|
185
|
-
|
|
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
|
|
207
|
-
|
|
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
|
|
227
|
-
|
|
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
|
-
# ──
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
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
|
|
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.
|
|
128
|
+
3. Lead delivery (database or email)
|
|
137
129
|
4. Auth — required + per-provider
|
|
138
130
|
5. Stripe (sandbox-only)
|
|
139
|
-
6.
|
|
140
|
-
7. Persistent leads (optional)
|
|
131
|
+
6. Analytics
|
|
141
132
|
|
|
142
133
|
## What the skill never includes
|
|
143
134
|
|