handoff-app 2.0.0-0 → 2.0.0-2

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 (91) hide show
  1. package/README.md +39 -17
  2. package/config/docs/foundations/colors/guidelines.md +9 -0
  3. package/config/docs/foundations/effects/guidelines.md +9 -0
  4. package/config/docs/foundations/effects/resources.md +9 -0
  5. package/config/docs/foundations/grid.md +10 -0
  6. package/config/docs/foundations/icons/guidelines.md +9 -0
  7. package/config/docs/foundations/images/guidelines.md +9 -0
  8. package/config/docs/foundations/images/resources.md +9 -0
  9. package/config/docs/foundations/images.md +10 -0
  10. package/config/docs/foundations/logo/resources.md +9 -0
  11. package/config/docs/foundations/typography/guidelines.md +9 -0
  12. package/config/docs/foundations/typography/resources.md +9 -0
  13. package/config/docs/foundations.md +5 -3
  14. package/dist/app-builder/build.js +89 -46
  15. package/dist/app-builder/client-config.js +5 -5
  16. package/dist/app-builder/paths.js +23 -1
  17. package/dist/app-builder/static-export.js +25 -0
  18. package/dist/app-builder/vercel-output.js +2 -2
  19. package/dist/artifacts/index.d.ts +1 -1
  20. package/dist/artifacts/index.js +4 -1
  21. package/dist/artifacts/url.d.ts +18 -0
  22. package/dist/artifacts/url.js +31 -1
  23. package/dist/cli/create.js +48 -3
  24. package/dist/commands/checkout/index.d.ts +6 -6
  25. package/dist/commands/checkout/index.js +8 -12
  26. package/dist/commands/publish/index.d.ts +6 -5
  27. package/dist/commands/publish/index.js +8 -11
  28. package/dist/commands/utils.d.ts +6 -0
  29. package/dist/commands/utils.js +9 -1
  30. package/dist/index.d.ts +12 -8
  31. package/dist/index.js +26 -10
  32. package/dist/registry/asset-storage/resolve.d.ts +1 -1
  33. package/dist/registry/asset-storage/resolve.js +1 -1
  34. package/dist/registry/checkout/index.d.ts +10 -5
  35. package/dist/registry/checkout/index.js +62 -11
  36. package/dist/registry/client.d.ts +2 -1
  37. package/dist/registry/client.js +8 -0
  38. package/dist/registry/db/client.d.ts +9 -9
  39. package/dist/registry/db/client.js +7 -7
  40. package/dist/registry/db/{adapter.d.ts → driver.d.ts} +16 -14
  41. package/dist/registry/db/{adapter.js → driver.js} +17 -15
  42. package/dist/registry/db/index.d.ts +1 -1
  43. package/dist/registry/db/index.js +9 -9
  44. package/dist/registry/db/migrate.d.ts +1 -1
  45. package/dist/registry/db/migrate.js +5 -5
  46. package/dist/registry/publish/index.d.ts +8 -1
  47. package/dist/registry/publish/index.js +88 -2
  48. package/dist/registry/transfer.d.ts +13 -0
  49. package/dist/types/config.d.ts +12 -8
  50. package/drizzle.config.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/app/components/Foundations/DisplayLogo.tsx +8 -2
  53. package/src/app/lib/docs-api/runtime-config.ts +10 -10
  54. package/src/app/lib/registry-api/transfer.ts +29 -0
  55. package/src/app/lib/registry-connection.ts +2 -2
  56. package/src/app/next.config.mjs +1 -1
  57. package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
  58. package/src/app/pages/assets/logos.tsx +4 -1
  59. package/src/app/pages/foundations/icons/index.tsx +7 -5
  60. package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +8 -8
  61. package/src/app/pages/foundations/logo.tsx +5 -4
  62. package/src/app-builder/build.ts +75 -55
  63. package/src/app-builder/client-config.ts +3 -3
  64. package/src/app-builder/paths.ts +25 -1
  65. package/src/app-builder/static-export.ts +27 -0
  66. package/src/app-builder/vercel-output.ts +2 -2
  67. package/src/artifacts/index.ts +8 -1
  68. package/src/artifacts/url.ts +32 -0
  69. package/src/cli/create.ts +58 -3
  70. package/src/commands/checkout/index.ts +10 -15
  71. package/src/commands/publish/index.ts +10 -14
  72. package/src/commands/utils.ts +12 -0
  73. package/src/index.ts +26 -12
  74. package/src/registry/asset-storage/resolve.ts +1 -1
  75. package/src/registry/checkout/index.ts +73 -11
  76. package/src/registry/client.ts +7 -1
  77. package/src/registry/db/client.ts +12 -12
  78. package/src/registry/db/{adapter.ts → driver.ts} +19 -17
  79. package/src/registry/db/index.ts +4 -4
  80. package/src/registry/db/migrate.ts +5 -5
  81. package/src/registry/publish/index.ts +100 -2
  82. package/src/registry/transfer.ts +14 -0
  83. package/src/types/config.ts +12 -8
  84. package/templates/default/README.md.tpl +40 -1
  85. package/templates/default/gitignore.tpl +18 -3
  86. package/templates/default/handoff.config.js.tpl +14 -0
  87. package/templates/default/handoff.config.ts.tpl +14 -0
  88. package/templates/default/handoff.config.with-examples.js.tpl +14 -0
  89. package/templates/default/handoff.config.with-examples.ts.tpl +14 -0
  90. package/templates/default/package.json.tpl +2 -1
  91. package/templates/default/vercel.json.tpl +4 -0
