create-ailk 0.3.0 → 0.5.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 (155) hide show
  1. package/component-catalog.json +11939 -0
  2. package/component-catalog.md +176 -64
  3. package/dist/cli.js +48 -10
  4. package/dist/derive-architecture.js +5 -2
  5. package/dist/lib/extract-module-bundle.d.ts +88 -1
  6. package/dist/lib/extract-module-bundle.js +375 -80
  7. package/dist/lib/fetch-guard.d.ts +28 -0
  8. package/dist/lib/fetch-guard.js +101 -0
  9. package/dist/lib/fetch-module.js +4 -0
  10. package/dist/lib/fetch-template.d.ts +20 -0
  11. package/dist/lib/fetch-template.js +133 -0
  12. package/dist/lib/module-license-gate.d.ts +8 -0
  13. package/dist/lib/module-license-gate.js +12 -4
  14. package/dist/module-architecture.d.ts +116 -7
  15. package/dist/module-architecture.js +465 -94
  16. package/dist/parse-args.d.ts +8 -1
  17. package/dist/parse-args.js +49 -2
  18. package/dist/programmatic.d.ts +36 -1
  19. package/dist/programmatic.js +216 -5
  20. package/dist/site-chrome.d.ts +80 -0
  21. package/dist/site-chrome.js +135 -0
  22. package/dist/surfaces.d.ts +21 -6
  23. package/dist/surfaces.js +140 -23
  24. package/dist/template-manifest.d.ts +144 -0
  25. package/dist/template-manifest.js +337 -0
  26. package/dist/template-names.d.ts +41 -0
  27. package/dist/template-names.js +45 -0
  28. package/package.json +10 -2
  29. package/site-templates/_base/ailk-template.json +6 -0
  30. package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
  31. package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +79 -0
  32. package/site-templates/_base/content/en/TermsPage/terms.mdx +80 -0
  33. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
  34. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
  35. package/site-templates/marketing-site/ailk-template.json +11 -0
  36. package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
  37. package/site-templates/marketing-site/provenance.json +4 -0
  38. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
  39. package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
  40. package/site-templates/marketing-site-free/ailk-template.json +9 -0
  41. package/site-templates/marketing-site-free/provenance.json +4 -0
  42. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
  43. package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
  44. package/site-templates/minimalism-site/ailk-template.json +21 -0
  45. package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
  46. package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
  47. package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
  48. package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
  49. package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
  50. package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
  51. package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
  52. package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
  53. package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
  54. package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
  55. package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
  56. package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
  57. package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
  58. package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
  59. package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
  60. package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
  61. package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
  62. package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
  63. package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
  64. package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
  65. package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
  66. package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
  67. package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
  68. package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
  69. package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
  70. package/site-templates/minimalism-site/provenance.json +4 -0
  71. package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
  72. package/site-templates/saas-product/ailk-template.json +16 -0
  73. package/site-templates/saas-product/provenance.json +10 -0
  74. package/templates/.claude/skills/README.md +16 -25
  75. package/templates/.env.example +32 -7
  76. package/templates/.github/workflows/ci.yml +48 -0
  77. package/templates/README.md +3 -1
  78. package/templates/apps/api/CLAUDE.md +4 -1
  79. package/templates/apps/api/eslint.config.mjs +69 -0
  80. package/templates/apps/api/package.json +6 -2
  81. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
  82. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
  83. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +173 -0
  84. package/templates/apps/api/src/config/modules.ts +1 -1
  85. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  86. package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
  87. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
  88. package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
  89. package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
  90. package/templates/apps/api/src/routes/leads/list.ts +104 -0
  91. package/templates/apps/api/src/server.ts +122 -3
  92. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
  93. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
  94. package/templates/apps/api/src/services/default-tenant.ts +261 -0
  95. package/templates/apps/api/src/services/lead-promotion.ts +161 -0
  96. package/templates/apps/api/src/services/leads-inbox.ts +263 -0
  97. package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
  98. package/templates/apps/mcp/CLAUDE.md +1 -1
  99. package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
  100. package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
  101. package/templates/apps/mcp/eslint.config.mjs +69 -0
  102. package/templates/apps/mcp/package.json +6 -2
  103. package/templates/apps/mcp/src/server.ts +2 -2
  104. package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
  105. package/templates/apps/mcp/src/tools/index.ts +2 -2
  106. package/templates/apps/web/.env.example +3 -3
  107. package/templates/apps/web/CLAUDE.md +2 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
  109. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
  110. package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
  111. package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
  112. package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
  113. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
  114. package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
  115. package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
  116. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
  117. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
  118. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
  119. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
  120. package/templates/apps/web/app/[locale]/layout.tsx +214 -17
  121. package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
  122. package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
  123. package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
  124. package/templates/apps/web/components/google-analytics.tsx +18 -1
  125. package/templates/apps/web/eslint.config.mjs +69 -0
  126. package/templates/apps/web/jest.config.cjs +12 -15
  127. package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
  128. package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
  129. package/templates/apps/web/lib/nav-config.ts +27 -7
  130. package/templates/apps/web/lib/server-api.ts +29 -13
  131. package/templates/apps/web/lib/site-chrome.ts +29 -0
  132. package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
  133. package/templates/apps/web/messages/en.json +86 -1
  134. package/templates/apps/web/next.config.mjs +20 -0
  135. package/templates/apps/web/package.json +6 -2
  136. package/templates/apps/web/sentry.client.config.ts +131 -0
  137. package/templates/apps/web/sentry.edge.config.ts +72 -0
  138. package/templates/apps/web/sentry.server.config.ts +72 -0
  139. package/templates/apps/web/src/lib/routes.ts +13 -18
  140. package/templates/brand_assets/README.md +24 -0
  141. package/templates/content/en/AboutPage/about.mdx +160 -15
  142. package/templates/content/en/ContactPage/contact.mdx +90 -13
  143. package/templates/content/en/FAQPage/faq.mdx +62 -22
  144. package/templates/content/en/HomePage/home.mdx +244 -20
  145. package/templates/content/en/LandingPage/blog.mdx +83 -0
  146. package/templates/content/en/LandingPage/features.mdx +103 -0
  147. package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
  148. package/templates/database/CHANGELOG.md +21 -0
  149. package/templates/database/__tests__/seed.test.ts +1 -1
  150. package/templates/database/eslint.config.mjs +69 -0
  151. package/templates/database/package.json +7 -3
  152. package/templates/database/seed.ts +1 -1
  153. package/templates/package.json +1 -14
  154. package/versions.json +52 -0
  155. /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Tests for the ailkScaffold MCP tool handler.
