Skip to content
Merged
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/cuddly-games-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Adds package source-kind metadata to scanner results, tightens scan stats to match the runtime contract, and adds focused markdown destination rewrite coverage while preserving the current name-based allowlist behavior.
4 changes: 2 additions & 2 deletions packages/intent/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function listIntentSkills(
warningCount: result.warnings.length,
noticeCount: result.notices.length,
conflictCount: result.conflicts.length,
scan: scan.stats ?? fsCache.getStats(),
scan: scan.stats,
}
}

Expand Down Expand Up @@ -344,7 +344,7 @@ function resolveIntentSkillInCwd(
excludes: excludePatterns,
resolution: 'full-scan',
resolved,
scan: scanResult.stats ?? fsCache.getStats(),
scan: scanResult.stats,
scope,
})
: undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/intent/src/discovery/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface CreatePackageRegistrarOptions {
deriveIntentConfig: (pkgJson: PackageJson) => IntentConfig | null
discoverSkills: (skillsDir: string, packageName: string) => Array<SkillEntry>
getPackageDepth: (packageRoot: string, projectRoot: string) => number
getPackageKind: (packageRoot: string) => IntentPackage['kind']
packageIndexes: Map<string, number>
packages: Array<IntentPackage>
projectRoot: string
Expand Down Expand Up @@ -120,6 +121,7 @@ export function createPackageRegistrar(opts: CreatePackageRegistrarOptions) {
intent,
skills,
packageRoot: dirPath,
kind: opts.getPackageKind(dirPath),
source,
}
const existingIndex = opts.packageIndexes.get(name)
Expand Down
41 changes: 40 additions & 1 deletion packages/intent/src/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import {
} from './utils.js'
import { createIntentFsCache } from './fs-cache.js'
import { detectPackageManager } from './package-manager.js'
import { findWorkspaceRoot } from './workspace-patterns.js'
import {
findWorkspacePackages,
findWorkspaceRoot,
} from './workspace-patterns.js'
import type { IntentFsCache } from './fs-cache.js'
import type { ReadFs } from './utils.js'
import type {
Expand Down Expand Up @@ -451,6 +454,28 @@ function getScanScope(options: ScanOptions): ScanScope {
return options.scope ?? (options.includeGlobal ? 'local-and-global' : 'local')
}

function createWorkspacePackageKeySet(
workspaceRoot: string | null,
getFsIdentity: (path: string) => string,
): Set<string> {
if (!workspaceRoot) return new Set()

return new Set(
findWorkspacePackages(workspaceRoot).map((dir) => getFsIdentity(dir)),
)
}

function createPackageKindResolver(
workspacePackageKeys: Set<string>,
getFsIdentity: (path: string) => string,
): (packageRoot: string) => IntentPackage['kind'] {
return (packageRoot: string): IntentPackage['kind'] => {
return workspacePackageKeys.has(getFsIdentity(packageRoot))
? 'workspace'
: 'npm'
}
}

export function scanForIntents(
root?: string,
options: ScanOptions = {},
Expand Down Expand Up @@ -495,6 +520,11 @@ export function scanForIntents(
>()
let pnpApi: PnpApi | null | undefined

const getPackageKind = createPackageKindResolver(
createWorkspacePackageKeySet(workspaceRoot, fsCache.getFsIdentity),
fsCache.getFsIdentity,
)

function getPnpApi(): PnpApi | null {
if (scanScope === 'global') return null
if (pnpApi === undefined) {
Expand Down Expand Up @@ -545,6 +575,7 @@ export function scanForIntents(
deriveIntentConfig,
discoverSkills: (skillsDir) => discoverSkills(skillsDir, fsCache),
getPackageDepth,
getPackageKind,
getFsIdentity: fsCache.getFsIdentity,
exists: fsCache.exists,
packageIndexes,
Expand Down Expand Up @@ -726,6 +757,13 @@ export function scanIntentPackageAtRoot(
const warnings: Array<string> = []
const packageIndexes = new Map<string, number>()
const fsCache = options.fsCache ?? createIntentFsCache()
const getPackageKind = createPackageKindResolver(
createWorkspacePackageKeySet(
findWorkspaceRoot(projectRoot),
fsCache.getFsIdentity,
),
fsCache.getFsIdentity,
)

function readPkgJson(dirPath: string): Record<string, unknown> | null {
return fsCache.readPackageJson(dirPath)
Expand All @@ -744,6 +782,7 @@ export function scanIntentPackageAtRoot(
)
: (skillsDir) => discoverSkills(skillsDir, fsCache),
getPackageDepth,
getPackageKind,
getFsIdentity: fsCache.getFsIdentity,
exists: fsCache.exists,
packageIndexes,
Expand Down
3 changes: 2 additions & 1 deletion packages/intent/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface ScanResult {
local: NodeModulesScanTarget
global: NodeModulesScanTarget
}
stats?: ScanStats
stats: ScanStats
}

export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown'
Expand Down Expand Up @@ -54,6 +54,7 @@ export interface IntentPackage {
intent: IntentConfig
skills: Array<SkillEntry>
packageRoot: string
kind: 'npm' | 'workspace'
source: 'local' | 'global'
}

Expand Down
5 changes: 5 additions & 0 deletions packages/intent/tests/install-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function pkg(overrides: Partial<IntentPackage>): IntentPackage {
intent: { version: 1, repo: 'test/pkg', docs: 'docs/' },
skills: [],
packageRoot: 'node_modules/pkg',
kind: 'npm',
source: 'local',
...overrides,
}
Expand All @@ -73,6 +74,10 @@ function scanResult(packages: Array<IntentPackage>): ScanResult {
scanned: false,
},
},
stats: {
packageJsonCacheHits: 0,
packageJsonReadCount: 0,
},
}
}

Expand Down
40 changes: 40 additions & 0 deletions packages/intent/tests/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { rewriteLoadedSkillMarkdownDestinations } from '../src/core/markdown.js'

const cwd = '/repo'
const packageRoot = join(cwd, 'node_modules', 'pkg')
const skillFilePath = join(packageRoot, 'skills', 'core', 'SKILL.md')

function rewrite(content: string): string {
return rewriteLoadedSkillMarkdownDestinations({
content,
cwd,
packageRoot,
skillFilePath,
})
}

describe('rewriteLoadedSkillMarkdownDestinations', () => {
it('rewrites nested-label links while preserving query and hash suffixes', () => {
expect(rewrite('[API [v1]](docs/api.md?raw=1#setup)')).toBe(
'[API [v1]](node_modules/pkg/skills/core/docs/api.md?raw=1#setup)',
)
})

it('rewrites image destinations with escaped closing parens', () => {
expect(rewrite('![Diagram](assets/flow\\).png)')).toBe(
'![Diagram](node_modules/pkg/skills/core/assets/flow\\).png)',
)
})

it('preserves malformed inline links', () => {
expect(rewrite('[Broken](docs/api.md')).toBe('[Broken](docs/api.md')
})

it('does not rewrite links in fenced code blocks', () => {
expect(rewrite('~~~md\n[Keep](docs/api.md)\n~~~')).toBe(
'~~~md\n[Keep](docs/api.md)\n~~~',
)
})
})
5 changes: 5 additions & 0 deletions packages/intent/tests/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function intentPackage(
},
packageRoot: `node_modules/${overrides.name}`,
skills: [skill('core')],
kind: 'npm',
source: 'local',
version: '1.0.0',
...overrides,
Expand Down Expand Up @@ -64,6 +65,10 @@ function scanResult(
},
packageManager: 'npm',
packages,
stats: {
packageJsonCacheHits: 0,
packageJsonReadCount: 0,
},
warnings,
}
}
Expand Down
10 changes: 6 additions & 4 deletions packages/intent/tests/scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ describe('scanForIntents', () => {
const result = scanForIntents(root)
expect(result.packages).toHaveLength(1)
expect(result.packages[0]!.name).toBe('@tanstack/db')
expect(result.packages[0]!.kind).toBe('npm')
expect(result.packages[0]!.version).toBe('0.5.2')
expect(result.packages[0]!.packageRoot).toBe(pkgDir)
expect(result.packages[0]!.skills).toHaveLength(1)
Expand All @@ -128,7 +129,7 @@ describe('scanForIntents', () => {
packageJsonCacheHits: expect.any(Number),
}),
)
expect(result.stats!.packageJsonReadCount).toBeGreaterThan(0)
expect(result.stats.packageJsonReadCount).toBeGreaterThan(0)
})

it('does not throw when skills exists but is not a directory', () => {
Expand Down Expand Up @@ -1556,7 +1557,7 @@ describe('scanForIntents', () => {
'@tanstack/query',
'@tanstack/store',
])
expect(result.stats!.packageJsonReadCount).toBeLessThan(10)
expect(result.stats.packageJsonReadCount).toBeLessThan(10)
})

it('does not crawl package source trees during nested node_modules discovery', () => {
Expand Down Expand Up @@ -1591,7 +1592,7 @@ describe('scanForIntents', () => {
const result = scanForIntents(root)

expect(result.packages).toEqual([])
expect(result.stats!.packageJsonReadCount).toBeLessThan(4)
expect(result.stats.packageJsonReadCount).toBeLessThan(4)
})

it('dedupes recursive workspace symlink paths by real package identity', () => {
Expand Down Expand Up @@ -1639,7 +1640,8 @@ describe('scanForIntents', () => {

expect(result.packages).toHaveLength(1)
expect(result.packages[0]!.name).toBe('b')
expect(result.stats!.packageJsonReadCount).toBeLessThan(10)
expect(result.packages[0]!.kind).toBe('workspace')
expect(result.stats.packageJsonReadCount).toBeLessThan(10)
})

it('prefers valid semver versions over invalid ones at the same depth', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/intent/tests/source-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function pkg(name: string, skillNames: Array<string>): IntentPackage {
intent: { version: 1, repo: 'owner/repo', docs: '' },
skills: skillNames.map(skill),
packageRoot: `/root/node_modules/${name}`,
kind: 'npm',
source: 'local',
}
}
Expand Down
Loading