@next/codemod 16.2.0-canary.4 → 16.2.0-canary.41

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.
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ /**
3
+ * CLI handler for `npx @next/codemod agents-md`.
4
+ * See ../lib/agents-md.ts for the core logic.
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.runAgentsMd = runAgentsMd;
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const prompts_1 = __importDefault(require("prompts"));
14
+ const picocolors_1 = __importDefault(require("picocolors"));
15
+ const shared_1 = require("./shared");
16
+ const agents_md_1 = require("../lib/agents-md");
17
+ const utils_1 = require("../lib/utils");
18
+ const DOCS_DIR_NAME = '.next-docs';
19
+ function formatSize(bytes) {
20
+ if (bytes < 1024)
21
+ return `${bytes} B`;
22
+ const kb = bytes / 1024;
23
+ if (kb < 1024)
24
+ return `${kb.toFixed(1)} KB`;
25
+ const mb = kb / 1024;
26
+ return `${mb.toFixed(1)} MB`;
27
+ }
28
+ async function runAgentsMd(options) {
29
+ const cwd = process.cwd();
30
+ // Mode logic:
31
+ // 1. No flags → interactive mode (prompts for version + target file)
32
+ // 2. --version provided → --output is REQUIRED (error if missing)
33
+ // 3. --output alone → auto-detect version, error if not found
34
+ let nextjsVersion;
35
+ let targetFile;
36
+ if (options.version) {
37
+ // --version provided: --output is required
38
+ if (!options.output) {
39
+ throw new shared_1.BadInput('When using --version, --output is also required.\n' +
40
+ 'Example: npx @next/codemod agents-md --version 15.1.3 --output CLAUDE.md');
41
+ }
42
+ nextjsVersion = options.version;
43
+ targetFile = options.output;
44
+ }
45
+ else if (options.output) {
46
+ // --output alone: auto-detect version
47
+ const detected = (0, agents_md_1.getNextjsVersion)(cwd);
48
+ if (!detected.version) {
49
+ throw new shared_1.BadInput('Could not detect Next.js version. Use --version to specify.\n' +
50
+ `Example: npx @next/codemod agents-md --version 15.1.3 --output ${options.output}`);
51
+ }
52
+ nextjsVersion = detected.version;
53
+ targetFile = options.output;
54
+ }
55
+ else {
56
+ // No flags: interactive mode
57
+ const promptedOptions = await promptForOptions(cwd);
58
+ nextjsVersion = promptedOptions.nextVersion;
59
+ targetFile = promptedOptions.targetFile;
60
+ }
61
+ const claudeMdPath = path_1.default.join(cwd, targetFile);
62
+ const docsPath = path_1.default.join(cwd, DOCS_DIR_NAME);
63
+ const docsLinkPath = `./${DOCS_DIR_NAME}`;
64
+ let sizeBefore = 0;
65
+ let isNewFile = true;
66
+ let existingContent = '';
67
+ if (fs_1.default.existsSync(claudeMdPath)) {
68
+ existingContent = fs_1.default.readFileSync(claudeMdPath, 'utf-8');
69
+ sizeBefore = Buffer.byteLength(existingContent, 'utf-8');
70
+ isNewFile = false;
71
+ }
72
+ console.log(`\nDownloading Next.js ${picocolors_1.default.cyan(nextjsVersion)} documentation to ${picocolors_1.default.cyan(DOCS_DIR_NAME)}...`);
73
+ const pullResult = await (0, agents_md_1.pullDocs)({
74
+ cwd,
75
+ version: nextjsVersion,
76
+ docsDir: docsPath,
77
+ });
78
+ if (!pullResult.success) {
79
+ throw new shared_1.BadInput(`Failed to pull docs: ${pullResult.error}`);
80
+ }
81
+ const docFiles = (0, agents_md_1.collectDocFiles)(docsPath);
82
+ const sections = (0, agents_md_1.buildDocTree)(docFiles);
83
+ const indexContent = (0, agents_md_1.generateClaudeMdIndex)({
84
+ docsPath: docsLinkPath,
85
+ sections,
86
+ outputFile: targetFile,
87
+ });
88
+ const newContent = (0, agents_md_1.injectIntoClaudeMd)(existingContent, indexContent);
89
+ fs_1.default.writeFileSync(claudeMdPath, newContent, 'utf-8');
90
+ const sizeAfter = Buffer.byteLength(newContent, 'utf-8');
91
+ const gitignoreResult = (0, agents_md_1.ensureGitignoreEntry)(cwd);
92
+ const action = isNewFile ? 'Created' : 'Updated';
93
+ const sizeInfo = isNewFile
94
+ ? formatSize(sizeAfter)
95
+ : `${formatSize(sizeBefore)} → ${formatSize(sizeAfter)}`;
96
+ console.log(`${picocolors_1.default.green('✓')} ${action} ${picocolors_1.default.bold(targetFile)} (${sizeInfo})`);
97
+ if (gitignoreResult.updated) {
98
+ console.log(`${picocolors_1.default.green('✓')} Added ${picocolors_1.default.bold(DOCS_DIR_NAME)} to .gitignore`);
99
+ }
100
+ console.log('');
101
+ }
102
+ async function promptForOptions(cwd) {
103
+ // Detect Next.js version for default
104
+ const versionResult = (0, agents_md_1.getNextjsVersion)(cwd);
105
+ const detectedVersion = versionResult.version;
106
+ console.log(picocolors_1.default.cyan('\n@next/codemod agents-md - Next.js Documentation for AI Agents\n'));
107
+ if (detectedVersion) {
108
+ console.log(picocolors_1.default.gray(` Detected Next.js version: ${detectedVersion}\n`));
109
+ }
110
+ const response = await (0, prompts_1.default)([
111
+ {
112
+ type: 'text',
113
+ name: 'nextVersion',
114
+ message: 'Next.js version',
115
+ initial: detectedVersion || '',
116
+ validate: (value) => value.trim() ? true : 'Please enter a Next.js version',
117
+ },
118
+ {
119
+ type: 'select',
120
+ name: 'targetFile',
121
+ message: 'Target markdown file',
122
+ choices: [
123
+ { title: 'CLAUDE.md', value: 'CLAUDE.md' },
124
+ { title: 'AGENTS.md', value: 'AGENTS.md' },
125
+ { title: 'Custom...', value: '__custom__' },
126
+ ],
127
+ initial: 0,
128
+ },
129
+ ], { onCancel: utils_1.onCancel });
130
+ // Handle cancelled prompts
131
+ if (response.nextVersion === undefined || response.targetFile === undefined) {
132
+ console.log(picocolors_1.default.yellow('\nCancelled.'));
133
+ process.exit(0);
134
+ }
135
+ let targetFile = response.targetFile;
136
+ if (targetFile === '__custom__') {
137
+ const customResponse = await (0, prompts_1.default)({
138
+ type: 'text',
139
+ name: 'customFile',
140
+ message: 'Enter custom file path',
141
+ initial: 'CLAUDE.md',
142
+ validate: (value) => value.trim() ? true : 'Please enter a file path',
143
+ }, { onCancel: utils_1.onCancel });
144
+ if (customResponse.customFile === undefined) {
145
+ console.log(picocolors_1.default.yellow('\nCancelled.'));
146
+ process.exit(0);
147
+ }
148
+ targetFile = customResponse.customFile;
149
+ }
150
+ return {
151
+ nextVersion: response.nextVersion,
152
+ targetFile,
153
+ };
154
+ }
155
+ //# sourceMappingURL=agents-md.js.map
@@ -12,6 +12,7 @@
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  const commander_1 = require("commander");
14
14
  const upgrade_1 = require("./upgrade");
15
+ const agents_md_1 = require("./agents-md");
15
16
  const transform_1 = require("./transform");
16
17
  const shared_1 = require("./shared");
17
18
  const packageJson = require('../package.json');
@@ -56,5 +57,24 @@ program
56
57
  process.exit(1);
57
58
  }
58
59
  });
60
+ program
61
+ .command('agents-md')
62
+ .description('Generate Next.js documentation index for AI coding agents (Claude, Cursor, etc.).')
63
+ .option('--version <version>', 'Next.js version (auto-detected if not provided)')
64
+ .option('--output <file>', 'Target file path (e.g., CLAUDE.md, AGENTS.md)')
65
+ .action(async (options) => {
66
+ try {
67
+ await (0, agents_md_1.runAgentsMd)(options);
68
+ }
69
+ catch (error) {
70
+ if (error instanceof shared_1.BadInput) {
71
+ console.error(error.message);
72
+ }
73
+ else {
74
+ console.error(error);
75
+ }
76
+ process.exit(1);
77
+ }
78
+ });
59
79
  program.parse(process.argv);
60
80
  //# sourceMappingURL=next-codemod.js.map
@@ -0,0 +1,294 @@
1
+ /* global jest */
2
+ jest.autoMockOff()
3
+
4
+ const fs = require('fs')
5
+ const path = require('path')
6
+ const os = require('os')
7
+ const { runAgentsMd } = require('../../bin/agents-md')
8
+
9
+ /**
10
+ * TRUE E2E TESTS
11
+ * These tests invoke the actual CLI entry point (runAgentsMd),
12
+ * simulating what happens when a user runs:
13
+ * npx @next/codemod agents-md --version 15.0.0 --output CLAUDE.md
14
+ */
15
+ describe('agents-md e2e (CLI invocation)', () => {
16
+ let testProjectDir
17
+ let originalConsoleLog
18
+ let consoleOutput
19
+
20
+ beforeEach(() => {
21
+ // Create isolated test project directory
22
+ const tmpBase = process.env.NEXT_TEST_DIR || os.tmpdir()
23
+ testProjectDir = path.join(
24
+ tmpBase,
25
+ `agents-md-e2e-${Date.now()}-${(Math.random() * 1000) | 0}`
26
+ )
27
+ fs.mkdirSync(testProjectDir, { recursive: true })
28
+
29
+ // Mock console.log to capture CLI output
30
+ originalConsoleLog = console.log
31
+ consoleOutput = []
32
+ console.log = (...args) => {
33
+ consoleOutput.push(args.join(' '))
34
+ }
35
+ })
36
+
37
+ afterEach(() => {
38
+ // Restore console.log
39
+ console.log = originalConsoleLog
40
+
41
+ // Clean up test directory
42
+ if (testProjectDir && fs.existsSync(testProjectDir)) {
43
+ fs.rmSync(testProjectDir, { recursive: true, force: true })
44
+ }
45
+ })
46
+
47
+ it('creates CLAUDE.md and .next-docs directory when run with --version and --output', async () => {
48
+ // Create a minimal package.json (not required, but realistic)
49
+ const packageJson = {
50
+ name: 'test-project',
51
+ version: '1.0.0',
52
+ dependencies: {
53
+ next: '15.0.0',
54
+ },
55
+ }
56
+ fs.writeFileSync(
57
+ path.join(testProjectDir, 'package.json'),
58
+ JSON.stringify(packageJson, null, 2)
59
+ )
60
+
61
+ // Change to test directory
62
+ const originalCwd = process.cwd()
63
+ process.chdir(testProjectDir)
64
+
65
+ try {
66
+ // Run the actual CLI command
67
+ await runAgentsMd({
68
+ version: '15.0.0',
69
+ output: 'CLAUDE.md',
70
+ })
71
+
72
+ // Verify .next-docs directory was created and populated
73
+ const docsDir = path.join(testProjectDir, '.next-docs')
74
+ expect(fs.existsSync(docsDir)).toBe(true)
75
+
76
+ const docFiles = fs.readdirSync(docsDir, { recursive: true })
77
+ expect(docFiles.length).toBeGreaterThan(0)
78
+
79
+ // Should contain mdx/md files
80
+ const mdxFiles = docFiles.filter(
81
+ (f) => f.endsWith('.mdx') || f.endsWith('.md')
82
+ )
83
+ expect(mdxFiles.length).toBeGreaterThan(0)
84
+
85
+ // Verify CLAUDE.md was created
86
+ const claudeMdPath = path.join(testProjectDir, 'CLAUDE.md')
87
+ expect(fs.existsSync(claudeMdPath)).toBe(true)
88
+
89
+ const claudeMdContent = fs.readFileSync(claudeMdPath, 'utf-8')
90
+
91
+ // Verify content structure
92
+ expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-START -->')
93
+ expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-END -->')
94
+ expect(claudeMdContent).toContain('[Next.js Docs Index]')
95
+ expect(claudeMdContent).toContain('root: ./.next-docs')
96
+
97
+ // Verify paths are normalized to forward slashes (cross-platform)
98
+ const lines = claudeMdContent.split('|')
99
+ const pathLines = lines.filter((line) => line.includes(':'))
100
+ pathLines.forEach((line) => {
101
+ // Should not contain Windows backslashes in the output
102
+ const pathPart = line.split(':')[0]
103
+ if (pathPart && pathPart.includes('/')) {
104
+ expect(line).not.toMatch(/[^:]\\/)
105
+ }
106
+ })
107
+
108
+ // Verify .gitignore was updated
109
+ const gitignorePath = path.join(testProjectDir, '.gitignore')
110
+ if (fs.existsSync(gitignorePath)) {
111
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8')
112
+ expect(gitignoreContent).toContain('.next-docs')
113
+ }
114
+
115
+ // Verify console output
116
+ const output = consoleOutput.join('\n')
117
+ expect(output).toContain('Downloading Next.js')
118
+ expect(output).toContain('15.0.0')
119
+ expect(output).toContain('CLAUDE.md')
120
+ } finally {
121
+ // Restore original directory
122
+ process.chdir(originalCwd)
123
+ }
124
+ })
125
+
126
+ it('updates existing CLAUDE.md without losing content', async () => {
127
+ const originalCwd = process.cwd()
128
+ process.chdir(testProjectDir)
129
+
130
+ try {
131
+ // Create existing CLAUDE.md with custom content
132
+ const existingContent = `# My Project
133
+
134
+ This is my project documentation.
135
+
136
+ ## Features
137
+ - Feature 1
138
+ - Feature 2
139
+ `
140
+ fs.writeFileSync(
141
+ path.join(testProjectDir, 'CLAUDE.md'),
142
+ existingContent
143
+ )
144
+
145
+ // Run CLI
146
+ await runAgentsMd({
147
+ version: '15.0.0',
148
+ output: 'CLAUDE.md',
149
+ })
150
+
151
+ // Verify file was updated, not replaced
152
+ const claudeMdContent = fs.readFileSync(
153
+ path.join(testProjectDir, 'CLAUDE.md'),
154
+ 'utf-8'
155
+ )
156
+
157
+ // Original content should still be there
158
+ expect(claudeMdContent).toContain('# My Project')
159
+ expect(claudeMdContent).toContain('This is my project documentation.')
160
+ expect(claudeMdContent).toContain('## Features')
161
+
162
+ // New index should be injected
163
+ expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-START -->')
164
+ expect(claudeMdContent).toContain('[Next.js Docs Index]')
165
+ } finally {
166
+ process.chdir(originalCwd)
167
+ }
168
+ })
169
+
170
+ it('handles custom output filename', async () => {
171
+ const originalCwd = process.cwd()
172
+ process.chdir(testProjectDir)
173
+
174
+ try {
175
+ // Run with custom output file
176
+ await runAgentsMd({
177
+ version: '15.0.0',
178
+ output: 'AGENTS.md',
179
+ })
180
+
181
+ // Verify AGENTS.md was created (not CLAUDE.md)
182
+ expect(fs.existsSync(path.join(testProjectDir, 'AGENTS.md'))).toBe(true)
183
+ expect(fs.existsSync(path.join(testProjectDir, 'CLAUDE.md'))).toBe(false)
184
+
185
+ const agentsMdContent = fs.readFileSync(
186
+ path.join(testProjectDir, 'AGENTS.md'),
187
+ 'utf-8'
188
+ )
189
+ expect(agentsMdContent).toContain('[Next.js Docs Index]')
190
+ } finally {
191
+ process.chdir(originalCwd)
192
+ }
193
+ })
194
+
195
+ it('works when run from a subdirectory', async () => {
196
+ const originalCwd = process.cwd()
197
+
198
+ // Create a subdirectory
199
+ const subDir = path.join(testProjectDir, 'packages', 'app')
200
+ fs.mkdirSync(subDir, { recursive: true })
201
+
202
+ // Create package.json in root
203
+ const packageJson = {
204
+ dependencies: { next: '15.0.0' },
205
+ }
206
+ fs.writeFileSync(
207
+ path.join(testProjectDir, 'package.json'),
208
+ JSON.stringify(packageJson)
209
+ )
210
+
211
+ // Change to subdirectory
212
+ process.chdir(subDir)
213
+
214
+ try {
215
+ // Run from subdirectory - should create files in CWD (subdirectory)
216
+ await runAgentsMd({
217
+ version: '15.0.0',
218
+ output: 'CLAUDE.md',
219
+ })
220
+
221
+ // Verify files created in subdirectory
222
+ expect(fs.existsSync(path.join(subDir, 'CLAUDE.md'))).toBe(true)
223
+ expect(fs.existsSync(path.join(subDir, '.next-docs'))).toBe(true)
224
+ } finally {
225
+ process.chdir(originalCwd)
226
+ }
227
+ })
228
+
229
+ it('normalizes paths on Windows (cross-platform test)', async () => {
230
+ const originalCwd = process.cwd()
231
+ process.chdir(testProjectDir)
232
+
233
+ try {
234
+ await runAgentsMd({
235
+ version: '15.0.0',
236
+ output: 'CLAUDE.md',
237
+ })
238
+
239
+ const claudeMdContent = fs.readFileSync(
240
+ path.join(testProjectDir, 'CLAUDE.md'),
241
+ 'utf-8'
242
+ )
243
+
244
+ // Extract the index content between markers
245
+ const startMarker = '<!-- NEXT-AGENTS-MD-START -->'
246
+ const endMarker = '<!-- NEXT-AGENTS-MD-END -->'
247
+ const startIdx = claudeMdContent.indexOf(startMarker) + startMarker.length
248
+ const endIdx = claudeMdContent.indexOf(endMarker)
249
+ const indexContent = claudeMdContent.slice(startIdx, endIdx)
250
+
251
+ // Parse the index (format: "dir:{file1,file2}|dir2:{file3}")
252
+ const sections = indexContent.split('|').filter((s) => s.includes(':'))
253
+
254
+ sections.forEach((section) => {
255
+ const [dirPath, filesStr] = section.split(':')
256
+ if (dirPath && dirPath.trim() && !dirPath.includes('root')) {
257
+ // Verify no Windows backslashes in directory paths
258
+ expect(dirPath).not.toContain('\\')
259
+ // Verify uses forward slashes
260
+ if (dirPath.includes('/')) {
261
+ expect(dirPath).toMatch(/^[^\\]+$/)
262
+ }
263
+ }
264
+ })
265
+ } finally {
266
+ process.chdir(originalCwd)
267
+ }
268
+ })
269
+
270
+ it('handles version that requires git clone from GitHub', async () => {
271
+ const originalCwd = process.cwd()
272
+ process.chdir(testProjectDir)
273
+
274
+ try {
275
+ // Use a known stable version
276
+ await runAgentsMd({
277
+ version: '14.2.0',
278
+ output: 'CLAUDE.md',
279
+ })
280
+
281
+ // Verify docs were downloaded
282
+ const docsDir = path.join(testProjectDir, '.next-docs')
283
+ expect(fs.existsSync(docsDir)).toBe(true)
284
+
285
+ const docFiles = fs.readdirSync(docsDir, { recursive: true })
286
+ const mdxFiles = docFiles.filter(
287
+ (f) => f.endsWith('.mdx') || f.endsWith('.md')
288
+ )
289
+ expect(mdxFiles.length).toBeGreaterThan(50) // Should have many doc files
290
+ } finally {
291
+ process.chdir(originalCwd)
292
+ }
293
+ }, 30000) // Increase timeout for git clone
294
+ })
@@ -0,0 +1,529 @@
1
+ "use strict";
2
+ /**
3
+ * agents-md: Generate Next.js documentation index for AI coding agents.
4
+ *
5
+ * Downloads docs from GitHub via git sparse-checkout, builds a compact
6
+ * index of all doc files, and injects it into CLAUDE.md or AGENTS.md.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getNextjsVersion = getNextjsVersion;
13
+ exports.pullDocs = pullDocs;
14
+ exports.collectDocFiles = collectDocFiles;
15
+ exports.buildDocTree = buildDocTree;
16
+ exports.generateClaudeMdIndex = generateClaudeMdIndex;
17
+ exports.injectIntoClaudeMd = injectIntoClaudeMd;
18
+ exports.ensureGitignoreEntry = ensureGitignoreEntry;
19
+ const execa_1 = __importDefault(require("execa"));
20
+ const fs_1 = __importDefault(require("fs"));
21
+ const path_1 = __importDefault(require("path"));
22
+ const os_1 = __importDefault(require("os"));
23
+ function getNextjsVersion(cwd) {
24
+ const packageJsonPath = path_1.default.join(cwd, 'package.json');
25
+ if (!fs_1.default.existsSync(packageJsonPath)) {
26
+ return {
27
+ version: null,
28
+ error: 'No package.json found in the current directory',
29
+ };
30
+ }
31
+ try {
32
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
33
+ const dependencies = packageJson.dependencies || {};
34
+ const devDependencies = packageJson.devDependencies || {};
35
+ const nextVersion = dependencies.next || devDependencies.next;
36
+ if (nextVersion) {
37
+ const cleanVersion = nextVersion.replace(/^[\^~>=<]+/, '');
38
+ return { version: cleanVersion };
39
+ }
40
+ // Not found at root - check for monorepo workspace
41
+ const workspace = detectWorkspace(cwd);
42
+ if (workspace.isMonorepo && workspace.packages.length > 0) {
43
+ const highestVersion = findNextjsInWorkspace(cwd, workspace.packages);
44
+ if (highestVersion) {
45
+ return { version: highestVersion };
46
+ }
47
+ return {
48
+ version: null,
49
+ error: `No Next.js found in ${workspace.type} workspace packages.`,
50
+ };
51
+ }
52
+ return {
53
+ version: null,
54
+ error: 'Next.js is not installed in this project.',
55
+ };
56
+ }
57
+ catch (err) {
58
+ return {
59
+ version: null,
60
+ error: `Failed to parse package.json: ${err instanceof Error ? err.message : String(err)}`,
61
+ };
62
+ }
63
+ }
64
+ function versionToGitHubTag(version) {
65
+ return version.startsWith('v') ? version : `v${version}`;
66
+ }
67
+ async function pullDocs(options) {
68
+ const { cwd, version: versionOverride, docsDir } = options;
69
+ let nextjsVersion;
70
+ if (versionOverride) {
71
+ nextjsVersion = versionOverride;
72
+ }
73
+ else {
74
+ const versionResult = getNextjsVersion(cwd);
75
+ if (!versionResult.version) {
76
+ return {
77
+ success: false,
78
+ error: versionResult.error || 'Could not detect Next.js version',
79
+ };
80
+ }
81
+ nextjsVersion = versionResult.version;
82
+ }
83
+ const docsPath = docsDir ?? fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'next-agents-md-'));
84
+ const useTempDir = !docsDir;
85
+ try {
86
+ if (useTempDir && fs_1.default.existsSync(docsPath)) {
87
+ fs_1.default.rmSync(docsPath, { recursive: true });
88
+ }
89
+ const tag = versionToGitHubTag(nextjsVersion);
90
+ await cloneDocsFolder(tag, docsPath);
91
+ return {
92
+ success: true,
93
+ docsPath,
94
+ nextjsVersion,
95
+ };
96
+ }
97
+ catch (error) {
98
+ if (useTempDir && fs_1.default.existsSync(docsPath)) {
99
+ fs_1.default.rmSync(docsPath, { recursive: true });
100
+ }
101
+ return {
102
+ success: false,
103
+ error: error instanceof Error ? error.message : String(error),
104
+ };
105
+ }
106
+ }
107
+ async function cloneDocsFolder(tag, destDir) {
108
+ const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'next-agents-md-'));
109
+ try {
110
+ try {
111
+ await (0, execa_1.default)('git', [
112
+ 'clone',
113
+ '--depth',
114
+ '1',
115
+ '--filter=blob:none',
116
+ '--sparse',
117
+ '--branch',
118
+ tag,
119
+ 'https://github.com/vercel/next.js.git',
120
+ '.',
121
+ ], { cwd: tempDir });
122
+ }
123
+ catch (error) {
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ if (message.includes('not found') || message.includes('did not match')) {
126
+ throw new Error(`Could not find documentation for Next.js ${tag}. This version may not exist on GitHub yet.`);
127
+ }
128
+ throw error;
129
+ }
130
+ await (0, execa_1.default)('git', ['sparse-checkout', 'set', 'docs'], { cwd: tempDir });
131
+ const sourceDocsDir = path_1.default.join(tempDir, 'docs');
132
+ if (!fs_1.default.existsSync(sourceDocsDir)) {
133
+ throw new Error('docs folder not found in cloned repository');
134
+ }
135
+ if (fs_1.default.existsSync(destDir)) {
136
+ fs_1.default.rmSync(destDir, { recursive: true });
137
+ }
138
+ fs_1.default.mkdirSync(destDir, { recursive: true });
139
+ fs_1.default.cpSync(sourceDocsDir, destDir, { recursive: true });
140
+ }
141
+ finally {
142
+ if (fs_1.default.existsSync(tempDir)) {
143
+ fs_1.default.rmSync(tempDir, { recursive: true });
144
+ }
145
+ }
146
+ }
147
+ function collectDocFiles(dir) {
148
+ return fs_1.default.readdirSync(dir, { recursive: true })
149
+ .filter((f) => (f.endsWith('.mdx') || f.endsWith('.md')) &&
150
+ !/[/\\]index\.mdx$/.test(f) &&
151
+ !/[/\\]index\.md$/.test(f) &&
152
+ !f.startsWith('index.'))
153
+ .sort()
154
+ .map((f) => ({ relativePath: f.replace(/\\/g, '/') }));
155
+ }
156
+ function buildDocTree(files) {
157
+ const sections = new Map();
158
+ for (const file of files) {
159
+ const parts = file.relativePath.split(/[/\\]/);
160
+ if (parts.length < 2)
161
+ continue;
162
+ const topLevelDir = parts[0];
163
+ if (!sections.has(topLevelDir)) {
164
+ sections.set(topLevelDir, {
165
+ name: topLevelDir,
166
+ files: [],
167
+ subsections: [],
168
+ });
169
+ }
170
+ const section = sections.get(topLevelDir);
171
+ if (parts.length === 2) {
172
+ section.files.push({ relativePath: file.relativePath });
173
+ }
174
+ else {
175
+ const subsectionDir = parts[1];
176
+ let subsection = section.subsections.find((s) => s.name === subsectionDir);
177
+ if (!subsection) {
178
+ subsection = { name: subsectionDir, files: [], subsections: [] };
179
+ section.subsections.push(subsection);
180
+ }
181
+ if (parts.length === 3) {
182
+ subsection.files.push({ relativePath: file.relativePath });
183
+ }
184
+ else {
185
+ const subSubDir = parts[2];
186
+ let subSubsection = subsection.subsections.find((s) => s.name === subSubDir);
187
+ if (!subSubsection) {
188
+ subSubsection = { name: subSubDir, files: [], subsections: [] };
189
+ subsection.subsections.push(subSubsection);
190
+ }
191
+ subSubsection.files.push({ relativePath: file.relativePath });
192
+ }
193
+ }
194
+ }
195
+ const sortedSections = Array.from(sections.values()).sort((a, b) => a.name.localeCompare(b.name));
196
+ for (const section of sortedSections) {
197
+ section.files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
198
+ section.subsections.sort((a, b) => a.name.localeCompare(b.name));
199
+ for (const subsection of section.subsections) {
200
+ subsection.files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
201
+ subsection.subsections.sort((a, b) => a.name.localeCompare(b.name));
202
+ }
203
+ }
204
+ return sortedSections;
205
+ }
206
+ function generateClaudeMdIndex(data) {
207
+ const { docsPath, sections, outputFile } = data;
208
+ const parts = [];
209
+ parts.push('[Next.js Docs Index]');
210
+ parts.push(`root: ${docsPath}`);
211
+ parts.push('STOP. What you remember about Next.js is WRONG for this project. Always search docs and read before any task.');
212
+ const targetFile = outputFile || 'CLAUDE.md';
213
+ parts.push(`If docs missing, run this command first: npx @next/codemod agents-md --output ${targetFile}`);
214
+ const allFiles = collectAllFilesFromSections(sections);
215
+ const grouped = groupByDirectory(allFiles);
216
+ for (const [dir, files] of grouped) {
217
+ parts.push(`${dir}:{${files.join(',')}}`);
218
+ }
219
+ return parts.join('|');
220
+ }
221
+ function collectAllFilesFromSections(sections) {
222
+ const files = [];
223
+ for (const section of sections) {
224
+ for (const file of section.files) {
225
+ files.push(file.relativePath);
226
+ }
227
+ files.push(...collectAllFilesFromSections(section.subsections));
228
+ }
229
+ return files;
230
+ }
231
+ function groupByDirectory(files) {
232
+ const grouped = new Map();
233
+ for (const filePath of files) {
234
+ const lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
235
+ const dir = lastSlash === -1 ? '.' : filePath.slice(0, lastSlash);
236
+ const fileName = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
237
+ const existing = grouped.get(dir);
238
+ if (existing) {
239
+ existing.push(fileName);
240
+ }
241
+ else {
242
+ grouped.set(dir, [fileName]);
243
+ }
244
+ }
245
+ return grouped;
246
+ }
247
+ const START_MARKER = '<!-- NEXT-AGENTS-MD-START -->';
248
+ const END_MARKER = '<!-- NEXT-AGENTS-MD-END -->';
249
+ function hasExistingIndex(content) {
250
+ return content.includes(START_MARKER);
251
+ }
252
+ function wrapWithMarkers(content) {
253
+ return `${START_MARKER}${content}${END_MARKER}`;
254
+ }
255
+ function injectIntoClaudeMd(claudeMdContent, indexContent) {
256
+ const wrappedContent = wrapWithMarkers(indexContent);
257
+ if (hasExistingIndex(claudeMdContent)) {
258
+ const startIdx = claudeMdContent.indexOf(START_MARKER);
259
+ const endIdx = claudeMdContent.indexOf(END_MARKER) + END_MARKER.length;
260
+ return (claudeMdContent.slice(0, startIdx) +
261
+ wrappedContent +
262
+ claudeMdContent.slice(endIdx));
263
+ }
264
+ const separator = claudeMdContent.endsWith('\n') ? '\n' : '\n\n';
265
+ return claudeMdContent + separator + wrappedContent + '\n';
266
+ }
267
+ const GITIGNORE_ENTRY = '.next-docs/';
268
+ function ensureGitignoreEntry(cwd) {
269
+ const gitignorePath = path_1.default.join(cwd, '.gitignore');
270
+ const entryRegex = /^\s*\.next-docs(?:\/.*)?$/;
271
+ let content = '';
272
+ if (fs_1.default.existsSync(gitignorePath)) {
273
+ content = fs_1.default.readFileSync(gitignorePath, 'utf-8');
274
+ }
275
+ const hasEntry = content.split(/\r?\n/).some((line) => entryRegex.test(line));
276
+ if (hasEntry) {
277
+ return { path: gitignorePath, updated: false, alreadyPresent: true };
278
+ }
279
+ const needsNewline = content.length > 0 && !content.endsWith('\n');
280
+ const header = content.includes('# next-agents-md')
281
+ ? ''
282
+ : '# next-agents-md\n';
283
+ const newContent = content + (needsNewline ? '\n' : '') + header + `${GITIGNORE_ENTRY}\n`;
284
+ fs_1.default.writeFileSync(gitignorePath, newContent, 'utf-8');
285
+ return { path: gitignorePath, updated: true, alreadyPresent: false };
286
+ }
287
+ function detectWorkspace(cwd) {
288
+ const packageJsonPath = path_1.default.join(cwd, 'package.json');
289
+ // Check pnpm workspaces (pnpm-workspace.yaml)
290
+ const pnpmWorkspacePath = path_1.default.join(cwd, 'pnpm-workspace.yaml');
291
+ if (fs_1.default.existsSync(pnpmWorkspacePath)) {
292
+ const packages = parsePnpmWorkspace(pnpmWorkspacePath);
293
+ if (packages.length > 0) {
294
+ return { isMonorepo: true, type: 'pnpm', packages };
295
+ }
296
+ }
297
+ // Check npm/yarn workspaces (package.json workspaces field)
298
+ if (fs_1.default.existsSync(packageJsonPath)) {
299
+ const packages = parsePackageJsonWorkspaces(packageJsonPath);
300
+ if (packages.length > 0) {
301
+ return { isMonorepo: true, type: 'npm', packages };
302
+ }
303
+ }
304
+ // Check Lerna (lerna.json)
305
+ const lernaPath = path_1.default.join(cwd, 'lerna.json');
306
+ if (fs_1.default.existsSync(lernaPath)) {
307
+ const packages = parseLernaConfig(lernaPath);
308
+ if (packages.length > 0) {
309
+ return { isMonorepo: true, type: 'lerna', packages };
310
+ }
311
+ }
312
+ // Check Nx (nx.json)
313
+ const nxPath = path_1.default.join(cwd, 'nx.json');
314
+ if (fs_1.default.existsSync(nxPath)) {
315
+ const packages = parseNxWorkspace(cwd, packageJsonPath);
316
+ if (packages.length > 0) {
317
+ return { isMonorepo: true, type: 'nx', packages };
318
+ }
319
+ }
320
+ return { isMonorepo: false, type: null, packages: [] };
321
+ }
322
+ function parsePnpmWorkspace(filePath) {
323
+ try {
324
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
325
+ const lines = content.split('\n');
326
+ const packages = [];
327
+ let inPackages = false;
328
+ for (const line of lines) {
329
+ const trimmed = line.trim();
330
+ if (trimmed === 'packages:') {
331
+ inPackages = true;
332
+ continue;
333
+ }
334
+ if (inPackages) {
335
+ if (trimmed && !trimmed.startsWith('-') && !trimmed.startsWith('#')) {
336
+ break;
337
+ }
338
+ const match = trimmed.match(/^-\s*['"]?([^'"]+)['"]?$/);
339
+ if (match) {
340
+ packages.push(match[1]);
341
+ }
342
+ }
343
+ }
344
+ return packages;
345
+ }
346
+ catch {
347
+ return [];
348
+ }
349
+ }
350
+ function parsePackageJsonWorkspaces(filePath) {
351
+ try {
352
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
353
+ const pkg = JSON.parse(content);
354
+ if (Array.isArray(pkg.workspaces)) {
355
+ return pkg.workspaces;
356
+ }
357
+ if (pkg.workspaces?.packages && Array.isArray(pkg.workspaces.packages)) {
358
+ return pkg.workspaces.packages;
359
+ }
360
+ return [];
361
+ }
362
+ catch {
363
+ return [];
364
+ }
365
+ }
366
+ function parseLernaConfig(filePath) {
367
+ try {
368
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
369
+ const config = JSON.parse(content);
370
+ if (Array.isArray(config.packages)) {
371
+ return config.packages;
372
+ }
373
+ return ['packages/*'];
374
+ }
375
+ catch {
376
+ return [];
377
+ }
378
+ }
379
+ function parseNxWorkspace(cwd, packageJsonPath) {
380
+ if (fs_1.default.existsSync(packageJsonPath)) {
381
+ const packages = parsePackageJsonWorkspaces(packageJsonPath);
382
+ if (packages.length > 0) {
383
+ return packages;
384
+ }
385
+ }
386
+ const defaultPatterns = ['apps/*', 'libs/*', 'packages/*'];
387
+ const existingPatterns = [];
388
+ for (const pattern of defaultPatterns) {
389
+ const basePath = path_1.default.join(cwd, pattern.replace('/*', ''));
390
+ if (fs_1.default.existsSync(basePath)) {
391
+ existingPatterns.push(pattern);
392
+ }
393
+ }
394
+ return existingPatterns;
395
+ }
396
+ function findNextjsInWorkspace(cwd, patterns) {
397
+ const packagePaths = expandWorkspacePatterns(cwd, patterns);
398
+ const versions = [];
399
+ for (const pkgPath of packagePaths) {
400
+ const packageJsonPath = path_1.default.join(pkgPath, 'package.json');
401
+ if (!fs_1.default.existsSync(packageJsonPath))
402
+ continue;
403
+ try {
404
+ const content = fs_1.default.readFileSync(packageJsonPath, 'utf-8');
405
+ const pkg = JSON.parse(content);
406
+ const nextVersion = pkg.dependencies?.next || pkg.devDependencies?.next;
407
+ if (nextVersion) {
408
+ versions.push(nextVersion.replace(/^[\^~>=<]+/, ''));
409
+ }
410
+ }
411
+ catch {
412
+ // Skip invalid package.json
413
+ }
414
+ }
415
+ return findHighestVersion(versions);
416
+ }
417
+ function expandWorkspacePatterns(cwd, patterns) {
418
+ const packagePaths = [];
419
+ for (const pattern of patterns) {
420
+ if (pattern.startsWith('!'))
421
+ continue;
422
+ if (pattern.includes('*')) {
423
+ packagePaths.push(...expandGlobPattern(cwd, pattern));
424
+ }
425
+ else {
426
+ const fullPath = path_1.default.join(cwd, pattern);
427
+ if (fs_1.default.existsSync(fullPath)) {
428
+ packagePaths.push(fullPath);
429
+ }
430
+ }
431
+ }
432
+ return [...new Set(packagePaths)];
433
+ }
434
+ function expandGlobPattern(cwd, pattern) {
435
+ const parts = pattern.split('/');
436
+ const results = [];
437
+ function walk(currentPath, partIndex) {
438
+ if (partIndex >= parts.length) {
439
+ if (fs_1.default.existsSync(path_1.default.join(currentPath, 'package.json'))) {
440
+ results.push(currentPath);
441
+ }
442
+ return;
443
+ }
444
+ const part = parts[partIndex];
445
+ if (part === '*') {
446
+ if (!fs_1.default.existsSync(currentPath))
447
+ return;
448
+ try {
449
+ for (const entry of fs_1.default.readdirSync(currentPath)) {
450
+ const fullPath = path_1.default.join(currentPath, entry);
451
+ if (isDirectory(fullPath)) {
452
+ if (partIndex === parts.length - 1) {
453
+ if (fs_1.default.existsSync(path_1.default.join(fullPath, 'package.json'))) {
454
+ results.push(fullPath);
455
+ }
456
+ }
457
+ else {
458
+ walk(fullPath, partIndex + 1);
459
+ }
460
+ }
461
+ }
462
+ }
463
+ catch {
464
+ // Permission denied
465
+ }
466
+ }
467
+ else if (part === '**') {
468
+ walkRecursive(currentPath, results);
469
+ }
470
+ else {
471
+ walk(path_1.default.join(currentPath, part), partIndex + 1);
472
+ }
473
+ }
474
+ walk(cwd, 0);
475
+ return results;
476
+ }
477
+ function walkRecursive(dir, results) {
478
+ if (!fs_1.default.existsSync(dir))
479
+ return;
480
+ if (fs_1.default.existsSync(path_1.default.join(dir, 'package.json'))) {
481
+ results.push(dir);
482
+ }
483
+ try {
484
+ for (const entry of fs_1.default.readdirSync(dir)) {
485
+ if (entry === 'node_modules' || entry.startsWith('.'))
486
+ continue;
487
+ const fullPath = path_1.default.join(dir, entry);
488
+ if (isDirectory(fullPath)) {
489
+ walkRecursive(fullPath, results);
490
+ }
491
+ }
492
+ }
493
+ catch {
494
+ // Permission denied
495
+ }
496
+ }
497
+ function isDirectory(dirPath) {
498
+ try {
499
+ return fs_1.default.statSync(dirPath).isDirectory();
500
+ }
501
+ catch {
502
+ return false;
503
+ }
504
+ }
505
+ function findHighestVersion(versions) {
506
+ if (versions.length === 0)
507
+ return null;
508
+ if (versions.length === 1)
509
+ return versions[0];
510
+ return versions.reduce((highest, current) => {
511
+ return compareVersions(current, highest) > 0 ? current : highest;
512
+ });
513
+ }
514
+ function compareVersions(a, b) {
515
+ const parseVersion = (v) => {
516
+ const match = v.match(/^(\d+)\.(\d+)\.(\d+)/);
517
+ if (!match)
518
+ return [0, 0, 0];
519
+ return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
520
+ };
521
+ const [aMajor, aMinor, aPatch] = parseVersion(a);
522
+ const [bMajor, bMinor, bPatch] = parseVersion(b);
523
+ if (aMajor !== bMajor)
524
+ return aMajor - bMajor;
525
+ if (aMinor !== bMinor)
526
+ return aMinor - bMinor;
527
+ return aPatch - bPatch;
528
+ }
529
+ //# sourceMappingURL=agents-md.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "16.2.0-canary.4",
3
+ "version": "16.2.0-canary.41",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",