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
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeBasePath = exports.buildArtifactUrl = exports.ARTIFACTS_ROUTE_SEGMENT = exports.SHARED_STYLES_CSS_ARTIFACT_PATH = exports.SHARED_MAIN_JS_ARTIFACT_PATH = exports.SHARED_MAIN_CSS_ARTIFACT_PATH = exports.REACT_PREVIEW_FORMAT_VERSION = exports.ARTIFACT_METADATA_PATH = void 0;
3
+ exports.normalizeBasePath = exports.buildAssetUrl = exports.buildAssetDownloadUrl = exports.buildArtifactUrl = exports.ASSETS_ROUTE_SEGMENT = exports.ARTIFACTS_ROUTE_SEGMENT = exports.SHARED_STYLES_CSS_ARTIFACT_PATH = exports.SHARED_MAIN_JS_ARTIFACT_PATH = exports.SHARED_MAIN_CSS_ARTIFACT_PATH = exports.REACT_PREVIEW_FORMAT_VERSION = exports.ARTIFACT_METADATA_PATH = void 0;
4
4
  var types_1 = require("./types");
5
5
  Object.defineProperty(exports, "ARTIFACT_METADATA_PATH", { enumerable: true, get: function () { return types_1.ARTIFACT_METADATA_PATH; } });
6
6
  Object.defineProperty(exports, "REACT_PREVIEW_FORMAT_VERSION", { enumerable: true, get: function () { return types_1.REACT_PREVIEW_FORMAT_VERSION; } });
