@payloadcms/figma 0.0.1-alpha.63 → 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 (48) 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 +14 -6
  7. package/dist/auth/project-token.d.ts +15 -10
  8. package/dist/auth/project-token.js +147 -64
  9. package/dist/auth/token-store-migration.js +2 -2
  10. package/dist/auth/token-store.d.ts +2 -0
  11. package/dist/auth/token-store.js +43 -3
  12. package/dist/auth/types.d.ts +11 -0
  13. package/dist/cli.js +15 -1
  14. package/dist/commands/bootstrap.d.ts +18 -0
  15. package/dist/commands/bootstrap.js +90 -0
  16. package/dist/commands/init.d.ts +4 -0
  17. package/dist/commands/init.js +76 -4
  18. package/dist/config/oauth.d.ts +2 -1
  19. package/dist/config/oauth.js +7 -1
  20. package/dist/db-content-api/generated/content-api-types.d.ts +6 -0
  21. package/dist/db-content-api/index.d.ts +2 -0
  22. package/dist/db-content-api/index.js +36 -74
  23. package/dist/lib/download-skill.d.ts +13 -0
  24. package/dist/lib/download-skill.js +79 -0
  25. package/dist/oauth/endpoints/getLoginEndpoint.js +26 -107
  26. package/dist/oauth/endpoints/getTokenLoginEndpoint.d.ts +17 -0
  27. package/dist/oauth/endpoints/getTokenLoginEndpoint.js +105 -0
  28. package/dist/oauth/index.js +8 -0
  29. package/dist/oauth/utilities/establishSession.d.ts +23 -0
  30. package/dist/oauth/utilities/establishSession.js +82 -0
  31. package/dist/oauth/utilities/exchangeCodeForAccessToken.d.ts +24 -0
  32. package/dist/oauth/utilities/exchangeCodeForAccessToken.js +28 -0
  33. package/dist/oauth/utilities/isAbsoluteURL.d.ts +2 -0
  34. package/dist/oauth/utilities/isAbsoluteURL.js +3 -0
  35. package/dist/plugin/build-config.js +65 -46
  36. package/dist/types.d.ts +2 -0
  37. package/dist/utils/download-template.d.ts +9 -1
  38. package/dist/utils/download-template.js +24 -19
  39. package/dist/utils/messages.js +9 -0
  40. package/dist/utils/parse-template-spec.d.ts +12 -0
  41. package/dist/utils/parse-template-spec.js +62 -0
  42. package/dist/utils/payload-config-modifier.js +96 -107
  43. package/dist/utils/payload-package-check.d.ts +21 -1
  44. package/dist/utils/payload-package-check.js +66 -26
  45. package/dist/utils/project.d.ts +2 -1
  46. package/dist/utils/project.js +2 -2
  47. package/package.json +9 -1
  48. package/dist/db-content-api/README.md +0 -98
