diff --git a/.changeset/vast-bags-switch.md b/.changeset/vast-bags-switch.md new file mode 100644 index 0000000..67d10df --- /dev/null +++ b/.changeset/vast-bags-switch.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Fix intent stale so monorepo package paths resolve to the targeted workspace package instead of scanning the whole workspace. diff --git a/packages/intent/src/cli-support.ts b/packages/intent/src/cli-support.ts index 84db99e..8bb0e3b 100644 --- a/packages/intent/src/cli-support.ts +++ b/packages/intent/src/cli-support.ts @@ -1,7 +1,8 @@ import { existsSync, readFileSync } from 'node:fs' -import { dirname, join, relative } from 'node:path' +import { dirname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { fail } from './cli-error.js' +import { resolveProjectContext } from './core/project-context.js' import type { ScanResult, StalenessReport } from './types.js' export function printWarnings(warnings: Array): void { @@ -47,10 +48,28 @@ export async function resolveStaleTargets( targetDir?: string, ): Promise<{ reports: Array }> { const resolvedRoot = targetDir - ? join(process.cwd(), targetDir) + ? resolve(process.cwd(), targetDir) : process.cwd() + const context = resolveProjectContext({ + cwd: process.cwd(), + targetPath: targetDir, + }) const { checkStaleness } = await import('./staleness.js') + if ( + context.packageRoot && + (context.targetSkillsDir !== null || resolvedRoot !== context.workspaceRoot) + ) { + return { + reports: [ + await checkStaleness( + context.packageRoot, + readPackageName(context.packageRoot), + ), + ], + } + } + if (existsSync(join(resolvedRoot, 'skills'))) { return { reports: [ diff --git a/packages/intent/tests/cli.test.ts b/packages/intent/tests/cli.test.ts index 1f2e2e8..28217d0 100644 --- a/packages/intent/tests/cli.test.ts +++ b/packages/intent/tests/cli.test.ts @@ -229,6 +229,43 @@ describe('cli commands', () => { expect(output).toContain('Template variables applied:') }) + it('copies github workflow templates to the workspace root', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-setup-gha-mono-')) + tempDirs.push(root) + + writeJson(join(root, 'package.json'), { + private: true, + workspaces: ['packages/*'], + }) + writeJson(join(root, 'packages', 'router', 'package.json'), { + name: '@tanstack/router', + version: '1.0.0', + intent: { version: 1, repo: 'TanStack/router', docs: 'docs/' }, + }) + writeSkillMd(join(root, 'packages', 'router', 'skills', 'routing'), { + name: 'routing', + description: 'Routing skill', + }) + + process.chdir(join(root, 'packages', 'router')) + + const exitCode = await main(['setup-github-actions']) + const rootWorkflowsDir = join(root, '.github', 'workflows') + const packageWorkflowsDir = join( + root, + 'packages', + 'router', + '.github', + 'workflows', + ) + const output = logSpy.mock.calls.flat().join('\n') + + expect(exitCode).toBe(0) + expect(existsSync(rootWorkflowsDir)).toBe(true) + expect(existsSync(packageWorkflowsDir)).toBe(false) + expect(output).toContain('Mode: monorepo') + }) + it('lists installed intent packages as json', async () => { const root = mkdtempSync(join(realTmpdir, 'intent-cli-list-')) tempDirs.push(root) @@ -484,6 +521,174 @@ describe('cli commands', () => { fetchSpy.mockRestore() }) + + it('checks only the targeted workspace package for staleness', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-stale-target-')) + tempDirs.push(root) + + writeJson(join(root, 'package.json'), { + private: true, + workspaces: ['packages/*'], + }) + writeJson(join(root, 'packages', 'router', 'package.json'), { + name: '@tanstack/router', + }) + writeJson(join(root, 'packages', 'query', 'package.json'), { + name: '@tanstack/query', + }) + writeSkillMd(join(root, 'packages', 'router', 'skills', 'routing'), { + name: 'routing', + description: 'Routing skill', + library_version: '1.0.0', + }) + writeSkillMd(join(root, 'packages', 'query', 'skills', 'cache'), { + name: 'cache', + description: 'Caching skill', + library_version: '1.0.0', + }) + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: async () => ({ version: '1.0.0' }), + } as Response) + + process.chdir(root) + + const exitCode = await main(['stale', 'packages/router/skills', '--json']) + const output = logSpy.mock.calls.at(-1)?.[0] + const reports = JSON.parse(String(output)) as Array<{ library: string }> + + expect(exitCode).toBe(0) + expect(reports).toHaveLength(1) + expect(reports[0]!.library).toBe('@tanstack/router') + expect(fetchSpy).toHaveBeenCalledTimes(1) + + fetchSpy.mockRestore() + }) + + it('checks only the targeted workspace package when path omits /skills suffix', async () => { + const root = mkdtempSync( + join(realTmpdir, 'intent-cli-stale-target-nosuffix-'), + ) + tempDirs.push(root) + + writeJson(join(root, 'package.json'), { + private: true, + workspaces: ['packages/*'], + }) + writeJson(join(root, 'packages', 'router', 'package.json'), { + name: '@tanstack/router', + }) + writeJson(join(root, 'packages', 'query', 'package.json'), { + name: '@tanstack/query', + }) + writeSkillMd(join(root, 'packages', 'router', 'skills', 'routing'), { + name: 'routing', + description: 'Routing skill', + library_version: '1.0.0', + }) + writeSkillMd(join(root, 'packages', 'query', 'skills', 'cache'), { + name: 'cache', + description: 'Caching skill', + library_version: '1.0.0', + }) + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: async () => ({ version: '1.0.0' }), + } as Response) + + process.chdir(root) + + const exitCode = await main(['stale', 'packages/router', '--json']) + const output = logSpy.mock.calls.at(-1)?.[0] + const reports = JSON.parse(String(output)) as Array<{ library: string }> + + expect(exitCode).toBe(0) + expect(reports).toHaveLength(1) + expect(reports[0]!.library).toBe('@tanstack/router') + expect(fetchSpy).toHaveBeenCalledTimes(1) + + fetchSpy.mockRestore() + }) + + it('checks the current workspace package for staleness from package cwd', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-stale-package-cwd-')) + tempDirs.push(root) + + writeJson(join(root, 'package.json'), { + private: true, + workspaces: ['packages/*'], + }) + writeJson(join(root, 'packages', 'router', 'package.json'), { + name: '@tanstack/router', + }) + writeJson(join(root, 'packages', 'query', 'package.json'), { + name: '@tanstack/query', + }) + writeSkillMd(join(root, 'packages', 'router', 'skills', 'routing'), { + name: 'routing', + description: 'Routing skill', + library_version: '1.0.0', + }) + writeSkillMd(join(root, 'packages', 'query', 'skills', 'cache'), { + name: 'cache', + description: 'Caching skill', + library_version: '1.0.0', + }) + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: async () => ({ version: '1.0.0' }), + } as Response) + + process.chdir(join(root, 'packages', 'router')) + + const exitCode = await main(['stale', '--json']) + const output = logSpy.mock.calls.at(-1)?.[0] + const reports = JSON.parse(String(output)) as Array<{ library: string }> + + expect(exitCode).toBe(0) + expect(reports).toHaveLength(1) + expect(reports[0]!.library).toBe('@tanstack/router') + expect(fetchSpy).toHaveBeenCalledTimes(1) + + fetchSpy.mockRestore() + }) + + it('handles absolute targetDir path correctly', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-stale-abs-')) + tempDirs.push(root) + + writeJson(join(root, 'package.json'), { + name: '@tanstack/router', + version: '1.0.0', + }) + writeSkillMd(join(root, 'skills', 'routing'), { + name: 'routing', + description: 'Routing skill', + library_version: '1.0.0', + }) + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: async () => ({ version: '1.0.0' }), + } as Response) + + const elsewhere = mkdtempSync(join(realTmpdir, 'intent-cli-stale-abs-cwd-')) + tempDirs.push(elsewhere) + process.chdir(elsewhere) + + const exitCode = await main(['stale', root, '--json']) + const output = logSpy.mock.calls.at(-1)?.[0] + const reports = JSON.parse(String(output)) as Array<{ library: string }> + + expect(exitCode).toBe(0) + expect(reports).toHaveLength(1) + expect(reports[0]!.library).toBe('@tanstack/router') + + fetchSpy.mockRestore() + }) }) describe('package metadata', () => {