create-ailk 0.10.0 → 0.11.1

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 (103) hide show
  1. package/README.md +7 -2
  2. package/dist/cli.js +5 -1
  3. package/dist/content-payloads.d.ts +37 -0
  4. package/dist/content-payloads.js +64 -0
  5. package/dist/lib/fetch-module.d.ts +3 -3
  6. package/dist/lib/fetch-module.js +11 -5
  7. package/dist/lib/fetch-template.d.ts +1 -1
  8. package/dist/lib/fetch-template.js +7 -4
  9. package/dist/lib/scaffold-render-sweep.d.ts +16 -0
  10. package/dist/lib/scaffold-render-sweep.js +52 -3
  11. package/dist/module-architecture.d.ts +9 -0
  12. package/dist/module-architecture.js +43 -12
  13. package/dist/parse-args.d.ts +2 -0
  14. package/dist/parse-args.js +7 -0
  15. package/dist/surfaces.js +2 -3
  16. package/package.json +3 -3
  17. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +4 -3
  18. package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +8 -8
  19. package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +8 -8
  20. package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +8 -8
  21. package/site-templates/shapes/organization-service/content/en/Service/first-engagement.mdx +8 -8
  22. package/site-templates/shapes/organization-service/content/en/Service/second-engagement.mdx +8 -8
  23. package/site-templates/shapes/organization-service/content/en/Service/third-engagement.mdx +8 -8
  24. package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +8 -8
  25. package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +8 -8
  26. package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +8 -8
  27. package/templates/.claude/settings.json +12 -0
  28. package/templates/.env.example +1 -5
  29. package/templates/CLAUDE.md +38 -22
  30. package/templates/README.md +12 -12
  31. package/templates/apps/api/jest.config.cjs +1 -0
  32. package/templates/apps/api/package.json +2 -1
  33. package/templates/apps/mcp/CLAUDE.md +23 -10
  34. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +160 -17
  35. package/templates/apps/mcp/__tests__/index.test.ts +118 -1
  36. package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +425 -5
  37. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +67 -3
  38. package/templates/apps/mcp/__tests__/server.test.ts +162 -1
  39. package/templates/apps/mcp/package.json +1 -1
  40. package/templates/apps/mcp/src/index.ts +35 -3
  41. package/templates/apps/mcp/src/lib/commerce-claim.ts +193 -24
  42. package/templates/apps/mcp/src/server.ts +77 -5
  43. package/templates/apps/mcp/src/tools/purchase.ts +1 -1
  44. package/templates/apps/web/CLAUDE.md +1 -1
  45. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +15 -0
  46. package/templates/apps/web/app/.well-known/mcp.json/route.ts +10 -3
  47. package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +0 -10
  48. package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +0 -10
  49. package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +1 -14
  50. package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +0 -10
  51. package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +0 -10
  52. package/templates/apps/web/app/[locale]/for/[slug]/page.tsx +0 -10
  53. package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +1 -14
  54. package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +0 -10
  55. package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +0 -10
  56. package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +0 -10
  57. package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +0 -10
  58. package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +1 -15
  59. package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +0 -10
  60. package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +0 -10
  61. package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +0 -10
  62. package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +1 -14
  63. package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +4 -16
  64. package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +3 -15
  65. package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +0 -10
  66. package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +1 -14
  67. package/templates/apps/web/src/lib/commerce-claim.ts +15 -6
  68. package/templates/content/_site.mdx +23 -4
  69. package/templates/database/CHANGELOG.md +7 -0
  70. package/templates/database/package.json +1 -1
  71. package/versions.json +34 -34
  72. package/templates/.claude/skills/README.md +0 -37
  73. package/templates/.claude/skills/add-auth/SKILL.md +0 -313
  74. package/templates/.claude/skills/add-auth/references/env-vars.md +0 -106
  75. package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +0 -180
  76. package/templates/.claude/skills/add-page/SKILL.md +0 -260
  77. package/templates/.claude/skills/add-page/references/page-types.md +0 -123
  78. package/templates/.claude/skills/add-page/scripts/site-pages.mjs +0 -87
  79. package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +0 -99
  80. package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +0 -18
  81. package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +0 -43
  82. package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +0 -52
  83. package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +0 -38
  84. package/templates/.claude/skills/check-site/SKILL.md +0 -245
  85. package/templates/.claude/skills/check-site/references/common-errors.md +0 -116
  86. package/templates/.claude/skills/check-site/references/findings-glossary.md +0 -140
  87. package/templates/.claude/skills/check-site/references/severity-levels.md +0 -114
  88. package/templates/.claude/skills/fill-page/SKILL.md +0 -199
  89. package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +0 -80
  90. package/templates/.claude/skills/fill-page/references/section-sources.md +0 -41
  91. package/templates/.claude/skills/setup-database/SKILL.md +0 -156
  92. package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +0 -154
  93. package/templates/.claude/skills/setup-deploy/SKILL.md +0 -312
  94. package/templates/.claude/skills/setup-deploy/references/runbook-template.md +0 -204
  95. package/templates/.claude/skills/setup-deploy/references/vercel-json-template.md +0 -98
  96. package/templates/.claude/skills/update-page/SKILL.md +0 -117
  97. package/templates/.claude/skills/update-page/scripts/update-page.ts +0 -640
  98. package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +0 -31
  99. package/templates/.claude/skills/upgrade-plan/SKILL.md +0 -121
  100. package/templates/.claude/skills/upgrade-plan/references/options.md +0 -24
  101. package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +0 -527
  102. package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +0 -75
  103. package/templates/apps/mcp/src/lib/require-paid-tier.ts +0 -44
