create-ailk 0.25.1 → 0.27.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 (75) hide show
  1. package/README.md +14 -5
  2. package/dist/lib/fetch-template-snapshot.d.ts +2 -1
  3. package/dist/lib/fetch-template-snapshot.js +6 -5
  4. package/dist/lib/module-license-gate.js +2 -3
  5. package/dist/lib/module-tier.d.ts +2 -8
  6. package/dist/lib/module-tier.js +13 -12
  7. package/dist/module-architecture.js +14 -2
  8. package/dist/starter-grid.d.ts +6 -6
  9. package/dist/starter-grid.js +12 -5
  10. package/dist/surfaces.js +36 -2
  11. package/dist/template-record.d.ts +11 -3
  12. package/dist/template-record.js +20 -7
  13. package/dist/topology.js +23 -4
  14. package/dist/update.d.ts +1 -1
  15. package/dist/update.js +240 -20
  16. package/package.json +3 -3
  17. package/release-scope.json +1 -1
  18. package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +8 -2
  19. package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +2 -2
  20. package/site-templates/marketing-site/ailk-template.json +7 -1
  21. package/site-templates/marketing-site/provenance.json +7 -1
  22. package/site-templates/saas-product/ailk-template.json +2 -1
  23. package/site-templates/saas-product/provenance.json +1 -0
  24. package/templates/.env.example +22 -0
  25. package/templates/CLAUDE.md +20 -20
  26. package/templates/README.md +19 -19
  27. package/templates/apps/api/.env.example +28 -0
  28. package/templates/apps/api/CLAUDE.md +35 -9
  29. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +16 -0
  30. package/templates/apps/api/vercel.json +7 -1
  31. package/templates/apps/mcp/CLAUDE.md +27 -10
  32. package/templates/apps/web/.env.example +13 -0
  33. package/templates/apps/web/CLAUDE.md +34 -6
  34. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +43 -4
  35. package/templates/apps/web/app/[locale]/(auth)/legal-footnote.tsx +35 -0
  36. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/next-path.test.ts +24 -1
  37. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +168 -8
  38. package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +13 -0
  39. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +22 -39
  40. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +6 -6
  41. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +20 -35
  42. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +301 -0
  43. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +111 -5
  44. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -18
  45. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +132 -12
  46. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +53 -1
  47. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +38 -0
  48. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +4 -2
  49. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +47 -24
  50. package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +23 -4
  51. package/templates/apps/web/app/[locale]/layout.tsx +48 -13
  52. package/templates/apps/web/app/[locale]/meet/confirmed/BookingHandOff.tsx +128 -0
  53. package/templates/apps/web/app/[locale]/meet/confirmed/__tests__/BookingHandOff.test.tsx +126 -0
  54. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +118 -0
  55. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +14 -0
  56. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +51 -0
  57. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +13 -0
  58. package/templates/apps/web/auth.ts +8 -4
  59. package/templates/apps/web/i18n/request.ts +49 -1
  60. package/templates/apps/web/lib/__tests__/account-config.test.ts +7 -0
  61. package/templates/apps/web/lib/__tests__/auth-email.test.ts +276 -0
  62. package/templates/apps/web/lib/__tests__/relay.test.ts +289 -1
  63. package/templates/apps/web/lib/auth-email.ts +102 -20
  64. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +92 -23
  65. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +97 -70
  66. package/templates/apps/web/lib/prisma-for-auth.ts +5 -2
  67. package/templates/apps/web/lib/relay.ts +116 -1
  68. package/templates/apps/web/lib/sign-up-agreements.ts +302 -28
  69. package/templates/apps/web/lib/site-contributions.ts +18 -0
  70. package/templates/apps/web/messages/en.json +271 -20
  71. package/templates/apps/web/messages/site/en.json +1 -0
  72. package/templates/apps/web/middleware.ts +8 -1
  73. package/templates/apps/web/next.config.mjs +9 -1
  74. package/templates/apps/web/package.json +2 -1
  75. package/versions.json +48 -48
package/README.md CHANGED
@@ -18,15 +18,17 @@ Compose the site from modules with `--modules`. The recognized vocabulary is
18
18
  `multi-tenant`, `workspace`, `scheduling`, `signal-collectors`, `commerce` —
19
19
  and `--modules` TRIMS or ADDS relative to the level your licence key unlocks
