Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
208 lines
5.5 KiB
TypeScript
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,
|
|
}
|
|
}
|