@@ -1,156 +0,0 @@
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()`.
@@ -1,154 +0,0 @@
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,312 +0,0 @@
1
- ---
2
- name: setup-deploy
3
- description: Emit deploy configuration for an AILK consumer site — `vercel.json` and `RUNBOOK.md`, plus an env checklist read from the scaffold's own `.env.example`. 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
- ---
5
-
6
- # setup-deploy
7
-
8
- Emit `vercel.json` and `RUNBOOK.md` at the consumer's project root,
9
- and bring the scaffold's own `.env.example` up to date with what the
10
- site's code reads, ready for a Vercel deploy. Vercel is the only valid
11
- `platform` value at v1. Other platforms (Netlify, Cloudflare, Render,
12
- Fly, self-host) reject with an explicit "not yet supported" message
13
- and emit nothing.
14
-
15
- ## When to use
16
-
17
- The consumer is preparing to deploy their AILK site for the first
18
- time, or wants to verify their deploy config is complete. Triggers:
19
-
20
- - "provision Vercel config" / "set up Vercel"
21
- - "what env vars do I need to deploy?"
22
- - "I'm ready to ship — what files do I need?"
23
- - "regenerate `vercel.json`" / "refresh `.env.example`"
24
-
25
- Skip this skill when:
26
-
27
- - The consumer is on a non-Vercel platform — emit the rejection
28
- message (see Step 1) and stop.
29
- - The consumer asks about runtime environment values
30
- (`process.env.X` debugging) — that's an app-level concern, not a
31
- config-emission concern.
32
- - The consumer is asking about Stripe live-mode setup — that's
33
- hard-floor blocked across AILK; sandbox-only at v1. Mention and stop.
34
-
35
- ## What this skill does (and does not do)
36
-
37
- **Does:**
38
-
39
- - Validate the `platform` parameter — only `vercel` is accepted at v1.
40
- - Detect existing `vercel.json`, `.env.example`, and `RUNBOOK.md` at
41
- the consumer's project root.
42
- - For each artifact, ask the consumer how to handle conflicts
43
- (merge / overwrite / skip) before writing.
44
- - Emit `vercel.json` and `RUNBOOK.md` at the consumer's project root,
45
- derived from templates in `references/`.
46
- - Read the env contract from the scaffold's own `.env.example` — the
47
- file every scaffold ships at its root — plus a `process.env` scan
48
- of the site's code, and turn it into the env checklist (Step 5).
49
- - Ask where leads go (Step 5a) — the email key, or a database via
50
- `setup-database` — and warn, verbatim, when neither is set.
51
- - Validate the emitted output (parse `vercel.json` as JSON, check
52
- `.env.example` declares every variable the `process.env` scan
53
- found, confirm `RUNBOOK.md` has the two lead-destination headings).
54
-
55
- **Does not:**
56
-
57
- - Call any Vercel API. The consumer runs `vercel deploy` themselves.
58
- - Read or write live secrets. `.env.example` uses placeholder values
59
- only.
60
- - Configure Cloudflare anything (deploy target, KV, D1, R2). CF is
61
- deferred entirely as a coherent follow-up unit.
62
- - Emit Netlify / Render / Fly / self-host config. v1 is Vercel only.
63
- - Modify the consumer's actual `.env.local` or any other file
64
- containing real secrets.
65
-
66
- ## The mechanic
67
-
68
- ### Step 1 — Validate the platform parameter
69
-
70
- Default platform is `vercel`. If the consumer asks for any other
71
- platform, respond:
72
-
73
- > "`setup-deploy` is Vercel-only at v1. Netlify / Cloudflare /
74
- > Render / Fly / self-host support are tracked at the upstream repo —
75
- > file an issue at <https://github.com/working-theory-labs/ai-launch-kit/issues>
76
- > if your platform is missing.
77
- >
78
- > Cloudflare is deferred entirely (deploy target + KV/D1/R2 data
79
- > layer) as a coherent follow-up."
80
-
81
- Then stop. Do not emit any files.
82
-
83
- ### Step 2 — Detect existing artifacts
84
-
85
- Look for `vercel.json`, `.env.example`, and `RUNBOOK.md` at the
86
- consumer's project root. For each one, note:
87
-
88
- - **Missing** — will emit fresh.
89
- - **Present** — ask how to handle (Step 3).
90
-
91
- `.env.example` is the exception: every scaffold ships one, and it is
92
- the source this skill reads, not a file it writes from scratch. If
93
- it is missing, tell the consumer to restore it from git history and
94
- re-run — never write one from memory.
95
-
96
- ### Step 3 — Conflict handling per file
97
-
98
- For each existing artifact, show the consumer:
99
-
100
- 1. The first ~10 lines of the existing file (preview).
101
- 2. A summary of what the skill would emit (template name + key
102
- fields; for `.env.example`, the variables Step 5 would add).
103
- 3. Three options: **merge** (only for `RUNBOOK.md` and
104
- `.env.example` — see merge logic below), **overwrite**
105
- (replace contents; not offered for `.env.example`, which has no
106
- other source), **skip** (leave existing file untouched).
107
-
108
- Wait for the consumer's choice per file. Do not blindly overwrite.
109
-
110
- **Merge logic — `.env.example`:** if a var the Step 5 `process.env`
111
- scan found is **missing** from every `.env.example` Step 5 reads —
112
- neither set (`NAME=`) nor listed commented (`# NAME=`) — append it
113
- (with comment) at the bottom of the root file
114
- under a `# ── Added by setup-deploy ──` heading. Lines already in
115
- the file are never modified; the consumer's comments and values win.
116
-
117
- **Merge logic — `RUNBOOK.md`:** if the "Leads — where messages go"
118
- section (with its two headings, `Email (default)` and
119
- `Lead inbox (setup-database)`) is missing from the existing file,
120
- append it at the end. Existing sections are not modified.
121
-
122
- **Merge logic — `vercel.json`:** **not supported.** `vercel.json` is
123
- either overwritten or skipped — no field-level merge. Reason: the
124
- file is small and consumers are likely to have customized it
125
- intentionally; merging risks subtle config drift.
126
-
127
- ### Step 4 — Emit `vercel.json`
128
-
129
- Use the template at `references/vercel-json-template.md`. The
130
- emitted file has these fields:
131
-
132
- - `version: 2` — Vercel config schema version.
133
- - `framework: "nextjs"` — Vercel auto-detects `apps/web` as Next.js.
134
- - `buildCommand` — pnpm-aware build (typically
135
- `cd ../.. && pnpm install --frozen-lockfile && pnpm --filter ./apps/web build`
136
- for monorepo deploys, or `pnpm build` for single-app deploys).
137
- - `installCommand` — match the consumer's package manager.
138
- - `env` — env-var passthrough; AILK ships an empty `env: {}` and
139
- expects Vercel's project-level env-var dashboard to handle real
140
- values.
141
-
142
- After write, parse the JSON. If parse fails, the skill has a bug —
143
- report it to the consumer with the literal error.
144
-
145
- ### Step 5 — Build the env checklist from `.env.example`
146
-
147
- The env contract is the scaffold's own `.env.example` at the project
148
- root, plus the per-app `apps/*/.env.example` files it points to. Read
149
- them; keep no second copy of them anywhere, including in this skill.
150
-
151
- 1. **Read the declared names.** In each of those files, every
152
- uncommented `NAME=value` line is a variable the scaffold sets (for
153
- example `CONTENT_ADAPTER=mdx`); every commented `# NAME=` line is
154
- an opt-in variable. The `# ── <Section> ──` heading above a line
155
- is its group, and the comment above an entry says what it does
156
- and whether production needs it. A name counts as declared when
157
- any of the files declares it.
158
- 2. **Scan the code.** Grep the site's source (`apps/`, `database/`,
159
- and `packages/` if the scaffold has one) for `process.env.NAME`
160
- and `process.env["NAME"]`. Skip `NODE_ENV`, variables the platform
161
- sets itself (`VERCEL_*`, `CI`), and any name the files' comments
162
- call deprecated — the code reads those only as a fallback, and the
163
- checklist uses the name that replaced them.
164
- 3. **Add what is missing.** A name the scan found that `.env.example`
165
- does not declare goes at the bottom under
166
- `# ── Added by setup-deploy ──`, commented out with an empty
167
- placeholder and a one-line comment naming the file that reads it.
168
- Do this only when the consumer chose **merge** in Step 3; on
169
- **skip**, list those names in the checklist instead. Either way,
170
- never silently omit a var the consumer's site reads.
171
- 4. **Show the checklist.** List each section heading with its
172
- variables, marking the ones the file sets, and quote the entry's
173
- own comment for what production needs. Call out every value the
174
- file sets that the consumer must change for their own site — for
175
- example `AILK_PUBLIC_DOMAIN`, and `STEALTH`, which stays `true`
176
- until launch. Use the names exactly as `.env.example` spells
177
- them — do not restate a variable from memory.
178
-
179
- The skill **does not** read live `.env` files. Placeholder values
180
- only. No `sk_live_*`, no `pk_live_*`, no real Resend keys, no real
181
- auth secrets.
182
-
183
- ### Step 5a — Lead delivery
184
-
185
- Where a contact-form message goes is decided by the API project's
186
- environment (RUNBOOK § "Leads — where messages go"). Ask the consumer
187
- two questions:
188
-
189
- 1. **Will this site use a database?** If yes, point them to the
190
- `setup-database` skill — it provisions Postgres, sets
191
- `DATABASE_URL`, runs migrations and verifies a test lead reaches
192
- the inbox. Do not collect a connection string here.
193
- 2. **What email should messages go to?** Ask for
194
- `RESEND_API_KEY`, `AILK_LEAD_NOTIFICATION_TO` (their address) and
195
- `AILK_LEAD_NOTIFICATION_FROM` (a sender on their Resend-verified
196
- domain). All three are needed — a key alone sends nothing.
197
- `.env.example` already lists all three as commented entries; tell
198
- the consumer to set the real values in the **API** project's
199
- dashboard. Never write a key into a file.
200
-
201
- If the consumer provides neither a database nor all three email
202
- values, print this, verbatim:
203
-
204
- > ⚠ No lead destination is set. In production your contact page will show
205
- > your contact email instead of the form, and no messages will be collected.
206
- > Run setup-database, or set RESEND_API_KEY, AILK_LEAD_NOTIFICATION_TO and
207
- > AILK_LEAD_NOTIFICATION_FROM on the API project.
208
-
209
- Then continue — the warning does not stop the deploy config.
210
-
211
- ### Step 6 — Emit `RUNBOOK.md`
212
-
213
- Use the template at `references/runbook-template.md`. The emitted
214
- runbook has these sections:
215
-
216
- 1. **Pre-deploy checklist** — what to set in Vercel's project
217
- dashboard before clicking Deploy.
218
- 2. **Leads — where messages go** — the two lead destinations, in
219
- plain words:
220
- - **Email (default)** — no database; each message is emailed to
221
- the owner (`RESEND_API_KEY`, `AILK_LEAD_NOTIFICATION_TO`,
222
- `AILK_LEAD_NOTIFICATION_FROM` on the API project).
223
- - **Lead inbox (setup-database)** — `DATABASE_URL` set; messages
224
- land in `/leads` and newsletter signups are collected.
225
- With neither, production `/contact` shows the site's contact email
226
- instead of the form. On a non-Vercel host, the API process needs
227
- `NODE_ENV=production`.
228
- 3. **Custom domain + DNS** — pointer at Vercel's domain docs.
229
- 4. **Production callbacks for OAuth** — for consumers who used
230
- `add-auth` and need to register prod redirect URIs.
231
-
232
- After write, grep the RUNBOOK for the two lead-destination headings
233
- (`Email (default)`, `Lead inbox (setup-database)`). Both must be
234
- present. If either is missing, the skill has a bug — flag with the
235
- literal error.
236
-
237
- ### Step 7 — Closing summary
238
-
239
- Print:
240
-
241
- - Which artifacts were emitted vs skipped (per consumer's Step 3
242
- choices).
243
- - Where leads will go (email, the lead inbox, or — with the Step 5a
244
- warning repeated — nowhere), with a pointer to `RUNBOOK.md`'s
245
- "Leads — where messages go" section.
246
- - A reminder that Vercel project-level env-vars need to be set in
247
- the Vercel dashboard before deploy — `.env.example` is the
248
- contract, not the values.
249
- - The next step: `vercel deploy` (or `git push` if the consumer is
250
- using Vercel's GitHub integration).
251
-
252
- ## Re-invocation
253
-
254
- The skill is idempotent. Re-running on a project with all three
255
- artifacts present asks the consumer how to handle each conflict
256
- (per Step 3). A consumer who picks **skip** for all three gets a
257
- no-op run with a "all artifacts already present, nothing to do"
258
- message.
259
-
260
- ## Validation
261
-
262
- Before reporting "done":
263
-
264
- - `vercel.json` parses as JSON ✓
265
- - Every variable the `process.env` scan found is declared in an
266
- `.env.example`, set or commented — or, when the consumer chose
267
- **skip**, listed in the checklist ✓
268
- - `RUNBOOK.md` contains both lead-destination headings
269
- (`Email (default)`, `Lead inbox (setup-database)`) ✓
270
- - Step 5a ran: the consumer chose a lead destination, or saw the
271
- no-destination warning ✓
272
-
273
- If any check fails, surface the failure to the consumer (not silent).
274
-
275
- ## What this skill does NOT validate
276
-
277
- - That the consumer's Vercel project actually deploys. That's
278
- `vercel deploy`'s job.
279
- - That env-var values in the consumer's Vercel dashboard are
280
- correct or live. The skill never reads dashboard state.
281
- - That OAuth callback URLs in `add-auth`'s RUNBOOK are
282
- registered in production OAuth apps. That's the consumer's job
283
- pre-launch.
284
-
285
- ## References
286
-
287
- - `references/vercel-json-template.md` — minimal viable `vercel.json`.
288
- - The scaffold's own `.env.example` at the project root, and the
289
- per-app `apps/*/.env.example` files — the env contract Step 5
290
- reads. This skill keeps no copy of it.
291
- - `references/runbook-template.md` — `RUNBOOK.md` with the two lead
292
- destinations (Email (default), Lead inbox (setup-database)).
293
-
294
- ## Coordination
295
-
296
- - **`setup-database`** — takes a site from no database to a working
297
- lead inbox. Step 5a hands off to it; this skill never collects a
298
- connection string.
299
- - **`add-auth`** — produces its own `RUNBOOK.md`
300
- sections for OAuth setup (dev callback URLs). This skill's
301
- RUNBOOK adds production callback URLs and merges with any
302
- existing runbook content (not blindly overwrites).
303
- - **`@working-theory/env`** — at v1 a placeholder package, not yet a Zod
304
- schema. This skill reads the env contract from the scaffold's
305
- `.env.example`, not from a runtime schema. When `@working-theory/env`
306
- ships its schema, this skill should be revised to read from it.
307
- - **Stripe sandbox-only** — hard-floor enforced globally. Nothing
308
- this skill adds to `.env.example` carries a `sk_live_*` or
309
- `pk_live_*` value.
310
- - **Cloudflare deferred** — neither CF Pages (deploy target) nor
311
- CF KV / D1 / R2 (data layer) is in v1. Will ship as a coherent
312
- follow-up PR.