@payloadcms/figma 0.0.1-alpha.55 → 0.0.1-alpha.57

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 (37) hide show
  1. package/dist/auth/crypto-utils.d.ts +6 -0
  2. package/dist/auth/crypto-utils.js +11 -9
  3. package/dist/auth/oauth-flow.d.ts +5 -0
  4. package/dist/auth/oauth-flow.js +20 -3
  5. package/dist/auth/token-store.d.ts +2 -0
  6. package/dist/auth/token-store.js +57 -13
  7. package/dist/cli.js +3 -0
  8. package/dist/commands/debug.js +15 -0
  9. package/dist/commands/deploy.js +12 -7
  10. package/dist/commands/init.js +63 -55
  11. package/dist/constants.d.ts +9 -1
  12. package/dist/constants.js +16 -1
  13. package/dist/db-content-api/index.d.ts +1 -0
  14. package/dist/db-content-api/index.js +53 -7
  15. package/dist/db-content-api/utilities/data/index.d.ts +7 -0
  16. package/dist/db-content-api/utilities/data/index.js +36 -4
  17. package/dist/db-content-api/utilities/meta/buildMeta.d.ts +5 -0
  18. package/dist/db-content-api/utilities/meta/buildMeta.js +9 -1
  19. package/dist/db-content-api/utilities/meta/buildUniquePaths.d.ts +13 -0
  20. package/dist/db-content-api/utilities/meta/buildUniquePaths.js +62 -0
  21. package/dist/db-content-api/utilities/where.js +7 -1
  22. package/dist/deploy/schedule-extract-plugin.d.ts +11 -0
  23. package/dist/deploy/schedule-extract-plugin.js +34 -0
  24. package/dist/plugin/build-config.d.ts +2 -1
  25. package/dist/plugin/build-config.js +4 -1
  26. package/dist/types.d.ts +4 -0
  27. package/dist/utils/build-lambda-zip.js +1 -1
  28. package/dist/utils/formatter.js +20 -6
  29. package/dist/utils/lambda-config.js +69 -67
  30. package/dist/utils/messages.d.ts +0 -1
  31. package/dist/utils/messages.js +1 -3
  32. package/dist/utils/payload-config-ast.d.ts +0 -5
  33. package/dist/utils/payload-config-ast.js +7 -76
  34. package/dist/utils/payload-config-modifier.js +9 -1
  35. package/dist/utils/resolve-environment.d.ts +1 -1
  36. package/dist/utils/resolve-environment.js +4 -1
  37. package/package.json +1 -2
@@ -30,4 +30,10 @@
30
30
  * @throws Error if unable to derive key due to missing machine information
31
31
  */
32
32
  export declare function deriveEncryptionKey(): string;
33
+ /**
34
+ * Get a short hash of the encryption key for diagnostics.
35
+ * Returns first 8 chars of SHA-256 hash — enough to compare across invocations
36
+ * without exposing the actual key.
37
+ */
38
+ export declare function getEncryptionKeyHash(): string;
33
39
  //# sourceMappingURL=crypto-utils.d.ts.map
@@ -33,19 +33,13 @@ import os from 'os';
33
33
  * @throws Error if unable to gather sufficient entropy
34
34
  */ function gatherMachineEntropy() {
35
35
  try {
36
- const userInfo = os.userInfo();
37
- const networkInterfaces = os.networkInterfaces();
38
- // Get MAC address from first available network interface (if available)
39
- const macAddress = Object.values(networkInterfaces).flat().find((iface)=>iface && !iface.internal && iface.mac !== '00:00:00:00:00:00')?.mac;
40
36
  const entropy = [
41
37
  os.hostname(),
42
38
  os.homedir(),
43
- userInfo.username,
39
+ os.userInfo().username,
44
40
  os.platform(),
45
- os.arch(),
46
- macAddress
47
- ].filter(Boolean) // Remove any undefined values
48
- ;
41
+ os.arch()
42
+ ];
49
43
  if (entropy.length < 4) {
50
44
  throw new Error('Insufficient machine entropy available');
51
45
  }
@@ -92,5 +86,13 @@ import os from 'os';
92
86
  const key = crypto.pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
93
87
  return key.toString('hex');
94
88
  }