@@ -0,0 +1,82 @@
1
+ import { APIError, generateCookie } from 'payload';
2
+ import { getProjectToken, getUserInfo } from '../../api/figma-api.js';
3
+ import { createCookieOptions } from './createCookieOptions.js';
4
+ import { getCookieExpiration } from './getCookieExpiration.js';
5
+ /**
6
+ * Given a valid OAuth access_token: fetch user info (non-fatal), mint a
7
+ * project token, and write the main + refresh + user-info cookies to
8
+ * `req.responseHeaders`. Throws `APIError` if the project token mint fails.
9
+ */ export const establishSession = async ({ accessToken, collection, collectionOptions, contentSystemId, debugLogger, pluginOptions, refreshTokenExpiresIn, req, strategy })=>{
10
+ const oauthCredential = {
11
+ type: 'oauth',
12
+ token: accessToken
13
+ };
14
+ // Non-fatal: user just won't have handle/image on first request.
15
+ let figmaUserInfo = null;
16
+ try {
17
+ figmaUserInfo = await getUserInfo(oauthCredential);
18
+ debugLogger.info({
19
+ figmaUserInfo,
20
+ msg: 'Fetched Figma user info'
21
+ });
22
+ } catch (err) {
23
+ debugLogger.error({
24
+ err,
25
+ msg: 'Failed to fetch Figma user info'
26
+ });
27
+ }
28
+ const projectToken = await getProjectToken(oauthCredential, contentSystemId);
29
+ if (!projectToken.token) {
30
+ const errMsg = 'No project token returned from Figma API';
31
+ req.payload.logger.error({
32
+ msg: errMsg
33
+ });
34
+ throw new APIError(errMsg);
35
+ }
36
+ const cookieOptions = createCookieOptions({
37
+ collection,
38
+ headers: req.headers,
39
+ pluginOptions
40
+ });
41
+ if (!req.responseHeaders) {
42
+ req.responseHeaders = new Headers();
43
+ }
44
+ // The -refresh cookie holds the OAuth access_token so refreshTokens can
45
+ // re-mint project_tokens from it on JWT expiry.
46
+ if (!collectionOptions.usePayloadJWT) {
47
+ const refreshCookie = generateCookie({
48
+ ...cookieOptions,
49
+ name: `${strategy.cookieName}-refresh`,
50
+ expires: getCookieExpiration(cookieOptions?.expires ?? refreshTokenExpiresIn ?? 31_556_952),
51
+ returnCookieAsObject: false,
52
+ value: accessToken
53
+ });
54
+ req.responseHeaders.append('Set-Cookie', refreshCookie);
55
+ }
56
+ const tokenExpires = cookieOptions?.expires ? getCookieExpiration(cookieOptions.expires) : new Date(projectToken.expiresAt);
57
+ const tokenCookie = generateCookie({
58
+ ...cookieOptions,
59
+ name: strategy.cookieName,
60
+ expires: tokenExpires,
61
+ returnCookieAsObject: false,
62
+ value: projectToken.token
63
+ });
64
+ req.responseHeaders.append('Set-Cookie', tokenCookie);
65
+ // Short-lived handoff cookie consumed by defaultVerify on the next request.
66
+ if (figmaUserInfo) {
67
+ const userInfoValue = Buffer.from(JSON.stringify({
68
+ handle: figmaUserInfo.handle,
69
+ img_url: figmaUserInfo.img_url
70
+ })).toString('base64');
71
+ const userInfoCookie = generateCookie({
72
+ ...cookieOptions,
73
+ name: pluginOptions?.userInfoCookieName ?? 'figma-user-info',
74
+ expires: new Date(Date.now() + 60_000),
75
+ returnCookieAsObject: false,
76
+ value: userInfoValue
77
+ });
78
+ req.responseHeaders.append('Set-Cookie', userInfoCookie);
79
+ }
80
+ };
81
+
82
+ //# sourceMappingURL=establishSession.js.map
@@ -0,0 +1,24 @@
1
+ import type { Strategy } from '../strategy/index.js';
2
+ interface Args {
3
+ /** Authorization code received from the IdP redirect. */
4
+ code: string;
5
+ /** PKCE verifier; omit for non-PKCE flows. */
6
+ codeVerifier?: string;
7
+ /** Must match the redirect_uri the authorization request used. */
8
+ redirectUri: string;
9
+ strategy: Strategy;
10
+ }
11
+ export interface TokenEndpointResponse {
12
+ access_token?: null | string;
13
+ error?: string;
14
+ error_description?: string;
15
+ expires_in?: number;
16
+ }
17
+ /**
18
+ * POSTs an authorization code to the IdP's `token_endpoint` and returns the
19
+ * raw response body. Callers handle error/missing-token logging in their own
20
+ * style — this helper only owns the request shape.
21
+ */
22
+ export declare const exchangeCodeForAccessToken: ({ code, codeVerifier, redirectUri, strategy, }: Args) => Promise<TokenEndpointResponse>;
23
+ export {};
24
+ //# sourceMappingURL=exchangeCodeForAccessToken.d.ts.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * POSTs an authorization code to the IdP's `token_endpoint` and returns the
3
+ * raw response body. Callers handle error/missing-token logging in their own
4
+ * style — this helper only owns the request shape.
5
+ */ export const exchangeCodeForAccessToken = async ({ code, codeVerifier, redirectUri, strategy })=>{
6
+ const body = {
7
+ client_id: strategy.clientID,
8
+ code,
9
+ grant_type: 'authorization_code',
10
+ redirect_uri: redirectUri
11
+ };
12
+ if (codeVerifier) {
13
+ body.code_verifier = codeVerifier;
14
+ }
15
+ if (strategy.collectionOptions.clientSecret) {
16
+ body.client_secret = strategy.collectionOptions.clientSecret;
17
+ }
18
+ const res = await fetch(strategy.meta.token_endpoint, {
19
+ body: new URLSearchParams(body),
20
+ headers: {
21
+ 'Content-Type': 'application/x-www-form-urlencoded'
22
+ },
23
+ method: 'POST'
24
+ });
25
+ return await res.json();
26
+ };
27
+
28
+ //# sourceMappingURL=exchangeCodeForAccessToken.js.map
@@ -0,0 +1,2 @@
1
+ export declare const isAbsoluteURL: (url: string) => boolean;
2
+ //# sourceMappingURL=isAbsoluteURL.d.ts.map
@@ -0,0 +1,3 @@
1
+ export const isAbsoluteURL = (url)=>/^[a-z][a-z\d+.-]*:/.test(url);
2
+
3
+ //# sourceMappingURL=isAbsoluteURL.js.map
@@ -104,6 +104,66 @@ function missingOAuthCredential(name) {
104
104
  function messageOf(error) {
105
105
  return error instanceof Error ? error.message : 'Unknown error';
106
106
  }
107
+ /**
108
+ * Creates storage configuration for upload collections
109
+ */ function createStorageConfig(url, contentSystemId) {
110
+ const storageConfig = process.env.FIGMA_CONTENT_API_ACCESS_KEY ? {
111
+ baseUrl: url,
112
+ contentApiKey: process.env.FIGMA_CONTENT_API_ACCESS_KEY
113
+ } : process.env.FIGMA_DEV_JWT === 'true' ? {
114
+ auth: {
115
+ mode: 'devJwt'
116
+ },
117
+ baseUrl: url,
118
+ contentSystemId
119
+ } : {
120
+ auth: {
121
+ mode: 'tokenStore',
122
+ tokenStore: getTokenStore()
123
+ },
124
+ baseUrl: url,
125
+ contentSystemId
126
+ };
127
+ const adapter = contentApiStorageAdapter(storageConfig);
128
+ const storageClient = createStorageClient(storageConfig);
129
+ return {
130
+ adapter,
131
+ storageClient
132
+ };
133
+ }
134
+ /**
135
+ * Plugin that applies cloud storage configuration to all upload-enabled collections.
136
+ * Runs at the end of the plugin chain so it sees collections added by user plugins too.
137
+ */ function createStoragePlugin(url, contentSystemId) {
138
+ return (incomingConfig)=>{
139
+ const uploadCollections = (incomingConfig.collections || []).filter((c)=>c.upload);
140
+ if (uploadCollections.length === 0) {
141
+ return incomingConfig;
142
+ }
143
+ const { adapter, storageClient } = createStorageConfig(url, contentSystemId);
144
+ const collectionsMap = uploadCollections.reduce((acc, c)=>{
145
+ acc[c.slug] = {
146
+ adapter,
147
+ disableLocalStorage: true
148
+ };
149
+ return acc;
150
+ }, {});
151
+ initClientUploads({
152
+ clientHandler: '@payloadcms/figma/client#ContentApiClientUploadHandler',
153
+ collections: collectionsMap,
154
+ config: incomingConfig,
155
+ enabled: true,
156
+ serverHandler: getGenerateSignedURLHandler({
157
+ client: storageClient
158
+ }),
159
+ serverHandlerPath: '/content-api-storage-signed-url'
160
+ });
161
+ const storagePlugin = cloudStoragePlugin({
162
+ collections: collectionsMap
163
+ });
164
+ return storagePlugin(incomingConfig);
165
+ };
166
+ }
107
167
  export async function buildFigmaConfig(config) {
108
168
  const envConfig = getEnvConfig();
109
169
  // Resolve contentSystemId: config first, env var, then local store fallback (dev)
@@ -160,6 +220,7 @@ export async function buildFigmaConfig(config) {
160
220
  mode: 'apiKey'
161
221
  },
162
222
  contentSystemId,
223
+ environmentName: process.env.FIGMA_ENVIRONMENT_NAME,
163
224
  url
164
225
  });
