Files
next.js/packages/next-codemod/bin/agents-md.ts
Jude GaoandGitHub 1e859c06aa feat(next-codemod): add agents-md command for AI coding agents (#88961)
## Summary

Adds a new `agents-md` subcommand to `@next/codemod` that generates a
Next.js documentation index for AI coding agents (Claude, Cursor, GitHub
Copilot, etc.).

## Usage

```bash
# Interactive mode - prompts for version and target file
npx @next/codemod agents-md

# Non-interactive mode
npx @next/codemod agents-md --version 15.1.0 --output CLAUDE.md

# Auto-detect version, specify output
npx @next/codemod agents-md --output AGENTS.md
```

## What it does

1. Downloads the Next.js documentation matching your project's version
to `.next-docs/` (via git sparse-checkout)
2. Generates a compact index of all doc files grouped by directory
3. Injects the index into `CLAUDE.md` or `AGENTS.md` between marker
comments
4. Adds `.next-docs/` to `.gitignore`

The index helps AI agents prefer local documentation over pre-trained
knowledge, ensuring answers match your Next.js version.

## Options

| Option | Description |
|--------|-------------|
| `--version <version>` | Specify Next.js version (auto-detected from
`package.json` if not provided) |
| `--output <file>` | Target file path (e.g., `CLAUDE.md`, `AGENTS.md`)
|

## Output format

The generated index is a single line with pipe separators:

```
<!-- NEXT-AGENTS-MD-START -->[Next.js Docs Index]|root: ./.next-docs|IMPORTANT: Prefer retrieval-led reasoning...|dir:{file1.mdx,file2.mdx}|...<!-- NEXT-AGENTS-MD-END -->
```

## Test plan

- [x] Build passes
- [x] Tested interactive mode
- [x] Tested non-interactive mode with `--version` and `--output`
- [x] Tested auto-detect version with `--output` only
- [x] Verified `.gitignore` is updated
- [x] Verified index is injected correctly (new file and existing file)

## Eval results

Tested against 19 Next.js-specific agent evals with 100% pass rate:

| Eval | Result |
|------|--------|
| agent-000-app-router-migration-simple | ✅✅✅ (89.5s) |
| agent-021-avoid-fetch-in-effect | ✅✅✅ (90.4s) |
| agent-022-prefer-server-actions | ✅✅✅ (67.1s) |
| agent-023-avoid-getserversideprops | ✅✅✅ (102.5s) |
| agent-024-avoid-redundant-usestate | ✅✅✅ (62.9s) |
| agent-025-prefer-next-link | ✅✅✅ (69.5s) |
| agent-026-no-serial-await | ✅✅✅ (96.2s) |
| agent-027-prefer-next-image | ✅✅✅ (68.0s) |
| agent-028-prefer-next-font | ✅✅✅ (64.2s) |
| agent-029-use-cache-directive | ✅✅✅ (76.6s) |
| agent-030-app-router-migration-hard | ✅✅✅ (220.8s) |
| agent-031-proxy-middleware | ✅✅✅ (92.4s) |
| agent-032-use-cache-directive | ✅✅✅ (71.3s) |
| agent-033-forbidden-auth | ✅✅✅ (100.6s) |
| agent-034-async-cookies | ✅✅✅ (68.7s) |
| agent-035-connection-dynamic | ✅✅✅ (65.2s) |
| agent-036-after-response | ✅✅✅ (68.7s) |
| agent-037-updatetag-cache | ✅✅✅ (70.3s) |
| agent-038-refresh-settings | ✅✅✅ (71.8s) |
| **Overall** | **19/19/19 (100%, 100%, 100%)** |

Legend: ✅✅✅ = Build/Lint/Test
2026-01-23 21:31:32 -05:00

208 lines
5.5 KiB
TypeScript

/**
* CLI handler for `npx @next/codemod agents-md`.
* See ../lib/agents-md.ts for the core logic.
*/
import fs from 'fs'
import path from 'path'
import prompts from 'prompts'
import pc from 'picocolors'
import { BadInput } from './shared'
import {
getNextjsVersion,
pullDocs,
collectDocFiles,
buildDocTree,
generateClaudeMdIndex,
injectIntoClaudeMd,
ensureGitignoreEntry,
} from '../lib/agents-md'
import { onCancel } from '../lib/utils'
export interface AgentsMdOptions {
version?: string
output?: string
}
const DOCS_DIR_NAME = '.next-docs'
function formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
const kb = bytes / 1024
if (kb < 1024) return `${kb.toFixed(1)} KB`
const mb = kb / 1024
return `${mb.toFixed(1)} MB`
}
export async function runAgentsMd(options: AgentsMdOptions): Promise<void> {
const cwd = process.cwd()
// Mode logic:
// 1. No flags → interactive mode (prompts for version + target file)
// 2. --version provided → --output is REQUIRED (error if missing)
// 3. --output alone → auto-detect version, error if not found
let nextjsVersion: string
let targetFile: string
if (options.version) {
// --version provided: --output is required
if (!options.output) {
throw new BadInput(
'When using --version, --output is also required.\n' +
'Example: npx @next/codemod agents-md --version 15.1.3 --output CLAUDE.md'
)
}
nextjsVersion = options.version
targetFile = options.output
} else if (options.output) {
// --output alone: auto-detect version
const detected = getNextjsVersion(cwd)
if (!detected.version) {
throw new BadInput(
'Could not detect Next.js version. Use --version to specify.\n' +
`Example: npx @next/codemod agents-md --version 15.1.3 --output ${options.output}`
)
}
nextjsVersion = detected.version
targetFile = options.output
} else {
// No flags: interactive mode
const promptedOptions = await promptForOptions(cwd)
nextjsVersion = promptedOptions.nextVersion
targetFile = promptedOptions.targetFile
}
const claudeMdPath = path.join(cwd, targetFile)
const docsPath = path.join(cwd, DOCS_DIR_NAME)
const docsLinkPath = `./${DOCS_DIR_NAME}`
let sizeBefore = 0
let isNewFile = true
let existingContent = ''
if (fs.existsSync(claudeMdPath)) {
existingContent = fs.readFileSync(claudeMdPath, 'utf-8')
sizeBefore = Buffer.byteLength(existingContent, 'utf-8')
isNewFile = false
}
console.log(
`\nDownloading Next.js ${pc.cyan(nextjsVersion)} documentation to ${pc.cyan(DOCS_DIR_NAME)}...`
)
const pullResult = await pullDocs({
cwd,
version: nextjsVersion,
docsDir: docsPath,
})
if (!pullResult.success) {
throw new BadInput(`Failed to pull docs: ${pullResult.error}`)
}
const docFiles = collectDocFiles(docsPath)
const sections = buildDocTree(docFiles)
const indexContent = generateClaudeMdIndex({
docsPath: docsLinkPath,
sections,
outputFile: targetFile,
})
const newContent = injectIntoClaudeMd(existingContent, indexContent)
fs.writeFileSync(claudeMdPath, newContent, 'utf-8')
const sizeAfter = Buffer.byteLength(newContent, 'utf-8')
const gitignoreResult = ensureGitignoreEntry(cwd)
const action = isNewFile ? 'Created' : 'Updated'
const sizeInfo = isNewFile
? formatSize(sizeAfter)
: `${formatSize(sizeBefore)} → ${formatSize(sizeAfter)}`
console.log(`${pc.green('✓')} ${action} ${pc.bold(targetFile)} (${sizeInfo})`)
if (gitignoreResult.updated) {
console.log(
`${pc.green('✓')} Added ${pc.bold(DOCS_DIR_NAME)} to .gitignore`
)
}
console.log('')
}
async function promptForOptions(
cwd: string
): Promise<{ nextVersion: string; targetFile: string }> {
// Detect Next.js version for default
const versionResult = getNextjsVersion(cwd)
const detectedVersion = versionResult.version
console.log(
pc.cyan('\n@next/codemod agents-md - Next.js Documentation for AI Agents\n')
)
if (detectedVersion) {
console.log(pc.gray(` Detected Next.js version: ${detectedVersion}\n`))
}
const response = await prompts(
[
{
type: 'text',
name: 'nextVersion',
message: 'Next.js version',
initial: detectedVersion || '',
validate: (value: string) =>
value.trim() ? true : 'Please enter a Next.js version',
},
{
type: 'select',
name: 'targetFile',
message: 'Target markdown file',
choices: [
{ title: 'CLAUDE.md', value: 'CLAUDE.md' },
{ title: 'AGENTS.md', value: 'AGENTS.md' },
{ title: 'Custom...', value: '__custom__' },
],
initial: 0,
},
],
{ onCancel }
)
// Handle cancelled prompts
if (response.nextVersion === undefined || response.targetFile === undefined) {
console.log(pc.yellow('\nCancelled.'))
process.exit(0)
}
let targetFile = response.targetFile
if (targetFile === '__custom__') {
const customResponse = await prompts(
{
type: 'text',
name: 'customFile',
message: 'Enter custom file path',
initial: 'CLAUDE.md',
validate: (value: string) =>
value.trim() ? true : 'Please enter a file path',
},
{ onCancel }
)
if (customResponse.customFile === undefined) {
console.log(pc.yellow('\nCancelled.'))
process.exit(0)
}
targetFile = customResponse.customFile
}
return {
nextVersion: response.nextVersion,
targetFile,
}
}