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,7 +1,7 @@
1
1
  /**
2
2
  * Connected-workspace asset publish orchestration.
3
3
  *
4
- * `publish assets [collection]` runs a fresh build (`handoff.fetch()`), discovers the collections, and
4
+ * `publish assets [collection...]` runs a fresh build (`handoff.fetch()`), discovers the collections, and
5
5
  * uploads each changed one. A collection is skipped when its deterministic `sourceHash` is unchanged;
6
6
  * content transfers blob-by-blob (only hashes the registry lacks, since content-addressed dedup is the
7
7
  * per-blob skip-unchanged), and the manifest is finalized atomically only after its blobs exist.
@@ -13,4 +13,4 @@ import Handoff from '../../index';
13
13
  * blobs, and finalizes each manifest atomically. Reports published/unchanged/uploaded/failed counts;
14
14
  * throws at the end if any collection failed.
15
15
  */
16
- export declare const publishAssets: (handoff: Handoff, collection?: string) => Promise<void>;
16
+ export declare const publishAssets: (handoff: Handoff, selection?: string[]) => Promise<void>;
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Connected-workspace asset publish orchestration.
4
4
  *
5
- * `publish assets [collection]` runs a fresh build (`handoff.fetch()`), discovers the collections, and
5
+ * `publish assets [collection...]` runs a fresh build (`handoff.fetch()`), discovers the collections, and
6
6
  * uploads each changed one. A collection is skipped when its deterministic `sourceHash` is unchanged;
7
7
  * content transfers blob-by-blob (only hashes the registry lacks, since content-addressed dedup is the
8
8
  * per-blob skip-unchanged), and the manifest is finalized atomically only after its blobs exist.
@@ -20,7 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.publishAssets = void 0;
21
21
  const logger_1 = require("../../utils/logger");
22
22
  const sets_1 = require("../assets/sets");
23
- const client_1 = require("../client");
23
+ const selection_1 = require("../selection");
24
24
  const errors_1 = require("./errors");
25
25
  const index_1 = require("./index");
26
26
  const publish_build_1 = require("./publish-build");
