Skip to content

Commit 29d0c76

Browse files
Improve Intent core loading for monorepos and agent adapters (#124)
* fall back to Yarn PnP for stale node_modules * add shared core skill API * route list and load through core * add hard package excludes * fast-path load resolution * resolve package-prefixed skills by short name * split core implementation helpers * add debug output for list and load * ci: apply automated fixes * skip content reads for path loads * harden load resolution and list identity * reduce direct load path filesystem work * reduce CLI and load path overhead * cover load content and large workspace paths * ci: apply automated fixes * fix intent core cwd handling and review feedback * changeset * changset * ci: apply automated fixes * update docs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b5653be commit 29d0c76

23 files changed

Lines changed: 3177 additions & 506 deletions

.changeset/five-lizards-peel.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tanstack/intent': patch
3+
---
4+
5+
Add the `@tanstack/intent/core` entrypoint for programmatic skill discovery and loading.
6+
7+
`intent load` now uses the core APIs and a direct dependency fast path, avoiding broad workspace scans when a requested skill can be resolved from the target package. This significantly improves load performance, especially in large workspaces, while preserving markdown link rewriting, warnings, debug output, and existing CLI behavior.

benchmarks/intent/load.bench.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { rmSync } from 'node:fs'
2+
import { join } from 'node:path'
3+
import { afterAll, beforeAll, bench, describe } from 'vitest'
4+
import {
5+
createBenchOptions,
6+
createCliRunner,
7+
createConsoleSilencer,
8+
createTempDir,
9+
writeFile,
10+
writeJson,
11+
writePackage,
12+
} from './helpers.js'
13+
14+
type LoadFixture = {
15+
root: string
16+
runner: ReturnType<typeof createCliRunner>
17+
workspaceRoot: string
18+
}
19+
20+
const consoleSilencer = createConsoleSilencer()
21+
let fixture: LoadFixture | null = null
22+
23+
function createFixture(): LoadFixture {
24+
const root = createTempDir('load')
25+
const workspaceRoot = createTempDir('load-workspace')
26+
27+
writeLoadProject(root)
28+
writeLargeWorkspaceProject(workspaceRoot)
29+
30+
return {
31+
root,
32+
runner: createCliRunner({ cwd: root }),
33+
workspaceRoot,
34+
}
35+
}
36+
37+
function writeLoadProject(root: string): void {
38+
writeJson(join(root, 'package.json'), {
39+
name: 'intent-load-benchmark',
40+
private: true,
41+
dependencies: {
42+
'@bench/query': '1.0.0',
43+
},
44+
})
45+
46+
writePackage(join(root, 'node_modules'), '@bench/query', '1.0.0', {
47+
skills: ['query/core', 'query/cache', 'query/testing'],
48+
})
49+
50+
writeQueryCacheContent(join(root, 'node_modules', '@bench', 'query'))
51+
}
52+
53+
function writeLargeWorkspaceProject(root: string): void {
54+
writeJson(join(root, 'package.json'), {
55+
name: 'intent-large-workspace-load-benchmark',
56+
private: true,
57+
workspaces: ['packages/*'],
58+
dependencies: {
59+
'@bench/query': '1.0.0',
60+
},
61+
})
62+
writeFile(join(root, 'pnpm-workspace.yaml'), "packages:\n - 'packages/*'\n")
63+
64+
for (let index = 0; index < 120; index++) {
65+
writeJson(join(root, 'packages', `pkg-${index}`, 'package.json'), {
66+
name: `@bench/workspace-pkg-${index}`,
67+
version: '1.0.0',
68+
dependencies:
69+
index % 10 === 0
70+
? {
71+
'@bench/query': '1.0.0',
72+
}
73+
: undefined,
74+
})
75+
}
76+
77+
writePackage(join(root, 'node_modules'), '@bench/query', '1.0.0', {
78+
skills: ['query/core', 'query/cache', 'query/testing'],
79+
})
80+
81+
writeQueryCacheContent(join(root, 'node_modules', '@bench', 'query'))
82+
}
83+
84+
function writeQueryCacheContent(packageRoot: string): void {
85+
writeFile(
86+
join(packageRoot, 'docs', 'cache-guide.md'),
87+
'# Cache guide\n\nUse the cache workflow for repeated queries.\n',
88+
)
89+
writeFile(
90+
join(packageRoot, 'assets', 'cache.txt'),
91+
'cache diagram placeholder\n',
92+
)
93+
writeFile(
94+
join(packageRoot, 'skills', 'query', 'cache', 'setup.md'),
95+
'# Cache setup\n\nConfigure query cache defaults.\n',
96+
)
97+
writeFile(
98+
join(packageRoot, 'skills', 'query', 'cache', 'SKILL.md'),
99+
[
100+
'---',
101+
'name: "query/cache"',
102+
'description: "query/cache benchmark guidance"',
103+
'type: "framework"',
104+
'requires:',
105+
' - "query"',
106+
'---',
107+
'',
108+
'# Query Cache',
109+
'',
110+
'See [cache guide](../../../docs/cache-guide.md).',
111+
'Use [local setup](setup.md#configure).',
112+
'![Cache diagram](../../../assets/cache.txt)',
113+
'',
114+
'```md',
115+
'[ignored code link](setup.md)',
116+
'```',
117+
'',
118+
...Array.from(
119+
{ length: 20 },
120+
(_, index) =>
121+
`${index + 1}. Keep cache guidance aligned with [setup](setup.md) and [guide](../../../docs/cache-guide.md#cache).`,
122+
),
123+
'',
124+
].join('\n'),
125+
)
126+
}
127+
128+
function getFixture(): LoadFixture {
129+
if (!fixture) {
130+
consoleSilencer.silence()
131+
try {
132+
fixture = createFixture()
133+
} catch (err) {
134+
consoleSilencer.restore()
135+
throw err
136+
}
137+
}
138+
139+
return fixture
140+
}
141+
142+
async function setup(): Promise<void> {
143+
await getFixture().runner.setup()
144+
}
145+
146+
function teardown(): void {
147+
if (fixture) {
148+
fixture.runner.teardown()
149+
rmSync(fixture.root, { recursive: true, force: true })
150+
rmSync(fixture.workspaceRoot, { recursive: true, force: true })
151+
fixture = null
152+
}
153+
154+
consoleSilencer.restore()
155+
}
156+
157+
async function runInCwd(
158+
cwd: string,
159+
callback: () => Promise<void>,
160+
): Promise<void> {
161+
const previousCwd = process.cwd()
162+
process.chdir(cwd)
163+
try {
164+
await callback()
165+
} finally {
166+
process.chdir(previousCwd)
167+
}
168+
}
169+
170+
describe('intent load', () => {
171+
beforeAll(setup)
172+
afterAll(teardown)
173+
174+
bench(
175+
'loads a direct dependency skill',
176+
async () => {
177+
const state = getFixture()
178+
for (let index = 0; index < 10; index++) {
179+
await state.runner.run(['load', '@bench/query#query/cache', '--path'])
180+
}
181+
},
182+
createBenchOptions(setup, teardown),
183+
)
184+
185+
bench(
186+
'loads direct dependency content as json',
187+
async () => {
188+
const state = getFixture()
189+
for (let index = 0; index < 10; index++) {
190+
await state.runner.run(['load', '@bench/query#query/cache', '--json'])
191+
}
192+
},
193+
createBenchOptions(setup, teardown),
194+
)
195+
196+
bench(
197+
'loads a direct dependency from a large workspace',
198+
async () => {
199+
const state = getFixture()
200+
await runInCwd(state.workspaceRoot, async () => {
201+
for (let index = 0; index < 10; index++) {
202+
await state.runner.run(['load', '@bench/query#query/cache', '--path'])
203+
}
204+
})
205+
},
206+
createBenchOptions(setup, teardown),
207+
)
208+
})

docs/cli/intent-list.md

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ id: intent-list
66
`intent list` discovers skill-enabled packages and prints available skills.
77

88
```bash
9-
npx @tanstack/intent@latest list [--json] [--global] [--global-only]
9+
npx @tanstack/intent@latest list [--json] [--debug] [--exclude <pattern>] [--global] [--global-only]
1010
```
1111

1212
## Options
1313

1414
- `--json`: print JSON instead of text output
15+
- `--debug`: print discovery debug details to stderr
16+
- `--exclude <pattern>`: exclude package names matching a simple glob; can be passed more than once
1517
- `--global`: include global packages after project packages
1618
- `--global-only`: list global packages only
1719

@@ -20,44 +22,43 @@ npx @tanstack/intent@latest list [--json] [--global] [--global-only]
2022
- Scans project and workspace dependencies for intent-enabled packages and skills
2123
- Includes global packages only when `--global` or `--global-only` is passed
2224
- Includes warnings from discovery
25+
- Excludes packages matched by package.json `intent.exclude` or `--exclude`
26+
- Prints debug details to stderr when `--debug` is passed
2327
- If no packages are discovered, prints `No intent-enabled packages found.`
24-
- Summary line with package count, skill count, and detected package manager
25-
- Package table columns: `PACKAGE`, `SOURCE`, `VERSION`, `SKILLS`, `REQUIRES`
28+
- Summary line with package count and skill count
29+
- Package table columns: `PACKAGE`, `SOURCE`, `VERSION`, `SKILLS`
2630
- Skill tree grouped by package
2731
- Optional warnings section (`⚠ ...` per warning)
2832

29-
`REQUIRES` uses `intent.requires` values joined by a comma and space; empty values render as ``.
3033
`SOURCE` is a lightweight indicator showing whether the selected package came from local discovery or explicit global scanning.
3134
When both local and global packages are scanned, local packages take precedence.
3235

3336
## JSON output
3437

35-
`--json` prints the `ScanResult` object:
38+
`--json` prints an adapter-friendly skill list:
3639

3740
```json
3841
{
39-
"packageManager": "npm | pnpm | yarn | bun | unknown",
42+
"skills": [
43+
{
44+
"use": "@tanstack/query#fetching",
45+
"packageName": "@tanstack/query",
46+
"packageRoot": "/path/to/project/node_modules/@tanstack/query",
47+
"packageVersion": "5.0.0",
48+
"packageSource": "local",
49+
"skillName": "fetching",
50+
"description": "Query data fetching patterns",
51+
"type": "skill (optional)",
52+
"framework": "react (optional)"
53+
}
54+
],
4055
"packages": [
4156
{
42-
"name": "string",
43-
"version": "string",
44-
"source": "local | global",
45-
"packageRoot": "string",
46-
"intent": {
47-
"version": 1,
48-
"repo": "string",
49-
"docs": "string",
50-
"requires": ["string"]
51-
},
52-
"skills": [
53-
{
54-
"name": "string",
55-
"path": "string",
56-
"description": "string",
57-
"type": "string (optional)",
58-
"framework": "string (optional)"
59-
}
60-
]
57+
"name": "@tanstack/query",
58+
"version": "5.0.0",
59+
"source": "local",
60+
"packageRoot": "/path/to/project/node_modules/@tanstack/query",
61+
"skillCount": 1
6162
}
6263
],
6364
"warnings": ["string"],
@@ -75,28 +76,27 @@ When both local and global packages are scanned, local packages take precedence.
7576
}
7677
]
7778
}
78-
],
79-
"nodeModules": {
80-
"local": {
81-
"path": "string | null",
82-
"detected": true,
83-
"exists": true,
84-
"scanned": true
85-
},
86-
"global": {
87-
"path": "string | null",
88-
"detected": true,
89-
"exists": true,
90-
"scanned": false,
91-
"source": "string (optional)"
92-
}
93-
}
79+
]
9480
}
9581
```
9682

97-
`packages` are ordered using `intent.requires` when possible.
9883
When the same package exists both locally and globally and global scanning is enabled, `intent list` prefers the local package.
99-
When project `node_modules` exists, `intent list` scans it. In Yarn PnP projects without `node_modules`, `intent list` uses Yarn's PnP API.
84+
When project `node_modules` exists, `intent list` scans it. In Yarn PnP projects without usable `node_modules`, `intent list` uses Yarn's PnP API.
85+
86+
## Excludes
87+
88+
Package excludes are hard filters for packages that should not be used in a repo.
89+
Intent reads `intent.exclude` arrays from package.json files while walking from the workspace or project root to the current working directory, then appends any `--exclude` flags.
90+
91+
```json
92+
{
93+
"intent": {
94+
"exclude": ["@tanstack/*devtools*"]
95+
}
96+
}
97+
```
98+
99+
Exclude patterns match full package names. In v1, only exact names and `*` wildcards are supported.
100100

101101
## Common errors
102102

docs/cli/intent-load.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ id: intent-load
66
`intent load` loads a compact skill identity from the current install and prints the matching `SKILL.md` content.
77

88
```bash
9-
npx @tanstack/intent@latest load <package>#<skill> [--path] [--json] [--global] [--global-only]
9+
npx @tanstack/intent@latest load <package>#<skill> [--path] [--json] [--debug] [--exclude <pattern>] [--global] [--global-only]
1010
```
1111

1212
## Options
1313

1414
- `--path`: print the resolved skill path instead of the file content
1515
- `--json`: print structured JSON with metadata and content
16+
- `--debug`: print resolution debug details to stderr
17+
- `--exclude <pattern>`: exclude package names matching a simple glob; can be passed more than once
1618
- `--global`: load from project packages first, then global packages
1719
- `--global-only`: load from global packages only
1820

@@ -21,9 +23,12 @@ npx @tanstack/intent@latest load <package>#<skill> [--path] [--json] [--global]
2123
- Validates `<package>#<skill>` before scanning
2224
- Scans project-local packages by default
2325
- Includes global packages only when `--global` or `--global-only` is passed
26+
- Fails before scanning when the target package matches package.json `intent.exclude` or `--exclude`
2427
- Prefers local packages when `--global` is used and the same package exists locally and globally
28+
- Accepts an unambiguous short skill name when a package-prefixed skill exists
2529
- Prints raw `SKILL.md` content by default
2630
- Prints the scanner-reported path when `--path` is passed
31+
- Prints debug details to stderr when `--debug` is passed
2732

2833
The package can be scoped or unscoped. The skill can include slash-separated sub-skill names.
2934

@@ -32,6 +37,7 @@ Examples:
3237
```bash
3338
npx @tanstack/intent@latest load @tanstack/query#fetching
3439
npx @tanstack/intent@latest load @tanstack/query#core/fetching
40+
npx @tanstack/intent@latest load @tanstack/router-core#auth-and-guards
3541
npx @tanstack/intent@latest load some-lib#core --path
3642
```
3743

@@ -59,6 +65,8 @@ npx @tanstack/intent@latest load some-lib#core --path
5965
- Empty skill: `Invalid skill use "@tanstack/query#": skill is required.`
6066
- Missing package: `Cannot resolve skill use "...": package "..." was not found.`
6167
- Missing skill: `Cannot resolve skill use "...": skill "..." was not found in package "...".`
68+
- Skill suggestion: `Did you mean @tanstack/router-core#router-core/auth-and-guards?`
69+
- Excluded package: `Cannot load skill use "...": package "..." is excluded by Intent configuration.`
6270

6371
## Related
6472

0 commit comments

Comments
 (0)