handoff-app 2.0.0-3 → 2.0.0-5

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 (209) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +302 -378
  4. package/dist/app-builder/build.js +2 -2
  5. package/dist/app-builder/nav-shell.js +8 -15
  6. package/dist/app-builder/vercel-output.js +4 -4
  7. package/dist/cli/make.js +2 -2
  8. package/dist/cli.js +39 -7
  9. package/dist/commands/build/app.js +1 -5
  10. package/dist/commands/build/components.js +1 -5
  11. package/dist/commands/build/index.js +1 -5
  12. package/dist/commands/checkout/index.d.ts +15 -12
  13. package/dist/commands/checkout/index.js +15 -54
  14. package/dist/commands/compat/deprecations.d.ts +46 -0
  15. package/dist/commands/compat/deprecations.js +62 -0
  16. package/dist/commands/compat/push-args.d.ts +48 -0
  17. package/dist/commands/compat/push-args.js +73 -0
  18. package/dist/commands/db/migrate.js +1 -5
  19. package/dist/commands/dev/index.js +1 -5
  20. package/dist/commands/eject/config.js +1 -5
  21. package/dist/commands/eject/pages.js +1 -5
  22. package/dist/commands/eject/theme.js +1 -5
  23. package/dist/commands/fetch/index.js +1 -5
  24. package/dist/commands/index.js +4 -0
  25. package/dist/commands/login/index.js +1 -5
  26. package/dist/commands/logout/index.js +1 -5
  27. package/dist/commands/make/component.js +1 -5
  28. package/dist/commands/make/page.js +3 -7
  29. package/dist/commands/make/template.js +1 -5
  30. package/dist/commands/publish/index.d.ts +18 -12
  31. package/dist/commands/publish/index.js +18 -54
  32. package/dist/commands/pull/index.d.ts +13 -0
  33. package/dist/commands/pull/index.js +20 -0
  34. package/dist/commands/push/index.d.ts +23 -0
  35. package/dist/commands/push/index.js +58 -0
  36. package/dist/commands/scaffold/index.js +1 -5
  37. package/dist/commands/start/index.js +1 -5
  38. package/dist/commands/types.d.ts +10 -0
  39. package/dist/commands/utils.d.ts +74 -6
  40. package/dist/commands/utils.js +89 -10
  41. package/dist/commands/validate/components.js +1 -5
  42. package/dist/config/index.d.ts +2 -1
  43. package/dist/config/index.js +2 -1
  44. package/dist/config/loader.d.ts +19 -6
  45. package/dist/config/loader.js +43 -13
  46. package/dist/config/normalizers/page.d.ts +4 -4
  47. package/dist/config/normalizers/page.js +2 -2
  48. package/dist/config/runtime.js +9 -25
  49. package/dist/index.d.ts +69 -22
  50. package/dist/index.js +148 -35
  51. package/dist/nav/resolver.js +3 -1
  52. package/dist/nav/workspace.js +2 -2
  53. package/dist/registry/auth/access-tokens.d.ts +11 -3
  54. package/dist/registry/auth/access-tokens.js +29 -3
  55. package/dist/registry/auth/index.d.ts +4 -3
  56. package/dist/registry/auth/index.js +10 -2
  57. package/dist/registry/auth/sync-secret.d.ts +27 -0
  58. package/dist/registry/auth/sync-secret.js +65 -0
  59. package/dist/registry/auth/types.d.ts +20 -1
  60. package/dist/registry/auth/types.js +3 -1
  61. package/dist/registry/auth/validation.d.ts +11 -0
  62. package/dist/registry/auth/validation.js +18 -1
  63. package/dist/registry/checkout/assets.d.ts +1 -1
  64. package/dist/registry/checkout/assets.js +21 -6
  65. package/dist/registry/checkout/index.d.ts +8 -1
  66. package/dist/registry/checkout/index.js +39 -6
  67. package/dist/registry/checkout/tokens.d.ts +1 -1
  68. package/dist/registry/checkout/tokens.js +12 -5
  69. package/dist/registry/connection.d.ts +10 -0
  70. package/dist/registry/connection.js +52 -7
  71. package/dist/registry/content-kinds.d.ts +27 -0
  72. package/dist/registry/content-kinds.js +33 -0
  73. package/dist/registry/db/client.d.ts +6 -1
  74. package/dist/registry/db/client.js +16 -3
  75. package/dist/registry/db/schema.d.ts +37 -3
  76. package/dist/registry/db/schema.js +9 -7
  77. package/dist/registry/publish/assets.d.ts +2 -2
  78. package/dist/registry/publish/assets.js +46 -35
  79. package/dist/registry/publish/index.d.ts +29 -5
  80. package/dist/registry/publish/index.js +87 -34
  81. package/dist/registry/publish/tokens.d.ts +2 -2
  82. package/dist/registry/publish/tokens.js +30 -24
  83. package/dist/registry/selection.d.ts +12 -0
  84. package/dist/registry/selection.js +22 -0
  85. package/dist/store/registry.d.ts +48 -0
  86. package/dist/store/registry.js +75 -12
  87. package/dist/transformers/preview/types.d.ts +2 -2
  88. package/dist/utils/markdown.d.ts +10 -0
  89. package/dist/utils/markdown.js +17 -0
  90. package/dist/utils/menu-shell.js +14 -7
  91. package/dist/utils/pages.d.ts +22 -0
  92. package/dist/utils/pages.js +73 -0
  93. package/dist/utils/path.d.ts +5 -0
  94. package/dist/utils/path.js +9 -0
  95. package/docs/api.md +1 -3
  96. package/docs/cli.md +1 -1
  97. package/drizzle/0002_add_pages.sql +5 -3
  98. package/drizzle/meta/0002_snapshot.json +17 -8
  99. package/drizzle/meta/0003_snapshot.json +17 -8
  100. package/drizzle/meta/0004_snapshot.json +17 -8
  101. package/drizzle/meta/0005_snapshot.json +17 -8
  102. package/package.json +1 -1
  103. package/src/app/components/util/index.ts +5 -23
  104. package/src/app/lib/docs-api/backend.ts +11 -0
  105. package/src/app/lib/docs-api/errors.ts +8 -7
  106. package/src/app/lib/docs-api/handler.ts +2 -3
  107. package/src/app/lib/docs-api/index.ts +2 -0
  108. package/src/app/lib/docs-api/page-discovery.ts +126 -0
  109. package/src/app/lib/docs-api/page-rendering.ts +17 -0
  110. package/src/app/lib/docs-api/page-search.ts +191 -0
  111. package/src/app/lib/docs-api/records.ts +11 -19
  112. package/src/app/lib/docs-api/registry.ts +90 -11
  113. package/src/app/lib/docs-api/search.ts +115 -0
  114. package/src/app/lib/registry-api/allowlist.ts +7 -5
  115. package/src/app/lib/registry-api/asset-transfer.ts +58 -31
  116. package/src/app/lib/registry-api/auth.ts +89 -9
  117. package/src/app/lib/registry-api/entity-routes.ts +17 -7
  118. package/src/app/lib/registry-api/errors.ts +10 -4
  119. package/src/app/lib/registry-api/handler.ts +13 -15
  120. package/src/app/lib/registry-api/meta.ts +4 -0
  121. package/src/app/lib/registry-api/revalidate.ts +31 -12
  122. package/src/app/lib/registry-api/store.ts +89 -26
  123. package/src/app/lib/registry-api/sync-compat.ts +166 -0
  124. package/src/app/lib/registry-api/token-transfer.ts +44 -12
  125. package/src/app/lib/registry-api/transfer-batch.ts +116 -0
  126. package/src/app/lib/registry-api/transfer.ts +62 -35
  127. package/src/app/lib/registry-api/validation.ts +33 -1
  128. package/src/app/lib/registry-connection.ts +1 -1
  129. package/src/app/next.config.mjs +6 -0
  130. package/src/app/pages/[...slug]/index.tsx +37 -22
  131. package/src/app/pages/api/account/tokens/index.ts +4 -13
  132. package/src/app/pages/api/docs/search/pages.json.ts +25 -0
  133. package/src/app/pages/api/oauth/revoke.ts +14 -7
  134. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
  135. package/src/app/pages/api/registry/components/[id].ts +2 -2
  136. package/src/app/pages/api/registry/components/index.ts +1 -1
  137. package/src/app/pages/api/registry/pages/[...id].ts +20 -0
  138. package/src/app/pages/api/registry/pages/index.ts +19 -0
  139. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
  140. package/src/app/pages/api/registry/patterns/[id].ts +2 -2
  141. package/src/app/pages/api/registry/patterns/index.ts +1 -1
  142. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
  143. package/src/app/pages/api/sync/upload.ts +21 -0
  144. package/src/app/pages/index/index.tsx +14 -0
  145. package/src/app/pages/index.tsx +30 -5
  146. package/src/app-builder/build.ts +2 -2
  147. package/src/app-builder/nav-shell.ts +8 -13
  148. package/src/app-builder/vercel-output.ts +4 -4
  149. package/src/cli/make.ts +2 -2
  150. package/src/cli.ts +31 -7
  151. package/src/commands/build/app.ts +2 -3
  152. package/src/commands/build/components.ts +2 -3
  153. package/src/commands/build/index.ts +2 -3
  154. package/src/commands/checkout/index.ts +18 -44
  155. package/src/commands/compat/deprecations.ts +79 -0
  156. package/src/commands/compat/push-args.ts +106 -0
  157. package/src/commands/db/migrate.ts +2 -3
  158. package/src/commands/dev/index.ts +2 -3
  159. package/src/commands/eject/config.ts +2 -3
  160. package/src/commands/eject/pages.ts +2 -3
  161. package/src/commands/eject/theme.ts +2 -3
  162. package/src/commands/fetch/index.ts +2 -3
  163. package/src/commands/index.ts +4 -0
  164. package/src/commands/login/index.ts +2 -3
  165. package/src/commands/logout/index.ts +2 -3
  166. package/src/commands/make/component.ts +2 -3
  167. package/src/commands/make/page.ts +4 -5
  168. package/src/commands/make/template.ts +2 -3
  169. package/src/commands/publish/index.ts +21 -44
  170. package/src/commands/pull/index.ts +32 -0
  171. package/src/commands/push/index.ts +115 -0
  172. package/src/commands/scaffold/index.ts +2 -3
  173. package/src/commands/start/index.ts +2 -3
  174. package/src/commands/types.ts +11 -1
  175. package/src/commands/utils.ts +80 -11
  176. package/src/commands/validate/components.ts +2 -3
  177. package/src/config/index.ts +2 -1
  178. package/src/config/loader.ts +57 -12
  179. package/src/config/normalizers/page.ts +4 -4
  180. package/src/config/runtime.ts +9 -24
  181. package/src/index.ts +183 -42
  182. package/src/nav/resolver.ts +3 -1
  183. package/src/nav/workspace.ts +2 -2
  184. package/src/registry/auth/access-tokens.ts +29 -5
  185. package/src/registry/auth/index.ts +7 -1
  186. package/src/registry/auth/sync-secret.ts +67 -0
  187. package/src/registry/auth/types.ts +24 -1
  188. package/src/registry/auth/validation.ts +18 -0
  189. package/src/registry/checkout/assets.ts +24 -7
  190. package/src/registry/checkout/index.ts +41 -5
  191. package/src/registry/checkout/tokens.ts +12 -6
  192. package/src/registry/connection.ts +53 -5
  193. package/src/registry/content-kinds.ts +39 -0
  194. package/src/registry/db/client.ts +22 -2
  195. package/src/registry/db/schema.ts +9 -7
  196. package/src/registry/publish/assets.ts +47 -37
  197. package/src/registry/publish/index.ts +102 -38
  198. package/src/registry/publish/tokens.ts +35 -26
  199. package/src/registry/selection.ts +19 -0
  200. package/src/store/registry.ts +118 -13
  201. package/src/transformers/preview/types.ts +2 -2
  202. package/src/utils/README.md +1 -0
  203. package/src/utils/markdown.ts +11 -0
  204. package/src/utils/menu-shell.ts +14 -7
  205. package/src/utils/pages.ts +52 -0
  206. package/src/utils/path.ts +15 -6
  207. package/templates/default/package.json.tpl +4 -0
  208. package/config/docs/foundations/logos.md +0 -7
  209. package/src/app/lib/api/redact.ts +0 -46
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Whether a credential can be presented unambiguously in a request header. One rule shared by the
3
+ * authentication headers and by whatever a deployment configures as a secret, so a value is never
4
+ * accepted through one carrier and rejected by the other.
5
+ *
6
+ * Whitespace is out so nothing can be trimmed away or smuggled past a parser, and the comma with it:
7
+ * repeats of a header Node does not de-duplicate arrive joined with `, `, and no accepted credential
8
+ * may look like that. Non-ASCII bytes are out too, since their header encoding is ambiguous and a
9
+ * credential has to reproduce exactly.
10
+ */
11
+ export declare const isTransmittableCredential: (value: string) => boolean;
1
12
  export declare const validateRegistryEmail: (email: string) => string | null;
