@next/codemod 16.5.0-canary.8 → 16.5.0-canary.9

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 CHANGED
@@ -58,18 +58,22 @@ async function runAgentsMd(options) {
58
58
  nextjsVersion = promptedOptions.nextVersion;
59
59
  targetFile = promptedOptions.targetFile;
60
60
  }
61
- const agentsMdPath = path_1.default.join(cwd, targetFile);
62
- // Next.js >= 16.2.0 ships its docs inside the published package. When the
63
- // installed version matches the requested one, index the bundled docs
64
- // directly instead of downloading a copy into .next-docs.
65
- const bundledDocs = (0, agents_md_1.getBundledDocsInfo)(cwd);
66
- const useBundledDocs = bundledDocs !== null && bundledDocs.version === nextjsVersion;
61
+ const agentsMdPath = path_1.default.resolve(cwd, targetFile);
62
+ // Agents resolve paths in the index from the directory of the output file,
63
+ // so every link written into it is relative to that directory, not the cwd.
64
+ const outputDir = path_1.default.dirname(agentsMdPath);
65
+ // Next.js >= 16.2.0 ships its docs inside the published package. When an
66
+ // installed version (from the cwd or, in a monorepo, a workspace package)
67
+ // matches the requested one, index the bundled docs directly instead of
68
+ // downloading a copy into .next-docs.
69
+ const bundledDocs = (0, agents_md_1.getBundledDocsInfo)(cwd, nextjsVersion);
70
+ const useBundledDocs = bundledDocs !== null;
67
71
  const docsPath = useBundledDocs
68
72
  ? bundledDocs.docsPath
69
73
  : path_1.default.join(cwd, DOCS_DIR_NAME);
70
74
  const docsLinkPath = useBundledDocs
71
- ? (0, agents_md_1.getBundledDocsLinkPath)(cwd, bundledDocs.docsPath)
72
- : `./${DOCS_DIR_NAME}`;
75
+ ? (0, agents_md_1.getBundledDocsLinkPath)(outputDir, bundledDocs.docsPath, bundledDocs.resolvedFrom)
76
+ : (0, agents_md_1.getRelativeLinkPath)(outputDir, docsPath);
73
77
  let sizeBefore = 0;
74
78
  let isNewFile = true;
75
79
  let existingContent = '';
@@ -79,7 +83,10 @@ async function runAgentsMd(options) {
79
83
  isNewFile = false;
80
84
  }
81
85
  if (useBundledDocs) {
82
- console.log(`\nUsing the docs bundled with Next.js ${picocolors_1.default.cyan(nextjsVersion)} at ${picocolors_1.default.cyan(docsLinkPath)} (no download needed).`);
86
+ const fromWorkspacePackage = path_1.default.resolve(bundledDocs.resolvedFrom) !== path_1.default.resolve(cwd)
87
+ ? ` from workspace package ${picocolors_1.default.cyan(path_1.default.relative(cwd, bundledDocs.resolvedFrom).replace(/\\/g, '/'))}`
88
+ : '';
89
+ console.log(`\nUsing the docs bundled with Next.js ${picocolors_1.default.cyan(nextjsVersion)}${fromWorkspacePackage} at ${picocolors_1.default.cyan(docsLinkPath)} (no download needed).`);
83
90
  }
84
91
  else {
85
92
  console.log(`\nDownloading Next.js ${picocolors_1.default.cyan(nextjsVersion)} documentation to ${picocolors_1.default.cyan(DOCS_DIR_NAME)}...`);
@@ -97,7 +104,8 @@ async function runAgentsMd(options) {
97
104
  const indexContent = (0, agents_md_1.generateAgentsMdIndex)({
98
105
  docsPath: docsLinkPath,
99
106
  sections,
100
- outputFile: targetFile,
107
+ // The regeneration hint is run from the output file's directory.
108
+ outputFile: path_1.default.relative(outputDir, agentsMdPath).replace(/\\/g, '/'),
101
109
  });
102
110
  const newContent = (0, agents_md_1.injectIntoAgentsMd)(existingContent, indexContent);
103
111
  fs_1.default.writeFileSync(agentsMdPath, newContent, 'utf-8');
@@ -406,6 +406,418 @@ This is my project documentation.
406
406
  }, 30000) // Increase timeout for git clone
407
407
  })