@@ -9,5 +9,8 @@ Object.defineProperty(exports, "SHARED_MAIN_JS_ARTIFACT_PATH", { enumerable: tru
9
9
  Object.defineProperty(exports, "SHARED_STYLES_CSS_ARTIFACT_PATH", { enumerable: true, get: function () { return types_1.SHARED_STYLES_CSS_ARTIFACT_PATH; } });
10
10
  var url_1 = require("./url");
11
11
  Object.defineProperty(exports, "ARTIFACTS_ROUTE_SEGMENT", { enumerable: true, get: function () { return url_1.ARTIFACTS_ROUTE_SEGMENT; } });
12
+ Object.defineProperty(exports, "ASSETS_ROUTE_SEGMENT", { enumerable: true, get: function () { return url_1.ASSETS_ROUTE_SEGMENT; } });
12
13
  Object.defineProperty(exports, "buildArtifactUrl", { enumerable: true, get: function () { return url_1.buildArtifactUrl; } });
14
+ Object.defineProperty(exports, "buildAssetDownloadUrl", { enumerable: true, get: function () { return url_1.buildAssetDownloadUrl; } });
15
+ Object.defineProperty(exports, "buildAssetUrl", { enumerable: true, get: function () { return url_1.buildAssetUrl; } });
13
16
  Object.defineProperty(exports, "normalizeBasePath", { enumerable: true, get: function () { return url_1.normalizeBasePath; } });
@@ -17,6 +17,8 @@
17
17
  export declare const ARTIFACTS_ROUTE_SEGMENT = "api/docs/artifacts";
18
18
  /** Route prefix for docs read API metadata (`.json`) reads. */
19
19
  export declare const DOCS_ROUTE_SEGMENT = "api/docs";
20
+ /** Route prefix all asset content URLs are served under, regardless of runtime mode. */
21
+ export declare const ASSETS_ROUTE_SEGMENT = "api/docs/assets";
20
22
  /**
21
23
  * Normalize a configured basePath to either an empty string or a single-leading-slash,
22
24
  * no-trailing-slash form. Mirrors the Next app's `resolveBasePath` so builder output composes
@@ -47,3 +49,19 @@ export declare const buildComponentDetailUrl: (id: string, basePath?: string | n
47
49
  * (`{basePath}/api/docs/patterns/{id}.json`). The id is URL-encoded; `basePath` is preserved.
48
50
  */
49
51
  export declare const buildPatternDetailUrl: (id: string, basePath?: string | null) => string;
52
+ /**
53
+ * Build the basePath-aware content URL for a single asset in a collection
54
+ * (`{basePath}/api/docs/assets/{collection}/{logicalPath}`). This is the same route the docs read API
55
+ * serves in every runtime mode: from the database in registry mode, and from statically materialized
56
+ * files in a static export. Each segment is encoded on its own and traversal segments are rejected,
57
+ * matching {@link buildArtifactUrl}.
58
+ *
59
+ * @throws If the collection or logical path has no usable segments, or contains a `.`/`..` segment.
60
+ */
61
+ export declare const buildAssetUrl: (collection: string, logicalPath: string, basePath?: string | null) => string;
62
+ /**
63
+ * Build the download URL for a collection's zip bundle
64
+ * (`{basePath}/api/docs/assets/{collection}/{collection}.zip`). This is the default target for the
65
+ * docs "Download" links and can be overridden via `assets_zip_links`.
66
+ */
67
+ export declare const buildAssetDownloadUrl: (collection: string, basePath?: string | null) => string;
@@ -15,11 +15,13 @@
15
15
  * Next app can both import it.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.buildPatternDetailUrl = exports.buildComponentDetailUrl = exports.buildArtifactUrl = exports.normalizeBasePath = exports.DOCS_ROUTE_SEGMENT = exports.ARTIFACTS_ROUTE_SEGMENT = void 0;
18
+ exports.buildAssetDownloadUrl = exports.buildAssetUrl = exports.buildPatternDetailUrl = exports.buildComponentDetailUrl = exports.buildArtifactUrl = exports.normalizeBasePath = exports.ASSETS_ROUTE_SEGMENT = exports.DOCS_ROUTE_SEGMENT = exports.ARTIFACTS_ROUTE_SEGMENT = void 0;
19
19
  /** Canonical route prefix all artifact URLs are served under. */
20
20
  exports.ARTIFACTS_ROUTE_SEGMENT = 'api/docs/artifacts';
21
21
  /** Route prefix for docs read API metadata (`.json`) reads. */
22
22
  exports.DOCS_ROUTE_SEGMENT = 'api/docs';
23
+ /** Route prefix all asset content URLs are served under, regardless of runtime mode. */
24
+ exports.ASSETS_ROUTE_SEGMENT = 'api/docs/assets';
23
25
  /**
24
26
  * Normalize a configured basePath to either an empty string or a single-leading-slash,
25
27
  * no-trailing-slash form. Mirrors the Next app's `resolveBasePath` so builder output composes
@@ -84,3 +86,31 @@ exports.buildComponentDetailUrl = buildComponentDetailUrl;
84
86
  */
85
87
  const buildPatternDetailUrl = (id, basePath) => `${(0, exports.normalizeBasePath)(basePath)}/${exports.DOCS_ROUTE_SEGMENT}/patterns/${encodeURIComponent(id)}.json`;
86
88
  exports.buildPatternDetailUrl = buildPatternDetailUrl;
89
+ /**
90
+ * Build the basePath-aware content URL for a single asset in a collection
91
+ * (`{basePath}/api/docs/assets/{collection}/{logicalPath}`). This is the same route the docs read API
92
+ * serves in every runtime mode: from the database in registry mode, and from statically materialized
93
+ * files in a static export. Each segment is encoded on its own and traversal segments are rejected,
94
+ * matching {@link buildArtifactUrl}.
95
+ *
96
+ * @throws If the collection or logical path has no usable segments, or contains a `.`/`..` segment.
97
+ */
98
+ const buildAssetUrl = (collection, logicalPath, basePath) => {
99
+ const segments = [...toArtifactSegments(collection), ...toArtifactSegments(logicalPath)];
100
+ if (segments.length < 2) {
101
+ throw new Error(`Cannot build asset URL: empty collection ("${collection}") or path ("${logicalPath}").`);
102
+ }
103
+ if (segments.some(isTraversalSegment)) {
104
+ throw new Error(`Cannot build asset URL: path traversal is not allowed in "${collection}/${logicalPath}".`);
105
+ }
106
+ const encoded = segments.map((segment) => encodeURIComponent(segment)).join('/');
107
+ return `${(0, exports.normalizeBasePath)(basePath)}/${exports.ASSETS_ROUTE_SEGMENT}/${encoded}`;
108
+ };
109
+ exports.buildAssetUrl = buildAssetUrl;
110
+ /**
111
+ * Build the download URL for a collection's zip bundle
112
+ * (`{basePath}/api/docs/assets/{collection}/{collection}.zip`). This is the default target for the
113
+ * docs "Download" links and can be overridden via `assets_zip_links`.
114
+ */
115
+ const buildAssetDownloadUrl = (collection, basePath) => (0, exports.buildAssetUrl)(collection, `${collection}.zip`, basePath);
116
+ exports.buildAssetDownloadUrl = buildAssetDownloadUrl;
@@ -85,7 +85,7 @@ const createDirectory = (dirPath) => {
85
85
  * Get mandatory templates based on whether examples are included
86
86
  * Uses different handoff config templates for blank vs. with-examples projects
87
87
  */
88
- const getMandatoryTemplates = (includeExamples, languagePreference) => {
88
+ const getMandatoryTemplates = (includeExamples, languagePreference, includeVercel) => {
89
89
  const configFormat = languagePreference;
90
90
  const configTemplateBaseName = includeExamples ? 'handoff.config.with-examples' : 'handoff.config';
91
91
  const configTemplateSource = `${configTemplateBaseName}.${configFormat === 'typescript' ? 'ts' : 'js'}.tpl`;
@@ -103,6 +103,9 @@ const getMandatoryTemplates = (includeExamples, languagePreference) => {
103
103
  if (languagePreference === 'typescript') {
104
104
  templates.push({ source: 'tsconfig.json.tpl', destination: 'tsconfig.json' });
105
105
  }
106
+ if (includeVercel) {
107
+ templates.push({ source: 'vercel.json.tpl', destination: 'vercel.json' });
108
+ }
106
109
  return templates;
107
110
  };
108
111
  const getExampleTemplates = (languagePreference) => {
@@ -127,7 +130,8 @@ const renderTemplate = (content, data) => {
127
130
  });
128
131
  };
129
132
  const getDependenciesToInstall = (options) => {
130
- const dependencies = ['handoff-app@latest'];
133
+ // TODO: switch back to `handoff-app@latest` before the 2.0 stable release.
134
+ const dependencies = ['handoff-app@next'];
131
135
  const devDependencies = [];
132
136
  if (options.includeExamples) {
133
137
  dependencies.push('react', 'react-dom');
@@ -250,6 +254,45 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
250
254
  process.exit(0);
251
255
  }
252
256
  const languagePreference = languagePreferenceSelection;
257
+ const includeVercelSelection = yield p.confirm({
258
+ message: 'Add a Vercel deployment config (vercel.json)?',
259
+ initialValue: true,
260
+ });
261
+ if (p.isCancel(includeVercelSelection)) {
262
+ p.cancel('Project creation cancelled.');
263
+ process.exit(0);
264
+ }
265
+ const includeVercel = includeVercelSelection;
266
+ // When a Vercel config is scaffolded, choose which deployment its build command produces. Both
267
+ // package to `.vercel/output`; they differ in what is served. Registry is the recommended,
268
+ // pre-selected option; static stays available as the simplest deploy.
269
+ let vercelBuildCommand = 'npm run build -- --target registry --package vercel';
270
+ if (includeVercel) {
271
+ const vercelDeploymentSelection = yield p.select({
272
+ message: 'Which deployment should Vercel build?',
273
+ options: [
274
+ {
275
+ value: 'registry',
276
+ label: 'Registry (dynamic)',
277
+ hint: 'Live, database-backed app (needs a Postgres DATABASE_URL). The more capable, actively evolving option — recommended for a shared design system.',
278
+ },
279
+ {
280
+ value: 'static',
281
+ label: 'Static site',
282
+ hint: 'Prebuilt snapshot served as plain files. The simplest to deploy and run.',
283
+ },
284
+ ],
285
+ initialValue: 'registry',
286
+ });
287
+ if (p.isCancel(vercelDeploymentSelection)) {
288
+ p.cancel('Project creation cancelled.');
289
+ process.exit(0);
290
+ }
291
+ vercelBuildCommand =
292
+ vercelDeploymentSelection === 'registry'
293
+ ? 'npm run build -- --target registry --package vercel'
294
+ : 'npm run build -- --target static --package vercel';
295
+ }
253
296
  // Get Figma project ID
254
297
  p.log.step(chalk_1.default.blue('Figma Configuration'));
255
298
  const figmaProjectId = yield p.text({
@@ -277,6 +320,8 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
277
320
  figmaAccessToken: (figmaAccessToken || '').trim(),
278
321
  languagePreference,
279
322
  includeExamples,
323
+ includeVercel,
324
+ vercelBuildCommand,
280
325
  };
281
326
  // Create files
282
327
  const spinner = p.spinner();
@@ -287,7 +332,7 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
287
332
  }
288
333
  // Combine mandatory templates with example templates based on user choice
289
334
  const templates = [
290
- ...getMandatoryTemplates(includeExamples, languagePreference),
335
+ ...getMandatoryTemplates(includeExamples, languagePreference, includeVercel),
291
336
  ...(includeExamples ? getExampleTemplates(languagePreference) : []),
292
337
  ];
293
338
  try {
@@ -1,15 +1,15 @@
1
1
  import { CommandModule } from 'yargs';
2
2
  import { SharedArgs } from '../types';
3
- /** Entity kinds checkout-able from a connected workspace. */
4
- declare const ENTITY_KINDS: readonly ["component", "pattern", "page", "tokens", "assets"];
3
+ import { RegistryEntityKind } from '../utils';
5
4
  export interface CheckoutArgs extends SharedArgs {
6
- type: (typeof ENTITY_KINDS)[number];
5
+ type: RegistryEntityKind;
7
6
  id?: string;
8
7
  }
9
8
  /**
10
- * `handoff-app checkout <component|pattern|page> <id>` — pull an entity from the connected registry
11
- * into this workspace: writes its source files in standard authoring form and synthesizes a local
12
- * declaration.
9
+ * `handoff-app checkout <components|patterns|pages> [id]` pulls entities from the connected registry
10
+ * into this workspace, writing their source files in standard authoring form and synthesizing local
11
+ * declarations. Pass an `id` to pull that one entity, or omit it to pull every published entity of
12
+ * that kind.
13
13
  *
14
14
  * `handoff-app checkout tokens [setId]` — pull every published token set (or only the named set) into
15
15
  * this workspace: reconstruct `tokens.json` and restore the generated token files.
@@ -15,12 +15,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const __1 = __importDefault(require("../../"));
16
16
  const logger_1 = require("../../utils/logger");
17
17
  const utils_1 = require("../utils");
18
- /** Entity kinds checkout-able from a connected workspace. */
19
- const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'];
20
18
  /**
21
- * `handoff-app checkout <component|pattern|page> <id>` — pull an entity from the connected registry
22
- * into this workspace: writes its source files in standard authoring form and synthesizes a local
23
- * declaration.
19
+ * `handoff-app checkout <components|patterns|pages> [id]` pulls entities from the connected registry
20
+ * into this workspace, writing their source files in standard authoring form and synthesizing local
21
+ * declarations. Pass an `id` to pull that one entity, or omit it to pull every published entity of
22
+ * that kind.
24
23
  *
25
24
  * `handoff-app checkout tokens [setId]` — pull every published token set (or only the named set) into
26
25
  * this workspace: reconstruct `tokens.json` and restore the generated token files.
@@ -33,16 +32,16 @@ const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'];
33
32
  */
34
33
  const command = {
35
34
  command: 'checkout <type> [id]',
36
- describe: 'Pull a component, pattern, page, design tokens, or assets from the connected registry into this workspace',
35
+ describe: 'Pull components, patterns, pages, design tokens, or assets from the connected registry into this workspace',
37
36
  builder: (yargs) => {
38
37
  return (0, utils_1.getSharedOptions)(yargs)
39
38
  .positional('type', {
40
39
  describe: 'The kind of entity to checkout',
41
- choices: ENTITY_KINDS,
40
+ choices: utils_1.REGISTRY_ENTITY_KINDS,
42
41
  type: 'string',
43
42
  })
44
43
  .positional('id', {
45
- describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); optional for tokens/assets',
44
+ describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); omit to checkout all of that kind',
46
45
  type: 'string',
47
46
  });
48
47
  },
@@ -57,10 +56,7 @@ const command = {
57
56
  yield handoff.checkoutAssets(args.id);
58
57
  return;
59
58
  }
60
- if (!args.id) {
61
- throw new Error(`An id is required to checkout a ${args.type} (e.g. "handoff-app checkout ${args.type} <id>").`);
62
- }
63
- yield handoff.checkout(args.type, args.id);
59
+ yield handoff.checkout(utils_1.ENTITY_WIRE_KIND[args.type], args.id);
64
60
  }
65
61
  catch (error) {
66
62
  logger_1.Logger.error(error instanceof Error ? error.message : String(error));
@@ -1,14 +1,15 @@
1
1
  import { CommandModule } from 'yargs';
2
2
  import { SharedArgs } from '../types';
3
- /** Entity kinds publishable from a connected workspace. */
4
- declare const ENTITY_KINDS: readonly ["component", "pattern", "page", "tokens", "assets"];
3
+ import { RegistryEntityKind } from '../utils';
5
4
  export interface PublishArgs extends SharedArgs {
6
- type: (typeof ENTITY_KINDS)[number];
5
+ type: RegistryEntityKind;
7
6
  id?: string;
8
7
  }
9
8
  /**
10
- * `handoff-app publish <component|pattern|page> <id>` — fresh targeted local build + upload of the
11
- * selected entity to the connected registry.
9
+ * `handoff-app publish <components|patterns|pages> [id]` runs a fresh local build and uploads to the
10
+ * connected registry. Pass an `id` to build and upload that one entity, or omit it to build the kind
11
+ * once and upload every declared entity, skipping any whose content already matches the registry
12
+ * (pass `--force` to re-upload everything).
12
13
  *
13
14
  * `handoff-app publish tokens [setId]` — fresh token build + upload of every logical token set, or
14
15
  * only the named set (`foundation/colors`, `component/button`).
@@ -15,11 +15,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const __1 = __importDefault(require("../../"));
16
16
  const logger_1 = require("../../utils/logger");
17
17
  const utils_1 = require("../utils");
18
- /** Entity kinds publishable from a connected workspace. */
19
- const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'];
20
18
  /**
21
- * `handoff-app publish <component|pattern|page> <id>` — fresh targeted local build + upload of the
22
- * selected entity to the connected registry.
19
+ * `handoff-app publish <components|patterns|pages> [id]` runs a fresh local build and uploads to the
20
+ * connected registry. Pass an `id` to build and upload that one entity, or omit it to build the kind
21
+ * once and upload every declared entity, skipping any whose content already matches the registry
22
+ * (pass `--force` to re-upload everything).
23
23
  *
24
24
  * `handoff-app publish tokens [setId]` — fresh token build + upload of every logical token set, or
25
25
  * only the named set (`foundation/colors`, `component/button`).
@@ -30,16 +30,16 @@ const ENTITY_KINDS = ['component', 'pattern', 'page', 'tokens', 'assets'];
30
30
  */
31
31
  const command = {
32
32
  command: 'publish <type> [id]',
33
- describe: 'Build and publish a component, pattern, page, design tokens, or assets to the connected registry',
33
+ describe: 'Build and publish components, patterns, pages, design tokens, or assets to the connected registry',
34
34
  builder: (yargs) => {
35
35
  return (0, utils_1.getSharedOptions)(yargs)
36
36
  .positional('type', {
37
37
  describe: 'The kind of entity to publish',
38
- choices: ENTITY_KINDS,
38
+ choices: utils_1.REGISTRY_ENTITY_KINDS,
39
39
  type: 'string',
40
40
  })
41
41
  .positional('id', {
42
- describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); optional for tokens/assets',
42
+ describe: 'The stable id of the entity (component/pattern/page id, token set id, or asset collection); omit to publish all of that kind',
43
43
  type: 'string',
44
44
  });
45
45
  },
@@ -54,10 +54,7 @@ const command = {
54
54
  yield handoff.publishAssets(args.id);
55
55
  return;
56
56
  }
57
- if (!args.id) {
58
- throw new Error(`An id is required to publish a ${args.type} (e.g. "handoff-app publish ${args.type} <id>").`);
59
- }
60
- yield handoff.publish(args.type, args.id);
57
+ yield handoff.publish(utils_1.ENTITY_WIRE_KIND[args.type], args.id);
61
58
  }
62
59
  catch (error) {
63
60
  logger_1.Logger.error(error instanceof Error ? error.message : String(error));
@@ -1,4 +1,10 @@
1
1
  import { Argv } from 'yargs';
2
+ import type { TransferEntityKind } from '../registry/transfer';
3
+ /** Plural entity kinds accepted by the `publish`/`checkout` commands. */
4
+ export declare const REGISTRY_ENTITY_KINDS: readonly ["components", "patterns", "pages", "tokens", "assets"];
5
+ export type RegistryEntityKind = (typeof REGISTRY_ENTITY_KINDS)[number];
6
+ /** Map the plural CLI kind to the singular wire kind used by entity publish/checkout. */
7
+ export declare const ENTITY_WIRE_KIND: Record<'components' | 'patterns' | 'pages', TransferEntityKind>;
2
8
  export declare const getSharedOptions: (yargs: Argv) => Argv<import("yargs").Omit<{}, "debug" | "config" | "force"> & import("yargs").InferredOptionTypes<{
3
9
  config: {
4
10
  alias: string;
@@ -1,6 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSharedOptions = void 0;
3
+ exports.getSharedOptions = exports.ENTITY_WIRE_KIND = exports.REGISTRY_ENTITY_KINDS = void 0;
4
+ /** Plural entity kinds accepted by the `publish`/`checkout` commands. */
5
+ exports.REGISTRY_ENTITY_KINDS = ['components', 'patterns', 'pages', 'tokens', 'assets'];
6
+ /** Map the plural CLI kind to the singular wire kind used by entity publish/checkout. */
7
+ exports.ENTITY_WIRE_KIND = {
8
+ components: 'component',
9
+ patterns: 'pattern',
10
+ pages: 'page',
11
+ };
4
12
  const getSharedOptions = (yargs) => {
5
13
  return yargs.options({
6
14
  config: {
package/dist/index.d.ts CHANGED
@@ -45,17 +45,21 @@ declare class Handoff {
45
45
  */
46
46
  dbMigrate(): Promise<Handoff>;
47
47
  /**
48
- * Publish a single component, pattern, or page from this connected workspace to the configured
49
- * remote registry. Components and patterns receive a fresh targeted build before upload. Pages
50
- * upload their record and markdown source because they render at runtime. The publish module is
51
- * loaded lazily so the registry client and build code never enter the docs app bundle.
48
+ * Publish components, patterns, or pages from this connected workspace to the configured remote
49
+ * registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
50
+ * the kind once and publishes every declared entity, skipping ones whose content is unchanged on
51
+ * the registry (`--force` re-uploads all). Pages upload their record and markdown source because
52
+ * they render at runtime. The publish module is loaded lazily so the registry client and build code
53
+ * never enter the docs app bundle.
52
54
  */
53
- publish(kind: 'component' | 'pattern' | 'page', id: string): Promise<Handoff>;
55
+ publish(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff>;
54
56
  /**
55
- * Checkout a single component, pattern, or page from the connected remote registry. The checkout
56
- * module is loaded lazily so the registry client never enters the docs app bundle.
57
+ * Checkout components, patterns, or pages from the connected remote registry into this workspace.
58
+ * With `id` it checks out that one entity; without `id` it checks out every published entity of the
59
+ * kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
60
+ * checkout module is loaded lazily so the registry client never enters the docs app bundle.
57
61
  */
58
- checkout(kind: 'component' | 'pattern' | 'page', id: string): Promise<Handoff>;
62
+ checkout(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff>;
59
63
  /**
60
64
  * Publish design token sets from this connected workspace to the configured remote registry. Runs a
61
65
  * fresh token build (Figma extract + style transformers), discovers the logical sets, and uploads
package/dist/index.js CHANGED
@@ -159,28 +159,44 @@ class Handoff {
159
159
  });
160
160
  }
161
161
  /**
162
- * Publish a single component, pattern, or page from this connected workspace to the configured
163
- * remote registry. Components and patterns receive a fresh targeted build before upload. Pages
164
- * upload their record and markdown source because they render at runtime. The publish module is
165
- * loaded lazily so the registry client and build code never enter the docs app bundle.
162
+ * Publish components, patterns, or pages from this connected workspace to the configured remote
163
+ * registry. With `id` it publishes that one entity after a targeted build; without `id` it builds
164
+ * the kind once and publishes every declared entity, skipping ones whose content is unchanged on
165
+ * the registry (`--force` re-uploads all). Pages upload their record and markdown source because
166
+ * they render at runtime. The publish module is loaded lazily so the registry client and build code
167
+ * never enter the docs app bundle.
166
168
  */
167
169
  publish(kind, id) {
168
170
  return __awaiter(this, void 0, void 0, function* () {
169
171
  this.preRunner();
170
- const { publishEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
171
- yield publishEntity(this, kind, id);
172
+ if (id) {
173
+ const { publishEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
174
+ yield publishEntity(this, kind, id);
175
+ }
176
+ else {
177
+ const { publishEntities } = yield Promise.resolve().then(() => __importStar(require('./registry/publish')));
178
+ yield publishEntities(this, kind);
179
+ }
172
180
  return this;
173
181
  });
174
182
  }
175
183
  /**
176
- * Checkout a single component, pattern, or page from the connected remote registry. The checkout
177
- * module is loaded lazily so the registry client never enters the docs app bundle.
184
+ * Checkout components, patterns, or pages from the connected remote registry into this workspace.
185
+ * With `id` it checks out that one entity; without `id` it checks out every published entity of the
186
+ * kind. Overwriting existing local files requires `--force` or an interactive confirmation. The
187
+ * checkout module is loaded lazily so the registry client never enters the docs app bundle.
178
188
  */
179
189
  checkout(kind, id) {
180
190
  return __awaiter(this, void 0, void 0, function* () {
181
191
  this.preRunner();
182
- const { checkoutEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
183
- yield checkoutEntity(this, kind, id);
192
+ if (id) {
193
+ const { checkoutEntity } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
194
+ yield checkoutEntity(this, kind, id);
195
+ }
196
+ else {
197
+ const { checkoutEntities } = yield Promise.resolve().then(() => __importStar(require('./registry/checkout')));
198
+ yield checkoutEntities(this, kind);
199
+ }
184
200
  return this;
185
201
  });
186
202
  }
@@ -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
  import type { Config } from '../../types/config';
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * `runtime.registry.assetStorage` selects the active provider for **new** uploads: the built-in
6
6
  * `database` inline default, the pre-packaged `vercel-blob` adapter, or a `custom` module. Mirrors
7
- * {@link import('../db/adapter')}: only provider selection, module location, non-secret options, and
7
+ * {@link import('../db/driver')}: only provider selection, module location, non-secret options, and
8
8
  * env-var *names* are ever persisted; secret *values* are read from `process.env` at request time.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
@@ -25,9 +25,14 @@ export declare class CheckoutError extends Error {
25
25
  */
26
26
  export declare const resolveConnectionOrThrow: (handoff: Handoff) => import("../connection").ResolvedRegistryConnection;
27
27
  /**
28
- * Checkout a single component or pattern from a connected workspace: precondition checks → fetch the
29
- * normalized record + source files → resolve the local target → explicit-overwrite guard → write
30
- * source files and synthesize the local declaration. Throws {@link CheckoutError} with actionable
31
- * messaging on any precondition or fetch failure.
28
+ * Checkout a single component, pattern, or page from the connected registry into this workspace.
29
+ * Overwriting existing local files requires `--force` or an interactive confirmation.
32
30
  */
33
31
  export declare const checkoutEntity: (handoff: Handoff, kind: TransferEntityKind, id: string) => Promise<void>;
32
+ /**
33
+ * Checkout every published component, pattern, or page of the kind into this workspace. Enumerates
34
+ * the registry's published ids, then checks each out through the shared client. A per-entity failure
35
+ * is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
36
+ * prompting is per entity (skipped under `--force`).
37
+ */
38
+ export declare const checkoutEntities: (handoff: Handoff, kind: TransferEntityKind) => Promise<void>;
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Connected-workspace checkout orchestration.
4
4
  *
5
- * `checkout <component|pattern> <id>` reads a normalized registry record and its registry-safe
5
+ * `checkout <components|patterns> <id>` reads a normalized registry record and its registry-safe
6
6
  * source files from the connected registry through the shared registry client, then writes them
7
7
  * into the local workspace in standard authoring form. The local **declaration is synthesized
8
8
  * locally** in the configured `runtime.workspace.declarationFormat` — declarations are never read
@@ -60,7 +60,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
60
60
  return (mod && mod.__esModule) ? mod : { "default": mod };
61
61
  };
62
62
  Object.defineProperty(exports, "__esModule", { value: true });
63
- exports.checkoutEntity = exports.resolveConnectionOrThrow = exports.CheckoutError = void 0;
63
+ exports.checkoutEntities = exports.checkoutEntity = exports.resolveConnectionOrThrow = exports.CheckoutError = void 0;
64
64
  const p = __importStar(require("@clack/prompts"));
65
65
  const fs_extra_1 = __importDefault(require("fs-extra"));
66
66
  const path_1 = __importDefault(require("path"));
@@ -551,26 +551,25 @@ const checkoutPage = (handoff, id, payload) => __awaiter(void 0, void 0, void 0,
551
551
  logger_1.Logger.success(`Checked out page "${id}" into ${path_1.default.relative(handoff.workingPath, pagesRoot) || '.'} ` + `(${payload.files.length} markdown file(s)).`);
552
552
  });
553
553
  /**
554
- * Checkout a single component or pattern from a connected workspace: precondition checks → fetch the
555
- * normalized record + source files → resolve the local target → explicit-overwrite guard → write
556
- * source files and synthesize the local declaration. Throws {@link CheckoutError} with actionable
557
- * messaging on any precondition or fetch failure.
554
+ * Checkout one entity through an already-resolved client: precondition checks → fetch the normalized
555
+ * record + source files → resolve the local target → explicit-overwrite guard → write source files
556
+ * and synthesize the local declaration. Shared by the single-id and bulk entry points so both behave
557
+ * identically. Throws {@link CheckoutError} with actionable messaging on any precondition or fetch
558
+ * failure.
558
559
  */
559
- const checkoutEntity = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
560
+ const checkoutSingle = (handoff, kind, id, client, registryUrl) => __awaiter(void 0, void 0, void 0, function* () {
560
561
  const validId = kind === 'page' ? (0, path_2.isSafeRelativePath)(id) : (0, path_2.isSafePathSegment)(id);
561
562
  if (!validId) {
562
563
  throw new CheckoutError(`Cannot checkout ${kind} with unsafe id "${id}".`);
563
564
  }
564
- const connection = (0, exports.resolveConnectionOrThrow)(handoff);
565
- logger_1.Logger.info(`Fetching ${kind} "${id}" from ${connection.url}…`);
566
- const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
565
+ logger_1.Logger.info(`Fetching ${kind} "${id}" from ${registryUrl}…`);
567
566
  let payload;
568
567
  try {
569
568
  payload = yield client.checkout(kind, id);
570
569
  }
571
570
  catch (error) {
572
571
  if (error instanceof client_1.RegistryClientError) {
573
- throw new CheckoutError(describeFetchFailure(error, kind, id, connection.url));
572
+ throw new CheckoutError(describeFetchFailure(error, kind, id, registryUrl));
574
573
  }
575
574
  throw error;
576
575
  }
@@ -607,4 +606,56 @@ const checkoutEntity = (handoff, kind, id) => __awaiter(void 0, void 0, void 0,
607
606
  logger_1.Logger.success(`Checked out ${kind} "${id}" into ${path_1.default.relative(handoff.workingPath, targetDir) || '.'} ` +
608
607
  `(${payload.files.length} source file(s) + ${declarationFileName}).`);
609
608
  });
609
+ /**
610
+ * Checkout a single component, pattern, or page from the connected registry into this workspace.
611
+ * Overwriting existing local files requires `--force` or an interactive confirmation.
612
+ */
613
+ const checkoutEntity = (handoff, kind, id) => __awaiter(void 0, void 0, void 0, function* () {
614
+ const connection = (0, exports.resolveConnectionOrThrow)(handoff);
615
+ const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
616
+ yield checkoutSingle(handoff, kind, id, client, connection.url);
617
+ });
610
618
  exports.checkoutEntity = checkoutEntity;
619
+ /**
620
+ * Checkout every published component, pattern, or page of the kind into this workspace. Enumerates
621
+ * the registry's published ids, then checks each out through the shared client. A per-entity failure
622
+ * is collected and never aborts the rest; the run throws at the end if any entity failed. Overwrite
623
+ * prompting is per entity (skipped under `--force`).
624
+ */
625
+ const checkoutEntities = (handoff, kind) => __awaiter(void 0, void 0, void 0, function* () {
626
+ const connection = (0, exports.resolveConnectionOrThrow)(handoff);
627
+ const client = (0, client_1.createRegistryClient)({ baseUrl: connection.url, accessToken: connection.accessToken });
628
+ let summaries;
629
+ try {
630
+ summaries = yield client.listEntities(kind);
631
+ }
632
+ catch (error) {
633
+ if (error instanceof client_1.RegistryClientError) {
634
+ throw new CheckoutError(`Could not list ${kind}s from the registry at ${connection.url}: ${error.message}`);
635
+ }
636
+ throw error;
637
+ }
638
+ if (summaries.length === 0) {
639
+ logger_1.Logger.success(`No ${kind}s are published in the registry; nothing to checkout.`);
640
+ return;
641
+ }
642
+ let checkedOut = 0;
643
+ const failed = [];
644
+ for (const { id } of summaries) {
645
+ try {
646
+ yield checkoutSingle(handoff, kind, id, client, connection.url);
647
+ checkedOut += 1;
648
+ }
649
+ catch (error) {
650
+ failed.push({ id, message: error instanceof Error ? error.message : String(error) });
651
+ }
652
+ }
653
+ logger_1.Logger.success(`${kind[0].toUpperCase()}${kind.slice(1)}s checkout complete — ${checkedOut} checked out${failed.length ? `, ${failed.length} failed` : ''}.`);
654
+ if (failed.length > 0) {
655
+ for (const failure of failed) {
656
+ logger_1.Logger.error(` - ${failure.id}: ${failure.message}`);
657
+ }
658
+ throw new CheckoutError(`${failed.length} ${kind}(s) failed to checkout.`);
659
+ }
660
+ });
661
+ exports.checkoutEntities = checkoutEntities;
@@ -7,7 +7,7 @@
7
7
  * surface a clear message instead of a raw fetch failure. Transport is the global `fetch` (Node 18+
8
8
  * and the browser both provide it), so this module stays runtime-agnostic.
9
9
  */
10
- import type { CheckoutPayload, TransferEntityKind, TransferPackage } from './transfer';
10
+ import type { CheckoutPayload, EntitySummary, TransferEntityKind, TransferPackage } from './transfer';
11
11
  import type { TokenSetCheckoutPayload, TokenSetSummary, TokenSetTransferPackage } from './tokens/transfer';
12
12
  import type { AssetCollectionCheckoutPayload, AssetCollectionSummary, AssetCollectionTransferPackage } from './assets/transfer';
13
13
  /** Standard registry response envelope. */
@@ -44,6 +44,7 @@ export interface RegistryClientOptions {
44
44
  export interface RegistryClient {
45
45
  publish(kind: TransferEntityKind, id: string, pkg: TransferPackage): Promise<RegistryEnvelope>;
46
46
  checkout(kind: TransferEntityKind, id: string): Promise<CheckoutPayload>;
47
+ listEntities(kind: TransferEntityKind): Promise<EntitySummary[]>;
47
48
  listTokenSets(): Promise<TokenSetSummary[]>;
48
49
  publishTokens(pkg: TokenSetTransferPackage): Promise<RegistryEnvelope>;
49
50
  checkoutTokens(id: string): Promise<TokenSetCheckoutPayload>;