@payloadcms/figma 0.1.0-alpha.3 → 0.1.0-alpha.4

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 (78) hide show
  1. package/dist/api/check-project-subdomain-availability.d.ts +12 -0
  2. package/dist/api/check-project-subdomain-availability.js +32 -0
  3. package/dist/api/control-plane-error.d.ts +6 -0
  4. package/dist/api/control-plane-error.js +8 -0
  5. package/dist/api/control-plane-fetch.d.ts +7 -0
  6. package/dist/api/control-plane-fetch.js +37 -0
  7. package/dist/api/control-plane.d.ts +0 -8
  8. package/dist/api/control-plane.js +1 -47
  9. package/dist/api/create-project.d.ts +18 -0
  10. package/dist/api/create-project.js +37 -0
  11. package/dist/api/list-project-workspaces.d.ts +8 -0
  12. package/dist/api/list-project-workspaces.js +28 -0
  13. package/dist/api/project-resource-type.d.ts +1 -0
  14. package/dist/api/project-resource-type.js +1 -0
  15. package/dist/auth/headless-oauth-flow.d.ts +23 -0
  16. package/dist/auth/headless-oauth-flow.js +69 -0
  17. package/dist/auth/oauth-flow.d.ts +2 -0
  18. package/dist/auth/oauth-flow.js +13 -8
  19. package/dist/auth/token-store.d.ts +4 -1
  20. package/dist/auth/token-store.js +14 -0
  21. package/dist/auth/types.d.ts +12 -0
  22. package/dist/cli.d.ts +10 -1
  23. package/dist/cli.js +39 -4
  24. package/dist/commands/bootstrap.js +2 -1
  25. package/dist/commands/deploy.d.ts +10 -0
  26. package/dist/commands/deploy.js +39 -11
  27. package/dist/commands/detect-project-resource-type.d.ts +3 -0
  28. package/dist/commands/detect-project-resource-type.js +28 -0
  29. package/dist/commands/init.d.ts +2 -2
  30. package/dist/commands/init.js +163 -78
  31. package/dist/commands/login.d.ts +11 -1
  32. package/dist/commands/login.js +66 -4
  33. package/dist/commands/prompt-to-create-project/format-project-subdomain.d.ts +1 -0
  34. package/dist/commands/prompt-to-create-project/format-project-subdomain.js +3 -0
  35. package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.d.ts +1 -0
  36. package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.js +7 -0
  37. package/dist/commands/prompt-to-create-project/get-default-project-name.d.ts +1 -0
  38. package/dist/commands/prompt-to-create-project/get-default-project-name.js +13 -0
  39. package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.d.ts +7 -0
  40. package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.js +32 -0
  41. package/dist/commands/prompt-to-create-project/resolve-project-region.d.ts +8 -0
  42. package/dist/commands/prompt-to-create-project/resolve-project-region.js +27 -0
  43. package/dist/commands/prompt-to-create-project/resolve-project-subdomain.d.ts +9 -0
  44. package/dist/commands/prompt-to-create-project/resolve-project-subdomain.js +54 -0
  45. package/dist/commands/prompt-to-create-project/resolve-text-input.d.ts +9 -0
  46. package/dist/commands/prompt-to-create-project/resolve-text-input.js +23 -0
  47. package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.d.ts +12 -0
  48. package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.js +36 -0
  49. package/dist/commands/prompt-to-create-project/validate-subdomain.d.ts +1 -0
  50. package/dist/commands/prompt-to-create-project/validate-subdomain.js +9 -0
  51. package/dist/commands/prompt-to-create-project.d.ts +24 -0
  52. package/dist/commands/prompt-to-create-project.js +69 -0
  53. package/dist/commands/setup-agent.d.ts +12 -0
  54. package/dist/commands/setup-agent.js +123 -0
  55. package/dist/commands/write-project-configuration.d.ts +12 -0
  56. package/dist/commands/write-project-configuration.js +16 -0
  57. package/dist/db-content-api/index.js +21 -16
  58. package/dist/db-content-api/utilities/data/injectGlobalType.d.ts +6 -0
  59. package/dist/db-content-api/utilities/data/injectGlobalType.js +11 -0
  60. package/dist/db-content-api/utilities/data/stringifyNumericIds.d.ts +1 -0
  61. package/dist/db-content-api/utilities/data/stringifyNumericIds.js +19 -0
  62. package/dist/db-content-api/utilities/isRecord.d.ts +9 -0
  63. package/dist/db-content-api/utilities/isRecord.js +19 -0
  64. package/dist/db-content-api/utilities/where.js +6 -4
  65. package/dist/lib/write-agent-config.d.ts +12 -0
  66. package/dist/lib/write-agent-config.js +52 -0
  67. package/dist/skills/deploy-to-figma/SKILL.md +234 -0
  68. package/dist/skills/deploy-to-figma/references/cli-reference.md +146 -0
  69. package/dist/types.d.ts +9 -0
  70. package/dist/utils/lambda-config.js +4 -14
  71. package/dist/utils/messages.js +22 -3
  72. package/dist/utils/payload-generate.d.ts +10 -0
  73. package/dist/utils/payload-generate.js +59 -0
  74. package/dist/utils/pnpm-builds.d.ts +14 -0
  75. package/dist/utils/pnpm-builds.js +125 -0
  76. package/package.json +2 -1
  77. package/dist/lib/download-skill.d.ts +0 -12
  78. package/dist/lib/download-skill.js +0 -77
