@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.
- package/dist/api/check-project-subdomain-availability.d.ts +12 -0
- package/dist/api/check-project-subdomain-availability.js +32 -0
- package/dist/api/control-plane-error.d.ts +6 -0
- package/dist/api/control-plane-error.js +8 -0
- package/dist/api/control-plane-fetch.d.ts +7 -0
- package/dist/api/control-plane-fetch.js +37 -0
- package/dist/api/control-plane.d.ts +0 -8
- package/dist/api/control-plane.js +1 -47
- package/dist/api/create-project.d.ts +18 -0
- package/dist/api/create-project.js +37 -0
- package/dist/api/list-project-workspaces.d.ts +8 -0
- package/dist/api/list-project-workspaces.js +28 -0
- package/dist/api/project-resource-type.d.ts +1 -0
- package/dist/api/project-resource-type.js +1 -0
- package/dist/auth/headless-oauth-flow.d.ts +23 -0
- package/dist/auth/headless-oauth-flow.js +69 -0
- package/dist/auth/oauth-flow.d.ts +2 -0
- package/dist/auth/oauth-flow.js +13 -8
- package/dist/auth/token-store.d.ts +4 -1
- package/dist/auth/token-store.js +14 -0
- package/dist/auth/types.d.ts +12 -0
- package/dist/cli.d.ts +10 -1
- package/dist/cli.js +39 -4
- package/dist/commands/bootstrap.js +2 -1
- package/dist/commands/deploy.d.ts +10 -0
- package/dist/commands/deploy.js +39 -11
- package/dist/commands/detect-project-resource-type.d.ts +3 -0
- package/dist/commands/detect-project-resource-type.js +28 -0
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +163 -78
- package/dist/commands/login.d.ts +11 -1
- package/dist/commands/login.js +66 -4
- package/dist/commands/prompt-to-create-project/format-project-subdomain.d.ts +1 -0
- package/dist/commands/prompt-to-create-project/format-project-subdomain.js +3 -0
- package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.d.ts +1 -0
- package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.js +7 -0
- package/dist/commands/prompt-to-create-project/get-default-project-name.d.ts +1 -0
- package/dist/commands/prompt-to-create-project/get-default-project-name.js +13 -0
- package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.d.ts +7 -0
- package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.js +32 -0
- package/dist/commands/prompt-to-create-project/resolve-project-region.d.ts +8 -0
- package/dist/commands/prompt-to-create-project/resolve-project-region.js +27 -0
- package/dist/commands/prompt-to-create-project/resolve-project-subdomain.d.ts +9 -0
- package/dist/commands/prompt-to-create-project/resolve-project-subdomain.js +54 -0
- package/dist/commands/prompt-to-create-project/resolve-text-input.d.ts +9 -0
- package/dist/commands/prompt-to-create-project/resolve-text-input.js +23 -0
- package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.d.ts +12 -0
- package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.js +36 -0
- package/dist/commands/prompt-to-create-project/validate-subdomain.d.ts +1 -0
- package/dist/commands/prompt-to-create-project/validate-subdomain.js +9 -0
- package/dist/commands/prompt-to-create-project.d.ts +24 -0
- package/dist/commands/prompt-to-create-project.js +69 -0
- package/dist/commands/setup-agent.d.ts +12 -0
- package/dist/commands/setup-agent.js +123 -0
- package/dist/commands/write-project-configuration.d.ts +12 -0
- package/dist/commands/write-project-configuration.js +16 -0
- package/dist/db-content-api/index.js +21 -16
- package/dist/db-content-api/utilities/data/injectGlobalType.d.ts +6 -0
- package/dist/db-content-api/utilities/data/injectGlobalType.js +11 -0
- package/dist/db-content-api/utilities/data/stringifyNumericIds.d.ts +1 -0
- package/dist/db-content-api/utilities/data/stringifyNumericIds.js +19 -0
- package/dist/db-content-api/utilities/isRecord.d.ts +9 -0
- package/dist/db-content-api/utilities/isRecord.js +19 -0
- package/dist/db-content-api/utilities/where.js +6 -4
- package/dist/lib/write-agent-config.d.ts +12 -0
- package/dist/lib/write-agent-config.js +52 -0
- package/dist/skills/deploy-to-figma/SKILL.md +234 -0
- package/dist/skills/deploy-to-figma/references/cli-reference.md +146 -0
- package/dist/types.d.ts +9 -0
- package/dist/utils/lambda-config.js +4 -14
- package/dist/utils/messages.js +22 -3
- package/dist/utils/payload-generate.d.ts +10 -0
- package/dist/utils/payload-generate.js +59 -0
- package/dist/utils/pnpm-builds.d.ts +14 -0
- package/dist/utils/pnpm-builds.js +125 -0
- package/package.json +2 -1
- package/dist/lib/download-skill.d.ts +0 -12
- 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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|