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`, and `stale` behave correctly from repo roots and package directories. 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-`packages/*` 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: 9 additions & 25 deletions packages/intent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,10 @@ async function cmdMeta(args: Array<string>): Promise<void> {
console.log(`Path: node_modules/@tanstack/intent/meta/<name>/SKILL.md`)
}

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

Expand Down Expand Up @@ -260,28 +263,7 @@ function collectPackagingWarnings(root: string): Array<string> {
// Only warn about !skills/_artifacts for non-monorepo packages.
// In monorepos, artifacts live at the repo root, so the negation
// pattern is intentionally omitted by edit-package-json.
const isMonorepoPkg = (() => {
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
})()

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 All @@ -294,7 +276,7 @@ function collectPackagingWarnings(root: string): Array<string> {
function resolvePackageRoot(startDir: string): string {
let dir = startDir

while (true) {
for (;;) {
if (existsSync(join(dir, 'package.json'))) {
return dir
}
Expand Down Expand Up @@ -496,7 +478,9 @@ async function cmdValidate(args: Array<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