Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nested-variant-skill-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Fix `intent load` for a skill that ships in a nested copy of a dependency. When a workspace package such as `packages/bar` installed its own version of `@tanstack/query`, and the root `node_modules/@tanstack/query` was a different copy, `list` showed the nested skill but `load` looked for it under `node_modules/@tanstack/query/...` and failed. Intent now uses the `node_modules/<name>/...` path only when that entry is the same copy. Otherwise it reports the project-relative path, such as `packages/bar/node_modules/@tanstack/query/skills/fetching/SKILL.md`. Hoisted installs and pnpm or bun symlinks to the same copy keep their `node_modules/<name>/...` paths.
17 changes: 12 additions & 5 deletions packages/intent/src/skills/paths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync } from 'node:fs'
import { realpathSync } from 'node:fs'
import { join, relative } from 'node:path'
import { toPosixPath } from '../shared/utils.js'
import { formatIntentCommand } from '../shared/command-runner.js'
Expand Down Expand Up @@ -52,12 +52,19 @@ export function rewriteSkillLoadPaths({
}): void {
const topLevelPackageRoot =
packageName !== '' ? join(projectRoot, 'node_modules', packageName) : ''
const hasStableSymlink =
topLevelPackageRoot !== '' &&
(packageRoot === topLevelPackageRoot || existsSync(topLevelPackageRoot))
let matchesTopLevelEntry: boolean
try {
matchesTopLevelEntry =
topLevelPackageRoot !== '' &&
(packageRoot === topLevelPackageRoot ||
realpathSync.native(topLevelPackageRoot) ===
realpathSync.native(packageRoot))
} catch {
matchesTopLevelEntry = false
}

for (const skill of skills) {
if (hasStableSymlink) {
if (matchesTopLevelEntry) {
const relFromPackage = toPosixPath(relative(packageRoot, skill.path))
skill.path = `node_modules/${packageName}/${relFromPackage}`
} else {
Expand Down
35 changes: 35 additions & 0 deletions packages/intent/tests/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,41 @@ describe('loadIntentSkill', () => {
)
})

it('loads a nested dependency copy when the root has a different copy', () => {
const barDir = join(root, 'packages', 'bar')
writeJson(join(root, 'package.json'), {
name: 'test-monorepo',
private: true,
workspaces: ['packages/*'],
})
writeJson(join(barDir, 'package.json'), {
name: 'bar',
dependencies: {
'@tanstack/query': '5.0.0',
},
})
writeJson(
join(root, 'node_modules', '@tanstack', 'query', 'package.json'),
{
name: '@tanstack/query',
version: '4.0.0',
},
)
writeInstalledIntentPackage(barDir, {
name: '@tanstack/query',
version: '5.0.0',
skillName: 'fetching',
description: 'Query data fetching patterns',
})

const result = loadIntentSkill('@tanstack/query#fetching', { cwd: root })

expect(result.version).toBe('5.0.0')
expect(result.path).toBe(
'packages/bar/node_modules/@tanstack/query/skills/fetching/SKILL.md',
)
})

it('rewrites relative markdown destinations in loaded content', () => {
const pkgDir = join(root, 'node_modules', '@tanstack', 'query')
const skillDir = join(pkgDir, 'skills', 'fetching')
Expand Down
66 changes: 64 additions & 2 deletions packages/intent/tests/skill-paths.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { mkdirSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('skill path helpers', () => {
expect(isStableLoadPath('')).toBe(false)
})

it('rewrites to stable node_modules load paths when a top-level package path exists', () => {
it('rewrites to stable node_modules load paths when the package root is the top-level entry', () => {
const root = tempRoot()
const packageRoot = join(root, 'node_modules', '@tanstack', 'query')
const skillPath = join(packageRoot, 'skills', 'core', 'SKILL.md')
Expand All @@ -98,6 +98,68 @@ describe('skill path helpers', () => {
)
})

it('keeps the project-relative path when the top-level package is a different copy', () => {
const root = tempRoot()
mkdirSync(join(root, 'node_modules', '@tanstack', 'query'), {
recursive: true,
})
const packageRoot = join(
root,
'packages',
'bar',
'node_modules',
'@tanstack',
'query',
)
const skillPath = join(packageRoot, 'skills', 'core', 'SKILL.md')
mkdirSync(join(packageRoot, 'skills', 'core'), { recursive: true })
const skills = [skill(skillPath)]

rewriteSkillLoadPaths({
packageName: '@tanstack/query',
packageRoot,
projectRoot: root,
skills,
})

expect(skills[0]!.path).toBe(
'packages/bar/node_modules/@tanstack/query/skills/core/SKILL.md',
)
})

it('rewrites a symlinked store copy to the top-level link that targets it', () => {
const root = tempRoot()
const packageRoot = join(
root,
'node_modules',
'.pnpm',
'@tanstack+query@5.0.0',
'node_modules',
'@tanstack',
'query',
)
const skillPath = join(packageRoot, 'skills', 'core', 'SKILL.md')
mkdirSync(join(packageRoot, 'skills', 'core'), { recursive: true })
mkdirSync(join(root, 'node_modules', '@tanstack'))
symlinkSync(
packageRoot,
join(root, 'node_modules', '@tanstack', 'query'),
'dir',
)
const skills = [skill(skillPath)]

rewriteSkillLoadPaths({
packageName: '@tanstack/query',
packageRoot,
projectRoot: root,
skills,
})

expect(skills[0]!.path).toBe(
'node_modules/@tanstack/query/skills/core/SKILL.md',
)
})

it('does not invent stable top-level load paths for pnpm-internal package paths', () => {
const root = tempRoot()
const packageRoot = join(
Expand Down