-
-
Notifications
You must be signed in to change notification settings - Fork 7
feat: add dependency-walked discovery and source-aware package reporting #112
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
base: main
Are you sure you want to change the base?
Changes from all commits
bd18e63
4e956d3
2913fba
77b453a
649ce2b
bf6018d
562b262
d18d9c0
3124fc5
ac8a0f1
4467812
513a263
f92a818
016ffde
36a2803
4aa03c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ id: overview | |
|
|
||
| `@tanstack/intent` is a CLI for shipping and consuming Agent Skills as package artifacts. | ||
|
|
||
| Skills are markdown documents that teach AI coding agents how to use your library correctly. Intent versions them with your releases, ships them inside npm packages, discovers them from local and accessible global `node_modules`, and helps agents load them automatically when working on matching tasks. | ||
| Skills are markdown documents that teach AI coding agents how to use your library correctly. Intent versions them with your releases, ships them inside npm packages, discovers them from your project and workspace by default, and helps agents load them automatically when working on matching tasks. | ||
|
Contributor
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. Use proper casing for “Markdown”. Use 🧰 Tools🪛 LanguageTool[uncategorized] ~8-~8: Did you mean the formatting language “Markdown” (= proper noun)? Skills are markdown documents that teach AI coding agents h... (MARKDOWN_NNP) 🤖 Prompt for AI Agents |
||
|
|
||
| ## What Intent does | ||
|
|
||
|
|
@@ -30,7 +30,7 @@ Intent provides tooling for two workflows: | |
| npx @tanstack/intent@latest list | ||
| ``` | ||
|
|
||
| Scans local `node_modules` and any accessible global `node_modules` for intent-enabled packages, preferring local packages when both exist. | ||
| Scans the current project's `node_modules` and workspace dependencies for intent-enabled packages. The CLI intentionally includes accessible global packages for this command and still prefers local packages when both exist. | ||
|
|
||
| ```bash | ||
| npx @tanstack/intent@latest install | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { createPackageRegistrar } from './register.js' | ||
| export { createDependencyWalker } from './walk.js' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import { existsSync } from 'node:fs' | ||
| import { join, relative, sep } from 'node:path' | ||
| import { listNodeModulesPackageDirs, toPosixPath } from '../utils.js' | ||
| import type { | ||
| IntentConfig, | ||
| IntentPackage, | ||
| NodeModulesScanTarget, | ||
| SkillEntry, | ||
| } from '../types.js' | ||
|
|
||
| type PackageJson = Record<string, unknown> | ||
|
|
||
| export interface CreatePackageRegistrarOptions { | ||
| comparePackageVersions: (a: string, b: string) => number | ||
| deriveIntentConfig: (pkgJson: PackageJson) => IntentConfig | null | ||
| discoverSkills: (skillsDir: string, baseName: string) => Array<SkillEntry> | ||
| getPackageDepth: (packageRoot: string, projectRoot: string) => number | ||
| packageIndexes: Map<string, number> | ||
| packages: Array<IntentPackage> | ||
| projectRoot: string | ||
| readPkgJson: (dirPath: string) => PackageJson | null | ||
| rememberVariant: (pkg: IntentPackage) => void | ||
| validateIntentField: (pkgName: string, intent: unknown) => IntentConfig | null | ||
| warnings: Array<string> | ||
| } | ||
|
|
||
| export function createPackageRegistrar(opts: CreatePackageRegistrarOptions) { | ||
| function scanTarget( | ||
| target: NodeModulesScanTarget, | ||
| source: IntentPackage['source'] = 'local', | ||
| ): void { | ||
| if (!target.path || !target.exists || target.scanned) return | ||
| target.scanned = true | ||
|
|
||
| for (const dirPath of listNodeModulesPackageDirs(target.path)) { | ||
| tryRegister(dirPath, 'unknown', source) | ||
| } | ||
| } | ||
|
|
||
| function tryRegister( | ||
| dirPath: string, | ||
| fallbackName: string, | ||
| source: IntentPackage['source'] = 'local', | ||
| ): boolean { | ||
| const skillsDir = join(dirPath, 'skills') | ||
| if (!existsSync(skillsDir)) return false | ||
|
|
||
| const pkgJson = opts.readPkgJson(dirPath) | ||
| if (!pkgJson) { | ||
| opts.warnings.push(`Could not read package.json for ${dirPath}`) | ||
| return false | ||
| } | ||
|
|
||
| const name = typeof pkgJson.name === 'string' ? pkgJson.name : fallbackName | ||
| const version = | ||
| typeof pkgJson.version === 'string' ? pkgJson.version : '0.0.0' | ||
| const intent = | ||
| opts.validateIntentField(name, pkgJson.intent) ?? | ||
| opts.deriveIntentConfig(pkgJson) | ||
| if (!intent) { | ||
| opts.warnings.push( | ||
| `${name} has a skills/ directory but could not determine repo/docs from package.json (add a "repository" field or explicit "intent" config)`, | ||
| ) | ||
| return false | ||
| } | ||
|
|
||
| const skills = opts.discoverSkills(skillsDir, name) | ||
|
|
||
| const isLocal = | ||
| dirPath.startsWith(opts.projectRoot + sep) || | ||
| dirPath.startsWith(opts.projectRoot + '/') | ||
| if (isLocal) { | ||
| const hasStableSymlink = | ||
| name !== '' && existsSync(join(opts.projectRoot, 'node_modules', name)) | ||
| for (const skill of skills) { | ||
| if (hasStableSymlink) { | ||
| const relFromPkg = toPosixPath(relative(dirPath, skill.path)) | ||
| skill.path = `node_modules/${name}/${relFromPkg}` | ||
| } else { | ||
| skill.path = toPosixPath(relative(opts.projectRoot, skill.path)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const candidate: IntentPackage = { | ||
| name, | ||
| version, | ||
| intent, | ||
| skills, | ||
| packageRoot: dirPath, | ||
| source, | ||
| } | ||
| const existingIndex = opts.packageIndexes.get(name) | ||
| if (existingIndex === undefined) { | ||
| opts.rememberVariant(candidate) | ||
| opts.packageIndexes.set(name, opts.packages.push(candidate) - 1) | ||
| return true | ||
| } | ||
|
|
||
| const existing = opts.packages[existingIndex]! | ||
| if (existing.packageRoot === candidate.packageRoot) { | ||
| return false | ||
| } | ||
|
|
||
| opts.rememberVariant(existing) | ||
| opts.rememberVariant(candidate) | ||
|
|
||
| const existingDepth = opts.getPackageDepth( | ||
| existing.packageRoot, | ||
| opts.projectRoot, | ||
| ) | ||
| const candidateDepth = opts.getPackageDepth( | ||
| candidate.packageRoot, | ||
| opts.projectRoot, | ||
| ) | ||
| const shouldReplace = | ||
| candidateDepth < existingDepth || | ||
| (candidateDepth === existingDepth && | ||
| opts.comparePackageVersions(candidate.version, existing.version) > 0) | ||
|
|
||
| if (shouldReplace) { | ||
| opts.packages[existingIndex] = candidate | ||
| } | ||
|
Comment on lines
+108
to
+123
Contributor
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. Enforce local-over-global precedence before depth/version tie-breaking. The replacement rule ignores 💡 Suggested fix const shouldReplace =
- candidateDepth < existingDepth ||
- (candidateDepth === existingDepth &&
- opts.comparePackageVersions(candidate.version, existing.version) > 0)
+ existing.source === 'global' && candidate.source === 'local'
+ ? true
+ : existing.source === 'local' && candidate.source === 'global'
+ ? false
+ : candidateDepth < existingDepth ||
+ (candidateDepth === existingDepth &&
+ opts.comparePackageVersions(
+ candidate.version,
+ existing.version,
+ ) > 0)🤖 Prompt for AI Agents |
||
|
|
||
| return true | ||
| } | ||
|
|
||
| return { scanTarget, tryRegister } | ||
| } | ||
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.
The discovery scope text is still narrower than the implementation.
scanForIntents()walks workspace packages directly, not just dependencies, so this wording undersells whatintent listcan return.📝 Suggested wording
🤖 Prompt for AI Agents