408
408
 
409
+ // The index `root` is resolved by agents from the directory of the output
410
+ // file, so it must be relative to that directory, not to the cwd.
411
+ describe('paths relative to the output file', () => {
412
+ const agentsMdLib = require('../../lib/agents-md')
413
+ let originalCwd
414
+ let pullDocsSpy
415
+
416
+ // Install `next` the way pnpm's isolated linker does: the real package
417
+ // lives in the root `node_modules/.pnpm` store and is only symlinked into
418
+ // the package that depends on it.
419
+ function installNextIsolated(projectDir, pkgDir, version) {
420
+ const storeDir = path.join(
421
+ projectDir,
422
+ 'node_modules',
423
+ '.pnpm',
424
+ `next@${version}`,
425
+ 'node_modules',
426
+ 'next'
427
+ )
428
+ writeBundledNextPackage(storeDir, version)
429
+ const linkDir = path.join(pkgDir, 'node_modules')
430
+ fs.mkdirSync(linkDir, { recursive: true })
431
+ fs.symlinkSync(storeDir, path.join(linkDir, 'next'), 'junction')
432
+ }
433
+
434
+ function writeBundledNextPackage(nextDir, version) {
435
+ const docsDir = path.join(nextDir, 'dist', 'docs', '01-app')
436
+ fs.mkdirSync(docsDir, { recursive: true })
437
+ fs.writeFileSync(
438
+ path.join(nextDir, 'package.json'),
439
+ JSON.stringify({ name: 'next', version })
440
+ )
441
+ fs.writeFileSync(path.join(docsDir, `docs-${version}.md`), '# Docs')
442
+ }
443
+
444
+ function readIndex(agentsMdPath) {
445
+ const content = fs.readFileSync(agentsMdPath, 'utf-8')
446
+ return {
447
+ content,
448
+ root: content.match(/\|root: ([^|]+)\|/)[1],
449
+ }
450
+ }
451
+
452
+ // Returns the absolute path `root` points at, resolved the way an agent
453
+ // reading the output file would resolve it.
454
+ function resolveRoot(agentsMdPath) {
455
+ return path.resolve(path.dirname(agentsMdPath), readIndex(agentsMdPath).root)
456
+ }
457
+
458
+ beforeEach(() => {
459
+ originalCwd = process.cwd()
460
+ // Stub the download so the `.next-docs` branch is testable offline.
461
+ pullDocsSpy = jest
462
+ .spyOn(agentsMdLib, 'pullDocs')
463
+ .mockImplementation(async ({ docsDir }) => {
464
+ const sectionDir = path.join(docsDir, '01-app')
465
+ fs.mkdirSync(sectionDir, { recursive: true })
466
+ fs.writeFileSync(path.join(sectionDir, 'downloaded.md'), '# Docs')
467
+ return { success: true, docsPath: docsDir }
468
+ })
469
+ })
470
+
471
+ afterEach(() => {
472
+ process.chdir(originalCwd)
473
+ pullDocsSpy.mockRestore()
474
+ })
475
+
476
+ it('links bundled docs relative to an output file outside the cwd', async () => {
477
+ const appDir = path.join(testProjectDir, 'apps', 'web')
478
+ installNextIsolated(testProjectDir, appDir, '16.4.0')
479
+ process.chdir(appDir)
480
+
481
+ await runAgentsMd({ output: '../../AGENTS.md' })
482
+
483
+ const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
484
+ expect(readIndex(agentsMdPath).root).toBe(
485
+ './apps/web/node_modules/next/dist/docs'
486
+ )
487
+ expect(
488
+ fs.existsSync(path.join(resolveRoot(agentsMdPath), '01-app'))
489
+ ).toBe(true)
490
+ expect(pullDocsSpy).not.toHaveBeenCalled()
491
+ })
492
+
493
+ it('links downloaded docs relative to an output file outside the cwd', async () => {
494
+ const appDir = path.join(testProjectDir, 'apps', 'web')
495
+ fs.mkdirSync(appDir, { recursive: true })
496
+ process.chdir(appDir)
497
+
498
+ await runAgentsMd({ version: '15.0.0', output: '../../AGENTS.md' })
499
+
500
+ const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
501
+ expect(readIndex(agentsMdPath).root).toBe('./apps/web/.next-docs')
502
+ expect(fs.realpathSync(resolveRoot(agentsMdPath))).toBe(
503
+ fs.realpathSync(path.join(appDir, '.next-docs'))
504
+ )
505
+ })
506
+
507
+ it('keeps the ./ prefix for the .next-docs dot-directory', async () => {
508
+ process.chdir(testProjectDir)
509
+
510
+ await runAgentsMd({ version: '15.0.0', output: 'AGENTS.md' })
511
+
512
+ expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
513
+ './.next-docs'
514
+ )
515
+ })
516
+
517
+ it('links the indexed install when the output dir resolves a different next', async () => {
518
+ // Root has a hoisted next@16.3.0; the app has its own next@16.4.0.
519
+ writeBundledNextPackage(
520
+ path.join(testProjectDir, 'node_modules', 'next'),
521
+ '16.3.0'
522
+ )
523
+ const appDir = path.join(testProjectDir, 'apps', 'web')
524
+ installNextIsolated(testProjectDir, appDir, '16.4.0')
525
+ process.chdir(appDir)
526
+
527
+ await runAgentsMd({ output: '../../AGENTS.md' })
528
+
529
+ const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
530
+ const linkedPkg = JSON.parse(
531
+ fs.readFileSync(
532
+ path.join(resolveRoot(agentsMdPath), '..', '..', 'package.json'),
533
+ 'utf-8'
534
+ )
535
+ )
536
+ expect(linkedPkg.version).toBe('16.4.0')
537
+ expect(readIndex(agentsMdPath).content).toContain('docs-16.4.0.md')
538
+ })
539
+
540
+ it('writes a regeneration hint that is relative to the output file', async () => {
541
+ const appDir = path.join(testProjectDir, 'apps', 'web')
542
+ installNextIsolated(testProjectDir, appDir, '16.4.0')
543
+ process.chdir(appDir)
544
+
545
+ await runAgentsMd({ output: '../../AGENTS.md' })
546
+
547
+ const { content } = readIndex(path.join(testProjectDir, 'AGENTS.md'))
548
+ expect(content).toContain(
549
+ "If docs missing, run this command from this file's directory: npx @next/codemod agents-md --output AGENTS.md|"
550
+ )
551
+ expect(content).not.toContain('--output ../../AGENTS.md')
552
+ })
553
+
554
+ it('supports an absolute --output path', async () => {
555
+ const appDir = path.join(testProjectDir, 'apps', 'web')
556
+ installNextIsolated(testProjectDir, appDir, '16.4.0')
557
+ const outputDir = path.join(testProjectDir, 'docs-out')
558
+ fs.mkdirSync(outputDir)
559
+ const agentsMdPath = path.join(outputDir, 'CLAUDE.md')
560
+ process.chdir(appDir)
561
+
562
+ await runAgentsMd({ output: agentsMdPath })
563
+
564
+ expect(fs.existsSync(agentsMdPath)).toBe(true)
565
+ const { root, content } = readIndex(agentsMdPath)
566
+ expect(root).toBe('../apps/web/node_modules/next/dist/docs')
567
+ expect(
568
+ fs.existsSync(path.join(resolveRoot(agentsMdPath), '01-app'))
569
+ ).toBe(true)
570
+ expect(content).toContain('--output CLAUDE.md|')
571
+ })
572
+
573
+ // The cwd/output path and the path `next` resolves to can be spelled
574
+ // differently for the same directory: a symlinked alias, or an 8.3 short
575
+ // name like `C:\Users\RUNNER~1\...` on Windows, where `process.cwd()`
576
+ // keeps the short name while module resolution returns the long one.
577
+ // Links must still be computed between consistent spellings.
578
+ describe('when the cwd is spelled differently than the realpath', () => {
579
+ let aliasBase
580
+ let aliasDir
581
+ let cwdSpy
582
+
583
+ // Run from `dir` (inside the real project) while `process.cwd()`
584
+ // reports it through the alias, like a non-canonical cwd on Windows.
585
+ function chdirViaAlias(dir) {
586
+ process.chdir(dir)
587
+ const aliased = path.join(
588
+ aliasDir,
589
+ path.relative(fs.realpathSync(testProjectDir), fs.realpathSync(dir))
590
+ )
591
+ cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(aliased)
592
+ }
593
+
594
+ beforeEach(() => {
595
+ aliasBase = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-md-alias-'))
596
+ aliasDir = path.join(aliasBase, 'project')
597
+ fs.symlinkSync(testProjectDir, aliasDir, 'junction')
598
+ })
599
+
600
+ afterEach(() => {
601
+ cwdSpy?.mockRestore()
602
+ cwdSpy = undefined
603
+ fs.rmSync(aliasBase, { recursive: true, force: true })
604
+ })
605
+
606
+ it('links the conventional path through a symlinked cwd', async () => {
607
+ writeBundledNextPackage(
608
+ path.join(testProjectDir, 'node_modules', 'next'),
609
+ '16.4.0'
610
+ )
611
+ chdirViaAlias(testProjectDir)
612
+
613
+ await runAgentsMd({ output: 'AGENTS.md' })
614
+
615
+ const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
616
+ expect(readIndex(agentsMdPath).root).toBe(
617
+ './node_modules/next/dist/docs'
618
+ )
619
+ })
620
+
621
+ it('links a hoisted install canonically through a symlinked cwd', async () => {
622
+ writeBundledNextPackage(
623
+ path.join(testProjectDir, 'node_modules', 'next'),
624
+ '16.4.0'
625
+ )
626
+ const appDir = path.join(testProjectDir, 'apps', 'web')
627
+ fs.mkdirSync(appDir, { recursive: true })
628
+ chdirViaAlias(appDir)
629
+
630
+ await runAgentsMd({ output: 'AGENTS.md' })
631
+
632
+ const agentsMdPath = path.join(testProjectDir, 'apps', 'web', 'AGENTS.md')
633
+ expect(readIndex(agentsMdPath).root).toBe(
634
+ '../../node_modules/next/dist/docs'
635
+ )
636
+ expect(
637
+ fs.existsSync(path.join(resolveRoot(agentsMdPath), '01-app'))
638
+ ).toBe(true)
639
+ })
640
+
641
+ it('detects the same install when only the native realpath canonicalizes', async () => {
642
+ // Simulate Windows 8.3 short names: the JS `fs.realpathSync` leaves
643
+ // the alias spelling as-is, while `fs.realpathSync.native` expands it.
644
+ writeBundledNextPackage(
645
+ path.join(testProjectDir, 'node_modules', 'next'),
646
+ '16.4.0'
647
+ )
648
+ chdirViaAlias(testProjectDir)
649
+ const nativeRealpath = fs.realpathSync.native
650
+ const realpathSpy = jest
651
+ .spyOn(fs, 'realpathSync')
652
+ .mockImplementation((p) => path.resolve(String(p)))
653
+ fs.realpathSync.native = nativeRealpath
654
+
655
+ try {
656
+ await runAgentsMd({ output: 'AGENTS.md' })
657
+ } finally {
658
+ realpathSpy.mockRestore()
659
+ }
660
+
661
+ expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
662
+ './node_modules/next/dist/docs'
663
+ )
664
+ })
665
+ })
666
+
667
+ it('always prefixes relative links with ./ or ../', () => {
668
+ const { getRelativeLinkPath } = agentsMdLib
669
+ const base = path.join(testProjectDir, 'a', 'b')
670
+
671
+ expect(getRelativeLinkPath(base, base)).toBe('./')
672
+ expect(getRelativeLinkPath(base, path.join(testProjectDir, 'a'))).toBe(
673
+ '../'
674
+ )
675
+ expect(getRelativeLinkPath(base, path.join(base, '.next-docs'))).toBe(
676
+ './.next-docs'
677
+ )
678
+ expect(
679
+ getRelativeLinkPath(base, path.join(testProjectDir, 'a', 'docs'))
680
+ ).toBe('../docs')
681
+ expect(getRelativeLinkPath(base, path.join(base, '..docs'))).toBe(
682
+ './..docs'
683
+ )
684
+ })
685
+
686
+ // In a pnpm monorepo `next` is usually not resolvable from the repo root,
687
+ // so the bundled docs have to be found through the workspace packages.
688
+ describe('bundled docs from workspace packages', () => {
689
+ function setupWorkspace(patterns, packages) {
690
+ fs.writeFileSync(
691
+ path.join(testProjectDir, 'package.json'),
692
+ JSON.stringify({ name: 'root', private: true })
693
+ )
694
+ fs.writeFileSync(
695
+ path.join(testProjectDir, 'pnpm-workspace.yaml'),
696
+ `packages:\n${patterns.map((p) => ` - '${p}'`).join('\n')}\n`
697
+ )
698
+ for (const [pkg, version] of Object.entries(packages)) {
699
+ const pkgDir = path.join(testProjectDir, pkg)
700
+ fs.mkdirSync(pkgDir, { recursive: true })
701
+ fs.writeFileSync(
702
+ path.join(pkgDir, 'package.json'),
703
+ JSON.stringify({ name: path.basename(pkg) })
704
+ )
705
+ if (version) installNextIsolated(testProjectDir, pkgDir, version)
706
+ }
707
+ }
708
+
709
+ function output() {
710
+ // Package names are colored; strip ANSI so FORCE_COLOR doesn't matter.
711
+ return require('strip-ansi')(consoleOutput.join('\n'))
712
+ }
713
+
714
+ it('uses the bundled docs of a workspace package when run from the root', async () => {
715
+ setupWorkspace(['apps/*'], { 'apps/web': '16.4.0' })
716
+ process.chdir(testProjectDir)
717
+
718
+ await runAgentsMd({ output: 'AGENTS.md' })
719
+
720
+ const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
721
+ expect(readIndex(agentsMdPath).root).toBe(
722
+ './apps/web/node_modules/next/dist/docs'
723
+ )
724
+ expect(
725
+ fs.existsSync(path.join(resolveRoot(agentsMdPath), '01-app'))
726
+ ).toBe(true)
727
+ expect(pullDocsSpy).not.toHaveBeenCalled()
728
+ expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
729
+ false
730
+ )
731
+ expect(fs.existsSync(path.join(testProjectDir, '.gitignore'))).toBe(
732
+ false
733
+ )
734
+ expect(output()).toContain('workspace package apps/web')
735
+ })
736
+
737
+ it('indexes the package with the highest Next.js version', async () => {
738
+ setupWorkspace(['apps/a', 'apps/b'], {
739
+ 'apps/a': '16.3.0',
740
+ 'apps/b': '16.4.0',
741
+ })
742
+ process.chdir(testProjectDir)
743
+
744
+ await runAgentsMd({ output: 'AGENTS.md' })
745
+
746
+ const { root, content } = readIndex(
747
+ path.join(testProjectDir, 'AGENTS.md')
748
+ )
749
+ expect(root).toBe('./apps/b/node_modules/next/dist/docs')
750
+ expect(content).toContain('docs-16.4.0.md')
751
+ expect(pullDocsSpy).not.toHaveBeenCalled()
752
+ })
753
+
754
+ it('picks the first matching package in workspace order and logs it', async () => {
755
+ // Listed b before a, so the choice follows the workspace patterns
756
+ // rather than alphabetical order.
757
+ setupWorkspace(['apps/b', 'apps/a'], {
758
+ 'apps/a': '16.4.0',
759
+ 'apps/b': '16.4.0',
760
+ })
761
+ process.chdir(testProjectDir)
762
+
763
+ await runAgentsMd({ output: 'AGENTS.md' })
764
+
765
+ expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
766
+ './apps/b/node_modules/next/dist/docs'
767
+ )
768
+ expect(output()).toContain('workspace package apps/b')
769
+ expect(output()).not.toContain('workspace package apps/a')
770
+ })
771
+
772
+ it('prefers a matching install resolved from the cwd', async () => {
773
+ setupWorkspace(['apps/*'], { 'apps/web': '16.4.0' })
774
+ writeBundledNextPackage(
775
+ path.join(testProjectDir, 'node_modules', 'next'),
776
+ '16.4.0'
777
+ )
778
+ process.chdir(testProjectDir)
779
+
780
+ await runAgentsMd({ output: 'AGENTS.md' })
781
+
782
+ expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
783
+ './node_modules/next/dist/docs'
784
+ )
785
+ expect(output()).not.toContain('workspace package')
786
+ })
787
+
788
+ it('uses a workspace package matching --version when the cwd resolves another version', async () => {
789
+ setupWorkspace(['apps/*'], { 'apps/web': '16.4.0' })
790
+ writeBundledNextPackage(
791
+ path.join(testProjectDir, 'node_modules', 'next'),
792
+ '16.3.0'
793
+ )
794
+ process.chdir(testProjectDir)
795
+
796
+ await runAgentsMd({ version: '16.4.0', output: 'AGENTS.md' })
797
+
798
+ const { root, content } = readIndex(
799
+ path.join(testProjectDir, 'AGENTS.md')
800
+ )
801
+ expect(root).toBe('./apps/web/node_modules/next/dist/docs')
802
+ expect(content).toContain('docs-16.4.0.md')
803
+ expect(pullDocsSpy).not.toHaveBeenCalled()
804
+ })
805
+
806
+ it('still downloads when no workspace package has matching bundled docs', async () => {
807
+ setupWorkspace(['apps/*'], { 'apps/web': '16.4.0' })
808
+ process.chdir(testProjectDir)
809
+
810
+ await runAgentsMd({ version: '15.0.0', output: 'AGENTS.md' })
811
+
812
+ expect(pullDocsSpy).toHaveBeenCalled()
813
+ expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
814
+ './.next-docs'
815
+ )
816
+ expect(output()).not.toContain('workspace package')
817
+ })
818
+ })
819
+ })
820
+
409
821
  describe('getNextjsVersion', () => {
410
822
  const fixturesDir = path.join(__dirname, 'fixtures/agents-md')
411
823
 
package/lib/agents-md.js CHANGED
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.refreshAgentRulesBlock = refreshAgentRulesBlock;
13
13
  exports.getNextjsVersion = getNextjsVersion;
14
14
  exports.getBundledDocsInfo = getBundledDocsInfo;
15
+ exports.getRelativeLinkPath = getRelativeLinkPath;
15
16
  exports.getBundledDocsLinkPath = getBundledDocsLinkPath;
16
17
  exports.pullDocs = pullDocs;
17
18
  exports.collectDocFiles = collectDocFiles;
@@ -87,33 +88,98 @@ function getNextjsVersion(cwd) {
87
88
  }
88
89
  /**
89
90
  * Next.js ships its documentation inside the published package (at
90
- * `dist/docs`) since 16.2.0. When the install resolved from `cwd` has
91
- * bundled docs, the index can point at them directly instead of
92
- * downloading a copy into `.next-docs`.
91
+ * `dist/docs`) since 16.2.0. When an installed `next` of the requested
92
+ * `version` has bundled docs, the index can point at them directly instead
93
+ * of downloading a copy into `.next-docs`.
94
+ *
95
+ * The install resolved from `cwd` wins; otherwise the workspace packages are
96
+ * searched.
93
97
  */
94
- function getBundledDocsInfo(cwd) {
98
+ function getBundledDocsInfo(cwd, version) {
99
+ const fromCwd = getBundledDocsInfoFrom(cwd);
100
+ if (fromCwd?.version === version) {
101
+ return fromCwd;
102
+ }
103
+ // Mirror getNextjsVersion: with isolated installs (pnpm), `next` is often
104
+ // only resolvable from the workspace package that depends on it, not from
105
+ // the repo root. Use the first package, in workspace order, that ships
106
+ // bundled docs for the requested version.
107
+ const workspace = detectWorkspace(cwd);
108
+ if (workspace.isMonorepo) {
109
+ for (const pkgPath of expandWorkspacePatterns(cwd, workspace.packages)) {
110
+ const info = getBundledDocsInfoFrom(pkgPath);
111
+ if (info?.version === version) {
112
+ return info;
113
+ }
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ function getBundledDocsInfoFrom(dir) {
95
119
  try {
96
- const nextPkgPath = require.resolve('next/package.json', { paths: [cwd] });
120
+ const nextPkgPath = require.resolve('next/package.json', { paths: [dir] });
97
121
  const pkg = JSON.parse(fs_1.default.readFileSync(nextPkgPath, 'utf-8'));
98
122
  const docsPath = path_1.default.join(path_1.default.dirname(nextPkgPath), 'dist', 'docs');
99
123
  if (!pkg.version || collectDocFiles(docsPath).length === 0) {
100
124
  return null;
101
125
  }
102
- return { docsPath, version: pkg.version };
126
+ return { docsPath, version: pkg.version, resolvedFrom: dir };
103
127
  }
104
128
  catch {
105
129
  return null;
106
130
  }
107
131
  }
108
- function getBundledDocsLinkPath(cwd, docsPath) {
109
- // Prefer the conventional path when it resolves from the project
110
- // (covers hoisted installs; pnpm exposes next via a node_modules symlink).
111
- const conventional = path_1.default.join(cwd, 'node_modules', 'next', 'dist', 'docs');
112
- if (fs_1.default.existsSync(conventional)) {
113
- return './node_modules/next/dist/docs';
114
- }
115
- const relative = path_1.default.relative(cwd, docsPath).replace(/\\/g, '/');
116
- return relative.startsWith('.') ? relative : `./${relative}`;
132
+ /**
133
+ * Returns `target` as a `./`- or `../`-prefixed, forward-slash path relative
134
+ * to `fromDir`. Agents resolve paths in the index from the directory of the
135
+ * output file, so `fromDir` must be that directory, not the cwd.
136
+ */
137
+ function getRelativeLinkPath(fromDir, target) {
138
+ const relative = path_1.default.relative(fromDir, target).replace(/\\/g, '/');
139
+ if (relative === '')
140
+ return './';
141
+ if (relative === '..')
142
+ return '../';
143
+ // A dot-directory such as `.next-docs` still needs the `./` prefix.
144
+ return relative.startsWith('../') ? relative : `./${relative}`;
145
+ }
146
+ /**
147
+ * Canonical spelling of `p`: symlinks resolved and, on Windows, 8.3 short
148
+ * names (e.g. `C:\Users\RUNNER~1`) expanded. `process.cwd()` can keep a
149
+ * non-canonical spelling while module resolution returns the canonical one,
150
+ * so compare and relate paths only in this form. Falls back to the resolved
151
+ * path when `p` cannot be canonicalized (e.g. it does not exist).
152
+ */
153
+ function canonicalPath(p) {
154
+ try {
155
+ return fs_1.default.realpathSync.native(p);
156
+ }
157
+ catch {
158
+ return path_1.default.resolve(p);
159
+ }
160
+ }
161
+ /**
162
+ * Link to the bundled docs of the install resolved from `resolvedFrom`,
163
+ * relative to `fromDir` (the directory of the output file).
164
+ */
165
+ function getBundledDocsLinkPath(fromDir, docsPath, resolvedFrom) {
166
+ // Prefer the conventional path of the resolved install over its realpath:
167
+ // pnpm's `.pnpm/next@x.y.z/...` realpath changes on every upgrade, while
168
+ // the `node_modules/next` symlink stays stable. Check it under the
169
+ // directory `next` was resolved from, not under `fromDir`, which may see a
170
+ // different `next` install.
171
+ //
172
+ // Paths are related in canonical form (see canonicalPath) so a cwd or
173
+ // --output spelled differently than the resolved install still produces a
174
+ // clean link. Only the parent of the `node_modules/next` symlink is
175
+ // canonicalized, never the symlink itself, so the stable path is kept.
176
+ const outputDir = canonicalPath(fromDir);
177
+ const conventional = path_1.default.join(canonicalPath(resolvedFrom), 'node_modules', 'next', 'dist', 'docs');
178
+ if (fs_1.default.existsSync(conventional) &&
179
+ canonicalPath(conventional) === canonicalPath(docsPath)) {
180
+ return getRelativeLinkPath(outputDir, conventional);
181
+ }
182
+ return getRelativeLinkPath(outputDir, canonicalPath(docsPath));
117
183
  }
118
184
  function versionToGitHubTag(version) {
119
185
  return version.startsWith('v') ? version : `v${version}`;
@@ -264,7 +330,7 @@ function generateAgentsMdIndex(data) {
264
330
  parts.push(`root: ${docsPath}`);
265
331
  parts.push('STOP. What you remember about Next.js is WRONG for this project. Always search docs and read before any task.');
266
332
  const targetFile = outputFile || 'AGENTS.md';
267
- parts.push(`If docs missing, run this command first: npx @next/codemod agents-md --output ${targetFile}`);
333
+ parts.push(`If docs missing, run this command from this file's directory: npx @next/codemod agents-md --output ${targetFile}`);
268
334
  const allFiles = collectAllFilesFromSections(sections);
269
335
  const grouped = groupByDirectory(allFiles);
270
336
  for (const [dir, files] of grouped) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "16.5.0-canary.8",
3
+ "version": "16.5.0-canary.9",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",