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/fix-monorepo-workspace-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Fix monorepo workspace detection so `setup-github-actions`, `validate`, `edit-package-json`, and `stale` behave correctly from repo roots and package directories. Resolve workspace packages using recursive glob matching, which supports nested patterns like `apps/*/packages/*`. Generated workflows now derive skill and watch globs from actual workspace config, including `pnpm-workspace.yaml`, `package.json` workspaces, and Deno workspace files, which avoids broken paths, wrong labels, and false packaging warnings in non-standard layouts.
32 changes: 17 additions & 15 deletions packages/intent/meta/templates/workflows/validate-skills.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,25 @@ jobs:

- name: Find and validate skills
run: |
# Find all directories containing SKILL.md files
SKILLS_DIR=""
shopt -s globstar 2>/dev/null || true
FOUND=false
# Root-level skills directory
if [ -d "skills" ]; then
SKILLS_DIR="skills"
elif [ -d "packages" ]; then
# Monorepo — find skills/ under packages
for dir in packages/*/skills; do
if [ -d "$dir" ]; then
echo "Validating $dir..."
intent validate "$dir"
fi
done
exit 0
echo "Validating skills..."
intent validate skills
FOUND=true
fi
if [ -n "$SKILLS_DIR" ]; then
intent validate "$SKILLS_DIR"
else
# Workspace package skills derived from workspace config
for dir in {{WORKSPACE_SKILL_GLOBS}}; do
if [ -d "$dir" ]; then
echo "Validating $dir..."
intent validate "$dir"
FOUND=true
fi
done
if [ "$FOUND" = "false" ]; then
echo "No skills/ directory found — skipping validation."
fi
34 changes: 8 additions & 26 deletions packages/intent/src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,10 @@ function buildValidationFailure(
return lines.join('\n')
}

function isInsideMonorepo(root: string): boolean {
let dir = join(root, '..')
for (let i = 0; i < 5; i++) {
const parentPkg = join(dir, 'package.json')
if (existsSync(parentPkg)) {
try {
const parent = JSON.parse(readFileSync(parentPkg, 'utf8'))
return Array.isArray(parent.workspaces) || parent.workspaces?.packages
} catch {
return false
}
}
const next = dirname(dir)
if (next === dir) break
dir = next
}
return false
}

function collectPackagingWarnings(root: string): Array<string> {
function collectPackagingWarnings(
root: string,
isMonorepo: boolean,
): Array<string> {
const pkgJsonPath = join(root, 'package.json')
if (!existsSync(pkgJsonPath)) return []

Expand Down Expand Up @@ -79,11 +63,7 @@ function collectPackagingWarnings(root: string): Array<string> {
)
}

// In monorepos, _artifacts lives at repo root, not under packages —
// the negation pattern is a no-op and shouldn't be added.
const isMonorepoPkg = isInsideMonorepo(root)

if (!isMonorepoPkg && !files.includes('!skills/_artifacts')) {
if (!isMonorepo && !files.includes('!skills/_artifacts')) {
warnings.push(
'"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily',
)
Expand Down Expand Up @@ -238,7 +218,9 @@ export async function runValidateCommand(dir?: string): Promise<void> {
}
}

const warnings = collectPackagingWarnings(packageRoot)
const { findWorkspaceRoot } = await import('../setup.js')
const isMonorepo = findWorkspaceRoot(join(packageRoot, '..')) !== null
const warnings = collectPackagingWarnings(packageRoot, isMonorepo)

if (errors.length > 0) {
fail(buildValidationFailure(errors, warnings))
Expand Down
23 changes: 10 additions & 13 deletions packages/intent/src/library-scanner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { existsSync, readdirSync } from 'node:fs'
import { dirname, join, relative, sep } from 'node:path'
import { getDeps, parseFrontmatter, resolveDepDir } from './utils.js'
import {
getDeps,
parseFrontmatter,
readPkgJsonFile,
resolveDepDir,
} from './utils.js'
import type { SkillEntry } from './types.js'
import type { Dirent } from 'node:fs'

Expand All @@ -24,14 +29,6 @@ export interface LibraryScanResult {
// Helpers
// ---------------------------------------------------------------------------

function readPkgJson(dir: string): Record<string, unknown> | null {
try {
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
} catch {
return null
}
}

function findHomeDir(scriptPath: string): string | null {
let dir = dirname(scriptPath)
for (;;) {
Expand Down Expand Up @@ -117,7 +114,7 @@ export function scanLibrary(
}
}

const homePkg = readPkgJson(homeDir)
const homePkg = readPkgJsonFile(homeDir)
if (!homePkg) {
return { packages, warnings: ['Could not read home package.json'] }
}
Expand All @@ -128,7 +125,7 @@ export function scanLibrary(
if (visited.has(name)) return
visited.add(name)

const pkg = readPkgJson(dir)
const pkg = readPkgJsonFile(dir)
if (!pkg) {
warnings.push(`Could not read package.json for ${name}`)
return
Expand All @@ -145,7 +142,7 @@ export function scanLibrary(
for (const depName of getDeps(pkg)) {
const depDir = resolveDepDir(depName, dir)
if (!depDir) continue
const depPkg = readPkgJson(depDir)
const depPkg = readPkgJsonFile(depDir)
if (depPkg && isIntentPackage(depPkg)) {
processPackage(depName, depDir)
}
Expand Down
13 changes: 5 additions & 8 deletions packages/intent/src/scanner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { existsSync, readFileSync, readdirSync, type Dirent } from 'node:fs'
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { join, relative, sep } from 'node:path'
import {
detectGlobalNodeModules,
getDeps,
listNodeModulesPackageDirs,
normalizeRepoUrl,
parseFrontmatter,
resolveDepDir,
} from './utils.js'
Expand All @@ -21,6 +22,7 @@ import type {
SkillEntry,
VersionConflict,
} from './types.js'
import type { Dirent } from 'node:fs'

// ---------------------------------------------------------------------------
// Package manager detection
Expand Down Expand Up @@ -97,18 +99,13 @@ function deriveIntentConfig(
// Derive repo from repository field
let repo: string | null = null
if (typeof pkgJson.repository === 'string') {
repo = pkgJson.repository
repo = normalizeRepoUrl(pkgJson.repository)
} else if (
pkgJson.repository &&
typeof pkgJson.repository === 'object' &&
typeof (pkgJson.repository as Record<string, unknown>).url === 'string'
) {
repo = (pkgJson.repository as Record<string, unknown>).url as string
// Normalize git+https://github.com/foo/bar.git → foo/bar
repo = repo
.replace(/^git\+/, '')
.replace(/\.git$/, '')
.replace(/^https?:\/\/github\.com\//, '')
repo = normalizeRepoUrl((pkgJson.repository as { url: string }).url)
}

// Derive docs from homepage field
Expand Down
Loading
Loading