create-ailk 0.25.1 → 0.26.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 (60) hide show
  1. package/README.md +12 -3
  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 +12 -2
  8. package/dist/starter-grid.d.ts +6 -6
  9. package/dist/starter-grid.js +12 -5
  10. package/dist/surfaces.js +29 -0
  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 +188 -18
  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 +15 -0
  25. package/templates/apps/api/.env.example +21 -0
  26. package/templates/apps/api/CLAUDE.md +18 -8
  27. package/templates/apps/api/src/lib/__mocks__/prisma-client.js +16 -0
  28. package/templates/apps/mcp/CLAUDE.md +27 -10
  29. package/templates/apps/web/CLAUDE.md +22 -1
  30. package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +43 -4
  31. package/templates/apps/web/app/[locale]/(auth)/legal-footnote.tsx +35 -0
  32. package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +117 -8
  33. package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +14 -37
  34. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +6 -6
  35. package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +20 -35
  36. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +108 -0
  37. package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +111 -5
  38. package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +78 -16
  39. package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +132 -12
  40. package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +53 -1
  41. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +38 -0
  42. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +4 -2
  43. package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +47 -24
  44. package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +23 -4
  45. package/templates/apps/web/app/[locale]/layout.tsx +33 -13
  46. package/templates/apps/web/app/[locale]/meet/confirmed/BookingHandOff.tsx +128 -0
  47. package/templates/apps/web/app/[locale]/meet/confirmed/__tests__/BookingHandOff.test.tsx +126 -0
  48. package/templates/apps/web/auth.ts +8 -4
  49. package/templates/apps/web/i18n/request.ts +49 -1
  50. package/templates/apps/web/lib/__tests__/account-config.test.ts +7 -0
  51. package/templates/apps/web/lib/__tests__/auth-email.test.ts +276 -0
  52. package/templates/apps/web/lib/auth-email.ts +102 -20
  53. package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +72 -23
  54. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +94 -70
  55. package/templates/apps/web/lib/prisma-for-auth.ts +5 -2
  56. package/templates/apps/web/lib/sign-up-agreements.ts +291 -28
  57. package/templates/apps/web/lib/site-contributions.ts +18 -0
  58. package/templates/apps/web/messages/en.json +186 -20
  59. package/templates/apps/web/messages/site/en.json +1 -0
  60. 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
@@ -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,16 @@ 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",
835
845
  ],
836
846
  // patches — the dependency add that delivers the shared package to the
837
847
  // 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
@@ -725,6 +725,12 @@ 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",
728
734
  // #5977 — the Pro content host's editor routes (spec 5872 FR1/FR2), added
729
735
  // to apps/web by #5957. Genuinely new route directories, so they attach at
730
736
  // today's fixed-group version, the same "attach at today's version" rule
@@ -785,6 +791,13 @@ export function surfaceForRouteDir(routeDir) {
785
791
  return "core";
786
792
  return GATEABLE_SURFACES.find((s) => s.routes.includes(top))?.id;
787
793
  }
794
+ /** #9746 D10 — what each Magnets and Meetings tab page imports beside itself. */
795
+ const ADMIN_PANEL_ROUTE_DEPENDENCIES = [
796
+ "apps/web/lib/server-api.ts",
797
+ `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
798
+ `${ROUTE_ROOT}/(authed)/admin/admin-nav.ts`,
799
+ `${ROUTE_ROOT}/(authed)/admin/admin-panels.tsx`,
800
+ ];
788
801
  /**
789
802
  * Non-route consumer-side files a route's own source imports, that a sync
790
803
  * must ALSO bring along so `--apply` never leaves a route that cannot
@@ -964,6 +977,8 @@ export const ROUTE_DEPENDENCIES = {
964
977
  ],
965
978
  "(authed)/account/billing": [
966
979
  "apps/web/lib/account-config.ts",
980
+ // #9401 D7 — the site's own Billing sections, the frozen stub the page imports.
981
+ "apps/web/lib/site-contributions.ts",
967
982
  "apps/web/lib/server-api.ts",
968
983
  `${ROUTE_ROOT}/(authed)/account/account-data.ts`,
969
984
  `${ROUTE_ROOT}/(authed)/account/actions.ts`,
@@ -1034,6 +1049,8 @@ export const ROUTE_DEPENDENCIES = {
1034
1049
  "(authed)/admin": [
1035
1050
  "apps/web/lib/server-api.ts",
1036
1051
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
1052
+ // #9746 D8 — the shared nav every /admin tab imports.
1053
+ `${ROUTE_ROOT}/(authed)/admin/admin-nav.ts`,
1037
1054
  ],
1038
1055
  "(authed)/leads": ["apps/web/lib/server-api.ts"],
1039
1056
  // #5977 — the editor routes' own imports, read from their sources: both
@@ -1053,11 +1070,19 @@ export const ROUTE_DEPENDENCIES = {
1053
1070
  "(authed)/admin/site": [
1054
1071
  "apps/web/lib/server-api.ts",
1055
1072
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
1073
+ `${ROUTE_ROOT}/(authed)/admin/admin-nav.ts`,
1056
1074
  ],
1057
1075
  "(authed)/admin/team": [
1058
1076
  "apps/web/lib/server-api.ts",
1059
1077
  `${ROUTE_ROOT}/(authed)/waitlist/components/ForbiddenState.tsx`,
1078
+ `${ROUTE_ROOT}/(authed)/admin/admin-nav.ts`,
1060
1079
  ],
1080
+ // #9746 D10 — the Magnets and Meetings tabs: the nav plus the client
1081
+ // bindings of the shared admin-ui components.
1082
+ "(authed)/admin/magnets": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1083
+ "(authed)/admin/magnets/new/[kind]": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1084
+ "(authed)/admin/meetings": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1085
+ "(authed)/admin/meetings/[slug]": ADMIN_PANEL_ROUTE_DEPENDENCIES,
1061
1086
  "(authed)/waitlist": ["apps/web/lib/server-api.ts"],
1062
1087
  // #8843 D6 — the booking pages share one builder over the meetings read;
1063
1088
  // `/private` returns the index frame that lives under `meet/`.
@@ -1115,6 +1140,10 @@ export const ROUTE_MESSAGE_NAMESPACES = {
1115
1140
  "(authed)/edit/[type]/[slug]": ["edit"],
1116
1141
  "(authed)/admin/site": ["admin"],
1117
1142
  "(authed)/admin/team": ["admin"],
1143
+ "(authed)/admin/magnets": ["admin"],
1144
+ "(authed)/admin/magnets/new/[kind]": ["admin"],
1145
+ "(authed)/admin/meetings": ["admin"],
1146
+ "(authed)/admin/meetings/[slug]": ["admin"],
1118
1147
  "(authed)/waitlist": ["waitlistDashboard.gate"],
1119
1148
  // #7744 — the /team hub's empty state reads the `team` namespace.
1120
1149
  team: ["team"],
@@ -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;