handoff-app 2.0.0-3 → 2.0.0-5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +302 -378
  4. package/dist/app-builder/build.js +2 -2
  5. package/dist/app-builder/nav-shell.js +8 -15
  6. package/dist/app-builder/vercel-output.js +4 -4
  7. package/dist/cli/make.js +2 -2
  8. package/dist/cli.js +39 -7
  9. package/dist/commands/build/app.js +1 -5
  10. package/dist/commands/build/components.js +1 -5
  11. package/dist/commands/build/index.js +1 -5
  12. package/dist/commands/checkout/index.d.ts +15 -12
  13. package/dist/commands/checkout/index.js +15 -54
  14. package/dist/commands/compat/deprecations.d.ts +46 -0
  15. package/dist/commands/compat/deprecations.js +62 -0
  16. package/dist/commands/compat/push-args.d.ts +48 -0
  17. package/dist/commands/compat/push-args.js +73 -0
  18. package/dist/commands/db/migrate.js +1 -5
  19. package/dist/commands/dev/index.js +1 -5
  20. package/dist/commands/eject/config.js +1 -5
  21. package/dist/commands/eject/pages.js +1 -5
  22. package/dist/commands/eject/theme.js +1 -5
  23. package/dist/commands/fetch/index.js +1 -5
  24. package/dist/commands/index.js +4 -0
  25. package/dist/commands/login/index.js +1 -5
  26. package/dist/commands/logout/index.js +1 -5
  27. package/dist/commands/make/component.js +1 -5
  28. package/dist/commands/make/page.js +3 -7
  29. package/dist/commands/make/template.js +1 -5
  30. package/dist/commands/publish/index.d.ts +18 -12
  31. package/dist/commands/publish/index.js +18 -54
  32. package/dist/commands/pull/index.d.ts +13 -0
  33. package/dist/commands/pull/index.js +20 -0
  34. package/dist/commands/push/index.d.ts +23 -0
  35. package/dist/commands/push/index.js +58 -0
  36. package/dist/commands/scaffold/index.js +1 -5
  37. package/dist/commands/start/index.js +1 -5
  38. package/dist/commands/types.d.ts +10 -0
  39. package/dist/commands/utils.d.ts +74 -6
  40. package/dist/commands/utils.js +89 -10
  41. package/dist/commands/validate/components.js +1 -5
  42. package/dist/config/index.d.ts +2 -1
  43. package/dist/config/index.js +2 -1
  44. package/dist/config/loader.d.ts +19 -6
  45. package/dist/config/loader.js +43 -13
  46. package/dist/config/normalizers/page.d.ts +4 -4
  47. package/dist/config/normalizers/page.js +2 -2
  48. package/dist/config/runtime.js +9 -25
  49. package/dist/index.d.ts +69 -22
  50. package/dist/index.js +148 -35
  51. package/dist/nav/resolver.js +3 -1
  52. package/dist/nav/workspace.js +2 -2
  53. package/dist/registry/auth/access-tokens.d.ts +11 -3
  54. package/dist/registry/auth/access-tokens.js +29 -3
  55. package/dist/registry/auth/index.d.ts +4 -3
  56. package/dist/registry/auth/index.js +10 -2
  57. package/dist/registry/auth/sync-secret.d.ts +27 -0
  58. package/dist/registry/auth/sync-secret.js +65 -0
  59. package/dist/registry/auth/types.d.ts +20 -1
  60. package/dist/registry/auth/types.js +3 -1
  61. package/dist/registry/auth/validation.d.ts +11 -0
  62. package/dist/registry/auth/validation.js +18 -1
  63. package/dist/registry/checkout/assets.d.ts +1 -1
  64. package/dist/registry/checkout/assets.js +21 -6
  65. package/dist/registry/checkout/index.d.ts +8 -1
  66. package/dist/registry/checkout/index.js +39 -6
  67. package/dist/registry/checkout/tokens.d.ts +1 -1
  68. package/dist/registry/checkout/tokens.js +12 -5
  69. package/dist/registry/connection.d.ts +10 -0
  70. package/dist/registry/connection.js +52 -7
  71. package/dist/registry/content-kinds.d.ts +27 -0
  72. package/dist/registry/content-kinds.js +33 -0
  73. package/dist/registry/db/client.d.ts +6 -1
  74. package/dist/registry/db/client.js +16 -3
  75. package/dist/registry/db/schema.d.ts +37 -3
  76. package/dist/registry/db/schema.js +9 -7
  77. package/dist/registry/publish/assets.d.ts +2 -2
  78. package/dist/registry/publish/assets.js +46 -35
  79. package/dist/registry/publish/index.d.ts +29 -5
  80. package/dist/registry/publish/index.js +87 -34
  81. package/dist/registry/publish/tokens.d.ts +2 -2
  82. package/dist/registry/publish/tokens.js +30 -24
  83. package/dist/registry/selection.d.ts +12 -0
  84. package/dist/registry/selection.js +22 -0
  85. package/dist/store/registry.d.ts +48 -0
  86. package/dist/store/registry.js +75 -12
  87. package/dist/transformers/preview/types.d.ts +2 -2
  88. package/dist/utils/markdown.d.ts +10 -0
  89. package/dist/utils/markdown.js +17 -0
  90. package/dist/utils/menu-shell.js +14 -7
  91. package/dist/utils/pages.d.ts +22 -0
  92. package/dist/utils/pages.js +73 -0
  93. package/dist/utils/path.d.ts +5 -0
  94. package/dist/utils/path.js +9 -0
  95. package/docs/api.md +1 -3
  96. package/docs/cli.md +1 -1
  97. package/drizzle/0002_add_pages.sql +5 -3
  98. package/drizzle/meta/0002_snapshot.json +17 -8
  99. package/drizzle/meta/0003_snapshot.json +17 -8
  100. package/drizzle/meta/0004_snapshot.json +17 -8
  101. package/drizzle/meta/0005_snapshot.json +17 -8
  102. package/package.json +1 -1
  103. package/src/app/components/util/index.ts +5 -23
  104. package/src/app/lib/docs-api/backend.ts +11 -0
  105. package/src/app/lib/docs-api/errors.ts +8 -7
  106. package/src/app/lib/docs-api/handler.ts +2 -3
  107. package/src/app/lib/docs-api/index.ts +2 -0
  108. package/src/app/lib/docs-api/page-discovery.ts +126 -0
  109. package/src/app/lib/docs-api/page-rendering.ts +17 -0
  110. package/src/app/lib/docs-api/page-search.ts +191 -0
  111. package/src/app/lib/docs-api/records.ts +11 -19
  112. package/src/app/lib/docs-api/registry.ts +90 -11
  113. package/src/app/lib/docs-api/search.ts +115 -0
  114. package/src/app/lib/registry-api/allowlist.ts +7 -5
  115. package/src/app/lib/registry-api/asset-transfer.ts +58 -31
  116. package/src/app/lib/registry-api/auth.ts +89 -9
  117. package/src/app/lib/registry-api/entity-routes.ts +17 -7
  118. package/src/app/lib/registry-api/errors.ts +10 -4
  119. package/src/app/lib/registry-api/handler.ts +13 -15
  120. package/src/app/lib/registry-api/meta.ts +4 -0
  121. package/src/app/lib/registry-api/revalidate.ts +31 -12
  122. package/src/app/lib/registry-api/store.ts +89 -26
  123. package/src/app/lib/registry-api/sync-compat.ts +166 -0
  124. package/src/app/lib/registry-api/token-transfer.ts +44 -12
  125. package/src/app/lib/registry-api/transfer-batch.ts +116 -0
  126. package/src/app/lib/registry-api/transfer.ts +62 -35
  127. package/src/app/lib/registry-api/validation.ts +33 -1
  128. package/src/app/lib/registry-connection.ts +1 -1
  129. package/src/app/next.config.mjs +6 -0
  130. package/src/app/pages/[...slug]/index.tsx +37 -22
  131. package/src/app/pages/api/account/tokens/index.ts +4 -13
  132. package/src/app/pages/api/docs/search/pages.json.ts +25 -0
  133. package/src/app/pages/api/oauth/revoke.ts +14 -7
  134. package/src/app/pages/api/registry/components/[id]/files/[...filePath].ts +1 -1
  135. package/src/app/pages/api/registry/components/[id].ts +2 -2
  136. package/src/app/pages/api/registry/components/index.ts +1 -1
  137. package/src/app/pages/api/registry/pages/[...id].ts +20 -0
  138. package/src/app/pages/api/registry/pages/index.ts +19 -0
  139. package/src/app/pages/api/registry/patterns/[id]/files/[...filePath].ts +1 -1
  140. package/src/app/pages/api/registry/patterns/[id].ts +2 -2
  141. package/src/app/pages/api/registry/patterns/index.ts +1 -1
  142. package/src/app/pages/api/registry/transfer/assets/blobs/have.ts +2 -2
  143. package/src/app/pages/api/sync/upload.ts +21 -0
  144. package/src/app/pages/index/index.tsx +14 -0
  145. package/src/app/pages/index.tsx +30 -5
  146. package/src/app-builder/build.ts +2 -2
  147. package/src/app-builder/nav-shell.ts +8 -13
  148. package/src/app-builder/vercel-output.ts +4 -4
  149. package/src/cli/make.ts +2 -2
  150. package/src/cli.ts +31 -7
  151. package/src/commands/build/app.ts +2 -3
  152. package/src/commands/build/components.ts +2 -3
  153. package/src/commands/build/index.ts +2 -3
  154. package/src/commands/checkout/index.ts +18 -44
  155. package/src/commands/compat/deprecations.ts +79 -0
  156. package/src/commands/compat/push-args.ts +106 -0
  157. package/src/commands/db/migrate.ts +2 -3
  158. package/src/commands/dev/index.ts +2 -3
  159. package/src/commands/eject/config.ts +2 -3
  160. package/src/commands/eject/pages.ts +2 -3
  161. package/src/commands/eject/theme.ts +2 -3
  162. package/src/commands/fetch/index.ts +2 -3
  163. package/src/commands/index.ts +4 -0
  164. package/src/commands/login/index.ts +2 -3
  165. package/src/commands/logout/index.ts +2 -3
  166. package/src/commands/make/component.ts +2 -3
  167. package/src/commands/make/page.ts +4 -5
  168. package/src/commands/make/template.ts +2 -3
  169. package/src/commands/publish/index.ts +21 -44
  170. package/src/commands/pull/index.ts +32 -0
  171. package/src/commands/push/index.ts +115 -0
  172. package/src/commands/scaffold/index.ts +2 -3
  173. package/src/commands/start/index.ts +2 -3
  174. package/src/commands/types.ts +11 -1
  175. package/src/commands/utils.ts +80 -11
  176. package/src/commands/validate/components.ts +2 -3
  177. package/src/config/index.ts +2 -1
  178. package/src/config/loader.ts +57 -12
  179. package/src/config/normalizers/page.ts +4 -4
  180. package/src/config/runtime.ts +9 -24
  181. package/src/index.ts +183 -42
  182. package/src/nav/resolver.ts +3 -1
  183. package/src/nav/workspace.ts +2 -2
  184. package/src/registry/auth/access-tokens.ts +29 -5
  185. package/src/registry/auth/index.ts +7 -1
  186. package/src/registry/auth/sync-secret.ts +67 -0
  187. package/src/registry/auth/types.ts +24 -1
  188. package/src/registry/auth/validation.ts +18 -0
  189. package/src/registry/checkout/assets.ts +24 -7
  190. package/src/registry/checkout/index.ts +41 -5
  191. package/src/registry/checkout/tokens.ts +12 -6
  192. package/src/registry/connection.ts +53 -5
  193. package/src/registry/content-kinds.ts +39 -0
  194. package/src/registry/db/client.ts +22 -2
  195. package/src/registry/db/schema.ts +9 -7
  196. package/src/registry/publish/assets.ts +47 -37
  197. package/src/registry/publish/index.ts +102 -38
  198. package/src/registry/publish/tokens.ts +35 -26
  199. package/src/registry/selection.ts +19 -0
  200. package/src/store/registry.ts +118 -13
  201. package/src/transformers/preview/types.ts +2 -2
  202. package/src/utils/README.md +1 -0
  203. package/src/utils/markdown.ts +11 -0
  204. package/src/utils/menu-shell.ts +14 -7
  205. package/src/utils/pages.ts +52 -0
  206. package/src/utils/path.ts +15 -6
  207. package/templates/default/package.json.tpl +4 -0
  208. package/config/docs/foundations/logos.md +0 -7
  209. package/src/app/lib/api/redact.ts +0 -46
