@payloadcms/figma 0.0.1-alpha.64 → 0.0.1-alpha.65

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 (40) hide show
  1. package/dist/auth/callback-server.d.ts +19 -7
  2. package/dist/auth/callback-server.js +72 -31
  3. package/dist/auth/crypto-utils.d.ts +11 -0
  4. package/dist/auth/crypto-utils.js +22 -1
  5. package/dist/auth/oauth-flow.d.ts +3 -1
  6. package/dist/auth/oauth-flow.js +13 -6
  7. package/dist/auth/token-store.d.ts +2 -0
  8. package/dist/auth/token-store.js +41 -1
  9. package/dist/auth/types.d.ts +7 -0
  10. package/dist/cli.js +6 -1
  11. package/dist/commands/init.d.ts +4 -0
  12. package/dist/commands/init.js +44 -2
  13. package/dist/config/oauth.d.ts +2 -1
  14. package/dist/config/oauth.js +7 -1
  15. package/dist/db-content-api/generated/content-api-types.d.ts +6 -0
  16. package/dist/db-content-api/index.d.ts +2 -0
  17. package/dist/db-content-api/index.js +9 -1
  18. package/dist/lib/download-skill.d.ts +13 -0
  19. package/dist/lib/download-skill.js +79 -0
  20. package/dist/oauth/endpoints/getLoginEndpoint.js +26 -107
  21. package/dist/oauth/endpoints/getTokenLoginEndpoint.d.ts +17 -0
  22. package/dist/oauth/endpoints/getTokenLoginEndpoint.js +105 -0
  23. package/dist/oauth/index.js +8 -0
  24. package/dist/oauth/utilities/establishSession.d.ts +23 -0
  25. package/dist/oauth/utilities/establishSession.js +82 -0
  26. package/dist/oauth/utilities/exchangeCodeForAccessToken.d.ts +24 -0
  27. package/dist/oauth/utilities/exchangeCodeForAccessToken.js +28 -0
  28. package/dist/oauth/utilities/isAbsoluteURL.d.ts +2 -0
  29. package/dist/oauth/utilities/isAbsoluteURL.js +3 -0
  30. package/dist/plugin/build-config.js +3 -0
  31. package/dist/types.d.ts +2 -0
  32. package/dist/utils/download-template.d.ts +9 -1
  33. package/dist/utils/download-template.js +24 -19
  34. package/dist/utils/messages.js +2 -0
  35. package/dist/utils/parse-template-spec.d.ts +12 -0
  36. package/dist/utils/parse-template-spec.js +62 -0
  37. package/dist/utils/project.d.ts +2 -1
  38. package/dist/utils/project.js +2 -2
  39. package/package.json +9 -1
  40. package/dist/db-content-api/README.md +0 -98
@@ -7,6 +7,14 @@ export interface CallbackResult {
7
7
  /** State parameter for CSRF verification */
8
8
  state: string;
9
9
  }
10
+ export type CallbackServerOptions = {
11
+ /** Additional callback ports to try after port. */
12
+ fallbackPorts?: number[];
13
+ /** First callback port to try. Defaults to the configured callback port range. */
14
+ port?: number;
15
+ /** Callback timeout in milliseconds. */
16
+ timeoutMs?: number;
17
+ };
10
18
  /**
11
19
  * Error from OAuth callback
12
20
  */
