Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions .changeset/five-lizards-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/intent': patch
---

Add the `@tanstack/intent/core` entrypoint for programmatic skill discovery and loading.

`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.
208 changes: 208 additions & 0 deletions benchmarks/intent/load.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { rmSync } from 'node:fs'
import { join } from 'node:path'
import { afterAll, beforeAll, bench, describe } from 'vitest'
import {
createBenchOptions,
createCliRunner,
createConsoleSilencer,
createTempDir,
writeFile,
writeJson,
writePackage,
} from './helpers.js'

type LoadFixture = {
root: string
runner: ReturnType<typeof createCliRunner>
workspaceRoot: string
}

const consoleSilencer = createConsoleSilencer()
let fixture: LoadFixture | null = null

function createFixture(): LoadFixture {
const root = createTempDir('load')
const workspaceRoot = createTempDir('load-workspace')

writeLoadProject(root)
writeLargeWorkspaceProject(workspaceRoot)

return {
root,
runner: createCliRunner({ cwd: root }),
workspaceRoot,
Comment thread
LadyBluenotes marked this conversation as resolved.
}
}

function writeLoadProject(root: string): void {
writeJson(join(root, 'package.json'), {
name: 'intent-load-benchmark',
private: true,
dependencies: {
'@bench/query': '1.0.0',
},
})

writePackage(join(root, 'node_modules'), '@bench/query', '1.0.0', {
skills: ['query/core', 'query/cache', 'query/testing'],
})

writeQueryCacheContent(join(root, 'node_modules', '@bench', 'query'))
}

function writeLargeWorkspaceProject(root: string): void {
writeJson(join(root, 'package.json'), {
name: 'intent-large-workspace-load-benchmark',
private: true,
workspaces: ['packages/*'],
dependencies: {
'@bench/query': '1.0.0',
},
})
writeFile(join(root, 'pnpm-workspace.yaml'), "packages:\n - 'packages/*'\n")

for (let index = 0; index < 120; index++) {
writeJson(join(root, 'packages', `pkg-${index}`, 'package.json'), {
name: `@bench/workspace-pkg-${index}`,
version: '1.0.0',
dependencies:
index % 10 === 0
? {
'@bench/query': '1.0.0',
}
: undefined,
})
}

writePackage(join(root, 'node_modules'), '@bench/query', '1.0.0', {
skills: ['query/core', 'query/cache', 'query/testing'],
})

writeQueryCacheContent(join(root, 'node_modules', '@bench', 'query'))
}

function writeQueryCacheContent(packageRoot: string): void {
writeFile(
join(packageRoot, 'docs', 'cache-guide.md'),
'# Cache guide\n\nUse the cache workflow for repeated queries.\n',
)
writeFile(
join(packageRoot, 'assets', 'cache.txt'),
'cache diagram placeholder\n',
)
writeFile(
join(packageRoot, 'skills', 'query', 'cache', 'setup.md'),
'# Cache setup\n\nConfigure query cache defaults.\n',
)
writeFile(
join(packageRoot, 'skills', 'query', 'cache', 'SKILL.md'),
[
'---',
'name: "query/cache"',
'description: "query/cache benchmark guidance"',
'type: "framework"',
'requires:',
' - "query"',
'---',
'',
'# Query Cache',
'',
'See [cache guide](../../../docs/cache-guide.md).',
'Use [local setup](setup.md#configure).',
'![Cache diagram](../../../assets/cache.txt)',
'',
'```md',
'[ignored code link](setup.md)',
'```',
'',
...Array.from(
{ length: 20 },
(_, index) =>
`${index + 1}. Keep cache guidance aligned with [setup](setup.md) and [guide](../../../docs/cache-guide.md#cache).`,
),
'',
].join('\n'),
)
}

