Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/neat-pots-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Make `scanForIntents` synchronous instead of returning a Promise for purely synchronous work. This aligns the exported API with its actual behavior and cleans up incorrect async usage in the package.
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const config = [
'unused-imports': unusedImports,
},
rules: {
'@typescript-eslint/array-type': 'off',
'import/order': 'warn',
'no-case-declarations': 'off',
'no-shadow': 'off',
'unused-imports/no-unused-imports': 'warn',
Expand Down
6 changes: 3 additions & 3 deletions packages/intent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ function getMetaDir(): string {
// Commands
// ---------------------------------------------------------------------------

async function cmdList(args: Array<string>): Promise<void> {
function cmdList(args: Array<string>): void {
const jsonOutput = args.includes('--json')

let result: ScanResult
try {
result = await scanForIntents()
result = scanForIntents()
} catch (err) {
console.error((err as Error).message)
process.exit(1)
Expand Down Expand Up @@ -450,7 +450,7 @@ const commandArgs = process.argv.slice(3)

switch (command) {
case 'list':
await cmdList(commandArgs)
cmdList(commandArgs)
break
case 'meta':
cmdMeta(commandArgs)
Expand Down
6 changes: 3 additions & 3 deletions packages/intent/src/intent-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import type { LibraryScanResult } from './library-scanner.js'
// Commands
// ---------------------------------------------------------------------------

async function cmdList(): Promise<void> {
function cmdList(): void {
let result: LibraryScanResult
try {
result = await scanLibrary(process.argv[1]!)
result = scanLibrary(process.argv[1]!)
} catch (err) {
console.error((err as Error).message)
process.exit(1)
Expand Down Expand Up @@ -136,7 +136,7 @@ const command = process.argv[2]
switch (command) {
case 'list':
case undefined:
await cmdList()
cmdList()
break
case 'install':
cmdInstall()
Expand Down
4 changes: 2 additions & 2 deletions packages/intent/src/library-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ function discoverSkills(skillsDir: string): Array<SkillEntry> {
// Main scanner
// ---------------------------------------------------------------------------

export async function scanLibrary(
export function scanLibrary(
scriptPath: string,
projectRoot?: string,
): Promise<LibraryScanResult> {
): LibraryScanResult {
const nodeModulesDir = join(projectRoot ?? process.cwd(), 'node_modules')
const packages: Array<LibraryPackage> = []
const warnings: Array<string> = []
Expand Down
4 changes: 2 additions & 2 deletions packages/intent/src/scanner.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { join, relative, sep } from 'node:path'
import { getDeps, parseFrontmatter, resolveDepDir } from './utils.js'
import type { Dirent } from 'node:fs'
import type {
IntentConfig,
IntentPackage,
ScanResult,
SkillEntry,
} from './types.js'
import type { Dirent } from 'node:fs'

// ---------------------------------------------------------------------------
// Package manager detection
Expand Down Expand Up @@ -190,7 +190,7 @@ function topoSort(packages: Array<IntentPackage>): Array<IntentPackage> {
// Main scanner
// ---------------------------------------------------------------------------

export async function scanForIntents(root?: string): Promise<ScanResult> {
export function scanForIntents(root?: string): ScanResult {
const projectRoot = root ?? process.cwd()
const packageManager = detectPackageManager(projectRoot)
const nodeModulesDir = join(projectRoot, 'node_modules')
Expand Down
22 changes: 6 additions & 16 deletions packages/intent/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ describe('intent meta', () => {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
expect(match, `${entry.name} should have frontmatter`).not.toBeNull()

const fm = parseYaml(match![1]!) as Record<string, unknown>
if (!match?.[1]) continue

const fm = parseYaml(match[1]) as Record<string, unknown>
expect(
fm.description,
`${entry.name} should have a description`,
Expand All @@ -51,20 +53,8 @@ describe('intent meta', () => {
// ── Validate command logic ──

describe('intent validate', () => {
it('finds SKILL.md files in meta directory', () => {
function findSkillFiles(dir: string): Array<string> {
const files: Array<string> = []
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...findSkillFiles(fullPath))
} else if (entry.name === 'SKILL.md') {
files.push(fullPath)
}
}
return files
}

it('finds SKILL.md files in meta directory', async () => {
const { findSkillFiles } = await import('../src/utils.js')
const files = findSkillFiles(metaDir)
expect(files.length).toBeGreaterThan(0)
})
Expand All @@ -80,7 +70,7 @@ describe('intent list --json shape', () => {
const { tmpdir } = await import('node:os')
const root = mkdtempSync(join(tmpdir(), 'cli-test-'))

const result = await scanForIntents(root)
const result = scanForIntents(root)
expect(result).toHaveProperty('packageManager')
expect(result).toHaveProperty('packages')
expect(result).toHaveProperty('warnings')
Expand Down
36 changes: 18 additions & 18 deletions packages/intent/tests/library-scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ afterEach(() => {
// ---------------------------------------------------------------------------

describe('scanLibrary', () => {
it('returns the home package with its skills', async () => {
it('returns the home package with its skills', () => {
const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router')
writeJson(join(pkgDir, 'package.json'), {
name: '@tanstack/router',
Expand All @@ -67,7 +67,7 @@ describe('scanLibrary', () => {
description: 'File-based route definitions',
})

const result = await scanLibrary(shimPath(pkgDir), root)
const result = scanLibrary(shimPath(pkgDir), root)

expect(result.warnings).toEqual([])
expect(result.packages).toHaveLength(1)
Expand All @@ -81,7 +81,7 @@ describe('scanLibrary', () => {
)
})

it('includes the full path to each SKILL.md', async () => {
it('includes the full path to each SKILL.md', () => {
const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router')
writeJson(join(pkgDir, 'package.json'), {
name: '@tanstack/router',
Expand All @@ -91,13 +91,13 @@ describe('scanLibrary', () => {
const skillDir = createDir(pkgDir, 'skills', 'routing')
writeSkillMd(skillDir, { name: 'routing', description: 'Routing patterns' })

const result = await scanLibrary(shimPath(pkgDir), root)
const result = scanLibrary(shimPath(pkgDir), root)

const skill = result.packages[0]!.skills[0]!
expect(skill.path).toBe(join(pkgDir, 'skills', 'routing', 'SKILL.md'))
})

it('recursively discovers deps with bin.intent', async () => {
it('recursively discovers deps with bin.intent', () => {
// Home package: @tanstack/router, depends on @tanstack/query
const routerDir = createDir(root, 'node_modules', '@tanstack', 'router')
writeJson(join(routerDir, 'package.json'), {
Expand Down Expand Up @@ -127,7 +127,7 @@ describe('scanLibrary', () => {
description: 'Query and mutation patterns',
})

const result = await scanLibrary(shimPath(routerDir), root)
const result = scanLibrary(shimPath(routerDir), root)

expect(result.warnings).toEqual([])
expect(result.packages).toHaveLength(2)
Expand All @@ -141,7 +141,7 @@ describe('scanLibrary', () => {
expect(query.skills[0]!.description).toBe('Query and mutation patterns')
})

it('discovers deps via peerDependencies', async () => {
it('discovers deps via peerDependencies', () => {
const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router')
writeJson(join(pkgDir, 'package.json'), {
name: '@tanstack/router',
Expand All @@ -159,13 +159,13 @@ describe('scanLibrary', () => {
const querySkill = createDir(queryDir, 'skills', 'fetching')
writeSkillMd(querySkill, { name: 'fetching', description: 'Fetching' })

const result = await scanLibrary(shimPath(pkgDir), root)
const result = scanLibrary(shimPath(pkgDir), root)

const names = result.packages.map((p) => p.name)
expect(names).toContain('@tanstack/query')
})

it('skips deps without bin.intent', async () => {
it('skips deps without bin.intent', () => {
const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router')
writeJson(join(pkgDir, 'package.json'), {
name: '@tanstack/router',
Expand All @@ -183,13 +183,13 @@ describe('scanLibrary', () => {
const reactSkill = createDir(reactDir, 'skills', 'hooks')
writeSkillMd(reactSkill, { name: 'hooks', description: 'React hooks' })

const result = await scanLibrary(shimPath(pkgDir), root)
const result = scanLibrary(shimPath(pkgDir), root)

const names = result.packages.map((p) => p.name)
expect(names).not.toContain('react')
})

it('handles packages with no skills/ directory', async () => {
it('handles packages with no skills/ directory', () => {
const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router')
writeJson(join(pkgDir, 'package.json'), {
name: '@tanstack/router',
Expand All @@ -198,13 +198,13 @@ describe('scanLibrary', () => {
})
// No skills/ directory

const result = await scanLibrary(shimPath(pkgDir), root)
const result = scanLibrary(shimPath(pkgDir), root)

expect(result.packages).toHaveLength(1)
expect(result.packages[0]!.skills).toEqual([])
})

it('does not visit the same package twice (cycle detection)', async () => {
it('does not visit the same package twice (cycle detection)', () => {
// router -> query -> router (circular)
const routerDir = createDir(root, 'node_modules', '@tanstack', 'router')
writeJson(join(routerDir, 'package.json'), {
Expand All @@ -222,15 +222,15 @@ describe('scanLibrary', () => {
dependencies: { '@tanstack/router': '^1.0.0' }, // circular back
})

const result = await scanLibrary(shimPath(routerDir), root)
const result = scanLibrary(shimPath(routerDir), root)

// Each package appears exactly once
const names = result.packages.map((p) => p.name)
expect(names).toHaveLength(2)
expect(new Set(names).size).toBe(2)
})

it('discovers sub-skills within a package', async () => {
it('discovers sub-skills within a package', () => {
const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router')
writeJson(join(pkgDir, 'package.json'), {
name: '@tanstack/router',
Expand All @@ -248,7 +248,7 @@ describe('scanLibrary', () => {
description: 'Nested route patterns',
})

const result = await scanLibrary(shimPath(pkgDir), root)
const result = scanLibrary(shimPath(pkgDir), root)

const skills = result.packages[0]!.skills
expect(skills).toHaveLength(2)
Expand All @@ -257,10 +257,10 @@ describe('scanLibrary', () => {
expect(names).toContain('routing/nested-routes')
})

it('returns a warning when home package.json cannot be found', async () => {
it('returns a warning when home package.json cannot be found', () => {
const fakeScript = join(root, 'nowhere', 'bin', 'intent.js')

const result = await scanLibrary(fakeScript, root)
const result = scanLibrary(fakeScript, root)

expect(result.packages).toEqual([])
expect(result.warnings).toHaveLength(1)
Expand Down
Loading
Loading