Skip to content
Open
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/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),
),
],
}
Comment on lines +59 to +71
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