Skip to content
Closed
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
---

Point a skill from a nested dependency copy at that copy. When a workspace package installed its own version of a package, such as `packages/a/node_modules/foo`, and the root had a different copy at `node_modules/foo`, `list` showed the nested skill but `load` reported its path as `node_modules/foo/...`. Loading then failed with `Resolved skill file was not found`, or read the root copy's file instead. Intent now uses `node_modules/<name>/...` only when that entry is the same copy, and otherwise reports a project-relative path such as `packages/a/node_modules/foo/skills/core/SKILL.md`. pnpm and bun direct dependencies keep their stable `node_modules/<name>/...` paths.
14 changes: 10 additions & 4 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 @@ -50,9 +50,15 @@ export function rewriteSkillLoadPaths({
projectRoot: string
skills: Array<SkillEntry>
}): void {
const hasStableSymlink =
packageName !== '' &&
existsSync(join(projectRoot, 'node_modules', packageName))
let hasStableSymlink: boolean
try {
hasStableSymlink =
packageName !== '' &&
realpathSync.native(join(projectRoot, 'node_modules', packageName)) ===
realpathSync.native(packageRoot)
} catch {
hasStableSymlink = false
}

for (const skill of skills) {
if (hasStableSymlink) {
Expand Down
57 changes: 57 additions & 0 deletions packages/intent/tests/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,39 @@ function writeInstalledIntentPackage(
})
}

function writeNestedVariantMonorepo(
root: string,
{ rootCopyHasSkill }: { rootCopyHasSkill: boolean },
): void {
writeJson(join(root, 'package.json'), {
name: 'root',
private: true,
workspaces: ['packages/*'],
dependencies: { foo: '1.0.0' },
})
writeJson(join(root, 'packages', 'a', 'package.json'), {
name: 'a',
dependencies: { foo: '2.0.0' },
})
for (const [pkgDir, version, hasSkill] of [
[join(root, 'node_modules', 'foo'), '1.0.0', rootCopyHasSkill],
[join(root, 'packages', 'a', 'node_modules', 'foo'), '2.0.0', true],
] as const) {
writeJson(join(pkgDir, 'package.json'), {
name: 'foo',
version,
repository: 'https://github.com/x/foo',
})
if (hasSkill) {
writeSkillMd({
content: `foo v${version[0]} content`,
dir: join(pkgDir, 'skills', 'core'),
frontmatter: { name: 'core', description: `foo v${version[0]} core` },
})
}
}
}

let root: string
let originalCwd: string

Expand Down Expand Up @@ -968,6 +1001,30 @@ describe('loadIntentSkill', () => {
)
})

it('loads a nested workspace dependency variant when the root link is a different copy', () => {
writeNestedVariantMonorepo(root, { rootCopyHasSkill: false })

const listed = listIntentSkills({ audience: 'human', cwd: root })
const nestedRoot = join(root, 'packages', 'a', 'node_modules', 'foo')
expect(listed.skills).toEqual([
expect.objectContaining({
use: 'foo#core',
packageRoot: nestedRoot,
packageVersion: '2.0.0',
}),
])

const resolved = resolveIntentSkill('foo#core', { cwd: root })
expect(resolved.packageRoot).toBe(nestedRoot)
expect(resolved.path).toBe(
'packages/a/node_modules/foo/skills/core/SKILL.md',
)

const loaded = loadIntentSkill('foo#core', { cwd: root })
expect(loaded.version).toBe('2.0.0')
expect(loaded.content).toContain('foo v2 content')
})

it('rewrites relative markdown destinations in loaded content', () => {
const pkgDir = join(root, 'node_modules', '@tanstack', 'query')
const skillDir = join(pkgDir, 'skills', 'fetching')
Expand Down
97 changes: 97 additions & 0 deletions packages/intent/tests/scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,103 @@ describe('scanIntentPackageAtRoot', () => {
})
})

