handoff-app 2.0.0-4 → 2.0.0-6

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 (234) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +346 -377
  4. package/dist/app-builder/build.js +11 -4
  5. package/dist/app-builder/client-config.js +1 -0
  6. package/dist/app-builder/nav-shell.js +8 -15
  7. package/dist/app-builder/vercel-output.js +4 -4
  8. package/dist/cli/make.js +2 -2
  9. package/dist/cli.js +38 -6
  10. package/dist/commands/build/app.js +1 -5
  11. package/dist/commands/build/components.js +1 -5
  12. package/dist/commands/build/index.js +1 -5
  13. package/dist/commands/checkout/index.d.ts +15 -12
  14. package/dist/commands/checkout/index.js +15 -54
  15. package/dist/commands/compat/deprecations.d.ts +46 -0
  16. package/dist/commands/compat/deprecations.js +62 -0
  17. package/dist/commands/compat/push-args.d.ts +48 -0
  18. package/dist/commands/compat/push-args.js +73 -0
  19. package/dist/commands/db/migrate.js +1 -5
  20. package/dist/commands/dev/index.js +1 -5
  21. package/dist/commands/eject/config.js +1 -5
  22. package/dist/commands/eject/pages.js +1 -5
  23. package/dist/commands/eject/theme.js +1 -5
  24. package/dist/commands/fetch/index.js +1 -5
  25. package/dist/commands/index.js +4 -0
  26. package/dist/commands/login/index.js +1 -5
  27. package/dist/commands/logout/index.js +1 -5
  28. package/dist/commands/make/component.js +1 -5
  29. package/dist/commands/make/page.js +3 -7
  30. package/dist/commands/make/template.js +1 -5
  31. package/dist/commands/publish/index.d.ts +18 -12
  32. package/dist/commands/publish/index.js +18 -54
  33. package/dist/commands/pull/index.d.ts +13 -0
  34. package/dist/commands/pull/index.js +20 -0
  35. package/dist/commands/push/index.d.ts +23 -0
  36. package/dist/commands/push/index.js +58 -0
  37. package/dist/commands/scaffold/index.js +1 -5
  38. package/dist/commands/start/index.js +1 -5
  39. package/dist/commands/types.d.ts +10 -0
  40. package/dist/commands/utils.d.ts +74 -6
  41. package/dist/commands/utils.js +89 -10
  42. package/dist/commands/validate/components.js +1 -5
  43. package/dist/config/defaults.d.ts +2 -0
  44. package/dist/config/defaults.js +4 -1
  45. package/dist/config/index.d.ts +3 -2
  46. package/dist/config/index.js +3 -1
  47. package/dist/config/loader.d.ts +19 -6
  48. package/dist/config/loader.js +43 -13
  49. package/dist/config/normalizers/page.d.ts +4 -4
  50. package/dist/config/normalizers/page.js +2 -2
  51. package/dist/config/runtime.js +9 -25
  52. package/dist/index.d.ts +69 -22
  53. package/dist/index.js +148 -35
  54. package/dist/nav/resolver.js +3 -1
  55. package/dist/nav/workspace.js +2 -2
  56. package/dist/registry/auth/access-tokens.d.ts +11 -3
  57. package/dist/registry/auth/access-tokens.js +29 -3
  58. package/dist/registry/auth/index.d.ts +4 -3
  59. package/dist/registry/auth/index.js +10 -2
  60. package/dist/registry/auth/sync-secret.d.ts +27 -0
  61. package/dist/registry/auth/sync-secret.js +65 -0
  62. package/dist/registry/auth/types.d.ts +20 -1
  63. package/dist/registry/auth/types.js +3 -1
  64. package/dist/registry/auth/validation.d.ts +11 -0
  65. package/dist/registry/auth/validation.js +18 -1
  66. package/dist/registry/checkout/assets.d.ts +1 -1
  67. package/dist/registry/checkout/assets.js +21 -6
  68. package/dist/registry/checkout/index.d.ts +8 -1
  69. package/dist/registry/checkout/index.js +39 -6
  70. package/dist/registry/checkout/tokens.d.ts +1 -1
  71. package/dist/registry/checkout/tokens.js +12 -5
  72. package/dist/registry/connection.d.ts +10 -0
  73. package/dist/registry/connection.js +52 -7
  74. package/dist/registry/content-kinds.d.ts +27 -0
  75. package/dist/registry/content-kinds.js +33 -0
  76. package/dist/registry/db/client.d.ts +6 -1
  77. package/dist/registry/db/client.js +16 -3
  78. package/dist/registry/db/schema.d.ts +37 -3
  79. package/dist/registry/db/schema.js +9 -7
  80. package/dist/registry/publish/assets.d.ts +2 -2
  81. package/dist/registry/publish/assets.js +46 -35
  82. package/dist/registry/publish/index.d.ts +29 -5
  83. package/dist/registry/publish/index.js +87 -34
  84. package/dist/registry/publish/tokens.d.ts +2 -2
  85. package/dist/registry/publish/tokens.js +30 -24
  86. package/dist/registry/selection.d.ts +12 -0
  87. package/dist/registry/selection.js +22 -0
  88. package/dist/store/registry.d.ts +48 -0
  89. package/dist/store/registry.js +75 -12
  90. package/dist/transformers/preview/types.d.ts +2 -2
  91. package/dist/types/config.d.ts +7 -0
  92. package/dist/utils/markdown.d.ts +10 -0
  93. package/dist/utils/markdown.js +17 -0
  94. package/dist/utils/menu-shell.js +14 -7
  95. package/dist/utils/pages.d.ts +22 -0
  96. package/dist/utils/pages.js +73 -0
  97. package/dist/utils/path.d.ts +5 -0
  98. package/dist/utils/path.js +9 -0
  99. package/docs/api.md +1 -3
  100. package/docs/cli.md +1 -1
  101. package/drizzle/0002_add_pages.sql +5 -3
  102. package/drizzle/meta/0002_snapshot.json +17 -8
  103. package/drizzle/meta/0003_snapshot.json +17 -8
  104. package/drizzle/meta/0004_snapshot.json +17 -8
  105. package/drizzle/meta/0005_snapshot.json +17 -8
  106. package/package.json +4 -2
  107. package/src/app/components/CopyToClipboard.tsx +27 -26
  108. package/src/app/components/Layout/Header.tsx +4 -12
  109. package/src/app/components/Layout/RuntimeModeBadge.tsx +18 -0
  110. package/src/app/components/McpIntegration/McpConfigDialog.tsx +349 -0
  111. package/src/app/components/McpIntegration/McpIcon.tsx +10 -0
  112. package/src/app/components/Navigation/MobileNav.tsx +3 -1
  113. package/src/app/components/ui/dialog.tsx +73 -0
  114. package/src/app/components/util/index.ts +5 -23
  115. package/src/app/lib/docs-api/backend.ts +16 -1
  116. package/src/app/lib/docs-api/errors.ts +8 -7
  117. package/src/app/lib/docs-api/handler.ts +2 -3
  118. package/src/app/lib/docs-api/index.ts +2 -0
  119. package/src/app/lib/docs-api/page-discovery.ts +121 -0
  120. package/src/app/lib/docs-api/page-rendering.ts +37 -0
  121. package/src/app/lib/docs-api/page-search.ts +191 -0
  122. package/src/app/lib/docs-api/records.ts +31 -24
  123. package/src/app/lib/docs-api/registry.ts +92 -11
  124. package/src/app/lib/docs-api/runtime-config.ts +5 -0
  125. package/src/app/lib/docs-api/search.ts +120 -0
  126. package/src/app/lib/mcp/auth.ts +64 -0
  127. package/src/app/lib/mcp/index.ts +2 -0
  128. package/src/app/lib/mcp/server.ts +343 -0
  129. package/src/app/lib/mcp/shape.ts +595 -0
  130. package/src/app/lib/registry-api/allowlist.ts +7 -5
  131. package/src/app/lib/registry-api/asset-transfer.ts +58 -31
  132. package/src/app/lib/registry-api/auth.ts +89 -9
  133. package/src/app/lib/registry-api/entity-routes.ts +17 -7
  134. package/src/app/lib/registry-api/errors.ts +10 -4
  135. package/src/app/lib/registry-api/handler.ts +13 -15
  136. package/src/app/lib/registry-api/meta.ts +4 -0
  137. package/src/app/lib/registry-api/revalidate.ts +31 -12
  138. package/src/app/lib/registry-api/store.ts +89 -26
  139. package/src/app/lib/registry-api/sync-compat.ts +166 -0
  140. package/src/app/lib/registry-api/token-transfer.ts +44 -12
  141. package/src/app/lib/registry-api/transfer-batch.ts +116 -0
  142. package/src/app/lib/registry-api/transfer.ts +62 -35
  143. package/src/app/lib/registry-api/validation.ts +33 -1
  144. package/src/app/lib/registry-connection.ts +1 -1
  145. package/src/app/next.config.mjs +9 -0
  146. package/src/app/pages/[...slug]/index.tsx +52 -51
  147. package/src/app/pages/api/account/tokens/index.ts +4 -13
  148. package/src/app/pages/api/docs/search/pages.json.ts +25 -0
  149. package/src/app/pages/api/mcp.ts +56 -0
  150. package/src/app/pages/api/oauth/revoke.ts +14 -7
  151. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
  152. package/src/app/pages/api/registry/components/[id].ts +2 -2
  153. package/src/app/pages/api/registry/components/index.ts +1 -1
  154. package/src/app/pages/api/registry/pages/[...id].ts +20 -0
  155. package/src/app/pages/api/registry/pages/index.ts +19 -0
  156. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
  157. package/src/app/pages/api/registry/patterns/[id].ts +2 -2
  158. package/src/app/pages/api/registry/patterns/index.ts +1 -1
  159. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
  160. package/src/app/pages/api/sync/upload.ts +21 -0
  161. package/src/app/pages/index/index.tsx +14 -0
  162. package/src/app/pages/index.tsx +31 -5
  163. package/src/app-builder/build.ts +11 -4
  164. package/src/app-builder/client-config.ts +2 -1
  165. package/src/app-builder/nav-shell.ts +8 -13
  166. package/src/app-builder/vercel-output.ts +4 -4
  167. package/src/cli/make.ts +2 -2
  168. package/src/cli.ts +30 -6
  169. package/src/commands/build/app.ts +2 -3
  170. package/src/commands/build/components.ts +2 -3
  171. package/src/commands/build/index.ts +2 -3
  172. package/src/commands/checkout/index.ts +18 -44
  173. package/src/commands/compat/deprecations.ts +79 -0
  174. package/src/commands/compat/push-args.ts +106 -0
  175. package/src/commands/db/migrate.ts +2 -3
  176. package/src/commands/dev/index.ts +2 -3
  177. package/src/commands/eject/config.ts +2 -3
  178. package/src/commands/eject/pages.ts +2 -3
  179. package/src/commands/eject/theme.ts +2 -3
  180. package/src/commands/fetch/index.ts +2 -3
  181. package/src/commands/index.ts +4 -0
  182. package/src/commands/login/index.ts +2 -3
  183. package/src/commands/logout/index.ts +2 -3
  184. package/src/commands/make/component.ts +2 -3
  185. package/src/commands/make/page.ts +4 -5
  186. package/src/commands/make/template.ts +2 -3
  187. package/src/commands/publish/index.ts +21 -44
  188. package/src/commands/pull/index.ts +32 -0
  189. package/src/commands/push/index.ts +115 -0
  190. package/src/commands/scaffold/index.ts +2 -3
  191. package/src/commands/start/index.ts +2 -3
  192. package/src/commands/types.ts +11 -1
  193. package/src/commands/utils.ts +80 -11
  194. package/src/commands/validate/components.ts +2 -3
  195. package/src/config/defaults.ts +3 -0
  196. package/src/config/index.ts +3 -2
  197. package/src/config/loader.ts +57 -12
  198. package/src/config/normalizers/page.ts +4 -4
  199. package/src/config/runtime.ts +9 -24
  200. package/src/index.ts +183 -42
  201. package/src/nav/resolver.ts +3 -1
  202. package/src/nav/workspace.ts +2 -2
  203. package/src/registry/auth/access-tokens.ts +29 -5
  204. package/src/registry/auth/index.ts +7 -1
  205. package/src/registry/auth/sync-secret.ts +67 -0
  206. package/src/registry/auth/types.ts +24 -1
  207. package/src/registry/auth/validation.ts +18 -0
  208. package/src/registry/checkout/assets.ts +24 -7
  209. package/src/registry/checkout/index.ts +41 -5
  210. package/src/registry/checkout/tokens.ts +12 -6
  211. package/src/registry/connection.ts +53 -5
  212. package/src/registry/content-kinds.ts +39 -0
  213. package/src/registry/db/client.ts +22 -2
  214. package/src/registry/db/schema.ts +9 -7
  215. package/src/registry/publish/assets.ts +47 -37
  216. package/src/registry/publish/index.ts +102 -38
  217. package/src/registry/publish/tokens.ts +35 -26
  218. package/src/registry/selection.ts +19 -0
  219. package/src/store/registry.ts +118 -13
  220. package/src/transformers/preview/types.ts +2 -2
  221. package/src/types/config.ts +7 -0
  222. package/src/utils/README.md +1 -0
  223. package/src/utils/markdown.ts +11 -0
  224. package/src/utils/menu-shell.ts +14 -7
  225. package/src/utils/pages.ts +52 -0
  226. package/src/utils/path.ts +15 -6
  227. package/templates/default/README.md.tpl +10 -0
  228. package/templates/default/handoff.config.js.tpl +6 -3
  229. package/templates/default/handoff.config.ts.tpl +6 -3
  230. package/templates/default/handoff.config.with-examples.js.tpl +6 -3
  231. package/templates/default/handoff.config.with-examples.ts.tpl +6 -3
  232. package/templates/default/package.json.tpl +4 -0
  233. package/config/docs/foundations/logos.md +0 -7
  234. package/src/app/lib/api/redact.ts +0 -46
@@ -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`,
@@ -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>;