@servicetitan/hammer-token 2.5.0 → 3.0.0

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 (147) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +332 -0
  3. package/build/web/core/component-variables.scss +1088 -131
  4. package/build/web/core/component.d.ts +558 -0
  5. package/build/web/core/component.js +6685 -249
  6. package/build/web/core/component.scss +557 -69
  7. package/build/web/core/css-utils/a2-border.css +47 -45
  8. package/build/web/core/css-utils/a2-color.css +443 -227
  9. package/build/web/core/css-utils/a2-font.css +0 -2
  10. package/build/web/core/css-utils/a2-spacing.css +476 -478
  11. package/build/web/core/css-utils/a2-utils.css +992 -772
  12. package/build/web/core/css-utils/border.css +47 -45
  13. package/build/web/core/css-utils/color.css +443 -227
  14. package/build/web/core/css-utils/font.css +0 -2
  15. package/build/web/core/css-utils/spacing.css +476 -478
  16. package/build/web/core/css-utils/utils.css +992 -772
  17. package/build/web/core/index.d.ts +6 -0
  18. package/build/web/core/index.js +1 -1
  19. package/build/web/core/primitive-variables.scss +148 -65
  20. package/build/web/core/primitive.d.ts +209 -0
  21. package/build/web/core/primitive.js +779 -61
  22. package/build/web/core/primitive.scss +207 -124
  23. package/build/web/core/semantic-variables.scss +363 -239
  24. package/build/web/core/semantic.d.ts +221 -0
  25. package/build/web/core/semantic.js +1613 -347
  26. package/build/web/core/semantic.scss +219 -137
  27. package/build/web/index.d.ts +3 -4
  28. package/build/web/index.js +0 -1
  29. package/build/web/types.d.ts +17 -0
  30. package/config.js +121 -497
  31. package/eslint.config.mjs +11 -1
  32. package/package.json +15 -5
  33. package/src/global/primitive/breakpoint.tokens.json +54 -0
  34. package/src/global/primitive/color.tokens.json +1092 -0
  35. package/src/global/primitive/duration.tokens.json +44 -0
  36. package/src/global/primitive/font.tokens.json +151 -0
  37. package/src/global/primitive/radius.tokens.json +94 -0
  38. package/src/global/primitive/size.tokens.json +174 -0
  39. package/src/global/primitive/transition.tokens.json +32 -0
  40. package/src/theme/core/background.tokens.json +1312 -0
  41. package/src/theme/core/border.tokens.json +192 -0
  42. package/src/theme/core/chart.tokens.json +982 -0
  43. package/src/theme/core/component/ai-mark.tokens.json +20 -0
  44. package/src/theme/core/component/alert.tokens.json +261 -0
  45. package/src/theme/core/component/announcement.tokens.json +460 -0
  46. package/src/theme/core/component/avatar.tokens.json +137 -0
  47. package/src/theme/core/component/badge.tokens.json +42 -0
  48. package/src/theme/core/component/breadcrumb.tokens.json +42 -0
  49. package/src/theme/core/component/button-toggle.tokens.json +428 -0
  50. package/src/theme/core/component/button.tokens.json +941 -0
  51. package/src/theme/core/component/calendar.tokens.json +391 -0
  52. package/src/theme/core/component/card.tokens.json +107 -0
  53. package/src/theme/core/component/checkbox.tokens.json +631 -0
  54. package/src/theme/core/component/chip.tokens.json +169 -0
  55. package/src/theme/core/component/combobox.tokens.json +269 -0
  56. package/src/theme/core/component/details.tokens.json +152 -0
  57. package/src/theme/core/component/dialog.tokens.json +87 -0
  58. package/src/theme/core/component/divider.tokens.json +23 -0
  59. package/src/theme/core/component/dnd.tokens.json +208 -0
  60. package/src/theme/core/component/drawer.tokens.json +61 -0
  61. package/src/theme/core/component/drilldown.tokens.json +61 -0
  62. package/src/theme/core/component/edit-card.tokens.json +381 -0
  63. package/src/theme/core/component/field-label.tokens.json +42 -0
  64. package/src/theme/core/component/field-message.tokens.json +74 -0
  65. package/src/theme/core/component/icon.tokens.json +42 -0
  66. package/src/theme/core/component/link.tokens.json +108 -0
  67. package/src/theme/core/component/list-view.tokens.json +82 -0
  68. package/src/theme/core/component/listbox.tokens.json +283 -0
  69. package/src/theme/core/component/menu.tokens.json +230 -0
  70. package/src/theme/core/component/overflow.tokens.json +84 -0
  71. package/src/theme/core/component/page.tokens.json +377 -0
  72. package/src/theme/core/component/pagination.tokens.json +63 -0
  73. package/src/theme/core/component/popover.tokens.json +122 -0
  74. package/src/theme/core/component/progress-bar.tokens.json +133 -0
  75. package/src/theme/core/component/radio.tokens.json +631 -0
  76. package/src/theme/core/component/segmented-control.tokens.json +175 -0
  77. package/src/theme/core/component/select-card.tokens.json +943 -0
  78. package/src/theme/core/component/side-nav.tokens.json +349 -0
  79. package/src/theme/core/component/skeleton.tokens.json +42 -0
  80. package/src/theme/core/component/spinner.tokens.json +96 -0
  81. package/src/theme/core/component/status-icon.tokens.json +164 -0
  82. package/src/theme/core/component/stepper.tokens.json +484 -0
  83. package/src/theme/core/component/switch.tokens.json +285 -0
  84. package/src/theme/core/component/tab.tokens.json +192 -0
  85. package/src/theme/core/component/text-field.tokens.json +160 -0
  86. package/src/theme/core/component/text.tokens.json +59 -0
  87. package/src/theme/core/component/toast.tokens.json +343 -0
  88. package/src/theme/core/component/toolbar.tokens.json +114 -0
  89. package/src/theme/core/component/tooltip.tokens.json +61 -0
  90. package/src/theme/core/focus.tokens.json +56 -0
  91. package/src/theme/core/foreground.tokens.json +416 -0
  92. package/src/theme/core/gradient.tokens.json +41 -0
  93. package/src/theme/core/opacity.tokens.json +25 -0
  94. package/src/theme/core/shadow.tokens.json +81 -0
  95. package/src/theme/core/status.tokens.json +74 -0
  96. package/src/theme/core/typography.tokens.json +163 -0
  97. package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
  98. package/src/utils/__tests__/sd-build-configs.test.js +306 -0
  99. package/src/utils/__tests__/sd-formats.test.js +950 -0
  100. package/src/utils/__tests__/sd-transforms.test.js +336 -0
  101. package/src/utils/__tests__/token-helpers.test.js +1160 -0
  102. package/src/utils/copy-css-utils-cli.js +13 -1
  103. package/src/utils/css-utils-format-utils.js +105 -176
  104. package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
  105. package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
  106. package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
  107. package/src/utils/figma/auth.js +355 -0
  108. package/src/utils/figma/constants.js +22 -0
  109. package/src/utils/figma/errors.js +80 -0
  110. package/src/utils/figma/figma-api.js +1069 -0
  111. package/src/utils/figma/get-token.js +348 -0
  112. package/src/utils/figma/sync-components.js +909 -0
  113. package/src/utils/figma/sync-main.js +692 -0
  114. package/src/utils/figma/sync-orchestration.js +683 -0
  115. package/src/utils/figma/sync-primitives.js +230 -0
  116. package/src/utils/figma/sync-semantic.js +1056 -0
  117. package/src/utils/figma/token-conversion.js +340 -0
  118. package/src/utils/figma/token-parsing.js +186 -0
  119. package/src/utils/figma/token-resolution.js +569 -0
  120. package/src/utils/figma/utils.js +199 -0
  121. package/src/utils/sd-build-configs.js +305 -0
  122. package/src/utils/sd-formats.js +965 -0
  123. package/src/utils/sd-transforms.js +165 -0
  124. package/src/utils/token-helpers.js +848 -0
  125. package/tsconfig.json +18 -0
  126. package/vitest.config.js +17 -0
  127. package/.turbo/turbo-build.log +0 -37
  128. package/build/web/core/raw.js +0 -229
  129. package/src/global/primitive/breakpoint.js +0 -19
  130. package/src/global/primitive/color.js +0 -231
  131. package/src/global/primitive/duration.js +0 -16
  132. package/src/global/primitive/font.js +0 -60
  133. package/src/global/primitive/radius.js +0 -31
  134. package/src/global/primitive/size.js +0 -55
  135. package/src/global/primitive/transition.js +0 -16
  136. package/src/theme/core/background.js +0 -170
  137. package/src/theme/core/border.js +0 -103
  138. package/src/theme/core/charts.js +0 -439
  139. package/src/theme/core/component/button.js +0 -708
  140. package/src/theme/core/component/checkbox.js +0 -405
  141. package/src/theme/core/focus.js +0 -35
  142. package/src/theme/core/foreground.js +0 -148
  143. package/src/theme/core/overlay.js +0 -137
  144. package/src/theme/core/shadow.js +0 -29
  145. package/src/theme/core/status.js +0 -49
  146. package/src/theme/core/typography.js +0 -82
  147. package/type/types.ts +0 -341