165
226
  } else if (process.env.FIGMA_DEV_JWT === 'true') {
@@ -168,6 +229,7 @@ export async function buildFigmaConfig(config) {
168
229
  mode: 'devJwt'
169
230
  },
170
231
  contentSystemId,
232
+ environmentName: process.env.FIGMA_ENVIRONMENT_NAME,
171
233
  url
172
234
  });
173
235
  } else {
@@ -177,53 +239,10 @@ export async function buildFigmaConfig(config) {
177
239
  tokenStore: getTokenStore()
178
240
  },
179
241
  contentSystemId,
242
+ environmentName: process.env.FIGMA_ENVIRONMENT_NAME,
180
243
  url
181
244
  });
182
245
  }
183
- // Build storage plugin if there are upload collections and storage is not disabled
184
- const uploadCollections = (config.collections || []).filter((c)=>c.upload);
185
- let storagePlugin;
186
- if (uploadCollections.length > 0 && config.figma.storage !== false) {
187
- const storageConfig = process.env.FIGMA_CONTENT_API_ACCESS_KEY ? {
188
- baseUrl: url,
189
- contentApiKey: process.env.FIGMA_CONTENT_API_ACCESS_KEY
190
- } : process.env.FIGMA_DEV_JWT === 'true' ? {
191
- auth: {
192
- mode: 'devJwt'
193
- },
194
- baseUrl: url,
195
- contentSystemId
196
- } : {
197
- auth: {
198
- mode: 'tokenStore',
199
- tokenStore: getTokenStore()
200
- },
201
- baseUrl: url,
202
- contentSystemId
203
- };
204
- const adapter = contentApiStorageAdapter(storageConfig);
205
- const storageClient = createStorageClient(storageConfig);
206
- const collectionsMap = uploadCollections.reduce((acc, c)=>{
207
- acc[c.slug] = {
208
- adapter,
209
- disableLocalStorage: true
210
- };
211
- return acc;
212
- }, {});
213
- initClientUploads({
214
- clientHandler: '@payloadcms/figma/client#ContentApiClientUploadHandler',
215
- collections: collectionsMap,
216
- config: config,
217
- enabled: true,
218
- serverHandler: getGenerateSignedURLHandler({
219
- client: storageClient
220
- }),
221
- serverHandlerPath: '/content-api-storage-signed-url'
222
- });
223
- storagePlugin = cloudStoragePlugin({
224
- collections: collectionsMap
225
- });
226
- }
227
246
  // Build complete config with Figma platform defaults
