diff --git a/.changeset/neat-pots-melt.md b/.changeset/neat-pots-melt.md new file mode 100644 index 0000000..20b683d --- /dev/null +++ b/.changeset/neat-pots-melt.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Make `scanForIntents` and `scanLibrary` synchronous instead of returning Promises for purely synchronous work. Clean up unnecessary async/await throughout source and tests, extract DRY test helpers, and improve type narrowing. diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 1d8efe8..0c74230 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -35,7 +35,7 @@ async function scanIntentsOrFail(): Promise { const { scanForIntents } = await import('./scanner.js') try { - return await scanForIntents() + return scanForIntents() } catch (err) { fail((err as Error).message) } diff --git a/packages/intent/src/intent-library.ts b/packages/intent/src/intent-library.ts index 0fddfc6..c468674 100644 --- a/packages/intent/src/intent-library.ts +++ b/packages/intent/src/intent-library.ts @@ -9,10 +9,10 @@ import type { LibraryScanResult } from './library-scanner.js' // Commands // --------------------------------------------------------------------------- -async function cmdList(): Promise { +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) @@ -90,7 +90,7 @@ const command = process.argv[2] switch (command) { case 'list': case undefined: - await cmdList() + cmdList() break case 'install': cmdInstall() diff --git a/packages/intent/src/library-scanner.ts b/packages/intent/src/library-scanner.ts index 8990b50..44e3e81 100644 --- a/packages/intent/src/library-scanner.ts +++ b/packages/intent/src/library-scanner.ts @@ -101,10 +101,10 @@ function discoverSkills(skillsDir: string): Array { // Main scanner // --------------------------------------------------------------------------- -export async function scanLibrary( +export function scanLibrary( scriptPath: string, _projectRoot?: string, -): Promise { +): LibraryScanResult { const packages: Array = [] const warnings: Array = [] const visited = new Set() diff --git a/packages/intent/src/scanner.ts b/packages/intent/src/scanner.ts index d1b0a12..f61e129 100644 --- a/packages/intent/src/scanner.ts +++ b/packages/intent/src/scanner.ts @@ -318,7 +318,7 @@ function toVersionConflict( // Main scanner // --------------------------------------------------------------------------- -export async function scanForIntents(root?: string): Promise { +export function scanForIntents(root?: string): ScanResult { const projectRoot = root ?? process.cwd() const packageManager = detectPackageManager(projectRoot) const nodeModulesDir = join(projectRoot, 'node_modules') diff --git a/packages/intent/tests/library-scanner.test.ts b/packages/intent/tests/library-scanner.test.ts index 29d0e1f..e7eaf26 100644 --- a/packages/intent/tests/library-scanner.test.ts +++ b/packages/intent/tests/library-scanner.test.ts @@ -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', @@ -67,7 +67,7 @@ describe('scanLibrary', () => { description: 'File-based route definitions', }) - const result = await scanLibrary(scriptPath(pkgDir), root) + const result = scanLibrary(scriptPath(pkgDir), root) expect(result.warnings).toEqual([]) expect(result.packages).toHaveLength(1) @@ -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', @@ -91,13 +91,13 @@ describe('scanLibrary', () => { const skillDir = createDir(pkgDir, 'skills', 'routing') writeSkillMd(skillDir, { name: 'routing', description: 'Routing patterns' }) - const result = await scanLibrary(scriptPath(pkgDir), root) + const result = scanLibrary(scriptPath(pkgDir), root) const skill = result.packages[0]!.skills[0]! expect(skill.path).toBe(join(pkgDir, 'skills', 'routing', 'SKILL.md')) }) - it('recursively discovers deps with tanstack-intent keyword', async () => { + it('recursively discovers deps with tanstack-intent keyword', () => { // Home package: @tanstack/router, depends on @tanstack/query const routerDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(routerDir, 'package.json'), { @@ -127,7 +127,7 @@ describe('scanLibrary', () => { description: 'Query and mutation patterns', }) - const result = await scanLibrary(scriptPath(routerDir), root) + const result = scanLibrary(scriptPath(routerDir), root) expect(result.warnings).toEqual([]) expect(result.packages).toHaveLength(2) @@ -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', @@ -159,13 +159,13 @@ describe('scanLibrary', () => { const querySkill = createDir(queryDir, 'skills', 'fetching') writeSkillMd(querySkill, { name: 'fetching', description: 'Fetching' }) - const result = await scanLibrary(scriptPath(pkgDir), root) + const result = scanLibrary(scriptPath(pkgDir), root) const names = result.packages.map((p) => p.name) expect(names).toContain('@tanstack/query') }) - it('skips deps without tanstack-intent keyword or bin.intent', async () => { + it('skips deps without tanstack-intent keyword or bin.intent', () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', @@ -183,13 +183,13 @@ describe('scanLibrary', () => { const reactSkill = createDir(reactDir, 'skills', 'hooks') writeSkillMd(reactSkill, { name: 'hooks', description: 'React hooks' }) - const result = await scanLibrary(scriptPath(pkgDir), root) + const result = scanLibrary(scriptPath(pkgDir), root) const names = result.packages.map((p) => p.name) expect(names).not.toContain('react') }) - it('follows deps with legacy bin.intent (backwards compat)', async () => { + it('follows deps with legacy bin.intent (backwards compat)', () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', @@ -210,13 +210,13 @@ describe('scanLibrary', () => { const querySkill = createDir(queryDir, 'skills', 'fetching') writeSkillMd(querySkill, { name: 'fetching', description: 'Fetching' }) - const result = await scanLibrary(scriptPath(pkgDir), root) + const result = scanLibrary(scriptPath(pkgDir), root) const names = result.packages.map((p) => p.name) expect(names).toContain('@tanstack/query') }) - it('skips deps with other keywords but not tanstack-intent', async () => { + it('skips deps with other keywords but not tanstack-intent', () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', @@ -234,13 +234,13 @@ describe('scanLibrary', () => { const libSkill = createDir(libDir, 'skills', 'core') writeSkillMd(libSkill, { name: 'core', description: 'Core patterns' }) - const result = await scanLibrary(scriptPath(pkgDir), root) + const result = scanLibrary(scriptPath(pkgDir), root) const names = result.packages.map((p) => p.name) expect(names).not.toContain('some-lib') }) - 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', @@ -249,13 +249,13 @@ describe('scanLibrary', () => { }) // No skills/ directory - const result = await scanLibrary(scriptPath(pkgDir), root) + const result = scanLibrary(scriptPath(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'), { @@ -273,7 +273,7 @@ describe('scanLibrary', () => { dependencies: { '@tanstack/router': '^1.0.0' }, // circular back }) - const result = await scanLibrary(scriptPath(routerDir), root) + const result = scanLibrary(scriptPath(routerDir), root) // Each package appears exactly once const names = result.packages.map((p) => p.name) @@ -281,7 +281,7 @@ describe('scanLibrary', () => { 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', @@ -299,7 +299,7 @@ describe('scanLibrary', () => { description: 'Nested route patterns', }) - const result = await scanLibrary(scriptPath(pkgDir), root) + const result = scanLibrary(scriptPath(pkgDir), root) const skills = result.packages[0]!.skills expect(skills).toHaveLength(2) @@ -308,10 +308,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) diff --git a/packages/intent/tests/scanner.test.ts b/packages/intent/tests/scanner.test.ts index 1edf70a..50ceeb2 100644 --- a/packages/intent/tests/scanner.test.ts +++ b/packages/intent/tests/scanner.test.ts @@ -59,24 +59,24 @@ afterEach(() => { // ── Tests ── describe('scanForIntents', () => { - it('returns empty packages when no node_modules exists', async () => { - const result = await scanForIntents(root) + it('returns empty packages when no node_modules exists', () => { + const result = scanForIntents(root) expect(result.packages).toEqual([]) expect(result.warnings).toEqual([]) expect(result.nodeModules.local.exists).toBe(false) }) - it('returns empty packages when node_modules has no intent packages', async () => { + it('returns empty packages when node_modules has no intent packages', () => { createDir(root, 'node_modules', 'some-lib') writeJson(join(root, 'node_modules', 'some-lib', 'package.json'), { name: 'some-lib', version: '1.0.0', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toEqual([]) }) - it('discovers an intent-enabled package with skills', async () => { + it('discovers an intent-enabled package with skills', () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'db') writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/db', @@ -94,7 +94,7 @@ describe('scanForIntents', () => { type: 'core', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('@tanstack/db') expect(result.packages[0]!.version).toBe('0.5.2') @@ -106,7 +106,7 @@ describe('scanForIntents', () => { ) }) - it('discovers packages through symlinks (pnpm layout)', async () => { + it('discovers packages through symlinks (pnpm layout)', () => { // pnpm stores packages outside node_modules and symlinks them in const store = createDir(root, '.pnpm-store', '@tanstack', 'db') writeJson(join(store, 'package.json'), { @@ -125,13 +125,13 @@ describe('scanForIntents', () => { createDir(root, 'node_modules', '@tanstack') symlinkSync(store, join(root, 'node_modules', '@tanstack', 'db')) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('@tanstack/db') expect(result.packages[0]!.skills).toHaveLength(1) }) - it('discovers unscoped packages through symlinks (pnpm layout)', async () => { + it('discovers unscoped packages through symlinks (pnpm layout)', () => { const store = createDir(root, '.pnpm-store', 'my-lib') writeJson(join(store, 'package.json'), { name: 'my-lib', @@ -144,12 +144,12 @@ describe('scanForIntents', () => { createDir(root, 'node_modules') symlinkSync(store, join(root, 'node_modules', 'my-lib')) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('my-lib') }) - it('discovers sub-skills', async () => { + it('discovers sub-skills', () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'db') writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/db', @@ -164,14 +164,14 @@ describe('scanForIntents', () => { description: 'Queries', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages[0]!.skills).toHaveLength(2) const names = result.packages[0]!.skills.map((s) => s.name) expect(names).toContain('db-core') expect(names).toContain('db-core/live-queries') }) - it('warns on skills/ dir without valid intent field', async () => { + it('warns on skills/ dir without valid intent field', () => { const pkgDir = createDir(root, 'node_modules', 'bad-pkg') writeJson(join(pkgDir, 'package.json'), { name: 'bad-pkg', @@ -180,13 +180,13 @@ describe('scanForIntents', () => { }) createDir(pkgDir, 'skills', 'some-skill') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(0) expect(result.warnings).toHaveLength(1) expect(result.warnings[0]).toContain('bad-pkg') }) - it('warns on invalid intent version', async () => { + it('warns on invalid intent version', () => { const pkgDir = createDir(root, 'node_modules', 'wrong-ver') writeJson(join(pkgDir, 'package.json'), { name: 'wrong-ver', @@ -195,12 +195,12 @@ describe('scanForIntents', () => { }) createDir(pkgDir, 'skills', 'some-skill') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(0) expect(result.warnings).toHaveLength(1) }) - it('sorts packages by dependency order (requires)', async () => { + it('sorts packages by dependency order (requires)', () => { // Create core package (no requires) const coreDir = createDir(root, 'node_modules', '@tanstack', 'db') writeJson(join(coreDir, 'package.json'), { @@ -229,14 +229,14 @@ describe('scanForIntents', () => { description: 'React bindings', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(2) // Core should come first expect(result.packages[0]!.name).toBe('@tanstack/db') expect(result.packages[1]!.name).toBe('@tanstack/react-db') }) - it('skips packages without skills/ directory', async () => { + it('skips packages without skills/ directory', () => { const pkgDir = createDir(root, 'node_modules', 'no-skills') writeJson(join(pkgDir, 'package.json'), { name: 'no-skills', @@ -245,12 +245,12 @@ describe('scanForIntents', () => { }) // No skills/ directory - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(0) expect(result.warnings).toHaveLength(0) }) - it('discovers global-only intent packages', async () => { + it('discovers global-only intent packages', () => { process.env.INTENT_GLOBAL_NODE_MODULES = globalRoot const pkgDir = createDir(globalRoot, '@tanstack', 'query') @@ -264,7 +264,7 @@ describe('scanForIntents', () => { description: 'Global fetching skill', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.nodeModules.global.detected).toBe(true) expect(result.nodeModules.global.exists).toBe(true) @@ -273,7 +273,7 @@ describe('scanForIntents', () => { expect(result.packages[0]!.name).toBe('@tanstack/query') }) - it('prefers local packages over global packages with the same name', async () => { + it('prefers local packages over global packages with the same name', () => { process.env.INTENT_GLOBAL_NODE_MODULES = globalRoot const localPkgDir = createDir(root, 'node_modules', '@tanstack', 'query') @@ -298,7 +298,7 @@ describe('scanForIntents', () => { description: 'Global fetching skill', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.nodeModules.global.detected).toBe(true) expect(result.nodeModules.global.scanned).toBe(true) @@ -316,7 +316,7 @@ describe('scanForIntents', () => { ).toBe(true) }) - it('chooses the highest version when duplicate package names exist at the same depth', async () => { + it('chooses the highest version when duplicate package names exist at the same depth', () => { writeJson(join(root, 'package.json'), { name: 'app', private: true, @@ -402,7 +402,7 @@ describe('scanForIntents', () => { description: 'Query v3 skill', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) const versionWarning = result.warnings.find((warning) => warning.includes('@tanstack/query'), ) @@ -418,7 +418,7 @@ describe('scanForIntents', () => { expect(versionWarning).toContain('Using 5.0.0') }) - it('prefers stable releases over prereleases at the same depth', async () => { + it('prefers stable releases over prereleases at the same depth', () => { writeJson(join(root, 'package.json'), { name: 'app', private: true, @@ -474,14 +474,14 @@ describe('scanForIntents', () => { description: 'Stable query skill', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.version).toBe('5.0.0') expect(result.packages[0]!.packageRoot).toBe(stableDir) }) - it('finds hoisted deps when scanning from a workspace package subdir', async () => { + it('finds hoisted deps when scanning from a workspace package subdir', () => { // Simulate npm/yarn/bun monorepo: deps hoisted to root node_modules writeJson(join(root, 'package.json'), { name: 'monorepo', @@ -516,12 +516,12 @@ describe('scanForIntents', () => { }) // Scan from the workspace package subdir (not root) - const result = await scanForIntents(appDir) + const result = scanForIntents(appDir) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('@tanstack/db') }) - it('discovers skills in workspace package dependencies from monorepo root', async () => { + it('discovers skills in workspace package dependencies from monorepo root', () => { writeFileSync( join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n', @@ -558,13 +558,13 @@ describe('scanForIntents', () => { createDir(root, 'node_modules') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('@tanstack/db') expect(result.packages[0]!.skills).toHaveLength(1) }) - it('discovers transitive skills through workspace package deps', async () => { + it('discovers transitive skills through workspace package deps', () => { writeFileSync( join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n', @@ -605,12 +605,12 @@ describe('scanForIntents', () => { createDir(root, 'node_modules') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('skills-pkg') }) - it('discovers skills using package.json workspaces', async () => { + it('discovers skills using package.json workspaces', () => { writeJson(join(root, 'package.json'), { name: 'monorepo', private: true, @@ -642,12 +642,12 @@ describe('scanForIntents', () => { description: 'Core database concepts', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('@tanstack/db') }) - it('prefers valid semver versions over invalid ones at the same depth', async () => { + it('prefers valid semver versions over invalid ones at the same depth', () => { writeJson(join(root, 'package.json'), { name: 'app', private: true, @@ -703,7 +703,7 @@ describe('scanForIntents', () => { description: 'Valid version query skill', }) - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.version).toBe('5.0.0') @@ -712,50 +712,47 @@ describe('scanForIntents', () => { }) describe('package manager detection', () => { - it('detects npm from package-lock.json', async () => { + it('detects npm from package-lock.json', () => { writeFileSync(join(root, 'package-lock.json'), '{}') createDir(root, 'node_modules') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packageManager).toBe('npm') }) - it('detects pnpm from pnpm-lock.yaml', async () => { + it('detects pnpm from pnpm-lock.yaml', () => { writeFileSync(join(root, 'pnpm-lock.yaml'), '') createDir(root, 'node_modules') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packageManager).toBe('pnpm') }) - it('detects yarn from yarn.lock', async () => { + it('detects yarn from yarn.lock', () => { writeFileSync(join(root, 'yarn.lock'), '') createDir(root, 'node_modules') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packageManager).toBe('yarn') }) - it('detects bun from bun.lockb', async () => { + it('detects bun from bun.lockb', () => { writeFileSync(join(root, 'bun.lockb'), '') createDir(root, 'node_modules') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packageManager).toBe('bun') }) - it('returns unknown when no lockfile found', async () => { + it('returns unknown when no lockfile found', () => { createDir(root, 'node_modules') - const result = await scanForIntents(root) + const result = scanForIntents(root) expect(result.packageManager).toBe('unknown') }) - it('throws for Yarn PnP', async () => { + it('throws for Yarn PnP', () => { writeFileSync(join(root, '.pnp.cjs'), '') - await expect(scanForIntents(root)).rejects.toThrow('Yarn PnP') + expect(() => scanForIntents(root)).toThrow('Yarn PnP') }) - it('throws for Deno without node_modules', async () => { + it('throws for Deno without node_modules', () => { writeFileSync(join(root, 'deno.json'), '{}') - // No node_modules dir - await expect(scanForIntents(root)).rejects.toThrow( - 'Deno without node_modules', - ) + expect(() => scanForIntents(root)).toThrow('Deno without node_modules') }) }) diff --git a/packages/intent/tests/skills.test.ts b/packages/intent/tests/skills.test.ts index 7a69040..36bfbbc 100644 --- a/packages/intent/tests/skills.test.ts +++ b/packages/intent/tests/skills.test.ts @@ -1,7 +1,8 @@ -import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs' +import { readFileSync } from 'node:fs' import { join, relative, sep } from 'node:path' import { describe, expect, it } from 'vitest' import { parse as parseYaml } from 'yaml' +import { findSkillFiles } from '../src/utils.js' // ── Types ── @@ -21,31 +22,15 @@ interface SkillFrontmatter { const META_DIR = join(__dirname, '..', 'meta') const MAX_META_SKILL_LINES = 1000 -function findSkillFiles(dir: string): Array { - const files: Array = [] - if (!existsSync(dir)) return files - - for (const entry of readdirSync(dir)) { - const fullPath = join(dir, entry) - const stat = statSync(fullPath) - if (stat.isDirectory()) { - files.push(...findSkillFiles(fullPath)) - } else if (entry === 'SKILL.md') { - files.push(fullPath) - } - } - return files -} - function extractFrontmatter( content: string, ): { frontmatter: SkillFrontmatter; body: string } | null { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/) - if (!match) return null + if (!match || !match[1] || match[2] === undefined) return null try { - const frontmatter = parseYaml(match[1]!) as SkillFrontmatter - return { frontmatter, body: match[2]! } + const frontmatter = parseYaml(match[1]) as SkillFrontmatter + return { frontmatter, body: match[2] } } catch { return null } diff --git a/packages/intent/tests/staleness.test.ts b/packages/intent/tests/staleness.test.ts index d6c2333..03d265a 100644 --- a/packages/intent/tests/staleness.test.ts +++ b/packages/intent/tests/staleness.test.ts @@ -1,6 +1,6 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' import { tmpdir } from 'node:os' +import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { checkStaleness } from '../src/staleness.js' @@ -46,12 +46,30 @@ function writeSyncState(dir: string, state: Record): void { writeFileSync(join(skillsDir, 'sync-state.json'), JSON.stringify(state)) } +function requireFirstSkill(report: Awaited>) { + const skill = report.skills[0] + expect(skill).toBeDefined() + if (!skill) throw new Error('Expected at least one skill in staleness report') + return skill +} + // --------------------------------------------------------------------------- // Mock fetch for npm registry // --------------------------------------------------------------------------- const originalFetch = globalThis.fetch +function mockFetchVersion(version: string): void { + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ version }), + } as Response) +} + +function mockFetchNotOk(): void { + globalThis.fetch = vi.fn().mockResolvedValue({ ok: false } as Response) +} + beforeEach(() => { tmpDir = setupDir() }) @@ -92,7 +110,7 @@ describe('checkStaleness', () => { description: 'Advanced usage', }) - globalThis.fetch = vi.fn().mockResolvedValue({ ok: false } as Response) + mockFetchNotOk() const report = await checkStaleness(tmpDir, '@example/lib') expect(report.skills).toHaveLength(2) @@ -109,17 +127,15 @@ describe('checkStaleness', () => { library_version: '1.2.3', }) - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ version: '2.0.0' }), - } as Response) + mockFetchVersion('2.0.0') const report = await checkStaleness(tmpDir, '@example/lib') expect(report.skillVersion).toBe('1.2.3') expect(report.currentVersion).toBe('2.0.0') expect(report.versionDrift).toBe('major') - expect(report.skills[0]!.needsReview).toBe(true) - expect(report.skills[0]!.reasons[0]).toContain('version drift') + const skill = requireFirstSkill(report) + expect(skill.needsReview).toBe(true) + expect(skill.reasons[0]).toContain('version drift') }) it('detects minor version drift', async () => { @@ -129,10 +145,7 @@ describe('checkStaleness', () => { library_version: '1.2.3', }) - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ version: '1.4.0' }), - } as Response) + mockFetchVersion('1.4.0') const report = await checkStaleness(tmpDir, '@example/lib') expect(report.versionDrift).toBe('minor') @@ -145,10 +158,7 @@ describe('checkStaleness', () => { library_version: '1.2.3', }) - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ version: '1.2.5' }), - } as Response) + mockFetchVersion('1.2.5') const report = await checkStaleness(tmpDir, '@example/lib') expect(report.versionDrift).toBe('patch') @@ -161,14 +171,11 @@ describe('checkStaleness', () => { library_version: '1.2.3', }) - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ version: '1.2.3' }), - } as Response) + mockFetchVersion('1.2.3') const report = await checkStaleness(tmpDir, '@example/lib') expect(report.versionDrift).toBeNull() - expect(report.skills[0]!.needsReview).toBe(false) + expect(requireFirstSkill(report).needsReview).toBe(false) }) it('handles npm fetch failure gracefully', async () => { @@ -203,11 +210,12 @@ describe('checkStaleness', () => { }, }) - globalThis.fetch = vi.fn().mockResolvedValue({ ok: false } as Response) + mockFetchNotOk() const report = await checkStaleness(tmpDir, '@example/lib') - expect(report.skills[0]!.needsReview).toBe(true) - expect(report.skills[0]!.reasons).toEqual( + const skill = requireFirstSkill(report) + expect(skill.needsReview).toBe(true) + expect(skill.reasons).toEqual( expect.arrayContaining([expect.stringContaining('new source')]), ) }) @@ -219,10 +227,10 @@ describe('checkStaleness', () => { sources: ['docs/api.md'], }) - globalThis.fetch = vi.fn().mockResolvedValue({ ok: false } as Response) + mockFetchNotOk() const report = await checkStaleness(tmpDir, '@example/lib') - expect(report.skills[0]!.needsReview).toBe(false) + expect(requireFirstSkill(report).needsReview).toBe(false) }) it('ignores malformed sync-state entries instead of flagging false positives', async () => { @@ -240,11 +248,12 @@ describe('checkStaleness', () => { }, }) - globalThis.fetch = vi.fn().mockResolvedValue({ ok: false } as Response) + mockFetchNotOk() const report = await checkStaleness(tmpDir, '@example/lib') - expect(report.skills[0]!.needsReview).toBe(false) - expect(report.skills[0]!.reasons).toEqual([]) + const skill = requireFirstSkill(report) + expect(skill.needsReview).toBe(false) + expect(skill.reasons).toEqual([]) }) it('handles nested skill directories', async () => { @@ -254,15 +263,13 @@ describe('checkStaleness', () => { library_version: '1.0.0', }) - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ version: '2.0.0' }), - } as Response) + mockFetchVersion('2.0.0') const report = await checkStaleness(tmpDir, '@example/lib') expect(report.skills).toHaveLength(1) - expect(report.skills[0]!.name).toBe('react/hooks') - expect(report.skills[0]!.needsReview).toBe(true) + const skill = requireFirstSkill(report) + expect(skill.name).toBe('react/hooks') + expect(skill.needsReview).toBe(true) }) it('uses directory name when frontmatter has no name', async () => { @@ -270,10 +277,10 @@ describe('checkStaleness', () => { description: 'A skill with no name field', }) - globalThis.fetch = vi.fn().mockResolvedValue({ ok: false } as Response) + mockFetchNotOk() const report = await checkStaleness(tmpDir, '@example/lib') - expect(report.skills[0]!.name).toBe('my-skill') + expect(requireFirstSkill(report).name).toBe('my-skill') }) it('uses skillVersion from first skill that has library_version', async () => { @@ -284,10 +291,7 @@ describe('checkStaleness', () => { library_version: '3.5.0', }) - globalThis.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ version: '4.0.0' }), - } as Response) + mockFetchVersion('4.0.0') const report = await checkStaleness(tmpDir, '@example/lib') expect(report.skillVersion).toBe('3.5.0') @@ -299,10 +303,10 @@ describe('checkStaleness', () => { mkdirSync(skillDir, { recursive: true }) writeFileSync(join(skillDir, 'SKILL.md'), 'no frontmatter here') - globalThis.fetch = vi.fn().mockResolvedValue({ ok: false } as Response) + mockFetchNotOk() const report = await checkStaleness(tmpDir, '@example/lib') expect(report.skills).toHaveLength(1) - expect(report.skills[0]!.needsReview).toBe(false) + expect(requireFirstSkill(report).needsReview).toBe(false) }) }) diff --git a/packages/intent/tsconfig.json b/packages/intent/tsconfig.json index a7910a7..195c1f9 100644 --- a/packages/intent/tsconfig.json +++ b/packages/intent/tsconfig.json @@ -2,9 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": ".", - "outDir": "dist", - "noEmit": false, - "declaration": true + "noEmit": true }, - "include": ["src", "tests"] + "include": ["src", "tests", "vitest.config.ts"] }