@@ -49,22 +49,29 @@ const toManifestEntry = (asset) => {
49
49
  * blobs, and finalizes each manifest atomically. Reports published/unchanged/uploaded/failed counts;
50
50
  * throws at the end if any collection failed.
51
51
  */
52
- const publishAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0, function* () {
53
- const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
54
- if (collection && !(0, sets_1.isAssetCollection)(collection)) {
55
- throw new index_1.PublishError(`Unknown asset collection "${collection}". Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
52
+ const publishAssets = (handoff, selection) => __awaiter(void 0, void 0, void 0, function* () {
53
+ var _a;
54
+ const { client, url } = yield (0, index_1.resolveTransport)(handoff);
55
+ const unsupported = (_a = selection === null || selection === void 0 ? void 0 : selection.filter((collection) => !(0, sets_1.isAssetCollection)(collection))) !== null && _a !== void 0 ? _a : [];
56
+ if (unsupported.length > 0) {
57
+ throw new index_1.PublishError(`Unknown asset collection ${unsupported.map((name) => `"${name}"`).join(', ')}. ` +
58
+ `Supported collections: ${sets_1.ASSET_COLLECTIONS.join(', ')}.`);
56
59
  }
57
- logger_1.Logger.info(collection ? `Building assets to publish "${collection}"…` : 'Building assets for publish…');
58
- yield handoff.fetch();
59
- const available = yield handoff.store.assets.listCollections();
60
- const targets = collection && (0, sets_1.isAssetCollection)(collection) ? [collection] : available.filter(sets_1.isAssetCollection);
61
- const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
62
- let remote = [];
63
- try {
64
- remote = yield client.listAssetCollections();
60
+ if (!handoff.skipBuild) {
61
+ logger_1.Logger.info(selection ? `Building assets to publish ${selection.join(', ')}…` : 'Building assets for publish…');
62
+ yield handoff.fetch();
65
63
  }
66
- catch (_a) {
67
- logger_1.Logger.info('Could not read current registry asset collections; publishing all selected collections.');
64
+ const available = (yield handoff.store.assets.listCollections()).filter(sets_1.isAssetCollection);
65
+ const targets = (0, selection_1.selectIds)(available, selection).selected;
66
+ // Without a registry there is nothing to compare against, so a dry run reports every collection.
67
+ let remote = [];
68
+ if (client) {
69
+ try {
70
+ remote = yield client.listAssetCollections();
71
+ }
72
+ catch (_b) {
73
+ logger_1.Logger.info('Could not read current registry asset collections; publishing all selected collections.');
74
+ }
68
75
  }
69
76
  const remoteHashByCollection = new Map(remote.map((entry) => [entry.collection, entry.sourceHash]));
70
77
  let published = 0;
@@ -90,35 +97,39 @@ const publishAssets = (handoff, collection) => __awaiter(void 0, void 0, void 0,
90
97
  if (!byHash.has(asset.contentHash))
91
98
  byHash.set(asset.contentHash, asset);
92
99
  }
93
- const missing = yield client.assetBlobHaveCheck(Array.from(byHash.keys()));
94
- for (const hash of missing) {
95
- const asset = byHash.get(hash);
96
- if (!asset)
97
- continue;
98
- const content = yield handoff.store.assets.getAssetContent(col, asset.path);
99
- if (!(content === null || content === void 0 ? void 0 : content.body)) {
100
- throw new Error(`Could not read bytes for asset "${asset.path}" in collection "${col}".`);
100
+ // A dry run has no registry to ask, so every deduped blob counts as one that would upload.
101
+ const missing = client ? yield client.assetBlobHaveCheck(Array.from(byHash.keys())) : Array.from(byHash.keys());
102
+ if (client) {
103
+ for (const hash of missing) {
104
+ const asset = byHash.get(hash);
105
+ if (!asset)
106
+ continue;
107
+ const content = yield handoff.store.assets.getAssetContent(col, asset.path);
108
+ if (!(content === null || content === void 0 ? void 0 : content.body)) {
109
+ throw new Error(`Could not read bytes for asset "${asset.path}" in collection "${col}".`);
110
+ }
111
+ yield client.uploadAssetBlob(hash, content.contentType, content.body);
112
+ uploaded += 1;
101
113
  }
102
- yield client.uploadAssetBlob(hash, content.contentType, content.body);
103
- uploaded += 1;
114
+ const pkg = {
115
+ collection: col,
116
+ build: (0, publish_build_1.createCurrentBuild)({ sourceHash }),
117
+ assets: manifest.map(toManifestEntry),
118
+ };
119
+ yield client.publishAssetManifest(pkg);
104
120
  }
105
- const pkg = {
106
- collection: col,
107
- build: (0, publish_build_1.createCurrentBuild)({ sourceHash }),
108
- assets: manifest.map(toManifestEntry),
109
- };
110
- yield client.publishAssetManifest(pkg);
111
121
  published += 1;
112
- logger_1.Logger.info(`Published: ${col} (${manifest.length} asset(s), ${missing.length} new blob(s))`);
122
+ logger_1.Logger.info(`${(0, index_1.publishedLabel)(handoff)}: ${col} (${manifest.length} asset(s), ${missing.length} new blob(s))`);
113
123
  }
114
124
  catch (error) {
115
125
  failed.push({
116
126
  collection: col,
117
- message: (0, errors_1.describePublishError)(error, connection.url, 'asset collection'),
127
+ message: (0, errors_1.describePublishError)(error, url, 'asset collection'),
118
128
  });
119
129
  }
120
130
  }
121
- logger_1.Logger.success(`Assets publish complete: ${published} published, ${unchanged} unchanged, ${uploaded} blob(s) uploaded${failed.length ? `, ${failed.length} failed` : ''}.`);
131
+ logger_1.Logger.success(`Assets publish complete: ${published} ${handoff.dryRun ? 'would be published' : 'published'}, ` +
132
+ `${unchanged} unchanged, ${uploaded} blob(s) uploaded${failed.length ? `, ${failed.length} failed` : ''}.`);
122
133
  if (failed.length > 0) {
123
134
  for (const failure of failed) {
124
135
  logger_1.Logger.error(` - ${failure.collection}: ${failure.message}`);
@@ -9,26 +9,50 @@
9
9
  * that still guarantees the entity's artifacts (and the shared artifacts they reference) are current.
10
10
  */
11
11
  import Handoff from '../../index';
12
+ import { type RegistryClient } from '../client';
12
13
  import type { TransferEntityKind } from '../transfer';
13
14
  /** A connected-workspace configuration or precondition failure surfaced to the CLI. */
14
15
  export declare class PublishError extends Error {
15
16
  constructor(message: string);
16
17
  }
18
+ /**
19
+ * Ensure this project may publish at all: a registry host stores and serves what was published to it,
20
+ * so only a workspace runtime can be the source. Kept apart from the connection lookup because a dry
21
+ * run still has to be refused on a registry host while needing no URL or token itself.
22
+ */
23
+ export declare const assertPublishableWorkspace: (handoff: Handoff) => void;
17
24
  /**
18
25
  * Ensure the workspace is a connected workspace able to publish: workspace runtime mode (registry
19
26
  * hosts do not publish) and a resolved registry URL + access token. Throws an actionable
20
27
  * {@link PublishError} naming the exact misconfiguration.
21
28
  */
22
29
  export declare const resolveConnectionOrThrow: (handoff: Handoff) => Promise<import("../connection").ResolvedRegistryConnection>;
30
+ /**
31
+ * How a publish run reaches the registry. `client` is `null` on a dry run: the documented dry run
32
+ * needs no registry URL or token, so it stops at the workspace guard and contacts nothing.
33
+ */
34
+ export interface PublishTransport {
35
+ client: RegistryClient | null;
36
+ /** The registry URL, or a placeholder on a dry run. Used only in log lines and error messages. */
37
+ url: string;
38
+ }
39
+ /**
40
+ * Resolve the transport for a publish run. Shared by the entity, token and asset paths so the dry-run
41
+ * short-circuit lives in one place instead of four call sites.
42
+ */
43
+ export declare const resolveTransport: (handoff: Handoff) => Promise<PublishTransport>;
44
+ /** Prefix for a per-item progress line: `Published:` normally, `Would publish:` on a dry run. */
45
+ export declare const publishedLabel: (handoff: Handoff) => string;
23
46
  /**
24
47
  * Publish a single component, pattern, or page from a connected workspace. Components and patterns
25
48
  * receive a targeted build, while pages are packaged directly from their source.
26
49
  */
27
50
  export declare const publishEntity: (handoff: Handoff, kind: TransferEntityKind, id: string) => Promise<void>;
28
51
  /**
29
- * Publish every component, pattern, or page declared in this connected workspace. Builds the kind
30
- * once, assembles each entity's package, skips entities whose content hash already matches the
31
- * registry (unless `--force`), and reports published/unchanged/failed counts. A per-entity failure is
32
- * collected and never aborts the rest; the run throws at the end if any entity failed.
52
+ * Publish the components, patterns or pages declared in this connected workspace: every one of the
53
+ * kind, or only `ids` when given. Builds the kind once, assembles each entity's package, skips
54
+ * entities whose content hash already matches the registry (unless `--force`), and reports
55
+ * published/unchanged/failed counts. A per-entity failure is collected and never aborts the rest; the
56
+ * run throws at the end if any entity failed.
33
57
  */
34
- export declare const publishEntities: (handoff: Handoff, kind: TransferEntityKind) => Promise<void>;
58
+ export declare const publishEntities: (handoff: Handoff, kind: TransferEntityKind, ids?: string[]) => Promise<void>;
@@ -22,7 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.publishEntities = exports.publishEntity = exports.resolveConnectionOrThrow = exports.PublishError = void 0;
25
+ exports.publishEntities = exports.publishEntity = exports.publishedLabel = exports.resolveTransport = exports.resolveConnectionOrThrow = exports.assertPublishableWorkspace = exports.PublishError = void 0;
26
26
  const css_1 = require("../../transformers/preview/component/css");
27
27
  const javascript_1 = require("../../transformers/preview/component/javascript");
28
28
  const builder_1 = __importDefault(require("../../transformers/preview/component/builder"));
@@ -30,6 +30,7 @@ const builder_2 = require("../../transformers/preview/pattern/builder");
30
30
  const logger_1 = require("../../utils/logger");
31
31
  const client_1 = require("../client");
32
32
  const connection_1 = require("../connection");
33
+ const selection_1 = require("../selection");
33
34
  const errors_1 = require("./errors");
34
35
  const package_1 = require("./package");
35
36
  /** A connected-workspace configuration or precondition failure surfaced to the CLI. */
@@ -41,17 +42,26 @@ class PublishError extends Error {
41
42
  }
42
43
  exports.PublishError = PublishError;
43
44
  /**
44
- * Ensure the workspace is a connected workspace able to publish: workspace runtime mode (registry
45
- * hosts do not publish) and a resolved registry URL + access token. Throws an actionable
46
- * {@link PublishError} naming the exact misconfiguration.
45
+ * Ensure this project may publish at all: a registry host stores and serves what was published to it,
46
+ * so only a workspace runtime can be the source. Kept apart from the connection lookup because a dry
47
+ * run still has to be refused on a registry host while needing no URL or token itself.
47
48
  */
48
- const resolveConnectionOrThrow = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
49
+ const assertPublishableWorkspace = (handoff) => {
49
50
  var _a, _b, _c;
50
51
  const mode = (_c = (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.runtime) === null || _b === void 0 ? void 0 : _b.mode) !== null && _c !== void 0 ? _c : 'workspace';
51
52
  if (mode !== 'workspace') {
52
53
  throw new PublishError(`publish is only available from a connected workspace (runtime.mode: "workspace"); this project is "${mode}". ` +
53
54
  'A registry host stores and serves what was published to it; it does not publish.');
54
55
  }
56
+ };
57
+ exports.assertPublishableWorkspace = assertPublishableWorkspace;
58
+ /**
59
+ * Ensure the workspace is a connected workspace able to publish: workspace runtime mode (registry
60
+ * hosts do not publish) and a resolved registry URL + access token. Throws an actionable
61
+ * {@link PublishError} naming the exact misconfiguration.
62
+ */
63
+ const resolveConnectionOrThrow = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
64
+ (0, exports.assertPublishableWorkspace)(handoff);
55
65
  const connection = yield (0, connection_1.resolveAuthenticatedRegistryConnection)(handoff.config, handoff.workingPath);
56
66
  if (!connection.url) {
57
67
  throw new PublishError(`No registry is configured. Run \`handoff-app login --url <registry-url>\`, set runtime.registryConnection.url, ` +
@@ -64,6 +74,25 @@ const resolveConnectionOrThrow = (handoff) => __awaiter(void 0, void 0, void 0,
64
74
  return connection;
65
75
  });
66
76
  exports.resolveConnectionOrThrow = resolveConnectionOrThrow;
77
+ /**
78
+ * Resolve the transport for a publish run. Shared by the entity, token and asset paths so the dry-run
79
+ * short-circuit lives in one place instead of four call sites.
80
+ */
81
+ const resolveTransport = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
82
+ if (handoff.dryRun) {
83
+ (0, exports.assertPublishableWorkspace)(handoff);
84
+ return { client: null, url: '(dry run)' };
85
+ }
86
+ const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
87
+ return {
88
+ client: (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken }),
89
+ url: connection.url,
90
+ };
91
+ });
92
+ exports.resolveTransport = resolveTransport;
93
+ /** Prefix for a per-item progress line: `Published:` normally, `Would publish:` on a dry run. */
94
+ const publishedLabel = (handoff) => (handoff.dryRun ? 'Would publish' : 'Published');
95
+ exports.publishedLabel = publishedLabel;
67
96
  /** Run the smallest build that guarantees the selected entity's artifacts are current. */