package/src/cli/create.ts CHANGED
@@ -23,6 +23,8 @@ interface CreateOptions {
23
23
  figmaAccessToken: string;
24
24
  languagePreference: 'typescript' | 'javascript';
25
25
  includeExamples: boolean;
26
+ includeVercel: boolean;
27
+ vercelBuildCommand: string;
26
28
  [key: string]: string | boolean; // Allow dynamic access for template replacement
27
29
  }
28
30
 
@@ -67,7 +69,8 @@ interface TemplateConfig {
67
69
  */
68
70
  const getMandatoryTemplates = (
69
71
  includeExamples: boolean,
70
- languagePreference: 'typescript' | 'javascript'
72
+ languagePreference: 'typescript' | 'javascript',
73
+ includeVercel: boolean
71
74
  ): TemplateConfig[] => {
72
75
  const configFormat = languagePreference;
73
76
  const configTemplateBaseName = includeExamples ? 'handoff.config.with-examples' : 'handoff.config';
@@ -89,6 +92,10 @@ const getMandatoryTemplates = (
89
92
  templates.push({ source: 'tsconfig.json.tpl', destination: 'tsconfig.json' });
90
93
  }
91
94
 
95
+ if (includeVercel) {
96
+ templates.push({ source: 'vercel.json.tpl', destination: 'vercel.json' });
97
+ }
98
+
92
99
  return templates;
93
100
  };
94
101
 
@@ -118,7 +125,8 @@ const renderTemplate = (content: string, data: CreateOptions): string => {
118
125
  };
119
126
 
120
127
  const getDependenciesToInstall = (options: CreateOptions): { dependencies: string[]; devDependencies: string[] } => {
121
- const dependencies = ['handoff-app@latest'];
128
+ // TODO: switch back to `handoff-app@latest` before the 2.0 stable release.
129
+ const dependencies = ['handoff-app@next'];
122
130
  const devDependencies: string[] = [];
123
131
 
124
132
  if (options.includeExamples) {
@@ -263,6 +271,51 @@ const create = async (): Promise<void> => {
263
271
 
264
272
  const languagePreference = languagePreferenceSelection as 'typescript' | 'javascript';
265
273
 
274
+ const includeVercelSelection = await p.confirm({
275
+ message: 'Add a Vercel deployment config (vercel.json)?',
276
+ initialValue: true,
277
+ });
278
+
279
+ if (p.isCancel(includeVercelSelection)) {
280
+ p.cancel('Project creation cancelled.');
281
+ process.exit(0);
282
+ }
283
+
284
+ const includeVercel = includeVercelSelection as boolean;
285
+
286
+ // When a Vercel config is scaffolded, choose which deployment its build command produces. Both
287
+ // package to `.vercel/output`; they differ in what is served. Registry is the recommended,
288
+ // pre-selected option; static stays available as the simplest deploy.
289
+ let vercelBuildCommand = 'npm run build -- --target registry --package vercel';
290
+ if (includeVercel) {
291
+ const vercelDeploymentSelection = await p.select({
292
+ message: 'Which deployment should Vercel build?',
293
+ options: [
294
+ {
295
+ value: 'registry',
296
+ label: 'Registry (dynamic)',
297
+ hint: 'Live, database-backed app (needs a Postgres DATABASE_URL). The more capable, actively evolving option — recommended for a shared design system.',
298
+ },
299
+ {
300
+ value: 'static',
301
+ label: 'Static site',
302
+ hint: 'Prebuilt snapshot served as plain files. The simplest to deploy and run.',
303
+ },
304
+ ],
305
+ initialValue: 'registry',
306
+ });
307
+
308
+ if (p.isCancel(vercelDeploymentSelection)) {
309
+ p.cancel('Project creation cancelled.');
310
+ process.exit(0);
311
+ }
312
+
313
+ vercelBuildCommand =
314
+ vercelDeploymentSelection === 'registry'
315
+ ? 'npm run build -- --target registry --package vercel'
316
+ : 'npm run build -- --target static --package vercel';
317
+ }
318
+
266
319
  // Get Figma project ID
267
320
  p.log.step(chalk.blue('Figma Configuration'));
268
321
  const figmaProjectId = await p.text({
@@ -295,6 +348,8 @@ const create = async (): Promise<void> => {
295
348
  figmaAccessToken: ((figmaAccessToken as string) || '').trim(),
296
349
  languagePreference,
297
350
  includeExamples,
351
+ includeVercel,
352
+ vercelBuildCommand,
298
353
  };
299
354
 
300
355
  // Create files
@@ -309,7 +364,7 @@ const create = async (): Promise<void> => {
309
364
 
310
365
  // Combine mandatory templates with example templates based on user choice
311
366
  const templates = [
312
- ...getMandatoryTemplates(includeExamples, languagePreference),
367
+ ...getMandatoryTemplates(includeExamples, languagePreference, includeVercel),
313
368
  ...(includeExamples ? getExampleTemplates(languagePreference) : []),
314
369
  ];
315
370
 
@@ -2,20 +2,18 @@ import { CommandModule } from 'yargs';
2
2
  import Handoff from '../../';
3
3
  import { Logger } from '../../utils/logger';
4
4
  import { SharedArgs } from '../types';
5
- import { getSharedOptions } from '../utils';
6
-
7
- /** Entity kinds checkout-able from a connected workspace. */
8
- const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'] as const;
5
+ import { ENTITY_WIRE_KIND, getSharedOptions, REGISTRY_ENTITY_KINDS, RegistryEntityKind } from '../utils';
9
6
 
10
7
  export interface CheckoutArgs extends SharedArgs {
11
- type: (typeof ENTITY_KINDS)[number];
8
+ type: RegistryEntityKind;
12
9
  id?: string;
13
10
  }
14
11
 
15
12
  /**
16
- * `handoff-app checkout <component|pattern|page> <id>` — pull an entity from the connected registry
17
- * into this workspace: writes its source files in standard authoring form and synthesizes a local
18
- * declaration.
13
+ * `handoff-app checkout <components|patterns|pages> [id]` pulls entities from the connected registry
14
+ * into this workspace, writing their source files in standard authoring form and synthesizing local
15
+ * declarations. Pass an `id` to pull that one entity, or omit it to pull every published entity of
16
+ * that kind.
19
17
  *
20
18
  * `handoff-app checkout tokens [setId]` — pull every published token set (or only the named set) into
21
19
  * this workspace: reconstruct `tokens.json` and restore the generated token files.
@@ -28,16 +26,16 @@ export interface CheckoutArgs extends SharedArgs {
28
26
  */
29
27
  const command: CommandModule<{}, CheckoutArgs> = {
30
28
  command: 'checkout <type> [id]',
31
- describe: 'Pull a component, pattern, page, design tokens, or assets from the connected registry into this workspace',
29
+ describe: 'Pull components, patterns, pages, design tokens, or assets from the connected registry into this workspace',
32
30
  builder: (yargs) => {
33
31
  return getSharedOptions(yargs)
34
32
  .positional('type', {
35
33
  describe: 'The kind of entity to checkout',
36
- choices: ENTITY_KINDS,
34
+ choices: REGISTRY_ENTITY_KINDS,
37
35
  type: 'string',
38
36
  })
39
37
  .positional('id', {
40
- describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); optional for tokens/assets',
38
+ describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); omit to checkout all of that kind',
41
39
  type: 'string',
42
40
  });
43
41
  },
@@ -52,10 +50,7 @@ const command: CommandModule<{}, CheckoutArgs> = {
52
50
  await handoff.checkoutAssets(args.id);
53
51
  return;
54
52
  }
55
- if (!args.id) {
56
- throw new Error(`An id is required to checkout a ${args.type} (e.g. "handoff-app checkout ${args.type} <id>").`);
57
- }
58
- await handoff.checkout(args.type, args.id);
53
+ await handoff.checkout(ENTITY_WIRE_KIND[args.type], args.id);
59
54
  } catch (error) {
60
55
  Logger.error(error instanceof Error ? error.message : String(error));
61
56
  process.exitCode = 1;
@@ -2,19 +2,18 @@ import { CommandModule } from 'yargs';
2
2
  import Handoff from '../../';
3
3
  import { Logger } from '../../utils/logger';
4
4
  import { SharedArgs } from '../types';
5
- import { getSharedOptions } from '../utils';
6
-
7
- /** Entity kinds publishable from a connected workspace. */
8
- const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'] as const;
5
+ import { ENTITY_WIRE_KIND, getSharedOptions, REGISTRY_ENTITY_KINDS, RegistryEntityKind } from '../utils';
9
6
 
10
7
  export interface PublishArgs extends SharedArgs {
11
- type: (typeof ENTITY_KINDS)[number];
8
+ type: RegistryEntityKind;
12
9
  id?: string;
13
10
  }
14
11
 
15
12
  /**
16
- * `handoff-app publish <component|pattern|page> <id>` — fresh targeted local build + upload of the
17
- * selected entity to the connected registry.
13
+ * `handoff-app publish <components|patterns|pages> [id]` runs a fresh local build and uploads to the
14
+ * connected registry. Pass an `id` to build and upload that one entity, or omit it to build the kind
15
+ * once and upload every declared entity, skipping any whose content already matches the registry
16
+ * (pass `--force` to re-upload everything).
18
17
  *
19
18
  * `handoff-app publish tokens [setId]` — fresh token build + upload of every logical token set, or
20
19
  * only the named set (`foundation/colors`, `component/button`).
@@ -25,16 +24,16 @@ export interface PublishArgs extends SharedArgs {
25
24
  */
26
25
  const command: CommandModule<{}, PublishArgs> = {
27
26
  command: 'publish <type> [id]',
28
- describe: 'Build and publish a component, pattern, page, design tokens, or assets to the connected registry',
27
+ describe: 'Build and publish components, patterns, pages, design tokens, or assets to the connected registry',
29
28
  builder: (yargs) => {
30
29
  return getSharedOptions(yargs)
31
30
  .positional('type', {
32
31
  describe: 'The kind of entity to publish',
33
- choices: ENTITY_KINDS,
32
+ choices: REGISTRY_ENTITY_KINDS,
34
33
  type: 'string',
35
34
  })
36
35
  .positional('id', {
37
- describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); optional for tokens/assets',
36
+ describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); omit to publish all of that kind',
38
37
  type: 'string',
39
38
  });
40
39
  },
@@ -49,10 +48,7 @@ const command: CommandModule<{}, PublishArgs> = {
49
48
  await handoff.publishAssets(args.id);
50
49
  return;
51
50
  }
52
- if (!args.id) {
53
- throw new Error(`An id is required to publish a ${args.type} (e.g. "handoff-app publish ${args.type} <id>").`);
54
- }
55
- await handoff.publish(args.type, args.id);
51
+ await handoff.publish(ENTITY_WIRE_KIND[args.type], args.id);
56
52
  } catch (error) {
57
53
  Logger.error(error instanceof Error ? error.message : String(error));
58
54
  process.exitCode = 1;
@@ -1,4 +1,16 @@
1
1
  import { Argv } from 'yargs';
2
+ import type { TransferEntityKind } from '../registry/transfer';
3
+
4
+ /** Plural entity kinds accepted by the `publish`/`checkout` commands. */
5
+ export const REGISTRY_ENTITY_KINDS = ['components', 'patterns', 'pages', 'tokens', 'assets'] as const;
6
+ export type RegistryEntityKind = (typeof REGISTRY_ENTITY_KINDS)[number];
7
+
8
+ /** Map the plural CLI kind to the singular wire kind used by entity publish/checkout. */
9
+ export const ENTITY_WIRE_KIND: Record<'components' | 'patterns' | 'pages', TransferEntityKind> = {
10
+ components: 'component',
11
+ patterns: 'pattern',
12
+ pages: 'page',
13
+ };
2
14
 
3
15
  export const getSharedOptions = (yargs: Argv) => {
4
16
  return yargs.options({
package/src/index.ts CHANGED
@@ -138,26 +138,40 @@ class Handoff {
138
138
  }
139
139
 
140
140
  /**
141
- * Publish a single component, pattern, or page from this connected workspace to the configured
142
- * remote registry. Components and patterns receive a fresh targeted build before upload. Pages
143
- * upload their record and markdown source because they render at runtime. The publish module is
144
- * loaded lazily so the registry client and build code never enter the docs app bundle.
141
+ * Publish components, patterns, or pages from this connected workspace to the configured remote
142
+ * registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
143
+ * the kind once and publishes every declared entity, skipping ones whose content is unchanged on
144
+ * the registry (`--force` re-uploads all). Pages upload their record and markdown source because
145
+ * they render at runtime. The publish module is loaded lazily so the registry client and build code
146
+ * never enter the docs app bundle.
145
147
  */
146
- async publish(kind: 'component' | 'pattern' | 'page', id: string): Promise<Handoff> {
148
+ async publish(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff> {
147
149
  this.preRunner();
148
- const { publishEntity } = await import('./registry/publish');
149
- await publishEntity(this, kind, id);
150
+ if (id) {
151
+ const { publishEntity } = await import('./registry/publish');
152
+ await publishEntity(this, kind, id);
153
+ } else {
154
+ const { publishEntities } = await import('./registry/publish');
155
+ await publishEntities(this, kind);
156
+ }
150
157
  return this;
151
158
  }
152
159
 
153
160
  /**
154
- * Checkout a single component, pattern, or page from the connected remote registry. The checkout
155
- * module is loaded lazily so the registry client never enters the docs app bundle.
161
+ * Checkout components, patterns, or pages from the connected remote registry into this workspace.
162
+ * With `id` it checks out that one entity; without `id` it checks out every published entity of the
163
+ * kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
164
+ * checkout module is loaded lazily so the registry client never enters the docs app bundle.
156
165
  */
157
- async checkout(kind: 'component' | 'pattern' | 'page', id: string): Promise<Handoff> {
166
+ async checkout(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff> {
158
167
  this.preRunner();
159
- const { checkoutEntity } = await import('./registry/checkout');
160
- await checkoutEntity(this, kind, id);
168
+ if (id) {
169
+ const { checkoutEntity } = await import('./registry/checkout');
170
+ await checkoutEntity(this, kind, id);
171
+ } else {
172
+ const { checkoutEntities } = await import('./registry/checkout');
173
+ await checkoutEntities(this, kind);
174
+ }
161
175
  return this;
162
176
  }
163
177
 
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
5
5
  * `database` inline default, the pre-packaged `vercel-blob` adapter, or a `custom` module. Mirrors
6
- * {@link import('../db/adapter')}: only provider selection, module location, non-secret options, and
6
+ * {@link import('../db/driver')}: only provider selection, module location, non-secret options, and
7
7
  * env-var *names* are ever persisted; secret *values* are read from `process.env` at request time.
8
8
  */
9
9
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Connected-workspace checkout orchestration.
3
3
  *
4
- * `checkout <component|pattern> <id>` reads a normalized registry record and its registry-safe
4
+ * `checkout <components|patterns> <id>` reads a normalized registry record and its registry-safe
5
5
  * source files from the connected registry through the shared registry client, then writes them
6
6
  * into the local workspace in standard authoring form. The local **declaration is synthesized
7
7
  * locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
@@ -20,7 +20,7 @@ import { resolveComponentDeclaration } from '../../config/runtime';
20
20
  import Handoff from '../../index';
21
21
  import type { DeclarationFormat } from '../../types/config';
22
22
  import { Logger } from '../../utils/logger';
23
- import { createRegistryClient, RegistryClientError } from '../client';
23
+ import { createRegistryClient, type RegistryClient, RegistryClientError } from '../client';
24
24
  import { resolveRegistryConnection } from '../connection';
25
25
  import { isSafePathSegment, isSafeRelativePath, resolvePathWithin } from '../path';
26
26
  import type { CheckoutPayload, TransferEntityKind, TransferFile } from '../transfer';
@@ -530,26 +530,31 @@ const checkoutPage = async (handoff: Handoff, id: string, payload: CheckoutPaylo
530
530
  };
531
531
 
532
532
  /**
533
- * Checkout a single component or pattern from a connected workspace: precondition checks → fetch the
534
- * normalized record + source files → resolve the local target → explicit-overwrite guard → write
535
- * source files and synthesize the local declaration. Throws {@link CheckoutError} with actionable
536
- * messaging on any precondition or fetch failure.
533
+ * Checkout one entity through an already-resolved client: precondition checks → fetch the normalized
534
+ * record + source files → resolve the local target → explicit-overwrite guard → write source files
535
+ * and synthesize the local declaration. Shared by the single-id and bulk entry points so both behave
536
+ * identically. Throws {@link CheckoutError} with actionable messaging on any precondition or fetch
537
+ * failure.
537
538
  */
538
- export const checkoutEntity = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<void> => {
539
+ const checkoutSingle = async (
540
+ handoff: Handoff,
541
+ kind: TransferEntityKind,
542
+ id: string,
543
+ client: RegistryClient,
544
+ registryUrl: string
545
+ ): Promise<void> => {
539
546
  const validId = kind === 'page' ? isSafeRelativePath(id) : isSafePathSegment(id);
540
547
  if (!validId) {
541
548
  throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
542
549
  }
543
- const connection = resolveConnectionOrThrow(handoff);
544
550
 
545
- Logger.info(`Fetching ${kind} "${id}" from ${connection.url}…`);
546
- const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
551
+ Logger.info(`Fetching ${kind} "${id}" from ${registryUrl}…`);
547
552
  let payload: CheckoutPayload;
548
553
  try {
549
554
  payload = await client.checkout(kind, id);
550
555
  } catch (error) {
551
556
  if (error instanceof RegistryClientError) {
552
- throw new CheckoutError(describeFetchFailure(error, kind, id, connection.url));
557
+ throw new CheckoutError(describeFetchFailure(error, kind, id, registryUrl));
553
558
  }
554
559
  throw error;
555
560
  }
@@ -594,3 +599,60 @@ export const checkoutEntity = async (handoff: Handoff, kind: TransferEntityKind,
594
599
  `(${payload.files.length} source file(s) + ${declarationFileName}).`
595
600
  );
596
601
  };
602
+
603
+ /**
604
+ * Checkout a single component, pattern, or page from the connected registry into this workspace.
605
+ * Overwriting existing local files requires `--force` or an interactive confirmation.
606
+ */
607
+ export const checkoutEntity = async (handoff: Handoff, kind: TransferEntityKind, id: string): Promise<void> => {
608
+ const connection = resolveConnectionOrThrow(handoff);
609
+ const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
610
+ await checkoutSingle(handoff, kind, id, client, connection.url);
611
+ };
612
+
613
+ /**
614
+ * Checkout every published component, pattern, or page of the kind into this workspace. Enumerates
615
+ * the registry's published ids, then checks each out through the shared client. A per-entity failure
616
+ * is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
617
+ * prompting is per entity (skipped under `--force`).
618
+ */
619
+ export const checkoutEntities = async (handoff: Handoff, kind: TransferEntityKind): Promise<void> => {
620
+ const connection = resolveConnectionOrThrow(handoff);
621
+ const client = createRegistryClient({ baseUrl: connection.url, accessToken: connection.accessToken });
622
+
623
+ let summaries;
624
+ try {
625
+ summaries = await client.listEntities(kind);
626
+ } catch (error) {
627
+ if (error instanceof RegistryClientError) {
628
+ throw new CheckoutError(`Could not list ${kind}s from the registry at ${connection.url}: ${error.message}`);
629
+ }
630
+ throw error;
631
+ }
632
+
633
+ if (summaries.length === 0) {
634
+ Logger.success(`No ${kind}s are published in the registry; nothing to checkout.`);
635
+ return;
636
+ }
637
+
638
+ let checkedOut = 0;
639
+ const failed: { id: string; message: string }[] = [];
640
+ for (const { id } of summaries) {
641
+ try {
642
+ await checkoutSingle(handoff, kind, id, client, connection.url);
643
+ checkedOut += 1;
644
+ } catch (error) {
645
+ failed.push({ id, message: error instanceof Error ? error.message : String(error) });
646
+ }
647
+ }
648
+
649
+ Logger.success(
650
+ `${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} checked out${failed.length ? `, ${failed.length} failed` : ''}.`
651
+ );
652
+ if (failed.length > 0) {
653
+ for (const failure of failed) {
654
+ Logger.error(` - ${failure.id}: ${failure.message}`);
655
+ }
656
+ throw new CheckoutError(`${failed.length} ${kind}(s) failed to checkout.`);
657
+ }
658
+ };
@@ -8,7 +8,7 @@
8
8
  * and the browser both provide it), so this module stays runtime-agnostic.
9
9
  */
10
10
 
11
- import type { CheckoutPayload, TransferEntityKind, TransferPackage } from './transfer';
11
+ import type { CheckoutPayload, EntitySummary, TransferEntityKind, TransferPackage } from './transfer';
12
12
  import type { TokenSetCheckoutPayload, TokenSetSummary, TokenSetTransferPackage } from './tokens/transfer';
13
13
  import type { AssetCollectionCheckoutPayload, AssetCollectionSummary, AssetCollectionTransferPackage } from './assets/transfer';
14
14
 
@@ -55,6 +55,7 @@ export interface RegistryClientOptions {
55
55
  export interface RegistryClient {
56
56
  publish(kind: TransferEntityKind, id: string, pkg: TransferPackage): Promise<RegistryEnvelope>;
57
57
  checkout(kind: TransferEntityKind, id: string): Promise<CheckoutPayload>;
58
+ listEntities(kind: TransferEntityKind): Promise<EntitySummary[]>;
58
59
  listTokenSets(): Promise<TokenSetSummary[]>;
59
60
  publishTokens(pkg: TokenSetTransferPackage): Promise<RegistryEnvelope>;
60
61
  checkoutTokens(id: string): Promise<TokenSetCheckoutPayload>;
@@ -154,6 +155,11 @@ export const createRegistryClient = ({ baseUrl, accessToken }: RegistryClientOpt
154
155
  }
155
156
  return envelope.data;
156
157
  },
158
+ /** List the registry's entities of one kind (id + build hashes) for skip-unchanged / bulk checkout. */
159
+ async listEntities(kind: TransferEntityKind): Promise<EntitySummary[]> {
160
+ const envelope = await request<{ entities: EntitySummary[] }>('GET', `/api/registry/transfer/${kind}`);
161
+ return envelope.data?.entities ?? [];
162
+ },
157
163
  /** List the registry's token sets (id + kind + source hash) for skip-unchanged / bulk checkout. */
158
164
  async listTokenSets(): Promise<TokenSetSummary[]> {
159
165
  const envelope = await request<{ sets: TokenSetSummary[] }>('GET', '/api/registry/transfer/tokens');
@@ -1,24 +1,24 @@
1
1
  /**
2
2
  * Bundled Drizzle client + migrator for the registry database.
3
3
  *
4
- * The package ships both built-in adapters (`pg` and `neon`) and the Drizzle client/migrator, so a
4
+ * The package ships both built-in drivers (`pg` and `neon`) and the Drizzle client/migrator, so a
5
5
  * consuming project needs no global Drizzle install or separate CLI. Driver modules are loaded
6
- * lazily (dynamic import) for the selected adapter only, so workspace dev/build never pulls the
6
+ * lazily (dynamic import) for the selected driver only, so workspace dev/build never pulls the
7
7
  * Postgres/Neon drivers into memory when the registry is not in use.
8
8
  */
9
9
 
10
10
  import type { NeonDatabase } from 'drizzle-orm/neon-serverless';
11
11
  import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
12
- import type { RegistryDatabaseAdapter } from './adapter';
12
+ import type { RegistryDatabaseDriver } from './driver';
13
13
  import * as schema from './schema';
14
14
 
15
- /** Typed Drizzle database over the registry schema, regardless of which adapter backs it. */
15
+ /** Typed Drizzle database over the registry schema, regardless of which driver backs it. */
16
16
  export type RegistryDatabase = NodePgDatabase<typeof schema> | NeonDatabase<typeof schema>;
17
17
 
18
18
  /** A live registry database connection plus its lifecycle operations. */
19
19
  export interface RegistryDbConnection {
20
- /** Selected adapter that produced this connection. */
21
- adapter: RegistryDatabaseAdapter;
20
+ /** Selected driver that produced this connection. */
21
+ driver: RegistryDatabaseDriver;
22
22
  /** Typed Drizzle database. */
23
23
  db: RegistryDatabase;
24
24
  /** Apply the package-owned migrations found in `migrationsFolder`. */
@@ -28,20 +28,20 @@ export interface RegistryDbConnection {
28
28
  }
29
29
 
30
30
  export interface CreateRegistryDbConnectionParams {
31
- adapter: RegistryDatabaseAdapter;
31
+ driver: RegistryDatabaseDriver;
32
32
  connectionString: string;
33
33
  }
34
34
 
35
35
  /**
36
- * Create a registry database connection for the resolved adapter. Both adapters target the
36
+ * Create a registry database connection for the resolved driver. Both drivers target the
37
37
  * Postgres dialect and share the same schema, so the returned connection is interchangeable for
38
38
  * migration and (later) the registry store.
39
39
  */
40
40
  export const createRegistryDbConnection = async ({
41
- adapter,
41
+ driver,
42
42
  connectionString,
43
43
  }: CreateRegistryDbConnectionParams): Promise<RegistryDbConnection> => {
44
- if (adapter === 'neon') {
44
+ if (driver === 'neon') {
45
45
  const { Pool, neonConfig } = await import('@neondatabase/serverless');
46
46
  // Neon's serverless driver needs a WebSocket constructor in Node; reuse the bundled `ws`.
47
47
  const ws = await import('ws');
@@ -59,7 +59,7 @@ export const createRegistryDbConnection = async ({
59
59
  const db = drizzle(pool, { schema });
60
60
 
61
61
  return {
62
- adapter,
62
+ driver,
63
63
  db,
64
64
  migrate: (migrationsFolder: string) => migrate(db, { migrationsFolder }),
65
65
  close: () => pool.end(),
@@ -80,7 +80,7 @@ export const createRegistryDbConnection = async ({
80
80
  const db = drizzle(pool, { schema });
81
81
 
82
82
  return {
83
- adapter,
83
+ driver,
84
84
  db,
85
85
  migrate: (migrationsFolder: string) => migrate(db, { migrationsFolder }),
86
86
  close: () => pool.end(),
@@ -1,17 +1,19 @@
1
1
  /**
2
- * Registry database adapter resolution.
2
+ * Registry database driver resolution.
3
3
  *
4
- * `runtime.registry.database.adapter` selects one of two built-in adapters shipped with the
5
- * `handoff-app` package — `pg` (default) and `neon` — both targeting the Postgres dialect.
6
- * `build --target registry` and `db:migrate` resolve the **same** adapter so build and migration
7
- * never diverge. Connection credentials are referenced by environment-variable name only and
8
- * resolved to a value at runtime; the value is never stored in config.
4
+ * PostgreSQL is the supported database. `runtime.registry.database.driver` selects one of two
5
+ * built-in connection drivers shipped with the `handoff-app` package — `pg` (default) and `neon` —
6
+ * both targeting the same Postgres dialect over one package-owned schema and migration set. The
7
+ * driver picks *how* to connect, not *which* database engine to use. `build --target registry` and
8
+ * `db:migrate` resolve the **same** driver so build and migration never diverge. Connection
9
+ * credentials are referenced by environment-variable name only and resolved to a value at runtime;
10
+ * the value is never stored in config.
9
11
  */
10
12
 
11
13
  import type { Config } from '../../types/config';
12
14
 
13
- /** Built-in registry database adapter. Both ship with the package and target Postgres. */
14
- export type RegistryDatabaseAdapter = 'pg' | 'neon';
15
+ /** Built-in registry database connection driver. Both ship with the package and target Postgres. */
16
+ export type RegistryDatabaseDriver = 'pg' | 'neon';
15
17
 
16
18
  /** Default env-var name holding the database connection string. */
17
19
  export const DEFAULT_DATABASE_URL_ENV = 'DATABASE_URL';
@@ -19,13 +21,13 @@ export const DEFAULT_DATABASE_URL_ENV = 'DATABASE_URL';
19
21
  /** Default env-var name holding the registry management API bearer token. */
20
22
  export const DEFAULT_REGISTRY_API_TOKEN_ENV = 'HANDOFF_REGISTRY_API_TOKEN';
21
23
 
22
- /** Default registry database adapter when none is configured. */
23
- export const DEFAULT_REGISTRY_ADAPTER: RegistryDatabaseAdapter = 'pg';
24
+ /** Default registry database driver when none is configured. */
25
+ export const DEFAULT_REGISTRY_DRIVER: RegistryDatabaseDriver = 'pg';
24
26
 
25
27
  /** Fully resolved registry database connection inputs. */
26
28
  export interface ResolvedRegistryDatabase {
27
- /** Selected built-in adapter. */
28
- adapter: RegistryDatabaseAdapter;
29
+ /** Selected built-in driver. */
30
+ driver: RegistryDatabaseDriver;
29
31
  /** Name of the env var the connection string was read from. */
30
32
  databaseUrlEnv: string;
31
33
  /** Resolved connection string (never persisted in config). */
@@ -48,9 +50,9 @@ export const resolveApiTokenEnv = (config: Config | null | undefined): string =>
48
50
  return configured || DEFAULT_REGISTRY_API_TOKEN_ENV;
49
51
  };
50
52
 
51
- /** Resolve the configured registry adapter (defaults to `pg`). */
52
- export const resolveRegistryAdapter = (config: Config | null | undefined): RegistryDatabaseAdapter => {
53
- return config?.runtime?.registry?.database?.adapter ?? DEFAULT_REGISTRY_ADAPTER;
53
+ /** Resolve the configured registry database driver (defaults to `pg`). */
54
+ export const resolveRegistryDriver = (config: Config | null | undefined): RegistryDatabaseDriver => {
55
+ return config?.runtime?.registry?.database?.driver ?? DEFAULT_REGISTRY_DRIVER;
54
56
  };
55
57
 
56
58
  /**
@@ -59,7 +61,7 @@ export const resolveRegistryAdapter = (config: Config | null | undefined): Regis
59
61
  * message instead of failing deep inside the driver.
60
62
  */
61
63
  export const resolveRegistryDatabase = (config: Config | null | undefined): ResolvedRegistryDatabase => {
62
- const adapter = resolveRegistryAdapter(config);
64
+ const driver = resolveRegistryDriver(config);
63
65
  const databaseUrlEnv = resolveDatabaseUrlEnv(config);
64
66
  const connectionString = process.env[databaseUrlEnv]?.trim();
65
67
 
@@ -70,5 +72,5 @@ export const resolveRegistryDatabase = (config: Config | null | undefined): Reso
70
72
  );
71
73
  }
72
74
 
73
- return { adapter, databaseUrlEnv, connectionString };
75
+ return { driver, databaseUrlEnv, connectionString };
74
76
  };
@@ -15,15 +15,15 @@ export type { ArtifactBuildStatus as RegistryBuildStatus } from '../../artifacts
15
15
 
16
16
  export {
17
17
  DEFAULT_DATABASE_URL_ENV,
18
- DEFAULT_REGISTRY_ADAPTER,
18
+ DEFAULT_REGISTRY_DRIVER,
19
19
  DEFAULT_REGISTRY_API_TOKEN_ENV,
20
20
  resolveApiTokenEnv,
21
21
  resolveDatabaseUrlEnv,
22
- resolveRegistryAdapter,
22
+ resolveRegistryDriver,
23
23
  resolveRegistryDatabase,
24
- type RegistryDatabaseAdapter,
24
+ type RegistryDatabaseDriver,
25
25
  type ResolvedRegistryDatabase,
26
- } from './adapter';
26
+ } from './driver';
27
27
 
28
28
  export {
29
29
  createRegistryDbConnection,