@@ -0,0 +1,13 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ export async function getDefaultProjectName(projectPath) {
4
+ try {
5
+ const packageJson = JSON.parse(await fs.readFile(path.join(projectPath, 'package.json'), 'utf-8'));
6
+ if (typeof packageJson.name === 'string' && packageJson.name.trim()) {
7
+ return packageJson.name.replace(/^@[^/]+\//, '');
8
+ }
9
+ } catch {
10
+ // Fall back to directory name.
11
+ }
12
+ return path.basename(path.resolve(projectPath)) || 'figma-project';
13
+ }
@@ -0,0 +1,7 @@
1
+ import type { AuthCredential } from '../../auth/credentials.js';
2
+ interface PromptForAvailableSubdomainArgs {
3
+ credential: AuthCredential;
4
+ initialValue: string;
5
+ }
6
+ export declare function promptForAvailableSubdomain({ credential, initialValue, }: PromptForAvailableSubdomainArgs): Promise<string>;
7
+ export {};
@@ -0,0 +1,32 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { checkProjectSubdomainAvailability } from '../../api/check-project-subdomain-availability.js';
4
+ import { formatUnavailableSubdomainReason } from './format-unavailable-subdomain-reason.js';
5
+ import { validateSubdomain } from './validate-subdomain.js';
6
+ export async function promptForAvailableSubdomain({ credential, initialValue }) {
7
+ let nextInitialValue = initialValue;
8
+ while(true){
9
+ const promptedSubdomain = await p.text({
10
+ initialValue: nextInitialValue,
11
+ message: 'Project subdomain:',
12
+ validate: validateSubdomain
13
+ });
14
+ const subdomain = unwrapPrompt(promptedSubdomain).trim();
15
+ const availability = await checkProjectSubdomainAvailability({
16
+ credential,
17
+ subdomain
18
+ });
19
+ if (availability.available) {
20
+ return subdomain;
21
+ }
22
+ p.log.error(pc.red(formatUnavailableSubdomainReason(availability.reason)));
23
+ nextInitialValue = subdomain;
24
+ }
25
+ }
26
+ function unwrapPrompt(value) {
27
+ if (p.isCancel(value)) {
28
+ p.cancel('Operation cancelled');
29
+ process.exit(0);
30
+ }
31
+ return value;
32
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProjectResourceType } from '../../api/project-resource-type.js';
2
+ interface ResolveProjectRegionArgs {
3
+ nonInteractive?: boolean;
4
+ resourceType: ProjectResourceType;
5
+ value?: string;
6
+ }
7
+ export declare function resolveProjectRegion({ nonInteractive, resourceType, value, }: ResolveProjectRegionArgs): Promise<string>;
8
+ export {};
@@ -0,0 +1,27 @@
1
+ import * as p from '@clack/prompts';
2
+ export async function resolveProjectRegion({ nonInteractive, resourceType, value }) {
3
+ if (value !== undefined) {
4
+ const region = value.trim();
5
+ if (!region) {
6
+ throw new Error('Region is required');
7
+ }
8
+ return region;
9
+ }
10
+ if (resourceType === 'vite' || nonInteractive) {
11
+ return 'us-east-1';
12
+ }
13
+ const selectedRegion = await p.select({
14
+ message: 'Select a region:',
15
+ options: [
16
+ {
17
+ label: 'US East (us-east-1)',
18
+ value: 'us-east-1'
19
+ }
20
+ ]
21
+ });
22
+ if (p.isCancel(selectedRegion)) {
23
+ p.cancel('Operation cancelled');
24
+ process.exit(0);
25
+ }
26
+ return selectedRegion;
27
+ }
@@ -0,0 +1,9 @@
1
+ import type { AuthCredential } from '../../auth/credentials.js';
2
+ interface ResolveProjectSubdomainArgs {
3
+ credential: AuthCredential;
4
+ initialValue: string;
5
+ nonInteractive?: boolean;
6
+ value?: string;
7
+ }
8
+ export declare function resolveProjectSubdomain({ credential, initialValue, nonInteractive, value, }: ResolveProjectSubdomainArgs): Promise<string>;
9
+ export {};
@@ -0,0 +1,54 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { checkProjectSubdomainAvailability } from '../../api/check-project-subdomain-availability.js';
4
+ import { formatUnavailableSubdomainReason } from './format-unavailable-subdomain-reason.js';
5
+ import { promptForAvailableSubdomain } from './prompt-for-available-subdomain.js';
6
+ import { validateSubdomain } from './validate-subdomain.js';
7
+ const GENERATED_SUBDOMAIN_ATTEMPT_COUNT = 5;
8
+ export async function resolveProjectSubdomain({ credential, initialValue, nonInteractive, value }) {
9
+ const providedSubdomain = value === undefined ? undefined : validateProvidedSubdomain(value);
10
+ if (!nonInteractive) {
11
+ if (providedSubdomain) {
12
+ const availability = await checkProjectSubdomainAvailability({
13
+ credential,
14
+ subdomain: providedSubdomain
15
+ });
16
+ if (availability.available) {
17
+ return providedSubdomain;
18
+ }
19
+ p.log.error(pc.red(formatUnavailableSubdomainReason(availability.reason)));
20
+ }
21
+ return promptForAvailableSubdomain({
22
+ credential,
23
+ initialValue: providedSubdomain ?? initialValue
24
+ });
25
+ }
26
+ const baseSubdomain = providedSubdomain ?? initialValue;
27
+ for(let attempt = 0; attempt < GENERATED_SUBDOMAIN_ATTEMPT_COUNT; attempt++){
28
+ const subdomain = attempt === 0 ? baseSubdomain : addRandomSuffix(baseSubdomain);
29
+ const availability = await checkProjectSubdomainAvailability({
30
+ credential,
31
+ subdomain
32
+ });
33
+ if (availability.available) {
34
+ return subdomain;
35
+ }
36
+ if (providedSubdomain) {
37
+ throw new Error(`${formatUnavailableSubdomainReason(availability.reason)} Pass a different --subdomain.`);
38
+ }
39
+ }
40
+ throw new Error(`Could not generate an available subdomain after ${GENERATED_SUBDOMAIN_ATTEMPT_COUNT} attempts. Pass --subdomain.`);
41
+ }
42
+ function validateProvidedSubdomain(value) {
43
+ const validationError = validateSubdomain(value);
44
+ if (validationError) {
45
+ throw new Error(validationError);
46
+ }
47
+ return value.trim();
48
+ }
49
+ function addRandomSuffix(value) {
50
+ // Append a hyphen and six random lowercase alphanumeric characters, e.g. `-a1b2c3`.
51
+ const suffix = Math.random().toString(36).slice(2, 8).padEnd(6, '0');
52
+ const prefix = value.slice(0, 53).replace(/-+$/g, '');
53
+ return `${prefix}-${suffix}`;
54
+ }
@@ -0,0 +1,9 @@
1
+ interface ResolveTextInputArgs {
2
+ fieldName: string;
3
+ initialValue: string;
4
+ message: string;
5
+ nonInteractive?: boolean;
6
+ value?: string;
7
+ }
8
+ export declare function resolveTextInput({ fieldName, initialValue, message, nonInteractive, value, }: ResolveTextInputArgs): Promise<string>;
9
+ export {};
@@ -0,0 +1,23 @@
1
+ import * as p from '@clack/prompts';
2
+ export async function resolveTextInput({ fieldName, initialValue, message, nonInteractive, value }) {
3
+ if (value !== undefined) {
4
+ const trimmedValue = value.trim();
5
+ if (!trimmedValue) {
6
+ throw new Error(`${fieldName} is required`);
7
+ }
8
+ return trimmedValue;
9
+ }
10
+ if (nonInteractive) {
11
+ return initialValue;
12
+ }
13
+ const promptedValue = await p.text({
14
+ initialValue,
15
+ message,
16
+ validate: (candidate)=>candidate?.trim() ? undefined : `${fieldName} is required`
17
+ });
18
+ if (p.isCancel(promptedValue)) {
19
+ p.cancel('Operation cancelled');
20
+ process.exit(0);
21
+ }
22
+ return promptedValue.trim();
23
+ }
@@ -0,0 +1,12 @@
1
+ interface ProjectWorkspace {
2
+ isDefault: boolean;
3
+ name: string;
4
+ planKey: string;
5
+ }
6
+ interface ResolveWorkspacePlanKeyArgs {
7
+ nonInteractive?: boolean;
8
+ value?: string;
9
+ workspaces: ProjectWorkspace[];
10
+ }
11
+ export declare function resolveWorkspacePlanKey({ nonInteractive, value, workspaces, }: ResolveWorkspacePlanKeyArgs): Promise<string>;
12
+ export {};
@@ -0,0 +1,36 @@
1
+ import * as p from '@clack/prompts';
2
+ export async function resolveWorkspacePlanKey({ nonInteractive, value, workspaces }) {
3
+ if (value !== undefined) {
4
+ const planKey = value.trim();
5
+ const workspace = workspaces.find((candidate)=>candidate.planKey === planKey);
6
+ if (!workspace) {
7
+ throw new Error(`Workspace plan key "${planKey}" is not eligible. Available: ${formatWorkspaces(workspaces)}`);
8
+ }
9
+ return workspace.planKey;
10
+ }
11
+ if (workspaces.length === 1) {
12
+ return workspaces[0].planKey;
13
+ }
14
+ if (nonInteractive) {
15
+ const defaultWorkspace = workspaces.find((workspace)=>workspace.isDefault);
16
+ if (defaultWorkspace) {
17
+ return defaultWorkspace.planKey;
18
+ }
19
+ throw new Error(`Multiple eligible Figma workspaces. Pass --workspace <plan-key>. Available: ${formatWorkspaces(workspaces)}`);
20
+ }
21
+ const selectedWorkspacePlanKey = await p.select({
22
+ message: 'Select a Figma workspace:',
23
+ options: workspaces.map((workspace)=>({
24
+ label: workspace.name,
25
+ value: workspace.planKey
26
+ }))
27
+ });
28
+ if (p.isCancel(selectedWorkspacePlanKey)) {
29
+ p.cancel('Operation cancelled');
30
+ process.exit(0);
31
+ }
32
+ return selectedWorkspacePlanKey;
33
+ }
34
+ function formatWorkspaces(workspaces) {
35
+ return workspaces.map((workspace)=>`${workspace.name} (${workspace.planKey})`).join(', ');
36
+ }
@@ -0,0 +1 @@
1
+ export declare function validateSubdomain(value: string | undefined): "Subdomain is required" | "Use 1–60 lowercase letters, numbers, or hyphens" | undefined;
@@ -0,0 +1,9 @@
1
+ export function validateSubdomain(value) {
2
+ if (!value?.trim()) {
3
+ return 'Subdomain is required';
4
+ }
5
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,58}[a-z0-9])?$/.test(value.trim())) {
6
+ return 'Use 1–60 lowercase letters, numbers, or hyphens';
7
+ }
8
+ return undefined;
9
+ }
@@ -0,0 +1,24 @@
1
+ import type { ProjectResourceType } from '../api/project-resource-type.js';
2
+ import type { AuthCredential } from '../auth/credentials.js';
3
+ interface ProjectCreationInputs {
4
+ description?: string;
5
+ name?: string;
6
+ region?: string;
7
+ subdomain?: string;
8
+ workspacePlanKey?: string;
9
+ }
10
+ interface PromptToCreateProjectArgs {
11
+ credential: AuthCredential;
12
+ inputs?: ProjectCreationInputs;
13
+ nonInteractive?: boolean;
14
+ projectPath: string;
15
+ resourceType: ProjectResourceType;
16
+ }
17
+ interface PromptToCreateProjectResponse {
18
+ domainName: string;
19
+ id: string;
20
+ name: string;
21
+ }
22
+ /** Prompt for required project fields and create a blank Figma CMS project. */
23
+ export declare function promptToCreateProject({ credential, inputs, nonInteractive, projectPath, resourceType, }: PromptToCreateProjectArgs): Promise<PromptToCreateProjectResponse>;
24
+ export {};
@@ -0,0 +1,69 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { createProject } from '../api/create-project.js';
4
+ import { listProjectWorkspaces } from '../api/list-project-workspaces.js';
5
+ import { formatProjectSubdomain } from './prompt-to-create-project/format-project-subdomain.js';
6
+ import { getDefaultProjectName } from './prompt-to-create-project/get-default-project-name.js';
7
+ import { resolveProjectRegion } from './prompt-to-create-project/resolve-project-region.js';
8
+ import { resolveProjectSubdomain } from './prompt-to-create-project/resolve-project-subdomain.js';
9
+ import { resolveTextInput } from './prompt-to-create-project/resolve-text-input.js';
10
+ import { resolveWorkspacePlanKey } from './prompt-to-create-project/resolve-workspace-plan-key.js';
11
+ /** Prompt for required project fields and create a blank Figma CMS project. */ export async function promptToCreateProject({ credential, inputs, nonInteractive, projectPath, resourceType }) {
12
+ const workspaces = await listProjectWorkspaces(credential);
13
+ if (workspaces.length === 0) {
14
+ if (nonInteractive) {
15
+ throw new Error('No eligible Figma workspaces found.');
16
+ }
17
+ p.log.error(pc.red('No eligible Figma workspaces found.'));
18
+ process.exit(1);
19
+ }
20
+ const defaultProjectName = await getDefaultProjectName(projectPath);
21
+ const projectName = await resolveTextInput({
22
+ fieldName: 'Project name',
23
+ initialValue: defaultProjectName,
24
+ message: 'Project name:',
25
+ nonInteractive,
26
+ value: inputs?.name
27
+ });
28
+ const projectDescription = await resolveTextInput({
29
+ fieldName: 'Description',
30
+ initialValue: `${projectName} project`,
31
+ message: 'Project description:',
32
+ nonInteractive,
33
+ value: inputs?.description
34
+ });
35
+ const projectSubdomain = await resolveProjectSubdomain({
36
+ credential,
37
+ initialValue: formatProjectSubdomain(projectName),
38
+ nonInteractive,
39
+ value: inputs?.subdomain
40
+ });
41
+ const parentPlanKey = await resolveWorkspacePlanKey({
42
+ nonInteractive,
43
+ value: inputs?.workspacePlanKey,
44
+ workspaces
45
+ });
46
+ const region = await resolveProjectRegion({
47
+ nonInteractive,
48
+ resourceType,
49
+ value: inputs?.region
50
+ });
51
+ const spinner = p.spinner();
52
+ spinner.start('Creating Figma project...');
53
+ try {
54
+ const project = await createProject({
55
+ name: projectName,
56
+ credential,
57
+ description: projectDescription,
58
+ parentPlanKey,
59
+ region,
60
+ resourceType,
61
+ subdomain: projectSubdomain
62
+ });
63
+ spinner.stop(pc.green(`✓ Project created: ${project.domainName}`));
64
+ return project;
65
+ } catch (error) {
66
+ spinner.stop(pc.red('✗ Failed to create project'));
67
+ throw error;
68
+ }
69
+ }
@@ -0,0 +1,12 @@
1
+ export interface SetupAgentOptions {
2
+ global?: boolean;
3
+ project?: boolean;
4
+ yes?: boolean;
5
+ }
6
+ /** Install or update the bundled deploy skill through the standard skills CLI. */
7
+ export declare function setupAgentCommand(options: SetupAgentOptions, dependencies?: {
8
+ homeDir?: string;
9
+ isTTY?: boolean;
10
+ projectDir?: string;
11
+ skillDir?: string;
12
+ }): Promise<void>;
@@ -0,0 +1,123 @@
1
+ import * as p from '@clack/prompts';
2
+ import spawn from 'cross-spawn';
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs/promises';
5
+ import { createRequire } from 'node:module';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ const SKILL_NAME = 'deploy-to-figma';
9
+ const require = createRequire(import.meta.url);
10
+ const SKILLS_CLI_PATH = require.resolve('skills/bin/cli.mjs');
11
+ /** Install or update the bundled deploy skill through the standard skills CLI. */ export async function setupAgentCommand(options, dependencies = {}) {
12
+ if (options.global && options.project) {
13
+ throw new Error('Use either --global or --project, not both.');
14
+ }
15
+ const isTTY = dependencies.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
16
+ const scope = await resolveScope(options, isTTY);
17
+ const projectDir = dependencies.projectDir ?? process.cwd();
18
+ const homeDir = dependencies.homeDir ?? os.homedir();
19
+ const skillDir = dependencies.skillDir ?? resolveBundledSkillDir();
20
+ const installedDir = path.join(scope === 'global' ? homeDir : projectDir, '.agents', 'skills', SKILL_NAME);
21
+ const [bundledHash, installedHash] = await Promise.all([
22
+ hashDirectory(skillDir),
23
+ hashDirectory(installedDir)
24
+ ]);
25
+ if (!bundledHash) {
26
+ throw new Error(`Bundled ${SKILL_NAME} skill was not found at ${skillDir}.`);
27
+ }
28
+ if (bundledHash === installedHash) {
29
+ p.log.success(`✓ ${SKILL_NAME} skill is already up to date (${scope})`);
30
+ return;
31
+ }
32
+ p.log.step(`${installedHash ? 'Updating' : 'Installing'} ${SKILL_NAME} skill (${scope})...`);
33
+ const args = [
34
+ 'add',
35
+ skillDir,
36
+ '--skill',
37
+ SKILL_NAME,
38
+ '--copy',
39
+ ...scope === 'global' ? [
40
+ '--global'
41
+ ] : [],
42
+ ...options.yes || !isTTY ? [
43
+ '--yes'
44
+ ] : []
45
+ ];
46
+ const exitCode = await run(process.execPath, [
47
+ SKILLS_CLI_PATH,
48
+ ...args
49
+ ], projectDir);
50
+ if (exitCode !== 0) {
51
+ throw new Error(`Skill installation failed with exit code ${exitCode}.`);
52
+ }
53
+ p.log.success(`✓ ${SKILL_NAME} skill ${installedHash ? 'updated' : 'installed'} (${scope})`);
54
+ }
55
+ async function resolveScope(options, isTTY) {
56
+ if (options.global) {
57
+ return 'global';
58
+ }
59
+ if (options.project) {
60
+ return 'project';
61
+ }
62
+ if (!isTTY) {
63
+ throw new Error('Non-interactive setup requires either --global or --project.');
64
+ }
65
+ const selected = await p.select({
66
+ initialValue: 'global',
67
+ message: 'Where should the Figma deploy skill be installed?',
68
+ options: [
69
+ {
70
+ label: 'Globally (recommended)',
71
+ value: 'global'
72
+ },
73
+ {
74
+ label: 'Current project only',
75
+ value: 'project'
76
+ }
77
+ ]
78
+ });
79
+ if (p.isCancel(selected)) {
80
+ throw new Error('Skill installation cancelled.');
81
+ }
82
+ return selected;
83
+ }
84
+ function resolveBundledSkillDir() {
85
+ return path.resolve(import.meta.dirname, '..', 'skills', SKILL_NAME);
86
+ }
87
+ async function hashDirectory(directory) {
88
+ if (!await pathExists(directory)) {
89
+ return null;
90
+ }
91
+ const hash = crypto.createHash('sha256');
92
+ async function visit(currentDir, relativeDir = '') {
93
+ const entries = await fs.readdir(currentDir, {
94
+ withFileTypes: true
95
+ });
96
+ entries.sort((left, right)=>left.name.localeCompare(right.name));
97
+ for (const entry of entries){
98
+ const relativePath = path.join(relativeDir, entry.name);
99
+ const absolutePath = path.join(currentDir, entry.name);
100
+ if (entry.isDirectory()) {
101
+ await visit(absolutePath, relativePath);
102
+ } else if (entry.isFile()) {
103
+ hash.update(relativePath);
104
+ hash.update(await fs.readFile(absolutePath));
105
+ }
106
+ }
107
+ }
108
+ await visit(directory);
109
+ return hash.digest('hex');
110
+ }
111
+ function run(command, args, cwd) {
112
+ return new Promise((resolve, reject)=>{
113
+ const child = spawn(command, args, {
114
+ cwd,
115
+ stdio: 'inherit'
116
+ });
117
+ child.once('error', reject);
118
+ child.once('close', (code)=>resolve(code ?? 1));
119
+ });
120
+ }
121
+ async function pathExists(filePath) {
122
+ return fs.access(filePath).then(()=>true).catch(()=>false);
123
+ }
@@ -0,0 +1,12 @@
1
+ import type { BootstrapInfo } from '../api/control-plane.js';
2
+ import type { TokenStore } from '../auth/token-store.js';
3
+ interface WriteProjectConfigurationArgs {
4
+ bootstrapInfo: BootstrapInfo | null;
5
+ environmentName: string;
6
+ projectDir: string;
7
+ projectId: string;
8
+ shouldWriteEnvironmentVariables?: boolean;
9
+ tokenStore: TokenStore;
10
+ }
11
+ export declare function writeProjectConfiguration({ bootstrapInfo, environmentName, projectDir, projectId, shouldWriteEnvironmentVariables, tokenStore, }: WriteProjectConfigurationArgs): Promise<void>;
12
+ export {};
@@ -0,0 +1,16 @@
1
+ import { getInfraEnvironment } from '../constants.js';
2
+ import { cacheAllEnvironments } from '../utils/cache-bootstrap.js';
3
+ import { addOrUpdateEnvVar } from '../utils/env-management.js';
4
+ export async function writeProjectConfiguration({ bootstrapInfo, environmentName, projectDir, projectId, shouldWriteEnvironmentVariables = true, tokenStore }) {
5
+ if (shouldWriteEnvironmentVariables) {
6
+ await addOrUpdateEnvVar(projectDir, 'FIGMA_PROJECT_ID', projectId);
7
+ await addOrUpdateEnvVar(projectDir, 'FIGMA_ENVIRONMENT_NAME', environmentName);
8
+ const infraEnvironment = getInfraEnvironment();
9
+ if (infraEnvironment !== 'production') {
10
+ await addOrUpdateEnvVar(projectDir, 'FIGMA_INFRA_ENV', infraEnvironment);
11
+ }
12
+ }
13
+ if (bootstrapInfo) {
14
+ cacheAllEnvironments(tokenStore, projectId, bootstrapInfo);
15
+ }
16
+ }
@@ -9,6 +9,7 @@ import { unwrapDocument, unwrapFindResponse } from './temp-utilities/unwrapDocum
9
9
  import { createAuthMiddleware, createErrorMiddleware } from './utilities/auth.js';