89
+ /**
90
+ * Get a short hash of the encryption key for diagnostics.
91
+ * Returns first 8 chars of SHA-256 hash — enough to compare across invocations
92
+ * without exposing the actual key.
93
+ */ export function getEncryptionKeyHash() {
94
+ const key = deriveEncryptionKey();
95
+ return crypto.createHash('sha256').update(key).digest('hex').substring(0, 8);
96
+ }
95
97
 
96
98
  //# sourceMappingURL=crypto-utils.js.map
@@ -59,4 +59,9 @@ export declare function executeOAuthFlow(tokenStore: TokenStore, options?: OAuth
59
59
  * @throws OAuthFlowError if refresh fails
60
60
  */
61
61
  export declare function getValidAccessToken(tokenStore: TokenStore): Promise<null | string>;
62
+ /**
63
+ * Like getValidAccessToken but returns null instead of throwing on refresh failure.
64
+ * Use in commands that want to fall through to a login prompt on failure.
65
+ */
66
+ export declare function tryGetAccessToken(tokenStore: TokenStore): Promise<null | string>;
62
67
  //# sourceMappingURL=oauth-flow.d.ts.map
@@ -155,19 +155,36 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
155
155
  if (!refreshToken) {
156
156
  return null;
157
157
  }
158
- // Try to refresh
158
+ // Try to refresh (with one retry for transient auth errors)
159
159
  try {
160
160
  const tokens = await refreshAccessToken(refreshToken);
161
161
  tokenStore.setTokens(tokens);
162
162
  return tokens.accessToken;
163
163
  } catch (error) {
164
- // Only clear tokens for actual auth failures, not transient errors
165
164
  const isAuthFailure = error instanceof TokenRefreshError && (error.statusCode === 401 || error.statusCode === 403 || error.statusCode === 400 && error.errorCode === 'invalid_grant');
166
165
  if (isAuthFailure) {
167
- tokenStore.clearTokens();
166
+ // Retry once — a transient 401 (e.g. misconfigured proxy) shouldn't wipe tokens
167
+ try {
168
+ const tokens = await refreshAccessToken(refreshToken);
169
+ tokenStore.setTokens(tokens);
170
+ return tokens.accessToken;
171
+ } catch {
172
+ // Retry also failed — token is genuinely revoked
173
+ tokenStore.clearTokens();
174
+ }
168
175
  }
169
176
  throw new OAuthFlowError('Failed to refresh access token', error instanceof Error ? error : undefined);
170
177
  }
171
178
  }