228
247
  const configWithFigmaDefaults = {
229
248
  ...config,
@@ -257,8 +276,8 @@ export async function buildFigmaConfig(config) {
257
276
  // Add oauth to plugins if not already present
258
277
  plugins: [
259
278
  ...config.plugins ?? [],
260
- ...storagePlugin ? [
261
- storagePlugin
279
+ ...config.figma.storage !== false ? [
280
+ createStoragePlugin(url, contentSystemId)
262
281
  ] : [],
263
282
  oAuth2Plugin({
264
283
  collections: [
package/dist/types.d.ts CHANGED
@@ -11,11 +11,13 @@ export interface Args extends arg.Spec {
11
11
  '--list': BooleanConstructor;
12
12
  '--logout': BooleanConstructor;
13
13
  '--name': StringConstructor;
14
+ '--template': StringConstructor;
14
15
  '--yes': BooleanConstructor;
15
16
  '-e': string;
16
17
  '-f': string;
17
18
  '-h': string;
18
19
  '-n': string;
20
+ '-t': string;
19
21
  '-y': string;
20
22
  }
21
23
  export type CliArgs = arg.Result<Args>;
@@ -5,11 +5,19 @@ export declare class TemplateDownloadError extends Error {
5
5
  cause?: Error | undefined;
6
6
  constructor(message: string, cause?: Error | undefined);
7
7
  }
8
+ export type TemplateSource = {
9
+ owner: string;
10
+ ref: string;
11
+ repo: string;
12
+ templatePath: string;
13
+ };
14
+ export declare const DEFAULT_TEMPLATE_SOURCE: TemplateSource;
8
15
  /**
9
16
  * Download Payload template from GitHub with retry logic
10
17
  *
11
18
  * @param projectDir - Directory to extract template into
19
+ * @param source - Optional override of repo / ref / template path
12
20
  * @throws TemplateDownloadError if download fails
13
21
  */
14
- export declare function downloadTemplateFromGitHub(projectDir: string): Promise<void>;
22
+ export declare function downloadTemplateFromGitHub(projectDir: string, source?: Partial<TemplateSource>): Promise<void>;
15
23
  //# sourceMappingURL=download-template.d.ts.map
@@ -11,23 +11,30 @@ import { x } from 'tar';
11
11
  this.name = 'TemplateDownloadError';
12
12
  }
13
13
  }
14
- const BRANCH_OR_TAG = 'v3.79.1';
15
- /** GitHub strips the 'v' prefix in tarball directory names */ const TARBALL_PREFIX = `payload-${BRANCH_OR_TAG.replace(/^v/, '')}`;
16
- const TEMPLATE_NAME = 'blank';
14
+ export const DEFAULT_TEMPLATE_SOURCE = {
15
+ owner: 'payloadcms',
16
+ ref: 'v3.84.1',
17
+ repo: 'payload',
18
+ templatePath: 'blank'
19
+ };
17
20
  const MAX_RETRIES = 3;
18
- const INITIAL_RETRY_DELAY = 1000 // 1 second
19
- ;
21
+ const INITIAL_RETRY_DELAY = 1000;
20
22
  /**
21
23
  * Download Payload template from GitHub with retry logic
22
24
  *
23
25
  * @param projectDir - Directory to extract template into
26
+ * @param source - Optional override of repo / ref / template path
24
27
  * @throws TemplateDownloadError if download fails
25
- */ export async function downloadTemplateFromGitHub(projectDir) {
28
+ */ export async function downloadTemplateFromGitHub(projectDir, source) {
29
+ const resolved = {
30
+ ...DEFAULT_TEMPLATE_SOURCE,
31
+ ...source
32
+ };
26
33
  let lastError;
27
34
  for(let attempt = 1; attempt <= MAX_RETRIES; attempt++){
28
35
  try {
29
- await downloadTemplateAttempt(projectDir);
30
- return; // Success
36
+ await downloadTemplateAttempt(projectDir, resolved);
37
+ return;
31
38
  } catch (error) {
32
39
  lastError = error instanceof Error ? error : new Error('Unknown error');
33
40
  if (attempt < MAX_RETRIES) {
@@ -37,31 +44,29 @@ const INITIAL_RETRY_DELAY = 1000 // 1 second
37
44
  }
38
45
  }
39
46
  }
40
- // All retries failed
41
47
  throw new TemplateDownloadError(`Failed to download template after ${MAX_RETRIES} attempts`, lastError);
42
48
  }
43
- /**
44
- * Single attempt to download template (extracted for retry logic)
45
- */ async function downloadTemplateAttempt(projectDir) {
46
- const url = `https://codeload.github.com/payloadcms/payload/tar.gz/${BRANCH_OR_TAG}`;
47
- const filter = `${TARBALL_PREFIX}/templates/${TEMPLATE_NAME}/`;
49
+ async function downloadTemplateAttempt(projectDir, source) {
50
+ const url = `https://codeload.github.com/${source.owner}/${source.repo}/tar.gz/${source.ref}`;
51
+ // GitHub strips the leading 'v' and replaces '/' with '-' when naming the tarball root dir
52
+ const refSlug = source.ref.replace(/^v/, '').replace(/\//g, '-');
53
+ const tarballPrefix = `${source.repo}-${refSlug}`;
54
+ const filter = `${tarballPrefix}/templates/${source.templatePath}/`;
55
+ const strip = 2 + source.templatePath.split('/').length;
48
56
  try {
49
- // Ensure target directory exists
50
57
  await fs.mkdir(projectDir, {
51
58
  recursive: true
52
59
  });
53
60
  await pipeline(await downloadTarStream(url), x({
54
61
  cwd: projectDir,
55
62
  filter: (p)=>p.includes(filter),
56
- strip: 2 + TEMPLATE_NAME.split('/').length
63
+ strip
57
64
  }));
58
65
  } catch (error) {
59
66
  throw new TemplateDownloadError('Failed to download template from GitHub', error instanceof Error ? error : undefined);
60
67
  }
61
68
  }
62
- /**
63
- * Download tar stream from URL
64
- */ async function downloadTarStream(url) {
69
+ async function downloadTarStream(url) {
65
70
  const res = await fetch(url);
66
71
  if (!res.ok) {
67
72
  throw new Error(`HTTP ${res.status}: ${res.statusText}`);
@@ -14,6 +14,7 @@ export function helpMessage() {
14
14
  ${pc.cyan('logout')} Clear all stored tokens
15
15
  ${pc.cyan('list-tokens')} Show stored token information
16
16
  ${pc.cyan('init')} Initialize a Figma CMS project
17
+ ${pc.cyan('bootstrap')} Print bootstrap info (tenant IDs, OAuth creds) for a project
17
18
  ${pc.cyan('debug')} Show debug info for troubleshooting
18
19
  ${pc.cyan('env')} Switch active environment
19
20
  ${pc.cyan('deploy')} Deploy your project to Figma
@@ -38,6 +39,14 @@ export function helpMessage() {
38
39
  ${pc.cyan('@payloadcms/figma init --id <id> --env staging')} Initialize for specific environment
39
40
  ${pc.dim('--name, -n <name>')} Set project directory name (skips prompt)
40
41
  ${pc.dim('--force')} Force reconfiguration of existing project
42
+ ${pc.dim('--no-skill')} Skip installing the Payload skill into .claude/skills/payload/
43
+ ${pc.dim('--template, -t <spec>')} Override scaffold template (e.g. v3.80.0:website, owner/repo#ref:path)
44
+
45
+ ${pc.bold('BOOTSTRAP COMMAND')}
46
+
47
+ ${pc.cyan('@payloadcms/figma bootstrap --id <cms-resource-id>')} Print bootstrap info
48
+ ${pc.dim('--env <environment>')} Filter to a single environment
49
+ ${pc.dim('--json')} Output JSON instead of styled note
41
50
 
42
51
  ${pc.bold('ENV COMMAND')}
43
52
 
@@ -0,0 +1,12 @@
1
+ import type { TemplateSource } from './download-template.js';
2
+ export declare class TemplateSpecParseError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /**
6
+ * Parse a --template spec string into a partial TemplateSource override.
7
+ *
8
+ * Format: [<owner>/<repo>#]<ref>:<template-path>
9
+ * Shorthand: a bare token with no `#` and no `:` is treated as the template path.
10
+ */
11
+ export declare function parseTemplateSpec(spec: string): Partial<TemplateSource>;
12
+ //# sourceMappingURL=parse-template-spec.d.ts.map
@@ -0,0 +1,62 @@
1
+ export class TemplateSpecParseError extends Error {
2
+ constructor(message){
3
+ super(message);
4
+ this.name = 'TemplateSpecParseError';
5
+ }
6
+ }
7
+ /**
8
+ * Parse a --template spec string into a partial TemplateSource override.
9
+ *
10
+ * Format: [<owner>/<repo>#]<ref>:<template-path>
11
+ * Shorthand: a bare token with no `#` and no `:` is treated as the template path.
12
+ */ export function parseTemplateSpec(spec) {
13
+ const trimmed = spec.trim();
14
+ if (!trimmed) {
15
+ throw new TemplateSpecParseError('Template spec is empty');
16
+ }
17
+ let rest = trimmed;
18
+ const result = {};
19
+ const hashIndex = rest.indexOf('#');
20
+ if (hashIndex !== -1) {
21
+ const repoPart = rest.slice(0, hashIndex);
22
+ rest = rest.slice(hashIndex + 1);
23
+ const slashCount = (repoPart.match(/\//g) ?? []).length;
24
+ if (slashCount !== 1) {
25
+ throw new TemplateSpecParseError(`Expected "<owner>/<repo>" before "#", got "${repoPart}"`);
26
+ }
27
+ const [owner, repo] = repoPart.split('/');
28
+ if (!owner || !repo) {
29
+ throw new TemplateSpecParseError(`Invalid owner/repo: "${repoPart}"`);
30
+ }
31
+ result.owner = owner;
32
+ result.repo = repo;
33
+ } else {
34
+ // Check for slash only in the portion before the first colon (the ref part).
35
+ // Slashes after the colon are valid multi-segment template paths.
36
+ const beforeColon = rest.includes(':') ? rest.slice(0, rest.indexOf(':')) : rest;
37
+ if (beforeColon.includes('/')) {
38
+ throw new TemplateSpecParseError(`"<owner>/<repo>" must be followed by "#<ref>" (got "${trimmed}")`);
39
+ }
40
+ }
41
+ const colonIndex = rest.indexOf(':');
42
+ if (colonIndex !== -1) {
43
+ const ref = rest.slice(0, colonIndex);
44
+ const templatePath = rest.slice(colonIndex + 1);
45
+ if (!ref) {
46
+ throw new TemplateSpecParseError('Ref before ":" is empty');
47
+ }
48
+ if (!templatePath) {
49
+ throw new TemplateSpecParseError('Template path after ":" is empty');
50
+ }
51
+ result.ref = ref;
52
+ result.templatePath = templatePath;
53
+ return result;
54
+ }
55
+ if (hashIndex !== -1) {
56
+ throw new TemplateSpecParseError('Spec with "<owner>/<repo>#<ref>" must include ":<template-path>"');
57
+ }
58
+ result.templatePath = rest;
59
+ return result;
60
+ }
61
+
62
+ //# sourceMappingURL=parse-template-spec.js.map