10
10
  import { getClientVersionHeaders } from './utilities/clientVersionHeaders.js';
11
11
  import { dataToContentAPI, resolveVersionContent } from './utilities/data/index.js';
12
+ import { injectGlobalType } from './utilities/data/injectGlobalType.js';
12
13
  import { validateRelationshipIds } from './utilities/data/validateRelationships.js';
13
14
  import { convertPayloadJoinsToContentAPI } from './utilities/joins.js';
14
15
  import { addFallbackLocale } from './utilities/locale/index.js';
@@ -301,9 +302,12 @@ async function updateVersion(args) {
301
302
  });
302
303
  }
303
304
  async function deleteVersions(args) {
304
- const collection = args.collection;
305
+ // Globals are addressed by `globalSlug` and map to the `_global-<slug>` collection.
306
+ // Only one of `collection`/`globalSlug` is set.
307
+ const globalSlug = args.globalSlug != null ? getGlobalSlug(args.globalSlug) : undefined;
308
+ const collection = args.collection ?? globalSlug;
305
309
  if (collection == null) {
306
- throw new Error('deleteVersions requires a collection');
310
+ throw new Error('deleteVersions requires a collection or globalSlug');
307
311
  }
308
312
  const locale = addFallbackLocale(args.locale, this.payload);
309
313
  const { error, response: httpResponse } = await this.client.POST('/api/v1/document_versions:delete', {
@@ -621,16 +625,22 @@ async function upsert(args) {
621
625
  data: args.data,
622
626
  payload: this.payload
623
627
  });
628
+ // `createdAt` is create-time metadata, so seed it on `createOnMissing` (used only when no
629
+ // document matches) rather than on `doc`. The server preserves an existing document's
630
+ // `createdAt` on update precisely because `doc` omits it; `updatedAt` is still refreshed on
631
+ // every write via `dataToContentAPI`. Sending `createdAt` on `doc` would overwrite it on
632
+ // every update (e.g. repeated `updateGlobal` calls).
633
+ const createdAt = new Date().toISOString();
624
634
  const { data: contentAPIResponse, error, response: httpResponse } = await this.client.POST('/api/v1/documents:update', {
625
635
  body: {
626
636
  collection: args.collection,
627
637
  contentSystemId: this.contentSystemId,
628
638
  createOnMissing: {
629
- id: documentId
639
+ id: documentId,
640
+ createdAt
630
641
  },
631
642
  doc: dataToContentAPI(this.payload, args.collection, args.data, {
632
- applyDefaults: true,
633
- createdAt: true
643
+ applyDefaults: true
634
644
  }),
635
645
  environmentName: this.environmentName,
636
646
  locale,
@@ -716,13 +726,13 @@ async function updateJobs(args) {
716
726
  payload: this.payload
717
727
  }));
718
728
  }
719
- function createGlobal(args) {
729
+ async function createGlobal(args) {
720
730
  // Globals are singletons identified by their slug. Use upsert so concurrent calls
721
731
  // (e.g. handleSchedules processing multiple queueables with the same null jobStats)
722
732
  // don't fail with "A record with this field already exists". The deep-merge semantics
723
733
  // of the update path are safe here: callers that want a fresh create (first run) get
724
734
  // an effective create, and callers that race a prior create get a merge instead.
725
- return this.upsert({
735
+ const doc = await this.upsert({
726
736
  collection: getGlobalSlug(args.slug),
727
737
  data: {
728
738
  ...args.data,
@@ -734,6 +744,7 @@ function createGlobal(args) {
734
744
  }
735
745
  }
736
746
  });
747
+ return injectGlobalType(doc, args.slug);
737
748
  }
738
749
  async function findGlobal(args) {
739
750
  const doc = await this.findOne({
@@ -741,17 +752,10 @@ async function findGlobal(args) {
741
752
  locale: args.locale,
742
753
  where: args.where ?? {}
743
754
  });
744
- if (doc != null) {
745
- ;
746
- doc.globalType = args.slug;
747
- }
748
- return doc;
755
+ return injectGlobalType(doc, args.slug);
749
756
  }
750
757
  async function updateGlobal(args) {
751
- // TODO: upsert's `createOnMissing` path generates a new `createdAt` on every call,
752
- // which overwrites the original timestamp on updates. The fix belongs in the Content API:
753
- // `createOnMissing` should preserve `createdAt` on updates and only set it on creates.
754
- return this.upsert({
758
+ const doc = await this.upsert({
755
759
  collection: getGlobalSlug(args.slug),
756
760
  data: args.data,
757
761
  where: {
@@ -760,6 +764,7 @@ async function updateGlobal(args) {
760
764
  }
761
765
  }
762
766
  });
767
+ return injectGlobalType(doc, args.slug);
763
768
  }
764
769
  // TODO: remove Omit when this PR is released: https://github.com/payloadcms/payload/pull/15183
765
770
  function findGlobalVersions(args) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Payload expects globals to carry a `globalType` (their slug) alongside the stored data.
3
+ * Globals are stored as `_global-<slug>` collections that have no such column, so the adapter
4
+ * attaches it to every global document it returns.
5
+ */
6
+ export declare function injectGlobalType<T>(doc: T, slug: string): T;
@@ -0,0 +1,11 @@
1
+ import { isRecord } from '../isRecord.js';
2
+ /**
3
+ * Payload expects globals to carry a `globalType` (their slug) alongside the stored data.
4
+ * Globals are stored as `_global-<slug>` collections that have no such column, so the adapter
5
+ * attaches it to every global document it returns.
6
+ */ export function injectGlobalType(doc, slug) {
7
+ return isRecord(doc) ? {
8
+ ...doc,
9
+ globalType: slug
10
+ } : doc;
11
+ }
@@ -0,0 +1 @@
1
+ export declare function stringifyNumericIds(value: unknown): unknown;