68
97
  const runTargetedBuild = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
69
98
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
@@ -105,19 +134,26 @@ const runTargetedBuild = (handoff, kind, id) => __awaiter(void 0, void 0, void 0
105
134
  * receive a targeted build, while pages are packaged directly from their source.
106
135
  */
107
136
  const publishEntity = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
108
- const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
109
- logger_1.Logger.info(kind === 'page' ? `Preparing page "${id}" for publish…` : `Building ${kind} "${id}" for publish…`);
110
- yield runTargetedBuild(handoff, kind, id);
137
+ const { client, url } = yield (0, exports.resolveTransport)(handoff);
138
+ if (!handoff.skipBuild) {
139
+ logger_1.Logger.info(kind === 'page' ? `Preparing page "${id}" for publish…` : `Building ${kind} "${id}" for publish…`);
140
+ yield runTargetedBuild(handoff, kind, id);
141
+ }
111
142
  const pkg = yield (0, package_1.buildPublishPackage)(handoff, kind, id);
112
143
  (0, package_1.assertRequiredArtifactsPresent)(pkg);
113
- logger_1.Logger.info(`Uploading ${kind} "${id}" to ${connection.url} (${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s))…`);
114
- const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
144
+ const contents = `${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s)`;
145
+ if (!client) {
146
+ logger_1.Logger.info(`Would publish ${kind} "${id}" (${contents}).`);
147
+ logger_1.Logger.success(`Dry run: ${kind} "${id}" would be published.`);
148
+ return;
149
+ }
150
+ logger_1.Logger.info(`Uploading ${kind} "${id}" to ${url} (${contents})…`);
115
151
  try {
116
152
  yield client.publish(kind, id, pkg);
117
153
  }
118
154
  catch (error) {
119
155
  if (error instanceof client_1.RegistryClientError) {
120
- throw new PublishError((0, errors_1.describeUploadFailure)(error, connection.url, 'package'));
156
+ throw new PublishError((0, errors_1.describeUploadFailure)(error, url, 'package'));
121
157
  }
122
158
  throw error;
123
159
  }
@@ -144,27 +180,46 @@ const runBulkBuild = (handoff, kind) => __awaiter(void 0, void 0, void 0, functi
144
180
  });