function getFixture(): LoadFixture {
if (!fixture) {
consoleSilencer.silence()
try {
fixture = createFixture()
} catch (err) {
consoleSilencer.restore()
throw err
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return fixture
}

async function setup(): Promise<void> {
await getFixture().runner.setup()
}

function teardown(): void {
if (fixture) {
fixture.runner.teardown()
rmSync(fixture.root, { recursive: true, force: true })
rmSync(fixture.workspaceRoot, { recursive: true, force: true })
fixture = null
}

consoleSilencer.restore()
}

async function runInCwd(
cwd: string,
callback: () => Promise<void>,
): Promise<void> {
const previousCwd = process.cwd()
process.chdir(cwd)
try {
await callback()
} finally {
process.chdir(previousCwd)
}
}

describe('intent load', () => {
beforeAll(setup)
afterAll(teardown)

bench(
'loads a direct dependency skill',
async () => {
const state = getFixture()
for (let index = 0; index < 10; index++) {
await state.runner.run(['load', '@bench/query#query/cache', '--path'])
}
},
createBenchOptions(setup, teardown),
)

bench(
'loads direct dependency content as json',
async () => {
const state = getFixture()
for (let index = 0; index < 10; index++) {
await state.runner.run(['load', '@bench/query#query/cache', '--json'])
}
},
createBenchOptions(setup, teardown),
)

bench(
'loads a direct dependency from a large workspace',
async () => {
const state = getFixture()
await runInCwd(state.workspaceRoot, async () => {
for (let index = 0; index < 10; index++) {
await state.runner.run(['load', '@bench/query#query/cache', '--path'])
}
})
},
createBenchOptions(setup, teardown),
)
})
86 changes: 43 additions & 43 deletions docs/cli/intent-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ id: intent-list
`intent list` discovers skill-enabled packages and prints available skills.

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

## Options

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

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

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

## JSON output

`--json` prints the `ScanResult` object:
`--json` prints an adapter-friendly skill list:

```json
{
"packageManager": "npm | pnpm | yarn | bun | unknown",
"skills": [
{
"use": "@tanstack/query#fetching",
"packageName": "@tanstack/query",
"packageRoot": "/path/to/project/node_modules/@tanstack/query",
"packageVersion": "5.0.0",
"packageSource": "local",
"skillName": "fetching",
"description": "Query data fetching patterns",
"type": "skill (optional)",
"framework": "react (optional)"
}
],
"packages": [
{
"name": "string",
"version": "string",
"source": "local | global",
"packageRoot": "string",
"intent": {
"version": 1,
"repo": "string",
"docs": "string",
"requires": ["string"]
},
"skills": [
{
"name": "string",
"path": "string",
"description": "string",
"type": "string (optional)",
"framework": "string (optional)"
}
]
"name": "@tanstack/query",
"version": "5.0.0",
"source": "local",
"packageRoot": "/path/to/project/node_modules/@tanstack/query",
"skillCount": 1
}
],
"warnings": ["string"],
Expand All @@ -75,28 +76,27 @@ When both local and global packages are scanned, local packages take precedence.
}
]
}
],
"nodeModules": {
"local": {
"path": "string | null",
"detected": true,
"exists": true,
"scanned": true
},
"global": {
"path": "string | null",
"detected": true,
"exists": true,
"scanned": false,
"source": "string (optional)"
}
}
]
}
```

`packages` are ordered using `intent.requires` when possible.
When the same package exists both locally and globally and global scanning is enabled, `intent list` prefers the local package.
When project `node_modules` exists, `intent list` scans it. In Yarn PnP projects without `node_modules`, `intent list` uses Yarn's PnP API.
When project `node_modules` exists, `intent list` scans it. In Yarn PnP projects without usable `node_modules`, `intent list` uses Yarn's PnP API.

## Excludes

Package excludes are hard filters for packages that should not be used in a repo.
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.

```json
{
"intent": {
"exclude": ["@tanstack/*devtools*"]
}
}
```

Exclude patterns match full package names. In v1, only exact names and `*` wildcards are supported.

## Common errors

Expand Down
10 changes: 9 additions & 1 deletion docs/cli/intent-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ id: intent-load
`intent load` loads a compact skill identity from the current install and prints the matching `SKILL.md` content.

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

## Options

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

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

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

Expand All @@ -32,6 +37,7 @@ Examples:
```bash
npx @tanstack/intent@latest load @tanstack/query#fetching
npx @tanstack/intent@latest load @tanstack/query#core/fetching
npx @tanstack/intent@latest load @tanstack/router-core#auth-and-guards
npx @tanstack/intent@latest load some-lib#core --path
```

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

## Related

Expand Down
Loading
Loading