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

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

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

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

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'],
})

return {
root,
runner: createCliRunner({ cwd: root }),
}
}

function getFixture(): LoadFixture {
if (!fixture) {
consoleSilencer.silence()
fixture = createFixture()
}
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 })
fixture = null
}

consoleSilencer.restore()
}

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),
)
})
84 changes: 41 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,41 @@ 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",
"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",
"skillCount": 1
}
],
"warnings": ["string"],
Expand All @@ -75,28 +74,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
1 change: 1 addition & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"entry": [
"src/index.ts",
"src/cli.ts",
"src/core.ts",
"src/setup.ts",
"src/intent-library.ts",
"src/library-scanner.ts"
Expand Down
8 changes: 6 additions & 2 deletions packages/intent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"./intent-library": {
"import": "./dist/intent-library.mjs",
"types": "./dist/intent-library.d.mts"
},
"./core": {
"import": "./dist/core.mjs",
"types": "./dist/core.d.mts"
}
},
"bin": {
Expand All @@ -37,8 +41,8 @@
},
"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:smoke": "pnpm run build && node dist/cli.mjs --help > /dev/null",
"build": "tsdown src/index.ts src/cli.ts src/setup.ts src/intent-library.ts src/library-scanner.ts src/core.ts --format esm --dts",
"test:smoke": "pnpm run build && node dist/cli.mjs --help > /dev/null && node dist/cli.mjs load --help > /dev/null",
"test:lib": "vitest run --exclude 'tests/integration/**'",
"test:integration": "vitest run tests/integration/",
"test:types": "tsc --noEmit"
Expand Down
40 changes: 40 additions & 0 deletions packages/intent/src/cli-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { fail } from './cli-error.js'
import { resolveProjectContext } from './core/project-context.js'
import type { IntentCoreOptions } from './core.js'
import type { ScanOptions, ScanResult, StalenessReport } from './types.js'

export { printWarnings } from './cli-output.js'

export interface GlobalScanFlags {
debug?: boolean
exclude?: string | Array<string>
global?: boolean
globalOnly?: boolean
}
Expand Down Expand Up @@ -74,6 +77,43 @@ export function scanOptionsFromGlobalFlags(
return { scope: 'local' }
}

export function coreOptionsFromGlobalFlags(
options: GlobalScanFlags,
): IntentCoreOptions {
if (options.global && options.globalOnly) {
fail('Use either --global or --global-only, not both.')
}

return {
debug: options.debug,
exclude: Array.isArray(options.exclude)
? options.exclude
: options.exclude
? [options.exclude]
: undefined,
global: options.global,
globalOnly: options.globalOnly,
}
}

function formatDebugValue(value: string | number | Array<string>): string {
if (Array.isArray(value)) {
return value.length > 0 ? value.join(', ') : '(none)'
}

return String(value)
}

export function printDebugInfo(
title: string,
fields: Array<[label: string, value: string | number | Array<string>]>,
): void {
console.error(`Debug: ${title}`)
for (const [label, value] of fields) {
console.error(` ${label}: ${formatDebugValue(value)}`)
}
}

export async function resolveStaleTargets(
targetDir?: string,
): Promise<StaleTargetResult> {
Expand Down
12 changes: 10 additions & 2 deletions packages/intent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ function createCli(): CAC {
'list',
'Discover intent-enabled packages from the project or workspace',
)
.usage('list [--json] [--global] [--global-only]')
.usage(
'list [--json] [--debug] [--exclude <pattern>] [--global] [--global-only]',
)
.option('--json', 'Output JSON')
.option('--debug', 'Print discovery debug details to stderr')
.option('--exclude <pattern>', 'Exclude package name glob')
.option('--global', 'Include global packages after project packages')
.option('--global-only', 'List global packages only')
.example('list')
Expand All @@ -47,9 +51,13 @@ function createCli(): CAC {

cli
.command('load [use]', 'Load a compact skill use and print its SKILL.md')
.usage('load <use> [--path] [--json] [--global] [--global-only]')
.usage(
'load <use> [--path] [--json] [--debug] [--exclude <pattern>] [--global] [--global-only]',
)
.option('--path', 'Print the resolved skill path instead of file content')
.option('--json', 'Output JSON')
.option('--debug', 'Print resolution debug details to stderr')
.option('--exclude <pattern>', 'Exclude package name glob')
.option('--global', 'Load from project packages, then global packages')
.option('--global-only', 'Load from global packages only')
.example('load @tanstack/query#core')
Expand Down
Loading
Loading