2
+ * Tests for the ailkCreateProject MCP tool handler.
3
3
  *
4
4
  * Acceptance criteria verified here:
5
5
  * - scaffoldInputSchema validates correct inputs
6
6
  * - scaffoldInputSchema rejects invalid inputs (empty projectName, missing targetDir)
7
- * - ailkScaffold scaffolds into a tmp dir: package.json + ≥1 file created
8
- * - ailkScaffold fails cleanly when targetDir is non-empty (rejects with Error)
7
+ * - ailkCreateProject scaffolds into a tmp dir: package.json + ≥1 file created
8
+ * - ailkCreateProject fails cleanly when targetDir is non-empty (rejects with Error)
9
9
  * - Output validates against scaffoldOutputSchema
10
10
  * - No direct create-ailk import in apps/mcp (architectural guard)
11
11
  *
12
12
  * Hermetic: uses os.tmpdir() + unique subdir. No network calls.
13
- * Integration tests pass CLI_PATH explicitly to ailkScaffold (second arg)
13
+ * Integration tests pass CLI_PATH explicitly to ailkCreateProject (second arg)
14
14
  * so they don't depend on process.cwd() being the repo root in Jest.
15
15
  */
16
16
 
@@ -26,7 +26,7 @@ import { join, resolve } from "node:path";
26
26
 
27
27
  import { scaffoldInputSchema, scaffoldOutputSchema } from "@working-theory/validation";
28
28
 
29
- import { ailkScaffold } from "../../src/tools/scaffold.js";
29
+ import { ailkCreateProject } from "../../src/tools/create_project.js";
30
30
 
31
31
  // ── Input schema tests ────────────────────────────────────────────────────────
32
32
 