2
13
  export declare const validateRegistryDisplayName: (name: string) => string | null;
3
14
  export declare const validateRegistryImageUrl: (image: string | null | undefined) => string | null | false;
@@ -1,8 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateRegistryImageUrl = exports.validateRegistryDisplayName = exports.validateRegistryEmail = void 0;
3
+ exports.validateRegistryImageUrl = exports.validateRegistryDisplayName = exports.validateRegistryEmail = exports.isTransmittableCredential = void 0;
4
4
  const crypto_1 = require("./crypto");
5
5
  const SIMPLE_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
+ /**
7
+ * Printable ASCII (`!` through `~`) except the comma: HTTP's `VCHAR`, minus the character Node joins
8
+ * duplicate header values with.
9
+ */
10
+ const TRANSMITTABLE_CREDENTIAL_PATTERN = /^[\x21-\x2B\x2D-\x7E]+$/;
11
+ /**
12
+ * Whether a credential can be presented unambiguously in a request header. One rule shared by the
13
+ * authentication headers and by whatever a deployment configures as a secret, so a value is never
14
+ * accepted through one carrier and rejected by the other.
15
+ *
16
+ * Whitespace is out so nothing can be trimmed away or smuggled past a parser, and the comma with it:
17
+ * repeats of a header Node does not de-duplicate arrive joined with `, `, and no accepted credential
18
+ * may look like that. Non-ASCII bytes are out too, since their header encoding is ambiguous and a
19
+ * credential has to reproduce exactly.
20
+ */
21
+ const isTransmittableCredential = (value) => TRANSMITTABLE_CREDENTIAL_PATTERN.test(value);
22
+ exports.isTransmittableCredential = isTransmittableCredential;
6
23
  const validateRegistryEmail = (email) => {
7
24
  const normalized = (0, crypto_1.normalizeEmail)(email);
8
25
  return normalized.length <= 320 && SIMPLE_EMAIL_PATTERN.test(normalized) ? normalized : null;
@@ -13,4 +13,4 @@ import Handoff from '../../index';
13
13
  * Checkout all published asset collections, or a single collection when `collection` is given.
14
14
  * Recreates workspace asset files; prompts before overwriting changed local files unless `--force`.
15
15
  */
16
- export declare const checkoutAssets: (handoff: Handoff, collection?: string) => Promise<void>;
16
+ export declare const checkoutAssets: (handoff: Handoff, selection?: string[]) => Promise<void>;
@@ -84,15 +84,19 @@ const wouldChange = (absolutePath, bytes) => {
84
84
  * Checkout all published asset collections, or a single collection when `collection` is given.
85
85
  * Recreates workspace asset files; prompts before overwriting changed local files unless `--force`.
86
86
  */
87
- const checkoutAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0, function* () {
87
+ const checkoutAssets = (handoff, selection) => __awaiter(void 0, void 0, void 0, function* () {
88
+ var _a;
88
89
  const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
89
- if (collection && !(0, sets_1.isAssetCollection)(collection)) {
90
- throw new index_1.CheckoutError(`Unknown asset collection "${collection}". Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
90
+ const unsupported = (_a = selection === null || selection === void 0 ? void 0 : selection.filter((name) => !(0, sets_1.isAssetCollection)(name))) !== null && _a !== void 0 ? _a : [];
91
+ if (unsupported.length > 0) {
92
+ throw new index_1.CheckoutError(`Unknown asset collection ${unsupported.map((name) => `"${name}"`).join(', ')}. ` +
93
+ `Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
91
94
  }
92
95
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
96
+ // A named selection is trusted as-is; only a full checkout has to ask the registry what exists.
93
97
  let collections;
94
- if (collection) {
95
- collections = [collection];
98
+ if (selection) {
99
+ collections = selection;
96
100
  }
97
101
  else {
98
102
  const summaries = yield client.listAssetCollections();
@@ -123,16 +127,27 @@ const checkoutAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0
123
127
  // Download each referenced blob once (dedup by hash), then resolve every asset to a physical write.
124
128
  const blobCache = new Map();
125
129
  const writes = [];
130
+ const plannedPaths = [];
126
131
  for (const payload of payloads) {
127
132
  for (const asset of payload.assets) {
133
+ const absolutePath = (0, layout_1.resolveAssetPhysicalPath)(asset.path, roots);
134
+ plannedPaths.push(absolutePath);
135
+ // A dry run reports paths only, so there is no reason to pull the bytes down.
136
+ if (handoff.dryRun) {
137
+ continue;
138
+ }
128
139
  let bytes = blobCache.get(asset.contentHash);
129
140
  if (!bytes) {
130
141
  bytes = yield client.downloadAssetBlob(asset.contentHash);
131
142
  blobCache.set(asset.contentHash, bytes);
132
143
  }
133
- writes.push({ absolutePath: (0, layout_1.resolveAssetPhysicalPath)(asset.path, roots), bytes });
144
+ writes.push({ absolutePath, bytes });
134
145
  }
135
146
  }
147
+ if (handoff.dryRun) {
148
+ (0, index_1.reportPlannedWrites)(handoff, 'asset collections', plannedPaths, plannedPaths.filter((target) => fs_extra_1.default.existsSync(target)));
149
+ return;
150
+ }
136
151
  const existingChanges = writes.filter((write) => fs_extra_1.default.existsSync(write.absolutePath) && wouldChange(write.absolutePath, write.bytes));
137
152
  if (existingChanges.length > 0 && !handoff.force) {
138
153
  const proceed = yield p.confirm({
@@ -24,6 +24,13 @@ export declare class CheckoutError extends Error {
24
24
  * actionable {@link CheckoutError} naming the exact misconfiguration.
25
25
  */
26
26
  export declare const resolveConnectionOrThrow: (handoff: Handoff) => Promise<import("../connection").ResolvedRegistryConnection>;
27
+ /**
28
+ * Report the files a checkout would create or replace, and write nothing. Called once the real
29
+ * checkout has computed its targets but before it prompts or writes, so a dry run exercises the same
30
+ * path resolution and conflict detection as the real thing. Shared by the entity, page, token and
31
+ * asset checkout paths.
32
+ */
33
+ export declare const reportPlannedWrites: (handoff: Handoff, label: string, targets: string[], conflicts: string[]) => void;
27
34
  /**
28
35
  * Checkout a single component, pattern, or page from the connected registry into this workspace.
29
36
  * Overwriting existing local files requires `--force` or an interactive confirmation.
@@ -35,4 +42,4 @@ export declare const checkoutEntity: (handoff: Handoff, kind: TransferEntityKind
35
42
  * is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
36
43
  * prompting is per entity (skipped under `--force`).
37
44
  */
38
- export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind) => Promise<void>;
45
+ export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind, ids?: string[]) => Promise<void>;
@@ -60,7 +60,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
60
60
  return (mod && mod.__esModule) ? mod : { "default": mod };
61
61
  };
62
62
  Object.defineProperty(exports, "__esModule", { value: true });
63
- exports.checkoutEntities = exports.checkoutEntity = exports.resolveConnectionOrThrow = exports.CheckoutError = void 0;
63
+ exports.checkoutEntities = exports.checkoutEntity = exports.reportPlannedWrites = exports.resolveConnectionOrThrow = exports.CheckoutError = void 0;
64
64
  const p = __importStar(require("@clack/prompts"));
65
65
  const fs_extra_1 = __importDefault(require("fs-extra"));
66
66
  const path_1 = __importDefault(require("path"));
@@ -70,6 +70,7 @@ const logger_1 = require("../../utils/logger");
70
70
  const client_1 = require("../client");
71
71
  const connection_1 = require("../connection");
72
72
  const path_2 = require("../path");
73
+ const selection_1 = require("../selection");
73
74
  /** A connected-workspace configuration or precondition failure surfaced to the CLI. */
74
75
  class CheckoutError extends Error {
75
76
  constructor(message) {
@@ -568,6 +569,21 @@ const writeSourceFile = (targetDir, file) => __awaiter(void 0, void 0, void 0, f
568
569
  yield fs_extra_1.default.writeFile(absolutePath, file.content, 'utf8');
569
570
  return absolutePath;
570
571
  });
572
+ /**
573
+ * Report the files a checkout would create or replace, and write nothing. Called once the real
574
+ * checkout has computed its targets but before it prompts or writes, so a dry run exercises the same
575
+ * path resolution and conflict detection as the real thing. Shared by the entity, page, token and
576
+ * asset checkout paths.
577
+ */
578
+ const reportPlannedWrites = (handoff, label, targets, conflicts) => {
579
+ const conflicting = new Set(conflicts);
580
+ logger_1.Logger.info(`Would checkout ${label} (${targets.length} file(s)):`);
581
+ for (const target of targets) {
582
+ const relative = path_1.default.relative(handoff.workingPath, target) || target;
583
+ logger_1.Logger.info(` ${conflicting.has(target) ? 'overwrite' : 'create'}: ${relative}`);
584
+ }
585
+ };
586
+ exports.reportPlannedWrites = reportPlannedWrites;
571
587
  /**
572
588
  * Checkout a page: write its single verbatim `.md` (transfer path `<id>.md`) back under
573
589
  * `<workingPath>/pages/`. Unlike components/patterns there is no declaration to synthesize — the
@@ -583,6 +599,10 @@ const checkoutPage = (handoff, id, payload) => __awaiter(void 0, void 0, void 0,
583
599
  return target;
584
600
  });
585
601
  const conflicts = targets.filter((file) => fs_extra_1.default.existsSync(file));
602
+ if (handoff.dryRun) {
603
+ (0, exports.reportPlannedWrites)(handoff, `page "${id}"`, targets, conflicts);
604
+ return;
605
+ }
586
606
  if (!(yield confirmOverwrite(handoff, conflicts))) {
587
607
  logger_1.Logger.warn(`Checkout of page "${id}" cancelled; no files were written.`);
588
608
  return;
@@ -633,7 +653,13 @@ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(voi
633
653
  }
634
654
  return { file, absolutePath };
635
655
  });
636
- const conflicts = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath].filter((file) => fs_extra_1.default.existsSync(file));
656
+ const plannedWrites = [...sourceTargets.map((entry) => entry.absolutePath), declarationPath];
657
+ const conflicts = plannedWrites.filter((file) => fs_extra_1.default.existsSync(file));
658
+ if (handoff.dryRun) {
659
+ // Returning null also skips `registerCheckedOut`, so a dry run never edits handoff.config either.
660
+ (0, exports.reportPlannedWrites)(handoff, `${kind} "${id}"`, plannedWrites, conflicts);
661
+ return null;
662
+ }
637
663
  if (!(yield confirmOverwrite(handoff, conflicts))) {
638
664
  logger_1.Logger.warn(`Checkout of ${kind} "${id}" cancelled; no files were written.`);
639
665
  return null;
@@ -694,7 +720,7 @@ exports.checkoutEntity = checkoutEntity;
694
720
  * is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
695
721
  * prompting is per entity (skipped under `--force`).
696
722
  */
697
- const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
723
+ const checkoutEntities = (handoff, kind, ids) => __awaiter(void 0, void 0, void 0, function* () {
698
724
  const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
699
725
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
700
726
  let summaries;
@@ -707,14 +733,20 @@ const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fu
707
733
  }
708
734
  throw error;
709
735
  }
710
- if (summaries.length === 0) {
736
+ const published = summaries.map((summary) => summary.id);
737
+ const { selected: targets, unknown } = (0, selection_1.selectIds)(published, ids);
738
+ if (unknown.length > 0) {
739
+ throw new CheckoutError(`No ${kind} named ${unknown.map((id) => `"${id}"`).join(', ')} is published in the registry. ` +
740
+ `Published ${kind}s: ${published.join(', ') || '(none)'}.`);
741
+ }
742
+ if (targets.length === 0) {
711
743
  logger_1.Logger.success(`No ${kind}s are published in the registry; nothing to checkout.`);
712
744
  return;
713
745
  }
714
746
  let checkedOut = 0;
715
747
  const failed = [];
716
748
  const targetDirs = [];
717
- for (const { id } of summaries) {
749
+ for (const id of targets) {
718
750
  try {
719
751
  const targetDir = yield checkoutSingle(handoff, kind, id, client, connection.url);
720
752
  if (targetDir) {
@@ -728,7 +760,8 @@ const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fu
728
760
  }
729
761
  // Register everything checked out in one pass, so the config is edited (and confirmed) once.
730
762
  yield registerCheckedOut(handoff, kind, targetDirs);
731
- logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} checked out${failed.length ? `, ${failed.length} failed` : ''}.`);
763
+ logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} ${handoff.dryRun ? 'would be checked out' : 'checked out'}` +
764
+ `${failed.length ? `, ${failed.length} failed` : ''}.`);
732
765
  if (failed.length > 0) {
733
766
  for (const failure of failed) {
734
767
  logger_1.Logger.error(` - ${failure.id}: ${failure.message}`);
@@ -12,4 +12,4 @@ import Handoff from '../../index';
12
12
  * Checkout all published token sets, or a single set when `setId` is given. Reconstructs `tokens.json`
13
13
  * and restores generated files; prompts before overwriting changed local files unless `--force`.
14
14
  */
15
- export declare const checkoutTokens: (handoff: Handoff, setId?: string) => Promise<void>;
15
+ export declare const checkoutTokens: (handoff: Handoff, selection?: string[]) => Promise<void>;
@@ -97,15 +97,18 @@ const wouldChange = (absolutePath, content) => {
97
97
  * Checkout all published token sets, or a single set when `setId` is given. Reconstructs `tokens.json`
98
98
  * and restores generated files; prompts before overwriting changed local files unless `--force`.
99
99
  */
100
- const checkoutTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, function* () {
101
- if (setId && !(0, sets_1.isTokenSetId)(setId)) {
102
- throw new index_1.CheckoutError(`Unknown or unsafe token set id "${setId}".`);
100
+ const checkoutTokens = (handoff, selection) => __awaiter(void 0, void 0, void 0, function* () {
101
+ var _a;
102
+ const unsupported = (_a = selection === null || selection === void 0 ? void 0 : selection.filter((id) => !(0, sets_1.isTokenSetId)(id))) !== null && _a !== void 0 ? _a : [];
103
+ if (unsupported.length > 0) {
104
+ throw new index_1.CheckoutError(`Unknown or unsafe token set id ${unsupported.map((id) => `"${id}"`).join(', ')}.`);
103
105
  }
104
106
  const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
105
107
  const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
108
+ // A named selection is trusted as-is; only a full checkout has to ask the registry what exists.
106
109
  let ids;
107
- if (setId) {
108
- ids = [setId];
110
+ if (selection) {
111
+ ids = selection;
109
112
  }
110
113
  else {
111
114
  const summaries = yield client.listTokenSets();
@@ -153,6 +156,10 @@ const checkoutTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, fun
153
156
  }
154
157
  }
155
158
  const existingChanges = changed.filter((filePath) => fs_extra_1.default.existsSync(filePath));
159
+ if (handoff.dryRun) {
160
+ (0, index_1.reportPlannedWrites)(handoff, 'token sets', changed, existingChanges);
161
+ return;
162
+ }
156
163
  if (existingChanges.length > 0 && !handoff.force) {
157
164
  const proceed = yield p.confirm({
158
165
  message: `Checkout will overwrite ${existingChanges.length} changed local token file(s). Continue?`,
@@ -6,6 +6,9 @@
6
6
  * config + environment: the registry URL may be given inline or via an env-var *name*, and the
7
7
  * access token is always read from an env-var name (never persisted as a value). This is the single
8
8
  * source the publish (and later checkout) client reads its target + credentials from.
9
+ *
10
+ * The older `HANDOFF_CLOUD_*` names still work as deprecated fallbacks; see
11
+ * {@link DEPRECATED_ENV_ALIASES}.
9
12
  */
10
13
  import type { Config } from '../types/config';
11
14
  /** Default env-var name holding the remote registry base URL. */
@@ -26,6 +29,9 @@ export interface ResolvedRegistryConnection {
26
29
  /**
27
30
  * Resolve the connected-workspace registry connection from config + environment. Never throws —
28
31
  * Callers inspect the resolved URL and access token to surface specific configuration errors.
32
+ *
33
+ * `urlEnv` and `accessTokenEnv` always report the canonical name, even when an alias supplied the
34
+ * value, so errors built from them point at the variable to adopt.
29
35
  */
30
36
  export declare const resolveRegistryConnection: (config: Config | null | undefined) => ResolvedRegistryConnection;
31
37
  /**
@@ -35,5 +41,9 @@ export declare const resolveRegistryConnection: (config: Config | null | undefin
35
41
  * keep precedence for deterministic CI: `HANDOFF_REGISTRY_URL` (or inline config) wins over the
36
42
  * saved URL, and `HANDOFF_REGISTRY_ACCESS_TOKEN` wins over the saved token. The saved token only
37
43
  * applies to the URL it was issued for.
44
+ *
45
+ * A configured `HANDOFF_SYNC_SECRET` is the last resort. Being deployment-wide rather than personal,
46
+ * it fills in only where there is no explicit token and no login (CI, in practice), instead of quietly
47
+ * speaking for a developer who is signed in.
38
48
  */
39
49
  export declare const resolveAuthenticatedRegistryConnection: (config: Config | null | undefined, workingPath?: string) => Promise<ResolvedRegistryConnection>;
@@ -7,6 +7,9 @@
7
7
  * config + environment: the registry URL may be given inline or via an env-var *name*, and the
8
8
  * access token is always read from an env-var name (never persisted as a value). This is the single
9
9
  * source the publish (and later checkout) client reads its target + credentials from.
10
+ *
11
+ * The older `HANDOFF_CLOUD_*` names still work as deprecated fallbacks; see
12
+ * {@link DEPRECATED_ENV_ALIASES}.
10
13
  */
11
14
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12
15
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
@@ -20,22 +23,60 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
20
23
  Object.defineProperty(exports, "__esModule", { value: true });
21
24
  exports.resolveAuthenticatedRegistryConnection = exports.resolveRegistryConnection = exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV = exports.DEFAULT_REGISTRY_URL_ENV = void 0;
22
25
  const store_1 = require("../cli/auth/store");
26
+ // Imported straight from the module, not the `auth` barrel, to keep the database schema off the CLI
27
+ // startup path; this file is resolved for every command.
28
+ const sync_secret_1 = require("./auth/sync-secret");
29
+ const logger_1 = require("../utils/logger");
23
30
  /** Default env-var name holding the remote registry base URL. */
24
31
  exports.DEFAULT_REGISTRY_URL_ENV = 'HANDOFF_REGISTRY_URL';
25
32
  /** Default env-var name holding the connected-workspace access token. */
26
33
  exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV = 'HANDOFF_REGISTRY_ACCESS_TOKEN';
34
+ /**
35
+ * Old env-var names we still read, keyed by the name that replaced them. Keeps workspaces set up
36
+ * from the older docs working instead of silently reporting no registry.
37
+ */
38
+ const DEPRECATED_ENV_ALIASES = {
39
+ [exports.DEFAULT_REGISTRY_URL_ENV]: 'HANDOFF_CLOUD_URL',
40
+ [exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV]: 'HANDOFF_CLOUD_TOKEN',
41
+ };
42
+ /** Aliases we've already warned about, so each one warns once per run and not once per lookup. */
43
+ const warnedAliases = new Set();
44
+ /**
45
+ * Value of the named env var, falling back to its deprecated alias. Only the default names have
46
+ * aliases: a custom `urlEnv`/`accessTokenEnv` is a deliberate choice, so a stray `HANDOFF_CLOUD_*`
47
+ * must not stand in for it. The warning goes through {@link Logger.warn}, so
48
+ * `HANDOFF_LOG_LEVEL=error` silences it in CI.
49
+ */
50
+ const readConnectionEnv = (name) => {
51
+ var _a, _b;
52
+ const configured = (_a = process.env[name]) === null || _a === void 0 ? void 0 : _a.trim();
53
+ if (configured)
54
+ return configured;
55
+ const alias = DEPRECATED_ENV_ALIASES[name];
56
+ const inherited = alias ? (_b = process.env[alias]) === null || _b === void 0 ? void 0 : _b.trim() : '';
57
+ if (!inherited)
58
+ return '';
59
+ if (!warnedAliases.has(alias)) {
60
+ warnedAliases.add(alias);
61
+ logger_1.Logger.warn(`${alias} is deprecated and will be removed in a future release; set ${name} instead.`);
62
+ }
63
+ return inherited;
64
+ };
27
65
  /**
28
66
  * Resolve the connected-workspace registry connection from config + environment. Never throws —
29
67
  * Callers inspect the resolved URL and access token to surface specific configuration errors.
68
+ *
69
+ * `urlEnv` and `accessTokenEnv` always report the canonical name, even when an alias supplied the
70
+ * value, so errors built from them point at the variable to adopt.
30
71
  */
31
72
  const resolveRegistryConnection = (config) => {
32
- var _a, _b, _c, _d, _e, _f, _g, _h;
73
+ var _a, _b, _c, _d;
33
74
  const connection = (_a = config === null || config === void 0 ? void 0 : config.runtime) === null || _a === void 0 ? void 0 : _a.registryConnection;
34
75
  const urlEnv = ((_b = connection === null || connection === void 0 ? void 0 : connection.urlEnv) === null || _b === void 0 ? void 0 : _b.trim()) || exports.DEFAULT_REGISTRY_URL_ENV;
35
76
  const accessTokenEnv = ((_c = connection === null || connection === void 0 ? void 0 : connection.accessTokenEnv) === null || _c === void 0 ? void 0 : _c.trim()) || exports.DEFAULT_REGISTRY_ACCESS_TOKEN_ENV;
36
77
  const inlineUrl = (_d = connection === null || connection === void 0 ? void 0 : connection.url) === null || _d === void 0 ? void 0 : _d.trim();
37
- const url = (_f = (inlineUrl || ((_e = process.env[urlEnv]) === null || _e === void 0 ? void 0 : _e.trim()))) !== null && _f !== void 0 ? _f : '';
38
- const accessToken = (_h = (_g = process.env[accessTokenEnv]) === null || _g === void 0 ? void 0 : _g.trim()) !== null && _h !== void 0 ? _h : '';
78
+ const url = inlineUrl || readConnectionEnv(urlEnv);
79
+ const accessToken = readConnectionEnv(accessTokenEnv);
39
80
  return {
40
81
  url,
41
82
  accessToken,
@@ -51,14 +92,18 @@ exports.resolveRegistryConnection = resolveRegistryConnection;
51
92
  * keep precedence for deterministic CI: `HANDOFF_REGISTRY_URL` (or inline config) wins over the
52
93
  * saved URL, and `HANDOFF_REGISTRY_ACCESS_TOKEN` wins over the saved token. The saved token only
53
94
  * applies to the URL it was issued for.
95
+ *
96
+ * A configured `HANDOFF_SYNC_SECRET` is the last resort. Being deployment-wide rather than personal,
97
+ * it fills in only where there is no explicit token and no login (CI, in practice), instead of quietly
98
+ * speaking for a developer who is signed in.
54
99
  */
55
100
  const resolveAuthenticatedRegistryConnection = (config_1, ...args_1) => __awaiter(void 0, [config_1, ...args_1], void 0, function* (config, workingPath = process.cwd()) {
56
101
  const connection = (0, exports.resolveRegistryConnection)(config);
57
102
  const auth = yield (0, store_1.readCliAuth)(workingPath);
58
- if (!(0, store_1.cliAuthTokenIsValid)(auth))
59
- return connection;
60
- const url = connection.url || auth.remoteUrl;
61
- const accessToken = connection.accessToken || ((0, store_1.cliAuthMatchesRegistry)(auth, url) ? auth.accessToken : '');
103
+ const login = (0, store_1.cliAuthTokenIsValid)(auth) ? auth : null;
104
+ const url = connection.url || (login === null || login === void 0 ? void 0 : login.remoteUrl) || '';
105
+ const savedToken = login && (0, store_1.cliAuthMatchesRegistry)(login, url) ? login.accessToken : '';
106
+ const accessToken = connection.accessToken || savedToken || (0, sync_secret_1.resolveRegistrySyncSecret)();
62
107
  return Object.assign(Object.assign({}, connection), { url, accessToken });
63
108
  });
64
109
  exports.resolveAuthenticatedRegistryConnection = resolveAuthenticatedRegistryConnection;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Logical content-kind vocabulary: the dependency-light boundary shared by the CLI, the
3
+ * publish/checkout orchestration, and the compatibility layer.
4
+ *
5
+ * A workspace publishes and checks out five kinds of content. Components, patterns and pages are
6
+ * entities with a singular wire kind ({@link TransferEntityKind}); tokens and assets travel through
7
+ * their own transfer endpoints. This module owns only the stable plural vocabulary and its ordering,
8
+ * so it stays free of fs, DB and React dependencies.
9
+ */
10
+ import type { TransferEntityKind } from './transfer';
11
+ /** Stable identity used to publish `pages/index.md` while serving it at the root route. */
12
+ export declare const HOME_PAGE_ID = "index";
13
+ export declare const HOME_PAGE_PATH = "/";
14
+ /** Plural content kinds accepted wherever a publish or checkout target is named. */
15
+ export declare const REGISTRY_ENTITY_KINDS: readonly ["components", "patterns", "pages", "tokens", "assets"];
16
+ export type RegistryEntityKind = (typeof REGISTRY_ENTITY_KINDS)[number];
17
+ /** Map the plural content kind to the singular wire kind used by entity publish/checkout. */
18
+ export declare const ENTITY_WIRE_KIND: Record<'components' | 'patterns' | 'pages', TransferEntityKind>;
19
+ /** The entity kinds, in the order a full run visits them: patterns compose components, pages reference both. */
20
+ export declare const ENTITY_KIND_ORDER: readonly ["components", "patterns", "pages"];
21
+ /**
22
+ * Every kind, in dependency order. Tokens and assets come first because rendered component
23
+ * artifacts reference the CSS variables and asset URLs they produce.
24
+ */
25
+ export declare const ALL_KIND_ORDER: readonly RegistryEntityKind[];
26
+ /** Whether a string names a supported content kind. */
27
+ export declare const isRegistryEntityKind: (value: string) => value is RegistryEntityKind;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * Logical content-kind vocabulary: the dependency-light boundary shared by the CLI, the
4
+ * publish/checkout orchestration, and the compatibility layer.
5
+ *
6
+ * A workspace publishes and checks out five kinds of content. Components, patterns and pages are
7
+ * entities with a singular wire kind ({@link TransferEntityKind}); tokens and assets travel through
8
+ * their own transfer endpoints. This module owns only the stable plural vocabulary and its ordering,
9
+ * so it stays free of fs, DB and React dependencies.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.isRegistryEntityKind = exports.ALL_KIND_ORDER = exports.ENTITY_KIND_ORDER = exports.ENTITY_WIRE_KIND = exports.REGISTRY_ENTITY_KINDS = exports.HOME_PAGE_PATH = exports.HOME_PAGE_ID = void 0;
13
+ /** Stable identity used to publish `pages/index.md` while serving it at the root route. */
14
+ exports.HOME_PAGE_ID = 'index';
15
+ exports.HOME_PAGE_PATH = '/';
16
+ /** Plural content kinds accepted wherever a publish or checkout target is named. */
17
+ exports.REGISTRY_ENTITY_KINDS = ['components', 'patterns', 'pages', 'tokens', 'assets'];
18
+ /** Map the plural content kind to the singular wire kind used by entity publish/checkout. */
19
+ exports.ENTITY_WIRE_KIND = {
20
+ components: 'component',
21
+ patterns: 'pattern',
22
+ pages: 'page',
23
+ };
24
+ /** The entity kinds, in the order a full run visits them: patterns compose components, pages reference both. */
25
+ exports.ENTITY_KIND_ORDER = ['components', 'patterns', 'pages'];
26
+ /**
27
+ * Every kind, in dependency order. Tokens and assets come first because rendered component
28
+ * artifacts reference the CSS variables and asset URLs they produce.
29
+ */
30
+ exports.ALL_KIND_ORDER = ['tokens', 'assets', ...exports.ENTITY_KIND_ORDER];
31
+ /** Whether a string names a supported content kind. */
32
+ const isRegistryEntityKind = (value) => exports.REGISTRY_ENTITY_KINDS.includes(value);
33
+ exports.isRegistryEntityKind = isRegistryEntityKind;
@@ -26,10 +26,15 @@ export interface RegistryDbConnection {
26
26
  export interface CreateRegistryDbConnectionParams {
27
27
  driver: RegistryDatabaseDriver;
28
28
  connectionString: string;
29
+ /**
30
+ * Apply {@link SERVING_POOL_LIMITS}. `db:migrate` leaves this unset, because a migration statement can run for
31
+ * minutes.
32
+ */
33
+ serving?: boolean;
29
34
  }
30
35
  /**
31
36
  * Create a registry database connection for the resolved driver. Both drivers target the
32
37
  * Postgres dialect and share the same schema, so the returned connection is interchangeable for
33
38
  * migration and (later) the registry store.
34
39
  */
35
- export declare const createRegistryDbConnection: ({ driver, connectionString, }: CreateRegistryDbConnectionParams) => Promise<RegistryDbConnection>;
40
+ export declare const createRegistryDbConnection: ({ driver, connectionString, serving, }: CreateRegistryDbConnectionParams) => Promise<RegistryDbConnection>;
@@ -42,13 +42,26 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.createRegistryDbConnection = void 0;
44
44
  const schema = __importStar(require("./schema"));
45
+ /**
46
+ * One pool backs the anonymous docs read API, the authenticated management API, and every auth query, so a request
47
+ * must not hold a connection, or wait for one, without a deadline.
48
+ *
49
+ * `statement_timeout` bounds one statement, not a transaction, so it does not cut a publish short — an ingest is many
50
+ * small row writes in one transaction. `connectionTimeoutMillis` makes a saturated pool fail a request instead of
51
+ * queueing it forever, which is the `pg` default.
52
+ */
53
+ const SERVING_POOL_LIMITS = {
54
+ statement_timeout: 15000,
55
+ connectionTimeoutMillis: 5000,
56
+ };
45
57
  /**
46
58
  * Create a registry database connection for the resolved driver. Both drivers target the
47
59
  * Postgres dialect and share the same schema, so the returned connection is interchangeable for
48
60
  * migration and (later) the registry store.
49
61
  */
50
- const createRegistryDbConnection = (_a) => __awaiter(void 0, [_a], void 0, function* ({ driver, connectionString, }) {
62
+ const createRegistryDbConnection = (_a) => __awaiter(void 0, [_a], void 0, function* ({ driver, connectionString, serving, }) {
51
63
  var _b;
64
+ const limits = serving ? SERVING_POOL_LIMITS : {};
52
65
  if (driver === 'neon') {
53
66
  const { Pool, neonConfig } = yield Promise.resolve().then(() => __importStar(require('@neondatabase/serverless')));
54
67
  // Neon's serverless driver needs a WebSocket constructor in Node; reuse the bundled `ws`.
@@ -56,7 +69,7 @@ const createRegistryDbConnection = (_a) => __awaiter(void 0, [_a], void 0, funct
56
69
  neonConfig.webSocketConstructor = (_b = ws.default) !== null && _b !== void 0 ? _b : ws;
57
70
  const { drizzle } = yield Promise.resolve().then(() => __importStar(require('drizzle-orm/neon-serverless')));
58
71
  const { migrate } = yield Promise.resolve().then(() => __importStar(require('drizzle-orm/neon-serverless/migrator')));
59
- const pool = new Pool({ connectionString });
72
+ const pool = new Pool(Object.assign({ connectionString }, limits));
60
73
  try {
61
74
  yield pool.query('select 1');
62
75
  }
@@ -75,7 +88,7 @@ const createRegistryDbConnection = (_a) => __awaiter(void 0, [_a], void 0, funct
75
88
  const { Pool } = yield Promise.resolve().then(() => __importStar(require('pg')));
76
89
  const { drizzle } = yield Promise.resolve().then(() => __importStar(require('drizzle-orm/node-postgres')));
77
90
  const { migrate } = yield Promise.resolve().then(() => __importStar(require('drizzle-orm/node-postgres/migrator')));
78
- const pool = new Pool({ connectionString });
91
+ const pool = new Pool(Object.assign({ connectionString }, limits));
79
92
  try {
80
93
  yield pool.query('select 1');
81
94
  }
@@ -998,9 +998,9 @@ export declare const pages: import("drizzle-orm/pg-core").PgTableWithColumns<{
998
998
  dialect: "pg";
999
999
  }>;
1000
1000
  /**
1001
- * Page source files. A page has a single verbatim `.md` (kind `markdown`) stored for byte-exact
1002
- * checkout and for resolving the rendered body at request time. Declaration files are rejected at the
1003
- * schema level (consistent with the other file tables, though pages never carry declarations).
1001
+ * Published pages store one verbatim `.md` file for byte-exact checkout and runtime body resolution.
1002
+ * The schema permits at most one row per page, and its path must be `${pageId}.md`. This invariant
1003
+ * lets readers select the body without ordering or deduplication.
1004
1004
  */
1005
1005
  export declare const pageFiles: import("drizzle-orm/pg-core").PgTableWithColumns<{
1006
1006
  name: "page_files";
@@ -1076,6 +1076,23 @@ export declare const pageFiles: import("drizzle-orm/pg-core").PgTableWithColumns
1076
1076
  identity: undefined;
1077
1077
  generated: undefined;
1078
1078
  }, {}, {}>;
1079
+ body: import("drizzle-orm/pg-core").PgColumn<{
1080
+ name: "body";
1081
+ tableName: "page_files";
1082
+ dataType: "string";
1083
+ columnType: "PgText";
1084
+ data: string;
1085
+ driverParam: string;
1086
+ notNull: true;
1087
+ hasDefault: false;
1088
+ isPrimaryKey: false;
1089
+ isAutoincrement: false;
1090
+ hasRuntimeDefault: false;
1091
+ enumValues: [string, ...string[]];
1092
+ baseColumn: never;
1093
+ identity: undefined;
1094
+ generated: undefined;
1095
+ }, {}, {}>;
1079
1096
  storageRef: import("drizzle-orm/pg-core").PgColumn<{
1080
1097
  name: "storage_ref";
1081
1098
  tableName: "page_files";
@@ -4376,6 +4393,23 @@ export declare const registrySchema: {
4376
4393
  identity: undefined;
4377
4394
  generated: undefined;
4378
4395
  }, {}, {}>;
4396
+ body: import("drizzle-orm/pg-core").PgColumn<{
4397
+ name: "body";
4398
+ tableName: "page_files";
4399
+ dataType: "string";
4400
+ columnType: "PgText";
4401
+ data: string;
4402
+ driverParam: string;
4403
+ notNull: true;
4404
+ hasDefault: false;
4405
+ isPrimaryKey: false;
4406
+ isAutoincrement: false;
4407
+ hasRuntimeDefault: false;
4408
+ enumValues: [string, ...string[]];
4409
+ baseColumn: never;
4410
+ identity: undefined;
4411
+ generated: undefined;
4412
+ }, {}, {}>;
4379
4413
  storageRef: import("drizzle-orm/pg-core").PgColumn<{
4380
4414
  name: "storage_ref";
4381
4415
  tableName: "page_files";
@@ -129,9 +129,9 @@ exports.patternFiles = (0, pg_core_1.pgTable)('pattern_files', {
129
129
  * the record — see {@link pageFiles}.
130
130
  */
131
131
  exports.pages = (0, pg_core_1.pgTable)('pages', {
132
- /** Stable page id (slug path, e.g. `guides/setup`). Join key across stores. */
132
+ /** Stable page id (e.g. `guides/setup`; `index` is the root page). Join key across stores. */
133
133
  id: (0, pg_core_1.text)('id').primaryKey(),
134
- /** Logical route the page is served at (e.g. `/guides/setup`). */
134
+ /** Logical route the page is served at (e.g. `/guides/setup`, or `/` for `index`). */
135
135
  path: (0, pg_core_1.text)('path').notNull(),
136
136
  title: (0, pg_core_1.text)('title'),
137
137
  description: (0, pg_core_1.text)('description'),
@@ -146,9 +146,9 @@ exports.pages = (0, pg_core_1.pgTable)('pages', {
146
146
  updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
147
147
  }, (table) => [(0, pg_core_1.index)('pages_group_idx').on(table.group)]);
148
148
  /**
149
- * Page source files. A page has a single verbatim `.md` (kind `markdown`) stored for byte-exact
150
- * checkout and for resolving the rendered body at request time. Declaration files are rejected at the
151
- * schema level (consistent with the other file tables, though pages never carry declarations).
149
+ * Published pages store one verbatim `.md` file for byte-exact checkout and runtime body resolution.
150
+ * The schema permits at most one row per page, and its path must be `${pageId}.md`. This invariant
151
+ * lets readers select the body without ordering or deduplication.
152
152
  */
153
153
  exports.pageFiles = (0, pg_core_1.pgTable)('page_files', {
154
154
  pageId: (0, pg_core_1.text)('page_id')
@@ -158,13 +158,15 @@ exports.pageFiles = (0, pg_core_1.pgTable)('page_files', {
158
158
  path: (0, pg_core_1.text)('path').notNull(),
159
159
  kind: (0, pg_core_1.text)('kind').$type().notNull(),
160
160
  content: (0, pg_core_1.text)('content'),
161
+ body: (0, pg_core_1.text)('body').notNull(),
161
162
  storageRef: (0, pg_core_1.text)('storage_ref'),
162
163
  contentType: (0, pg_core_1.text)('content_type').notNull(),
163
164
  createdAt: (0, pg_core_1.timestamp)('created_at', { withTimezone: true }).notNull().defaultNow(),
164
165
  updatedAt: (0, pg_core_1.timestamp)('updated_at', { withTimezone: true }).notNull().defaultNow(),
165
166
  }, (table) => [
166
- (0, pg_core_1.primaryKey)({ columns: [table.pageId, table.path] }),
167
- (0, pg_core_1.check)('page_files_kind_not_declaration', (0, drizzle_orm_1.sql) `${table.kind} <> 'declaration'`),
167
+ (0, pg_core_1.primaryKey)({ columns: [table.pageId] }),
168
+ (0, pg_core_1.check)('page_files_kind_markdown', (0, drizzle_orm_1.sql) `${table.kind} = 'markdown'`),
169
+ (0, pg_core_1.check)('page_files_canonical_path', (0, drizzle_orm_1.sql) `${table.path} = ${table.pageId} || '.md'`),
168
170
  ]);
169
171
  /**
170
172
  * Docs read-model artifacts. Path-keyed so shared/global artifacts (`component/main.css`,