Skip to content

Commit 9f9f343

Browse files
authored
feat(cpa): add --agent flag for coding agent skill installation (#16278)
# Overview Replace hard-coded `.cursor/rules` and `AGENTS.md` files in templates with a dynamic `--agent` flag in `create-payload-app`. When a user selects a coding agent during project creation, the Payload skill (`SKILL.md` + `reference/` docs) is downloaded from GitHub and placed in the correct directory for that agent. A `CLAUDE.md` or `AGENTS.md` is written at the project root to point the agent to the skill. Supports `claude`, `codex`, `cursor` initially. <img width="514" height="386" alt="image" src="https://github.com/user-attachments/assets/c5bc9e68-0c02-4881-ae6c-957e465e9f96" /> ## Key Changes - **New `--agent` / `-a` flag and `--no-agent` flag** - Supports `claude`, `codex`, and `cursor` as agent values - Interactive prompt shown when no flag is provided, with a "None" option to skip - `--no-agent` skips the prompt entirely, matching the `--no-deps` / `--no-git` pattern - **Runtime skill download via GitHub tarball** - New `download-skill.ts` module reuses the same `codeload.github.com` tarball + filter approach as `download-template.ts` - Downloads `tools/claude-plugin/skills/payload/` and extracts it to the agent's skill directory - Claude uses `.claude/skills/payload/`, Codex and Cursor use `.agents/skills/payload/` - Non-fatal on failure — project creation continues with a warning - **Agent config file at project root** - Claude → `CLAUDE.md`, Codex/Cursor → `AGENTS.md` (never both) - Points the agent to the installed skill location for discoverability - Only written on successful skill download - **Removed static agent files from templates** - Deleted `templates/_agents/` directory (source for copied rules) - Deleted `AGENTS.md` and `.cursor/rules/` from all 8 templates (~43k lines removed) - Stripped `skipAgents` / `copyAgentsFiles` logic from `generate-template-variations.ts` - **Added Common Gotchas and Best Practices to SKILL.md** - Closes a content gap vs. the old `AGENTS.md` — same 10 gotchas and 5 best-practice categories now in the skill ## Design Decisions Agent selection follows the existing `select-db.ts` pattern: check CLI flag first, validate, fall back to interactive prompt. This keeps the codebase consistent and the new feature immediately familiar to anyone who has worked on CPA. The skill is downloaded at runtime rather than bundled into the CPA package. This means the skill content always matches the latest `main` branch without requiring a CPA release. The `--branch` flag (already used for template downloads) controls which branch the skill is fetched from, keeping template and skill versions in sync. Codex and Cursor both use `.agents/skills/` (the universal skills directory convention), while Claude uses its own `.claude/skills/`. The mapping is defined in a single `agentChoices` array in `select-agent.ts`. The old `AGENTS.md` was a single 1,141-line file loaded automatically by agents. The new skill is 393 lines in `SKILL.md` plus 6,243 lines across 11 reference docs (6,636 total). To preserve the quick-hit value of the old file, Common Gotchas and Best Practices sections were added to `SKILL.md`. A root-level `CLAUDE.md` or `AGENTS.md` points agents to the skill for discoverability. ## Overall Flow ```mermaid sequenceDiagram participant User participant CPA as create-payload-app participant GitHub User->>CPA: npx create-payload-app --agent claude CPA->>CPA: Parse template, select DB CPA->>CPA: selectAgent() — validates flag or shows prompt CPA->>CPA: Download template, configure config, manage env CPA->>GitHub: GET codeload.github.com/.../tar.gz/main GitHub-->>CPA: Tarball (filtered to skills/payload/) CPA->>CPA: Extract to .claude/skills/payload/ CPA->>CPA: Write CLAUDE.md at project root CPA->>CPA: Install deps, init git CPA-->>User: Project ready with Payload skill installed ```
1 parent 62aa42b commit 9f9f343

134 files changed

Lines changed: 234 additions & 43346 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/create-payload-app/src/lib/create-project.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
66
import path from 'path'
77

88
import type {
9+
AgentType,
910
CliArgs,
1011
DbDetails,
1112
PackageManager,
@@ -18,9 +19,11 @@ import { debug, error, info, warning } from '../utils/log.js'
1819
import { configurePayloadConfig } from './configure-payload-config.js'
1920
import { configurePluginProject } from './configure-plugin-project.js'
2021
import { downloadExample } from './download-example.js'
22+
import { downloadSkill } from './download-skill.js'
2123
import { downloadTemplate } from './download-template.js'
2224
import { generateSecret } from './generate-secret.js'
2325
import { manageEnvFiles } from './manage-env-files.js'
26+
import { getAgentChoice } from './select-agent.js'
2427

2528
const filename = fileURLToPath(import.meta.url)
2629
const dirname = path.dirname(filename)
@@ -72,14 +75,15 @@ type TemplateOrExample =
7275

7376
export async function createProject(
7477
args: {
78+
agentType?: AgentType
7579
cliArgs: CliArgs
7680
dbDetails?: DbDetails
7781
packageManager: PackageManager
7882
projectDir: string
7983
projectName: string
8084
} & TemplateOrExample,
8185
): Promise<void> {
82-
const { cliArgs, dbDetails, packageManager, projectDir, projectName } = args
86+
const { agentType, cliArgs, dbDetails, packageManager, projectDir, projectName } = args
8387

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

177+
if (agentType) {
178+
spinner.message('Installing agent skill...')
179+
try {
180+
await downloadSkill({
181+
agentType,
182+
branch: cliArgs['--branch'] || undefined,
183+
debug: cliArgs['--debug'],
184+
projectDir,
185+
})
186+
187+
const { configFile, skillsDir } = getAgentChoice(agentType)
188+
const skillPath = `${skillsDir}/payload`
189+
const configContent =
190+
configFile === 'CLAUDE.md'
191+
? `# 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`
192+
: `# 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`
193+
await fse.writeFile(path.resolve(projectDir, configFile), configContent)
194+
} catch (err) {
195+
if (cliArgs['--debug'] && err instanceof Error) {
196+
debug(`Failed to download skill: ${err.message}`)
197+
}
198+
warning('Could not download agent skill. You can install it manually later.')
199+
}
200+
}
201+
173202
if (!cliArgs['--no-deps']) {
174203
info(`Using ${packageManager}.\n`)
175204
spinner.message('Installing dependencies...')
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import fse from 'fs-extra'
2+
import { Readable } from 'node:stream'
3+
import { pipeline } from 'node:stream/promises'
4+
import path from 'path'
5+
import { x } from 'tar'
6+
7+
import type { AgentType } from '../types.js'
8+
9+
import { debug as debugLog } from '../utils/log.js'
10+
import { getSkillsDir } from './select-agent.js'
11+
12+
export async function downloadSkill(args: {
13+
agentType: AgentType
14+
branch?: string
15+
debug?: boolean
16+
projectDir: string
17+
}): Promise<void> {
18+
const { agentType, branch = 'main', debug, projectDir } = args
19+
20+
const skillsDir = getSkillsDir(agentType)
21+
const destDir = path.join(projectDir, skillsDir, 'payload')
22+
23+
await fse.mkdirp(destDir)
24+
25+
const url = `https://codeload.github.com/payloadcms/payload/tar.gz/${branch}`
26+
const filter = `payload-${branch.replace(/^v/, '').replaceAll('/', '-')}/tools/claude-plugin/skills/payload/`
27+
28+
if (debug) {
29+
debugLog(`Downloading skill for agent: ${agentType}`)
30+
debugLog(`Skill codeload url: ${url}`)
31+
debugLog(`Skill filter: ${filter}`)
32+
debugLog(`Skill destination: ${destDir}`)
33+
}
34+
35+
const res = await fetch(url)
36+
37+
if (!res.ok) {
38+
throw new Error(`Failed to download skill: ${res.status} ${res.statusText} from ${url}`)
39+
}
40+
41+
if (!res.body) {
42+
throw new Error(`Failed to download skill: empty response from ${url}`)
43+
}
44+
45+
await pipeline(
46+
Readable.from(res.body as unknown as NodeJS.ReadableStream),
47+
x({
48+
cwd: destDir,
49+
filter: (p) => p.includes(filter),
50+
strip: filter.split('/').length - 1,
51+
}),
52+
)
53+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as p from '@clack/prompts'
2+
3+
import type { AgentType, CliArgs } from '../types.js'
4+
5+
type AgentChoice = {
6+
/** File to write at project root pointing agents to the skill */
7+
configFile: 'AGENTS.md' | 'CLAUDE.md'
8+
label: string
9+
skillsDir: string
10+
value: AgentType
11+
}
12+
13+
export const agentChoices: AgentChoice[] = [
14+
{ configFile: 'CLAUDE.md', label: 'Claude Code', skillsDir: '.claude/skills', value: 'claude' },
15+
{ configFile: 'AGENTS.md', label: 'Codex', skillsDir: '.agents/skills', value: 'codex' },
16+
{ configFile: 'AGENTS.md', label: 'Cursor', skillsDir: '.agents/skills', value: 'cursor' },
17+
]
18+
19+
const validAgentValues = agentChoices.map((c) => c.value)
20+
21+
export function getAgentChoice(agentType: AgentType): AgentChoice {
22+
const choice = agentChoices.find((c) => c.value === agentType)
23+
if (!choice) {
24+
throw new Error(`Unknown agent type: ${agentType}`)
25+
}
26+
return choice
27+
}
28+
29+
export function getSkillsDir(agentType: AgentType): string {
30+
return getAgentChoice(agentType).skillsDir
31+
}
32+
33+
export async function selectAgent(args: { cliArgs: CliArgs }): Promise<AgentType | undefined> {
34+
const { cliArgs } = args
35+
36+
if (cliArgs['--no-agent']) {
37+
return undefined
38+
}
39+
40+
if (cliArgs['--agent']) {
41+
const value = cliArgs['--agent'] as AgentType
42+
if (!validAgentValues.includes(value)) {
43+
throw new Error(
44+
`Invalid agent type: ${cliArgs['--agent']}. Valid types are: ${validAgentValues.join(', ')}`,
45+
)
46+
}
47+
return value
48+
}
49+
50+
const selected = await p.select<
51+
{ label: string; value: 'none' | AgentType }[],
52+
'none' | AgentType
53+
>({
54+
message: 'Select a coding agent to install the Payload skill for',
55+
options: [
56+
...agentChoices.map((choice) => ({
57+
label: choice.label,
58+
value: choice.value,
59+
})),
60+
{ label: 'None', value: 'none' as const },
61+
],
62+
})
63+
64+
if (p.isCancel(selected)) {
65+
process.exit(0)
66+
}
67+
68+
if (selected === 'none') {
69+
return undefined
70+
}
71+
72+
return selected
73+
}

packages/create-payload-app/src/main.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getNextAppDetails, initNext } from './lib/init-next.js'
1616
import { manageEnvFiles } from './lib/manage-env-files.js'
1717
import { parseProjectName } from './lib/parse-project-name.js'
1818
import { parseTemplate } from './lib/parse-template.js'
19+
import { selectAgent } from './lib/select-agent.js'
1920
import { selectDb } from './lib/select-db.js'
2021
import { getValidTemplates, validateTemplate } from './lib/templates.js'
2122
import { updatePayloadInProject } from './lib/update-payload-in-project.js'
@@ -36,6 +37,7 @@ export class Main {
3637
// @ts-expect-error bad typings
3738
this.args = arg(
3839
{
40+
'--agent': String,
3941
'--branch': String,
4042
'--db': String,
4143
'--db-accept-recommended': Boolean,
@@ -51,6 +53,9 @@ export class Main {
5153
// Next.js
5254
'--init-next': Boolean, // TODO: Is this needed if we detect if inside Next.js project?
5355

56+
// Agent
57+
'--no-agent': Boolean,
58+
5459
// Package manager
5560
'--no-deps': Boolean,
5661
'--use-bun': Boolean,
@@ -67,6 +72,7 @@ export class Main {
6772
'--dry-run': Boolean,
6873

6974
// Aliases
75+
'-a': '--agent',
7076
'-d': '--db',
7177
'-e': '--example',
7278
'-h': '--help',
@@ -231,7 +237,10 @@ export class Main {
231237
process.exit(1)
232238
}
233239

240+
const agentType = await selectAgent({ cliArgs: this.args })
241+
234242
await createProject({
243+
agentType,
235244
cliArgs: this.args,
236245
example,
237246
packageManager,
@@ -255,7 +264,9 @@ export class Main {
255264

256265
switch (template.type) {
257266
case 'plugin': {
267+
const agentType = await selectAgent({ cliArgs: this.args })
258268
await createProject({
269+
agentType,
259270
cliArgs: this.args,
260271
packageManager,
261272
projectDir,
@@ -266,8 +277,10 @@ export class Main {
266277
}
267278
case 'starter': {
268279
const dbDetails = await selectDb(this.args, projectName, template)
280+
const agentType = await selectAgent({ cliArgs: this.args })
269281

270282
await createProject({
283+
agentType,
271284
cliArgs: this.args,
272285
dbDetails,
273286
packageManager,

packages/create-payload-app/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type arg from 'arg'
33
import type { ALL_DATABASE_ADAPTERS, ALL_STORAGE_ADAPTERS } from './lib/ast/types.js'
44

55
export interface Args extends arg.Spec {
6+
'--agent': StringConstructor
67
'--beta': BooleanConstructor
78
'--branch': StringConstructor
89
'--db': StringConstructor
@@ -17,6 +18,7 @@ export interface Args extends arg.Spec {
1718
'--local-example': StringConstructor
1819
'--local-template': StringConstructor
1920
'--name': StringConstructor
21+
'--no-agent': BooleanConstructor
2022
'--no-deps': BooleanConstructor
2123
'--no-git': BooleanConstructor
2224
'--secret': StringConstructor
@@ -28,6 +30,7 @@ export interface Args extends arg.Spec {
2830

2931
// Aliases
3032

33+
'-a': string
3134
'-e': string
3235
'-h': string
3336
'-n': string
@@ -93,3 +96,5 @@ export type NextAppDetails = {
9396
export type NextConfigType = 'cjs' | 'esm' | 'ts'
9497

9598
export type StorageAdapterType = (typeof ALL_STORAGE_ADAPTERS)[number]
99+
100+
export type AgentType = 'claude' | 'codex' | 'cursor'

packages/create-payload-app/src/utils/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ export function helpMessage(): void {
3838
3939
{dim Available templates: ${formatTemplates(validTemplates)}}
4040
41+
-a {underline agent_name} Set coding agent (claude, codex, cursor)
42+
43+
{dim Installs the Payload skill for the selected agent}
44+
45+
--no-agent Skip agent skill installation
4146
--use-npm Use npm to install dependencies
4247
--use-yarn Use yarn to install dependencies
4348
--use-pnpm Use pnpm to install dependencies

0 commit comments

Comments
 (0)