Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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/vast-bags-switch.md
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 22 additions & 2 deletions packages/intent/src/cli-support.ts
Original file line number Diff line number Diff line change
@@ -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<string>): void {
Expand Down Expand Up @@ -47,10 +48,29 @@ export async function resolveStaleTargets(
targetDir?: string,
): Promise<{ reports: Array<StalenessReport> }> {
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')

const targetsResolvedPackage =
context.packageRoot !== null &&
(context.targetSkillsDir !== null || resolvedRoot !== context.workspaceRoot)

if (targetsResolvedPackage && context.packageRoot) {
return {
reports: [
await checkStaleness(
context.packageRoot,
readPackageName(context.packageRoot),
),
],
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Narrow this fast path to real package targets.

Line 61 is always true when context.workspaceRoot is null, so intent stale from a normal app root with a package.json now skips the installed-package scan at Lines 98-105 and reports the app package instead. The same branch also misclassifies workspace-root-owned paths like packages/ as a single-package target. Please gate this branch to explicit skills targets or subpackages whose packageRoot differs from the workspace root.

Suggested predicate tightening
-  const targetsResolvedPackage =
-    context.packageRoot !== null &&
-    (context.targetSkillsDir !== null || resolvedRoot !== context.workspaceRoot)
+  const targetsResolvedPackage =
+    context.packageRoot !== null &&
+    (
+      context.targetSkillsDir !== null ||
+      (
+        context.workspaceRoot !== null &&
+        context.packageRoot !== context.workspaceRoot &&
+        resolvedRoot !== context.workspaceRoot
+      )
+    )

Based on learnings, monorepo detection here intentionally needs to answer whether a package is inside a monorepo, not whether the workspace root itself should be treated as a targeted sub-package.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/cli-support.ts` around lines 59 - 71, The fast-path
predicate incorrectly fires when workspaceRoot is null or when packageRoot
equals the workspace root; restrict it so the branch only runs for explicit
skills targets or true subpackages: change the targetsResolvedPackage condition
to only be true when context.targetSkillsDir !== null OR (context.workspaceRoot
!== null AND resolvedRoot !== context.workspaceRoot), and keep the existing
guard that context.packageRoot is set before returning the single
checkStaleness(report) for that packageRoot/readPackageName; this ensures
workspace-root-owned paths and null workspaceRoot do not short-circuit the
installed-package scan.

}

if (existsSync(join(resolvedRoot, 'skills'))) {
return {
reports: [
Expand Down
123 changes: 123 additions & 0 deletions packages/intent/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -484,6 +521,92 @@ 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')

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')

fetchSpy.mockRestore()
})
})

describe('package metadata', () => {
Expand Down
Loading