Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion packages/create-payload-app/src/lib/create-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
import path from 'path'

import type {
AgentType,
CliArgs,
DbDetails,
PackageManager,
Expand All @@ -18,9 +19,11 @@ import { debug, error, info, warning } from '../utils/log.js'
import { configurePayloadConfig } from './configure-payload-config.js'
import { configurePluginProject } from './configure-plugin-project.js'
import { downloadExample } from './download-example.js'
import { downloadSkill } from './download-skill.js'
import { downloadTemplate } from './download-template.js'
import { generateSecret } from './generate-secret.js'
import { manageEnvFiles } from './manage-env-files.js'
import { getAgentChoice } from './select-agent.js'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
Expand Down Expand Up @@ -72,14 +75,15 @@ type TemplateOrExample =

export async function createProject(
args: {
agentType?: AgentType
cliArgs: CliArgs
dbDetails?: DbDetails
packageManager: PackageManager
projectDir: string
projectName: string
} & TemplateOrExample,
): Promise<void> {
const { cliArgs, dbDetails, packageManager, projectDir, projectName } = args
const { agentType, cliArgs, dbDetails, packageManager, projectDir, projectName } = args

if (cliArgs['--dry-run']) {
debug(`Dry run: Creating project in ${chalk.green(projectDir)}`)
Expand Down Expand Up @@ -170,6 +174,31 @@ export async function createProject(
template: 'template' in args ? args.template : undefined,
})

if (agentType) {
spinner.message('Installing agent skill...')
try {
await downloadSkill({
agentType,
branch: cliArgs['--branch'] || undefined,
debug: cliArgs['--debug'],
projectDir,
})

const { configFile, skillsDir } = getAgentChoice(agentType)
const skillPath = `${skillsDir}/payload`
const configContent =
configFile === 'CLAUDE.md'
? `# Claude Code\n\nThis project uses the Payload CMS skill at \`${skillPath}/\`.\nStart with \`${skillPath}/SKILL.md\` for a quick reference, then see \`${skillPath}/reference/\` for detailed docs.\n`
: `# Agents\n\nThis project uses the Payload CMS skill at \`${skillPath}/\`.\nStart with \`${skillPath}/SKILL.md\` for a quick reference, then see \`${skillPath}/reference/\` for detailed docs.\n`
await fse.writeFile(path.resolve(projectDir, configFile), configContent)
} catch (err) {
if (cliArgs['--debug'] && err instanceof Error) {
debug(`Failed to download skill: ${err.message}`)
}
warning('Could not download agent skill. You can install it manually later.')
}
}

if (!cliArgs['--no-deps']) {
info(`Using ${packageManager}.\n`)
spinner.message('Installing dependencies...')
Expand Down
53 changes: 53 additions & 0 deletions packages/create-payload-app/src/lib/download-skill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import fse from 'fs-extra'
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import path from 'path'
import { x } from 'tar'

import type { AgentType } from '../types.js'

import { debug as debugLog } from '../utils/log.js'
import { getSkillsDir } from './select-agent.js'

export async function downloadSkill(args: {
agentType: AgentType
branch?: string
debug?: boolean
projectDir: string
}): Promise<void> {
const { agentType, branch = 'main', debug, projectDir } = args

const skillsDir = getSkillsDir(agentType)
const destDir = path.join(projectDir, skillsDir, 'payload')

await fse.mkdirp(destDir)

const url = `https://codeload.github.com/payloadcms/payload/tar.gz/${branch}`
const filter = `payload-${branch.replace(/^v/, '').replaceAll('/', '-')}/tools/claude-plugin/skills/payload/`

if (debug) {
debugLog(`Downloading skill for agent: ${agentType}`)
debugLog(`Skill codeload url: ${url}`)
debugLog(`Skill filter: ${filter}`)
debugLog(`Skill destination: ${destDir}`)
}

const res = await fetch(url)

if (!res.ok) {
throw new Error(`Failed to download skill: ${res.status} ${res.statusText} from ${url}`)
}

if (!res.body) {
throw new Error(`Failed to download skill: empty response from ${url}`)
}

await pipeline(
Readable.from(res.body as unknown as NodeJS.ReadableStream),
x({
cwd: destDir,
filter: (p) => p.includes(filter),
strip: filter.split('/').length - 1,
}),
)
}
73 changes: 73 additions & 0 deletions packages/create-payload-app/src/lib/select-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as p from '@clack/prompts'

import type { AgentType, CliArgs } from '../types.js'

type AgentChoice = {
/** File to write at project root pointing agents to the skill */
configFile: 'AGENTS.md' | 'CLAUDE.md'
label: string
skillsDir: string
value: AgentType
}

export const agentChoices: AgentChoice[] = [
{ configFile: 'CLAUDE.md', label: 'Claude Code', skillsDir: '.claude/skills', value: 'claude' },
{ configFile: 'AGENTS.md', label: 'Codex', skillsDir: '.agents/skills', value: 'codex' },
{ configFile: 'AGENTS.md', label: 'Cursor', skillsDir: '.agents/skills', value: 'cursor' },
]

const validAgentValues = agentChoices.map((c) => c.value)

export function getAgentChoice(agentType: AgentType): AgentChoice {
const choice = agentChoices.find((c) => c.value === agentType)
if (!choice) {
throw new Error(`Unknown agent type: ${agentType}`)
}
return choice
}

export function getSkillsDir(agentType: AgentType): string {
return getAgentChoice(agentType).skillsDir
}

export async function selectAgent(args: { cliArgs: CliArgs }): Promise<AgentType | undefined> {
const { cliArgs } = args

if (cliArgs['--no-agent']) {
return undefined
}

if (cliArgs['--agent']) {
const value = cliArgs['--agent'] as AgentType
if (!validAgentValues.includes(value)) {
throw new Error(
`Invalid agent type: ${cliArgs['--agent']}. Valid types are: ${validAgentValues.join(', ')}`,
)
}
return value
}

const selected = await p.select<
{ label: string; value: 'none' | AgentType }[],
'none' | AgentType
>({
message: 'Select a coding agent to install the Payload skill for',
options: [
...agentChoices.map((choice) => ({
label: choice.label,
value: choice.value,
})),
{ label: 'None', value: 'none' as const },
],
})

if (p.isCancel(selected)) {
process.exit(0)
}

if (selected === 'none') {
return undefined
}

return selected
}
13 changes: 13 additions & 0 deletions packages/create-payload-app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getNextAppDetails, initNext } from './lib/init-next.js'
import { manageEnvFiles } from './lib/manage-env-files.js'
import { parseProjectName } from './lib/parse-project-name.js'
import { parseTemplate } from './lib/parse-template.js'
import { selectAgent } from './lib/select-agent.js'
import { selectDb } from './lib/select-db.js'
import { getValidTemplates, validateTemplate } from './lib/templates.js'
import { updatePayloadInProject } from './lib/update-payload-in-project.js'
Expand All @@ -36,6 +37,7 @@ export class Main {
// @ts-expect-error bad typings
this.args = arg(
{
'--agent': String,
'--branch': String,
'--db': String,
'--db-accept-recommended': Boolean,
Expand All @@ -51,6 +53,9 @@ export class Main {
// Next.js
'--init-next': Boolean, // TODO: Is this needed if we detect if inside Next.js project?

// Agent
'--no-agent': Boolean,

// Package manager
'--no-deps': Boolean,
'--use-bun': Boolean,
Expand All @@ -67,6 +72,7 @@ export class Main {
'--dry-run': Boolean,

// Aliases
'-a': '--agent',
'-d': '--db',
'-e': '--example',
'-h': '--help',
Expand Down Expand Up @@ -231,7 +237,10 @@ export class Main {
process.exit(1)
}

const agentType = await selectAgent({ cliArgs: this.args })

await createProject({
agentType,
cliArgs: this.args,
example,
packageManager,
Expand All @@ -255,7 +264,9 @@ export class Main {

switch (template.type) {
case 'plugin': {
const agentType = await selectAgent({ cliArgs: this.args })
await createProject({
agentType,
cliArgs: this.args,
packageManager,
projectDir,
Expand All @@ -266,8 +277,10 @@ export class Main {
}
case 'starter': {
const dbDetails = await selectDb(this.args, projectName, template)
const agentType = await selectAgent({ cliArgs: this.args })

await createProject({
agentType,
cliArgs: this.args,
dbDetails,
packageManager,
Expand Down
5 changes: 5 additions & 0 deletions packages/create-payload-app/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type arg from 'arg'
import type { ALL_DATABASE_ADAPTERS, ALL_STORAGE_ADAPTERS } from './lib/ast/types.js'

export interface Args extends arg.Spec {
'--agent': StringConstructor
'--beta': BooleanConstructor
'--branch': StringConstructor
'--db': StringConstructor
Expand All @@ -17,6 +18,7 @@ export interface Args extends arg.Spec {
'--local-example': StringConstructor
'--local-template': StringConstructor
'--name': StringConstructor
'--no-agent': BooleanConstructor
'--no-deps': BooleanConstructor
'--no-git': BooleanConstructor
'--secret': StringConstructor
Expand All @@ -28,6 +30,7 @@ export interface Args extends arg.Spec {

// Aliases

'-a': string
'-e': string
'-h': string
'-n': string
Expand Down Expand Up @@ -93,3 +96,5 @@ export type NextAppDetails = {
export type NextConfigType = 'cjs' | 'esm' | 'ts'

export type StorageAdapterType = (typeof ALL_STORAGE_ADAPTERS)[number]

export type AgentType = 'claude' | 'codex' | 'cursor'
5 changes: 5 additions & 0 deletions packages/create-payload-app/src/utils/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export function helpMessage(): void {

{dim Available templates: ${formatTemplates(validTemplates)}}

-a {underline agent_name} Set coding agent (claude, codex, cursor)

{dim Installs the Payload skill for the selected agent}

--no-agent Skip agent skill installation
--use-npm Use npm to install dependencies
--use-yarn Use yarn to install dependencies
--use-pnpm Use pnpm to install dependencies
Expand Down
Loading
Loading