handoff-app 2.0.0-1 → 2.0.0-3

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 (219) hide show
  1. package/README.md +170 -91
  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 +109 -49
  15. package/dist/app-builder/client-config.js +13 -12
  16. package/dist/app-builder/paths.js +7 -9
  17. package/dist/app-builder/static-export.js +6 -6
  18. package/dist/app-builder/vercel-output.js +2 -2
  19. package/dist/artifacts/url.d.ts +7 -7
  20. package/dist/artifacts/url.js +7 -7
  21. package/dist/cli/auth/browser.d.ts +2 -0
  22. package/dist/cli/auth/browser.js +33 -0
  23. package/dist/cli/auth/device.d.ts +11 -0
  24. package/dist/cli/auth/device.js +155 -0
  25. package/dist/cli/auth/store.d.ts +29 -0
  26. package/dist/cli/auth/store.js +146 -0
  27. package/dist/cli/create.js +48 -3
  28. package/dist/cli/scaffold.js +20 -154
  29. package/dist/commands/checkout/index.d.ts +6 -6
  30. package/dist/commands/checkout/index.js +8 -12
  31. package/dist/commands/index.js +4 -0
  32. package/dist/commands/login/index.d.ts +8 -0
  33. package/dist/commands/login/index.js +53 -0
  34. package/dist/commands/logout/index.d.ts +7 -0
  35. package/dist/commands/logout/index.js +54 -0
  36. package/dist/commands/publish/index.d.ts +6 -5
  37. package/dist/commands/publish/index.js +8 -11
  38. package/dist/commands/utils.d.ts +6 -0
  39. package/dist/commands/utils.js +9 -1
  40. package/dist/config/entries.d.ts +47 -0
  41. package/dist/config/entries.js +162 -0
  42. package/dist/config/index.d.ts +2 -0
  43. package/dist/config/index.js +5 -1
  44. package/dist/config/runtime.d.ts +9 -9
  45. package/dist/config/runtime.js +15 -15
  46. package/dist/index.d.ts +12 -8
  47. package/dist/index.js +26 -10
  48. package/dist/registry/asset-storage/resolve.d.ts +1 -1
  49. package/dist/registry/asset-storage/resolve.js +1 -1
  50. package/dist/registry/auth/access-tokens.d.ts +46 -0
  51. package/dist/registry/auth/access-tokens.js +165 -0
  52. package/dist/registry/auth/action-tokens.d.ts +66 -0
  53. package/dist/registry/auth/action-tokens.js +178 -0
  54. package/dist/registry/auth/crypto.d.ts +4 -0
  55. package/dist/registry/auth/crypto.js +16 -0
  56. package/dist/registry/auth/database.d.ts +7 -0
  57. package/dist/registry/auth/database.js +22 -0
  58. package/dist/registry/auth/device-authorizations.d.ts +51 -0
  59. package/dist/registry/auth/device-authorizations.js +170 -0
  60. package/dist/registry/auth/gravatar.d.ts +1 -0
  61. package/dist/registry/auth/gravatar.js +8 -0
  62. package/dist/registry/auth/index.d.ts +11 -0
  63. package/dist/registry/auth/index.js +72 -0
  64. package/dist/registry/auth/installation.d.ts +42 -0
  65. package/dist/registry/auth/installation.js +105 -0
  66. package/dist/registry/auth/password.d.ts +12 -0
  67. package/dist/registry/auth/password.js +58 -0
  68. package/dist/registry/auth/rate-limits.d.ts +22 -0
  69. package/dist/registry/auth/rate-limits.js +75 -0
  70. package/dist/registry/auth/types.d.ts +26 -0
  71. package/dist/registry/auth/types.js +9 -0
  72. package/dist/registry/auth/user-record.d.ts +13 -0
  73. package/dist/registry/auth/user-record.js +16 -0
  74. package/dist/registry/auth/users.d.ts +31 -0
  75. package/dist/registry/auth/users.js +135 -0
  76. package/dist/registry/auth/validation.d.ts +3 -0
  77. package/dist/registry/auth/validation.js +28 -0
  78. package/dist/registry/checkout/assets.js +4 -2
  79. package/dist/registry/checkout/index.d.ts +11 -6
  80. package/dist/registry/checkout/index.js +159 -30
  81. package/dist/registry/checkout/tokens.js +4 -2
  82. package/dist/registry/client.d.ts +2 -1
  83. package/dist/registry/client.js +8 -0
  84. package/dist/registry/connection.d.ts +9 -0
  85. package/dist/registry/connection.js +29 -1
  86. package/dist/registry/db/client.d.ts +9 -9
  87. package/dist/registry/db/client.js +7 -7
  88. package/dist/registry/db/driver.d.ts +46 -0
  89. package/dist/registry/db/{adapter.js → driver.js} +22 -19
  90. package/dist/registry/db/index.d.ts +2 -2
  91. package/dist/registry/db/index.js +15 -9
  92. package/dist/registry/db/migrate.d.ts +1 -1
  93. package/dist/registry/db/migrate.js +5 -5
  94. package/dist/registry/db/schema.d.ts +2521 -750
  95. package/dist/registry/db/schema.js +126 -1
  96. package/dist/registry/publish/assets.js +1 -1
  97. package/dist/registry/publish/errors.js +3 -3
  98. package/dist/registry/publish/index.d.ts +9 -2
  99. package/dist/registry/publish/index.js +96 -10
  100. package/dist/registry/publish/tokens.js +1 -1
  101. package/dist/registry/transfer.d.ts +13 -0
  102. package/dist/types/config.d.ts +11 -4
  103. package/docs/cli.md +4 -0
  104. package/drizzle/0005_add_registry_auth.sql +95 -0
  105. package/drizzle/meta/0005_snapshot.json +1926 -0
  106. package/drizzle/meta/_journal.json +8 -1
  107. package/drizzle.config.ts +1 -1
  108. package/package.json +5 -4
  109. package/src/app/components/Auth/AccountLayout.tsx +87 -0
  110. package/src/app/components/Auth/AuthControls.tsx +99 -0
  111. package/src/app/components/Auth/AuthShell.tsx +73 -0
  112. package/src/app/components/Auth/RegistryInstallGate.tsx +23 -0
  113. package/src/app/components/Auth/RegistrySessionProvider.tsx +15 -0
  114. package/src/app/components/Auth/api.ts +20 -0
  115. package/src/app/components/Foundations/DisplayLogo.tsx +3 -3
  116. package/src/app/components/Layout/Header.tsx +2 -0
  117. package/src/app/components/Navigation/MobileNav.tsx +2 -0
  118. package/src/app/lib/auth/api.ts +113 -0
  119. package/src/app/lib/auth/config.ts +175 -0
  120. package/src/app/lib/auth/email.ts +34 -0
  121. package/src/app/lib/docs-api/runtime-config.ts +10 -24
  122. package/src/app/lib/registry-api/auth.ts +29 -46
  123. package/src/app/lib/registry-api/errors.ts +2 -2
  124. package/src/app/lib/registry-api/handler.ts +19 -14
  125. package/src/app/lib/registry-api/token-transfer.ts +1 -1
  126. package/src/app/lib/registry-api/transfer.ts +31 -2
  127. package/src/app/lib/registry-connection.ts +2 -2
  128. package/src/app/next.config.mjs +13 -4
  129. package/src/app/pages/_app.tsx +10 -4
  130. package/src/app/pages/account/index.tsx +153 -0
  131. package/src/app/pages/account/tokens.tsx +230 -0
  132. package/src/app/pages/account/users.tsx +310 -0
  133. package/src/app/pages/api/account/index.ts +26 -0
  134. package/src/app/pages/api/account/tokens/[id].ts +17 -0
  135. package/src/app/pages/api/account/tokens/index.ts +44 -0
  136. package/src/app/pages/api/admin/users/[id]/[action].ts +89 -0
  137. package/src/app/pages/api/admin/users/index.ts +53 -0
  138. package/src/app/pages/api/auth/[...nextauth].ts +4 -0
  139. package/src/app/pages/api/auth/request-reset.ts +53 -0
  140. package/src/app/pages/api/auth/reset-password.ts +29 -0
  141. package/src/app/pages/api/install.ts +102 -0
  142. package/src/app/pages/api/oauth/device/approve.ts +23 -0
  143. package/src/app/pages/api/oauth/device/index.ts +41 -0
  144. package/src/app/pages/api/oauth/revoke.ts +24 -0
  145. package/src/app/pages/api/oauth/token.ts +31 -0
  146. package/src/app/pages/api/registry/health.ts +4 -3
  147. package/src/app/pages/api/registry/transfer/[kind]/[id].ts +3 -3
  148. package/src/app/pages/api/registry/transfer/[kind]/index.ts +21 -0
  149. package/src/app/pages/api/registry/transfer/assets/[collection].ts +2 -1
  150. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +2 -1
  151. package/src/app/pages/api/registry/transfer/tokens/[...setId].ts +2 -2
  152. package/src/app/pages/cli/device.tsx +143 -0
  153. package/src/app/pages/foundations/icons/index.tsx +2 -2
  154. package/src/app/pages/foundations/icons/{[name] → library/[name]}/index.tsx +6 -6
  155. package/src/app/pages/install.tsx +336 -0
  156. package/src/app/pages/login.tsx +117 -0
  157. package/src/app/pages/reset-password.tsx +159 -0
  158. package/src/app/types/next-auth.d.ts +27 -0
  159. package/src/app-builder/build.ts +96 -59
  160. package/src/app-builder/client-config.ts +12 -11
  161. package/src/app-builder/paths.ts +7 -9
  162. package/src/app-builder/static-export.ts +6 -6
  163. package/src/app-builder/vercel-output.ts +2 -2
  164. package/src/artifacts/url.ts +7 -7
  165. package/src/cli/auth/browser.ts +29 -0
  166. package/src/cli/auth/device.ts +181 -0
  167. package/src/cli/auth/store.ts +137 -0
  168. package/src/cli/create.ts +58 -3
  169. package/src/cli/scaffold.ts +20 -197
  170. package/src/commands/checkout/index.ts +10 -15
  171. package/src/commands/index.ts +4 -0
  172. package/src/commands/login/index.ts +48 -0
  173. package/src/commands/logout/index.ts +51 -0
  174. package/src/commands/publish/index.ts +10 -14
  175. package/src/commands/utils.ts +12 -0
  176. package/src/config/entries.ts +175 -0
  177. package/src/config/index.ts +4 -0
  178. package/src/config/runtime.ts +15 -14
  179. package/src/index.ts +26 -12
  180. package/src/registry/asset-storage/resolve.ts +1 -1
  181. package/src/registry/auth/access-tokens.ts +198 -0
  182. package/src/registry/auth/action-tokens.ts +225 -0
  183. package/src/registry/auth/crypto.ts +13 -0
  184. package/src/registry/auth/database.ts +15 -0
  185. package/src/registry/auth/device-authorizations.ts +223 -0
  186. package/src/registry/auth/gravatar.ts +6 -0
  187. package/src/registry/auth/index.ts +89 -0
  188. package/src/registry/auth/installation.ts +117 -0
  189. package/src/registry/auth/password.ts +46 -0
  190. package/src/registry/auth/rate-limits.ts +84 -0
  191. package/src/registry/auth/types.ts +34 -0
  192. package/src/registry/auth/user-record.ts +25 -0
  193. package/src/registry/auth/users.ts +145 -0
  194. package/src/registry/auth/validation.ts +24 -0
  195. package/src/registry/checkout/assets.ts +4 -2
  196. package/src/registry/checkout/index.ts +182 -30
  197. package/src/registry/checkout/tokens.ts +4 -2
  198. package/src/registry/client.ts +7 -1
  199. package/src/registry/connection.ts +23 -0
  200. package/src/registry/db/client.ts +12 -12
  201. package/src/registry/db/{adapter.ts → driver.ts} +24 -21
  202. package/src/registry/db/index.ts +16 -4
  203. package/src/registry/db/migrate.ts +5 -5
  204. package/src/registry/db/schema.ts +174 -1
  205. package/src/registry/publish/assets.ts +1 -1
  206. package/src/registry/publish/errors.ts +3 -3
  207. package/src/registry/publish/index.ts +108 -10
  208. package/src/registry/publish/tokens.ts +1 -1
  209. package/src/registry/transfer.ts +14 -0
  210. package/src/types/config.ts +11 -4
  211. package/templates/default/README.md.tpl +58 -1
  212. package/templates/default/gitignore.tpl +19 -3
  213. package/templates/default/handoff.config.js.tpl +13 -0
  214. package/templates/default/handoff.config.ts.tpl +13 -0
  215. package/templates/default/handoff.config.with-examples.js.tpl +13 -0
  216. package/templates/default/handoff.config.with-examples.ts.tpl +13 -0
  217. package/templates/default/package.json.tpl +2 -1
  218. package/templates/default/vercel.json.tpl +4 -0
  219. package/dist/registry/db/adapter.d.ts +0 -43