describe('nested dependency variants', () => {
function writeFoo(pkgDir: string, version: string): void {
writeJson(join(createDir(pkgDir), 'package.json'), {
name: 'foo',
version,
repository: 'https://github.com/x/foo',
})
}

function writeMonorepo({ rootCopyHasSkill }: { rootCopyHasSkill: boolean }): {
rootCopy: string
nestedCopy: string
} {
writeJson(join(root, 'package.json'), {
name: 'root',
private: true,
workspaces: ['packages/*'],
dependencies: { foo: '1.0.0' },
})
writeJson(join(createDir(root, 'packages', 'a'), 'package.json'), {
name: 'a',
dependencies: { foo: '2.0.0' },
})
const rootCopy = join(root, 'node_modules', 'foo')
const nestedCopy = join(root, 'packages', 'a', 'node_modules', 'foo')
writeFoo(rootCopy, '1.0.0')
writeFoo(nestedCopy, '2.0.0')
writeSkillMd(createDir(nestedCopy, 'skills', 'core'), {
name: 'core',
description: 'foo v2 core',
})
if (rootCopyHasSkill) {
writeSkillMd(createDir(rootCopy, 'skills', 'core'), {
name: 'core',
description: 'foo v1 core',
})
}
return { rootCopy, nestedCopy }
}

it('lists a nested variant with a path into its own copy, not the root link', () => {
const { nestedCopy } = writeMonorepo({ rootCopyHasSkill: false })

const result = scanForIntents(root)

const foo = result.packages.find((pkg) => pkg.name === 'foo')
expect(foo?.packageRoot).toBe(nestedCopy)
expect(foo?.skills.map((skill) => skill.path)).toEqual([
'packages/a/node_modules/foo/skills/core/SKILL.md',
])
})

it('never points a nested variant at the root copy of the same package', () => {
const { nestedCopy } = writeMonorepo({ rootCopyHasSkill: true })

const result = scanIntentPackageAtRoot(nestedCopy, {
fallbackName: 'foo',
projectRoot: root,
})

expect(result.package?.version).toBe('2.0.0')
expect(result.package?.skills).toEqual([
expect.objectContaining({
name: 'core',
path: 'packages/a/node_modules/foo/skills/core/SKILL.md',
description: 'foo v2 core',
}),
])
})

it('keeps the stable root path for a root link that targets the same copy', () => {
const storeCopy = join(
root,
'node_modules',
'.pnpm',
'foo@2.0.0',
'node_modules',
'foo',
)
writeFoo(storeCopy, '2.0.0')
writeSkillMd(createDir(storeCopy, 'skills', 'core'), {
name: 'core',
description: 'foo core',
})
symlinkSync(storeCopy, join(root, 'node_modules', 'foo'), 'dir')

const result = scanIntentPackageAtRoot(storeCopy, {
fallbackName: 'foo',
projectRoot: root,
})

expect(result.package?.skills.map((skill) => skill.path)).toEqual([
'node_modules/foo/skills/core/SKILL.md',
])
})
})

describe('back-compat frontmatter reader (metadata.* fallback)', () => {
function writeRawSkillMd(dir: string, frontmatter: string): void {
mkdirSync(dir, { recursive: true })
Expand Down
47 changes: 46 additions & 1 deletion 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 @@ -98,6 +98,51 @@ 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', 'foo'), { recursive: true })
const packageRoot = join(root, 'packages', 'a', 'node_modules', 'foo')
const skillPath = join(packageRoot, 'skills', 'core', 'SKILL.md')
mkdirSync(join(packageRoot, 'skills', 'core'), { recursive: true })
const skills = [skill(skillPath)]

rewriteSkillLoadPaths({
packageName: 'foo',
packageRoot,
projectRoot: root,
skills,
})

expect(skills[0]!.path).toBe(
'packages/a/node_modules/foo/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',
'foo@1.0.0',
'node_modules',
'foo',
)
const skillPath = join(packageRoot, 'skills', 'core', 'SKILL.md')
mkdirSync(join(packageRoot, 'skills', 'core'), { recursive: true })
symlinkSync(packageRoot, join(root, 'node_modules', 'foo'), 'dir')
const skills = [skill(skillPath)]

rewriteSkillLoadPaths({
packageName: 'foo',
packageRoot,
projectRoot: root,
skills,
})

expect(skills[0]!.path).toBe('node_modules/foo/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