@@ -1,20 +1,53 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
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',
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
13
  };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.runTarget = exports.withTargetPositionals = exports.getCheckoutOptions = exports.getPublishOptions = exports.getSharedOptions = exports.runRegistryCommand = exports.createHandoff = exports.REGISTRY_TARGET_KINDS = void 0;
16
+ const __1 = __importDefault(require("../"));
17
+ const content_kinds_1 = require("../registry/content-kinds");
18
+ const logger_1 = require("../utils/logger");
19
+ /** The `type` argument accepted by publish/checkout: any content kind, or `all` for every kind. */
20
+ exports.REGISTRY_TARGET_KINDS = [...content_kinds_1.REGISTRY_ENTITY_KINDS, 'all'];
21
+ const createHandoff = (args) => new __1.default({
22
+ debug: args.debug,
23
+ force: args.force,
24
+ configPath: args.config,
25
+ dryRun: args.dryRun,
26
+ // yargs gives `--no-build` as `build: false`; anything else leaves the build in place.
27
+ skipBuild: args.build === false,
28
+ });
29
+ exports.createHandoff = createHandoff;
30
+ /**
31
+ * Run a registry command against a fresh Handoff. Registry failures are the user's to act on, so they
32
+ * surface as one actionable line and a non-zero exit rather than a stack trace.
33
+ */
34
+ const runRegistryCommand = (args, run) => __awaiter(void 0, void 0, void 0, function* () {
35
+ const handoff = (0, exports.createHandoff)(args);
36
+ try {
37
+ yield run(handoff);
38
+ }
39
+ catch (error) {
40
+ logger_1.Logger.error(error instanceof Error ? error.message : String(error));
41
+ process.exitCode = 1;
42
+ }
43
+ });
44
+ exports.runRegistryCommand = runRegistryCommand;
12
45
  const getSharedOptions = (yargs) => {
13
46
  return yargs.options({
14
47
  config: {
15
48
  alias: 'c',
16
49
  type: 'string',
17
- description: 'Path to config file',
50
+ description: 'Path to config file, relative to the working directory',
18
51
  },
19
52
  force: {
20
53
  alias: 'f',
@@ -29,3 +62,49 @@ const getSharedOptions = (yargs) => {
29
62
  });
30
63
  };
31
64
  exports.getSharedOptions = getSharedOptions;
65
+ /** Shared options plus the transfer options: publish and its compatibility aliases use these. */
66
+ const getPublishOptions = (yargs) => (0, exports.getSharedOptions)(yargs).options({
67
+ 'dry-run': {
68
+ type: 'boolean',
69
+ description: 'Report what would be published without contacting the registry (the build still runs; add --no-build to skip it)',
70
+ },
71
+ build: {
72
+ type: 'boolean',
73
+ default: true,
74
+ description: 'Run a fresh build first; use --no-build to publish the existing output',
75
+ },
76
+ });
77
+ exports.getPublishOptions = getPublishOptions;
78
+ /** Shared options plus `--dry-run`: checkout and its compatibility aliases use these. */
79
+ const getCheckoutOptions = (yargs) => (0, exports.getSharedOptions)(yargs).options({
80
+ 'dry-run': {
81
+ type: 'boolean',
82
+ description: 'Report what would be written without changing any workspace file',
83
+ },
84
+ });
85
+ exports.getCheckoutOptions = getCheckoutOptions;
86
+ /**
87
+ * Positional `type` + variadic `id`, shared by publish, checkout, and their compatibility aliases.
88
+ * `choices` is always declared: yargs allows an omitted optional positional while still rejecting an
89
+ * unrecognized one, so the deprecated commands get the same guard as the canonical ones.
90
+ */
91
+ const withTargetPositionals = (yargs, verb) => yargs
92
+ .positional('type', {
93
+ describe: `The kind of content to ${verb}, or "all" for every kind`,
94
+ choices: exports.REGISTRY_TARGET_KINDS,
95
+ type: 'string',
96
+ })
97
+ .positional('id', {
98
+ describe: `The stable id (component/pattern/page id, token set id, or asset collection); omit to ${verb} all of that kind`,
99
+ type: 'string',
100
+ array: true,
101
+ });
102
+ exports.withTargetPositionals = withTargetPositionals;
103
+ /** Dispatch one `type` argument to the matching Handoff entry point. */
104
+ const runTarget = (handoff, type, ids, verb) => {
105
+ if (type === 'all') {
106
+ return verb === 'publish' ? handoff.publishAll() : handoff.checkoutAll();
107
+ }
108
+ return verb === 'publish' ? handoff.publishKind(type, ids) : handoff.checkoutKind(type, ids);
109
+ };
110
+ exports.runTarget = runTarget;
@@ -8,11 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
11
  Object.defineProperty(exports, "__esModule", { value: true });
15
- const __1 = __importDefault(require("../../"));
16
12
  const utils_1 = require("../utils");
17
13
  const command = {
18
14
  command: 'validate:components',
@@ -27,7 +23,7 @@ const command = {
27
23
  },
28
24
  handler: (args) => __awaiter(void 0, void 0, void 0, function* () {
29
25
  var _a;
30
- const handoff = new __1.default(args.debug, args.force);
26
+ const handoff = (0, utils_1.createHandoff)(args);
31
27
  yield handoff.validateComponents((_a = args.skipBuild) !== null && _a !== void 0 ? _a : false);
32
28
  }),
33
29
  };
@@ -1,6 +1,7 @@
1
1
  export { defaultConfig, getClientConfig } from './defaults';
2
2
  export type { ImageStyle } from './defaults';
3
- export { initConfig, initConfigWithMetadata } from './loader';
3
+ export { HandoffConfigError, initConfig, initConfigWithMetadata } from './loader';
4
+ export type { ConfigLoadContext } from './loader';
4
5
  export { defineConfig } from './helpers';
5
6
  export { initRuntimeConfig } from './runtime';
6
7
  export { validateConfig } from './validator';
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.writeEntries = exports.isEntryCovered = exports.validateConfig = exports.initRuntimeConfig = exports.defineConfig = exports.initConfigWithMetadata = exports.initConfig = exports.getClientConfig = exports.defaultConfig = void 0;
3
+ exports.writeEntries = exports.isEntryCovered = exports.validateConfig = exports.initRuntimeConfig = exports.defineConfig = exports.initConfigWithMetadata = exports.initConfig = exports.HandoffConfigError = exports.getClientConfig = exports.defaultConfig = void 0;
4
4
  // Config defaults and client config
5
5
  var defaults_1 = require("./defaults");
6
6
  Object.defineProperty(exports, "defaultConfig", { enumerable: true, get: function () { return defaults_1.defaultConfig; } });
7
7
  Object.defineProperty(exports, "getClientConfig", { enumerable: true, get: function () { return defaults_1.getClientConfig; } });
8
8
  // Config file loading
9
9
  var loader_1 = require("./loader");
10
+ Object.defineProperty(exports, "HandoffConfigError", { enumerable: true, get: function () { return loader_1.HandoffConfigError; } });
10
11
  Object.defineProperty(exports, "initConfig", { enumerable: true, get: function () { return loader_1.initConfig; } });
11
12
  Object.defineProperty(exports, "initConfigWithMetadata", { enumerable: true, get: function () { return loader_1.initConfigWithMetadata; } });
12
13
  // Config helpers
@@ -1,20 +1,33 @@
1
1
  import { Config } from '../types/config';
2
+ /** Raised when an explicitly requested config file cannot be used. */
3
+ export declare class HandoffConfigError extends Error {
4
+ constructor(message: string);
5
+ }
2
6
  type ConfigLoadResult = {
3
7
  config: Config;
4
8
  configPath?: string;
5
9
  };
10
+ /** Where to look for the config: the working directory, plus an optional explicit file. */
11
+ export type ConfigLoadContext = {
12
+ /** Directory relative config paths resolve from, and the root that gets searched. */
13
+ workingPath?: string;
14
+ /** Explicit config file (the CLI's `-c, --config`), replacing discovery entirely. */
15
+ configPath?: string;
16
+ };
6
17
  /**
7
- * Loads the handoff configuration from the project root and returns metadata.
18
+ * Loads the handoff configuration for the given working directory and returns metadata.
8
19
  */
9
- export declare const initConfigWithMetadata: (configOverride?: Partial<Config>) => ConfigLoadResult;
20
+ export declare const initConfigWithMetadata: (configOverride?: Partial<Config>, context?: ConfigLoadContext) => ConfigLoadResult;
10
21
  /**
11
- * Loads the handoff configuration from the project root.
22
+ * Loads the handoff configuration for the given working directory.
12
23
  *
13
- * Searches for config files in order: handoff.config.ts, handoff.config.js, handoff.config.cjs, handoff.config.json.
14
- * Merges file config with any provided overrides, then applies defaults for missing values.
24
+ * Searches for config files in order: handoff.config.ts, handoff.config.js, handoff.config.cjs, handoff.config.json,
25
+ * unless `context.configPath` names one explicitly. Merges file config with any provided overrides,
26
+ * then applies defaults for missing values.
15
27
  *
16
28
  * @param configOverride - Optional partial config to override file-loaded values.
29
+ * @param context - Optional working directory and explicit config file to load.
17
30
  * @returns The fully resolved Config object.
18
31
  */
19
- export declare const initConfig: (configOverride?: Partial<Config>) => Config;
32
+ export declare const initConfig: (configOverride?: Partial<Config>, context?: ConfigLoadContext) => Config;
20
33
  export {};
@@ -3,13 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.initConfig = exports.initConfigWithMetadata = void 0;
6
+ exports.initConfig = exports.initConfigWithMetadata = exports.HandoffConfigError = void 0;
7
7
  const esbuild_1 = __importDefault(require("esbuild"));
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const merge_1 = __importDefault(require("lodash/merge"));
10
10
  const module_1 = require("module");
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const logger_1 = require("../utils/logger");
13
+ const path_2 = require("../utils/path");
13
14
  const defaults_1 = require("./defaults");
14
15
  const CONFIG_FILE_PREFERENCE = [
15
16
  'handoff.config.ts',
@@ -17,6 +18,16 @@ const CONFIG_FILE_PREFERENCE = [
17
18
  'handoff.config.cjs',
18
19
  'handoff.config.json',
19
20
  ];
21
+ /** Extensions the loader knows how to evaluate, derived from the discoverable file names. */
22
+ const SUPPORTED_CONFIG_EXTENSIONS = Array.from(new Set(CONFIG_FILE_PREFERENCE.map((fileName) => path_1.default.extname(fileName))));
23
+ /** Raised when an explicitly requested config file cannot be used. */
24
+ class HandoffConfigError extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = 'HandoffConfigError';
28
+ }
29
+ }
30
+ exports.HandoffConfigError = HandoffConfigError;
20
31
  const evaluateTypeScriptConfig = (filePath, handoffModulePath) => {
21
32
  var _a, _b;
22
33
  const buildResult = esbuild_1.default.buildSync({
@@ -49,11 +60,12 @@ const evaluateTypeScriptConfig = (filePath, handoffModulePath) => {
49
60
  return mod.exports;
50
61
  };
51
62
  const loadConfigFile = (configPath) => {
52
- if (configPath.endsWith('.json')) {
63
+ const extension = path_1.default.extname(configPath);
64
+ if (extension === '.json') {
53
65
  const buffer = fs_extra_1.default.readFileSync(configPath);
54
66
  return JSON.parse(buffer.toString());
55
67
  }
56
- if (configPath.endsWith('.ts')) {
68
+ if (extension === '.ts') {
57
69
  const handoffModulePath = path_1.default.resolve(__dirname, '../..');
58
70
  const importedConfig = evaluateTypeScriptConfig(configPath, handoffModulePath);
59
71
  return (importedConfig.default || importedConfig);
@@ -63,9 +75,24 @@ const loadConfigFile = (configPath) => {
63
75
  const importedConfig = require(configPath);
64
76
  return (importedConfig.default || importedConfig);
65
77
  };
66
- const resolveConfigFilePath = () => {
78
+ /**
79
+ * Picks the config file to load. An explicitly requested file replaces discovery - it is resolved
80
+ * from `workingPath` and must exist in a format the loader can evaluate. Otherwise every known
81
+ * config name is looked for in `workingPath`, the first match winning.
82
+ */
83
+ const resolveConfigFilePath = (workingPath, requestedPath) => {
84
+ if (requestedPath) {
85
+ const selected = path_1.default.resolve(workingPath, requestedPath);
86
+ if (!fs_extra_1.default.existsSync(selected)) {
87
+ throw new HandoffConfigError(`Config file not found: "${requestedPath}" (resolved to ${selected})`);
88
+ }
89
+ if (!SUPPORTED_CONFIG_EXTENSIONS.includes(path_1.default.extname(selected))) {
90
+ throw new HandoffConfigError(`Unsupported config file "${path_1.default.basename(selected)}". Supported extensions: ${SUPPORTED_CONFIG_EXTENSIONS.join(', ')}`);
91
+ }
92
+ return { selected, ignored: [] };
93
+ }
67
94
  const existing = CONFIG_FILE_PREFERENCE
68
- .map((fileName) => path_1.default.resolve(process.cwd(), fileName))
95
+ .map((fileName) => path_1.default.resolve(workingPath, fileName))
69
96
  .filter((filePath) => fs_extra_1.default.existsSync(filePath));
70
97
  if (!existing.length) {
71
98
  return { selected: undefined, ignored: [] };
@@ -74,11 +101,12 @@ const resolveConfigFilePath = () => {
74
101
  return { selected, ignored };
75
102
  };
76
103
  /**
77
- * Loads the handoff configuration from the project root and returns metadata.
104
+ * Loads the handoff configuration for the given working directory and returns metadata.
78
105
  */
79
- const initConfigWithMetadata = (configOverride) => {
106
+ const initConfigWithMetadata = (configOverride, context) => {
107
+ var _a;
80
108
  let config = {};
81
- const { selected: configPath, ignored } = resolveConfigFilePath();
109
+ const { selected: configPath, ignored } = resolveConfigFilePath((_a = context === null || context === void 0 ? void 0 : context.workingPath) !== null && _a !== void 0 ? _a : (0, path_2.resolveWorkingPath)(), context === null || context === void 0 ? void 0 : context.configPath);
82
110
  if (ignored.length > 0 && configPath) {
83
111
  logger_1.Logger.warn(`Multiple config files found. Using "${path_1.default.basename(configPath)}" and ignoring: ${ignored
84
112
  .map((filePath) => `"${path_1.default.basename(filePath)}"`)
@@ -104,15 +132,17 @@ const initConfigWithMetadata = (configOverride) => {
104
132
  };
105
133
  exports.initConfigWithMetadata = initConfigWithMetadata;
106
134
  /**
107
- * Loads the handoff configuration from the project root.
135
+ * Loads the handoff configuration for the given working directory.
108
136
  *
109
- * Searches for config files in order: handoff.config.ts, handoff.config.js, handoff.config.cjs, handoff.config.json.
110
- * Merges file config with any provided overrides, then applies defaults for missing values.
137
+ * Searches for config files in order: handoff.config.ts, handoff.config.js, handoff.config.cjs, handoff.config.json,
138
+ * unless `context.configPath` names one explicitly. Merges file config with any provided overrides,
139
+ * then applies defaults for missing values.
111
140
  *
112
141
  * @param configOverride - Optional partial config to override file-loaded values.
142
+ * @param context - Optional working directory and explicit config file to load.
113
143
  * @returns The fully resolved Config object.
114
144
  */
115
- const initConfig = (configOverride) => {
116
- return (0, exports.initConfigWithMetadata)(configOverride).config;
145
+ const initConfig = (configOverride, context) => {
146
+ return (0, exports.initConfigWithMetadata)(configOverride, context).config;
117
147
  };
118
148
  exports.initConfig = initConfig;
@@ -1,18 +1,18 @@
1
1
  import { PageListObject } from '../../transformers/preview/types';
2
2
  type RawPageFrontmatter = Record<string, any>;
3
3
  type NormalizePageOptions = {
4
- /** The page id — its slug path (e.g. `guides/setup`), derived from the file location. */
4
+ /** The page id derived from its file location (`index` is reserved for the root home page). */
5
5
  id: string;
6
6
  /** Logical route the page is served at (e.g. `/guides/setup`). */
7
7
  routePath: string;
8
8
  /** Absolute filesystem path of the source `.md` (workspace-only, stripped on publish). */
9
- sourcePath: string;
9
+ sourcePath?: string;
10
10
  };
11
11
  /**
12
12
  * Normalize a page's parsed markdown frontmatter into a {@link PageListObject}. Pages have no
13
13
  * required authored fields (a bare `.md` is valid); the title falls back to a humanized last slug
14
- * segment. The id is always the slug path (a page is addressed by its URL, so id ≡ route): the file
15
- * location is the single source of identity for both filesystem and registry serving.
14
+ * segment. The file location is the source of identity for filesystem and registry serving. Normal
15
+ * page ids match their routes; the reserved `index` id is served at `/`.
16
16
  */
17
17
  export declare const normalizePageDeclaration: (raw: RawPageFrontmatter, options: NormalizePageOptions) => PageListObject;
18
18
  export {};
@@ -9,8 +9,8 @@ const humanize = (segment) => segment
9
9
  /**
10
10
  * Normalize a page's parsed markdown frontmatter into a {@link PageListObject}. Pages have no
11
11
  * required authored fields (a bare `.md` is valid); the title falls back to a humanized last slug
12
- * segment. The id is always the slug path (a page is addressed by its URL, so id ≡ route): the file
13
- * location is the single source of identity for both filesystem and registry serving.
12
+ * segment. The file location is the source of identity for filesystem and registry serving. Normal
13
+ * page ids match their routes; the reserved `index` id is served at `/`.
14
14
  */
15
15
  const normalizePageDeclaration = (raw, options) => {
16
16
  var _a;
@@ -6,12 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.toLowerCaseKeysAndValues = exports.getItemsForPath = exports.getComponentsForPath = exports.isComponentDirectory = exports.initRuntimeConfig = exports.resolveComponentDeclaration = void 0;
7
7
  const esbuild_1 = __importDefault(require("esbuild"));
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
- const gray_matter_1 = __importDefault(require("gray-matter"));
10
9
  const module_1 = require("module");
11
10
  const path_1 = __importDefault(require("path"));
11
+ const content_kinds_1 = require("../registry/content-kinds");
12
12
  const csf_1 = require("../transformers/utils/csf");
13
13
  const module_2 = require("../transformers/utils/module");
14
14
  const logger_1 = require("../utils/logger");
15
+ const markdown_1 = require("../utils/markdown");
16
+ const pages_1 = require("../utils/pages");
15
17
  const path_2 = require("../utils/path");
16
18
  const declaration_1 = require("./normalizers/declaration");
17
19
  const page_1 = require("./normalizers/page");
@@ -240,15 +242,16 @@ const initRuntimeConfig = (handoff) => {
240
242
  // Discover markdown pages
241
243
  // -------------------------------------------------------------------------
242
244
  // Working pages under `<workingPath>/pages/` are first-class registry entities (publish/checkout).
243
- // Discovery mirrors the catch-all/static-path walk: every `.md` except `index.md` becomes a page
244
- // keyed by its slug path. Package `config/docs` defaults are NOT entities (they ship with the tool).
245
+ // The root `index.md` uses the stable id `index` but is served at `/`; nested index files remain
246
+ // excluded because section indexes are owned by dedicated routes. Package defaults are not entities.
245
247
  const pagesRoot = path_1.default.resolve(handoff.workingPath, 'pages');
246
- for (const segments of collectPageMarkdownPaths(pagesRoot)) {
248
+ for (const segments of (0, pages_1.collectPageSlugSegments)(pagesRoot, { includeRootIndex: true })) {
247
249
  const slug = segments.join('/');
248
250
  const sourcePath = path_1.default.resolve(pagesRoot, `${slug}.md`);
249
251
  try {
250
- const { data: frontmatter } = (0, gray_matter_1.default)(fs_extra_1.default.readFileSync(sourcePath, 'utf-8'));
251
- const page = (0, page_1.normalizePageDeclaration)(frontmatter, { id: slug, routePath: `/${slug}`, sourcePath });
252
+ const { data: frontmatter } = (0, markdown_1.parseMarkdown)(fs_extra_1.default.readFileSync(sourcePath, 'utf-8'));
253
+ const routePath = slug === content_kinds_1.HOME_PAGE_ID ? content_kinds_1.HOME_PAGE_PATH : `/${slug}`;
254
+ const page = (0, page_1.normalizePageDeclaration)(frontmatter, { id: slug, routePath, sourcePath });
252
255
  result.entries.pages[page.id] = page;
253
256
  }
254
257
  catch (err) {
@@ -263,25 +266,6 @@ const initRuntimeConfig = (handoff) => {
263
266
  return [result, Array.from(configFiles), configFileIndex];
264
267
  };
265
268
  exports.initRuntimeConfig = initRuntimeConfig;
266
- /**
267
- * Recursively collect `.md` files under a root, returning slug segments (relative path without the
268
- * `.md`). `index.md` is excluded — section index content is served by dedicated/home routes.
269
- */
270
- const collectPageMarkdownPaths = (rootDir, relativeParts = []) => {
271
- if (!fs_extra_1.default.existsSync(rootDir))
272
- return [];
273
- const results = [];
274
- for (const entry of fs_extra_1.default.readdirSync(rootDir)) {
275
- const fullPath = path_1.default.join(rootDir, entry);
276
- if (fs_extra_1.default.lstatSync(fullPath).isDirectory()) {
277
- results.push(...collectPageMarkdownPaths(fullPath, [...relativeParts, entry]));
278
- }
279
- else if (entry.endsWith('.md') && entry !== 'index.md') {
280
- results.push([...relativeParts, entry.replace(/\.md$/, '')]);
281
- }
282
- }
283
- return results;
284
- };
285
269
  /**
286
270
  * True when `searchPath` is itself a single component directory: it holds a declaration file named
287
271
  * after the directory (`{dirname}.handoff.*`, or a legacy `{dirname}.{json,js,cjs}`). This is what
package/dist/index.d.ts CHANGED
@@ -1,12 +1,41 @@
1
1
  import 'dotenv/config';
2
2
  import { Types as CoreTypes, Handoff as HandoffRunner } from 'handoff-core';
3
3
  import { type BuildPackage, type BuildTarget } from './app-builder';
4
+ import { type RegistryEntityKind } from './registry/content-kinds';
5
+ import type { TransferEntityKind } from './registry/transfer';
4
6
  import { type HandoffStore } from './store';
5
7
  import { Config, ConfigFileEntry, RuntimeConfig } from './types/config';
8
+ /** How a {@link Handoff} instance is set up. */
9
+ export interface HandoffOptions {
10
+ debug?: boolean;
11
+ force?: boolean;
12
+ /**
13
+ * Report what would happen instead of doing it: publish uploads nothing and needs no registry URL
14
+ * or token, and checkout writes no workspace file. A publish dry run still runs the build, so build
15
+ * output on disk is refreshed; pair it with `skipBuild` to leave that alone too.
16
+ */
17
+ dryRun?: boolean;
18
+ /** Publish from the existing build output instead of running a fresh build. */
19
+ skipBuild?: boolean;
20
+ /** Partial config merged over the loaded config file. */
21
+ config?: Partial<Config>;
22
+ /** Explicit config file (the CLI's `-c, --config`), resolved from the working path. */
23
+ configPath?: string;
24
+ }
25
+ /**
26
+ * A content kind to act on, optionally narrowed to specific ids. The plain form is the common case
27
+ * (`'components'`); the object form publishes or checks out only part of a kind.
28
+ */
29
+ export type ContentTarget = RegistryEntityKind | {
30
+ kind: RegistryEntityKind;
31
+ ids?: string[];
32
+ };
6
33
  declare class Handoff {
7
34
  config: Config | null;
8
35
  debug: boolean;
9
36
  force: boolean;
37
+ dryRun: boolean;
38
+ skipBuild: boolean;
10
39
  modulePath: string;
11
40
  workingPath: string;
12
41
  exportsDirectory: string;
@@ -23,13 +52,13 @@ declare class Handoff {
23
52
  * and assets. It is created lazily and reads live from `runtimeConfig`.
24
53
  */
25
54
  get store(): HandoffStore;
26
- private _initialArgs;
55
+ private _options;
27
56
  private _configFilePaths;
28
57
  private _configFileIndex;
29
58
  private _mainConfigFilePath?;
30
59
  private _documentationObjectCache?;
31
60
  private _handoffRunner?;
32
- constructor(debug?: boolean, force?: boolean, config?: Partial<Config>);
61
+ constructor(options?: HandoffOptions);
33
62
  private construct;
34
63
  init(configOverride?: Partial<Config>): Handoff;
35
64
  reload(): Handoff;
@@ -46,50 +75,68 @@ declare class Handoff {
46
75
  dbMigrate(): Promise<Handoff>;
47
76
  /**
48
77
  * 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.
78
+ * registry. With a single `id` it publishes that one entity after a targeted build; with a list of
79
+ * ids, or none, it builds the kind once and publishes the selected (or every declared) entity,
80
+ * skipping ones whose content is unchanged on the registry (`--force` re-uploads all). Pages upload
81
+ * their record and markdown source because they render at runtime. The publish module is loaded
82
+ * lazily so the registry client and build code never enter the docs app bundle.
54
83
  */
55
- publish(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff>;
84
+ publish(kind: TransferEntityKind, id?: string | string[]): Promise<Handoff>;
56
85
  /**
57
86
  * 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.
87
+ * With a single `id` it checks out that one entity; with a list of ids, or none, it checks out the
88
+ * selected (or every published) entity of the kind. Overwriting existing local files requires
89
+ * `--force` or an interactive confirmation. The checkout module is loaded lazily so the registry
90
+ * client never enters the docs app bundle.
61
91
  */
62
- checkout(kind: 'component' | 'pattern' | 'page', id?: string): Promise<Handoff>;
92
+ checkout(kind: TransferEntityKind, id?: string | string[]): Promise<Handoff>;
63
93
  /**
64
94
  * Publish design token sets from this connected workspace to the configured remote registry. Runs a
65
95
  * fresh token build (Figma extract + style transformers), discovers the logical sets, and uploads
66
96
  * each changed set (its extracted record + generated artifacts). Publishes every set when `setId` is
67
- * omitted, or only the named set (`foundation/colors`, `component/<id>`). Loaded lazily so the
97
+ * omitted, or only the named set(s) (`foundation/colors`, `component/<id>`). Loaded lazily so the
68
98
  * registry client/build code never enters the docs app bundle.
69
99
  */
70
- publishTokens(setId?: string): Promise<Handoff>;
100
+ publishTokens(setId?: string | string[]): Promise<Handoff>;
71
101
  /**
72
102
  * Checkout design token sets from the connected remote registry into this workspace: reconstruct
73
103
  * the canonical local `tokens.json` and restore the generated token files to their configured output
74
- * paths. Checks out every published set when `setId` is omitted, or only the named set. Loaded lazily
75
- * so the registry client never enters the docs app bundle.
104
+ * paths. Checks out every published set when `setId` is omitted, or only the named set(s). Loaded
105
+ * lazily so the registry client never enters the docs app bundle.
76
106
  */
77
- checkoutTokens(setId?: string): Promise<Handoff>;
107
+ checkoutTokens(setId?: string | string[]): Promise<Handoff>;
78
108
  /**
79
109
  * Publish asset collections (icons/logos/fonts) from this connected workspace to the configured
80
110
  * remote registry. Runs a fresh build (Figma extract + asset generation), discovers the collections,
81
111
  * uploads only content hashes the registry lacks, and finalizes each collection manifest atomically.
82
- * Publishes every collection when `collection` is omitted, or only the named one. Loaded lazily so
112
+ * Publishes every collection when `collection` is omitted, or only the named one(s). Loaded lazily so
83
113
  * the registry client/build code never enters the docs app bundle.
84
114
  */
85
- publishAssets(collection?: string): Promise<Handoff>;
115
+ publishAssets(collection?: string | string[]): Promise<Handoff>;
86
116
  /**
87
117
  * Checkout asset collections from the connected remote registry into this workspace: recreate the
88
118
  * standard workspace asset files, collection JSON, icon sprite/manifest, and downloadable archives.
89
- * Checks out every published collection when `collection` is omitted, or only the named one. Loaded
90
- * lazily so the registry client never enters the docs app bundle.
119
+ * Checks out every published collection when `collection` is omitted, or only the named one(s).
120
+ * Loaded lazily so the registry client never enters the docs app bundle.
121
+ */
122
+ checkoutAssets(collection?: string | string[]): Promise<Handoff>;
123
+ /**
124
+ * Publish one content kind named by its plural form (`components`, `tokens`, …). The entry point for
125
+ * callers working in the plural vocabulary, so the plural-to-transfer-path mapping lives in one
126
+ * place.
127
+ */
128
+ publishKind(kind: RegistryEntityKind, id?: string | string[]): Promise<Handoff>;
129
+ /** Checkout one content kind named by its plural form. Mirrors {@link publishKind}. */
130
+ checkoutKind(kind: RegistryEntityKind, id?: string | string[]): Promise<Handoff>;
131
+ /**
132
+ * Publish every content kind, or the given subset of kinds and ids, in dependency order: tokens and
133
+ * assets first (rendered component artifacts reference the CSS variables and asset URLs they
134
+ * produce), then components, patterns and pages. A failing kind is reported and never aborts the
135
+ * rest; the run throws at the end if any kind failed.
91
136
  */
92
- checkoutAssets(collection?: string): Promise<Handoff>;
137
+ publishAll(targets?: readonly ContentTarget[]): Promise<Handoff>;
138
+ /** Checkout every content kind, or the given subset, in the same order. Mirrors {@link publishAll}. */
139
+ checkoutAll(targets?: readonly ContentTarget[]): Promise<Handoff>;
93
140
  ejectConfig(): Promise<Handoff>;
94
141
  ejectPages(): Promise<Handoff>;
95
142
  ejectTheme(): Promise<Handoff>;