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/short-sheep-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Reduce repeated filesystem work during Intent CLI scans by sharing package.json/skill discovery caches across scan paths and de-duping package-root and node_modules scan attempts within a single scan. Debug output now includes package.json read/cache-hit counts.
2 changes: 2 additions & 0 deletions packages/intent/src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ function printListDebug(result: IntentSkillList): void {
['skills', result.debug.skillCount],
['warnings', result.debug.warningCount],
['conflicts', result.debug.conflictCount],
['packageJsonReadCount', result.debug.scan.packageJsonReadCount],
['packageJsonCacheHits', result.debug.scan.packageJsonCacheHits],
])
}

Expand Down
2 changes: 2 additions & 0 deletions packages/intent/src/commands/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ function printLoadDebug(loaded: LoadedIntentSkill | ResolvedIntentSkill): void {
['skill', loaded.debug.skillName],
['path', loaded.debug.path],
['warnings', loaded.debug.warningCount],
['packageJsonReadCount', loaded.debug.scan.packageJsonReadCount],
['packageJsonCacheHits', loaded.debug.scan.packageJsonCacheHits],
])
}

Expand Down
21 changes: 19 additions & 2 deletions packages/intent/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isPackageExcluded,
warningMentionsPackage,
} from './core/excludes.js'
import { createIntentFsCache, type IntentFsCache } from './fs-cache.js'
import { rewriteLoadedSkillMarkdownDestinations } from './core/markdown.js'
import { resolveSkillUseFastPath } from './core/load-resolution.js'
import { resolveProjectContext } from './core/project-context.js'
Expand Down Expand Up @@ -80,6 +81,13 @@ function getScanScope(options: ScanOptions): ScanScope {
return options.scope ?? (options.includeGlobal ? 'local-and-global' : 'local')
}

function withFsCache(
options: ScanOptions,
fsCache: IntentFsCache,
): ScanOptions & { fsCache: IntentFsCache } {
return { ...options, fsCache }
}

function resolveCoreCwd(options: IntentCoreOptions): string {
return resolve(process.cwd(), options.cwd ?? process.cwd())
}
Expand All @@ -89,8 +97,9 @@ export function listIntentSkills(
): IntentSkillList {
const cwd = resolveCoreCwd(options)
const scanOptions = toScanOptions(options)
const fsCache = createIntentFsCache()
const projectContext = resolveProjectContext({ cwd })
const scanResult = scanForIntents(cwd, scanOptions)
const scanResult = scanForIntents(cwd, withFsCache(scanOptions, fsCache))
const excludePatterns = getEffectiveExcludePatterns(options, projectContext)
const excludeMatchers = compileExcludePatterns(excludePatterns)
const excludedPackages = scanResult.packages
Expand Down Expand Up @@ -144,6 +153,7 @@ export function listIntentSkills(
skillCount: result.skills.length,
warningCount: result.warnings.length,
conflictCount: result.conflicts.length,
scan: scanResult.stats ?? fsCache.getStats(),
}
}

