create-ailk 0.29.0 → 0.30.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 (46) hide show
  1. package/README.md +0 -5
  2. package/dist/add-route.js +1 -0
  3. package/dist/cli.js +15 -1
  4. package/dist/lib/env-catalog.js +1 -1
  5. package/dist/lib/import-mdx.js +18 -9
  6. package/dist/lib/scaffold-render-sweep.d.ts +1 -1
  7. package/dist/lib/scaffold-render-sweep.js +1 -1
  8. package/dist/module-architecture.js +6 -1
  9. package/dist/provision-env.d.ts +28 -0
  10. package/dist/provision-env.js +210 -0
  11. package/dist/surfaces.js +3 -0
  12. package/dist/topology.js +11 -0
  13. package/package.json +3 -3
  14. package/templates/.env.example +7 -0
  15. package/templates/apps/api/CLAUDE.md +7 -3
  16. package/templates/apps/mcp/.env.example +20 -0
  17. package/templates/apps/mcp/CLAUDE.md +42 -8
  18. package/templates/apps/web/CLAUDE.md +24 -1
  19. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +9 -1
  20. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +58 -0
  21. package/templates/apps/web/app/.well-known/mcp.json/route.ts +16 -0
  22. package/templates/apps/web/app/.well-known/oauth-authorization-server/route.ts +53 -0
  23. package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +8 -1
  24. package/templates/apps/web/app/[locale]/(auth)/consent/__tests__/consent-actions.test.ts +56 -0
  25. package/templates/apps/web/app/[locale]/(auth)/consent/__tests__/page.test.tsx +164 -0
  26. package/templates/apps/web/app/[locale]/(auth)/consent/consent-form.tsx +132 -0
  27. package/templates/apps/web/app/[locale]/(auth)/consent/consent.actions.ts +51 -0
  28. package/templates/apps/web/app/[locale]/(auth)/consent/page.tsx +144 -0
  29. package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
  30. package/templates/apps/web/app/api/auth/hub/continuation.ts +8 -3
  31. package/templates/apps/web/app/api/auth/mcp/__tests__/continuation.test.ts +92 -0
  32. package/templates/apps/web/app/api/auth/mcp/continuation.ts +89 -0
  33. package/templates/apps/web/app/api/auth/mcp/continue/[ref]/__tests__/route.test.ts +59 -0
  34. package/templates/apps/web/app/api/auth/mcp/continue/[ref]/route.ts +41 -0
  35. package/templates/apps/web/app/llms.txt/route.ts +10 -0
  36. package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +40 -0
  37. package/templates/apps/web/app/openapi.json/route.ts +4 -0
  38. package/templates/apps/web/app/robots.txt/route.ts +18 -1
  39. package/templates/apps/web/app/sitemap.xml/route.ts +10 -0
  40. package/templates/apps/web/auth.ts +184 -7
  41. package/templates/apps/web/lib/__tests__/auth-routes.test.ts +2 -1
  42. package/templates/apps/web/lib/auth-routes.ts +5 -3
  43. package/templates/apps/web/lib/mcp-oauth.ts +702 -0
  44. package/templates/apps/web/messages/en.json +42 -13
  45. package/templates/database/seed.ts +6 -0
  46. package/versions.json +48 -48
package/README.md CHANGED
@@ -592,11 +592,6 @@ command above. A site with no Admin, or a spoke, prints nothing about it. A
592
592
  seed that fails or is refused at scaffold time prints why and never fails the
593
593
  scaffold; `seed-workspace` is where a seed's result is an exit code.
594
594
 
595
- On a site carrying `signal-collectors`, the two `ThankYouPage` seeds
596
- `contact` and `newsletter` share an address (locale and slug) with the
597
- `ContactPage` and `NewsletterSignup` pages, so those two are reported
598
- `failed` and the seed exits 1 until one side's slug changes.
599
-
600
595
  To check a seed:
601
596
 
