-
-
Notifications
You must be signed in to change notification settings - Fork 5
feat: workspace-aware scanning, createRequire resolution, and integration tests #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa5a8a2
53b23d1
b531999
fd02ab6
33d75ed
0477d99
d45901f
2cadfd1
a87c4b4
fccd8a3
bf50f7d
86a520d
2d251d5
4f5f66d
1fd068b
e08f77d
94dfa34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/intent': patch | ||
| --- | ||
|
|
||
| Add workspace-aware scanning so `intent list` discovers skills in monorepo workspace packages when run from the root. Replace `resolveDepDir` with `createRequire`-based resolution that handles hoisted deps, pnpm symlinks, and export maps. The `resolveDepDir` public API signature changed from 4 parameters to 2 — callers using the old signature should update to `resolveDepDir(depName, parentDir)`. `detectPackageManager` now checks workspace root for lockfiles when scanning from a subdir. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,3 +61,5 @@ vite.config.js.timestamp-* | |
| vite.config.ts.timestamp-* | ||
|
|
||
| .angular | ||
|
|
||
| docs/superpowers | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -30,12 +30,15 @@ | |||||
| "yaml": "^2.7.0" | ||||||
| }, | ||||||
| "devDependencies": { | ||||||
| "tsdown": "^0.19.0" | ||||||
| "@verdaccio/node-api": "6.0.0-6-next.76", | ||||||
| "tsdown": "^0.19.0", | ||||||
| "verdaccio": "^6.3.2" | ||||||
| }, | ||||||
| "scripts": { | ||||||
| "prepack": "npm run build", | ||||||
| "build": "tsdown src/index.ts src/cli.ts src/setup.ts src/intent-library.ts src/library-scanner.ts --format esm --dts", | ||||||
| "test:lib": "vitest run", | ||||||
| "test:lib": "vitest run --exclude 'tests/integration/**'", | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Single quotes in Sources: Citations:
Use double quotes for the exclude glob in npm runs package scripts through 🔧 Proposed fix- "test:lib": "vitest run --exclude 'tests/integration/**'",
+ "test:lib": "vitest run --exclude \"tests/integration/**\"",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| "test:integration": "vitest run tests/integration/", | ||||||
| "test:types": "tsc --noEmit" | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -91,9 +91,8 @@ function discoverSkills(skillsDir: string): Array<SkillEntry> { | |
|
|
||
| export async function scanLibrary( | ||
| scriptPath: string, | ||
| projectRoot?: string, | ||
| _projectRoot?: string, | ||
| ): Promise<LibraryScanResult> { | ||
|
Comment on lines
92
to
95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep honoring
Also applies to: 133-135 🤖 Prompt for AI Agents |
||
| const nodeModulesDir = join(projectRoot ?? process.cwd(), 'node_modules') | ||
| const packages: Array<LibraryPackage> = [] | ||
| const warnings: Array<string> = [] | ||
| const visited = new Set<string>() | ||
|
|
@@ -132,7 +131,7 @@ export async function scanLibrary( | |
| }) | ||
|
|
||
| for (const depName of getDeps(pkg)) { | ||
| const depDir = resolveDepDir(depName, dir, name, nodeModulesDir) | ||
| const depDir = resolveDepDir(depName, dir) | ||
| if (!depDir) continue | ||
| const depPkg = readPkgJson(depDir) | ||
| if (depPkg && hasIntentBin(depPkg)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { existsSync, readFileSync, readdirSync } from 'node:fs' | ||
| import { existsSync, readFileSync, readdirSync, type Dirent } from 'node:fs' | ||
| import { join, relative, sep } from 'node:path' | ||
| import { | ||
| detectGlobalNodeModules, | ||
|
|
@@ -7,6 +7,11 @@ import { | |
| parseFrontmatter, | ||
| resolveDepDir, | ||
| } from './utils.js' | ||
| import { | ||
| findWorkspaceRoot, | ||
| readWorkspacePatterns, | ||
| resolveWorkspacePackages, | ||
| } from './setup.js' | ||
| import type { | ||
| InstalledVariant, | ||
| IntentConfig, | ||
|
|
@@ -16,7 +21,6 @@ import type { | |
| SkillEntry, | ||
| VersionConflict, | ||
| } from './types.js' | ||
| import type { Dirent } from 'node:fs' | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Package manager detection | ||
|
|
@@ -40,11 +44,17 @@ function detectPackageManager(root: string): PackageManager { | |
| ) | ||
| } | ||
|
|
||
| if (existsSync(join(root, 'pnpm-lock.yaml'))) return 'pnpm' | ||
| if (existsSync(join(root, 'bun.lockb')) || existsSync(join(root, 'bun.lock'))) | ||
| return 'bun' | ||
| if (existsSync(join(root, 'yarn.lock'))) return 'yarn' | ||
| if (existsSync(join(root, 'package-lock.json'))) return 'npm' | ||
| const dirsToCheck = [root] | ||
| const wsRoot = findWorkspaceRoot(root) | ||
| if (wsRoot && wsRoot !== root) dirsToCheck.push(wsRoot) | ||
|
|
||
| for (const dir of dirsToCheck) { | ||
| if (existsSync(join(dir, 'pnpm-lock.yaml'))) return 'pnpm' | ||
| if (existsSync(join(dir, 'bun.lockb')) || existsSync(join(dir, 'bun.lock'))) | ||
| return 'bun' | ||
| if (existsSync(join(dir, 'yarn.lock'))) return 'yarn' | ||
| if (existsSync(join(dir, 'package-lock.json'))) return 'npm' | ||
| } | ||
| return 'unknown' | ||
| } | ||
|
|
||
|
|
@@ -337,8 +347,6 @@ export async function scanForIntents(root?: string): Promise<ScanResult> { | |
| : undefined, | ||
| }, | ||
| } | ||
| const resolutionRoots = [nodeModulesDir] | ||
|
|
||
| // Track registered package names to avoid duplicates across phases | ||
| const packageIndexes = new Map<string, number>() | ||
| const packageJsonCache = new Map<string, Record<string, unknown> | null>() | ||
|
|
@@ -369,15 +377,6 @@ export async function scanForIntents(root?: string): Promise<ScanResult> { | |
| ? existsSync(detected.path) | ||
| : false | ||
| } | ||
|
|
||
| if ( | ||
| nodeModules.global.exists && | ||
| nodeModules.global.path && | ||
| nodeModules.global.path !== nodeModulesDir && | ||
| !resolutionRoots.includes(nodeModules.global.path) | ||
| ) { | ||
| resolutionRoots.push(nodeModules.global.path) | ||
| } | ||
| } | ||
|
|
||
| function readPkgJson(dirPath: string): Record<string, unknown> | null { | ||
|
|
@@ -490,7 +489,7 @@ export async function scanForIntents(root?: string): Promise<ScanResult> { | |
| } | ||
|
|
||
| for (const depName of getDeps(pkgJson)) { | ||
| const depDir = resolveDepDir(depName, pkgDir, pkgName, resolutionRoots) | ||
| const depDir = resolveDepDir(depName, pkgDir) | ||
| if (!depDir || walkVisited.has(depDir)) continue | ||
|
|
||
| tryRegister(depDir, depName) | ||
|
|
@@ -524,20 +523,52 @@ export async function scanForIntents(root?: string): Promise<ScanResult> { | |
| } | ||
|
|
||
| if (!projectPkg) return | ||
| walkDepsFromPkgJson(projectPkg, projectRoot, true) | ||
| } | ||
|
|
||
| for (const depName of getDeps(projectPkg, true)) { | ||
| const depDir = resolveDepDir( | ||
| depName, | ||
| projectRoot, | ||
| depName, | ||
| resolutionRoots, | ||
| ) | ||
| /** Resolve and walk deps listed in a package.json. */ | ||
| function walkDepsFromPkgJson( | ||
| pkgJson: Record<string, unknown>, | ||
| fromDir: string, | ||
| includeDevDeps = false, | ||
| ): void { | ||
| for (const depName of getDeps(pkgJson, includeDevDeps)) { | ||
| const depDir = resolveDepDir(depName, fromDir) | ||
| if (depDir && !walkVisited.has(depDir)) { | ||
| tryRegister(depDir, depName) | ||
| walkDeps(depDir, depName) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * In monorepos, discover workspace packages and walk their deps. | ||
| * Handles pnpm monorepos (workspace-specific node_modules) and ensures | ||
| * transitive skills packages are found through workspace package dependencies. | ||
| */ | ||
| function walkWorkspacePackages(): void { | ||
| const workspacePatterns = readWorkspacePatterns(projectRoot) | ||
| if (!workspacePatterns) return | ||
|
|
||
| for (const wsDir of resolveWorkspacePackages( | ||
| projectRoot, | ||
| workspacePatterns, | ||
| )) { | ||
| const wsNodeModules = join(wsDir, 'node_modules') | ||
| if (existsSync(wsNodeModules)) { | ||
| for (const dirPath of listNodeModulesPackageDirs(wsNodeModules)) { | ||
| tryRegister(dirPath, 'unknown') | ||
| } | ||
| } | ||
|
|
||
| const wsPkg = readPkgJson(wsDir) | ||
| if (wsPkg) { | ||
| walkDepsFromPkgJson(wsPkg, wsDir) | ||
| } | ||
| } | ||
|
Comment on lines
+549
to
+568
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Register the workspace package itself before walking its dependencies. This phase only scans Suggested fix const wsPkg = readPkgJson(wsDir)
if (wsPkg) {
+ const wsName = typeof wsPkg.name === 'string' ? wsPkg.name : 'unknown'
+ tryRegister(wsDir, wsName)
walkDepsFromPkgJson(wsPkg, wsDir)
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| walkWorkspacePackages() | ||
| walkKnownPackages() | ||
| walkProjectDeps() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -480,7 +480,7 @@ export function readWorkspacePatterns(root: string): Array<string> | null { | |||||||||||||||||||||||||||
| * Handles simple patterns like "packages/*" and "packages/**". | ||||||||||||||||||||||||||||
| * Each resolved directory must contain a package.json. | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| function resolveWorkspacePackages( | ||||||||||||||||||||||||||||
| export function resolveWorkspacePackages( | ||||||||||||||||||||||||||||
| root: string, | ||||||||||||||||||||||||||||
| patterns: Array<string>, | ||||||||||||||||||||||||||||
| ): Array<string> { | ||||||||||||||||||||||||||||
|
|
@@ -497,7 +497,13 @@ function resolveWorkspacePackages( | |||||||||||||||||||||||||||
| collectPackageDirs(baseDir, dirs) | ||||||||||||||||||||||||||||
| } else if (pattern.endsWith('/*')) { | ||||||||||||||||||||||||||||
| // Single level: direct children | ||||||||||||||||||||||||||||
| for (const entry of readdirSync(baseDir, { withFileTypes: true })) { | ||||||||||||||||||||||||||||
| let entries: Array<import('node:fs').Dirent> | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: With the rule’s default options, no: So import type { Dirent } from 'node:fs';If you want to allow Citations:
🏁 Script executed: # Check if the file exists and examine line 500
if [ -f "packages/intent/src/setup.ts" ]; then
echo "=== File exists ==="
echo "=== Lines around 500 ==="
sed -n '495,505p' "packages/intent/src/setup.ts"
else
echo "File not found at packages/intent/src/setup.ts"
fiRepository: TanStack/intent Length of output: 469 🏁 Script executed: # Find and check ESLint/TypeScript configuration files
echo "=== Checking for ESLint config files ==="
find . -maxdepth 2 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" -o -name "tsconfig.json" \) 2>/dev/null | head -20Repository: TanStack/intent Length of output: 134 🏁 Script executed: # Check if `@typescript-eslint/consistent-type-imports` rule is configured
echo "=== Searching for consistent-type-imports configuration ==="
rg "consistent-type-imports" --type json --type js --type ts 2>/dev/null | head -20Repository: TanStack/intent Length of output: 118 🏁 Script executed: # Read the ESLint config to check rule settings
echo "=== ESLint Configuration ==="
cat ./eslint.config.js | head -100Repository: TanStack/intent Length of output: 636 🏁 Script executed: # Also check the imports at the top of the setup.ts file to verify current state
echo "=== Top of setup.ts file (first 50 lines) ==="
head -50 "packages/intent/src/setup.ts"Repository: TanStack/intent Length of output: 1312 🏁 Script executed: # Check what `@tanstack/eslint-config` contains
echo "=== Looking for `@tanstack/eslint-config` ==="
fd "@tanstack/eslint-config" --type f 2>/dev/null | head -5Repository: TanStack/intent Length of output: 102 🏁 Script executed: # Search node_modules or check package.json for the config
echo "=== Checking package.json dependencies ==="
rg "@tanstack/eslint-config" . --type json 2>/dev/null | head -10Repository: TanStack/intent Length of output: 156 🏁 Script executed: # Check if there's a way to see what tanstackConfig includes
echo "=== Search for references to consistent-type-imports in the project ==="
rg "consistent-type-imports" . 2>/dev/nullRepository: TanStack/intent Length of output: 130 Add a type import for The inline type annotation Proposed fix import {
existsSync,
mkdirSync,
readFileSync,
readdirSync,
writeFileSync,
} from 'node:fs'
+import type { Dirent } from 'node:fs'
@@
- let entries: Array<import('node:fs').Dirent>
+ let entries: Array<Dirent>📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 500-500: ( 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| entries = readdirSync(baseDir, { withFileTypes: true }) | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+501
to
+505
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't hide Line 493 already proved 🔧 Proposed fix- } catch {
- continue
+ } catch (err: unknown) {
+ console.error(
+ `Warning: could not read directory ${baseDir}: ${err instanceof Error ? err.message : err}`,
+ )
+ continue
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| for (const entry of entries) { | ||||||||||||||||||||||||||||
| if (!entry.isDirectory()) continue | ||||||||||||||||||||||||||||
| const dir = join(baseDir, entry.name) | ||||||||||||||||||||||||||||
| if (existsSync(join(dir, 'package.json'))) { | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "name": "@test-intent/skills-leaf", | ||
| "version": "1.0.0", | ||
| "intent": { | ||
| "version": 1, | ||
| "repo": "test/skills-leaf", | ||
| "docs": "https://example.com/docs" | ||
| }, | ||
| "files": [ | ||
| "skills" | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| name: core | ||
| description: 'Core skill for integration testing' | ||
| type: core | ||
| --- | ||
|
|
||
| # Core Skill | ||
|
|
||
| This is a test skill used by integration tests. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "name": "@test-intent/wrapper-1", | ||
| "version": "1.0.0", | ||
| "dependencies": { | ||
| "@test-intent/skills-leaf": "1.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "name": "@test-intent/wrapper-2", | ||
| "version": "1.0.0", | ||
| "dependencies": { | ||
| "@test-intent/wrapper-1": "1.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "name": "@test-intent/wrapper-3", | ||
| "version": "1.0.0", | ||
| "dependencies": { | ||
| "@test-intent/wrapper-2": "1.0.0" | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a non-patch bump for this release.
This changeset documents a public
resolveDepDirsignature break, so shipping it aspatchwould let downstream users take a breaking change on a patch upgrade.🤖 Prompt for AI Agents