Expand Down Expand Up @@ -220,12 +230,14 @@ function toResolvedIntentSkill(
function createLoadedSkillDebug({
cwd,
excludes,
scan,
resolution,
resolved,
scope,
}: {
cwd: string
excludes: Array<string>
scan: LoadedIntentSkillDebug['scan']
resolution: LoadedIntentSkillDebug['resolution']
resolved: ResolveSkillResult
scope: ScanScope
Expand All @@ -241,6 +253,7 @@ function createLoadedSkillDebug({
source: resolved.source,
path: resolved.path,
warningCount: resolved.warnings.length,
scan,
}
}

Expand All @@ -263,6 +276,7 @@ function resolveIntentSkillInCwd(
)
}

const fsCache = createIntentFsCache()
const projectContext = resolveProjectContext({ cwd })
const excludePatterns = getEffectiveExcludePatterns(options, projectContext)
const excludeMatchers = compileExcludePatterns(excludePatterns)
Expand All @@ -281,6 +295,7 @@ function resolveIntentSkillInCwd(
options,
projectContext,
cwd,
fsCache,
)
if (fastPathResolved) {
return toResolvedIntentSkill(
Expand All @@ -293,13 +308,14 @@ function resolveIntentSkillInCwd(
excludes: excludePatterns,
resolution: 'fast-path',
resolved: fastPathResolved,
scan: fsCache.getStats(),
scope,
})
: undefined,
)
}

const scanResult = scanForIntents(cwd, scanOptions)
const scanResult = scanForIntents(cwd, withFsCache(scanOptions, fsCache))
let resolved: ReturnType<typeof resolveSkillUse>
try {
resolved = resolveSkillUse(use, scanResult)
Expand All @@ -322,6 +338,7 @@ function resolveIntentSkillInCwd(
excludes: excludePatterns,
resolution: 'full-scan',
resolved,
scan: scanResult.stats ?? fsCache.getStats(),
scope,
})
: undefined,
Expand Down
27 changes: 18 additions & 9 deletions packages/intent/src/core/load-resolution.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { existsSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { createIntentFsCache, type IntentFsCache } from '../fs-cache.js'
import { resolveSkillEntry, type ResolveSkillResult } from '../resolver.js'
import { scanIntentPackageAtRoot } from '../scanner.js'
import { resolveWorkspacePackages } from '../workspace-patterns.js'
import { findWorkspacePackages } from '../workspace-patterns.js'
import { getDeps, resolveDepDir } from '../utils.js'
import { warningMentionsPackage } from './excludes.js'
import { readPackageJson } from './package-json.js'
import {
resolveProjectContext,
type ProjectContext,
Expand All @@ -21,6 +21,7 @@ interface WorkspacePackageInfo {

function readWorkspacePackageInfos(
context: ProjectContext,
fsCache: IntentFsCache,
): Array<WorkspacePackageInfo> {
const dirs = new Set<string>()

Expand All @@ -31,16 +32,13 @@ function readWorkspacePackageInfos(
if (context.workspaceRoot) {
dirs.add(context.workspaceRoot)

for (const dir of resolveWorkspacePackages(
context.workspaceRoot,
context.workspacePatterns,
)) {
for (const dir of findWorkspacePackages(context.workspaceRoot)) {
dirs.add(dir)
}
}

return [...dirs].flatMap((dir) => {
const packageJson = readPackageJson(dir)
const packageJson = fsCache.readPackageJson(dir)
if (!packageJson) return []

return [
Expand Down Expand Up @@ -154,10 +152,11 @@ function getDirectLoadFastPathCandidateDirs(
function getWorkspaceLoadFastPathCandidateDirs(
packageName: string,
context: ProjectContext,
fsCache: IntentFsCache,
): Array<string> {
const candidates: Array<string> = []
const seen = new Set<string>()
const workspacePackages = readWorkspacePackageInfos(context)
const workspacePackages = readWorkspacePackageInfos(context, fsCache)

for (const pkg of workspacePackages) {
if (pkg.name === packageName) {
Expand Down Expand Up @@ -212,10 +211,12 @@ function resolveFromPackageRoots(
packageRoots: Array<string>,
parsedUse: SkillUse,
cwd: string,
fsCache: IntentFsCache,
): ResolveSkillResult | null {
for (const packageRoot of packageRoots) {
const scanned = scanIntentPackageAtRoot(packageRoot, {
fallbackName: parsedUse.packageName,
fsCache,
projectRoot: cwd,
skillNameHint: parsedUse.skillName,
})
Expand All @@ -225,6 +226,7 @@ function resolveFromPackageRoots(
if (scanned.package?.name === parsedUse.packageName) {
const fallbackScanned = scanIntentPackageAtRoot(packageRoot, {
fallbackName: parsedUse.packageName,
fsCache,
projectRoot: cwd,
})
const fallbackResolved = resolveScannedPackageSkill(
Expand All @@ -243,6 +245,7 @@ export function resolveSkillUseFastPath(
options: IntentCoreOptions,
context = resolveProjectContext({ cwd: process.cwd() }),
cwd = context.cwd,
fsCache = createIntentFsCache(),
): ResolveSkillResult | null {
if (options.globalOnly) return null
if (shouldSkipFastPathForYarnPnp(context, cwd)) return null
Expand All @@ -251,6 +254,7 @@ export function resolveSkillUseFastPath(
getDirectLoadFastPathCandidateDirs(parsedUse.packageName, context, cwd),
parsedUse,
cwd,
fsCache,
)
if (directResolved) return directResolved

Expand All @@ -259,8 +263,13 @@ export function resolveSkillUseFastPath(
}

return resolveFromPackageRoots(
getWorkspaceLoadFastPathCandidateDirs(parsedUse.packageName, context),
getWorkspaceLoadFastPathCandidateDirs(
parsedUse.packageName,
context,
fsCache,
),
parsedUse,
cwd,
fsCache,
)
}
11 changes: 10 additions & 1 deletion packages/intent/src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { IntentPackage, ScanScope, VersionConflict } from '../types.js'
import type {
IntentPackage,
ScanScope,
ScanStats,
VersionConflict,
} from '../types.js'

export interface IntentCoreOptions {
cwd?: string
Expand Down Expand Up @@ -60,6 +65,7 @@ export interface IntentSkillListDebug {
skillCount: number
warningCount: number
conflictCount: number
scan: IntentScanDebugStats
}

export interface LoadedIntentSkillDebug {
Expand All @@ -73,8 +79,11 @@ export interface LoadedIntentSkillDebug {
source: IntentPackage['source']
path: string
warningCount: number
scan: IntentScanDebugStats
}

export interface IntentScanDebugStats extends ScanStats {}

export type IntentCoreErrorCode =
| 'invalid-options'
| 'invalid-skill-use'
Expand Down
40 changes: 34 additions & 6 deletions packages/intent/src/discovery/register.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync } from 'node:fs'
import { join, sep } from 'node:path'
import { join, resolve, sep } from 'node:path'
import { rewriteSkillLoadPaths } from '../skill-paths.js'
import { listNodeModulesPackageDirs } from '../utils.js'
import type {
Expand All @@ -18,6 +18,10 @@ function isLocalToProject(dirPath: string, projectRoot: string): boolean {
)
}

function getFsIdentity(path: string): string {
return resolve(path)
}

export interface CreatePackageRegistrarOptions {
comparePackageVersions: (a: string, b: string) => number
deriveIntentConfig: (pkgJson: PackageJson) => IntentConfig | null
Expand All @@ -33,23 +37,47 @@ export interface CreatePackageRegistrarOptions {
}

export function createPackageRegistrar(opts: CreatePackageRegistrarOptions) {
const attemptedPackageRoots = new Set<string>()
const scannedNodeModulesDirs = new Set<string>()

function shouldAttemptPackageRoot(dirPath: string): boolean {
const key = getFsIdentity(dirPath)
if (attemptedPackageRoots.has(key)) return false
attemptedPackageRoots.add(key)
return true
}

function scanNodeModulesDir(
nodeModulesDir: string,
source: IntentPackage['source'] = 'local',
): void {
if (!existsSync(nodeModulesDir)) return

const key = getFsIdentity(nodeModulesDir)
if (scannedNodeModulesDirs.has(key)) return
scannedNodeModulesDirs.add(key)

for (const dirPath of listNodeModulesPackageDirs(nodeModulesDir)) {
tryRegister(dirPath, 'unknown', source)
}
}

function scanTarget(
target: NodeModulesScanTarget,
source: IntentPackage['source'] = 'local',
): void {
if (!target.path || !target.exists || target.scanned) return
target.scanned = true

for (const dirPath of listNodeModulesPackageDirs(target.path)) {
tryRegister(dirPath, 'unknown', source)
}
scanNodeModulesDir(target.path, source)
}

function tryRegister(
dirPath: string,
fallbackName: string,
source: IntentPackage['source'] = 'local',
): boolean {
if (!shouldAttemptPackageRoot(dirPath)) return false

const skillsDir = join(dirPath, 'skills')
if (!existsSync(skillsDir)) return false

Expand Down Expand Up @@ -126,5 +154,5 @@ export function createPackageRegistrar(opts: CreatePackageRegistrarOptions) {
return true
}

return { scanTarget, tryRegister }
return { scanNodeModulesDir, scanTarget, tryRegister }
}
Loading
Loading