602
597
  ```bash
package/dist/add-route.js CHANGED
@@ -336,6 +336,7 @@ const RESERVED_FIRST_SEGMENTS = new Set([
336
336
  "forgot-password",
337
337
  "reset-password",
338
338
  "verify-email",
339
+ "consent",
339
340
  ]);
340
341
  /** A first segment next-intl would read as a locale prefix. */
341
342
  const LOCALE_SHAPED_RE = /^[a-z]{2}(-[a-z]{2})?$/;
package/dist/cli.js CHANGED
@@ -27,6 +27,8 @@
27
27
  * print the report, exit (1 iff routes are missing in check mode)
28
28
  * on a `license set` subcommand: write the key into the scaffold, push it
29
29
  * to a linked Vercel project and redeploy, exit (#6683 D9)
30
+ * on a `provision-env` subcommand: set MCP_PUBLIC_HTTP_URL (and apps/mcp's
31
+ * BETTER_AUTH_URL) on the linked Vercel projects, exit (#10392 D9)
30
32
  * on an `apply-design` subcommand: rewrite an existing site's look,
31
33
  * print the changed-file report, exit (#6819)
32
34
  * on an `update` subcommand: bring template fixes into a site's
@@ -76,6 +78,7 @@ import { LICENSE_SET_HELP, runLicenseSet } from "./license-set.js";
76
78
  import { MIGRATE_CONTENT_HELP, MigrateContentRefusedError, migrateContent, parseMigrateContentArgs, } from "./migrate-content.js";
77
79
  import { PLUGIN_INSTALL_LINES, ParseError, helpText, parseCliArgs, parseModules, resolveCliShape, toScaffoldOptions, } from "./parse-args.js";
78
80
  import { addModule, removeModule, formatSurfaceDrift, scaffold, syncArchitectureSync, syncCatalogSync, syncClaudeMdSync, syncRoutesSync, } from "./programmatic.js";
81
+ import { PROVISION_ENV_HELP, runProvisionEnv } from "./provision-env.js";
79
82
  import { REMOVE_MODULE_HELP, RemoveModuleRefusedError, formatRemoveModuleReport, parseRemoveModuleArgs, } from "./remove-module.js";
80
83
  import { SEED_FLOWS_HELP, SeedFlowsRefusedError, parseSeedFlowsArgs, seedFlows, } from "./seed-flows.js";
81
84
  import { SEED_WORKSPACE_HELP, SeedWorkspaceRefusedError, parseSeedWorkspaceArgs, runScaffoldSeed, seedWorkspace, } from "./seed-workspace.js";
@@ -374,6 +377,15 @@ async function main() {
374
377
  readStdin: () => readFileSync(0, "utf8"),
375
378
  }));
376
379
  }
380
+ // `provision-env --web-origin <o> --mcp-origin <o> [dir]` (#10392 D9): its
381
+ // own argv shape, beside `license set`. Values reach `vercel` on stdin only.
382
+ if (argv[0] === "provision-env") {
383
+ process.exit(runProvisionEnv(argv.slice(1), {
384
+ cwd: process.cwd(),
385
+ stdout: (text) => process.stdout.write(text),
386
+ stderr: (text) => process.stderr.write(text),
387
+ }));
388
+ }
377
389
  // `apply-design <name|path> [--target <dir>]` (#6819): rewrite an existing
378
390
  // site's look only. Every refusal — the licence gate included — reaches
379
391
  // the top-level catch, exit 1, as a scaffold-time design refusal does.
@@ -476,7 +488,9 @@ async function main() {
476
488
  "\n" +
477
489
  UPDATE_HELP +
478
490
  "\n" +
479
- LICENSE_SET_HELP);
491
+ LICENSE_SET_HELP +
492
+ "\n" +
493
+ PROVISION_ENV_HELP);
480
494
  process.exit(0);
481
495
  }
482
496
  if (opts.version) {
@@ -164,7 +164,7 @@ export const ENV_CATALOG = {
164
164
  secret: false,
165
165
  },
166
166
  MCP_PUBLIC_HTTP_URL: {
167
- description: "The public URL of the MCP HTTP transport, advertised by /.well-known/mcp.json.",
167
+ description: "The public URL of the MCP HTTP transport (apps/mcp's `/mcp`), advertised by /.well-known/mcp.json; with BETTER_AUTH_URL it turns on the OAuth metadata. Set by create-ailk provision-env.",
168
168
  secret: false,
169
169
  },
170
170
  MCP_TRANSPORT: {
@@ -4,7 +4,7 @@
4
4
  * Reads a site's default-locale MDX pages through the site's own MDX adapter
5
5
  * and writes each one as a `text/page` Artifact through the tenant-guarded
6
6
  * workspace content routes. Per page (D3): read the draft; create on 404;
7
- * PATCH under `If-Match` when the content differs; write nothing when it is
7
+ * PATCH under `X-Draft-Version` (#10264) when the content differs; write nothing when it is
8
8
  * the same — so a second run over the same files creates nothing and reports
9
9
  * every page `unchanged`. A file the adapter rejects is `failed` with its path
10
10
  * and reason and the run goes on. The seed step (U6b) and `migrate-content`
@@ -204,7 +204,10 @@ async function call(t, method, url, body, ifMatch) {
204
204
  headers: {
205
205
  authorization: `Bearer ${t.key}`,
206
206
  ...(body !== undefined && { "content-type": "application/json" }),
207
- ...(ifMatch !== undefined && { "if-match": ifMatch }),
207
+ // #10264 — the version rides `x-draft-version`; `if-match` rides
208
+ // beside it for one release so this importer still saves against an
209
+ // API that predates the new header (the route prefers the new one).
210
+ ...(ifMatch !== undefined && { "x-draft-version": ifMatch, "if-match": ifMatch }),
208
211
  },
209
212
  ...(body !== undefined && { body: JSON.stringify(body) }),
210
213
  // A redirect would re-send the key and the body to another URL.
@@ -304,9 +307,12 @@ async function writePage(t, page, locale, content) {
304
307
  if (created.status === 201) {
305
308
  return wrongLocale(path, created, locale) ?? done("created");
306
309
  }
307
- // 409: created by someone else since the read — read it once more.
308
- if (created.status !== 409)
310
+ // 409 slug_taken: another type holds this page's registry link (#10268
311
+ // D5) — reported as it is, since a re-read at this address finds nothing.
312
+ // Any other 409: created by someone else since the read — read it once more.
313
+ if (created.status !== 409 || created.code === "slug_taken") {
309
314
  return refusedAnswer(path, created);
315
+ }
310
316
  }
311
317
  }
312
318
  // ── The report (D6) ─────────────────────────────────────────────────────────
@@ -348,21 +354,24 @@ export async function importMdx(opts) {
348
354
  outcomes.push(outcome);
349
355
  onLine(outcomeLine(outcome));
350
356
  };
351
- // Two pages at one (locale, slug) address would overwrite each other on
352
- // every run, so the first in processing order wins and the rest fail.
357
+ // Two pages at one (locale, type, slug) address would overwrite each other
358
+ // on every run — the legacy and the locale-tree copy of one page — so the
359
+ // first in processing order wins and the rest fail. Two pages of different
360
+ // types may share a slug (#10268 D7): the store addresses by type too.
353
361
  const addresses = new Map();
354
362
  let halt;
355
363
  for (const page of pages) {
356
- const holder = addresses.get(page.slug);
364
+ const address = `${page.type}/${page.slug}`;
365
+ const holder = addresses.get(address);
357
366
  if (holder !== undefined) {
358
367
  record({
359
368
  path: page.path,
360
369
  result: "failed",
361
- reason: `same address as ${holder} (a page is addressed by locale and slug)`,
370
+ reason: `same address as ${holder} (a page is addressed by locale, type and slug)`,
362
371
  });
363
372
  continue;
364
373
  }
365
- addresses.set(page.slug, page.path);
374
+ addresses.set(address, page.path);
366
375
  if (halt !== undefined) {
367
376
  record({ path: page.path, result: "failed", reason: halt });
368
377
  continue;
@@ -143,7 +143,7 @@ export declare function assessConfigGatedRoute(input: {
143
143
  * The environment the sitemap sweep's server runs under (#8501): `env` with
144
144
  * every `CONFIG_GATED_ROUTES` variable removed.
145
145
  *
146
- * `/sitemap.xml` is prerendered at `next build`, from the content seeds, so it
146
+ * `/sitemap.xml` is rendered on request, from the content seeds, so it
147
147
  * describes the scaffold AS GENERATED — and a config-gated route's seeds are
148
148
  * served only while its variable is unset (the Free placeholder hand-off,
149
149
  * #6942). Set it and the route serves its live module page instead: `/meet`
@@ -285,7 +285,7 @@ function headingTexts(html) {
285
285
  * The environment the sitemap sweep's server runs under (#8501): `env` with
286
286
  * every `CONFIG_GATED_ROUTES` variable removed.
287
287
  *
288
- * `/sitemap.xml` is prerendered at `next build`, from the content seeds, so it
288
+ * `/sitemap.xml` is rendered on request, from the content seeds, so it
289
289
  * describes the scaffold AS GENERATED — and a config-gated route's seeds are
290
290
  * served only while its variable is unset (the Free placeholder hand-off,
291
291
  * #6942). Set it and the route serves its live module page instead: `/meet`
@@ -1810,7 +1810,11 @@ export function coOwnedModuleDestinations() {
1810
1810
  // `review-surface-registration.test.ts` and
1811
1811
  // `workspace-module-registration.test.ts` (each a matrix over module
1812
1812
  // sets), and mcp's `parity.test.ts` (counts the Server Actions and
1813
- // tool handlers of the FULL tree, which a stripped scaffold lacks).
1813
+ // tool handlers of the FULL tree, which a stripped scaffold lacks),
1814
+ // and mcp's `workspace-oauth.test.ts` (#10482 — its AC-8 cases call
1815
+ // `get_agent_context` on `/mcp/workspace`, a tool only the
1816
+ // `@working-theory/mcp-workspace` pack ships, and the `workspace`,
1817
+ // `multi-tenant` and `signal-collectors` slices each carry that pack).
1814
1818
  // The exclusion is per file, not a `testPathIgnorePatterns` entry in the
1815
1819
  // scaffold's jest config: that would hide these suites from a keyed
1816
1820
  // scaffold too, and from the suites a slice owns.
@@ -1853,6 +1857,7 @@ export const EXCLUDE_TEMPLATE_DIRS = [
1853
1857
  "apps/api/src/__tests__/hub-install.test.ts",
1854
1858
  "apps/mcp/__tests__/workspace-surface.test.ts",
1855
1859
  "apps/mcp/__tests__/parity.test.ts",
1860
+ "apps/mcp/__tests__/workspace-oauth.test.ts",
1856
1861
  "apps/web/app/[locale]/pricing/__tests__/monorepo-only.test.tsx",
1857
1862
  "apps/web/app/[locale]/pricing/__tests__/__snapshots__/monorepo-only.test.tsx.snap",
1858
1863
  "apps/web/lib/__tests__/hub-children.test.tsx",
@@ -0,0 +1,28 @@
1
+ import { type LicenseSetRunner } from "./license-set.js";
2
+ /** The production variables each deployed app needs (#10392 D9). */
3
+ export declare function provisionEnv(opts: {
4
+ webOrigin: string;
5
+ mcpOrigin: string;
6
+ }): {
7
+ web: {
8
+ MCP_PUBLIC_HTTP_URL: string;
9
+ };
10
+ mcp: {
11
+ MCP_PUBLIC_HTTP_URL: string;
12
+ BETTER_AUTH_URL: string;
13
+ };
14
+ };
15
+ export interface ProvisionEnvIo {
16
+ /** Resolves a relative or absent `[dir]`. */
17
+ cwd: string;
18
+ stdout: (text: string) => void;
19
+ stderr: (text: string) => void;
20
+ /** Defaults to `license set`'s runner: `vercel`, never through a shell. */
21
+ runner?: LicenseSetRunner;
22
+ }
23
+ export declare const PROVISION_ENV_HELP = "create-ailk provision-env \u2014 set a deployed site's MCP discovery and OAuth variables.\n\nUsage:\n pnpm create ailk@latest provision-env --web-origin <origin> --mcp-origin <origin> [<target-dir>]\n\nArguments:\n --web-origin <origin> The site's public origin (apps/web), e.g. https://example.com\n --mcp-origin <origin> The MCP server's public origin (apps/mcp), e.g. https://mcp.example.com\n <target-dir> An already-scaffolded repo (contains\n architecture.yaml). Defaults to the current dir.\n\nSets MCP_PUBLIC_HTTP_URL on apps/web and apps/mcp, and BETTER_AUTH_URL on\napps/mcp, in each app's linked Vercel production environment. The variables\npersist across deploys. An app with no Vercel link gets the values printed to\nset on your host yourself. Re-run it whenever an origin changes.\n";
24
+ /**
25
+ * Run `provision-env` with `argv` = everything after the `provision-env` word.
26
+ * Returns the process exit code; never throws for an expected failure.
27
+ */
28
+ export declare function runProvisionEnv(argv: readonly string[], io: ProvisionEnvIo): number;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * `create-ailk provision-env --web-origin <o> --mcp-origin <o> [dir]` — set the
3
+ * production variables a deployed site needs for MCP discovery and OAuth
4
+ * (#10392 D9).
5
+ *
6
+ * `/.well-known/mcp.json` advertises the MCP HTTP endpoint only when
7
+ * `MCP_PUBLIC_HTTP_URL` is set (#7396), and names the OAuth metadata only when
8
+ * `BETTER_AUTH_URL` is set beside it (D8); apps/mcp turns its OAuth branch on
9
+ * only with both (#10391). This step derives every value from the two origins
10
+ * and puts them on each Vercel-linked app's production environment, so a
11
+ * deployment advertises its MCP endpoint without hand configuration:
12
+ *
13
+ * apps/web MCP_PUBLIC_HTTP_URL = <mcp-origin>/mcp
14
+ * apps/mcp MCP_PUBLIC_HTTP_URL = <mcp-origin>/mcp
15
+ * BETTER_AUTH_URL = <web-origin>
16
+ *
17
+ * 1. The target (default: cwd) must contain `architecture.yaml` — the
18
+ * `license set` precondition — else exit 1 with nothing run.
19
+ * 2. For each of `<dir>/apps/web` and `<dir>/apps/mcp` holding
20
+ * `.vercel/project.json`: print its `projectId`, then per variable
21
+ * `vercel env rm <NAME> production --yes` (failure tolerated: it may not
22
+ * exist yet) and `vercel env add <NAME> production`, the value on STDIN.
23
+ * No redeploy: the variables persist, and the next deploy reads them.
24
+ * 3. An app with no link → its `NAME=value` lines are printed, to set by
25
+ * hand. `vercel` missing → exit 1 with the manual commands. A failed
26
+ * `env add` → exit 1 naming the variable now unset, as `license set` does.
27
+ *
28
+ * A value is never an argv element of a spawned process. Local `.env.local`
29
+ * files are deliberately not given an MCP URL: locally apps/mcp defaults to
30
+ * stdio, so a loopback URL would advertise an endpoint nothing serves.
31
+ *
32
+ * Pure module: output and the process runner are injected, so jest drives it.
33
+ */
34
+ import { existsSync, readFileSync } from "node:fs";
35
+ import { join, resolve } from "node:path";
36
+ import { defaultRunner } from "./license-set.js";
37
+ /** apps/mcp's default `MCP_HTTP_PATH`. */
38
+ const MCP_PATH = "/mcp";
39
+ const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
40
+ /** The production variables each deployed app needs (#10392 D9). */
41
+ export function provisionEnv(opts) {
42
+ const webOrigin = assertOrigin(opts.webOrigin, "--web-origin");
43
+ const mcpOrigin = assertOrigin(opts.mcpOrigin, "--mcp-origin");
44
+ const mcpUrl = `${mcpOrigin}${MCP_PATH}`;
45
+ return {
46
+ web: { MCP_PUBLIC_HTTP_URL: mcpUrl },
47
+ mcp: { MCP_PUBLIC_HTTP_URL: mcpUrl, BETTER_AUTH_URL: webOrigin },
48
+ };
49
+ }
50
+ /**
51
+ * The origin `value` names: an absolute `https:` URL, or `http:` on a loopback
52
+ * host, with no path, query or fragment. Throws naming the flag otherwise.
53
+ */
54
+ function assertOrigin(value, flag) {
55
+ let url;
56
+ try {
57
+ url = new URL(value);
58
+ }
59
+ catch {
60
+ throw new ProvisionEnvError(`${flag} must be an absolute URL, e.g. https://example.com`);
61
+ }
62
+ const secure = url.protocol === "https:" ||
63
+ (url.protocol === "http:" && LOOPBACK_HOSTS.includes(url.hostname));
64
+ if (!secure) {
65
+ throw new ProvisionEnvError(`${flag} must be https (http is allowed only on localhost)`);
66
+ }
67
+ // A host name only: letters, digits, dots and hyphens (or a bracketed IPv6
68
+ // loopback). The URL parser keeps shell characters in a host, and the value
69
+ // is printed in a copyable manual command.
70
+ if (!/^(?:[A-Za-z0-9.-]+|\[::1\])$/.test(url.hostname)) {
71
+ throw new ProvisionEnvError(`${flag} must name a plain host (letters, digits, dots, hyphens)`);
72
+ }
73
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "") {
74
+ throw new ProvisionEnvError(`${flag} must be an origin only — no path, query or fragment`);
75
+ }
76
+ return url.origin;
77
+ }
78
+ class ProvisionEnvError extends Error {
79
+ }
80
+ export const PROVISION_ENV_HELP = `create-ailk provision-env — set a deployed site's MCP discovery and OAuth variables.
81
+
82
+ Usage:
83
+ pnpm create ailk@latest provision-env --web-origin <origin> --mcp-origin <origin> [<target-dir>]
84
+
85
+ Arguments:
86
+ --web-origin <origin> The site's public origin (apps/web), e.g. https://example.com
87
+ --mcp-origin <origin> The MCP server's public origin (apps/mcp), e.g. https://mcp.example.com
88
+ <target-dir> An already-scaffolded repo (contains
89
+ architecture.yaml). Defaults to the current dir.
90
+
91
+ Sets MCP_PUBLIC_HTTP_URL on apps/web and apps/mcp, and BETTER_AUTH_URL on
92
+ apps/mcp, in each app's linked Vercel production environment. The variables
93
+ persist across deploys. An app with no Vercel link gets the values printed to
94
+ set on your host yourself. Re-run it whenever an origin changes.
95
+ `;
96
+ const APPS = [
97
+ { dir: join("apps", "web"), key: "web" },
98
+ { dir: join("apps", "mcp"), key: "mcp" },
99
+ ];
100
+ function parseProvisionEnvArgs(argv) {
101
+ const parsed = {
102
+ help: false,
103
+ webOrigin: undefined,
104
+ mcpOrigin: undefined,
105
+ target: undefined,
106
+ };
107
+ for (let i = 0; i < argv.length; i += 1) {
108
+ const arg = argv[i];
109
+ if (arg === "--help" || arg === "-h")
110
+ parsed.help = true;
111
+ else if (arg === "--web-origin")
112
+ parsed.webOrigin = argv[++i];
113
+ else if (arg === "--mcp-origin")
114
+ parsed.mcpOrigin = argv[++i];
115
+ else if (arg.startsWith("-"))
116
+ throw new ProvisionEnvError(`unknown option ${arg}`);
117
+ else if (parsed.target === undefined)
118
+ parsed.target = arg;
119
+ else
120
+ throw new ProvisionEnvError("expected at most one target directory");
121
+ }
122
+ return parsed;
123
+ }
124
+ function readProjectId(dir) {
125
+ try {
126
+ const raw = JSON.parse(readFileSync(join(dir, ".vercel", "project.json"), "utf8"));
127
+ if (typeof raw.projectId === "string" && raw.projectId.length > 0)
128
+ return raw.projectId;
129
+ }
130
+ catch {
131
+ // fall through
132
+ }
133
+ throw new ProvisionEnvError(`${join(dir, ".vercel", "project.json")} has no readable projectId; run \`vercel link\` there, then re-run.`);
134
+ }
135
+ const envRm = (name) => ["env", "rm", name, "production", "--yes"];
136
+ const envAdd = (name) => ["env", "add", name, "production"];
137
+ /** The commands that set `vars` by hand, from `dir`. */
138
+ function manualCommands(dir, vars) {
139
+ return [
140
+ ` cd '${dir}'`,
141
+ ...Object.entries(vars).map(([name, value]) => ` printf '%s' '${value}' | vercel ${envAdd(name).join(" ")}`),
142
+ ].join("\n");
143
+ }
144
+ /**
145
+ * Run `provision-env` with `argv` = everything after the `provision-env` word.
146
+ * Returns the process exit code; never throws for an expected failure.
147
+ */
148
+ export function runProvisionEnv(argv, io) {
149
+ const runner = io.runner ?? defaultRunner;
150
+ const fail = (message) => {
151
+ io.stderr(`[create-ailk] provision-env: ${message}\n`);
152
+ return 1;
153
+ };
154
+ let args;
155
+ let values;
156
+ try {
157
+ args = parseProvisionEnvArgs(argv);
158
+ if (args.help) {
159
+ io.stdout(PROVISION_ENV_HELP);
160
+ return 0;
161
+ }
162
+ if (args.webOrigin === undefined || args.mcpOrigin === undefined) {
163
+ throw new ProvisionEnvError(`--web-origin and --mcp-origin are both required.\n\n${PROVISION_ENV_HELP}`);
164
+ }
165
+ values = provisionEnv({ webOrigin: args.webOrigin, mcpOrigin: args.mcpOrigin });
166
+ }
167
+ catch (err) {
168
+ if (err instanceof ProvisionEnvError)
169
+ return fail(err.message);
170
+ throw err;
171
+ }
172
+ const target = resolve(io.cwd, args.target ?? ".");
173
+ if (!existsSync(join(target, "architecture.yaml"))) {
174
+ return fail(`not a scaffolded repo: no architecture.yaml at ${target}. Run it from your site's root, or pass the site's directory.`);
175
+ }
176
+ for (const app of APPS) {
177
+ const dir = join(target, app.dir);
178
+ const vars = values[app.key];
179
+ if (!existsSync(join(dir, ".vercel", "project.json"))) {
180
+ io.stdout(`[create-ailk] ${app.dir} has no Vercel link (.vercel/project.json); set these on its host yourself:\n${Object.entries(vars)
181
+ .map(([name, value]) => ` ${name}=${value}`)
182
+ .join("\n")}\n`);
183
+ continue;
184
+ }
185
+ let projectId;
186
+ try {
187
+ projectId = readProjectId(dir);
188
+ }
189
+ catch (err) {
190
+ return fail(err.message);
191
+ }
192
+ io.stdout(`[create-ailk] Setting ${Object.keys(vars).join(", ")} (production) on Vercel project ${projectId}, linked at ${dir}\n`);
193
+ for (const [name, value] of Object.entries(vars)) {
194
+ const rm = runner(envRm(name), { cwd: dir });
195
+ if (rm.error?.code === "ENOENT") {
196
+ return fail(`the vercel CLI was not found; nothing was pushed. Install it (npm i -g vercel), or set them by hand:\n${manualCommands(dir, vars)}`);
197
+ }
198
+ const removed = rm.error === undefined && rm.status === 0;
199
+ const add = runner(envAdd(name), { cwd: dir, input: value });
200
+ if (add.error !== undefined || add.status !== 0) {
201
+ const state = removed
202
+ ? `The production variable ${name} is now UNSET there (the old value was removed).`
203
+ : `The production variable ${name} was not changed.`;
204
+ return fail(`\`vercel ${envAdd(name).join(" ")}\` failed on project ${projectId}. ${state} Finish with:\n${manualCommands(dir, { [name]: value })}`);
205
+ }
206
+ }
207
+ }
208
+ io.stdout("[create-ailk] MCP discovery and OAuth variables set; the next deploy reads them.\n");
209
+ return 0;
210
+ }
package/dist/surfaces.js CHANGED
@@ -818,6 +818,8 @@ export const ROUTE_SINCE = {
818
818
  "(auth)/forgot-password": "0.24.0",
819
819
  "(auth)/reset-password": "0.24.0",
820
820
  "(auth)/verify-email": "0.24.0",
821
+ // #10357 D13 — the MCP consent step.
822
+ "(auth)/consent": "0.53.0",
821
823
  // #5469 D2 — the generic `[slug]` content route. A genuinely new route
822
824
  // directory, so it attaches at today's fixed-group version, the same rule
823
825
  // #5479, #5371 and #5048 applied.
@@ -1210,6 +1212,7 @@ export const ROUTE_MESSAGE_NAMESPACES = {
1210
1212
  "(auth)/forgot-password": ["auth"],
1211
1213
  "(auth)/reset-password": ["auth"],
1212
1214
  "(auth)/verify-email": ["auth"],
1215
+ "(auth)/consent": ["mcpConsent"],
1213
1216
  "(authed)/account": ["account"],
1214
1217
  "(authed)/account/inbox/[kind]/[id]": ["account"],
1215
1218
  "(authed)/account/settings": ["account"],
package/dist/topology.js CHANGED
@@ -63,8 +63,12 @@ export const SPOKE_HUB_CHECKOUT_SEEDS = [
63
63
  export const SPOKE_REMOVAL_SEEDS = [
64
64
  "apps/web/auth.ts",
65
65
  "apps/web/lib/prisma-for-auth.ts",
66
+ // The MCP authorization server (#10357 D10): a spoke has no auth.ts to
67
+ // install it, and the root discovery route reads it.
68
+ "apps/web/lib/mcp-oauth.ts",
66
69
  "apps/web/lib/license-key-store.ts",
67
70
  "apps/web/app/api/auth",
71
+ "apps/web/app/.well-known/oauth-authorization-server",
68
72
  "apps/web/app/[locale]/(auth)",
69
73
  "apps/web/app/[locale]/(authed)",
70
74
  ...SPOKE_HUB_CHECKOUT_SEEDS,
@@ -949,6 +953,13 @@ export function renderSpokeHardFloors(opts) {
949
953
  " # Stripe webhook route, the checkout route, its price map and its e2e",
950
954
  " # workflow, and the sign-in paths are floored so their arrival here is a",
951
955
  " # human merge.",
956
+ " # Provenance (claude-setup#11353, claude-setup PR #11367): a `paths:` floor",
957
+ " # below fires only when the touched file is site-edited or not template-owned.",
958
+ " # A file whose PR-head bytes match its `.ailk-template-files.json` row AND the",
959
+ " # published create-ailk template at that row's version is exempt, so a",
960
+ " # `create-ailk update` that moves a template-owned file merges unattended.",
961
+ " # Never for a high-cost floor (security_settings, payments), never to an older",
962
+ " # template version, and only while the record already exists on main.",
952
963
  " - kind: pricing",
953
964
  " - kind: stripe_live_mode",
954
965
  " - paths:",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ailk",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,8 +28,8 @@
28
28
  "tar": "^7.5.21"
29
29
  },
30
30
  "devDependencies": {
31
- "@working-theory/schema": "0.51.0",
32
- "@working-theory/templates": "0.51.0"
31
+ "@working-theory/schema": "0.53.0",
32
+ "@working-theory/templates": "0.53.0"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",
@@ -606,6 +606,13 @@ CONTENT_ADAPTER=mdx
606
606
  # docs/runbooks/site-configuration.md § Seeding a spoke's flows.
607
607
  # AILK_SPOKE_FLOW_SEED=
608
608
  #
609
+ # HUB (apps/api, optional) — the always-loaded set's token budget (#10504):
610
+ # every Brief's body plus the Memory index an agent loads on each turn,
611
+ # estimated at four characters a token. Past it the knowledge-base read sets
612
+ # `warning: true` and still returns every doc whole. A whole number from 1 to
613
+ # 1000000; unset means 8000. Any other value stops the API at boot.
614
+ # AILK_CONTEXT_BUDGET_TOKENS=
615
+ #
609
616
  # SPOKE (apps/web and apps/mcp, server-only — never NEXT_PUBLIC_). The key the
610
617
  # hub's `spokes register` printed. Sent as `x-ailk-spoke-key` on every
611
618
  # server-side call to AILK_API_URL, and only when that is https (or