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