@@ -20,18 +28,18 @@ export declare class CallbackError extends Error {
20
28
  * Starts a temporary server on localhost to receive the OAuth authorization code.
21
29
  * The server automatically shuts down after receiving the callback or on timeout.
22
30
  *
23
- * Implements dynamic port allocation: tries preferred port first, falls back to
24
- * OS-assigned port if unavailable.
31
+ * Tries the registered localhost callback ports in order. It does not use an
32
+ * OS-assigned random port because OAuth redirect URIs must be registered.
25
33
  */
26
34
  export declare class CallbackServer {
27
35
  private actualPort?;
28
- private preferredPort;
36
+ private callbackPorts;
37
+ private readyPromise;
38
+ private rejectReady?;
39
+ private resolveReady?;
29
40
  private server;
30
41
  private timeoutMs;
31
- constructor(options?: {
32
- port?: number;
33
- timeoutMs?: number;
34
- });
42
+ constructor(options?: CallbackServerOptions);
35
43
  /**
36
44
  * Render error page shown in browser when auth fails
37
45
  */
@@ -57,5 +65,9 @@ export declare class CallbackServer {
57
65
  * @throws CallbackError if callback fails or times out
58
66
  */
59
67
  waitForCallback(expectedState: string): Promise<CallbackResult>;
68
+ /**
69
+ * Wait until the callback server is listening.
70
+ */
71
+ waitUntilReady(): Promise<number>;
60
72
  }
61
73
  //# sourceMappingURL=callback-server.d.ts.map
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs';
2
2
  import { createServer } from 'http';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
- import { DEFAULT_CALLBACK_PORT } from '../config/oauth.js';
5
+ import { DEFAULT_CALLBACK_PORT, DEFAULT_CALLBACK_PORTS } from '../config/oauth.js';
6
6
  const filename = fileURLToPath(import.meta.url);
7
7
  const dirname = path.dirname(filename);
8
8
  /**
@@ -20,17 +20,19 @@ const dirname = path.dirname(filename);
20
20
  * Starts a temporary server on localhost to receive the OAuth authorization code.
21
21
  * The server automatically shuts down after receiving the callback or on timeout.
22
22
  *
23
- * Implements dynamic port allocation: tries preferred port first, falls back to
24
- * OS-assigned port if unavailable.
23
+ * Tries the registered localhost callback ports in order. It does not use an
24
+ * OS-assigned random port because OAuth redirect URIs must be registered.
25
25
  */ export class CallbackServer {
26
26
  actualPort;
27
- preferredPort;
27
+ callbackPorts;
28
+ readyPromise = null;
29
+ rejectReady;
30
+ resolveReady;
28
31
  server = null;
29
32
  timeoutMs;
30
33
  constructor(options = {}){
31
- this.preferredPort = options.port || DEFAULT_CALLBACK_PORT;
34
+ this.callbackPorts = resolveCallbackPorts(options);
32
35
  this.timeoutMs = options.timeoutMs || 120000; // 2 minutes default
33
- this.server = createServer();
34
36
  }
35
37
  /**
36
38
  * Render error page shown in browser when auth fails
@@ -47,7 +49,7 @@ const dirname = path.dirname(filename);
47
49
  * Get the actual port the server is listening on
48
50
  * @returns Port number (may differ from preferred if that was in use)
49
51
  */ getActualPort() {
50
- return this.actualPort || this.preferredPort;
52
+ return this.actualPort || this.callbackPorts[0];
51
53
  }
52
54
  /**
53
55
  * Stop the server if running
@@ -65,6 +67,17 @@ const dirname = path.dirname(filename);
65
67
  * @throws CallbackError if callback fails or times out
66
68
  */ async waitForCallback(expectedState) {
67
69
  return new Promise((resolve, reject)=>{
70
+ if (this.server) {
71
+ reject(new CallbackError('Callback server is already running', 'server_error'));
72
+ return;
73
+ }
74
+ this.readyPromise = new Promise((resolveReady, rejectReady)=>{
75
+ this.resolveReady = resolveReady;
76
+ this.rejectReady = rejectReady;
77
+ });
78
+ this.readyPromise.catch(()=>{
79
+ // The caller may only await waitForCallback(); avoid an unhandled rejection.
80
+ });
68
81
  const cleanup = (handle)=>{
69
82
  clearTimeout(handle);
70
83
  if (this.server) {
@@ -77,8 +90,7 @@ const dirname = path.dirname(filename);
77
90
  cleanup(timeoutHandle);
78
91
  reject(new CallbackError('OAuth authorization timed out. Please try again.', 'timeout'));
79
92
  }, this.timeoutMs);
80
- // Create HTTP server
81
- this.server = createServer((req, res)=>{
93
+ const handleRequest = (req, res)=>{
82
94
  const url = new URL(req.url || '', `http://localhost:${this.getActualPort()}`);
83
95
  // Only handle /callback path
84
96
  if (url.pathname !== '/callback') {
@@ -134,36 +146,65 @@ const dirname = path.dirname(filename);
134
146
  code,
135
147
  state
136
148
  });
137
- });
138
- // Try to start server with dynamic port allocation
139
- const tryListen = (port, isRetry = false)=>{
140
- if (!this.server) {
141
- reject(new CallbackError('Server instance is not available', 'server_error'));
142
- return;
143
- }
144
- this.server.listen(port, '127.0.0.1', ()=>{
149
+ };
150
+ // Try registered callback ports in order.
151
+ const tryListen = (portIndex)=>{
152
+ const port = this.callbackPorts[portIndex];
153
+ const server = createServer(handleRequest);
154
+ this.server = server;
155
+ server.once('error', (err)=>{
156
+ if (this.server === server) {
157
+ this.server = null;
158
+ }
159
+ if (err.code === 'EADDRINUSE' && portIndex < this.callbackPorts.length - 1) {
160
+ tryListen(portIndex + 1);
161
+ return;
162
+ }
163
+ cleanup(timeoutHandle);
164
+ const callbackError = err.code === 'EADDRINUSE' ? new CallbackError(`OAuth callback ports are all in use: ${this.callbackPorts.join(', ')}. Close the process using one of these ports and run login again.`, 'ports_in_use') : new CallbackError(`Failed to start callback server: ${err.message}`, 'server_error');
165
+ this.rejectReady?.(callbackError);
166
+ reject(callbackError);
167
+ });
168
+ server.listen(port, '127.0.0.1', ()=>{
169
+ if (this.server !== server) {
170
+ return;
171
+ }
145
172
  // Capture the actual port that was assigned
146
- const addr = this.server.address();
173
+ const addr = server.address();
147
174
  if (addr && typeof addr === 'object') {
148
175
  this.actualPort = addr.port;
149
- }
150
- });
151
- // Handle server errors (port in use, etc)
152
- this.server.once('error', (err)=>{
153
- if (err.code === 'EADDRINUSE' && !isRetry) {
154
- // Port in use, try with OS-assigned port (0 = dynamic)
155
- this.server.removeAllListeners();
156
- tryListen(0, true);
157
- } else {
158
- cleanup(timeoutHandle);
159
- reject(new CallbackError(`Failed to start callback server: ${err.message}${!isRetry ? ' (tried fallback port)' : ''}`));
176
+ this.resolveReady?.(addr.port);
160
177
  }
161
178
  });
162
179
  };
163
- // Start with preferred port
164
- tryListen(this.preferredPort);
180
+ // Start with first configured port
181
+ tryListen(0);
165
182
  });
166
183
  }
184
+ /**
185
+ * Wait until the callback server is listening.
186
+ */ waitUntilReady() {
187
+ if (!this.readyPromise) {
188
+ return Promise.reject(new CallbackError('Callback server has not started', 'server_error'));
189
+ }
190
+ return this.readyPromise;
191
+ }
192
+ }
193
+ function resolveCallbackPorts(options) {
194
+ if (!options.port && !options.fallbackPorts) {
195
+ return DEFAULT_CALLBACK_PORTS;
196
+ }
197
+ const primaryPort = options.port || DEFAULT_CALLBACK_PORT;
198
+ const ports = uniquePorts([
199
+ primaryPort,
200
+ ...options.fallbackPorts || []
201
+ ]).slice(0, DEFAULT_CALLBACK_PORTS.length);
202
+ return ports.length > 0 ? ports : DEFAULT_CALLBACK_PORTS;
203
+ }
204
+ function uniquePorts(ports) {
205
+ return [
206
+ ...new Set(ports)
207
+ ];
167
208
  }
168
209
 
169
210
  //# sourceMappingURL=callback-server.js.map
@@ -30,6 +30,17 @@
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
+ * @deprecated Exists solely to read token stores written by older versions of
35
+ * this CLI that mixed `os.hostname()` into the encryption-key derivation. macOS
36
+ * rewrites the kernel hostname across DHCP/VPN/sleep events, which made those
37
+ * stores periodically un-decryptable. Used only by the transparent-migration
38
+ * path in `token-store.ts` and should be removed in a future release once
39
+ * upgraded clients are sufficiently common.
40
+ *
41
+ * @returns 64-character hex string (32-byte key) suitable for AES-256
42
+ */
43
+ export declare function deriveLegacyEncryptionKey(): string;
33
44
  /**
34
45
  * Get a short hash of the encryption key for diagnostics.
35
46
  * Returns first 8 chars of SHA-256 hash — enough to compare across invocations
@@ -36,7 +36,6 @@ import path from 'path';
36
36
  */ function gatherMachineEntropy() {
37
37
  try {
38
38
  const entropy = [
39
- os.hostname(),
40
39
  os.homedir(),
41
40
  os.userInfo().username,
42
41
  os.platform(),
@@ -88,6 +87,28 @@ import path from 'path';
88
87
  const key = crypto.pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
89
88
  return key.toString('hex');
90
89
  }
90
+ /**
91
+ * @deprecated Exists solely to read token stores written by older versions of
92
+ * this CLI that mixed `os.hostname()` into the encryption-key derivation. macOS
93
+ * rewrites the kernel hostname across DHCP/VPN/sleep events, which made those
94
+ * stores periodically un-decryptable. Used only by the transparent-migration
95
+ * path in `token-store.ts` and should be removed in a future release once
96
+ * upgraded clients are sufficiently common.
97
+ *
98
+ * @returns 64-character hex string (32-byte key) suitable for AES-256
99
+ */ export function deriveLegacyEncryptionKey() {
100
+ const entropy = [
101
+ os.hostname(),
102
+ os.homedir(),
103
+ os.userInfo().username,
104
+ os.platform(),
105
+ os.arch()
106
+ ];
107
+ const machineId = entropy.join('::');
108
+ const salt = getOrCreateSalt();
109
+ const key = crypto.pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
110
+ return key.toString('hex');
111
+ }
91
112
  /**
92
113
  * Get a short hash of the encryption key for diagnostics.
93
114
  * Returns first 8 chars of SHA-256 hash — enough to compare across invocations
@@ -14,7 +14,9 @@ export declare class OAuthFlowError extends Error {
14
14
  export interface OAuthFlowOptions {
15
15
  /** OAuth2 client ID (defaults to config) */
16
16
  clientId?: string;
17
- /** Callback server port (defaults to DEFAULT_CALLBACK_PORT) */
17
+ /** Callback server fallback ports to try after port */
18
+ fallbackPorts?: number[];
19
+ /** First callback server port to try */
18
20
  port?: number;
19
21
  /** Redirect URI (defaults to config) */
20
22
  redirectUri?: string;
@@ -1,6 +1,6 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import crypto from 'crypto';
3
- import { DEFAULT_CALLBACK_PORT, getOAuthConfig } from '../config/oauth.js';
3
+ import { getOAuthConfig } from '../config/oauth.js';
4
4
  import * as log from '../utils/log.js';
5
5
  import { buildAuthorizationUrl, openBrowser } from './browser.js';
6
6
  import { CallbackServer } from './callback-server.js';
@@ -53,25 +53,27 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
53
53
  };
54
54
  }
55
55
  // REAL OAUTH IMPLEMENTATION
56
- const { clientId = oauthConfig.clientId, port = DEFAULT_CALLBACK_PORT, redirectUri = oauthConfig.redirectUri, scopes = oauthConfig.scopes, timeoutMs = 120000 } = options;
56
+ const { clientId = oauthConfig.clientId, fallbackPorts, port, redirectUri = oauthConfig.redirectUri, scopes = oauthConfig.scopes, timeoutMs = 120000 } = options;
57
57
  // Generate PKCE pair for enhanced security
58
58
  const { codeChallenge, codeChallengeMethod, codeVerifier } = generatePKCEPair();
59
59
  // Generate random state for CSRF protection
60
60
  const state = crypto.randomBytes(32).toString('base64url');
61
61
  // Create callback server
62
62
  const callbackServer = new CallbackServer({
63
+ fallbackPorts,
63
64
  port,
64
65
  timeoutMs
65
66
  });
66
67
  try {
67
68
  // Start waiting for callback (this starts the server and captures actual port)
68
69
  const callbackPromise = callbackServer.waitForCallback(state);
69
- // Wait a bit for server to start and determine actual port
70
- await new Promise((resolve)=>setTimeout(resolve, 100));
70
+ callbackPromise.catch(()=>{
71
+ // The callback promise is awaited below; avoid an unhandled rejection if startup fails early.
72
+ });
71
73
  // Get the actual port (may differ from preferred if that was in use)
72
- const actualPort = callbackServer.getActualPort();
74
+ const actualPort = await callbackServer.waitUntilReady();
73
75
  // Build redirect URI with actual port
74
- const actualRedirectUri = redirectUri.replace(/:\d+/, `:${actualPort}`);
76
+ const actualRedirectUri = replaceRedirectUriPort(redirectUri, actualPort);
75
77
  // Build authorization URL
76
78
  const authUrl = buildAuthorizationUrl({
77
79
  authorizationUrl: oauthConfig.authorizationUrl,
@@ -213,5 +215,10 @@ import { exchangeCodeForTokens, refreshAccessToken, TokenRefreshError } from './
213
215
  return null;
214
216
  }
215
217
  }
218
+ function replaceRedirectUriPort(redirectUri, port) {
219
+ const url = new URL(redirectUri);
220
+ url.port = String(port);
221
+ return url.toString();
222
+ }
216
223
 
217
224
  //# sourceMappingURL=oauth-flow.js.map
@@ -12,6 +12,7 @@ export declare function getTokenStore(environment?: Environment): TokenStore;
12
12
  */
13
13
  export declare class TokenStore {
14
14
  private baseConfigName;
15
+ private deriveLegacyKey;
15
16
  private encryptionKey;
16
17
  private environment;
17
18
  private legacyRootDir;
@@ -69,6 +70,7 @@ export declare class TokenStore {
69
70
  private getLegacyRootDir;
70
71
  private getProjectStore;
71
72
  private createConfig;
73
+ private tryMigrateLegacyKey;
72
74
  private safeGet;
73
75
  }
74
76
  //# sourceMappingURL=token-store.d.ts.map
@@ -6,7 +6,7 @@ import { TOKEN_EXPIRY_BUFFER_SECONDS } from '../config/oauth.js';
6
6
  import { getInfraEnvironment } from '../constants.js';
7
7
  import { getEnvVarSync } from '../utils/env-management.js';
8
8
  import * as log from '../utils/log.js';
9
- import { deriveEncryptionKey } from './crypto-utils.js';
9
+ import { deriveEncryptionKey, deriveLegacyEncryptionKey } from './crypto-utils.js';
10
10
  import { migrateOAuth, migrateProjectData, needsMigration } from './token-store-migration.js';
11
11
  /**
12
12
  * Environment-keyed instances for singleton pattern.
@@ -30,6 +30,7 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
30
30
  * The constructor is still exported for test isolation.
31
31
  */ export class TokenStore {
32
32
  baseConfigName;
33
+ deriveLegacyKey;
33
34
  encryptionKey;
34
35
  environment;
35
36
  legacyRootDir;
@@ -45,6 +46,7 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
45
46
  const encryptionKey = options?.encryptionKey ?? deriveEncryptionKey();
46
47
  const baseConfigName = options?.configName || projectName;
47
48
  this.baseConfigName = baseConfigName;
49
+ this.deriveLegacyKey = options?.deriveLegacyKey ?? deriveLegacyEncryptionKey;
48
50
  this.encryptionKey = encryptionKey;
49
51
  this.environment = environment;
50
52
  this.projectName = projectName;
@@ -396,6 +398,13 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
396
398
  try {
397
399
  return new Conf(confOptions);
398
400
  } catch (error) {
401
+ const migrated = this.tryMigrateLegacyKey(confOptions);
402
+ if (migrated) {
403
+ if (this.environment === 'staging') {
404
+ log.info('Successfully migrated local token cache.');
405
+ }
406
+ return migrated;
407
+ }
399
408
  // Deserialization failed (e.g. encryption key changed).
400
409
  // Log diagnostics, then clear and replace with a fresh store.
401
410
  if (this.environment === 'staging') {
@@ -412,6 +421,37 @@ const PROJECT_STORE_SCHEMA_VERSION = 1;
412
421
  return config;
413
422
  }
414
423
  }
424
+ tryMigrateLegacyKey(confOptions) {
425
+ if (!confOptions.encryptionKey) {
426
+ return null;
427
+ }
428
+ let snapshot;
429
+ try {
430
+ const legacyConf = new Conf({
431
+ ...confOptions,
432
+ clearInvalidConfig: false,
433
+ encryptionKey: this.deriveLegacyKey()
434
+ });
435
+ snapshot = {
436
+ ...legacyConf.store
437
+ };
438
+ } catch {
439
+ return null;
440
+ }
441
+ try {
442
+ const rewritten = new Conf({
443
+ ...confOptions,
444
+ clearInvalidConfig: true
445
+ });
446
+ rewritten.clear();
447
+ for (const [key, value] of Object.entries(snapshot)){
448
+ rewritten.set(key, value);
449
+ }
450
+ return rewritten;
451
+ } catch {
452
+ return null;
453
+ }
454
+ }
415
455
  safeGet(params) {
416
456
  const { config, key } = params;
417
457
  try {
@@ -109,6 +109,13 @@ export interface OAuthTokenRefreshParams {
109
109
  export type TokenStoreConfig = {
110
110
  /** Config file name (for testing) */
111
111
  configName?: string;
112
+ /**
113
+ * Override for the legacy key derivation used by transparent migration.
114
+ * Tests pass an explicit function so they don't depend on the host machine's
115
+ * actual entropy. Production code leaves this undefined and the constructor
116
+ * falls back to `deriveLegacyEncryptionKey`.
117
+ */
118
+ deriveLegacyKey?: () => string;
112
119
  /** Optional override for storage encryption key (primarily for tests). */
113
120
  encryptionKey?: string;
114
121
  /** Environment for config file naming */
package/dist/cli.js CHANGED
@@ -45,8 +45,10 @@ class Main {
45
45
  '--infra-env': String,
46
46
  '--json': Boolean,
47
47
  '--name': String,
48
+ '--no-skill': Boolean,
48
49
  '--skip-auth': Boolean,
49
50
  '--skip-build': Boolean,
51
+ '--template': String,
50
52
  '--version': Boolean,
51
53
  '--yes': Boolean,
52
54
  // Aliases
@@ -55,6 +57,7 @@ class Main {
55
57
  '-f': '--force',
56
58
  '-h': '--help',
57
59
  '-n': '--name',
60
+ '-t': '--template',
58
61
  '-v': '--version',
59
62
  '-y': '--yes'
60
63
  }, {
@@ -146,7 +149,9 @@ class Main {
146
149
  debug: this.args['--debug'],
147
150
  env: this.args['--env'],
148
151
  force: this.args['--force'],
149
- skipAuth: this.args['--skip-auth']
152
+ noSkill: this.args['--no-skill'],
153
+ skipAuth: this.args['--skip-auth'],
154
+ template: this.args['--template']
150
155
  });
151
156
  break;
152
157
  case 'list-tokens':
@@ -12,8 +12,12 @@ export interface InitCommandOptions {
12
12
  id?: string;
13
13
  /** Project name (for scaffolding) */
14
14
  name?: string;
15
+ /** Skip Payload skill installation */
16
+ noSkill?: boolean;
15
17
  /** Skip authentication check (for testing/development) */
16
18
  skipAuth?: boolean;
19
+ /** GitHub template override, e.g. "v3.80.0:website" or "owner/repo#ref:template-path" */
20
+ template?: string;
17
21
  /** Skip prompts and use defaults where possible */
18
22
  yes?: boolean;
19
23
  }
@@ -8,6 +8,7 @@ import { tryGetCredential } 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
10
  import { getInfraEnvironment, getProjectNotFoundMessage } from '../constants.js';
11
+ import { downloadSkill } from '../lib/download-skill.js';
11
12
  import { ensureGitignore } from '../utils/config.js';
12
13
  import { addOrUpdateEnvVar } from '../utils/env-management.js';
13
14
  import { isDebug } from '../utils/is-debug.js';
@@ -21,6 +22,23 @@ import { resolveEnvironment } from '../utils/resolve-environment.js';
21
22
  import { checkForUpdates, getOwnVersion } from '../utils/version-check.js';
22
23
  import { loginCommand } from './login.js';
23
24
  import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
25
+ import { DEFAULT_TEMPLATE_SOURCE } from '../utils/download-template.js';
26
+ import { parseTemplateSpec, TemplateSpecParseError } from '../utils/parse-template-spec.js';
27
+ async function maybeInstallPayloadSkill(args) {
28
+ if (args.noSkill) {
29
+ return;
30
+ }
31
+ const result = await downloadSkill({
32
+ projectDir: args.projectDir
33
+ });
34
+ if (result.ok) {
35
+ return;
36
+ }
37
+ if (result.reason === 'already-installed') {
38
+ return;
39
+ }
40
+ p.log.warn(pc.yellow(`⚠ Could not install Payload skill (${result.reason}${result.detail ? `: ${result.detail}` : ''}). Continuing.`));
41
+ }
24
42
  /**
25
43
  * Generate and store project token for a content system
26
44
  * Non-blocking - will show warning but not exit on failure
@@ -111,6 +129,18 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
111
129
  process.exit(1);
112
130
  }
113
131
  const cmsResourceId = options.id;
132
+ // Parse --template spec early so malformed values abort before any I/O
133
+ let templateSource;
134
+ if (options.template) {
135
+ try {
136
+ templateSource = parseTemplateSpec(options.template);
137
+ } catch (error) {
138
+ const message = error instanceof TemplateSpecParseError ? error.message : 'Invalid --template value';
139
+ p.log.error(pc.red(`✗ ${message}`));
140
+ p.note('Format: [<owner>/<repo>#]<ref>:<template-path>\n' + 'Examples:\n' + ' blank\n' + ' v3.80.0:website\n' + ' myorg/payload-fork#feat-x:templates/custom', 'Template spec');
141
+ process.exit(1);
142
+ }
143
+ }
114
144
  // Fetch bootstrap info (oauth creds + environments)
115
145
  let bootstrapInfo = null;
116
146
  let resolvedEnv = null;
@@ -250,6 +280,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
250
280
  '.env.local',
251
281
  'lambda.zip'
252
282
  ]);
283
+ await maybeInstallPayloadSkill({
284
+ noSkill: options.noSkill,
285
+ projectDir: process.cwd()
286
+ });
253
287
  // Generate project token (unless skipping auth)
254
288
  if (!options.skipAuth && resolvedEnv) {
255
289
  await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s, {
@@ -301,9 +335,13 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
301
335
  process.exit(1);
302
336
  }
303
337
  // Scaffold project
304
- s.start('Downloading template from GitHub...');
338
+ const resolvedSource = {
339
+ ...DEFAULT_TEMPLATE_SOURCE,
340
+ ...templateSource
341
+ };
342
+ s.start(`Downloading template "${resolvedSource.templatePath}" from ` + `${resolvedSource.owner}/${resolvedSource.repo}@${resolvedSource.ref}...`);
305
343
  try {
306
- await scaffoldProject(fullPath, projectName, packageManager);
344
+ await scaffoldProject(fullPath, projectName, packageManager, templateSource);
307
345
  s.stop(pc.green('✓ Template downloaded'));
308
346
  s.start('Installing dependencies...');
309
347
  await installDependencies(fullPath, packageManager);
@@ -371,6 +409,10 @@ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
371
409
  if (!importMapResult) {
372
410
  p.log.warn(pc.yellow('⚠ Import map generation failed — it will be generated on first dev run'));
373
411
  }
412
+ await maybeInstallPayloadSkill({
413
+ noSkill: options.noSkill,
414
+ projectDir: fullPath
415
+ });
374
416
  // Generate project token (unless skipping auth)
375
417
  if (!options.skipAuth && resolvedEnv) {
376
418
  await generateProjectTokenWithFeedback(tokenStore, resolvedEnv.contentSystemId, s, {
@@ -1,5 +1,6 @@
1
1
  import type { OAuthConfig } from '../auth/types.js';
2
- export declare const DEFAULT_CALLBACK_PORT = 34462;
2
+ export declare const DEFAULT_CALLBACK_PORTS: number[];
3
+ export declare const DEFAULT_CALLBACK_PORT: number;
3
4
  /**
4
5
  * Get OAuth configuration for current environment.
5
6
  *
@@ -1,5 +1,11 @@
1
1
  import { getEnvConfig, getInfraEnvironment } from '../constants.js';
2
- export const DEFAULT_CALLBACK_PORT = 34462;
2
+ export const DEFAULT_CALLBACK_PORTS = [
3
+ 34462,
4
+ 34463,
5
+ 34464,
6
+ 34465
7
+ ];
8
+ export const DEFAULT_CALLBACK_PORT = DEFAULT_CALLBACK_PORTS[0];
3
9
  /**
4
10
  * Get OAuth configuration for current environment.
5
11
  *
@@ -3483,6 +3483,8 @@ export type components = {
3483
3483
  CreateDocumentRequest: {
3484
3484
  /** @example cms-xxxxx-xxxxx */
3485
3485
  contentSystemId: string;
3486
+ /** User-defined Payload environment name (e.g. "production"). */
3487
+ environmentName?: string;
3486
3488
  /** @example posts */
3487
3489
  collection: string;
3488
3490
  doc: components['schemas']['DocumentData'];
@@ -3594,6 +3596,8 @@ export type components = {
3594
3596
  UpdateDocumentRequest: {
3595
3597
  /** @example cms-xxxxx-xxxxx */
3596
3598
  contentSystemId: string;
3599
+ /** User-defined Payload environment name (e.g. "production"). */
3600
+ environmentName?: string;
3597
3601
  /** @example posts */
3598
3602
  collection: string;
3599
3603
  /** @example false */
@@ -3627,6 +3631,8 @@ export type components = {
3627
3631
  DeleteDocumentRequest: {
3628
3632
  /** @example cms-xxxxx-xxxxx */
3629
3633
  contentSystemId: string;
3634
+ /** User-defined Payload environment name (e.g. "production"). */
3635
+ environmentName?: string;
3630
3636
  /** @example posts */
3631
3637
  collection: string;
3632
3638
  locale?: components['schemas']['LocaleClause'];
@@ -6,6 +6,7 @@ type ContentAPIOptions = {
6
6
  allowIDOnCreate?: boolean;
7
7
  auth: AuthMode;
8
8
  contentSystemId: string;
9
+ environmentName?: string;
9
10
  url: string;
10
11
  };
11
12
  export type ContentAPIAdapter = {
@@ -13,6 +14,7 @@ export type ContentAPIAdapter = {
13
14
  clearDatabase: () => Promise<void>;
14
15
  client: ReturnType<typeof createClient<paths>>;
15
16
  contentSystemId: string;
17
+ environmentName?: string;
16
18
  idType: 'uuid';
17
19
  url: string;
18
20
  } & BaseDatabaseAdapter;