179
+ /**
180
+ * Like getValidAccessToken but returns null instead of throwing on refresh failure.
181
+ * Use in commands that want to fall through to a login prompt on failure.
182
+ */ export async function tryGetAccessToken(tokenStore) {
183
+ try {
184
+ return await getValidAccessToken(tokenStore);
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
172
189
 
173
190
  //# sourceMappingURL=oauth-flow.js.map
@@ -24,7 +24,9 @@ export declare function getTokenStore(environment?: Environment): TokenStore;
24
24
  */
25
25
  export declare class TokenStore {
26
26
  private config;
27
+ private environment;
27
28
  constructor(options?: TokenStoreConfig);
29
+ private safeGet;
28
30
  /**
29
31
  * Retrieve stored tokens
30
32
  * @returns FigmaTokens if stored, null otherwise
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable perfectionist/sort-classes */ import Conf from 'conf';
2
+ import fsSync from 'node:fs';
2
3
  import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
3
4
  import { getInfraEnvironment } from '../constants.js';
5
+ import * as log from '../utils/log.js';
4
6
  import { deriveEncryptionKey } from './crypto-utils.js';
5
7
  /**
6
8
  * Environment-keyed instances for singleton pattern
@@ -34,24 +36,57 @@ import { deriveEncryptionKey } from './crypto-utils.js';
34
36
  * The constructor is still exported for test isolation.
35
37
  */ export class TokenStore {
36
38
  config;
39
+ environment;
37
40
  constructor(options){
38
41
  if (process.env.AWS_EXECUTION_ENV) {
39
42
  throw new Error('TokenStore cannot be used in AWS Lambda environments');
40
43
  }
41
44
  const environment = options?.environment ?? 'production';
42
45
  const projectName = environment === 'production' ? 'payloadcms-figma' : `payloadcms-figma-${environment}`;
43
- this.config = new Conf({
44
- clearInvalidConfig: true,
46
+ this.environment = environment;
47
+ const confOptions = {
48
+ clearInvalidConfig: false,
45
49
  configName: options?.configName || projectName,
46
50
  encryptionKey: options?.encryptionKey || deriveEncryptionKey(),
47
51
  projectName
48
- });
52
+ };
53
+ try {
54
+ this.config = new Conf(confOptions);
55
+ } catch (error) {
56
+ // Deserialization failed (e.g. encryption key changed).
57
+ // Log diagnostics, clear the corrupted file, then create a fresh store.
58
+ if (environment === 'staging') {
59
+ log.warning(`Token store corrupted, resetting. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
60
+ }
61
+ // Temporarily allow clearing so Conf can reinitialize
62
+ this.config = new Conf({
63
+ ...confOptions,
64
+ clearInvalidConfig: true
65
+ });
66
+ // Force a write so the corrupt file is replaced with an empty store.
67
+ // Without this, clearInvalidConfig only ignores bad reads in memory
68
+ // and the corrupt file persists, triggering this fallback every time.
69
+ this.config.clear();
70
+ }
71
+ }
72
+ safeGet(key) {
73
+ try {
74
+ return this.config.get(key);
75
+ } catch (error) {
76
+ if (this.environment === 'staging') {
77
+ const filePath = this.config.path;
78
+ const fileExists = fsSync.existsSync(filePath);
79
+ const fileSize = fileExists ? fsSync.statSync(filePath).size : 0;
80
+ log.warning(`Token store read failed (key: ${key}). ` + `File: ${filePath}, exists: ${fileExists}, size: ${fileSize}B. ` + `Error: ${error instanceof Error ? error.message : 'Unknown'}`);
81
+ }
82
+ return undefined;
83
+ }
49
84
  }
50
85
  /**
51
86
  * Retrieve stored tokens
52
87
  * @returns FigmaTokens if stored, null otherwise
53
88
  */ getTokens() {
54
- const tokens = this.config.get('tokens');
89
+ const tokens = this.safeGet('tokens');
55
90
  return tokens || null;
56
91
  }
57
92
  /**
@@ -59,6 +94,16 @@ import { deriveEncryptionKey } from './crypto-utils.js';
59
94
  * @param tokens - Figma OAuth2 tokens to store
60
95
  */ setTokens(tokens) {
61
96
  this.config.set('tokens', tokens);
97
+ // Verify the round-trip: read back what we just wrote
98
+ const readBack = this.safeGet('tokens');
99
+ if (!readBack || readBack.accessToken !== tokens.accessToken) {
100
+ if (this.environment === 'staging') {
101
+ const filePath = this.config.path;
102
+ const fileExists = fsSync.existsSync(filePath);
103
+ const fileSize = fileExists ? fsSync.statSync(filePath).size : 0;
104
+ log.warning(`Token verify-after-write FAILED. ` + `File: ${filePath}, exists: ${fileExists}, size: ${fileSize}B. ` + `Written accessToken starts: ${tokens.accessToken.substring(0, 10)}..., ` + `Read back: ${readBack ? readBack.accessToken.substring(0, 10) + '...' : 'null'}`);
105
+ }
106
+ }
62
107
  }
63
108
  /**
64
109
  * Clear all stored tokens (used for logout)
@@ -80,8 +125,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
80
125
  * Includes a buffer time to avoid using tokens that are about to expire
81
126
  * @returns true if token is expired or will expire within buffer time
82
127
  */ isExpired() {
83
- // Access config directly to avoid circular dependency with getTokens()
84
- const tokens = this.config.get('tokens');
128
+ const tokens = this.safeGet('tokens');
85
129
  if (!tokens) {
86
130
  return true;
87
131
  }
@@ -136,7 +180,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
136
180
  * @param tenantId - The tenant/CMS ID
137
181
  * @returns ProjectToken if stored and valid, null otherwise
138
182
  */ getProjectToken(tenantId) {
139
- const projectTokens = this.config.get('projectTokens') || {};
183
+ const projectTokens = this.safeGet('projectTokens') || {};
140
184
  const projectToken = projectTokens[tenantId];
141
185
  if (!projectToken) {
142
186
  return null;
@@ -153,7 +197,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
153
197
  * @param tenantId - The tenant/CMS ID
154
198
  * @param projectToken - The project token to store
155
199
  */ setProjectToken(tenantId, projectToken) {
156
- const projectTokens = this.config.get('projectTokens') || {};
200
+ const projectTokens = this.safeGet('projectTokens') || {};
157
201
  projectTokens[tenantId] = projectToken;
158
202
  this.config.set('projectTokens', projectTokens);
159
203
  }
@@ -161,7 +205,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
161
205
  * Clear a project token for a specific tenant
162
206
  * @param tenantId - The tenant/CMS ID
163
207
  */ clearProjectToken(tenantId) {
164
- const projectTokens = this.config.get('projectTokens') || {};
208
+ const projectTokens = this.safeGet('projectTokens') || {};
165
209
  delete projectTokens[tenantId];
166
210
  this.config.set('projectTokens', projectTokens);
167
211
  }
@@ -204,7 +248,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
204
248
  * Get all tenant IDs that have stored project tokens
205
249
  * @returns Array of tenant IDs
206
250
  */ getAllProjectTokenTenantIds() {
207
- const projectTokens = this.config.get('projectTokens') || {};
251
+ const projectTokens = this.safeGet('projectTokens') || {};
208
252
  return Object.keys(projectTokens);
209
253
  }
210
254
  /**
@@ -221,7 +265,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
221
265
  * @param environmentName - The environment name
222
266
  * @returns BootstrapData if stored, null otherwise
223
267
  */ getBootstrapData(projectId, environmentName) {
224
- const allData = this.config.get('bootstrapData') || {};
268
+ const allData = this.safeGet('bootstrapData') || {};
225
269
  return allData[this.bootstrapKey(projectId, environmentName)] || null;
226
270
  }
227
271
  /**
@@ -230,7 +274,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
230
274
  * @param environmentName - The environment name
231
275
  * @param data - The bootstrap data to store
232
276
  */ setBootstrapData(projectId, environmentName, data) {
233
- const allData = this.config.get('bootstrapData') || {};
277
+ const allData = this.safeGet('bootstrapData') || {};
234
278
  allData[this.bootstrapKey(projectId, environmentName)] = data;
235
279
  this.config.set('bootstrapData', allData);
236
280
  }
@@ -239,7 +283,7 @@ import { deriveEncryptionKey } from './crypto-utils.js';
239
283
  * @param projectId - The CMS resource/project ID
240
284
  * @param environmentName - The environment name
241
285
  */ clearBootstrapData(projectId, environmentName) {
242
- const allData = this.config.get('bootstrapData') || {};
286
+ const allData = this.safeGet('bootstrapData') || {};
243
287
  delete allData[this.bootstrapKey(projectId, environmentName)];
244
288
  this.config.set('bootstrapData', allData);
245
289
  }
package/dist/cli.js CHANGED
@@ -43,8 +43,11 @@ class Main {
43
43
  '--version': Boolean,
44
44
  '--yes': Boolean,
45
45
  // Aliases
46
+ '--environment': '--env',
47
+ '-e': '--env',
46
48
  '-f': '--force',
47
49
  '-h': '--help',
50
+ '-n': '--name',
48
51
  '-v': '--version',
49
52
  '-y': '--yes'
50
53
  }, {
@@ -2,6 +2,7 @@ import fsSync from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import { getEncryptionKeyHash } from '../auth/crypto-utils.js';
5
6
  import { getTokenStore } from '../auth/token-store.js';
6
7
  import { getInfraEnvironment } from '../constants.js';
7
8
  import { getEnvVarSync } from '../utils/env-management.js';
@@ -22,6 +23,7 @@ import { getOwnVersion } from '../utils/version-check.js';
22
23
  ];
23
24
  appendSystemSection(lines, env);
24
25
  appendAuthSection(lines, tokenStore, cwd);
26
+ appendTokenStoreDiagnostics(lines, tokenStore);
25
27
  appendProjectSection(lines, tokenStore, cwd);
26
28
  await appendPackagesSection(lines);
27
29
  // eslint-disable-next-line no-console
@@ -52,6 +54,19 @@ function appendAuthSection(lines, tokenStore, cwd) {
52
54
  lines.push(` Token Expires: ${formatExpiryTime(tokens.expiresAt)}`);
53
55
  lines.push('');
54
56
  }
57
+ function appendTokenStoreDiagnostics(lines, tokenStore) {
58
+ lines.push('Token Store:');
59
+ const filePath = tokenStore.getStoragePath();
60
+ const fileExists = fsSync.existsSync(filePath);
61
+ const fileSize = fileExists ? fsSync.statSync(filePath).size : 0;
62
+ lines.push(` File: ${filePath}`);
63
+ lines.push(` File Exists: ${fileExists}`);
64
+ lines.push(` File Size: ${fileSize}B`);
65
+ const canRead = tokenStore.getTokens() !== null || tokenStore.getRefreshToken() !== null;
66
+ lines.push(` Decryption: ${fileExists ? canRead || fileSize === 0 ? 'OK' : 'FAILED' : 'N/A'}`);
67
+ lines.push(` Key Hash: ${getEncryptionKeyHash()}`);
68
+ lines.push('');
69
+ }
55
70
  function appendProjectSection(lines, tokenStore, cwd) {
56
71
  const projectId = getEnvVarSync(cwd, 'FIGMA_PROJECT_ID');
57
72
  const environmentName = getEnvVarSync(cwd, 'FIGMA_ENVIRONMENT_NAME');
@@ -3,9 +3,9 @@ import spawn from 'cross-spawn';
3
3
  import path from 'path';
4
4
  import pc from 'picocolors';
5
5
  import { ControlPlaneError, createDeployment, getBootstrapInfo, performDeployment } from '../api/control-plane.js';
6
- import { getValidAccessToken } from '../auth/oauth-flow.js';
6
+ import { tryGetAccessToken } from '../auth/oauth-flow.js';
7
7
  import { getTokenStore } from '../auth/token-store.js';
8
- import { CONTENT_SYSTEM_NOT_FOUND_MESSAGE } from '../constants.js';
8
+ import { getInfraEnvironment, getProjectNotFoundMessage } from '../constants.js';
9
9
  import { collectStaticAssets, createLambdaZip, getFileSize } from '../utils/asset-collection.js';
10
10
  import { detectBuild, getBuildCommand } from '../utils/build-detection.js';
11
11
  import { getEnvVar } from '../utils/env-management.js';
@@ -71,11 +71,11 @@ import { loginCommand } from './login.js';
71
71
  }
72
72
  // Check authentication
73
73
  const tokenStore = getTokenStore();
74
- let accessToken = await getValidAccessToken(tokenStore);
74
+ let accessToken = await tryGetAccessToken(tokenStore);
75
75
  if (!accessToken) {
76
76
  p.log.message('Please log in to continue');
77
77
  await loginCommand();
78
- accessToken = await getValidAccessToken(tokenStore);
78
+ accessToken = await tryGetAccessToken(tokenStore);
79
79
  if (!accessToken) {
80
80
  p.log.error(pc.red('✗ Authentication failed'));
81
81
  process.exit(1);
@@ -94,14 +94,19 @@ import { loginCommand } from './login.js';
94
94
  spinner.stop(pc.green(`✓ Target: ${resolvedEnv.name}`));
95
95
  } catch (error) {
96
96
  if (error instanceof ControlPlaneError && (error.statusCode === 404 || error.statusCode === 410)) {
97
- spinner.stop(pc.red('✗ Content system not found'));
98
- p.log.error(pc.red(CONTENT_SYSTEM_NOT_FOUND_MESSAGE));
97
+ spinner.stop(pc.red('✗ Project not found'));
98
+ p.log.error(pc.red(getProjectNotFoundMessage({
99
+ environment: getInfraEnvironment(),
100
+ projectId
101
+ })));
99
102
  process.exit(1);
100
103
  }
101
104
  spinner.stop(pc.red('✗ Failed to resolve deployment target'));
102
105
  p.log.error(error instanceof Error ? error.message : 'Unknown error');
103
106
  process.exit(1);
104
107
  }
108
+ // Signal buildFigmaConfig to extract schedules during build
109
+ process.env.FIGMA_EXTRACT_SCHEDULES = 'true';
105
110
  // ===== BUILD =====
106
111
  let buildInfo;
107
112
  if (options.skipBuild) {
@@ -179,7 +184,7 @@ import { loginCommand } from './login.js';
179
184
  buildInfo = await detectBuild(projectPath);
180
185
  if (!buildInfo) {
181
186
  p.log.error(pc.red('✗ Build completed but standalone build not found'));
182
- p.note('Ensure next.config.js has output: "standalone"', 'Check Lambda Configuration');
187
+ p.note('Ensure next.config has output: "standalone"', 'Check Lambda Configuration');
183
188
  process.exit(1);
184
189
  }
185
190
  }
@@ -4,10 +4,10 @@ import path from 'path';
4
4
  import pc from 'picocolors';
5
5
  import { ControlPlaneError, getBootstrapInfo } from '../api/control-plane.js';
6
6
  import { FigmaApiError } from '../api/figma-api.js';
7
- import { getValidAccessToken } from '../auth/oauth-flow.js';
7
+ import { tryGetAccessToken } from '../auth/oauth-flow.js';
8
8
  import { getValidProjectToken, ProjectTokenError } from '../auth/project-token.js';
9
9
  import { getTokenStore } from '../auth/token-store.js';
10
- import { CONTENT_SYSTEM_NOT_FOUND_MESSAGE, getInfraEnvironment } from '../constants.js';
10
+ import { getInfraEnvironment, getProjectNotFoundMessage } from '../constants.js';
11
11
  import { ensureGitignore } from '../utils/config.js';
12
12
  import { addOrUpdateEnvVar } from '../utils/env-management.js';
13
13
  import { isDebug } from '../utils/is-debug.js';
@@ -77,33 +77,24 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
77
77
  // Skip authentication for testing/development
78
78
  p.log.warn(pc.yellow('Skipping authentication (--skip-auth mode)'));
79
79
  } else {
80
- try {
81
- // Check for valid cached tokens first (no API call needed)
82
- if (!tokenStore.hasValidTokens()) {
83
- // Need to refresh or authenticate - show spinner for API call
84
- s.start('Checking authentication...');
85
- const token = await getValidAccessToken(tokenStore);
86
- if (!token) {
87
- s.stop('Authentication required to continue');
88
- // Run login command
89
- await loginCommand({
90
- showNextSteps: false
91
- });
92
- // Get token after auth
93
- const newToken = await getValidAccessToken(tokenStore);
94
- if (!newToken) {
95
- p.log.error('Authentication failed');
96
- process.exit(1);
97
- }
98
- } else {
99
- s.stop(pc.green('✓ Authenticated'));
80
+ // Check for valid cached tokens first (no API call needed)
81
+ if (!tokenStore.hasValidTokens()) {
82
+ // Need to refresh or authenticate - show spinner for API call
83
+ s.start('Checking authentication...');
84
+ const token = await tryGetAccessToken(tokenStore);
85
+ if (!token) {
86
+ s.stop('Authentication required to continue');
87
+ await loginCommand({
88
+ showNextSteps: false
89
+ });
90
+ const newToken = await tryGetAccessToken(tokenStore);
91
+ if (!newToken) {
92
+ p.log.error('Authentication failed');
93
+ process.exit(1);
100
94
  }
95
+ } else {
96
+ s.stop(pc.green('✓ Authenticated'));
101
97
  }
102
- } catch (error) {
103
- s.stop(pc.red('✗ Authentication failed'));
104
- log.error(error instanceof Error ? error.message : 'Unknown error');
105
- p.note('Run `figma auth` to authenticate', 'Tip');
106
- process.exit(1);
107
98
  }
108
99
  }
109
100
  // Note: We no longer check for figma.config.json here
@@ -122,21 +113,17 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
122
113
  let oauthClientId = null;
123
114
  let oauthClientSecret = null;
124
115
  if (!options.skipAuth) {
125
- const accessToken = await getValidAccessToken(tokenStore);
116
+ const accessToken = await tryGetAccessToken(tokenStore);
126
117
  if (accessToken) {
127
118
  try {
128
119
  bootstrapInfo = await getBootstrapInfo(accessToken, cmsResourceId);
129
- const envName = options.env ?? process.env.FIGMA_ENVIRONMENT_NAME ?? undefined;
130
- resolvedEnv = resolveEnvironment({
131
- environmentName: envName,
132
- environments: bootstrapInfo.environments
133
- });
134
- oauthClientId = bootstrapInfo.oauthCredentials.oauthClientId;
135
- oauthClientSecret = bootstrapInfo.oauthCredentials.oauthClientSecret;
136
120
  } catch (error) {
137
121
  log.debug(`Control Plane Error: ${error.statusCode} - ${error.message}`);
138
122
  if (error instanceof ControlPlaneError && (error.statusCode === 404 || error.statusCode === 410)) {
139
- p.log.error(pc.red(CONTENT_SYSTEM_NOT_FOUND_MESSAGE));
123
+ p.log.error(pc.red(getProjectNotFoundMessage({
124
+ environment: getInfraEnvironment(),
125
+ projectId: cmsResourceId
126
+ })));
140
127
  process.exit(1);
141
128
  }
142
129
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -144,6 +131,18 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
144
131
  p.log.error(pc.dim(`Details: ${message}`));
145
132
  process.exit(1);
146
133
  }
134
+ try {
135
+ const envName = options.env ?? process.env.FIGMA_ENVIRONMENT_NAME ?? undefined;
136
+ resolvedEnv = resolveEnvironment({
137
+ environmentName: envName,
138
+ environments: bootstrapInfo.environments
139
+ });
140
+ oauthClientId = bootstrapInfo.oauthCredentials.oauthClientId;
141
+ oauthClientSecret = bootstrapInfo.oauthCredentials.oauthClientSecret;
142
+ } catch (error) {
143
+ p.log.error(pc.red(error instanceof Error ? error.message : 'Unknown error'));
144
+ process.exit(1);
145
+ }
147
146
  }
148
147
  }
149
148
  // Step 4: Detect or scaffold Payload project
@@ -238,7 +237,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
238
237
  process.exit(1);
239
238
  }
240
239
  // Generate import map to prevent errors on first dev run
241
- await runScript(process.cwd(), 'generate:importmap', packageManager);
240
+ const importMapResult = await runScript(process.cwd(), 'generate:importmap', packageManager);
241
+ if (!importMapResult) {
242
+ p.log.warn(pc.yellow('⚠ Import map generation failed — it will be generated on first dev run'));
243
+ }
242
244
  // Apply Lambda modifications (run.sh, standalone output, lambda:buildzip script)
243
245
  await applyLambdaModifications(process.cwd(), packageManager);
244
246
  // Ensure .gitignore has required entries
@@ -260,27 +262,30 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
260
262
  } else {
261
263
  // ===== NEW PROJECT FLOW =====
262
264
  // p.log.info('No project detected - Creating New Project')
263
- // Prompt for path
264
- const projectPathInput = await p.text({
265
- initialValue: './',
266
- message: 'Enter path to create project:',
267
- placeholder: './my-cms-project',
268
- validate: (value)=>{
269
- if (!value) {
270
- return 'Path is required';
265
+ let projectPath;
266
+ if (options.name) {
267
+ projectPath = options.name;
268
+ } else {
269
+ const projectPathInput = await p.text({
270
+ initialValue: './',
271
+ message: 'Enter path to create project:',
272
+ placeholder: './my-cms-project',
273
+ validate: (value)=>{
274
+ if (!value) {
275
+ return 'Path is required';
276
+ }
277
+ // Allow relative or absolute paths
278
+ return undefined;
271
279
  }
272
- // Allow relative or absolute paths
273
- return undefined;
280
+ });
281
+ if (p.isCancel(projectPathInput)) {
282
+ p.cancel('Operation cancelled');
283
+ process.exit(0);
274
284
  }
275
- });
276
- if (p.isCancel(projectPathInput)) {
277
- p.cancel('Operation cancelled');
278
- process.exit(0);
285
+ projectPath = projectPathInput;
279
286
  }
280
- const projectPath = projectPathInput;
281
287
  const fullPath = path.resolve(process.cwd(), projectPath);
282
- // Get project name from path or prompt
283
- const projectName = options.name || path.basename(fullPath);
288
+ const projectName = path.basename(fullPath);
284
289
  const packageManager = detectPackageManagerFromEnvironment();
285
290
  // Create directory
286
291
  try {
@@ -346,7 +351,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
346
351
  p.log.warn(pc.yellow(`⚠ Failed to update .env file: ${error instanceof Error ? error.message : 'Unknown error'}`));
347
352
  }
348
353
  // Generate import map to prevent errors on first dev run
349
- await runScript(fullPath, 'generate:importmap', packageManager);
354
+ const importMapResult = await runScript(fullPath, 'generate:importmap', packageManager);
355
+ if (!importMapResult) {
356
+ p.log.warn(pc.yellow('⚠ Import map generation failed — it will be generated on first dev run'));
357
+ }
350
358
  // Generate project token (unless skipping auth)
351
359
  if (!options.skipAuth && resolvedEnv) {
352
360
  await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s);
@@ -23,9 +23,17 @@ type EnvironmentConfig = {
23
23
  export declare const ENV_CONFIG: Record<Environment, EnvironmentConfig>;
24
24
  /**
25
25
  * User-facing error message when Content API returns 404 or 410.
26
- * Used across CLI commands and the db adapter for consistent messaging.
26
+ * Used by the db adapter for consistent messaging at runtime.
27
27
  */
28
28
  export declare const CONTENT_SYSTEM_NOT_FOUND_MESSAGE = "Content system not found. Verify your Content System ID is correct, or contact your team admin for assistance.";
29
+ /**
30
+ * Build a user-facing error message for CLI commands when a project is not found (404/410).
31
+ * Includes infra-env context and actionable suggestions.
32
+ */
33
+ export declare function getProjectNotFoundMessage(params: {
34
+ environment: Environment;
35
+ projectId: string;
36
+ }): string;
29
37
  /**
30
38
  * Set infrastructure environment override (used by --infra-env CLI flag)
31
39
  * Pass undefined to clear the override
package/dist/constants.js CHANGED
@@ -22,8 +22,22 @@ import { getEnvVarSync } from './utils/env-management.js';
22
22
  };
23
23
  /**
24
24
  * User-facing error message when Content API returns 404 or 410.
25
- * Used across CLI commands and the db adapter for consistent messaging.
25
+ * Used by the db adapter for consistent messaging at runtime.
26
26
  */ export const CONTENT_SYSTEM_NOT_FOUND_MESSAGE = 'Content system not found. Verify your Content System ID is correct, or contact your team admin for assistance.';
27
+ /**
28
+ * Build a user-facing error message for CLI commands when a project is not found (404/410).
29
+ * Includes infra-env context and actionable suggestions.
30
+ */ export function getProjectNotFoundMessage(params) {
31
+ const { environment, projectId } = params;
32
+ return [
33
+ `Project not found on ${environment} (ID: ${projectId}).`,
34
+ '',
35
+ 'Suggestions:',
36
+ ' • Verify your project ID is correct',
37
+ " • Ensure you're logged into the correct Figma account",
38
+ ' • Contact your team admin for assistance'
39
+ ].join('\n');
40
+ }
27
41
  let envOverride;
28
42
  /**
29
43
  * Set infrastructure environment override (used by --infra-env CLI flag)
@@ -55,6 +69,7 @@ export function getInfraEnvironment() {
55
69
  return 'staging';
56
70
  }
57
71
  if (env !== 'production') {
72
+ // eslint-disable-next-line no-console
58
73
  console.warn(`Warning: Invalid FIGMA_INFRA_ENV value "${envFileValue}" in .env file. Using production.`);
59
74
  }
60
75
  return 'production';
@@ -13,6 +13,7 @@ export type ContentAPIAdapter = {
13
13
  clearDatabase: () => Promise<void>;
14
14
  client: ReturnType<typeof createClient<paths>>;
15
15
  contentSystemId: string;
16
+ idType: 'uuid';
16
17
  url: string;
17
18
  } & BaseDatabaseAdapter;
18
19
  export declare const contentAPIAdapter: (opts: ContentAPIOptions) => DatabaseAdapterObj;