20
20
  (#6759), never replaces it: Free is `marketing` alone; a Pro key switches
21
- on `docs-blog`, `scheduling` and `signal-collectors` too; Pro Plus adds
21
+ on `docs-blog`, `workspace` (the `/admin` settings surface, #9696),
22
+ `scheduling` and `signal-collectors` too; Pro Plus adds
22
23
  `multi-tenant`. A run with no key is a Free run, and Free is held for now
23
24
  (`release-scope.json` marks `levels.free` `deferred`, #6484): the CLI refuses
24
25
  it before its first write, naming `https://ailaunchkit.ai/pricing` and the
25
26
  two ways to pass a key (`--license <key>` or `AILK_LICENSE_KEY`). The Free
26
27
  template and level stay defined; only the keyless path is held. Naming modules here
27
28
  keeps exactly those, trimming the level's default; naming only `commerce`
28
- (a per-site add-on) or `workspace` (included at Pro, opt-in) adds it on top
29
- of the level's default without trimming. The level is always DERIVED from
29
+ (a per-site add-on) adds it on top of the level's default without trimming,
30
+ and naming only `workspace` is a no-op at Pro and Pro Plus, whose defaults already
31
+ carry it (below Pro it is refused, naming Pro). The level is always DERIVED from
30
32
  what the module registry answers for your key — never read from a flag, a
31
33
  manifest or a key claim (`src/lib/module-tier.ts` `LEVEL_MODULES`,
32
34
  `src/lib/module-license-gate.ts` `deriveLevel`):
@@ -76,6 +78,13 @@ AILK_LICENSE_KEY=<key> create-ailk add-module docs-blog
76
78
  AILK_LICENSE_KEY=<key> create-ailk upgrade docs-blog ./my-site
77
79
  ```
78
80
 
81
+ A Pro site scaffolded before a Pro key switched on `workspace` (#9696) has no
82
+ `/admin`. This is how it gains the owner's settings surface:
83
+
84
+ ```bash
85
+ AILK_LICENSE_KEY=<key> create-ailk add-module workspace
86
+ ```
87
+
79
88
  It fetches the module's bundle through the same licence gate a scaffold uses
80
89
  (no key, or a key that does not cover the module, is refused with the same
81
90
  message, before anything is written) and merges it **without overwriting
@@ -236,8 +245,8 @@ present, and this is the sign-in/account shell" from "always present, and
236
245
  this is the marketing core." A gateable surface's routes (`blog`, `docs`,
237
246
  `flows`, …) are checked only when the site carries that surface; a surface
238
247
  gated off at scaffold time is listed as skipped, not missing — its absence is
239
- deliberate, not drift. Add a surface with `--sync-architecture` and the
240
- `add-page` skill, not with `sync-routes`.
248
+ deliberate, not drift. Add a surface with `--sync-architecture` and
249
+ `/ailk:growth-ops page add`, not with `sync-routes`.
241
250
 
242
251
  **The local-module dependency contract (#5014, #9406).** Some routes import
243
252
  files OUTSIDE their own route directory — `purchase/success/page.tsx` imports
@@ -12,7 +12,8 @@ export interface FetchTemplateSnapshotOptions {
12
12
  }
13
13
  /**
14
14
  * Fetch and re-materialise create-ailk `version`'s `templates/apps/{web,api,mcp}/`
15
- * tree, plus its `templates/database/` boot files, from its published npm tarball.
15
+ * tree, plus its `templates/database/` boot files and root `templates/tools/`
16
+ * files, from its published npm tarball.
16
17
  *
17
18
  * @throws FetchTemplateSnapshotError on an invalid version, an unreachable
18
19
  * or malformed registry response, an integrity mismatch, or a symlink /
@@ -21,7 +21,7 @@ import { createHash } from "node:crypto";
21
21
  import { gunzipSync } from "node:zlib";
22
22
  import * as tar from "tar";
23
23
  import { templateDestination } from "../copy.js";
24
- import { DATABASE_BOOT_FILES } from "../template-record.js";
24
+ import { DATABASE_BOOT_FILES, ROOT_TOOL_FILES } from "../template-record.js";
25
25
  import { MAX_TOTAL_SIZE_BYTES } from "./extract-module-bundle.js";
26
26
  import { assertContentLengthWithinLimit, assertHttpsRegistryUrl, fetchTimeoutSignal, } from "./fetch-guard.js";
27
27
  export const DEFAULT_AILK_NPM_REGISTRY_URL = "https://registry.npmjs.org";
@@ -30,8 +30,8 @@ const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
30
30
  /** The one tar path prefix a snapshot is read from, and the three app roots inside it. */
31
31
  const TAR_ROOT = "package/templates/";
32
32
  const APP_PREFIXES = ["apps/web/", "apps/api/", "apps/mcp/"];
33
- /** The paths outside the app roots a snapshot keeps: a hub's three database boot files (#9569 DD8). */
34
- const BOOT_FILES = new Set(DATABASE_BOOT_FILES);
33
+ /** The paths outside the app roots a snapshot keeps: a hub's three database boot files (#9569 DD8) and the root tools/ files the app configs require (#9654). */
34
+ const BOOT_FILES = new Set([...DATABASE_BOOT_FILES, ...ROOT_TOOL_FILES]);
35
35
  export class FetchTemplateSnapshotError extends Error {
36
36
  constructor(message) {
37
37
  super(message);
@@ -113,7 +113,7 @@ async function fetchTarball(tarballUrl, integrity, version, fetchImpl) {
113
113
  }
114
114
  return buf;
115
115
  }
116
- /** The template-relative path (no `package/templates/` prefix) for a tar entry path, or undefined outside the three app trees and the database boot files. */
116
+ /** The template-relative path (no `package/templates/` prefix) for a tar entry path, or undefined outside the three app trees, the database boot files and the root tools/ files. */
117
117
  function appRelativePath(tarPath) {
118
118
  if (!tarPath.startsWith(TAR_ROOT))
119
119
  return undefined;
@@ -180,7 +180,8 @@ function parseSnapshot(gunzipped, version) {
180
180
  }
181
181
  /**
182
182
  * Fetch and re-materialise create-ailk `version`'s `templates/apps/{web,api,mcp}/`
183
- * tree, plus its `templates/database/` boot files, from its published npm tarball.
183
+ * tree, plus its `templates/database/` boot files and root `templates/tools/`
184
+ * files, from its published npm tarball.
184
185
  *
185
186
  * @throws FetchTemplateSnapshotError on an invalid version, an unreachable
186
187
  * or malformed registry response, an integrity mismatch, or a symlink /
@@ -29,7 +29,7 @@
29
29
  import releaseScopeJson from "../../release-scope.json" with { type: "json" };
30
30
  import { FetchModuleError, fetchModule } from "./fetch-module.js";
31
31
  import { assertLicenseKeyShape } from "./license-key-files.js";
32
- import { ADD_ONS, LEVEL_LABEL, LEVEL_MODULES, OPT_IN_MODULES, PAID_MODULES, classifyModules, composeModuleSet, levelOf, } from "./module-tier.js";
32
+ import { LEVEL_LABEL, LEVEL_MODULES, PAID_MODULES, classifyModules, composeModuleSet, isAddition, levelOf, } from "./module-tier.js";
33
33
  /**
34
34
  * Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
35
35
  * `/pro` route and there will not be one (#6439, operator decision OD-2) —
@@ -346,8 +346,7 @@ export async function deriveDesignLevel(options) {
346
346
  */
347
347
  export async function resolveModuleSet(options) {
348
348
  const namedList = options.named ?? [];
349
- const additions = [...ADD_ONS, ...OPT_IN_MODULES];
350
- const isKeep = namedList.some((m) => !additions.includes(m));
349
+ const isKeep = namedList.some((m) => !isAddition(m));
351
350
  if (isKeep) {
352
351
  const modules = composeModuleSet(namedList, "free");
353
352
  const { paid } = classifyModules(modules);
@@ -23,13 +23,6 @@ export declare const LEVEL_LABEL: Readonly<Record<Level, string>>;
23
23
  * an add-on ON TOP of.
24
24
  */
25
25
  export declare const ADD_ONS: readonly ModuleName[];
26
- /**
27
- * Included at Pro and above (the licence manifest's `pro` tier), but not
28
- * switched on by default at any level (D1) — an authed app surface
29
- * contributing no PUBLIC page to a marketing site. Naming it in `--modules`
30
- * adds it without trimming, exactly like an add-on.
31
- */
32
- export declare const OPT_IN_MODULES: readonly ModuleName[];
33
26
  /** Whether naming `m` in `--modules` adds it without trimming the level default. */
34
27
  export declare function isAddition(m: ModuleName): boolean;
35
28
  /**
@@ -42,7 +35,8 @@ export declare const LEVEL_MODULES: Readonly<Record<Level, readonly ModuleName[]
42
35
  /**
43
36
  * Which level (or add-on status) a module belongs to (D2). `"free"` for
44
37
  * `FREE_MODULES`, `"add-on"` for `ADD_ONS`, `"pro-plus"` for
45
- * `PRO_PLUS_MODULES`, else `"pro"` (`workspace` included — D1). Pure.
38
+ * `PRO_PLUS_MODULES`, else `"pro"` (`workspace` included — in the Pro default
39
+ * since #9696, the DECISION-LOG row in salessmyth/ailaunchkit-site#134). Pure.
46
40
  */
47
41
  export declare function levelOf(m: ModuleName): Level | "add-on";
48
42
  /**
@@ -43,22 +43,22 @@ export const LEVEL_LABEL = {
43
43
  * an add-on ON TOP of.
44
44
  */
45
45
  export const ADD_ONS = ["commerce"];
46
- /**
47
- * Included at Pro and above (the licence manifest's `pro` tier), but not
48
- * switched on by default at any level (D1) — an authed app surface
49
- * contributing no PUBLIC page to a marketing site. Naming it in `--modules`
50
- * adds it without trimming, exactly like an add-on.
51
- */
52
- export const OPT_IN_MODULES = ["workspace"];
53
46
  /** The Pro Plus membership: what Pro Plus switches on beyond Pro. */
54
47
  const PRO_PLUS_MODULES = ["multi-tenant"];
55
- /** Named modules that never trim the level default when composing a set (D2). */
56
- const ADDITIONS = [...ADD_ONS, ...OPT_IN_MODULES];
48
+ /**
49
+ * Named modules that never trim the level default when composing a set (D2):
50
+ * the add-ons, and `workspace` — the authed /admin surface, which contributes
51
+ * no PUBLIC page. Since #9696 (reversing #6759 D1; the DECISION-LOG row dated
52
+ * 2026-10-24, salessmyth/ailaunchkit-site#134) `workspace` is IN the Pro and
53
+ * Pro Plus defaults, so naming it is a no-op there; below Pro it adds it, and
54
+ * the gate refuses it naming Pro.
55
+ */
56
+ const ADDITIONS = [...ADD_ONS, "workspace"];
57
57
  /** Whether naming `m` in `--modules` adds it without trimming the level default. */
58
58
  export function isAddition(m) {
59
59
  return ADDITIONS.includes(m);
60
60
  }
61
- const PRO_EXCLUDED = new Set([...ADDITIONS, ...PRO_PLUS_MODULES]);
61
+ const PRO_EXCLUDED = new Set([...ADD_ONS, ...PRO_PLUS_MODULES]);
62
62
  /**
63
63
  * What a key at each level switches on with no `--modules` (D2). Derived —
64
64
  * every list is `RECOGNIZED_MODULES.filter(...)`, so declaration order is
@@ -70,12 +70,13 @@ export const LEVEL_MODULES = Object.freeze({
70
70
  pro: RECOGNIZED_MODULES.filter((m) => FREE_MODULES.includes(m) ||
71
71
  (PAID_MODULES.includes(m) && !PRO_EXCLUDED.has(m))),
72
72
  "pro-plus": RECOGNIZED_MODULES.filter((m) => FREE_MODULES.includes(m) ||
73
- (PAID_MODULES.includes(m) && !ADDITIONS.includes(m))),
73
+ (PAID_MODULES.includes(m) && !ADD_ONS.includes(m))),
74
74
  });
75
75
  /**
76
76
  * Which level (or add-on status) a module belongs to (D2). `"free"` for
77
77
  * `FREE_MODULES`, `"add-on"` for `ADD_ONS`, `"pro-plus"` for
78
- * `PRO_PLUS_MODULES`, else `"pro"` (`workspace` included — D1). Pure.
78
+ * `PRO_PLUS_MODULES`, else `"pro"` (`workspace` included — in the Pro default
79
+ * since #9696, the DECISION-LOG row in salessmyth/ailaunchkit-site#134). Pure.
79
80
  */
80
81
  export function levelOf(m) {
81
82
  if (FREE_MODULES.includes(m))
@@ -794,7 +794,7 @@ export const MODULE_SLICES = {
794
794
  webImports: [WEB_AUTH_ADAPTER_IMPORT, "packages/admin-ui"],
795
795
  packages: {
796
796
  "admin-ui": {
797
- description: "Shared Admin-settings components — the settings shell plus the General / Site / Team panels, presentational only (data + save callbacks as props)",
797
+ description: "Shared Admin components — the settings shell plus the General / Site / Team panels, and the Magnets (flow wizard, flow list) and Meetings (meeting types, meeting detail) screens, presentational only (data + save callbacks as props)",
798
798
  // packages/templates is required because AdminSettingsShell
799
799
  // composes AppShell from @working-theory/templates/shells — the same
800
800
  // composition apps/web's own account page uses.
@@ -807,7 +807,7 @@ export const MODULE_SLICES = {
807
807
  },
808
808
  },
809
809
  // SOURCE files only, and only the ones this module physically owns: the
810
- // three apps/web route files and the two apps/api route files.
810
+ // apps/web /admin route files and the two modules they share.
811
811
  //
812
812
  // `packages/admin-ui/**` is deliberately NOT listed (D1a). It is a
813
813
  // whole package that is paid end to end, whose strip disposition is
@@ -832,6 +832,18 @@ export const MODULE_SLICES = {
832
832
  "apps/web/app/[locale]/(authed)/admin/page.tsx",
833
833
  "apps/web/app/[locale]/(authed)/admin/site/page.tsx",
834
834
  "apps/web/app/[locale]/(authed)/admin/team/page.tsx",
835
+ // #9746 D10 — the shared nav, the client bindings, and the Magnets and
836
+ // Meetings tabs. Not routes themselves (the first two), but owned the
837
+ // same way: a keyless scaffold drops them with the pages that import
838
+ // them.
839
+ "apps/web/app/[locale]/(authed)/admin/admin-nav.ts",
840
+ "apps/web/app/[locale]/(authed)/admin/admin-panels.tsx",
841
+ "apps/web/app/[locale]/(authed)/admin/magnets/page.tsx",
842
+ "apps/web/app/[locale]/(authed)/admin/magnets/new/[kind]/page.tsx",
843
+ "apps/web/app/[locale]/(authed)/admin/meetings/page.tsx",
844
+ "apps/web/app/[locale]/(authed)/admin/meetings/[slug]/page.tsx",
845
+ // #9851 — the Integrations tab.
846
+ "apps/web/app/[locale]/(authed)/admin/integrations/page.tsx",
835
847
  ],
836
848
  // patches — the dependency add that delivers the shared package to the
837
849
  // buyer's apps/web, mirroring docs-blog's own fumadocs-schema merge-keys
@@ -83,10 +83,10 @@ export declare const SHAPE_ROSTERS: Readonly<Record<BuildableShape, readonly Tem
83
83
  * in the declaration order both keep. The plain name is the Pro one: it is
84
84
  * the flagship and the demo (Sam, 2026-09-14, parent § D12).
85
85
  *
86
- * `workspace` appears in no module list. It is a Pro module, but an authed app
87
- * surface contributing no PUBLIC page to a marketing site, and `modules` names
88
- * what the roster uses (parent § D15). A Pro key unlocks it regardless of any
89
- * template.
86
+ * Both paid aliases carry `workspace` because a Pro or Pro Plus key switches
87
+ * it on (#9696, reversing parent § D15), and a template's `requires` is what
88
+ * the alias path fetches, since it derives no level. It is an authed app
89
+ * surface, so it contributes no PUBLIC row to either roster.
90
90
  *
91
91
  * `commerce` is a per-site add-on available at any level, so no alias exists
92
92
  * FOR it (parent § D13); it still contributes rows to whichever alias takes
@@ -120,7 +120,7 @@ export declare const ALIASES: Readonly<{
120
120
  readonly "marketing-site": {
121
121
  readonly shape: "organization-product";
122
122
  readonly design: "standard";
123
- readonly modules: readonly ["marketing", "docs-blog", "scheduling", "signal-collectors"];
123
+ readonly modules: readonly ["marketing", "docs-blog", "scheduling", "signal-collectors", "workspace"];
124
124
  readonly categories: readonly ["accounting", "legal", "consulting", "hvac", "roofing", "plumbing", "electrical", "landscaping", "auto-repair", "real-estate", "technology", "medical-specialty"];
125
125
  readonly differences: {
126
126
  readonly drop: readonly ["/blog", "/meet", "/meet/[slug]"];
@@ -140,7 +140,7 @@ export declare const ALIASES: Readonly<{
140
140
  readonly "saas-product": {
141
141
  readonly shape: "organization-product";
142
142
  readonly design: "standard";
143
- readonly modules: readonly ["marketing", "docs-blog", "scheduling", "signal-collectors", "multi-tenant"];
143
+ readonly modules: readonly ["marketing", "docs-blog", "scheduling", "signal-collectors", "workspace", "multi-tenant"];
144
144
  readonly categories: readonly ["technology", "medical-specialty"];
145
145
  readonly differences: {
146
146
  readonly drop: readonly ["/blog", "/meet", "/meet/[slug]"];
@@ -372,10 +372,10 @@ export const SHAPE_ROSTERS = Object.freeze({
372
372
  * in the declaration order both keep. The plain name is the Pro one: it is
373
373
  * the flagship and the demo (Sam, 2026-09-14, parent § D12).
374
374
  *
375
- * `workspace` appears in no module list. It is a Pro module, but an authed app
376
- * surface contributing no PUBLIC page to a marketing site, and `modules` names
377
- * what the roster uses (parent § D15). A Pro key unlocks it regardless of any
378
- * template.
375
+ * Both paid aliases carry `workspace` because a Pro or Pro Plus key switches
376
+ * it on (#9696, reversing parent § D15), and a template's `requires` is what
377
+ * the alias path fetches, since it derives no level. It is an authed app
378
+ * surface, so it contributes no PUBLIC row to either roster.
379
379
  *
380
380
  * `commerce` is a per-site add-on available at any level, so no alias exists
381
381
  * FOR it (parent § D13); it still contributes rows to whichever alias takes
@@ -415,7 +415,13 @@ export const ALIASES = Object.freeze({
415
415
  "marketing-site": {
416
416
  shape: "organization-product",
417
417
  design: "standard",
418
- modules: ["marketing", "docs-blog", "scheduling", "signal-collectors"],
418
+ modules: [
419
+ "marketing",
420
+ "docs-blog",
421
+ "scheduling",
422
+ "signal-collectors",
423
+ "workspace",
424
+ ],
419
425
  // The flagship is the general-purpose template: it carries the whole
420
426
  // registry, so every group tile on the hub has at least one template.
421
427
  categories: [
@@ -455,6 +461,7 @@ export const ALIASES = Object.freeze({
455
461
  "docs-blog",
456
462
  "scheduling",
457
463
  "signal-collectors",
464
+ "workspace",
458
465
  "multi-tenant",
459
466
  ],
460
467
  categories: ["technology", "medical-specialty"],
package/dist/surfaces.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * The default (`--modules marketing`) enables NO gateable surface: every one of
22
22
  * them is a claim about the business that the scaffolder cannot make. The path
23
- * from there is the `/ailk:add-page` skill in the `ailk` Claude Code plugin.
23
+ * from there is `/ailk:growth-ops page add`, from the `ailk` Claude Code plugin.
24
24
  *
25
25
  * Zero runtime dependencies — create-ailk is a published, standalone CLI.
26
26
  *
@@ -725,6 +725,14 @@ export const ROUTE_SINCE = {
725
725
  "(authed)/admin": "0.22.1",
726
726
  "(authed)/admin/site": "0.22.1",
727
727
  "(authed)/admin/team": "0.22.1",
728
+ // #9746 D10 — the Magnets and Meetings tabs. Genuinely new route
729
+ // directories, so they attach at today's fixed-group version.
730
+ "(authed)/admin/magnets": "0.46.0",
731
+ "(authed)/admin/magnets/new/[kind]": "0.46.0",
732
+ "(authed)/admin/meetings": "0.46.0",
733
+ "(authed)/admin/meetings/[slug]": "0.46.0",
734
+ // #9851 D7 — the Integrations tab, attached at today's fixed-group version.
735
+ "(authed)/admin/integrations": "0.48.0",
728
736
  // #5977 — the Pro content host's editor routes (spec 5872 FR1/FR2), added
729
737
  // to apps/web by #5957. Genuinely new route directories, so they attach at
730
738
  // today's fixed-group version, the same "attach at today's version" rule
@@ -785,6 +793,13 @@ export function surfaceForRouteDir(routeDir) {
785
793
  return "core";
786
794
  return GATEABLE_SURFACES.find((s) => s.routes.includes(top))?.id;
787
795
  }
796
+ /** #9746 D10 — what each Magnets and Meetings tab page imports beside itself. */
797
+ const ADMIN_PANEL_ROUTE_DEPENDENCIES = [
798
+ "apps/web/lib/server-api.ts",
799
+ `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
800
+ `${ROUTE_ROOT}/(authed)/admin/admin-nav.ts`,
801
+ `${ROUTE_ROOT}/(authed)/admin/admin-panels.tsx`,
802
+ ];
788
803
  /**
789
804
  * Non-route consumer-side files a route's own source imports, that a sync
790
805
  * must ALSO bring along so `--apply` never leaves a route that cannot
@@ -964,6 +979,8 @@ export const ROUTE_DEPENDENCIES = {
964
979
  ],
965
980
  "(authed)/account/billing": [
966
981
  "apps/web/lib/account-config.ts",
982
+ // #9401 D7 — the site's own Billing sections, the frozen stub the page imports.
983
+ "apps/web/lib/site-contributions.ts",
967
984
  "apps/web/lib/server-api.ts",
968
985
  `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
969
986
  `${ROUTE_ROOT}/(authed)/account/actions.ts`,
@@ -1034,6 +1051,8 @@ export const ROUTE_DEPENDENCIES = {
1034
1051
  "(authed)/admin": [
1035
1052
  "apps/web/lib/server-api.ts",
1036
1053
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
1054
+ // #9746 D8 — the shared nav every /admin tab imports.
1055
+ `${ROUTE_ROOT}/(authed)/admin/admin-nav.ts`,
1037
1056
  ],
1038
1057
  "(authed)/leads": ["apps/web/lib/server-api.ts"],
1039
1058
  // #5977 — the editor routes' own imports, read from their sources: both
@@ -1053,11 +1072,21 @@ export const ROUTE_DEPENDENCIES = {
1053
1072
  "(authed)/admin/site": [
1054
1073
  "apps/web/lib/server-api.ts",
1055
1074
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
1075
+ `${ROUTE_ROOT}/(authed)/admin/admin-nav.ts`,
1056
1076
  ],
1057
1077
  "(authed)/admin/team": [
1058
1078
  "apps/web/lib/server-api.ts",
1059
1079
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
1080
+ `${ROUTE_ROOT}/(authed)/admin/admin-nav.ts`,
1060
1081
  ],
1082
+ // #9746 D10 — the Magnets and Meetings tabs: the nav plus the client
1083
+ // bindings of the shared admin-ui components.
1084
+ "(authed)/admin/magnets": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1085
+ "(authed)/admin/magnets/new/[kind]": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1086
+ "(authed)/admin/meetings": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1087
+ "(authed)/admin/meetings/[slug]": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1088
+ // #9851 D7 — the Integrations tab: the nav plus the Slack bindings.
1089
+ "(authed)/admin/integrations": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1061
1090
  "(authed)/waitlist": ["apps/web/lib/server-api.ts"],
1062
1091
  // #8843 D6 — the booking pages share one builder over the meetings read;
1063
1092
  // `/private` returns the index frame that lives under `meet/`.
@@ -1115,6 +1144,11 @@ export const ROUTE_MESSAGE_NAMESPACES = {
1115
1144
  "(authed)/edit/[type]/[slug]": ["edit"],
1116
1145
  "(authed)/admin/site": ["admin"],
1117
1146
  "(authed)/admin/team": ["admin"],
1147
+ "(authed)/admin/magnets": ["admin"],
1148
+ "(authed)/admin/magnets/new/[kind]": ["admin"],
1149
+ "(authed)/admin/meetings": ["admin"],
1150
+ "(authed)/admin/meetings/[slug]": ["admin"],
1151
+ "(authed)/admin/integrations": ["admin"],
1118
1152
  "(authed)/waitlist": ["waitlistDashboard.gate"],
1119
1153
  // #7744 — the /team hub's empty state reads the `team` namespace.
1120
1154
  team: ["team"],
@@ -1386,7 +1420,7 @@ export function renderNavConfig(enabled, options) {
1386
1420
  // below it, the nav is yours and add-module reports the change it wanted
1387
1421
  // instead.
1388
1422
  //
1389
- // Adding a link: scaffold the page (the \`/ailk:add-page\` skill), then add an
1423
+ // Adding a link: scaffold the page (\`/ailk:growth-ops page add\`), then add an
1390
1424
  // entry below with a \`labelKey\` that exists in the \`nav\` namespace of
1391
1425
  // \`apps/web/messages/<locale>.json\`. Paths are locale-agnostic — Nav prefixes
1392
1426
  // them with the active locale at render.
@@ -18,12 +18,20 @@ export declare const REGENERATED_APP_PATHS: readonly string[];
18
18
  * are bundle output, never recorded.
19
19
  */
20
20
  export declare const DATABASE_BOOT_FILES: readonly string[];
21
+ /**
22
+ * The root `tools/` files every app `jest.config.cjs` requires by its
23
+ * repo-relative path (#9654): the timezone pin (#8657) and the shared
24
+ * reporters (#9430) — `SNAPSHOT_FILES` in `scripts/build-templates.mjs`
25
+ * ships both at that path. Recorded and carried by `update` like an app
26
+ * file, so a merged config never names a file the site lacks.
27
+ */
28
+ export declare const ROOT_TOOL_FILES: readonly string[];
21
29
  /**
22
30
  * Whether a destination path (`/`-separated, as `templateDestination`
23
31
  * returns it) is one `update` ever records or touches (#8522 D2): one of
24
- * the three `DATABASE_BOOT_FILES`, or under one of the three app trees, not
25
- * a `package.json`, not an `.env*` file, no `.claude` segment, and not a
26
- * path a generator rewrites after the copy.
32
+ * the three `DATABASE_BOOT_FILES` or the `ROOT_TOOL_FILES`, or under one of
33
+ * the three app trees, not a `package.json`, not an `.env*` file, no
34
+ * `.claude` segment, and not a path a generator rewrites after the copy.
27
35
  */
28
36
  export declare function isRecordable(destRel: string): boolean;
29
37
  /** The hex sha256 digest of `bytes` — the one hash every record row uses. */
@@ -4,9 +4,10 @@
4
4
  *
5
5
  * Written beside `.ailk-site-manifest.json` at the site root, it lists every
6
6
  * `apps/web/`, `apps/api/` and `apps/mcp/` file the base `templates/` copy
7
- * wrote and no later layer rewrote, with the sha256 of the template's copy
8
- * of that file and the create-ailk version it came from. `create-ailk
9
- * update` (`update.ts`) reads it to tell an untouched copy from an owner's
7
+ * wrote and no later layer rewrote — and the few files outside them the
8
+ * record also carries (`DATABASE_BOOT_FILES`, `ROOT_TOOL_FILES`) — with the
9
+ * sha256 of the template's copy of that file and the create-ailk version it
10
+ * came from. `create-ailk update` (`update.ts`) reads it to tell an untouched copy from an owner's
10
11
  * edit; `scaffold()` (`programmatic.ts`) writes it, as its last write, from
11
12
  * the `onWrite` hook `copyTemplate` (`copy.ts`) calls once per file.
12
13
  *
@@ -60,15 +61,27 @@ export const DATABASE_BOOT_FILES = [
60
61
  "database/seed-test.ts",
61
62
  ];
62
63
  const DATABASE_BOOT_SET = new Set(DATABASE_BOOT_FILES);
64
+ /**
65
+ * The root `tools/` files every app `jest.config.cjs` requires by its
66
+ * repo-relative path (#9654): the timezone pin (#8657) and the shared
67
+ * reporters (#9430) — `SNAPSHOT_FILES` in `scripts/build-templates.mjs`
68
+ * ships both at that path. Recorded and carried by `update` like an app
69
+ * file, so a merged config never names a file the site lacks.
70
+ */
71
+ export const ROOT_TOOL_FILES = [
72
+ "tools/jest/pin-timezone.cjs",
73
+ "tools/jest/reporters.cjs",
74
+ ];
75
+ const ROOT_TOOL_SET = new Set(ROOT_TOOL_FILES);
63
76
  /**
64
77
  * Whether a destination path (`/`-separated, as `templateDestination`
65
78
  * returns it) is one `update` ever records or touches (#8522 D2): one of
66
- * the three `DATABASE_BOOT_FILES`, or under one of the three app trees, not
67
- * a `package.json`, not an `.env*` file, no `.claude` segment, and not a
68
- * path a generator rewrites after the copy.
79
+ * the three `DATABASE_BOOT_FILES` or the `ROOT_TOOL_FILES`, or under one of
80
+ * the three app trees, not a `package.json`, not an `.env*` file, no
81
+ * `.claude` segment, and not a path a generator rewrites after the copy.
69
82
  */
70
83
  export function isRecordable(destRel) {
71
- if (DATABASE_BOOT_SET.has(destRel))
84
+ if (DATABASE_BOOT_SET.has(destRel) || ROOT_TOOL_SET.has(destRel))
72
85
  return true;
73
86
  if (!RECORDABLE_APP_ROOT.test(destRel))
74
87
  return false;
package/dist/topology.js CHANGED
@@ -515,9 +515,11 @@ import { readLocales } from "${WEB_I18N_PACKAGE}";
515
515
  import { getTranslations } from "${NEXT_INTL_SERVER}";
516
516
  import type { ReactElement } from "${REACT_PACKAGE}";
517
517
 
518
+ import { legalFootnote } from "../(auth)/legal-footnote";
518
519
  import { resolveSignInCardText } from "../(auth)/sign-in/sign-in-text";
519
520
  import { getActiveProviderIds } from "../../../lib/auth-env";
520
521
  import { isAccountPath } from "../../../lib/auth-routes";
522
+ import { siteLogo } from "../../../lib/site-logo";
521
523
 
522
524
  import { SignInGate } from "./sign-in-gate";
523
525
 
@@ -539,9 +541,7 @@ export async function SignedOutGate({
539
541
  namespace: isAccount ? "account.gate" : "waitlistDashboard.gate",
540
542
  });
541
543
  const active = getActiveProviderIds();
542
- const emailLink = active.includes("magic-link")
543
- ? { text: await resolveSignInCardText(locale) }
544
- : undefined;
544
+ const emailLink = active.includes("magic-link");
545
545
  const providers = active.filter((id): id is AuthPanelProvider =>
546
546
  (SUPPORTED_PROVIDERS as readonly string[]).includes(id),
547
547
  );
@@ -575,6 +575,25 @@ export async function SignedOutGate({
575
575
  );
576
576
  }
577
577
 
578
+ // The sign-in card: with the email link on, and on the member gate, which
579
+ // also wears the one card's frame — the mark, the toggle line to /sign-up
580
+ // and the Terms + Privacy acknowledgement (#9662).
581
+ const text =
582
+ emailLink || isAccount ? await resolveSignInCardText(locale) : undefined;
583
+ const card = text
584
+ ? {
585
+ text,
586
+ emailLink,
587
+ ...(isAccount
588
+ ? {
589
+ logo: siteLogo(),
590
+ prospect: { ...text.prospect, href: localeHref(locale, "/sign-up") },
591
+ footnote: legalFootnote(locale, text.acknowledgement),
592
+ }
593
+ : {}),
594
+ }
595
+ : undefined;
596
+
578
597
  return (
579
598
  <SignInGate
580
599
  labels={{
@@ -584,7 +603,7 @@ export async function SignedOutGate({
584
603
  }}
585
604
  signInPath={signInPath}
586
605
  providers={providers}
587
- {...(emailLink ? { emailLink } : {})}
606
+ {...(card ? { card } : {})}
588
607
  />
589
608
  );
590
609
  }
package/dist/update.d.ts CHANGED
@@ -57,6 +57,6 @@ export interface UpdateArgs {
57
57
  target: string | undefined;
58
58
  help: boolean;
59
59
  }
60
- export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file whose template copy changed is\noverwritten. An edited file whose template copy also changed is three-way\nmerged with `git` (git must be on PATH); a conflict leaves the file alone\nand is listed under \"Needs review\". Every template test that imports a file\nleft under \"Needs review\" or \"Skipped\" (by a relative import) is held at the\nsite's version with it, its record entry is not advanced, and it is listed\nunder \"Held back with <file>\" \u2014 reconcile the file, then run update again to\nbring its tests forward. Every other file in the run still updates.\nA site with no record gets one on its first run, from the version its\n.ailk-site-manifest.json already records. A site scaffolded before\n@working-theory/mcp-server has its copied apps/mcp moved to the boot set:\nthe server and each module's paid MCP pack join apps/mcp/package.json, the\ncopied server files go, and apps/mcp/src/lib/paid-tools.generated.ts is\nrewritten on every run. A site scaffolded before @working-theory/api has its\ncopied apps/api moved to the boot file: the app becomes @working-theory/app-api,\nthe package and the six @working-theory/api-<module> packages join\napps/api/package.json, its bins and build:vercel delegate to the package, and\nthe copied server files go. Run `pnpm install` after either move, since the\napp's package.json gained dependencies. Commit before running, and review the\ndiff after.\n";
60
+ export declare const UPDATE_HELP = "create-ailk update \u2014 bring template fixes into a site's copied app code.\n\nUsage:\n create-ailk update [<target-dir>]\n\nArguments:\n <target-dir> The existing site. Defaults to the current dir.\n\nOptions:\n --help, -h Print this message.\n\nReads .ailk-template-files.json (written beside .ailk-site-manifest.json by\nevery template scaffold) to tell an untouched copy from an edit. An\nuntouched apps/web, apps/api or apps/mcp file, or a root tools/jest file the\napp jest configs require, whose template copy changed is overwritten. An\nedited file whose template copy also changed is three-way merged with\n`git` (git must be on PATH); a conflict leaves the file alone and is\nlisted under \"Needs review\". Every template test that imports a file\nleft under \"Needs review\" or \"Skipped\" (by a relative import) is held at the\nsite's version with it, its record entry is not advanced, and it is listed\nunder \"Held back with <file>\" \u2014 reconcile the file, then run update again to\nbring its tests forward. Every other file in the run still updates.\nA site with no record gets one on its first run, from the version its\n.ailk-site-manifest.json already records. A site scaffolded before\n@working-theory/mcp-server has its copied apps/mcp moved to the boot set:\nthe server and each module's paid MCP pack join apps/mcp/package.json, the\ncopied server files go, and apps/mcp/src/lib/paid-tools.generated.ts is\nrewritten on every run. A site scaffolded before @working-theory/api has its\ncopied apps/api moved to the boot file: the app becomes @working-theory/app-api,\nthe package and the six @working-theory/api-<module> packages join\napps/api/package.json, its bins and build:vercel delegate to the package, and\nthe copied server files go. Run `pnpm install` after either move, since the\napp's package.json gained dependencies. Commit before running, and review the\ndiff after.\n";
61
61
  /** Parse the argv AFTER the `update` word. Strict `node:util` parseArgs, like `parseAdoptArgs`. */
62
62
  export declare function parseUpdateArgs(argv: string[]): UpdateArgs;