145
181
  /** List every declared entity id of the kind from the workspace store. */
146
182
  const listWorkspaceEntityIds = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
147
- const store = kind === 'component' ? handoff.store.components : kind === 'pattern' ? handoff.store.patterns : handoff.store.pages;
183
+ const stores = { component: handoff.store.components, pattern: handoff.store.patterns, page: handoff.store.pages };
184
+ const store = stores[kind];
185
+ if (!store) {
186
+ throw new PublishError(`Unknown entity kind "${kind}".`);
187
+ }
148
188
  return (yield store.list()).map((entity) => entity.id);
149
189
  });
150
190
  /**
151
- * Publish every component, pattern, or page declared in this connected workspace. Builds the kind
152
- * once, assembles each entity's package, skips entities whose content hash already matches the
153
- * registry (unless `--force`), and reports published/unchanged/failed counts. A per-entity failure is
154
- * collected and never aborts the rest; the run throws at the end if any entity failed.
191
+ * Narrow the workspace's declared ids to the requested subset, naming any id that is not declared
192
+ * here instead of silently publishing fewer entities than asked for.
155
193
  */
156
- const publishEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
157
- const connection = yield (0, exports.resolveConnectionOrThrow)(handoff);
158
- logger_1.Logger.info(`Building ${kind}s for publish…`);
159
- yield runBulkBuild(handoff, kind);
160
- const ids = yield listWorkspaceEntityIds(handoff, kind);
161
- if (ids.length === 0) {
194
+ const selectEntityIds = (available, requested, kind) => {
195
+ const { selected, unknown } = (0, selection_1.selectIds)(available, requested);
196
+ if (unknown.length > 0) {
197
+ throw new PublishError(`No ${kind} named ${unknown.map((id) => `"${id}"`).join(', ')} is declared in this workspace. ` +
198
+ `Declared ${kind}s: ${available.join(', ') || '(none)'}.`);
199
+ }
200
+ return selected;
201
+ };
202
+ /**
203
+ * Publish the components, patterns or pages declared in this connected workspace: every one of the
204
+ * kind, or only `ids` when given. Builds the kind once, assembles each entity's package, skips
205
+ * entities whose content hash already matches the registry (unless `--force`), and reports
206
+ * published/unchanged/failed counts. A per-entity failure is collected and never aborts the rest; the
207
+ * run throws at the end if any entity failed.
208
+ */
209
+ const publishEntities = (handoff, kind, ids) => __awaiter(void 0, void 0, void 0, function* () {
210
+ const { client, url } = yield (0, exports.resolveTransport)(handoff);
211
+ if (!handoff.skipBuild) {
212
+ logger_1.Logger.info(`Building ${kind}s for publish…`);
213
+ yield runBulkBuild(handoff, kind);
214
+ }
215
+ const targets = selectEntityIds(yield listWorkspaceEntityIds(handoff, kind), ids, kind);
216
+ if (targets.length === 0) {
162
217
  logger_1.Logger.success(`No ${kind}s are declared in this workspace; nothing to publish.`);
163
218
  return;
164
219
  }
165
- const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
220
+ // Without a registry there is nothing to compare against, so a dry run reports every target.
166
221
  let remote = [];
167
- if (!handoff.force) {
222
+ if (client && !handoff.force) {
168
223
  try {
169
224
  remote = yield client.listEntities(kind);
170
225
  }
@@ -177,7 +232,7 @@ const publishEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fun
177
232
  let published = 0;
178
233
  let unchanged = 0;
179
234
  const failed = [];
180
- for (const id of ids) {
235
+ for (const id of targets) {
181
236
  try {
182
237
  const pkg = yield (0, package_1.buildPublishPackage)(handoff, kind, id);
183
238
  (0, package_1.assertRequiredArtifactsPresent)(pkg);
@@ -188,20 +243,18 @@ const publishEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, fun
188
243
  logger_1.Logger.info(`Unchanged: ${id}`);
189
244
  continue;
190
245
  }
191
- yield client.publish(kind, id, pkg);
246
+ if (client) {
247
+ yield client.publish(kind, id, pkg);
248
+ }
192
249
  published += 1;
193
- logger_1.Logger.info(`Published: ${id} (${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s))`);
250
+ logger_1.Logger.info(`${(0, exports.publishedLabel)(handoff)}: ${id} (${pkg.artifacts.length} artifact(s), ${pkg.files.length} source file(s))`);
194
251
  }
195
252
  catch (error) {
196
- const message = error instanceof client_1.RegistryClientError
197
- ? (0, errors_1.describeUploadFailure)(error, connection.url, 'package')
198
- : error instanceof Error
199
- ? error.message
200
- : String(error);
201
- failed.push({ id, message });
253
+ failed.push({ id, message: (0, errors_1.describePublishError)(error, url, 'package') });
202
254
  }
203
255
  }
204
- logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s publish complete — ${published} published, ${unchanged} unchanged${failed.length ? `, ${failed.length} failed` : ''}.`);
256
+ logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s publish complete — ${published} ${handoff.dryRun ? 'would be published' : 'published'}, ` +
257
+ `${unchanged} unchanged${failed.length ? `, ${failed.length} failed` : ''}.`);
205
258
  if (failed.length > 0) {
206
259
  for (const failure of failed) {
207
260
  logger_1.Logger.error(` - ${failure.id}: ${failure.message}`);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Connected-workspace token publish orchestration.
3
3
  *
4
- * `publish tokens [setId]` runs a **fresh token build** (`handoff.fetch()` — Figma extract +
4
+ * `publish tokens [setId...]` runs a **fresh token build** (`handoff.fetch()` — Figma extract +
5
5
  * `buildStyles`) so the local `tokens.json` and generated token outputs are current, discovers the
6
6
  * logical sets from that document, then uploads each selected set (its extracted record + generated
7
7
  * artifacts) to the connected registry. Each set is one atomic upsert; a set whose deterministic
@@ -15,4 +15,4 @@ import Handoff from '../../index';
15
15
  * counts. A per-set upload failure is collected and never leaves a partially written set (each set is
16
16
  * committed atomically server-side); the run throws at the end if any set failed.
17
17
  */
18
- export declare const publishTokens: (handoff: Handoff, setId?: string) => Promise<void>;
18
+ export declare const publishTokens: (handoff: Handoff, selection?: string[]) => Promise<void>;
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Connected-workspace token publish orchestration.
4
4
  *
5
- * `publish tokens [setId]` runs a **fresh token build** (`handoff.fetch()` — Figma extract +
5
+ * `publish tokens [setId...]` runs a **fresh token build** (`handoff.fetch()` — Figma extract +
6
6
  * `buildStyles`) so the local `tokens.json` and generated token outputs are current, discovers the
7
7
  * logical sets from that document, then uploads each selected set (its extracted record + generated
8
8
  * artifacts) to the connected registry. Each set is one atomic upsert; a set whose deterministic
@@ -22,7 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.publishTokens = void 0;
23
23
  const logger_1 = require("../../utils/logger");
24
24
  const stable_stringify_1 = require("../../utils/stable-stringify");
25
- const client_1 = require("../client");
25
+ const selection_1 = require("../selection");
26
26
  const errors_1 = require("./errors");
27
27
  const index_1 = require("./index");
28
28
  const publish_build_1 = require("./publish-build");
@@ -58,28 +58,31 @@ const buildTokenSetPackage = (handoff, set) => __awaiter(void 0, void 0, void 0,
58
58
  * counts. A per-set upload failure is collected and never leaves a partially written set (each set is
59
59
  * committed atomically server-side); the run throws at the end if any set failed.
60
60
  */
61
- const publishTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, function* () {
62
- const connection = yield (0, index_1.resolveConnectionOrThrow)(handoff);
63
- logger_1.Logger.info(setId ? `Building tokens to publish "${setId}"…` : 'Building tokens for publish…');
64
- yield handoff.fetch();
61
+ const publishTokens = (handoff, selection) => __awaiter(void 0, void 0, void 0, function* () {
62
+ const { client, url } = yield (0, index_1.resolveTransport)(handoff);
63
+ if (!handoff.skipBuild) {
64
+ logger_1.Logger.info(selection ? `Building tokens to publish ${selection.map((id) => `"${id}"`).join(', ')}…` : 'Building tokens for publish…');
65
+ yield handoff.fetch();
66
+ }
65
67
  const allSets = yield handoff.store.tokens.listSets();
66
- let targetSets = allSets;
67
- if (setId) {
68
- const found = allSets.find((set) => set.id === setId);
69
- if (!found) {
70
- throw new index_1.PublishError(`Token set "${setId}" was not found in the generated tokens. Discovered sets: ${allSets.map((set) => set.id).join(', ') || '(none)'}.`);
71
- }
72
- targetSets = [found];
68
+ const { selected, unknown } = (0, selection_1.selectIds)(allSets.map((set) => set.id), selection);
69
+ if (unknown.length > 0) {
70
+ throw new index_1.PublishError(`Token set ${unknown.map((id) => `"${id}"`).join(', ')} was not found in the generated tokens. ` +
71
+ `Discovered sets: ${allSets.map((set) => set.id).join(', ') || '(none)'}.`);
73
72
  }
73
+ const selectedIds = new Set(selected);
74
+ const targetSets = allSets.filter((set) => selectedIds.has(set.id));
74
75
  const packages = yield Promise.all(targetSets.map((set) => buildTokenSetPackage(handoff, set)));
75
- const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
76
+ // Without a registry there is nothing to compare against, so a dry run reports every set.
76
77
  let remote = [];
77
- try {
78
- remote = yield client.listTokenSets();
79
- }
80
- catch (error) {
81
- // If the summary listing fails we simply publish everything (the server still no-ops unchanged sets).
82
- logger_1.Logger.info('Could not read current registry token sets; publishing all selected sets.');
78
+ if (client) {
79
+ try {
80
+ remote = yield client.listTokenSets();
81
+ }
82
+ catch (error) {
83
+ // If the summary listing fails we simply publish everything (the server still no-ops unchanged sets).
84
+ logger_1.Logger.info('Could not read current registry token sets; publishing all selected sets.');
85
+ }
83
86
  }
84
87
  const remoteHashById = new Map(remote.map((entry) => [entry.id, entry.sourceHash]));
85
88
  let published = 0;
@@ -93,15 +96,18 @@ const publishTokens = (handoff, setId) => __awaiter(void 0, void 0, void 0, func
93
96
  continue;
94
97
  }
95
98
  try {
96
- yield client.publishTokens(pkg);
99
+ if (client) {
100
+ yield client.publishTokens(pkg);
101
+ }
97
102
  published += 1;
98
- logger_1.Logger.info(`Published: ${pkg.id} (${pkg.artifacts.length} artifact(s))`);
103
+ logger_1.Logger.info(`${(0, index_1.publishedLabel)(handoff)}: ${pkg.id} (${pkg.artifacts.length} artifact(s))`);
99
104
  }
100
105
  catch (error) {
101
- failed.push({ id: pkg.id, message: (0, errors_1.describePublishError)(error, connection.url, 'token set') });
106
+ failed.push({ id: pkg.id, message: (0, errors_1.describePublishError)(error, url, 'token set') });
102
107
  }
103
108
  }
104
- logger_1.Logger.success(`Tokens publish complete — ${published} published, ${unchanged} unchanged${failed.length ? `, ${failed.length} failed` : ''}.`);
109
+ logger_1.Logger.success(`Tokens publish complete — ${published} ${handoff.dryRun ? 'would be published' : 'published'}, ` +
110
+ `${unchanged} unchanged${failed.length ? `, ${failed.length} failed` : ''}.`);
105
111
  if (failed.length > 0) {
106
112
  for (const failure of failed) {
107
113
  logger_1.Logger.error(` - ${failure.id}: ${failure.message}`);
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Selection narrowing shared by the publish and checkout orchestration.
3
+ *
4
+ * Every bulk operation discovers what is available (declared locally or published remotely) and may
5
+ * be asked for only part of it. Splitting the request into what exists and what does not lets each
6
+ * caller build its own actionable error while the matching lives in one place.
7
+ */
8
+ /** Narrow an available list to the requested subset, separating out ids that do not exist. */
9
+ export declare const selectIds: (available: string[], requested?: string[]) => {
10
+ selected: string[];
11
+ unknown: string[];
12
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * Selection narrowing shared by the publish and checkout orchestration.
4
+ *
5
+ * Every bulk operation discovers what is available (declared locally or published remotely) and may
6
+ * be asked for only part of it. Splitting the request into what exists and what does not lets each
7
+ * caller build its own actionable error while the matching lives in one place.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.selectIds = void 0;
11
+ /** Narrow an available list to the requested subset, separating out ids that do not exist. */
12
+ const selectIds = (available, requested) => {
13
+ if (!requested) {
14
+ return { selected: available, unknown: [] };
15
+ }
16
+ const availableIds = new Set(available);
17
+ return {
18
+ selected: requested.filter((id) => availableIds.has(id)),
19
+ unknown: requested.filter((id) => !availableIds.has(id)),
20
+ };
21
+ };
22
+ exports.selectIds = selectIds;
@@ -57,6 +57,54 @@ export declare class RegistryPageStore implements PageStore {
57
57
  getSource(_ref: SourceReference): Promise<TextFileResource | null>;
58
58
  getRelatedSourceFiles(id: string): Promise<TextFileResource[]>;
59
59
  }
60
+ /**
61
+ * The page fields that search reads. The query cuts the stored parsed body to `bodyLength`.
62
+ * The body is empty for an external-link page and for a page with no stored markdown.
63
+ */
64
+ export interface PageSearchCandidate {
65
+ record: PageListObject;
66
+ body: string;
67
+ }
68
+ export interface PageSearchCandidateQuery {
69
+ /** Normalized terms. A page is a candidate when any term occurs in any searchable column. */
70
+ terms: string[];
71
+ /** Optional group filter, matched case-insensitively. */
72
+ group?: string;
73
+ /** Maximum rows returned to the server for one search. */
74
+ limit: number;
75
+ /**
76
+ * Code points of each returned body, and the prefix the body filter reads, so one search costs at most
77
+ * `limit × bodyLength`. The query cuts with `left`, which counts code points. Workspace search must count the same
78
+ * unit, or the modes match different text.
79
+ */
80
+ bodyLength: number;
81
+ }
82
+ /**
83
+ * Candidate pages for a search, filtered and capped in SQL.
84
+ *
85
+ * `enabled` and `menuTitle` are fields in the `record` JSON, so the query uses JSON operators. An
86
+ * absent `enabled` field means enabled. The body filter excludes frontmatter, so an excluded field
87
+ * such as `metaTitle` cannot consume a candidate slot.
88
+ *
89
+ * The filter and the projection share one body expression, so every term SQL matched is inside the text that the
90
+ * server ranks.
91
+ *
92
+ * The body expression is empty for an external-link page. Search reads such a page by title, menu title, and
93
+ * description only. The exclusion must happen in SQL. An external body otherwise satisfies the filter and takes one
94
+ * of the `limit` rows. It pushes a real candidate past the cap before the server can drop it.
95
+ *
96
+ * `C` collation orders IDs by code point, as the merge in registry search does. Both orders must
97
+ * agree, or the cap can keep different pages in each runtime mode.
98
+ */
99
+ export declare const searchPageCandidates: (db: RegistryDatabase, query: PageSearchCandidateQuery) => Promise<PageSearchCandidate[]>;
100
+ /**
101
+ * The candidate IDs that have a published page. Search asks only about packaged default IDs, because
102
+ * no other ID can replace a default. This keeps the query bounded as the registry grows.
103
+ *
104
+ * A published page replaces the default with the same ID even when it does not match the search
105
+ * terms, so this query ignores the term filter.
106
+ */
107
+ export declare const filterPublishedPageIds: (db: RegistryDatabase, candidateIds: string[]) => Promise<string[]>;
60
108
  export declare class RegistryTokenStore implements TokenStore {
61
109
  private readonly context;
62
110
  constructor(context: RegistryStoreContext);