Skip to content

Commit 09fea20

Browse files
LadyBluenotescoderabbitai[bot]KyleAMathewsclaudeautofix-ci[bot]
authored
refactor(intent): add shared project context resolver for validate and edit-package-json (#93)
* feat: implement workspace patterns management and package resolution * feat: refactor workspace patterns handling and improve package resolution logic * changeset * feat: add project context resolution and associated tests * feat: refactor validation logic to utilize project context and improve packaging warnings * feat: enhance package.json editing by integrating project context and improving monorepo detection * changeset * feat: enhance workspace patterns handling with exclusion support and add related tests * Update packages/intent/src/workspace-patterns.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: address review findings for workspace refactor - Add process.exitCode = 1 on JSON parse failure in setup.ts (was silently returning success) - Skip _artifacts validation for monorepo packages in validate.ts (artifacts live at workspace root) - Add JSDoc to resolveProjectContext explaining dual-fallback resolution strategy - Add comment to resolveWorkspacePatternSegments describing glob matching behavior - Add standalone (non-monorepo) project test for resolveProjectContext Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: apply automated fixes --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent a1ecb68 commit 09fea20

12 files changed

Lines changed: 728 additions & 214 deletions

.changeset/busy-peas-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/intent': patch
3+
---
4+
5+
Fix workspace package discovery for nested glob patterns, including support for `*` and `**`. Workspace patterns and resolved package roots are now normalized, deduped, and sorted, and the shared resolver has been extracted for reuse by internal workspace scanning.

.changeset/red-regions-shine.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/intent': patch
3+
---
4+
5+
Refactor @tanstack/intent to use a shared project context resolver for workspace and package detection. This fixes monorepo targeting bugs in validate and edit-package-json, including pnpm workspaces defined only by pnpm-workspace.yaml.

packages/intent/src/cli-support.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export async function resolveStaleTargets(
6060
}
6161

6262
const { findPackagesWithSkills, findWorkspaceRoot } =
63-
await import('./setup.js')
63+
await import('./workspace-patterns.js')
6464
const workspaceRoot = findWorkspaceRoot(resolvedRoot)
6565
if (workspaceRoot) {
6666
const packageDirs = findPackagesWithSkills(workspaceRoot)

packages/intent/src/commands/validate.ts

Lines changed: 17 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { existsSync, readFileSync } from 'node:fs'
2-
import { dirname, join, relative, sep } from 'node:path'
2+
import { join, relative, resolve, sep } from 'node:path'
33
import { fail } from '../cli-error.js'
44
import { printWarnings } from '../cli-support.js'
5+
import {
6+
type ProjectContext,
7+
resolveProjectContext,
8+
} from '../core/project-context.js'
59

610
interface ValidationError {
711
file: string
@@ -28,27 +32,10 @@ function buildValidationFailure(
2832
return lines.join('\n')
2933
}
3034

31-
function isInsideMonorepo(root: string): boolean {
32-
let dir = join(root, '..')
33-
for (let i = 0; i < 5; i++) {
34-
const parentPkg = join(dir, 'package.json')
35-
if (existsSync(parentPkg)) {
36-
try {
37-
const parent = JSON.parse(readFileSync(parentPkg, 'utf8'))
38-
return Array.isArray(parent.workspaces) || parent.workspaces?.packages
39-
} catch {
40-
return false
41-
}
42-
}
43-
const next = dirname(dir)
44-
if (next === dir) break
45-
dir = next
46-
}
47-
return false
48-
}
35+
function collectPackagingWarnings(context: ProjectContext): Array<string> {
36+
if (!context.packageRoot || !context.targetPackageJsonPath) return []
4937

50-
function collectPackagingWarnings(root: string): Array<string> {
51-
const pkgJsonPath = join(root, 'package.json')
38+
const pkgJsonPath = context.targetPackageJsonPath
5239
if (!existsSync(pkgJsonPath)) return []
5340

5441
let pkgJson: Record<string, unknown>
@@ -81,9 +68,7 @@ function collectPackagingWarnings(root: string): Array<string> {
8168

8269
// In monorepos, _artifacts lives at repo root, not under packages —
8370
// the negation pattern is a no-op and shouldn't be added.
84-
const isMonorepoPkg = isInsideMonorepo(root)
85-
86-
if (!isMonorepoPkg && !files.includes('!skills/_artifacts')) {
71+
if (!context.isMonorepo && !files.includes('!skills/_artifacts')) {
8772
warnings.push(
8873
'"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily',
8974
)
@@ -93,31 +78,17 @@ function collectPackagingWarnings(root: string): Array<string> {
9378
return warnings
9479
}
9580

96-
function resolvePackageRoot(startDir: string): string {
97-
let dir = startDir
98-
99-
while (true) {
100-
if (existsSync(join(dir, 'package.json'))) {
101-
return dir
102-
}
103-
104-
const next = dirname(dir)
105-
if (next === dir) {
106-
return startDir
107-
}
108-
109-
dir = next
110-
}
111-
}
112-
11381
export async function runValidateCommand(dir?: string): Promise<void> {
11482
const [{ parse: parseYaml }, { findSkillFiles }] = await Promise.all([
11583
import('yaml'),
11684
import('../utils.js'),
11785
])
11886
const targetDir = dir ?? 'skills'
119-
const skillsDir = join(process.cwd(), targetDir)
120-
const packageRoot = resolvePackageRoot(skillsDir)
87+
const context = resolveProjectContext({
88+
cwd: process.cwd(),
89+
targetPath: targetDir,
90+
})
91+
const skillsDir = context.targetSkillsDir ?? resolve(process.cwd(), targetDir)
12192

12293
if (!existsSync(skillsDir)) {
12394
fail(`Skills directory not found: ${skillsDir}`)
@@ -197,8 +168,9 @@ export async function runValidateCommand(dir?: string): Promise<void> {
197168
}
198169
}
199170

171+
// In monorepos, _artifacts lives at the workspace root, not under each package's skills/ dir.
200172
const artifactsDir = join(skillsDir, '_artifacts')
201-
if (existsSync(artifactsDir)) {
173+
if (!context.isMonorepo && existsSync(artifactsDir)) {
202174
const requiredArtifacts = [
203175
'domain_map.yaml',
204176
'skill_spec.md',
@@ -238,7 +210,7 @@ export async function runValidateCommand(dir?: string): Promise<void> {
238210
}
239211
}
240212

241-
const warnings = collectPackagingWarnings(packageRoot)
213+
const warnings = collectPackagingWarnings(context)
242214

243215
if (errors.length > 0) {
244216
fail(buildValidationFailure(errors, warnings))
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { existsSync, statSync } from 'node:fs'
2+
import { dirname, join, relative, resolve } from 'node:path'
3+
import {
4+
findWorkspaceRoot,
5+
readWorkspacePatterns,
6+
} from '../workspace-patterns.js'
7+
8+
export type ProjectContext = {
9+
cwd: string
10+
workspaceRoot: string | null
11+
packageRoot: string | null
12+
isMonorepo: boolean
13+
workspacePatterns: Array<string>
14+
targetPackageJsonPath: string | null
15+
targetSkillsDir: string | null
16+
}
17+
18+
/**
19+
* Resolves project structure by walking up from targetPath (or cwd) to find the
20+
* owning package.json, then searches for a workspace root from the package root.
21+
* Falls back to searching from cwd when targetPath points deep into a package.
22+
*/
23+
export function resolveProjectContext({
24+
cwd,
25+
targetPath,
26+
}: {
27+
cwd: string
28+
targetPath?: string
29+
}): ProjectContext {
30+
const resolvedCwd = resolve(cwd)
31+
const resolvedTargetPath = targetPath
32+
? resolve(resolvedCwd, targetPath)
33+
: resolvedCwd
34+
const packageRoot = findOwningPackageRoot(resolvedTargetPath)
35+
const workspaceRoot =
36+
findWorkspaceRoot(packageRoot ?? resolvedTargetPath) ??
37+
findWorkspaceRoot(resolvedCwd)
38+
const workspacePatterns = workspaceRoot
39+
? (readWorkspacePatterns(workspaceRoot) ?? [])
40+
: []
41+
42+
return {
43+
cwd: resolvedCwd,
44+
workspaceRoot,
45+
packageRoot,
46+
isMonorepo: workspaceRoot !== null,
47+
workspacePatterns,
48+
targetPackageJsonPath: packageRoot
49+
? join(packageRoot, 'package.json')
50+
: null,
51+
targetSkillsDir: resolveTargetSkillsDir(resolvedTargetPath, packageRoot),
52+
}
53+
}
54+
55+
function findOwningPackageRoot(startPath: string): string | null {
56+
let dir = toSearchDir(startPath)
57+
58+
while (true) {
59+
if (existsSync(join(dir, 'package.json'))) {
60+
return dir
61+
}
62+
63+
const next = dirname(dir)
64+
if (next === dir) {
65+
return null
66+
}
67+
68+
dir = next
69+
}
70+
}
71+
72+
function toSearchDir(path: string): string {
73+
if (!existsSync(path)) {
74+
return path
75+
}
76+
77+
return statSync(path).isDirectory() ? path : dirname(path)
78+
}
79+
80+
function resolveTargetSkillsDir(
81+
targetPath: string,
82+
packageRoot: string | null,
83+
): string | null {
84+
if (!packageRoot) {
85+
return null
86+
}
87+
88+
const packageSkillsDir = join(packageRoot, 'skills')
89+
90+
if (isWithinOrEqual(targetPath, packageSkillsDir)) {
91+
return packageSkillsDir
92+
}
93+
94+
if (targetPath === packageRoot && existsSync(packageSkillsDir)) {
95+
return packageSkillsDir
96+
}
97+
98+
return null
99+
}
100+
101+
function isWithinOrEqual(path: string, parentDir: string): boolean {
102+
const rel = relative(parentDir, path)
103+
return rel === '' || (!rel.startsWith('..') && !rel.startsWith('/'))
104+
}

packages/intent/src/scanner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
findWorkspaceRoot,
1212
readWorkspacePatterns,
1313
resolveWorkspacePackages,
14-
} from './setup.js'
14+
} from './workspace-patterns.js'
1515
import type {
1616
InstalledVariant,
1717
IntentConfig,

0 commit comments

Comments
 (0)