@@ -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));
@@ -15,6 +15,8 @@ const pages_1 = __importDefault(require("./eject/pages"));
15
15
  const theme_1 = __importDefault(require("./eject/theme"));
16
16
  const fetch_1 = __importDefault(require("./fetch"));
17
17
  const init_1 = __importDefault(require("./init"));
18
+ const login_1 = __importDefault(require("./login"));
19
+ const logout_1 = __importDefault(require("./logout"));
18
20
  const component_1 = __importDefault(require("./make/component"));
19
21
  const page_1 = __importDefault(require("./make/page"));
20
22
  const template_1 = __importDefault(require("./make/template"));
@@ -34,6 +36,8 @@ exports.commands = [
34
36
  theme_1.default,
35
37
  fetch_1.default,
36
38
  init_1.default,
39
+ login_1.default,
40
+ logout_1.default,
37
41
  page_1.default,
38
42
  component_1.default,
39
43
  template_1.default,
@@ -0,0 +1,8 @@
1
+ import { CommandModule } from 'yargs';
2
+ import { SharedArgs } from '../types';
3
+ export interface LoginArgs extends SharedArgs {
4
+ url?: string;
5
+ browser?: boolean;
6
+ }
7
+ declare const command: CommandModule<{}, LoginArgs>;
8
+ export default command;
@@ -0,0 +1,53 @@
1
+ "use strict";
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 };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const __1 = __importDefault(require("../../"));
16
+ const device_1 = require("../../cli/auth/device");
17
+ const store_1 = require("../../cli/auth/store");
18
+ const connection_1 = require("../../registry/connection");
19
+ const logger_1 = require("../../utils/logger");
20
+ const utils_1 = require("../utils");
21
+ const command = {
22
+ command: 'login',
23
+ describe: 'Sign in to a registry using browser device authorization',
24
+ builder: (yargs) => (0, utils_1.getSharedOptions)(yargs)
25
+ .option('url', {
26
+ type: 'string',
27
+ describe: 'Registry URL, including its base path when configured',
28
+ })
29
+ .option('browser', {
30
+ type: 'boolean',
31
+ default: true,
32
+ describe: 'Open the approval URL in the default browser (disable with --no-browser)',
33
+ }),
34
+ handler: (args) => __awaiter(void 0, void 0, void 0, function* () {
35
+ var _a;
36
+ const handoff = new __1.default(args.debug, args.force);
37
+ try {
38
+ const configured = (0, connection_1.resolveRegistryConnection)(handoff.config);
39
+ const remoteUrl = ((_a = args.url) === null || _a === void 0 ? void 0 : _a.trim()) || configured.url;
40
+ if (!remoteUrl) {
41
+ throw new Error(`Pass --url <registry-url>, configure runtime.registryConnection.url, or set "${configured.urlEnv}".`);
42
+ }
43
+ yield (0, device_1.loginWithDevice)(handoff.workingPath, remoteUrl, { openBrowser: args.browser !== false });
44
+ logger_1.Logger.success(`Logged in. Credentials saved to ${(0, store_1.cliAuthFilePath)(handoff.workingPath)}.`);
45
+ logger_1.Logger.info('Publish and checkout commands will use this login for the matching registry URL.');
46
+ }
47
+ catch (error) {
48
+ logger_1.Logger.error(error instanceof Error ? error.message : String(error));
49
+ process.exitCode = 1;
50
+ }
51
+ }),
52
+ };
53
+ exports.default = command;
@@ -0,0 +1,7 @@
1
+ import { CommandModule } from 'yargs';
2
+ import { SharedArgs } from '../types';
3
+ export interface LogoutArgs extends SharedArgs {
4
+ url?: string;
5
+ }
6
+ declare const command: CommandModule<{}, LogoutArgs>;
7
+ export default command;
@@ -0,0 +1,54 @@
1
+ "use strict";
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 };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const __1 = __importDefault(require("../../"));
16
+ const device_1 = require("../../cli/auth/device");
17
+ const store_1 = require("../../cli/auth/store");
18
+ const logger_1 = require("../../utils/logger");
19
+ const utils_1 = require("../utils");
20
+ const command = {
21
+ command: 'logout',
22
+ describe: 'Revoke and remove saved registry credentials',
23
+ builder: (yargs) => (0, utils_1.getSharedOptions)(yargs).option('url', {
24
+ type: 'string',
25
+ describe: 'Revoke credentials only when they belong to this exact registry URL',
26
+ }),
27
+ handler: (args) => __awaiter(void 0, void 0, void 0, function* () {
28
+ const handoff = new __1.default(args.debug, args.force);
29
+ const auth = yield (0, store_1.readCliAuth)(handoff.workingPath);
30
+ if (args.url && auth && !(0, store_1.cliAuthMatchesRegistry)(auth, args.url)) {
31
+ logger_1.Logger.error(`The saved login belongs to ${auth.remoteUrl}, not ${(0, store_1.normalizeRegistryUrl)(args.url)}. No credentials were changed.`);
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ if (auth) {
36
+ try {
37
+ yield (0, device_1.revokeAccessToken)(auth);
38
+ }
39
+ catch (error) {
40
+ logger_1.Logger.warn(`Could not revoke the registry token remotely (${error instanceof Error ? error.message : String(error)}). ` +
41
+ 'It will still be removed from this workspace.');
42
+ }
43
+ }
44
+ try {
45
+ yield (0, store_1.clearCliAuth)(handoff.workingPath);
46
+ logger_1.Logger.success(`Logged out. Removed ${(0, store_1.cliAuthFilePath)(handoff.workingPath)}.`);
47
+ }
48
+ catch (error) {
49
+ logger_1.Logger.error(`Could not remove saved credentials: ${error instanceof Error ? error.message : String(error)}`);
50
+ process.exitCode = 1;
51
+ }
52
+ }),
53
+ };
54
+ exports.default = command;
@@ -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: {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Registering directory-list entries (`entries.components` / `entries.patterns`) in the handoff
3
+ * config. Both scaffolding new component stubs and checking entities out of a registry need a
4
+ * freshly-written entity to be discoverable by the workspace build, so both have to handle every
5
+ * way a path can be declared:
6
+ *
7
+ * - a collection directory (e.g. `"components"`) whose subdirectories are entities: a new sibling
8
+ * is auto-discovered at runtime, so nothing needs writing to the config;
9
+ * - an individual entity directory (e.g. `"components/button"`): the entity stays invisible until
10
+ * its own path is added to the list.
11
+ *
12
+ * {@link isEntryCovered} answers "does this already load?" by reusing the runtime expansion
13
+ * ({@link getComponentsForPath}); {@link writeEntries} appends the paths that don't, across the
14
+ * `.json`, `.ts`, `.js`, and `.cjs` config formats.
15
+ */
16
+ import { Config } from '../types/config';
17
+ /** The `entries` keys whose values are declared as a list of directory paths. */
18
+ export type EntryKind = 'components' | 'patterns';
19
+ /** Minimal handoff shape needed here; avoids importing the full Handoff class (circular dep). */
20
+ interface ConfigContext {
21
+ config?: Config;
22
+ workingPath: string;
23
+ }
24
+ /** Outcome of {@link writeEntries}: `added` on success, `unsupported` when the config couldn't be edited. */
25
+ export interface WriteEntriesResult {
26
+ status: 'added' | 'unsupported';
27
+ /** The config file written (or that would need editing); null only when none exists and creation failed. */
28
+ configPath: string | null;
29
+ /** Workspace-relative POSIX paths written into the config. */
30
+ added: string[];
31
+ /** Workspace-relative POSIX paths the caller must add manually (`status: 'unsupported'`). */
32
+ pending: string[];
33
+ }
34
+ /**
35
+ * True when `targetDir` already loads through an existing `entries[kind]` declaration: listed
36
+ * directly, or sitting under a declared collection directory that runtime discovery expands.
37
+ * Reuses the same expansion the runtime uses, so every declaration style is covered.
38
+ */
39
+ export declare const isEntryCovered: (handoff: ConfigContext, kind: EntryKind, targetDir: string) => boolean;
40
+ /**
41
+ * Add `targetDirs` to `entries[kind]` in the workspace config so the build discovers them. Callers
42
+ * should first drop already-loading dirs via {@link isEntryCovered}. Paths are stored workspace-
43
+ * relative; a `.json` config is edited losslessly, a code config best-effort. When no config file
44
+ * exists a minimal `handoff.config.json` is created.
45
+ */
46
+ export declare const writeEntries: (handoff: ConfigContext, kind: EntryKind, targetDirs: string[]) => Promise<WriteEntriesResult>;
47
+ export {};
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * Registering directory-list entries (`entries.components` / `entries.patterns`) in the handoff
4
+ * config. Both scaffolding new component stubs and checking entities out of a registry need a
5
+ * freshly-written entity to be discoverable by the workspace build, so both have to handle every
6
+ * way a path can be declared:
7
+ *
8
+ * - a collection directory (e.g. `"components"`) whose subdirectories are entities: a new sibling
9
+ * is auto-discovered at runtime, so nothing needs writing to the config;
10
+ * - an individual entity directory (e.g. `"components/button"`): the entity stays invisible until
11
+ * its own path is added to the list.
12
+ *
13
+ * {@link isEntryCovered} answers "does this already load?" by reusing the runtime expansion
14
+ * ({@link getComponentsForPath}); {@link writeEntries} appends the paths that don't, across the
15
+ * `.json`, `.ts`, `.js`, and `.cjs` config formats.
16
+ */
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.writeEntries = exports.isEntryCovered = void 0;
31
+ const fs_extra_1 = __importDefault(require("fs-extra"));
32
+ const path_1 = __importDefault(require("path"));
33
+ const path_2 = require("../utils/path");
34
+ const runtime_1 = require("./runtime");
35
+ /** Config files in precedence order; the first that exists is the one we mutate. */
36
+ const CONFIG_FILES = ['handoff.config.ts', 'handoff.config.js', 'handoff.config.cjs', 'handoff.config.json'];
37
+ /** Workspace-relative, POSIX-separated path used as a config entry value. */
38
+ const toEntryPath = (handoff, targetDir) => path_1.default.relative(handoff.workingPath, targetDir).split(path_1.default.sep).join('/');
39
+ /**
40
+ * True when `targetDir` already loads through an existing `entries[kind]` declaration: listed
41
+ * directly, or sitting under a declared collection directory that runtime discovery expands.
42
+ * Reuses the same expansion the runtime uses, so every declaration style is covered.
43
+ */
44
+ const isEntryCovered = (handoff, kind, targetDir) => {
45
+ var _a, _b;
46
+ const configured = (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b[kind];
47
+ if (!(configured === null || configured === void 0 ? void 0 : configured.length)) {
48
+ return false;
49
+ }
50
+ return configured
51
+ .flatMap((entry) => (0, runtime_1.getComponentsForPath)(path_1.default.resolve(handoff.workingPath, entry)))
52
+ .some((dir) => (0, path_2.arePathsEqual)(dir, targetDir));
53
+ };
54
+ exports.isEntryCovered = isEntryCovered;
55
+ /** Format an entry array as source, matching the surrounding indentation of a code config. */
56
+ const formatEntryArray = (paths, indent) => {
57
+ if (paths.length === 0)
58
+ return '[]';
59
+ if (paths.length === 1)
60
+ return `['${paths[0]}']`;
61
+ return `[\n${paths.map((entry) => `${indent}'${entry}',`).join('\n')}\n${indent.slice(2)}]`;
62
+ };
63
+ /** Append entry paths to a structured `.json` config (lossless read/modify/write). */
64
+ const addToJsonConfig = (configPath, kind, relPaths) => __awaiter(void 0, void 0, void 0, function* () {
65
+ var _a;
66
+ try {
67
+ const config = yield fs_extra_1.default.readJSON(configPath);
68
+ config.entries = (_a = config.entries) !== null && _a !== void 0 ? _a : {};
69
+ const existing = Array.isArray(config.entries[kind]) ? config.entries[kind] : [];
70
+ const existingSet = new Set(existing);
71
+ for (const relPath of relPaths) {
72
+ if (!existingSet.has(relPath))
73
+ existing.push(relPath);
74
+ }
75
+ config.entries[kind] = existing;
76
+ yield fs_extra_1.default.writeJSON(configPath, config, { spaces: 2 });
77
+ return true;
78
+ }
79
+ catch (_b) {
80
+ return false;
81
+ }
82
+ });
83
+ /**
84
+ * Best-effort splice of entry paths into an executable `.ts` / `.js` / `.cjs` config. These are
85
+ * modules, not data, so there's no lossless structured write; if a computed or spread `entries`
86
+ * array can't be edited textually, the caller falls back to printing the paths for the user to add.
87
+ */
88
+ const addToCodeConfig = (configPath, kind, relPaths) => __awaiter(void 0, void 0, void 0, function* () {
89
+ try {
90
+ let content = yield fs_extra_1.default.readFile(configPath, 'utf8');
91
+ const arrayBlock = formatEntryArray(relPaths, ' ');
92
+ // 1) An `entries: { ... <kind>: [ ... ] }` array already exists, so merge into it.
93
+ const hasKeyArray = new RegExp(`entries\\s*:\\s*\\{[\\s\\S]*?${kind}\\s*:`).test(content);
94
+ const arrayRegex = new RegExp(`(${kind}\\s*:\\s*\\[)([^\\]]*)(\\])`);
95
+ const match = hasKeyArray ? content.match(arrayRegex) : null;
96
+ if (match) {
97
+ const existing = match[2]
98
+ .split(',')
99
+ .map((s) => s.trim().replace(/['"]/g, ''))
100
+ .filter(Boolean);
101
+ const existingSet = new Set(existing);
102
+ const toAdd = relPaths.filter((relPath) => !existingSet.has(relPath));
103
+ if (toAdd.length > 0) {
104
+ const indentMatch = content.match(new RegExp(`${kind}\\s*:\\s*\\[\\s*\\n(\\s*)`));
105
+ const indent = indentMatch ? indentMatch[1] : ' ';
106
+ content = content.replace(arrayRegex, `${kind}: ${formatEntryArray([...existing, ...toAdd], indent)}`);
107
+ yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
108
+ }
109
+ return true;
110
+ }
111
+ // 2) An `entries` object exists but not this key, so add the key.
112
+ if (/entries\s*:\s*\{/.test(content)) {
113
+ content = content.replace(/(entries\s*:\s*\{)/, `$1\n ${kind}: ${arrayBlock},`);
114
+ yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
115
+ return true;
116
+ }
117
+ // 3) No `entries` object, so insert one into the exported config object.
118
+ const entriesBlock = ` entries: {\n ${kind}: ${arrayBlock},\n },\n`;
119
+ for (const anchor of [/module\.exports\s*=\s*\{/, /export\s+default\s+\{/, /defineConfig\s*\(\s*\{/]) {
120
+ if (anchor.test(content)) {
121
+ content = content.replace(anchor, (matched) => `${matched}\n${entriesBlock}`);
122
+ yield fs_extra_1.default.writeFile(configPath, content, 'utf8');
123
+ return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+ catch (_a) {
129
+ return false;
130
+ }
131
+ });
132
+ /**
133
+ * Add `targetDirs` to `entries[kind]` in the workspace config so the build discovers them. Callers
134
+ * should first drop already-loading dirs via {@link isEntryCovered}. Paths are stored workspace-
135
+ * relative; a `.json` config is edited losslessly, a code config best-effort. When no config file
136
+ * exists a minimal `handoff.config.json` is created.
137
+ */
138
+ const writeEntries = (handoff, kind, targetDirs) => __awaiter(void 0, void 0, void 0, function* () {
139
+ const relPaths = [...new Set(targetDirs.map((dir) => toEntryPath(handoff, dir)))];
140
+ const configFile = CONFIG_FILES.find((file) => fs_extra_1.default.existsSync(path_1.default.resolve(handoff.workingPath, file)));
141
+ const configPath = configFile ? path_1.default.resolve(handoff.workingPath, configFile) : null;
142
+ if (relPaths.length === 0) {
143
+ return { status: 'added', configPath, added: [], pending: [] };
144
+ }
145
+ if (!configPath) {
146
+ const newConfigPath = path_1.default.resolve(handoff.workingPath, 'handoff.config.json');
147
+ try {
148
+ yield fs_extra_1.default.writeJSON(newConfigPath, { entries: { [kind]: relPaths } }, { spaces: 2 });
149
+ return { status: 'added', configPath: newConfigPath, added: relPaths, pending: [] };
150
+ }
151
+ catch (_a) {
152
+ return { status: 'unsupported', configPath: null, added: [], pending: relPaths };
153
+ }
154
+ }
155
+ const ok = configPath.endsWith('.json')
156
+ ? yield addToJsonConfig(configPath, kind, relPaths)
157
+ : yield addToCodeConfig(configPath, kind, relPaths);
158
+ return ok
159
+ ? { status: 'added', configPath, added: relPaths, pending: [] }
160
+ : { status: 'unsupported', configPath, added: [], pending: relPaths };
161
+ });
162
+ exports.writeEntries = writeEntries;
@@ -4,3 +4,5 @@ export { initConfig, initConfigWithMetadata } from './loader';
4
4
  export { defineConfig } from './helpers';
5
5
  export { initRuntimeConfig } from './runtime';
6
6
  export { validateConfig } from './validator';
7
+ export { isEntryCovered, writeEntries } from './entries';
8
+ export type { EntryKind, WriteEntriesResult } from './entries';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- 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.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; } });
@@ -18,3 +18,7 @@ Object.defineProperty(exports, "initRuntimeConfig", { enumerable: true, get: fun
18
18
  // Config validation
19
19
  var validator_1 = require("./validator");
20
20
  Object.defineProperty(exports, "validateConfig", { enumerable: true, get: function () { return validator_1.validateConfig; } });
21
+ // Registering directory-list entries (components/patterns)
22
+ var entries_1 = require("./entries");
23
+ Object.defineProperty(exports, "isEntryCovered", { enumerable: true, get: function () { return entries_1.isEntryCovered; } });
24
+ Object.defineProperty(exports, "writeEntries", { enumerable: true, get: function () { return entries_1.writeEntries; } });
@@ -21,17 +21,17 @@ export declare const resolveComponentDeclaration: (componentDir: string, compone
21
21
  */
22
22
  export declare const initRuntimeConfig: (handoff: HandoffContext) => [runtimeConfig: RuntimeConfig, configs: string[], configFileIndex: Map<string, ConfigFileEntry>];
23
23
  /**
24
- * Returns a list of component directories for a given path.
25
- *
26
- * This function determines whether the provided `searchPath` is:
27
- * 1. A single component directory (contains a config file named after the directory)
28
- * 2. A collection of component directories (subdirectories are components)
29
- *
30
- * A directory is considered a component if it contains a config file matching
31
- * `{dirname}.json`, `{dirname}.js`, or `{dirname}.cjs`.
24
+ * True when `searchPath` is itself a single component directory: it holds a declaration file named
25
+ * after the directory (`{dirname}.handoff.*`, or a legacy `{dirname}.{json,js,cjs}`). This is what
26
+ * separates a declared component from a collection directory.
27
+ */
28
+ export declare const isComponentDirectory: (searchPath: string) => boolean;
29
+ /**
30
+ * Returns the component directories for a given path. If `searchPath` is itself a component
31
+ * directory it's returned as-is; otherwise each subdirectory is treated as a potential component.
32
32
  *
33
33
  * @param searchPath - The absolute path to check for components.
34
- * @returns An array of string paths to component directories.
34
+ * @returns Paths to the component directories.
35
35
  */
36
36
  export declare const getComponentsForPath: (searchPath: string) => string[];
37
37
  /**
@@ -3,7 +3,7 @@ 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.toLowerCaseKeysAndValues = exports.getItemsForPath = exports.getComponentsForPath = exports.initRuntimeConfig = exports.resolveComponentDeclaration = void 0;
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
9
  const gray_matter_1 = __importDefault(require("gray-matter"));
@@ -283,24 +283,24 @@ const collectPageMarkdownPaths = (rootDir, relativeParts = []) => {
283
283
  return results;
284
284
  };
285
285
  /**
286
- * Returns a list of component directories for a given path.
287
- *
288
- * This function determines whether the provided `searchPath` is:
289
- * 1. A single component directory (contains a config file named after the directory)
290
- * 2. A collection of component directories (subdirectories are components)
291
- *
292
- * A directory is considered a component if it contains a config file matching
293
- * `{dirname}.json`, `{dirname}.js`, or `{dirname}.cjs`.
286
+ * True when `searchPath` is itself a single component directory: it holds a declaration file named
287
+ * after the directory (`{dirname}.handoff.*`, or a legacy `{dirname}.{json,js,cjs}`). This is what
288
+ * separates a declared component from a collection directory.
289
+ */
290
+ const isComponentDirectory = (searchPath) => !!(0, exports.resolveComponentDeclaration)(searchPath, path_1.default.basename(searchPath));
291
+ exports.isComponentDirectory = isComponentDirectory;
292
+ /**
293
+ * Returns the component directories for a given path. If `searchPath` is itself a component
294
+ * directory it's returned as-is; otherwise each subdirectory is treated as a potential component.
294
295
  *
295
296
  * @param searchPath - The absolute path to check for components.
296
- * @returns An array of string paths to component directories.
297
+ * @returns Paths to the component directories.
297
298
  */
298
299
  const getComponentsForPath = (searchPath) => {
299
- const dirName = path_1.default.basename(searchPath);
300
- const hasOwnConfig = !!(0, exports.resolveComponentDeclaration)(searchPath, dirName);
301
- // Check if searchPath itself is a component directory (has a config file named after the directory)
302
- if (hasOwnConfig) {
303
- // This directory is a single component
300
+ if ((0, exports.isComponentDirectory)(searchPath)) {
301
+ return [searchPath];
302
+ }
303
+ if (!fs_extra_1.default.existsSync(searchPath)) {
304
304
  return [searchPath];
305
305
  }
306
306
  // Otherwise, treat each subdirectory as a potential component