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.
- package/component-catalog.json +11939 -0
- package/component-catalog.md +176 -64
- package/dist/cli.js +48 -10
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/extract-module-bundle.d.ts +88 -1
- package/dist/lib/extract-module-bundle.js +375 -80
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.d.ts +20 -0
- package/dist/lib/fetch-template.js +133 -0
- package/dist/lib/module-license-gate.d.ts +8 -0
- package/dist/lib/module-license-gate.js +12 -4
- package/dist/module-architecture.d.ts +116 -7
- package/dist/module-architecture.js +465 -94
- package/dist/parse-args.d.ts +8 -1
- package/dist/parse-args.js +49 -2
- package/dist/programmatic.d.ts +36 -1
- package/dist/programmatic.js +216 -5
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +140 -23
- package/dist/template-manifest.d.ts +144 -0
- package/dist/template-manifest.js +337 -0
- package/dist/template-names.d.ts +41 -0
- package/dist/template-names.js +45 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +79 -0
- package/site-templates/_base/content/en/TermsPage/terms.mdx +80 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +331 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +393 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +285 -0
- package/site-templates/marketing-site-free/ailk-template.json +9 -0
- package/site-templates/marketing-site-free/provenance.json +4 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +271 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +324 -0
- package/site-templates/minimalism-site/ailk-template.json +21 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +214 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +32 -7
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/CLAUDE.md +4 -1
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/package.json +6 -2
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +6 -2
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +173 -0
- package/templates/apps/api/src/config/modules.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +339 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +108 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/server.ts +122 -3
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/default-tenant.ts +261 -0
- package/templates/apps/api/src/services/lead-promotion.ts +161 -0
- package/templates/apps/api/src/services/leads-inbox.ts +263 -0
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/mcp/CLAUDE.md +1 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +2 -2
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/package.json +6 -2
- package/templates/apps/mcp/src/server.ts +2 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/index.ts +2 -2
- package/templates/apps/web/.env.example +3 -3
- package/templates/apps/web/CLAUDE.md +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +4 -6
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +214 -17
- package/templates/apps/web/app/[locale]/pricing/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/privacy/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/terms/page.tsx +40 -0
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +12 -15
- package/templates/apps/web/lib/__tests__/server-api.test.ts +61 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +32 -5
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +29 -13
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +46 -0
- package/templates/apps/web/messages/en.json +86 -1
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +6 -2
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the
|
|
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
|
-
* -
|
|
8
|
-
* -
|
|
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
|
|
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 {
|
|
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("
|
|
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
|
-
`[
|
|
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("[
|
|
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
|
|
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("[
|
|
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
|
-
"[
|
|
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
|
|
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("[
|
|
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
|
-
"[
|
|
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
|
|
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("[
|
|
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
|
|
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("[
|
|
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
|
|
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("[
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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": "
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
122
|
+
ailkCreateProject,
|
|
123
123
|
type ScaffoldInput,
|
|
124
124
|
type ScaffoldOutput,
|
|
125
|
-
} from "./
|
|
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 `/
|
|
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]/
|
|
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: `/
|
|
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: `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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/
|
|
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
|
+
});
|