@@ -103,13 +103,13 @@ describe("scaffoldOutputSchema", () => {
103
103
  const REPO_ROOT = resolve(__dirname, "../../../..");
104
104
  const CLI_PATH = join(REPO_ROOT, "packages/create-ailk/dist/cli.js");
105
105
 
106
- describe("ailkScaffold handler", () => {
106
+ describe("ailkCreateProject handler", () => {
107
107
  let scratch: string;
108
108
 
109
109
  beforeAll(() => {
110
110
  if (!existsSync(CLI_PATH)) {
111
111
  console.warn(
112
- `[scaffold.test] create-ailk CLI not built: ${CLI_PATH} missing. ` +
112
+ `[create_project.test] create-ailk CLI not built: ${CLI_PATH} missing. ` +
113
113
  "Run `pnpm --filter create-ailk build` first.",
114
114
  );
115
115
  }
@@ -125,11 +125,11 @@ describe("ailkScaffold handler", () => {
125
125
 
126
126
  it("scaffolds at least one file into a fresh tmp dir and reports targetDir", async () => {
127
127
  if (!existsSync(CLI_PATH)) {
128
- console.warn("[scaffold.test] skipping integration test — CLI not built");
128
+ console.warn("[create_project.test] skipping integration test — CLI not built");
129
129
  return;
130
130
  }
131
131
  const target = join(scratch, "my-site");
132
- const output = await ailkScaffold(
132
+ const output = await ailkCreateProject(
133
133
  { targetDir: target, projectName: "my-site" },
134
134
  CLI_PATH,
135
135
  );
@@ -139,37 +139,37 @@ describe("ailkScaffold handler", () => {
139
139
 
140
140
  it("scaffolds package.json when full templates are built (build-templates required)", async () => {
141
141
  if (!existsSync(CLI_PATH)) {
142
- console.warn("[scaffold.test] skipping integration test — CLI not built");
142
+ console.warn("[create_project.test] skipping integration test — CLI not built");
143
143
  return;
144
144
  }
145
145
  const TEMPLATES_DIR = join(REPO_ROOT, "packages/create-ailk/templates");
146
146
  const PKG_JSON_IN_TEMPLATES = join(TEMPLATES_DIR, "package.json");
147
147
  if (!existsSync(PKG_JSON_IN_TEMPLATES)) {
148
148
  console.warn(
149
- "[scaffold.test] skipping package.json assertion — run `pnpm --filter create-ailk run build-templates` first",
149
+ "[create_project.test] skipping package.json assertion — run `pnpm --filter create-ailk run build-templates` first",
150
150
  );
151
151
  return;
152
152
  }
153
153
  const target = join(scratch, "my-site");
154
- await ailkScaffold({ targetDir: target, projectName: "my-site" }, CLI_PATH);
154
+ await ailkCreateProject({ targetDir: target, projectName: "my-site" }, CLI_PATH);
155
155
  expect(existsSync(join(target, "package.json"))).toBe(true);
156
156
  });
157
157
 
158
158
  it("scaffolds app dir when full templates are built (build-templates required)", async () => {
159
159
  if (!existsSync(CLI_PATH)) {
160
- console.warn("[scaffold.test] skipping integration test — CLI not built");
160
+ console.warn("[create_project.test] skipping integration test — CLI not built");
161
161
  return;
162
162
  }
163
163
  const TEMPLATES_DIR = join(REPO_ROOT, "packages/create-ailk/templates");
164
164
  const PKG_JSON_IN_TEMPLATES = join(TEMPLATES_DIR, "package.json");
165
165
  if (!existsSync(PKG_JSON_IN_TEMPLATES)) {
166
166
  console.warn(
167
- "[scaffold.test] skipping app dir assertion — run `pnpm --filter create-ailk run build-templates` first",
167
+ "[create_project.test] skipping app dir assertion — run `pnpm --filter create-ailk run build-templates` first",
168
168
  );
169
169
  return;
170
170
  }
171
171
  const target = join(scratch, "my-site");
172
- await ailkScaffold({ targetDir: target, projectName: "my-site" }, CLI_PATH);
172
+ await ailkCreateProject({ targetDir: target, projectName: "my-site" }, CLI_PATH);
173
173
  // The template produces apps/web/app/ from the monorepo template snapshot.
174
174
  const hasApp =
175
175
  existsSync(join(target, "app")) ||
@@ -179,22 +179,22 @@ describe("ailkScaffold handler", () => {
179
179
 
180
180
  it("returns at least one file (schema-typed page seed or README.md)", async () => {
181
181
  if (!existsSync(CLI_PATH)) {
182
- console.warn("[scaffold.test] skipping integration test — CLI not built");
182
+ console.warn("[create_project.test] skipping integration test — CLI not built");
183
183
  return;
184
184
  }
185
185
  const target = join(scratch, "my-site");
186
- await ailkScaffold({ targetDir: target, projectName: "my-site" }, CLI_PATH);
186
+ await ailkCreateProject({ targetDir: target, projectName: "my-site" }, CLI_PATH);
187
187
  // At minimum README.md is always emitted by the template.
188
188
  expect(existsSync(join(target, "README.md"))).toBe(true);
189
189
  });
190
190
 
191
191
  it("returns output conforming to scaffoldOutputSchema", async () => {
192
192
  if (!existsSync(CLI_PATH)) {
193
- console.warn("[scaffold.test] skipping integration test — CLI not built");
193
+ console.warn("[create_project.test] skipping integration test — CLI not built");
194
194
  return;
195
195
  }
196
196
  const target = join(scratch, "my-site");
197
- const output = await ailkScaffold(
197
+ const output = await ailkCreateProject(
198
198
  { targetDir: target, projectName: "my-site" },
199
199
  CLI_PATH,
200
200
  );
@@ -204,7 +204,7 @@ describe("ailkScaffold handler", () => {
204
204
 
205
205
  it("rejects with a clear error when targetDir is non-empty", async () => {
206
206
  if (!existsSync(CLI_PATH)) {
207
- console.warn("[scaffold.test] skipping integration test — CLI not built");
207
+ console.warn("[create_project.test] skipping integration test — CLI not built");
208
208
  return;
209
209
  }
210
210
  const target = join(scratch, "populated");
@@ -215,7 +215,7 @@ describe("ailkScaffold handler", () => {
215
215
  // the target exists and is non-empty. The MCP handler rejects with a
216
216
  // descriptive Error whose message matches the CLI stderr output.
217
217
  await expect(
218
- ailkScaffold({ targetDir: target, projectName: "populated" }, CLI_PATH),
218
+ ailkCreateProject({ targetDir: target, projectName: "populated" }, CLI_PATH),
219
219
  ).rejects.toThrow(/non-empty|exists/i);
220
220
  });
221
221
  });
@@ -223,11 +223,11 @@ describe("ailkScaffold handler", () => {
223
223
  // ── Architectural guard ───────────────────────────────────────────────────────
224
224
 
225
225
  describe("architectural guard — no create-ailk direct import", () => {
226
- it("apps/mcp/src/tools/scaffold.ts does not import create-ailk directly", async () => {
226
+ it("apps/mcp/src/tools/create_project.ts does not import create-ailk directly", async () => {
227
227
  const { readFileSync } = await import("node:fs");
228
228
  const { resolve: res } = await import("node:path");
229
229
  const src = readFileSync(
230
- res(__dirname, "../../src/tools/scaffold.ts"),
230
+ res(__dirname, "../../src/tools/create_project.ts"),
231
231
  "utf-8",
232
232
  );
233
233
  // Must not have a static import of create-ailk or its dist
@@ -0,0 +1,69 @@
1
+ // Self-contained ESLint flat config for this scaffolded package.
2
+ //
3
+ // It deliberately does NOT depend on @working-theory/eslint-config — that
4
+ // package is internal to the AI Launch Kit monorepo and is not published, so
5
+ // it cannot be a dependency of a scaffolded starter. Everything below comes
6
+ // from public npm packages already pinned as devDependencies (see
7
+ // package.json): @eslint/js, globals and typescript-eslint.
8
+ //
9
+ // Source file name (#5556): this file is checked in as `_eslint.config.mjs`,
10
+ // not `eslint.config.mjs` — build-templates.mjs renames it at snapshot time
11
+ // (renameScaffoldLintConfigs()). A file literally named `eslint.config.mjs`
12
+ // anywhere under this monorepo (even one this repo's own root eslint.config.ts
13
+ // lists in its `ignores`) is auto-discovered by ESLint's flat-config nested
14
+ // resolution as a competing config root, which breaks the monorepo's own
15
+ // lint runs (verified live: typescript-eslint's "multiple candidate
16
+ // TSConfigRootDirs" parser error across unrelated packages/create-ailk
17
+ // files). The underscore prefix keeps it invisible to that discovery while
18
+ // it lives in this checkout; only the snapshot in templates/ — which ships
19
+ // to the customer, not this repo — gets the real filename.
20
+ import js from "@eslint/js";
21
+ import globals from "globals";
22
+ import tseslint from "typescript-eslint";
23
+
24
+ export default tseslint.config(
25
+ {
26
+ ignores: [
27
+ "**/.next/**",
28
+ "**/dist/**",
29
+ "**/generated/**",
30
+ "**/node_modules/**",
31
+ ],
32
+ },
33
+ js.configs.recommended,
34
+ ...tseslint.configs.recommended,
35
+ {
36
+ languageOptions: {
37
+ globals: {
38
+ ...globals.node,
39
+ ...globals.browser,
40
+ ...globals.jest,
41
+ },
42
+ // Explicit, self-contained root — this scaffold config is never meant
43
+ // to combine with any sibling scaffold package's config (each ships
44
+ // standalone), and leaving it unset makes typescript-eslint's
45
+ // candidate-root disambiguation ambiguous the moment more than one
46
+ // scaffold package's config resolves in the same ESLint process (e.g.
47
+ // this monorepo's own `templates/` build, where all four exist
48
+ // side by side; a real customer scaffold never has that problem,
49
+ // since it only ever ships and lints ONE of these four at a time).
50
+ parserOptions: {
51
+ tsconfigRootDir: import.meta.dirname,
52
+ },
53
+ },
54
+ rules: {
55
+ // Downgraded rather than off, so both stay visible without failing a
56
+ // fresh scaffold's lint:
57
+ // - unused-vars: most of the source follows the underscore-prefix
58
+ // convention for intentionally-unused bindings, which this rule's
59
+ // default options don't recognize; a handful of test fixtures also
60
+ // carry genuinely-unused locals this minimal config doesn't try to
61
+ // triage file-by-file.
62
+ // - require-imports: CommonJS require() is the correct form in Jest
63
+ // config/mock files and in a few test-only dynamic re-require
64
+ // patterns.
65
+ "@typescript-eslint/no-unused-vars": "warn",
66
+ "@typescript-eslint/no-require-imports": "warn",
67
+ },
68
+ },
69
+ );
@@ -6,7 +6,7 @@
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
8
8
  "scripts": {
9
- "lint": "tsx ../../scripts/lint-package.ts",
9
+ "lint": "eslint .",
10
10
  "build": "tsc -b",
11
11
  "type-check": "tsc --noEmit",
12
12
  "dev": "node --experimental-vm-modules dist/index.js",
@@ -34,6 +34,10 @@
34
34
  "@types/node": "^22.20.0",
35
35
  "jest": "^30.4.2",
36
36
  "ts-jest": "^29.4.11",
37
- "typescript": "^6.0.3"
37
+ "typescript": "^6.0.3",
38
+ "eslint": "^10.6.0",
39
+ "@eslint/js": "^10.0.1",
40
+ "globals": "^17.7.0",
41
+ "typescript-eslint": "^8.62.1"
38
42
  }
39
43
  }
@@ -18,6 +18,7 @@ import { getMcpToolRegistrations, OSS_DISABLED_TOOLS } from "@working-theory/met
18
18
  import { isModuleEnabled } from "./config/modules.js";
19
19
  import { ailkAudit } from "./tools/audit.js";
20
20
  import { ailkCaptureLead } from "./tools/capture_lead.js";
21
+ import { ailkCreateProject } from "./tools/create_project.js";
21
22
  import { ailkGetBusinessHours } from "./tools/get_business_hours.js";
22
23
  import { ailkGetFaq } from "./tools/get_faq.js";
23
24
  import { ailkGetLocations } from "./tools/get_locations.js";
@@ -28,7 +29,6 @@ import { ailkListPages } from "./tools/list_pages.js";
28
29
  import { ailkPurchase } from "./tools/purchase.js";
29
30
  import { ailkReadContent } from "./tools/read_content.js";
30
31
  import { ailkRequestCallback } from "./tools/request_callback.js";
31
- import { ailkScaffold } from "./tools/scaffold.js";
32
32
  import { ailkSearchContent } from "./tools/search_content.js";
33
33
  import { ailkSubscribeNewsletter } from "./tools/subscribe_newsletter.js";
34
34
 
@@ -62,7 +62,7 @@ const HANDLERS: Record<string, (input: unknown) => Promise<unknown>> = {
62
62
  capture_lead: (input) => ailkCaptureLead(input as never),
63
63
  subscribe_newsletter: (input) => ailkSubscribeNewsletter(input as never),
64
64
  request_callback: (input) => ailkRequestCallback(input as never),
65
- scaffold: (input) => ailkScaffold(input as never),
65
+ create_project: (input) => ailkCreateProject(input as never),
66
66
  };
67
67
 
68
68
  type ToolHandler = (input: unknown) => Promise<unknown>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * scaffold — creates an AI Launch Kit starter project via create-ailk.
2
+ * create_project — creates an AI Launch Kit starter project via create-ailk.
3
3
  *
4
4
  * Agent-only: no web Server Action equivalent. Scaffolding is an
5
5
  * orchestration-layer action that only makes sense for an agent driving a
@@ -76,7 +76,7 @@ function countFiles(dir: string): number {
76
76
  }
77
77
 
78
78
  /**
79
- * ailkScaffold handler.
79
+ * ailkCreateProject handler.
80
80
  *
81
81
  * Spawns the create-ailk CLI binary to scaffold a starter project into
82
82
  * targetDir. The binary must be pre-built (`pnpm --filter create-ailk build`).
@@ -84,7 +84,7 @@ function countFiles(dir: string): number {
84
84
  * Accepts an optional `cliBin` override for testing (mirrors the contentRoot
85
85
  * override pattern in ailkAudit).
86
86
  */
87
- export async function ailkScaffold(
87
+ export async function ailkCreateProject(
88
88
  input: ScaffoldInput,
89
89
  cliBin?: string,
90
90
  ): Promise<ScaffoldOutput> {
@@ -119,10 +119,10 @@ export {
119
119
  export {
120
120
  scaffoldInputSchema,
121
121
  scaffoldOutputSchema,
122
- ailkScaffold,
122
+ ailkCreateProject,
123
123
  type ScaffoldInput,
124
124
  type ScaffoldOutput,
125
- } from "./scaffold.js";
125
+ } from "./create_project.js";
126
126
 
127
127
  // The scheduling tools (list_availability, get_event_meta, create_booking),
128
128
  // the content-write tools (create_page, update_page, delete_page), and the
@@ -77,12 +77,12 @@ CONTENT_BACKEND=mdx
77
77
  # =============================================================================
78
78
  # Scheduling — the scheduling module's booking page (#4633)
79
79
  # =============================================================================
80
- # Which event `/schedule` books, as "<owner>/<event-slug>" — the path of the
80
+ # Which event `/meet` books, as "<owner>/<event-slug>" — the path of the
81
81
  # provider's own public booking page. Read server-side in
82
- # app/[locale]/schedule/page.tsx and handed to the `booker` section, which
82
+ # app/[locale]/meet/page.tsx and handed to the `booker` section, which
83
83
  # sends it back to apps/api's /v1/schedule/* routes to name the event.
84
84
  #
85
- # Unset is a supported state: `/schedule` 404s, exactly as if the scheduling
85
+ # Unset is a supported state: `/meet` 404s, exactly as if the scheduling
86
86
  # module were not installed.
87
87
  #
88
88
  # NOT a credential, and deliberately NOT a NEXT_PUBLIC_ variable — the API key
@@ -31,6 +31,8 @@ MDX file → content adapter parses frontmatter → adapter emits `ContentPage`
31
31
 
32
32
  Use `@working-theory/ui` components for content patterns. See `content/CLAUDE.md` for the full vocabulary — it's identical across page types because the rule is about the component, not the page. For the per-page-type editorial map, see `packages/schema/CLAUDE.md` § `@working-theory/ui` component map.
33
33
 
34
+ Site-local components that aren't generic enough for `@working-theory/ui` live in `apps/web/components/` — the one component folder for this app.
35
+
34
36
  Raw HTML is allowed only when no `@working-theory/ui` component fits. Flag it in PR review when you add raw HTML.
35
37
 
36
38
  ## Theme & styling
@@ -12,12 +12,10 @@
12
12
  * email unverified — the profile row already says so) is skipped, never
13
13
  * thrown; every other error rethrows to the route error boundary.
14
14
  *
15
- * Chrome: `PortalShell` hard-codes its /portal nav and accepts only
16
- * `siteName` + `children`, so this composes `AppShell` + `SidebarNav`
17
- * directly — the same structure PortalShell itself uses — with this page's
18
- * own three in-page anchors. The page renders inside the locale layout
19
- * (which owns the marketing Nav/Footer) — the operator's decision; there is
20
- * no second MarketingShell here.
15
+ * Chrome: this page composes `AppShell` + `SidebarNav` directly, with this
16
+ * page's own three in-page anchors. The page renders inside the locale
17
+ * layout (which owns the marketing Nav/Footer) — the operator's decision;
18
+ * there is no second MarketingShell here.
21
19
  */
22
20
 
23
21
  import {
@@ -2,7 +2,7 @@
2
2
  * (authed) segment layout — the operator-dashboard session guard (#4665 D9).
3
3
  *
4
4
  * RSC: reads the active Better Auth session via `auth.api.getSession`
5
- * (mirrors `apps-paid/workspace/app/(authed)/layout.tsx`). With no session it
5
+ * (mirrors `apps-paid/admin/app/(authed)/layout.tsx`). With no session it
6
6
  * renders `SignInGate` — the dashboard's own front door — rather than
7
7
  * redirecting (a `redirect("/sign-in")` would render the visitor's requested
8
8
  * path only as a query param; rendering the gate in place keeps the URL, and
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Tests for the signed-in owner's lead inbox (#5678 Deliverable 2).
3
+ *
4
+ * Covers:
5
+ * 1. Renders one row per lead, with a `null` name shown as the
6
+ * "unavailable" sentinel rather than blank.
7
+ * 2. Renders the forward-only "next" link when `nextCursor` is present, and
8
+ * omits it when it is `null`.
9
+ * 3. Forwards `?cursor=` from `searchParams` to the api-client call.
10
+ * 4. Renders an EmptyState — not a headerless table — for an empty page.
11
+ * 5. Renders ForbiddenState on a 401/403; falls back to the first page
12
+ * when a supplied `?cursor=` 400s; rethrows every other error.
13
+ */
14
+ import { render, screen } from "@testing-library/react";
15
+
16
+ jest.mock("next-intl/server", () => ({
17
+ __esModule: true,
18
+ getTranslations: jest.fn().mockImplementation(async () => (key: string, values?: Record<string, unknown>) =>
19
+ values ? `copy:${key}:${JSON.stringify(values)}` : `copy:${key}`,
20
+ ),
21
+ }));
22
+
23
+ jest.mock("next/headers", () => ({
24
+ __esModule: true,
25
+ headers: jest.fn().mockResolvedValue(new Headers()),
26
+ cookies: jest.fn().mockResolvedValue({ toString: () => "session=abc" }),
27
+ }));
28
+
29
+ const mockList = jest.fn();
30
+ jest.mock("@working-theory/api-client", () => ({
31
+ __esModule: true,
32
+ leads: { list: (...args: unknown[]) => mockList(...args) },
33
+ }));
34
+
35
+ import Page from "../page";
36
+
37
+ const FIXTURE_PAGE = {
38
+ items: [
39
+ {
40
+ id: "lead_1",
41
+ name: "Ada Lovelace",
42
+ email: "ada@example.com",
43
+ source: "web",
44
+ createdAt: "2026-09-01T10:00:00.000Z",
45
+ },
46
+ {
47
+ id: "lead_2",
48
+ name: null,
49
+ email: "anon@example.com",
50
+ source: "conversational-intake",
51
+ createdAt: "2026-08-31T09:00:00.000Z",
52
+ },
53
+ ],
54
+ nextCursor: null as string | null,
55
+ };
56
+
57
+ function renderPage(cursor?: string) {
58
+ return Page({
59
+ params: Promise.resolve({ locale: "en" }),
60
+ searchParams: Promise.resolve(cursor ? { cursor } : {}),
61
+ });
62
+ }
63
+
64
+ describe("LeadsInboxPage", () => {
65
+ beforeEach(() => {
66
+ mockList.mockReset();
67
+ });
68
+
69
+ it("renders one row per lead, with email and source", async () => {
70
+ mockList.mockResolvedValue({ ok: true, body: FIXTURE_PAGE });
71
+ render(await renderPage());
72
+
73
+ expect(screen.getByTestId("leads-inbox-page")).toBeInTheDocument();
74
+ const first = screen.getByTestId("leads-table-row-lead_1");
75
+ expect(first).toHaveTextContent("Ada Lovelace");
76
+ expect(first).toHaveTextContent("ada@example.com");
77
+ expect(first).toHaveTextContent("web");
78
+ expect(screen.getByTestId("leads-table-row-lead_2")).toHaveTextContent(
79
+ "conversational-intake",
80
+ );
81
+ expect(screen.getAllByTestId(/^leads-table-row-/)).toHaveLength(2);
82
+ });
83
+
84
+ it("renders a null name as the unavailable sentinel", async () => {
85
+ mockList.mockResolvedValue({ ok: true, body: FIXTURE_PAGE });
86
+ render(await renderPage());
87
+
88
+ expect(screen.getByTestId("leads-table-row-lead_2")).toHaveTextContent(
89
+ "copy:table.unavailable",
90
+ );
91
+ });
92
+
93
+ it("omits the next link when nextCursor is null", async () => {
94
+ mockList.mockResolvedValue({ ok: true, body: FIXTURE_PAGE });
95
+ render(await renderPage());
96
+
97
+ expect(screen.queryByTestId("leads-table-next")).not.toBeInTheDocument();
98
+ });
99
+
100
+ it("renders the next link pointing at the next cursor", async () => {
101
+ mockList.mockResolvedValue({
102
+ ok: true,
103
+ body: { ...FIXTURE_PAGE, nextCursor: "lead_2" },
104
+ });
105
+ render(await renderPage());
106
+
107
+ const next = screen.getByTestId("leads-table-next");
108
+ expect(next).toBeInTheDocument();
109
+ expect(next).toHaveAttribute("href", "/en/leads?cursor=lead_2");
110
+ });
111
+
112
+ it("forwards ?cursor= to the api-client call", async () => {
113
+ mockList.mockResolvedValue({ ok: true, body: FIXTURE_PAGE });
114
+ render(await renderPage("lead_9"));
115
+
116
+ expect(mockList).toHaveBeenCalledWith(
117
+ { cursor: "lead_9" },
118
+ expect.objectContaining({ headers: expect.objectContaining({ cookie: "session=abc" }) }),
119
+ );
120
+ });
121
+
122
+ it("renders an EmptyState when the owner has no leads", async () => {
123
+ mockList.mockResolvedValue({ ok: true, body: { items: [], nextCursor: null } });
124
+ render(await renderPage());
125
+
126
+ expect(screen.getByTestId("leads-table-empty")).toBeInTheDocument();
127
+ expect(screen.queryByTestId("leads-table")).not.toBeInTheDocument();
128
+ });
129
+
130
+ it("renders ForbiddenState on a 401 rather than throwing", async () => {
131
+ mockList.mockResolvedValue({
132
+ ok: false,
133
+ status: 401,
134
+ body: { error: { code: "unauthorized", message: "Unauthorized" } },
135
+ });
136
+ render(await renderPage());
137
+
138
+ expect(screen.getByTestId("waitlist-forbidden")).toBeInTheDocument();
139
+ });
140
+
141
+ it("rethrows a non-401/403 error", async () => {
142
+ mockList.mockResolvedValue({
143
+ ok: false,
144
+ status: 500,
145
+ body: { error: { code: "internal_error", message: "Boom" } },
146
+ });
147
+ await expect(renderPage()).rejects.toThrow();
148
+ });
149
+
150
+ // PR #5705 security review, finding 3.
151
+ it("falls back to the first page when a supplied ?cursor= 400s", async () => {
152
+ mockList
153
+ .mockResolvedValueOnce({
154
+ ok: false,
155
+ status: 400,
156
+ body: {
157
+ error: {
158
+ code: "validation_failed",
159
+ message: "cursor does not resolve within this scope",
160
+ },
161
+ },
162
+ })
163
+ .mockResolvedValueOnce({ ok: true, body: FIXTURE_PAGE });
164
+
165
+ render(await renderPage("lead_gone"));
166
+
167
+ expect(screen.getByTestId("leads-table")).toBeInTheDocument();
168
+ expect(mockList).toHaveBeenNthCalledWith(1, { cursor: "lead_gone" }, expect.anything());
169
+ expect(mockList).toHaveBeenNthCalledWith(2, {}, expect.anything());
170
+ });
171
+
172
+ it("rethrows a 400 on the FIRST page — there is no cursor to blame", async () => {
173
+ mockList.mockResolvedValue({
174
+ ok: false,
175
+ status: 400,
176
+ body: { error: { code: "validation_failed", message: "Invalid query parameters" } },
177
+ });
178
+ await expect(renderPage()).rejects.toThrow();
179
+ expect(mockList).toHaveBeenCalledTimes(1);
180
+ });
181
+ });