From ef0d2f315d7f4f96df224189113bdd663996d78a Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 15 Mar 2026 10:43:04 -0600 Subject: [PATCH 1/6] feat: replace bin.intent detection with tanstack-intent keyword Replace the bin shim-based package detection in library-scanner with a simpler check for the "tanstack-intent" keyword in the keywords array. Remove the entire add-library-bin command and bin shim generation system, since the keyword already existed for registry discovery and now serves both purposes. Also fix collectPackagingWarnings to not warn about !skills/_artifacts in monorepo packages (matching edit-package-json behavior), and add dedicated test coverage for keyword addition in runEditPackageJson. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/cli/intent-scaffold.md | 1 - docs/cli/intent-setup.md | 19 +-- docs/cli/intent-validate.md | 4 +- .../quick-start-maintainers.md | 12 +- packages/intent/package.json | 2 +- packages/intent/src/cli.ts | 66 ++++---- packages/intent/src/index.ts | 2 - packages/intent/src/library-scanner.ts | 10 +- packages/intent/src/setup.ts | 146 +----------------- packages/intent/tests/cli.test.ts | 6 +- packages/intent/tests/library-scanner.test.ts | 72 ++++++--- packages/intent/tests/setup.test.ts | 145 ++++------------- 12 files changed, 136 insertions(+), 349 deletions(-) diff --git a/docs/cli/intent-scaffold.md b/docs/cli/intent-scaffold.md index eceb0e7..d58b8af 100644 --- a/docs/cli/intent-scaffold.md +++ b/docs/cli/intent-scaffold.md @@ -30,7 +30,6 @@ The prompt also includes a post-generation checklist: - Commit generated `skills/` and `skills/_artifacts/` - Ensure `@tanstack/intent` is in `devDependencies` - Run setup commands as needed: - - `npx @tanstack/intent@latest add-library-bin` - `npx @tanstack/intent@latest edit-package-json` - `npx @tanstack/intent@latest setup-github-actions` diff --git a/docs/cli/intent-setup.md b/docs/cli/intent-setup.md index 953ecfc..2effee9 100644 --- a/docs/cli/intent-setup.md +++ b/docs/cli/intent-setup.md @@ -3,32 +3,25 @@ title: setup commands id: intent-setup --- -Intent exposes setup as three separate commands. +Intent exposes setup as two separate commands. ```bash -npx @tanstack/intent@latest add-library-bin npx @tanstack/intent@latest edit-package-json npx @tanstack/intent@latest setup-github-actions ``` ## Commands -- `add-library-bin`: create a package-local `intent` shim in `bin/` - `edit-package-json`: add or normalize `package.json` entries needed to publish skills - `setup-github-actions`: copy workflow templates to `.github/workflows` ## What each command changes -- `add-library-bin` - - Creates `bin/intent.js` when `package.json.type` is `module`, otherwise `bin/intent.mjs` - - If either shim already exists, command skips creation - - Shim imports `@tanstack/intent/intent-library` - `edit-package-json` - Requires a valid `package.json` in current directory - - Ensures `bin.intent` points to `./bin/intent.` + - Ensures `keywords` includes `tanstack-intent` - Ensures `files` includes required publish entries - - Preserves existing indentation and existing `bin` entries - - Converts string shorthand `bin` to object when needed + - Preserves existing indentation - `setup-github-actions` - Copies templates from `@tanstack/intent/meta/templates/workflows` to `.github/workflows` - Applies variable substitution for `PACKAGE_NAME`, `REPO`, `DOCS_PATH`, `SRC_PATH` @@ -38,8 +31,8 @@ npx @tanstack/intent@latest setup-github-actions `edit-package-json` enforces different `files` sets based on package location: -- Monorepo package: `skills`, `bin` -- Non-monorepo package: `skills`, `bin`, `!skills/_artifacts` +- Monorepo package: `skills` +- Non-monorepo package: `skills`, `!skills/_artifacts` ## Common errors @@ -48,7 +41,7 @@ npx @tanstack/intent@latest setup-github-actions ## Notes -- `add-library-bin` and `setup-github-actions` skip existing files +- `setup-github-actions` skips existing files ## Related diff --git a/docs/cli/intent-validate.md b/docs/cli/intent-validate.md index 8e4b292..4b27e13 100644 --- a/docs/cli/intent-validate.md +++ b/docs/cli/intent-validate.md @@ -37,11 +37,9 @@ If `/_artifacts` exists, it also validates artifacts: Packaging warnings are always computed from `package.json` in the current working directory: - `@tanstack/intent` missing from `devDependencies` -- Missing `bin.intent` entry -- Missing shim (`bin/intent.js` or `bin/intent.mjs`) +- Missing `tanstack-intent` in keywords array - Missing `files` entries when `files` array exists: - `skills` - - `bin` - `!skills/_artifacts` Warnings are informational; they are printed on both pass and fail paths. diff --git a/docs/getting-started/quick-start-maintainers.md b/docs/getting-started/quick-start-maintainers.md index f310aa1..b24cf46 100644 --- a/docs/getting-started/quick-start-maintainers.md +++ b/docs/getting-started/quick-start-maintainers.md @@ -22,9 +22,6 @@ Or run commands without installing: npx @tanstack/intent@latest scaffold ``` -> [!WARNING] -> When using `npx` or `bunx`, always include `@latest`. Intent-enabled libraries ship a local `intent` binary shim, and without `@latest`, your package manager may resolve to that shim instead of the real CLI. - --- ## Initial Setup (With Agent) @@ -101,9 +98,6 @@ Artifacts enforce a consistent skill structure across versions, making it easier Run these commands to prepare your package for skill publishing: ```bash -# Generate the bin shim that consumers use for discovery -npx @tanstack/intent@latest add-library-bin - # Update package.json with required fields npx @tanstack/intent@latest edit-package-json @@ -113,11 +107,9 @@ npx @tanstack/intent@latest setup-github-actions **What these do:** -- `add-library-bin` creates `bin/intent.js` or `bin/intent.mjs` — a shim that lets consumers run `npx your-package intent` to access Intent CLI features - `edit-package-json` adds: - - `intent` field in package.json with version, repo, and docs metadata - - `bin.intent` entry pointing to the shim - - `files` array entries for `skills/` and `bin/` + - `tanstack-intent` keyword (used for package detection and registry discovery) + - `files` array entries for `skills/` - For single packages: also adds `!skills/_artifacts` to exclude artifacts from npm - For monorepos: skips the artifacts exclusion (artifacts live at repo root) - `setup-github-actions` copies workflow templates to `.github/workflows/` for automated validation and staleness checking diff --git a/packages/intent/package.json b/packages/intent/package.json index c10c621..4f18e25 100644 --- a/packages/intent/package.json +++ b/packages/intent/package.json @@ -1,6 +1,6 @@ { "name": "@tanstack/intent", - "version": "0.0.19", + "version": "0.0.20", "description": "Ship compositional knowledge for AI coding agents alongside your npm packages", "license": "MIT", "type": "module", diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index bac08b4..88cb76f 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -244,17 +244,9 @@ function collectPackagingWarnings(root: string): Array { warnings.push('@tanstack/intent is not in devDependencies') } - const bin = pkgJson.bin as Record | undefined - if (!bin?.intent) { - warnings.push('Missing "bin": { "intent": ... } entry in package.json') - } - - const shimJs = join(root, 'bin', 'intent.js') - const shimMjs = join(root, 'bin', 'intent.mjs') - if (!existsSync(shimJs) && !existsSync(shimMjs)) { - warnings.push( - 'No bin/intent.js or bin/intent.mjs shim found (run: npx @tanstack/intent add-library-bin)', - ) + const keywords = pkgJson.keywords + if (!Array.isArray(keywords) || !keywords.includes('tanstack-intent')) { + warnings.push('Missing "tanstack-intent" in keywords array') } const files = pkgJson.files as Array | undefined @@ -264,12 +256,30 @@ function collectPackagingWarnings(root: string): Array { '"skills" is not in the "files" array — skills won\'t be published', ) } - if (!files.includes('bin')) { - warnings.push( - '"bin" is not in the "files" array — shim won\'t be published', - ) - } - if (!files.includes('!skills/_artifacts')) { + + // Only warn about !skills/_artifacts for non-monorepo packages. + // In monorepos, artifacts live at the repo root, so the negation + // pattern is intentionally omitted by edit-package-json. + const isMonorepoPkg = (() => { + let dir = join(root, '..') + for (let i = 0; i < 5; i++) { + const parentPkg = join(dir, 'package.json') + if (existsSync(parentPkg)) { + try { + const parent = JSON.parse(readFileSync(parentPkg, 'utf8')) + return Array.isArray(parent.workspaces) || parent.workspaces?.packages + } catch { + return false + } + } + const next = dirname(dir) + if (next === dir) break + dir = next + } + return false + })() + + if (!isMonorepoPkg && !files.includes('!skills/_artifacts')) { warnings.push( '"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily', ) @@ -557,11 +567,10 @@ This produces: individual SKILL.md files. 1. Run \`intent validate\` in each package directory 2. Commit skills/ and artifacts -3. For each publishable package, run: \`npx @tanstack/intent add-library-bin\` -4. For each publishable package, run: \`npx @tanstack/intent edit-package-json\` -5. Ensure each package has \`@tanstack/intent\` as a devDependency -6. Create a \`skill:\` label on the GitHub repo for each skill (use \`gh label create\`) -7. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent@latest install\`" +3. For each publishable package, run: \`npx @tanstack/intent edit-package-json\` +4. Ensure each package has \`@tanstack/intent\` as a devDependency +5. Create a \`skill:\` label on the GitHub repo for each skill (use \`gh label create\`) +6. Add a README note: "If you use an AI agent, run \`npx @tanstack/intent@latest install\`" ` console.log(prompt) @@ -579,8 +588,7 @@ Usage: intent validate [] Validate skill files (default: skills/) intent install Print a skill that guides your coding agent to set up skill-to-task mappings intent scaffold Print maintainer scaffold prompt - intent add-library-bin Generate bin/intent.{js,mjs} bridge file - intent edit-package-json Wire package.json (files, bin) for skill publishing + intent edit-package-json Wire package.json (files, keywords) for skill publishing intent setup-github-actions Copy CI workflow templates to .github/workflows/ intent stale [dir] [--json] Check skills for staleness` @@ -618,12 +626,9 @@ Examples: intent stale intent stale packages/query intent stale --json`, - 'add-library-bin': `intent add-library-bin - -Generate bin/intent.{js,mjs} bridge files for publishable packages.`, 'edit-package-json': `intent edit-package-json -Update package.json files so skills and shims are published.`, +Update package.json files so skills are published.`, 'setup-github-actions': `intent setup-github-actions Copy Intent CI workflow templates into .github/workflows/.`, @@ -719,11 +724,6 @@ export async function main(argv: Array = process.argv.slice(2)) { } return 0 } - case 'add-library-bin': { - const { runAddLibraryBinAll } = await import('./setup.js') - runAddLibraryBinAll(process.cwd()) - return 0 - } case 'edit-package-json': { const { runEditPackageJsonAll } = await import('./setup.js') runEditPackageJsonAll(process.cwd()) diff --git a/packages/intent/src/index.ts b/packages/intent/src/index.ts index 1c943a4..eb0bb4a 100644 --- a/packages/intent/src/index.ts +++ b/packages/intent/src/index.ts @@ -18,12 +18,10 @@ export { resolveDepDir, } from './utils.js' export { - runAddLibraryBin, runEditPackageJson, runSetupGithubActions, } from './setup.js' export type { - AddLibraryBinResult, EditPackageJsonResult, SetupGithubActionsResult, } from './setup.js' diff --git a/packages/intent/src/library-scanner.ts b/packages/intent/src/library-scanner.ts index d91d00d..4e9268a 100644 --- a/packages/intent/src/library-scanner.ts +++ b/packages/intent/src/library-scanner.ts @@ -42,10 +42,10 @@ function findHomeDir(scriptPath: string): string | null { } } -function hasIntentBin(pkg: Record): boolean { - const bin = pkg.bin - if (!bin || typeof bin !== 'object') return false - return 'intent' in (bin as Record) +function hasIntentKeyword(pkg: Record): boolean { + const keywords = pkg.keywords + if (!Array.isArray(keywords)) return false + return keywords.includes('tanstack-intent') } function discoverSkills(skillsDir: string): Array { @@ -134,7 +134,7 @@ export async function scanLibrary( const depDir = resolveDepDir(depName, dir) if (!depDir) continue const depPkg = readPkgJson(depDir) - if (depPkg && hasIntentBin(depPkg)) { + if (depPkg && hasIntentKeyword(depPkg)) { processPackage(depName, depDir) } } diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index 92e0004..8ff32f0 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -13,11 +13,6 @@ import { findSkillFiles } from './utils.js' // Types // --------------------------------------------------------------------------- -export interface AddLibraryBinResult { - shim: string | null - skipped: string | null -} - export interface EditPackageJsonResult { added: Array alreadyPresent: Array @@ -220,91 +215,6 @@ function copyTemplates( return { copied, skipped } } -// --------------------------------------------------------------------------- -// Shim generation helpers -// --------------------------------------------------------------------------- - -function getShimContent(ext: string): string { - return `#!/usr/bin/env node -// Auto-generated by @tanstack/intent setup -// Exposes the intent end-user CLI for consumers of this library. -// Commit this file, then add to your package.json: -// "bin": { "intent": "./bin/intent.${ext}" } -try { - await import('@tanstack/intent/intent-library') -} catch (e) { - if (e?.code === 'ERR_MODULE_NOT_FOUND' || e?.code === 'MODULE_NOT_FOUND') { - console.error('@tanstack/intent is not installed.') - console.error('') - console.error('Install it as a dev dependency:') - console.error(' npm add -D @tanstack/intent') - console.error('') - console.error('Or run directly:') - console.error(' npx @tanstack/intent@latest list') - process.exit(1) - } - throw e -} -` -} - -function detectShimExtension(root: string): string { - try { - const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) - if (pkg.type === 'module') return 'js' - } catch (err: unknown) { - const isNotFound = - err && - typeof err === 'object' && - 'code' in err && - (err as NodeJS.ErrnoException).code === 'ENOENT' - if (!isNotFound) { - console.error( - `Warning: could not read package.json: ${err instanceof Error ? err.message : err}`, - ) - } - } - return 'mjs' -} - -function findExistingShim(root: string): string | null { - const shimJs = join(root, 'bin', 'intent.js') - if (existsSync(shimJs)) return shimJs - - const shimMjs = join(root, 'bin', 'intent.mjs') - if (existsSync(shimMjs)) return shimMjs - - return null -} - -// --------------------------------------------------------------------------- -// Command: add-library-bin -// --------------------------------------------------------------------------- - -export function runAddLibraryBin(root: string): AddLibraryBinResult { - const result: AddLibraryBinResult = { shim: null, skipped: null } - - const existingShim = findExistingShim(root) - if (existingShim) { - result.skipped = existingShim - console.log(` Already exists: ${existingShim}`) - return result - } - - const ext = detectShimExtension(root) - const shimPath = join(root, 'bin', `intent.${ext}`) - mkdirSync(join(root, 'bin'), { recursive: true }) - writeFileSync(shimPath, getShimContent(ext)) - result.shim = shimPath - - console.log(`✓ Generated intent shim: ${shimPath}`) - console.log( - `\n Run \`npx @tanstack/intent edit-package-json\` to wire package.json.`, - ) - - return result -} - // --------------------------------------------------------------------------- // Command: edit-package-json // --------------------------------------------------------------------------- @@ -338,14 +248,11 @@ export function runEditPackageJson(root: string): EditPackageJsonResult { pkg.keywords = [] } const keywords = pkg.keywords as Array - const requiredKeywords = ['tanstack-intent'] - for (const kw of requiredKeywords) { - if (keywords.includes(kw)) { - result.alreadyPresent.push(`keywords: "${kw}"`) - } else { - keywords.push(kw) - result.added.push(`keywords: "${kw}"`) - } + if (keywords.includes('tanstack-intent')) { + result.alreadyPresent.push('keywords: "tanstack-intent"') + } else { + keywords.push('tanstack-intent') + result.added.push('keywords: "tanstack-intent"') } // --- files array --- @@ -377,8 +284,8 @@ export function runEditPackageJson(root: string): EditPackageJsonResult { return false })() const requiredFiles = isMonorepo - ? ['skills', 'bin'] - : ['skills', 'bin', '!skills/_artifacts'] + ? ['skills'] + : ['skills', '!skills/_artifacts'] for (const entry of requiredFiles) { if (files.includes(entry)) { @@ -389,39 +296,6 @@ export function runEditPackageJson(root: string): EditPackageJsonResult { } } - // --- bin field --- - const existingShim = findExistingShim(root) - let ext: string - if (existingShim) { - ext = existingShim.endsWith('.mjs') ? 'mjs' : 'js' - } else { - ext = pkg.type === 'module' ? 'js' : 'mjs' - } - const shimRelative = `./bin/intent.${ext}` - - if (typeof pkg.bin === 'object' && pkg.bin !== null) { - const binObj = pkg.bin as Record - if (binObj.intent) { - result.alreadyPresent.push(`bin.intent`) - } else { - binObj.intent = shimRelative - result.added.push(`bin.intent: "${shimRelative}"`) - } - } else if (!pkg.bin) { - pkg.bin = { intent: shimRelative } - result.added.push(`bin.intent: "${shimRelative}"`) - } else if (typeof pkg.bin === 'string') { - // npm string shorthand: "bin": "./cli.js" means { "": "./cli.js" } - const pkgName = - typeof pkg.name === 'string' - ? pkg.name.replace(/^@[^/]+\//, '') - : 'unknown' - pkg.bin = { [pkgName]: pkg.bin, intent: shimRelative } - result.added.push( - `bin.intent: "${shimRelative}" (converted bin from string to object)`, - ) - } - writeFileSync(pkgPath, JSON.stringify(pkg, null, indentSize) + '\n') // Print results @@ -612,12 +486,6 @@ export function runEditPackageJsonAll( return runForEachPackage(root, runEditPackageJson) } -export function runAddLibraryBinAll( - root: string, -): Array> | AddLibraryBinResult { - return runForEachPackage(root, runAddLibraryBin) -} - // --------------------------------------------------------------------------- // Command: setup-github-actions // --------------------------------------------------------------------------- diff --git a/packages/intent/tests/cli.test.ts b/packages/intent/tests/cli.test.ts index b34b43c..756f590 100644 --- a/packages/intent/tests/cli.test.ts +++ b/packages/intent/tests/cli.test.ts @@ -270,11 +270,9 @@ describe('cli commands', () => { writeJson(join(root, 'packages', 'router', 'package.json'), { name: '@tanstack/router', devDependencies: { '@tanstack/intent': '^0.0.18' }, - bin: { intent: './bin/intent.js' }, - files: ['skills', 'bin', '!skills/_artifacts'], + keywords: ['tanstack-intent'], + files: ['skills', '!skills/_artifacts'], }) - mkdirSync(join(root, 'packages', 'router', 'bin'), { recursive: true }) - writeFileSync(join(root, 'packages', 'router', 'bin', 'intent.js'), '') writeSkillMd(join(root, 'packages', 'router', 'skills', 'db-core'), { name: 'db-core', description: 'Core database concepts', diff --git a/packages/intent/tests/library-scanner.test.ts b/packages/intent/tests/library-scanner.test.ts index 0e36d97..95ff0dc 100644 --- a/packages/intent/tests/library-scanner.test.ts +++ b/packages/intent/tests/library-scanner.test.ts @@ -28,9 +28,9 @@ function writeSkillMd(dir: string, frontmatter: Record): void { ) } -// Simulate the script path as it would appear in a library's bin/intent.js shim. +// Construct a script path inside the package directory. // findHomeDir walks up from dirname(scriptPath) to find the nearest package.json. -function shimPath(pkgDir: string): string { +function scriptPath(pkgDir: string): string { return join(pkgDir, 'bin', 'intent.js') } @@ -59,7 +59,7 @@ describe('scanLibrary', () => { name: '@tanstack/router', version: '1.2.0', description: 'Type-safe router for React', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], }) const skillDir = createDir(pkgDir, 'skills', 'routing') writeSkillMd(skillDir, { @@ -67,7 +67,7 @@ describe('scanLibrary', () => { description: 'File-based route definitions', }) - const result = await scanLibrary(shimPath(pkgDir), root) + const result = await scanLibrary(scriptPath(pkgDir), root) expect(result.warnings).toEqual([]) expect(result.packages).toHaveLength(1) @@ -86,25 +86,25 @@ describe('scanLibrary', () => { writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', version: '1.0.0', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], }) const skillDir = createDir(pkgDir, 'skills', 'routing') writeSkillMd(skillDir, { name: 'routing', description: 'Routing patterns' }) - const result = await scanLibrary(shimPath(pkgDir), root) + const result = await scanLibrary(scriptPath(pkgDir), root) const skill = result.packages[0]!.skills[0]! expect(skill.path).toBe(join(pkgDir, 'skills', 'routing', 'SKILL.md')) }) - it('recursively discovers deps with bin.intent', async () => { + it('recursively discovers deps with tanstack-intent keyword', async () => { // Home package: @tanstack/router, depends on @tanstack/query const routerDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(routerDir, 'package.json'), { name: '@tanstack/router', version: '1.0.0', description: 'Router', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], dependencies: { '@tanstack/query': '^5.0.0' }, }) const routerSkill = createDir(routerDir, 'skills', 'routing') @@ -119,7 +119,7 @@ describe('scanLibrary', () => { name: '@tanstack/query', version: '5.0.0', description: 'Async state management', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], }) const querySkill = createDir(queryDir, 'skills', 'fetching') writeSkillMd(querySkill, { @@ -127,7 +127,7 @@ describe('scanLibrary', () => { description: 'Query and mutation patterns', }) - const result = await scanLibrary(shimPath(routerDir), root) + const result = await scanLibrary(scriptPath(routerDir), root) expect(result.warnings).toEqual([]) expect(result.packages).toHaveLength(2) @@ -146,7 +146,7 @@ describe('scanLibrary', () => { writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', version: '1.0.0', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], peerDependencies: { '@tanstack/query': '^5.0.0' }, }) @@ -154,23 +154,23 @@ describe('scanLibrary', () => { writeJson(join(queryDir, 'package.json'), { name: '@tanstack/query', version: '5.0.0', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], }) const querySkill = createDir(queryDir, 'skills', 'fetching') writeSkillMd(querySkill, { name: 'fetching', description: 'Fetching' }) - const result = await scanLibrary(shimPath(pkgDir), root) + const result = await scanLibrary(scriptPath(pkgDir), root) const names = result.packages.map((p) => p.name) expect(names).toContain('@tanstack/query') }) - it('skips deps without bin.intent', async () => { + it('skips deps without tanstack-intent keyword', async () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', version: '1.0.0', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], dependencies: { react: '^18.0.0' }, }) @@ -178,27 +178,51 @@ describe('scanLibrary', () => { writeJson(join(reactDir, 'package.json'), { name: 'react', version: '18.0.0', - // no bin.intent + // no tanstack-intent keyword }) const reactSkill = createDir(reactDir, 'skills', 'hooks') writeSkillMd(reactSkill, { name: 'hooks', description: 'React hooks' }) - const result = await scanLibrary(shimPath(pkgDir), root) + const result = await scanLibrary(scriptPath(pkgDir), root) const names = result.packages.map((p) => p.name) expect(names).not.toContain('react') }) + it('skips deps with other keywords but not tanstack-intent', async () => { + const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') + writeJson(join(pkgDir, 'package.json'), { + name: '@tanstack/router', + version: '1.0.0', + keywords: ['tanstack-intent'], + dependencies: { 'some-lib': '^1.0.0' }, + }) + + const libDir = createDir(root, 'node_modules', 'some-lib') + writeJson(join(libDir, 'package.json'), { + name: 'some-lib', + version: '1.0.0', + keywords: ['react', 'state-management'], + }) + const libSkill = createDir(libDir, 'skills', 'core') + writeSkillMd(libSkill, { name: 'core', description: 'Core patterns' }) + + const result = await scanLibrary(scriptPath(pkgDir), root) + + const names = result.packages.map((p) => p.name) + expect(names).not.toContain('some-lib') + }) + it('handles packages with no skills/ directory', async () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', version: '1.0.0', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], }) // No skills/ directory - const result = await scanLibrary(shimPath(pkgDir), root) + const result = await scanLibrary(scriptPath(pkgDir), root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.skills).toEqual([]) @@ -210,7 +234,7 @@ describe('scanLibrary', () => { writeJson(join(routerDir, 'package.json'), { name: '@tanstack/router', version: '1.0.0', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], dependencies: { '@tanstack/query': '^5.0.0' }, }) @@ -218,11 +242,11 @@ describe('scanLibrary', () => { writeJson(join(queryDir, 'package.json'), { name: '@tanstack/query', version: '5.0.0', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], dependencies: { '@tanstack/router': '^1.0.0' }, // circular back }) - const result = await scanLibrary(shimPath(routerDir), root) + const result = await scanLibrary(scriptPath(routerDir), root) // Each package appears exactly once const names = result.packages.map((p) => p.name) @@ -235,7 +259,7 @@ describe('scanLibrary', () => { writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', version: '1.0.0', - bin: { intent: './bin/intent.js' }, + keywords: ['tanstack-intent'], }) const routingDir = createDir(pkgDir, 'skills', 'routing') writeSkillMd(routingDir, { @@ -248,7 +272,7 @@ describe('scanLibrary', () => { description: 'Nested route patterns', }) - const result = await scanLibrary(shimPath(pkgDir), root) + const result = await scanLibrary(scriptPath(pkgDir), root) const skills = result.packages[0]!.skills expect(skills).toHaveLength(2) diff --git a/packages/intent/tests/setup.test.ts b/packages/intent/tests/setup.test.ts index 1975828..e0170ec 100644 --- a/packages/intent/tests/setup.test.ts +++ b/packages/intent/tests/setup.test.ts @@ -10,16 +10,13 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { - runAddLibraryBin, runEditPackageJson, runEditPackageJsonAll, - runAddLibraryBinAll, runSetupGithubActions, } from '../src/setup.js' import type { MonorepoResult, EditPackageJsonResult, - AddLibraryBinResult, } from '../src/setup.js' let root: string @@ -54,83 +51,54 @@ afterEach(() => { rmSync(root, { recursive: true, force: true }) }) -describe('runAddLibraryBin', () => { - it('generates bin/intent.js for type:module packages', () => { - writePkg({ name: 'test-pkg', type: 'module' }) - - const result = runAddLibraryBin(root) - expect(result.shim).toBe(join(root, 'bin', 'intent.js')) - expect(result.skipped).toBeNull() - expect(existsSync(join(root, 'bin', 'intent.js'))).toBe(true) - }) - - it('generates bin/intent.mjs for non-module packages', () => { - writePkg({ name: 'test-pkg' }) - - const result = runAddLibraryBin(root) - expect(result.shim).toBe(join(root, 'bin', 'intent.mjs')) - expect(existsSync(join(root, 'bin', 'intent.mjs'))).toBe(true) - }) - - it('skips if bin/intent.js already exists', () => { - mkdirSync(join(root, 'bin'), { recursive: true }) - writeFileSync(join(root, 'bin', 'intent.js'), 'existing') - - const result = runAddLibraryBin(root) - expect(result.shim).toBeNull() - expect(result.skipped).toBe(join(root, 'bin', 'intent.js')) - expect(readFileSync(join(root, 'bin', 'intent.js'), 'utf8')).toBe( - 'existing', - ) - }) - - it('skips if bin/intent.mjs already exists', () => { - mkdirSync(join(root, 'bin'), { recursive: true }) - writeFileSync(join(root, 'bin', 'intent.mjs'), 'existing') - - const result = runAddLibraryBin(root) - expect(result.shim).toBeNull() - expect(result.skipped).toBe(join(root, 'bin', 'intent.mjs')) - }) - - it('generates shim with correct content', () => { - writePkg({ name: 'test-pkg', type: 'module' }) - - runAddLibraryBin(root) - - const content = readFileSync(join(root, 'bin', 'intent.js'), 'utf8') - expect(content).toContain('#!/usr/bin/env node') - expect(content).toContain('@tanstack/intent/intent-library') - }) -}) - describe('runEditPackageJson', () => { - it('adds skills, bin, and !skills/_artifacts to files array', () => { + it('adds skills and !skills/_artifacts to files array', () => { writePkg({ name: 'test-pkg', files: ['dist', 'src'] }, 2) const result = runEditPackageJson(root) expect(result.added).toContain('files: "skills"') - expect(result.added).toContain('files: "bin"') expect(result.added).toContain('files: "!skills/_artifacts"') const pkg = readPkg() expect(pkg.files).toContain('skills') - expect(pkg.files).toContain('bin') expect(pkg.files).toContain('!skills/_artifacts') expect(pkg.files).toContain('dist') expect(pkg.files).toContain('src') }) - it('adds bin field when missing', () => { + it('adds tanstack-intent keyword when keywords array is missing', () => { writePkg({ name: 'test-pkg', files: [] }, 2) const result = runEditPackageJson(root) - expect(result.added).toEqual( - expect.arrayContaining([expect.stringMatching(/^bin\.intent/)]), - ) + expect(result.added).toContain('keywords: "tanstack-intent"') + + const pkg = readPkg() + expect(pkg.keywords).toEqual(['tanstack-intent']) + }) + + it('adds tanstack-intent keyword to existing keywords array', () => { + writePkg({ name: 'test-pkg', files: [], keywords: ['react', 'router'] }, 2) + + const result = runEditPackageJson(root) + expect(result.added).toContain('keywords: "tanstack-intent"') const pkg = readPkg() - expect(pkg.bin.intent).toMatch(/\.\/bin\/intent\.(js|mjs)/) + expect(pkg.keywords).toContain('tanstack-intent') + expect(pkg.keywords).toContain('react') + expect(pkg.keywords).toContain('router') + }) + + it('reports already present when tanstack-intent keyword exists', () => { + writePkg( + { name: 'test-pkg', files: [], keywords: ['tanstack-intent'] }, + 2, + ) + + const result = runEditPackageJson(root) + expect(result.alreadyPresent).toContain('keywords: "tanstack-intent"') + expect(result.added).not.toEqual( + expect.arrayContaining([expect.stringContaining('keywords')]), + ) }) it('is idempotent — re-running does not duplicate entries', () => { @@ -167,7 +135,7 @@ describe('runEditPackageJson', () => { expect(pkg.files).toContain('dist') }) - it('preserves existing bin entries when adding intent', () => { + it('preserves existing bin entries untouched', () => { writePkg( { name: 'test-pkg', files: [], bin: { 'my-cli': './bin/cli.js' } }, 2, @@ -178,23 +146,7 @@ describe('runEditPackageJson', () => { const pkg = readPkg() as Record const bin = pkg.bin as Record expect(bin['my-cli']).toBe('./bin/cli.js') - expect(bin.intent).toMatch(/\.\/bin\/intent\.(js|mjs)/) - }) - - it('converts string bin to object preserving existing entry', () => { - writePkg({ name: '@scope/my-tool', files: [], bin: './dist/cli.js' }, 2) - - const result = runEditPackageJson(root) - - const pkg = readPkg() as Record - const bin = pkg.bin as Record - expect(bin['my-tool']).toBe('./dist/cli.js') - expect(bin.intent).toMatch(/\.\/bin\/intent\.(js|mjs)/) - expect(result.added).toEqual( - expect.arrayContaining([ - expect.stringContaining('converted bin from string'), - ]), - ) + expect(bin.intent).toBeUndefined() }) it('creates files array if missing', () => { @@ -204,7 +156,6 @@ describe('runEditPackageJson', () => { const pkg = readPkg() expect(pkg.files).toContain('skills') - expect(pkg.files).toContain('bin') expect(pkg.files).toContain('!skills/_artifacts') }) @@ -230,7 +181,6 @@ describe('runEditPackageJson', () => { const result = runEditPackageJson(pkgDir) expect(result.added).toContain('files: "skills"') - expect(result.added).toContain('files: "bin"') expect(result.added).not.toEqual( expect.arrayContaining([expect.stringContaining('!skills/_artifacts')]), ) @@ -488,36 +438,3 @@ describe('runEditPackageJsonAll', () => { }) }) -describe('runAddLibraryBinAll', () => { - it('generates shims only in packages with skills', () => { - const monoRoot = createMonorepo() - - const results = runAddLibraryBinAll(monoRoot) as Array< - MonorepoResult - > - - expect(Array.isArray(results)).toBe(true) - expect(results).toHaveLength(1) - expect(results[0]!.package).toBe(join('packages', 'lib-a')) - expect(results[0]!.result.shim).toBeTruthy() - - // lib-b should not have a shim - expect( - existsSync(join(monoRoot, 'packages', 'lib-b', 'bin', 'intent.mjs')), - ).toBe(false) - expect( - existsSync(join(monoRoot, 'packages', 'lib-b', 'bin', 'intent.js')), - ).toBe(false) - - rmSync(monoRoot, { recursive: true, force: true }) - }) - - it('falls back to single-package when no workspace config exists', () => { - writePkg({ name: 'test-pkg' }) - - const result = runAddLibraryBinAll(root) as AddLibraryBinResult - - expect(Array.isArray(result)).toBe(false) - expect(result.shim).toBeTruthy() - }) -}) From 26031ff4e40b3cf40b163fea8fa81086514f9b81 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 15 Mar 2026 10:43:32 -0600 Subject: [PATCH 2/6] chore: add changeset for keyword-based detection Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/keyword-based-detection.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/keyword-based-detection.md diff --git a/.changeset/keyword-based-detection.md b/.changeset/keyword-based-detection.md new file mode 100644 index 0000000..e5144d4 --- /dev/null +++ b/.changeset/keyword-based-detection.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Replace bin.intent detection with tanstack-intent keyword check for package discovery. Remove the `add-library-bin` command and bin shim generation system — packages are now identified by having `"tanstack-intent"` in their keywords array, which was already required for registry discovery. Also fix `collectPackagingWarnings` to skip the `!skills/_artifacts` warning for monorepo packages. From 24b8c98c5de095f36cf08faedad0bf9cdf54011b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 16:44:41 +0000 Subject: [PATCH 3/6] ci: apply automated fixes --- packages/intent/src/cli.ts | 4 +++- packages/intent/src/index.ts | 5 +---- packages/intent/tests/setup.test.ts | 11 ++--------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 88cb76f..1d8efe8 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -267,7 +267,9 @@ function collectPackagingWarnings(root: string): Array { if (existsSync(parentPkg)) { try { const parent = JSON.parse(readFileSync(parentPkg, 'utf8')) - return Array.isArray(parent.workspaces) || parent.workspaces?.packages + return ( + Array.isArray(parent.workspaces) || parent.workspaces?.packages + ) } catch { return false } diff --git a/packages/intent/src/index.ts b/packages/intent/src/index.ts index eb0bb4a..3c14fcd 100644 --- a/packages/intent/src/index.ts +++ b/packages/intent/src/index.ts @@ -17,10 +17,7 @@ export { parseFrontmatter, resolveDepDir, } from './utils.js' -export { - runEditPackageJson, - runSetupGithubActions, -} from './setup.js' +export { runEditPackageJson, runSetupGithubActions } from './setup.js' export type { EditPackageJsonResult, SetupGithubActionsResult, diff --git a/packages/intent/tests/setup.test.ts b/packages/intent/tests/setup.test.ts index e0170ec..0e45c9d 100644 --- a/packages/intent/tests/setup.test.ts +++ b/packages/intent/tests/setup.test.ts @@ -14,10 +14,7 @@ import { runEditPackageJsonAll, runSetupGithubActions, } from '../src/setup.js' -import type { - MonorepoResult, - EditPackageJsonResult, -} from '../src/setup.js' +import type { MonorepoResult, EditPackageJsonResult } from '../src/setup.js' let root: string let metaDir: string @@ -89,10 +86,7 @@ describe('runEditPackageJson', () => { }) it('reports already present when tanstack-intent keyword exists', () => { - writePkg( - { name: 'test-pkg', files: [], keywords: ['tanstack-intent'] }, - 2, - ) + writePkg({ name: 'test-pkg', files: [], keywords: ['tanstack-intent'] }, 2) const result = runEditPackageJson(root) expect(result.alreadyPresent).toContain('keywords: "tanstack-intent"') @@ -437,4 +431,3 @@ describe('runEditPackageJsonAll', () => { rmSync(monoRoot, { recursive: true, force: true }) }) }) - From edc34422db2f53e51734e3866ec67fac0804d234 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 15 Mar 2026 11:16:12 -0600 Subject: [PATCH 4/6] fix: keep legacy bin.intent fallback for backwards compatibility Packages already published with bin.intent but without the tanstack-intent keyword would silently lose transitive skill discovery. Keep both signals during the transition period. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/intent/src/library-scanner.ts | 16 +++++++--- packages/intent/tests/library-scanner.test.ts | 31 +++++++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/intent/src/library-scanner.ts b/packages/intent/src/library-scanner.ts index 4e9268a..25f0706 100644 --- a/packages/intent/src/library-scanner.ts +++ b/packages/intent/src/library-scanner.ts @@ -42,10 +42,18 @@ function findHomeDir(scriptPath: string): string | null { } } -function hasIntentKeyword(pkg: Record): boolean { +function isIntentPackage(pkg: Record): boolean { const keywords = pkg.keywords - if (!Array.isArray(keywords)) return false - return keywords.includes('tanstack-intent') + if (Array.isArray(keywords) && keywords.includes('tanstack-intent')) { + return true + } + // Legacy fallback: packages published before the keyword-based detection + // change may only have bin.intent. Keep this until a breaking release. + const bin = pkg.bin + if (bin && typeof bin === 'object' && 'intent' in (bin as Record)) { + return true + } + return false } function discoverSkills(skillsDir: string): Array { @@ -134,7 +142,7 @@ export async function scanLibrary( const depDir = resolveDepDir(depName, dir) if (!depDir) continue const depPkg = readPkgJson(depDir) - if (depPkg && hasIntentKeyword(depPkg)) { + if (depPkg && isIntentPackage(depPkg)) { processPackage(depName, depDir) } } diff --git a/packages/intent/tests/library-scanner.test.ts b/packages/intent/tests/library-scanner.test.ts index 95ff0dc..29d0e1f 100644 --- a/packages/intent/tests/library-scanner.test.ts +++ b/packages/intent/tests/library-scanner.test.ts @@ -165,7 +165,7 @@ describe('scanLibrary', () => { expect(names).toContain('@tanstack/query') }) - it('skips deps without tanstack-intent keyword', async () => { + it('skips deps without tanstack-intent keyword or bin.intent', async () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(pkgDir, 'package.json'), { name: '@tanstack/router', @@ -178,7 +178,7 @@ describe('scanLibrary', () => { writeJson(join(reactDir, 'package.json'), { name: 'react', version: '18.0.0', - // no tanstack-intent keyword + // no tanstack-intent keyword or bin.intent }) const reactSkill = createDir(reactDir, 'skills', 'hooks') writeSkillMd(reactSkill, { name: 'hooks', description: 'React hooks' }) @@ -189,6 +189,33 @@ describe('scanLibrary', () => { expect(names).not.toContain('react') }) + it('follows deps with legacy bin.intent (backwards compat)', async () => { + const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') + writeJson(join(pkgDir, 'package.json'), { + name: '@tanstack/router', + version: '1.0.0', + keywords: ['tanstack-intent'], + dependencies: { '@tanstack/query': '^5.0.0' }, + }) + const routerSkill = createDir(pkgDir, 'skills', 'routing') + writeSkillMd(routerSkill, { name: 'routing', description: 'Routing' }) + + // Legacy package: only has bin.intent, no keyword + const queryDir = createDir(root, 'node_modules', '@tanstack', 'query') + writeJson(join(queryDir, 'package.json'), { + name: '@tanstack/query', + version: '5.0.0', + bin: { intent: './bin/intent.js' }, + }) + const querySkill = createDir(queryDir, 'skills', 'fetching') + writeSkillMd(querySkill, { name: 'fetching', description: 'Fetching' }) + + const result = await scanLibrary(scriptPath(pkgDir), root) + + const names = result.packages.map((p) => p.name) + expect(names).toContain('@tanstack/query') + }) + it('skips deps with other keywords but not tanstack-intent', async () => { const pkgDir = createDir(root, 'node_modules', '@tanstack', 'router') writeJson(join(pkgDir, 'package.json'), { From b042a75bf6d7d00db22d77c9abd2d4a96402338f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:17:08 +0000 Subject: [PATCH 5/6] ci: apply automated fixes --- packages/intent/src/library-scanner.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/intent/src/library-scanner.ts b/packages/intent/src/library-scanner.ts index 25f0706..8990b50 100644 --- a/packages/intent/src/library-scanner.ts +++ b/packages/intent/src/library-scanner.ts @@ -50,7 +50,11 @@ function isIntentPackage(pkg: Record): boolean { // Legacy fallback: packages published before the keyword-based detection // change may only have bin.intent. Keep this until a breaking release. const bin = pkg.bin - if (bin && typeof bin === 'object' && 'intent' in (bin as Record)) { + if ( + bin && + typeof bin === 'object' && + 'intent' in (bin as Record) + ) { return true } return false From 70b8ee236ec796a5c2d0dbc8dbd9e44a81df87c1 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Sun, 15 Mar 2026 15:09:47 -0600 Subject: [PATCH 6/6] style: alphabetize import members in setup.test.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/intent/tests/setup.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/intent/tests/setup.test.ts b/packages/intent/tests/setup.test.ts index 0e45c9d..c74940b 100644 --- a/packages/intent/tests/setup.test.ts +++ b/packages/intent/tests/setup.test.ts @@ -14,7 +14,7 @@ import { runEditPackageJsonAll, runSetupGithubActions, } from '../src/setup.js' -import type { MonorepoResult, EditPackageJsonResult } from '../src/setup.js' +import type { EditPackageJsonResult, MonorepoResult } from '../src/setup.js' let root: string let metaDir: string