@@ -0,0 +1,692 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Figma Variables Sync Script - Main Entry Point
5
+ *
6
+ * Syncs design tokens from hammer-token/src to Figma variables using the Figma REST API.
7
+ * Handles token parsing, reference resolution, variable creation/updates, and mode management.
8
+ */
9
+
10
+ const { loadConfig, getAccessToken, getAuthMethod } = require("./auth");
11
+ const { debugLog, figmaRequest } = require("./figma-api");
12
+ const {
13
+ detectThemes,
14
+ loadThemeTokenFiles,
15
+ parseTokenFile,
16
+ flattenTokenTree,
17
+ pathToVariableName,
18
+ } = require("./token-parsing");
19
+ const { resolvePrimitiveReferences } = require("./token-resolution");
20
+ const { syncTheme } = require("./sync-orchestration");
21
+ const {
22
+ maskToken,
23
+ validateFileKey,
24
+ parseCliArgs,
25
+ formatDuration,
26
+ printHelp,
27
+ } = require("./utils");
28
+ const {
29
+ ConfigurationError,
30
+ AuthenticationError,
31
+ FileAccessError,
32
+ } = require("./errors");
33
+ const { FIGMA_PROD_FILE_KEY } = require("./constants");
34
+
35
+ // Configuration
36
+ const FIGMA_API_BASE = "https://api.figma.com/v1";
37
+
38
+ /**
39
+ * Main sync function
40
+ * @param {string} [fileKey] - Optional fileKey to override the default or env var
41
+ * @param {Object} [options] - Sync options
42
+ * @param {boolean} [options.dryRun=false] - If true, validate without making changes
43
+ * @param {boolean} [options.verbose=false] - Enable verbose logging
44
+ * @returns {Promise<Object>} Sync statistics
45
+ */
46
+ async function syncToFigma(fileKey = null, options = {}) {
47
+ const { dryRun = false, verbose = false } = options;
48
+ const startTime = Date.now();
49
+ const apiCallCount = 0;
50
+
51
+ if (dryRun) {
52
+ console.log("🔍 DRY RUN MODE - No changes will be made\n");
53
+ }
54
+ console.log("🚀 Starting Figma variables sync...\n");
55
+
56
+ try {
57
+ // Validate fileKey if provided
58
+ if (fileKey) {
59
+ validateFileKey(fileKey);
60
+ }
61
+
62
+ // Load configuration
63
+ const config = loadConfig(fileKey);
64
+ console.log(`📁 Using Figma file: ${config.fileKey}`);
65
+ console.log(`📦 Collection: ${config.collectionName}`);
66
+
67
+ // Get access token (supports both PAT and OAuth2)
68
+ const authMethod = await getAuthMethod(config);
69
+ const accessToken = await getAccessToken(config);
70
+ console.log(`🔑 Auth method: ${authMethod}`);
71
+ if (verbose) {
72
+ console.log(`🔑 Token: ${maskToken(accessToken)}\n`);
73
+ } else {
74
+ console.log();
75
+ }
76
+
77
+ // Set accessToken for API calls (for backward compatibility)
78
+ config.accessToken = accessToken;
79
+
80
+ // Skip dedicated file verification: GET /files/:key (even with ?depth=1) can return
81
+ // "Request too large" on very large files. Access is verified by the first real
82
+ // API call (getOrCreateCollection → GET /variables/local); 403 there is treated as FileAccessError below.
83
+
84
+ // Detect themes
85
+ console.log("🎨 Detecting themes...");
86
+ const themes = detectThemes();
87
+ console.log(` Found themes: ${themes.join(", ")}\n`);
88
+
89
+ if (themes.length === 0) {
90
+ throw new Error("No themes found in src/theme/ directory");
91
+ }
92
+
93
+ // Process core theme first
94
+ console.log("📂 Processing core theme...");
95
+ const coreThemeName = themes[0]; // Core should be first
96
+ if (coreThemeName !== "core") {
97
+ console.warn(
98
+ ` ⚠️ Warning: First theme is "${coreThemeName}", expected "core"`,
99
+ );
100
+ }
101
+
102
+ // Sync core theme (full sync)
103
+ const coreStats = await syncTheme(
104
+ config,
105
+ coreThemeName,
106
+ null,
107
+ config.collectionName,
108
+ { dryRun, verbose },
109
+ );
110
+
111
+ // Load core tokens for comparison with extended themes
112
+ const coreTokenFiles = loadThemeTokenFiles(coreThemeName);
113
+ const coreAllTokens = [];
114
+ for (const filePath of coreTokenFiles) {
115
+ const tokenData = parseTokenFile(filePath);
116
+ const flattened = flattenTokenTree(tokenData, [], [], filePath);
117
+ coreAllTokens.push(...flattened);
118
+ }
119
+ const coreTokenRegistry = new Map();
120
+ for (const tokenData of coreAllTokens) {
121
+ const variableName = pathToVariableName(tokenData.path);
122
+ coreTokenRegistry.set(variableName, {
123
+ ...tokenData,
124
+ variableName,
125
+ });
126
+ }
127
+ const coreResolvedTokens = resolvePrimitiveReferences(coreTokenRegistry);
128
+
129
+ // Process extended themes
130
+ const extendedThemes = themes.slice(1);
131
+ const allStats = { ...coreStats };
132
+ const allErrorMessages = [...(coreStats.errorMessages || [])];
133
+
134
+ for (const themeName of extendedThemes) {
135
+ const extendedCollectionName = themeName; // Use just the theme name for extended collections
136
+ const themeStats = await syncTheme(
137
+ config,
138
+ themeName,
139
+ coreResolvedTokens,
140
+ extendedCollectionName,
141
+ { dryRun, verbose },
142
+ );
143
+
144
+ // Accumulate stats
145
+ allStats.created += themeStats.created ?? 0;
146
+ allStats.updated += themeStats.updated ?? 0;
147
+ allStats.skipped += themeStats.skipped ?? 0;
148
+ allStats.deleted += themeStats.deleted ?? 0;
149
+ allStats.errors += themeStats.errors ?? 0;
150
+
151
+ // Collect error messages
152
+ if (themeStats.errorMessages) {
153
+ allErrorMessages.push(...themeStats.errorMessages);
154
+ }
155
+ }
156
+
157
+ const duration = Date.now() - startTime;
158
+
159
+ // Group errors by type
160
+ const errorGroups = new Map();
161
+ for (const error of allErrorMessages) {
162
+ let errorType = "Other";
163
+ if (error.includes("Could not find")) {
164
+ errorType = "Token Not Found";
165
+ } else if (error.includes("Could not resolve")) {
166
+ errorType = "Reference Resolution";
167
+ } else if (error.includes("Could not convert")) {
168
+ errorType = "Value Conversion";
169
+ } else if (
170
+ error.includes("Modes are missing") ||
171
+ error.includes("Mode IDs are missing")
172
+ ) {
173
+ errorType = "Mode Configuration";
174
+ } else if (
175
+ error.includes("Existing variable") ||
176
+ error.includes("has no ID")
177
+ ) {
178
+ errorType = "Variable ID Missing";
179
+ } else if (error.includes("Error parsing")) {
180
+ errorType = "Parsing Error";
181
+ } else if (error.includes("Error resolving references")) {
182
+ errorType = "Reference Resolution";
183
+ }
184
+
185
+ if (!errorGroups.has(errorType)) {
186
+ errorGroups.set(errorType, []);
187
+ }
188
+ errorGroups.get(errorType).push(error);
189
+ }
190
+
191
+ // Final summary
192
+ console.log("\n");
193
+ console.log("📊 Sync Summary:");
194
+ if (dryRun) {
195
+ console.log(" 🔍 DRY RUN - No changes were made");
196
+ } else {
197
+ console.log(" ✅ Sync complete!");
198
+ }
199
+ console.log(` ✅ Created: ${allStats.created ?? 0}`);
200
+ console.log(` 🔄 Updated: ${allStats.updated ?? 0}`);
201
+ console.log(` ⏭️ Skipped: ${allStats.skipped ?? 0}`);
202
+ if ((allStats.deleted ?? 0) > 0) {
203
+ console.log(` 🗑️ Deleted: ${allStats.deleted}`);
204
+ }
205
+ if (allStats.errors > 0) {
206
+ console.log(` ❌ Errors: ${allStats.errors}`);
207
+ console.log("\n Error Breakdown:");
208
+ for (const [errorType, errors] of errorGroups.entries()) {
209
+ console.log(` ${errorType}: ${errors.length}`);
210
+ // Always show error details (up to 5 per type) so user can fix; show all when verbose
211
+ const toShow = verbose ? errors : errors.slice(0, 5);
212
+ toShow.forEach((err) => {
213
+ console.log(` - ${err}`);
214
+ });
215
+ if (!verbose && errors.length > 5) {
216
+ console.log(
217
+ ` ... and ${errors.length - 5} more (run with --verbose to see all)`,
218
+ );
219
+ }
220
+ }
221
+ }
222
+ console.log(` ⏱️ Duration: ${formatDuration(duration)}`);
223
+ if (verbose) {
224
+ console.log(` 📡 API Calls: ${apiCallCount}`);
225
+ }
226
+ } catch (error) {
227
+ debugLog(
228
+ "sync-main.js:syncToFigma",
229
+ "Fatal error caught",
230
+ {
231
+ errorMessage: error.message,
232
+ errorMessageLength: error.message?.length,
233
+ errorStack: error.stack?.substring(0, 500),
234
+ errorCode: error.code,
235
+ errorName: error.name,
236
+ },
237
+ "I",
238
+ );
239
+
240
+ // Handle custom error types (including 403 from first real API call when we skip file verification)
241
+ if (error instanceof ConfigurationError) {
242
+ console.error("\n❌ Configuration Error:", error.message);
243
+ if (error.details?.errors) {
244
+ error.details.errors.forEach((err) => {
245
+ console.error(` - ${err}`);
246
+ });
247
+ }
248
+ } else if (
249
+ error instanceof FileAccessError ||
250
+ error.message.includes("403") ||
251
+ error.message.includes("forbidden")
252
+ ) {
253
+ console.error("\n❌ File Access Error:", error.message);
254
+ } else if (error instanceof AuthenticationError) {
255
+ console.error("\n❌ Authentication Error:", error.message);
256
+ } else {
257
+ console.error("\n❌ Fatal error:", error.message);
258
+ }
259
+ if (
260
+ error.stack &&
261
+ (process.env.DEBUG || process.env.NODE_ENV === "development")
262
+ ) {
263
+ console.error("\nStack trace:");
264
+ console.error(error.stack);
265
+ }
266
+ console.error("\n💡 Tips:");
267
+ console.error(
268
+ " - Check that OAuth2 credentials are set correctly (FIGMA_CLIENT_ID, FIGMA_CLIENT_SECRET, FIGMA_REFRESH_TOKEN)",
269
+ );
270
+ console.error(
271
+ " - Verify the refresh token hasn't expired (re-authenticate if needed)",
272
+ );
273
+ console.error(" - Verify the file key is correct");
274
+ console.error(" - Ensure you have access to the Figma file");
275
+ console.error(" - Verify your OAuth2 app has the required scopes");
276
+ console.error(" - Run with DEBUG=1 for more details");
277
+ process.exit(1);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Test API access and token validity
283
+ * @param {string} [fileKey] - Optional fileKey to override the default or env var
284
+ * @param {Object} [options] - Test options
285
+ * @param {boolean} [options.full=false] - Run full test suite
286
+ * @param {boolean} [options.verbose=false] - Enable verbose logging
287
+ */
288
+ async function testFigmaAccess(fileKey = null, options = {}) {
289
+ const { full: _full = false, verbose = false } = options;
290
+ console.log("🧪 Testing Figma API access...\n");
291
+
292
+ try {
293
+ // Validate fileKey if provided
294
+ if (fileKey) {
295
+ validateFileKey(fileKey);
296
+ }
297
+
298
+ const config = loadConfig(fileKey);
299
+ console.log(`📁 File key: ${config.fileKey}`);
300
+
301
+ // Get access token (OAuth2)
302
+ const authMethod = await getAuthMethod(config);
303
+ const accessToken = await getAccessToken(config);
304
+ console.log(`🔑 Auth method: ${authMethod}`);
305
+ if (verbose) {
306
+ console.log(`🔑 Token: ${maskToken(accessToken)}\n`);
307
+ } else {
308
+ console.log();
309
+ }
310
+
311
+ // Test 1: Verify token validity
312
+ console.log("1️⃣ Testing token validity...");
313
+ try {
314
+ const response = await fetch(`${FIGMA_API_BASE}/me`, {
315
+ headers: {
316
+ Authorization: `Bearer ${accessToken}`,
317
+ },
318
+ });
319
+
320
+ if (!response.ok) {
321
+ const errorText = await response.text();
322
+ let errorJson;
323
+ try {
324
+ errorJson = JSON.parse(errorText);
325
+ } catch {
326
+ // Not JSON, use raw text
327
+ }
328
+
329
+ // Check if it's a scope error (missing current_user:read)
330
+ const isScopeError =
331
+ response.status === 403 &&
332
+ (errorText.includes("current_user:read") ||
333
+ (errorJson?.err && errorJson.err.includes("current_user:read")));
334
+
335
+ if (isScopeError) {
336
+ console.warn(
337
+ " ⚠️ Token validation skipped (missing current_user:read scope)",
338
+ );
339
+ console.warn(
340
+ "\n 💡 The token doesn't have the 'current_user:read' scope required for /me endpoint.",
341
+ );
342
+ console.warn(
343
+ " This is not critical - the token will still work for file and variables API calls.",
344
+ );
345
+ console.warn(
346
+ " To fix: Re-authenticate with updated scopes that include 'current_user:read'.\n",
347
+ );
348
+ // Don't throw - continue with other tests
349
+ } else {
350
+ throw new Error(
351
+ `Token validation failed (${response.status}): ${errorText}`,
352
+ );
353
+ }
354
+ } else {
355
+ const userData = await response.json();
356
+ console.log(" ✅ Token is valid");
357
+ console.log(
358
+ ` 👤 Account: ${userData.email || userData.handle || "Unknown"}`,
359
+ );
360
+ console.log(` 📧 Email: ${userData.email || "Not available"}`);
361
+ console.log(` 🆔 User ID: ${userData.id || "Not available"}\n`);
362
+ }
363
+ } catch (error) {
364
+ // Only throw if it's not already handled (scope error case)
365
+ if (!error.message.includes("Token validation skipped")) {
366
+ console.error(" ❌ Token validation failed:", error.message);
367
+ console.error("\n 💡 This usually means:");
368
+ console.error(" - The token is invalid or has been revoked");
369
+ console.error(" - The token format is incorrect");
370
+ console.error(
371
+ " - The token is missing required scopes (e.g., current_user:read)",
372
+ );
373
+ throw error;
374
+ }
375
+ }
376
+
377
+ // Test 2: Verify file access
378
+ console.log("2️⃣ Testing file access...");
379
+ try {
380
+ const fileData = await figmaRequest(config, "");
381
+ console.log(" ✅ File access granted");
382
+ console.log(` 📄 File name: ${fileData.name || "Unknown"}`);
383
+ console.log(` 🔑 File key: ${config.fileKey}`);
384
+ console.log(
385
+ ` 📅 Last modified: ${fileData.lastModified || "Unknown"}\n`,
386
+ );
387
+ } catch (error) {
388
+ const is403 =
389
+ error.message.includes("403") || error.message.includes("forbidden");
390
+ const is404 =
391
+ error.message.includes("404") || error.message.includes("not found");
392
+
393
+ console.error(" ❌ File access failed:", error.message);
394
+ console.error("\n 💡 Troubleshooting:");
395
+
396
+ if (is403) {
397
+ console.error(" This is a 403 Forbidden error, which means:");
398
+ console.error(" - The file exists and the token is valid");
399
+ console.error(
400
+ " - But your OAuth2 app doesn't have API access to this file",
401
+ );
402
+ console.error("\n Possible solutions:");
403
+ console.error(
404
+ " 1. Check if the file is in a team/org you're a member of",
405
+ );
406
+ console.error(
407
+ " 2. Verify you can open the file in Figma: https://www.figma.com/design/" +
408
+ config.fileKey,
409
+ );
410
+ console.error(
411
+ " 3. Verify your OAuth2 app has the required scopes",
412
+ );
413
+ console.error(" 4. For Enterprise orgs:");
414
+ console.error(
415
+ " - Contact your Figma admin to enable API access",
416
+ );
417
+ console.error(
418
+ " - Check if your org has API access restrictions",
419
+ );
420
+ console.error(
421
+ " - You may need organization-level API permissions",
422
+ );
423
+ console.error(
424
+ " 5. Re-authenticate to get a new refresh token if needed",
425
+ );
426
+ } else if (is404) {
427
+ console.error(" This is a 404 Not Found error, which means:");
428
+ console.error(" - The file key might be incorrect");
429
+ console.error(" - Or the file doesn't exist");
430
+ console.error("\n Verify the file key from the Figma URL");
431
+ } else {
432
+ console.error(" - The file key might be incorrect");
433
+ console.error(" - Your account doesn't have access to this file");
434
+ console.error(
435
+ " - The file is in a team/org you're not a member of",
436
+ );
437
+ console.error(
438
+ " - For Enterprise orgs, API access may be restricted",
439
+ );
440
+ }
441
+ throw error;
442
+ }
443
+
444
+ // Test 3: Verify variables API access
445
+ console.log("3️⃣ Testing variables API access...");
446
+ try {
447
+ const variablesData = await figmaRequest(config, "/variables/local");
448
+ const collections = variablesData.meta?.variableCollections
449
+ ? Object.values(variablesData.meta.variableCollections)
450
+ : [];
451
+ const variables = variablesData.meta?.variables
452
+ ? Object.values(variablesData.meta.variables)
453
+ : [];
454
+
455
+ console.log(" ✅ Variables API access granted");
456
+ console.log(` 📦 Collections: ${collections.length}`);
457
+ console.log(` 🔢 Variables: ${variables.length}`);
458
+
459
+ if (collections.length > 0) {
460
+ console.log("\n Existing collections:");
461
+ collections.forEach((coll) => {
462
+ console.log(
463
+ ` - ${coll.name} (${coll.modes?.length || 0} modes)`,
464
+ );
465
+ });
466
+ }
467
+ console.log("");
468
+ } catch (error) {
469
+ console.error(" ❌ Variables API access failed:", error.message);
470
+ console.error("\n 💡 This usually means:");
471
+ console.error(" - The token doesn't have 'Design systems' scopes");
472
+ console.error(" - Required scopes:");
473
+ console.error(" • Read variables in accessible files");
474
+ console.error(
475
+ " • Create and modify variables in accessible files",
476
+ );
477
+ console.error(
478
+ " - For Enterprise orgs, variables API may be restricted",
479
+ );
480
+ throw error;
481
+ }
482
+
483
+ // Test 4: Check required scopes (informational)
484
+ console.log("4️⃣ Scope requirements:");
485
+ console.log(" Required scopes for this script:");
486
+ console.log(" ✓ Read variables in accessible files");
487
+ console.log(" ✓ Create and modify variables in accessible files");
488
+ console.log(
489
+ "\n ℹ️ Note: We cannot verify scopes via API, but the tests above",
490
+ );
491
+ console.log(" will fail if scopes are missing.\n");
492
+
493
+ console.log("✅ All tests passed! Your setup is ready for syncing.\n");
494
+ console.log("💡 Run 'pnpm figma:sync' to start syncing tokens.\n");
495
+ } catch (error) {
496
+ console.error("\n❌ Test failed:", error.message);
497
+ if (error.stack && process.env.DEBUG) {
498
+ console.error("\nStack trace:");
499
+ console.error(error.stack);
500
+ }
501
+ process.exit(1);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Validate token files without syncing
507
+ * @param {string} [fileKey] - Optional fileKey to override the default or env var
508
+ * @param {Object} [options] - Validation options
509
+ * @param {boolean} [options.verbose=false] - Enable verbose logging
510
+ */
511
+ async function validateTokens(fileKey = null, options = {}) {
512
+ const { verbose = false } = options;
513
+ console.log("🔍 Validating token files...\n");
514
+
515
+ try {
516
+ // Validate fileKey if provided
517
+ if (fileKey) {
518
+ validateFileKey(fileKey);
519
+ }
520
+
521
+ // Load configuration (skip validation since we're just validating tokens)
522
+ loadConfig(fileKey, { skipValidation: true });
523
+
524
+ // Detect themes
525
+ console.log("🎨 Detecting themes...");
526
+ const themes = detectThemes();
527
+ console.log(` Found themes: ${themes.join(", ")}\n`);
528
+
529
+ if (themes.length === 0) {
530
+ throw new Error("No themes found in src/theme/ directory");
531
+ }
532
+
533
+ let totalTokens = 0;
534
+ const errors = [];
535
+
536
+ // Validate each theme
537
+ for (const themeName of themes) {
538
+ console.log(`📂 Validating theme: ${themeName}`);
539
+ const tokenFiles = loadThemeTokenFiles(themeName);
540
+
541
+ if (tokenFiles.length === 0) {
542
+ console.log(` ⚠️ No token files found for theme "${themeName}"\n`);
543
+ continue;
544
+ }
545
+
546
+ // Parse tokens
547
+ const allTokens = [];
548
+ for (const filePath of tokenFiles) {
549
+ try {
550
+ const tokenData = parseTokenFile(filePath);
551
+ const flattened = flattenTokenTree(tokenData, [], [], filePath);
552
+ allTokens.push(...flattened);
553
+ } catch (error) {
554
+ errors.push(`Error parsing ${filePath}: ${error.message}`);
555
+ }
556
+ }
557
+
558
+ // Build token registry
559
+ const tokenRegistry = new Map();
560
+ for (const tokenData of allTokens) {
561
+ const variableName = pathToVariableName(tokenData.path);
562
+ tokenRegistry.set(variableName, {
563
+ ...tokenData,
564
+ variableName,
565
+ });
566
+ }
567
+
568
+ // Resolve primitive references
569
+ try {
570
+ const resolvedTokens = resolvePrimitiveReferences(tokenRegistry);
571
+ totalTokens += resolvedTokens.size;
572
+ console.log(` ✅ Validated ${resolvedTokens.size} tokens\n`);
573
+ } catch (error) {
574
+ errors.push(
575
+ `Error resolving references in ${themeName}: ${error.message}`,
576
+ );
577
+ }
578
+ }
579
+
580
+ // Summary
581
+ console.log("\n📊 Validation Summary:");
582
+ console.log(` ✅ Total tokens: ${totalTokens}`);
583
+ if (errors.length > 0) {
584
+ console.log(` ❌ Errors: ${errors.length}`);
585
+ errors.forEach((err) => {
586
+ console.log(` - ${err}`);
587
+ });
588
+ process.exit(1);
589
+ } else {
590
+ console.log(" ✅ All tokens validated successfully!\n");
591
+ }
592
+ } catch (error) {
593
+ console.error("\n❌ Validation failed:", error.message);
594
+ if (error.stack && (process.env.DEBUG || verbose)) {
595
+ console.error("\nStack trace:");
596
+ console.error(error.stack);
597
+ }
598
+ process.exit(1);
599
+ }
600
+ }
601
+
602
+ // Run if called directly
603
+ if (require.main === module) {
604
+ try {
605
+ const args = process.argv.slice(2);
606
+
607
+ // Handle help flag
608
+ if (args.includes("--help") || args.includes("-h")) {
609
+ printHelp();
610
+ process.exit(0);
611
+ }
612
+
613
+ // Parse CLI arguments
614
+ const options = parseCliArgs(args);
615
+
616
+ // Set verbose mode in environment if requested
617
+ if (options.verbose) {
618
+ process.env.VERBOSE = "1";
619
+ }
620
+
621
+ // Determine fileKey: explicit fileKey or default (FIGMA_PROD_FILE_KEY)
622
+ const fileKey = options.fileKey || FIGMA_PROD_FILE_KEY;
623
+
624
+ // Execute command
625
+ if (options.command === "test") {
626
+ testFigmaAccess(fileKey, {
627
+ full: options.full,
628
+ verbose: options.verbose,
629
+ });
630
+ } else if (options.command === "test:file") {
631
+ if (!fileKey) {
632
+ console.error("❌ Error: fileKey is required for test:file command");
633
+ console.error(
634
+ "\nUsage: pnpm figma:test:file <fileKey> [--full] [--verbose]",
635
+ );
636
+ process.exit(1);
637
+ }
638
+ testFigmaAccess(fileKey, {
639
+ full: options.full,
640
+ verbose: options.verbose,
641
+ });
642
+ } else if (options.command === "validate") {
643
+ validateTokens(fileKey, { verbose: options.verbose });
644
+ } else if (options.command === "file") {
645
+ // Legacy file command
646
+ if (!fileKey) {
647
+ console.error("❌ Error: fileKey is required for 'file' command");
648
+ console.error("\nUsage:");
649
+ console.error(
650
+ " pnpm figma:sync:file <fileKey> - Sync to specified file",
651
+ );
652
+ console.error(
653
+ " pnpm figma:test:file <fileKey> - Test access to specified file",
654
+ );
655
+ process.exit(1);
656
+ }
657
+ // Check if there's a subcommand
658
+ const remainingArgs = args.slice(args.indexOf("file") + 2);
659
+ if (remainingArgs.includes("test") || remainingArgs.includes("--test")) {
660
+ testFigmaAccess(fileKey, { verbose: options.verbose });
661
+ } else {
662
+ syncToFigma(fileKey, {
663
+ dryRun: options.dryRun,
664
+ verbose: options.verbose,
665
+ });
666
+ }
667
+ } else {
668
+ // Default to sync command
669
+ syncToFigma(fileKey, {
670
+ dryRun: options.dryRun,
671
+ verbose: options.verbose,
672
+ });
673
+ }
674
+ } catch (error) {
675
+ if (
676
+ error.message.includes("Missing value") ||
677
+ error.message.includes("requires")
678
+ ) {
679
+ console.error(`❌ Error: ${error.message}`);
680
+ printHelp();
681
+ } else {
682
+ console.error(`❌ Error: ${error.message}`);
683
+ if (error.stack && process.env.DEBUG) {
684
+ console.error("\nStack trace:");
685
+ console.error(error.stack);
686
+ }
687
+ }
688
+ process.exit(1);
689
+ }
690
+ }
691
+
692
+ module.exports = { syncToFigma, testFigmaAccess, validateTokens };