From 069f7646b139db8bfc8b2f5f92eb70b04252f608 Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Sat, 13 Jun 2026 22:33:27 -0700 Subject: [PATCH 1/7] changeset --- .changeset/crisp-flies-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/crisp-flies-mate.md diff --git a/.changeset/crisp-flies-mate.md b/.changeset/crisp-flies-mate.md new file mode 100644 index 0000000..aac033c --- /dev/null +++ b/.changeset/crisp-flies-mate.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': minor +--- + +Add `package.json#intent.skills` source allowlisting to gate which discovered packages can contribute skills. From b7df93e64f895353366651f8391b4b618cbf6b22 Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Sat, 13 Jun 2026 22:33:56 -0700 Subject: [PATCH 2/7] format --- .changeset/crisp-flies-mate.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/crisp-flies-mate.md b/.changeset/crisp-flies-mate.md index aac033c..2283bb4 100644 --- a/.changeset/crisp-flies-mate.md +++ b/.changeset/crisp-flies-mate.md @@ -3,3 +3,5 @@ --- Add `package.json#intent.skills` source allowlisting to gate which discovered packages can contribute skills. + +`intent.exclude` now supports skill-level matching (for example `@scope/pkg#skill-id` and globs), and policy filtering is applied consistently across `intent list`, `intent load`, `intent install`, and `intent stale`. Notices are surfaced separately from warnings to keep command output machine-safe. From 66dbbd72308ee2814d373695baf9ce4db8396857 Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Sat, 13 Jun 2026 22:50:45 -0700 Subject: [PATCH 3/7] add no notices option --- docs/cli/intent-install.md | 5 +- docs/cli/intent-list.md | 4 +- docs/concepts/configuration.md | 11 +++++ packages/intent/src/cli-output.ts | 23 +++++++++- packages/intent/src/cli-support.ts | 8 ++++ packages/intent/src/cli.ts | 6 ++- packages/intent/src/commands/install.ts | 23 +++++++--- packages/intent/src/commands/list.ts | 6 ++- packages/intent/tests/cli.test.ts | 61 +++++++++++++++++++++++++ 9 files changed, 134 insertions(+), 13 deletions(-) diff --git a/docs/cli/intent-install.md b/docs/cli/intent-install.md index a36e432..8d444e0 100644 --- a/docs/cli/intent-install.md +++ b/docs/cli/intent-install.md @@ -6,7 +6,7 @@ id: intent-install `intent install` creates or updates an `intent-skills` guidance block in a project guidance file. ```bash -npx @tanstack/intent@latest install [--map] [--dry-run] [--print-prompt] [--global] [--global-only] +npx @tanstack/intent@latest install [--map] [--dry-run] [--print-prompt] [--global] [--global-only] [--no-notices] ``` ## Options @@ -16,6 +16,7 @@ npx @tanstack/intent@latest install [--map] [--dry-run] [--print-prompt] [--glob - `--print-prompt`: print the agent setup prompt instead of writing files - `--global`: include global packages after project packages when `--map` is passed - `--global-only`: install mappings from global packages only when `--map` is passed +- `--no-notices`: suppress non-critical notices on stderr ## Behavior @@ -75,6 +76,8 @@ skills: - Placement tip: `Tip: Keep the intent-skills block near the top of AGENTS.md so agents read it before task-specific instructions.` - No actionable skills in `--map` mode: `No intent-enabled skills found.` +To suppress trust and migration notices in automation, pass `--no-notices`. + ## Related - [intent list](./intent-list) diff --git a/docs/cli/intent-list.md b/docs/cli/intent-list.md index 132d06f..d2af007 100644 --- a/docs/cli/intent-list.md +++ b/docs/cli/intent-list.md @@ -6,7 +6,7 @@ id: intent-list `intent list` discovers skill-enabled packages and prints available skills. ```bash -npx @tanstack/intent@latest list [--json] [--debug] [--exclude ] [--global] [--global-only] +npx @tanstack/intent@latest list [--json] [--debug] [--exclude ] [--global] [--global-only] [--no-notices] ``` ## Options @@ -16,6 +16,7 @@ npx @tanstack/intent@latest list [--json] [--debug] [--exclude ] [--glo - `--exclude `: 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 +- `--no-notices`: suppress non-critical notices on stderr ## What you get @@ -30,6 +31,7 @@ npx @tanstack/intent@latest list [--json] [--debug] [--exclude ] [--glo - Package table columns: `PACKAGE`, `SOURCE`, `VERSION`, `SKILLS` - Skill tree grouped by package - Optional warnings section (`⚠ ...` per warning) +- Optional notices section on stderr (`ℹ ...` per notice), suppressed by `--no-notices` `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. diff --git a/docs/concepts/configuration.md b/docs/concepts/configuration.md index 6f8edd1..d161bcc 100644 --- a/docs/concepts/configuration.md +++ b/docs/concepts/configuration.md @@ -46,6 +46,17 @@ A package that ships skills but is not listed is dropped. When packages are drop A project that has not set `intent.skills` keeps working. Intent surfaces every discovered package and prints the deprecation notice described under the absent form. Nothing breaks. Add an allowlist when you are ready, before a future version requires one. Run `intent list` to confirm which packages are surfaced. +### Suppressing notices temporarily + +Use `--no-notices` to suppress non-critical notices on stderr for one run: + +```bash +npx @tanstack/intent@latest list --no-notices +npx @tanstack/intent@latest install --map --no-notices +``` + +For CI or wrapper scripts, set `INTENT_NO_NOTICES=1` to suppress notices without changing command arguments. + ## `intent.exclude` `intent.exclude` removes packages or individual skills after the allowlist resolves. It also accepts the `--exclude ` flag on `list` and `load` for one-off runs. diff --git a/packages/intent/src/cli-output.ts b/packages/intent/src/cli-output.ts index 25623e8..6c465d9 100644 --- a/packages/intent/src/cli-output.ts +++ b/packages/intent/src/cli-output.ts @@ -7,8 +7,29 @@ export function printWarnings(warnings: Array): void { } } -export function printNotices(notices: Array): void { +export interface NoticeOutputOptions { + noNotices?: boolean +} + +const TRUE_LIKE_VALUES = new Set(['1', 'true', 'yes', 'on']) + +function envSuppressesNotices(): boolean { + const value = process.env.INTENT_NO_NOTICES?.trim().toLowerCase() + return value ? TRUE_LIKE_VALUES.has(value) : false +} + +export function shouldSuppressNotices( + options: NoticeOutputOptions = {}, +): boolean { + return options.noNotices === true || envSuppressesNotices() +} + +export function printNotices( + notices: Array, + options: NoticeOutputOptions = {}, +): void { if (notices.length === 0) return + if (shouldSuppressNotices(options)) return console.error('Notices:') for (const notice of notices) { diff --git a/packages/intent/src/cli-support.ts b/packages/intent/src/cli-support.ts index 0305d41..2c0445b 100644 --- a/packages/intent/src/cli-support.ts +++ b/packages/intent/src/cli-support.ts @@ -13,6 +13,8 @@ export interface GlobalScanFlags { exclude?: string | Array global?: boolean globalOnly?: boolean + notices?: boolean + noNotices?: boolean } export interface StaleTargetResult { @@ -99,6 +101,12 @@ export function coreOptionsFromGlobalFlags( } } +export function noticeOptionsFromGlobalFlags( + options: GlobalScanFlags, +): { noNotices?: boolean } { + return { noNotices: options.noNotices || options.notices === false } +} + function formatDebugValue(value: string | number | Array): string { if (Array.isArray(value)) { return value.length > 0 ? value.join(', ') : '(none)' diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 7f4570e..e0f26ba 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -21,13 +21,14 @@ function createCli(): CAC { 'Discover intent-enabled packages from the project or workspace', ) .usage( - 'list [--json] [--debug] [--exclude ] [--global] [--global-only]', + 'list [--json] [--debug] [--exclude ] [--global] [--global-only] [--no-notices]', ) .option('--json', 'Output JSON') .option('--debug', 'Print discovery debug details to stderr') .option('--exclude ', 'Exclude package name glob') .option('--global', 'Include global packages after project packages') .option('--global-only', 'List global packages only') + .option('--no-notices', 'Suppress non-critical notices on stderr') .example('list') .example('list --json') .example('list --global') @@ -86,7 +87,7 @@ function createCli(): CAC { 'Create or update skill loading guidance in an agent config file', ) .usage( - 'install [--map] [--dry-run] [--print-prompt] [--global] [--global-only]', + 'install [--map] [--dry-run] [--print-prompt] [--global] [--global-only] [--no-notices]', ) .option('--map', 'Write explicit skill-to-task mappings') .option('--dry-run', 'Print the generated block without writing') @@ -96,6 +97,7 @@ function createCli(): CAC { ) .option('--global', 'Include global packages after project packages') .option('--global-only', 'Install mappings from global packages only') + .option('--no-notices', 'Suppress non-critical notices on stderr') .example('install') .example('install --map') .example('install --dry-run') diff --git a/packages/intent/src/commands/install.ts b/packages/intent/src/commands/install.ts index e9953c5..d624447 100644 --- a/packages/intent/src/commands/install.ts +++ b/packages/intent/src/commands/install.ts @@ -3,6 +3,7 @@ import { fail } from '../cli-error.js' import { detectIntentCommandPackageManager } from '../command-runner.js' import { coreOptionsFromGlobalFlags, + noticeOptionsFromGlobalFlags, printNotices, printWarnings, } from '../cli-support.js' @@ -136,10 +137,11 @@ function formatMappingCount(mappingCount: number): string { function printNoActionableSkills( warnings: Array, notices: Array, + noticeOptions: { noNotices?: boolean }, ): void { console.log('No intent-enabled skills found.') printWarnings(warnings) - printNotices(notices) + printNotices(notices, noticeOptions) } function printPlacementTip(targetPath: string): void { @@ -201,6 +203,7 @@ export async function runInstallCommand( } const coreOptions = coreOptionsFromGlobalFlags(options) + const noticeOptions = noticeOptionsFromGlobalFlags(options) if (!options.map) { const generated = buildIntentSkillGuidanceBlock( @@ -256,8 +259,12 @@ export async function runInstallCommand( ) if (!targetPath) { - printNoActionableSkills(scanResult.warnings, scanResult.notices) - return + printNoActionableSkills( + scanResult.warnings, + scanResult.notices, + noticeOptions, + ) + return } console.log( @@ -265,7 +272,7 @@ export async function runInstallCommand( ) console.log(generated.block) printWarnings(scanResult.warnings) - printNotices(scanResult.notices) + printNotices(scanResult.notices, noticeOptions) return } @@ -275,7 +282,11 @@ export async function runInstallCommand( }) if (!result.targetPath) { - printNoActionableSkills(scanResult.warnings, scanResult.notices) + printNoActionableSkills( + scanResult.warnings, + scanResult.notices, + noticeOptions, + ) return } @@ -299,5 +310,5 @@ export async function runInstallCommand( printPlacementTip(result.targetPath) printWarnings(scanResult.warnings) - printNotices(scanResult.notices) + printNotices(scanResult.notices, noticeOptions) } diff --git a/packages/intent/src/commands/list.ts b/packages/intent/src/commands/list.ts index 0749021..c5136b5 100644 --- a/packages/intent/src/commands/list.ts +++ b/packages/intent/src/commands/list.ts @@ -1,5 +1,6 @@ import { coreOptionsFromGlobalFlags, + noticeOptionsFromGlobalFlags, printDebugInfo, printNotices, printWarnings, @@ -90,6 +91,7 @@ export async function runListCommand( options: ListCommandOptions, ): Promise { const result = listIntentSkills(coreOptionsFromGlobalFlags(options)) + const noticeOptions = noticeOptionsFromGlobalFlags(options) printListDebug(result) if (options.json) { @@ -111,7 +113,7 @@ export async function runListCommand( console.log() printWarnings(result.warnings) } - printNotices(result.notices) + printNotices(result.notices, noticeOptions) return } @@ -170,5 +172,5 @@ export async function runListCommand( console.log() printWarnings(result.warnings) - printNotices(result.notices) + printNotices(result.notices, noticeOptions) } diff --git a/packages/intent/tests/cli.test.ts b/packages/intent/tests/cli.test.ts index 3b8c85a..655094e 100644 --- a/packages/intent/tests/cli.test.ts +++ b/packages/intent/tests/cli.test.ts @@ -72,6 +72,7 @@ let errorSpy: ReturnType let stdoutWriteSpy: ReturnType let tempDirs: Array let previousGlobalNodeModules: string | undefined +let previousNoNotices: string | undefined function getHelpOutput(): string { return [...infoSpy.mock.calls, ...logSpy.mock.calls] @@ -83,7 +84,9 @@ beforeEach(() => { originalCwd = process.cwd() tempDirs = [] previousGlobalNodeModules = process.env.INTENT_GLOBAL_NODE_MODULES + previousNoNotices = process.env.INTENT_NO_NOTICES delete process.env.INTENT_GLOBAL_NODE_MODULES + delete process.env.INTENT_NO_NOTICES logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}) errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -99,6 +102,11 @@ afterEach(() => { } else { process.env.INTENT_GLOBAL_NODE_MODULES = previousGlobalNodeModules } + if (previousNoNotices === undefined) { + delete process.env.INTENT_NO_NOTICES + } else { + process.env.INTENT_NO_NOTICES = previousNoNotices + } logSpy.mockRestore() infoSpy.mockRestore() errorSpy.mockRestore() @@ -761,6 +769,59 @@ describe('cli commands', () => { expect(stdout).not.toContain('Notices:') }) + it('suppresses notices when --no-notices is passed', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-list-no-notices-')) + const isolatedGlobalRoot = mkdtempSync( + join(realTmpdir, 'intent-cli-list-no-notices-empty-global-'), + ) + tempDirs.push(root, isolatedGlobalRoot) + writeInstalledIntentPackage(root, { + name: '@tanstack/query', + version: '5.0.0', + skillName: 'fetching', + description: 'Query data fetching patterns', + }) + + process.env.INTENT_GLOBAL_NODE_MODULES = isolatedGlobalRoot + process.chdir(root) + + const exitCode = await main(['list', '--no-notices']) + const stdout = logSpy.mock.calls.flat().join('\n') + const stderr = errorSpy.mock.calls.flat().join('\n') + + expect(exitCode).toBe(0) + expect(stdout).toContain('@tanstack/query') + expect(stderr).not.toContain('intent.skills is not set') + expect(stderr).not.toContain('Notices:') + }) + + it('suppresses notices when INTENT_NO_NOTICES=1 is set', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-list-env-no-notices-')) + const isolatedGlobalRoot = mkdtempSync( + join(realTmpdir, 'intent-cli-list-env-no-notices-empty-global-'), + ) + tempDirs.push(root, isolatedGlobalRoot) + writeInstalledIntentPackage(root, { + name: '@tanstack/query', + version: '5.0.0', + skillName: 'fetching', + description: 'Query data fetching patterns', + }) + + process.env.INTENT_GLOBAL_NODE_MODULES = isolatedGlobalRoot + process.env.INTENT_NO_NOTICES = '1' + process.chdir(root) + + const exitCode = await main(['list']) + const stdout = logSpy.mock.calls.flat().join('\n') + const stderr = errorSpy.mock.calls.flat().join('\n') + + expect(exitCode).toBe(0) + expect(stdout).toContain('@tanstack/query') + expect(stderr).not.toContain('intent.skills is not set') + expect(stderr).not.toContain('Notices:') + }) + it('prints list debug details to stderr without changing json stdout', async () => { const root = mkdtempSync(join(realTmpdir, 'intent-cli-list-debug-')) tempDirs.push(root) From 63a831c0af1a2166d92fb35fde4ac622f9c209a3 Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Sat, 13 Jun 2026 23:16:31 -0700 Subject: [PATCH 4/7] exclude command --- docs/cli/intent-exclude.md | 43 ++++++ docs/cli/intent-list.md | 8 +- docs/cli/intent-load.md | 5 +- docs/concepts/configuration.md | 10 +- docs/config.json | 4 + packages/intent/src/cli-support.ts | 6 - packages/intent/src/cli.ts | 29 +++- packages/intent/src/commands/exclude.ts | 180 ++++++++++++++++++++++++ packages/intent/tests/cli.test.ts | 135 ++++++++++++++++-- 9 files changed, 388 insertions(+), 32 deletions(-) create mode 100644 docs/cli/intent-exclude.md create mode 100644 packages/intent/src/commands/exclude.ts diff --git a/docs/cli/intent-exclude.md b/docs/cli/intent-exclude.md new file mode 100644 index 0000000..1c1d4db --- /dev/null +++ b/docs/cli/intent-exclude.md @@ -0,0 +1,43 @@ +--- +title: intent exclude +id: intent-exclude +--- + +`intent exclude` manages `package.json#intent.exclude` entries. + +```bash +npx @tanstack/intent@latest exclude [list|add|remove] [pattern] [--json] +``` + +## Options + +- `--json`: print the configured exclude patterns as JSON + +## Actions + +1. `list` (default): print current excludes +2. `add `: append one exclude pattern +3. `remove `: remove one exclude pattern + +## Examples + +```bash +npx @tanstack/intent@latest exclude +npx @tanstack/intent@latest exclude list --json +npx @tanstack/intent@latest exclude add @tanstack/router#experimental-* +npx @tanstack/intent@latest exclude remove @tanstack/router#experimental-* +``` + +## Behavior + +- Reads and writes the current working directory `package.json` +- Creates `intent.exclude` when missing +- Keeps existing excludes and appends new patterns in order +- Validates pattern syntax before writing +- Refuses invalid `package.json` structures for `intent` and `intent.exclude` + +## Related + +- [Configuration](../concepts/configuration) +- [intent list](./intent-list) +- [intent load](./intent-load) diff --git a/docs/cli/intent-list.md b/docs/cli/intent-list.md index d2af007..3fe825e 100644 --- a/docs/cli/intent-list.md +++ b/docs/cli/intent-list.md @@ -6,14 +6,13 @@ id: intent-list `intent list` discovers skill-enabled packages and prints available skills. ```bash -npx @tanstack/intent@latest list [--json] [--debug] [--exclude ] [--global] [--global-only] [--no-notices] +npx @tanstack/intent@latest list [--json] [--debug] [--global] [--global-only] [--no-notices] ``` ## Options - `--json`: print JSON instead of text output - `--debug`: print discovery debug details to stderr -- `--exclude `: 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 - `--no-notices`: suppress non-critical notices on stderr @@ -24,7 +23,7 @@ npx @tanstack/intent@latest list [--json] [--debug] [--exclude ] [--glo - Surfaces packages permitted by `package.json#intent.skills` (see [Allowlist](#allowlist)) - Includes global packages only when `--global` or `--global-only` is passed - Includes warnings from discovery -- Excludes packages and skills matched by package.json `intent.exclude` or `--exclude` +- Excludes packages and skills matched by package.json `intent.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 and skill count @@ -115,7 +114,8 @@ A package that ships skills but is not listed is dropped. When packages are drop ## Excludes Package excludes are hard filters for packages that should not be used in a repo, applied after the allowlist. -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. +Intent reads `intent.exclude` arrays from package.json files while walking from the workspace or project root to the current working directory. +Manage persistent excludes with `intent exclude add|remove|list`. ```json { diff --git a/docs/cli/intent-load.md b/docs/cli/intent-load.md index 520d0b9..28043d2 100644 --- a/docs/cli/intent-load.md +++ b/docs/cli/intent-load.md @@ -6,7 +6,7 @@ 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 # [--path] [--json] [--debug] [--exclude ] [--global] [--global-only] +npx @tanstack/intent@latest load # [--path] [--json] [--debug] [--global] [--global-only] ``` ## Options @@ -14,7 +14,6 @@ npx @tanstack/intent@latest load # [--path] [--json] [--debug] [ - `--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 `: exclude a package or skill 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 @@ -24,7 +23,7 @@ npx @tanstack/intent@latest load # [--path] [--json] [--debug] [ - Scans project-local packages by default - Includes global packages only when `--global` or `--global-only` is passed - Refuses before scanning when the target package is not permitted by `package.json#intent.skills` -- Refuses before scanning when the target package or skill matches `intent.exclude` or `--exclude` +- Refuses before scanning when the target package or skill matches `intent.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 diff --git a/docs/concepts/configuration.md b/docs/concepts/configuration.md index d161bcc..60fbeaf 100644 --- a/docs/concepts/configuration.md +++ b/docs/concepts/configuration.md @@ -59,7 +59,15 @@ For CI or wrapper scripts, set `INTENT_NO_NOTICES=1` to suppress notices without ## `intent.exclude` -`intent.exclude` removes packages or individual skills after the allowlist resolves. It also accepts the `--exclude ` flag on `list` and `load` for one-off runs. +`intent.exclude` removes packages or individual skills after the allowlist resolves. + +Use `intent exclude` to manage this list from the CLI: + +```bash +npx @tanstack/intent@latest exclude add @tanstack/router#experimental-* +npx @tanstack/intent@latest exclude remove @tanstack/router#experimental-* +npx @tanstack/intent@latest exclude list +``` ```json { diff --git a/docs/config.json b/docs/config.json index ac979b5..656dad5 100644 --- a/docs/config.json +++ b/docs/config.json @@ -47,6 +47,10 @@ "label": "intent install", "to": "cli/intent-install" }, + { + "label": "intent exclude", + "to": "cli/intent-exclude" + }, { "label": "intent list", "to": "cli/intent-list" diff --git a/packages/intent/src/cli-support.ts b/packages/intent/src/cli-support.ts index 2c0445b..d1788d4 100644 --- a/packages/intent/src/cli-support.ts +++ b/packages/intent/src/cli-support.ts @@ -10,7 +10,6 @@ export { printNotices, printWarnings } from './cli-output.js' export interface GlobalScanFlags { debug?: boolean - exclude?: string | Array global?: boolean globalOnly?: boolean notices?: boolean @@ -91,11 +90,6 @@ export function coreOptionsFromGlobalFlags( return { debug: options.debug, - exclude: Array.isArray(options.exclude) - ? options.exclude - : options.exclude - ? [options.exclude] - : undefined, global: options.global, globalOnly: options.globalOnly, } diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index e0f26ba..8037480 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url' import { cac } from 'cac' import { fail, isCliFailure } from './cli-error.js' import type { CAC } from 'cac' +import type { ExcludeCommandOptions } from './commands/exclude.js' import type { InstallCommandOptions } from './commands/install.js' import type { ListCommandOptions } from './commands/list.js' import type { LoadCommandOptions } from './commands/load.js' @@ -21,11 +22,10 @@ function createCli(): CAC { 'Discover intent-enabled packages from the project or workspace', ) .usage( - 'list [--json] [--debug] [--exclude ] [--global] [--global-only] [--no-notices]', + 'list [--json] [--debug] [--global] [--global-only] [--no-notices]', ) .option('--json', 'Output JSON') .option('--debug', 'Print discovery debug details to stderr') - .option('--exclude ', 'Exclude package name glob') .option('--global', 'Include global packages after project packages') .option('--global-only', 'List global packages only') .option('--no-notices', 'Suppress non-critical notices on stderr') @@ -37,15 +37,36 @@ function createCli(): CAC { await runListCommand(options) }) + cli + .command( + 'exclude [action] [pattern]', + 'Manage package.json intent.exclude entries', + ) + .usage('exclude [list|add|remove] [pattern] [--json]') + .option('--json', 'Output JSON list of configured exclude patterns') + .example('exclude') + .example('exclude list --json') + .example('exclude add @tanstack/router#experimental-*') + .example('exclude remove @tanstack/router#experimental-*') + .action( + async ( + action: string | undefined, + pattern: string | undefined, + options: ExcludeCommandOptions, + ) => { + const { runExcludeCommand } = await import('./commands/exclude.js') + await runExcludeCommand(action, pattern, options) + }, + ) + cli .command('load [use]', 'Load a compact skill use and print its SKILL.md') .usage( - 'load [--path] [--json] [--debug] [--exclude ] [--global] [--global-only]', + 'load [--path] [--json] [--debug] [--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 ', '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') diff --git a/packages/intent/src/commands/exclude.ts b/packages/intent/src/commands/exclude.ts new file mode 100644 index 0000000..71b1df5 --- /dev/null +++ b/packages/intent/src/commands/exclude.ts @@ -0,0 +1,180 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { fail } from '../cli-error.js' +import { compileExcludePatterns } from '../core/excludes.js' + +export interface ExcludeCommandOptions { + json?: boolean +} + +type ExcludeAction = 'add' | 'list' | 'remove' + +function normalizeAction(action: string | undefined): ExcludeAction { + if (!action) return 'list' + if (action === 'list' || action === 'add' || action === 'remove') return action + fail(`Unknown exclude action: ${action}. Expected list, add, or remove.`) +} + +function getPackageJsonPath(cwd: string): string { + return join(cwd, 'package.json') +} + +function readPackageJson(cwd: string): Record { + const packageJsonPath = getPackageJsonPath(cwd) + if (!existsSync(packageJsonPath)) { + fail(`No package.json found in ${cwd}`) + } + + try { + return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record< + string, + unknown + > + } catch (err) { + fail( + `Failed to parse ${packageJsonPath}: ${err instanceof Error ? err.message : String(err)}`, + ) + } +} + +function readConfiguredExcludes(pkg: Record): Array { + const intent = pkg.intent + if (intent === undefined) return [] + if (!intent || typeof intent !== 'object') { + fail('Invalid package.json: intent must be an object when present.') + } + + const raw = (intent as Record).exclude + if (raw === undefined) return [] + if (!Array.isArray(raw)) { + fail('Invalid package.json: intent.exclude must be an array of strings.') + } + + const excludes: Array = [] + for (const entry of raw) { + if (typeof entry !== 'string') { + fail('Invalid package.json: intent.exclude must contain only strings.') + } + const trimmed = entry.trim() + if (trimmed.length === 0) continue + excludes.push(trimmed) + } + return excludes +} + +function setConfiguredExcludes( + pkg: Record, + excludes: Array, +): void { + const intent = + pkg.intent && typeof pkg.intent === 'object' + ? (pkg.intent as Record) + : {} + + intent.exclude = excludes + pkg.intent = intent +} + +function writePackageJson(cwd: string, pkg: Record): void { + const packageJsonPath = getPackageJsonPath(cwd) + writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8') +} + +function normalizePattern(pattern: string | undefined, action: ExcludeAction): string { + if (!pattern) { + fail(`Missing exclude pattern. Expected: intent exclude ${action} `) + } + const trimmed = pattern.trim() + if (trimmed.length === 0) { + fail(`Missing exclude pattern. Expected: intent exclude ${action} `) + } + return trimmed +} + +function validatePattern(pattern: string): void { + try { + compileExcludePatterns([pattern]) + } catch (err) { + fail( + `Invalid exclude pattern "${pattern}": ${err instanceof Error ? err.message : String(err)}`, + ) + } +} + +function printExcludes(excludes: Array, json?: boolean): void { + if (json) { + console.log(JSON.stringify(excludes, null, 2)) + return + } + + if (excludes.length === 0) { + console.log('No excludes configured.') + return + } + + console.log('Configured excludes:') + for (const pattern of excludes) { + console.log(`- ${pattern}`) + } +} + +export async function runExcludeCommand( + actionArg: string | undefined, + patternArg: string | undefined, + options: ExcludeCommandOptions, +): Promise { + const action = normalizeAction(actionArg) + const cwd = process.cwd() + const pkg = readPackageJson(cwd) + const currentExcludes = readConfiguredExcludes(pkg) + + if (action === 'list') { + if (patternArg) { + fail('Unexpected pattern for list. Use: intent exclude list [--json]') + } + printExcludes(currentExcludes, options.json) + return + } + + const pattern = normalizePattern(patternArg, action) + validatePattern(pattern) + + if (action === 'add') { + if (currentExcludes.includes(pattern)) { + if (options.json) { + printExcludes(currentExcludes, true) + return + } + console.log(`Exclude pattern "${pattern}" is already configured.`) + return + } + + const updated = [...currentExcludes, pattern] + setConfiguredExcludes(pkg, updated) + writePackageJson(cwd, pkg) + if (options.json) { + printExcludes(updated, true) + return + } + console.log(`Added exclude pattern "${pattern}" to package.json intent.exclude.`) + return + } + + const updated = currentExcludes.filter((value) => value !== pattern) + if (updated.length === currentExcludes.length) { + if (options.json) { + printExcludes(currentExcludes, true) + return + } + console.log(`Exclude pattern "${pattern}" is not configured.`) + return + } + + setConfiguredExcludes(pkg, updated) + writePackageJson(cwd, pkg) + if (options.json) { + printExcludes(updated, true) + return + } + console.log(`Removed exclude pattern "${pattern}" from package.json intent.exclude.`) +} diff --git a/packages/intent/tests/cli.test.ts b/packages/intent/tests/cli.test.ts index 655094e..f696cb1 100644 --- a/packages/intent/tests/cli.test.ts +++ b/packages/intent/tests/cli.test.ts @@ -220,6 +220,113 @@ describe('cli commands', () => { expect(logSpy).toHaveBeenCalledWith(INSTALL_PROMPT) }) + it('lists excludes when none are configured', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-exclude-list-empty-')) + tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: 'app', + private: true, + }) + process.chdir(root) + + const exitCode = await main(['exclude']) + + expect(exitCode).toBe(0) + expect(logSpy).toHaveBeenCalledWith('No excludes configured.') + }) + + it('adds and lists an exclude pattern', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-exclude-add-')) + tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: 'app', + private: true, + }) + process.chdir(root) + + const addExitCode = await main([ + 'exclude', + 'add', + '@tanstack/router#experimental-*', + ]) + const listExitCode = await main(['exclude']) + const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { + intent?: { exclude?: Array } + } + const output = logSpy.mock.calls.flat().join('\n') + + expect(addExitCode).toBe(0) + expect(listExitCode).toBe(0) + expect(pkg.intent?.exclude).toEqual(['@tanstack/router#experimental-*']) + expect(output).toContain('Configured excludes:') + expect(output).toContain('- @tanstack/router#experimental-*') + }) + + it('removes an exclude pattern', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-exclude-remove-')) + tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: 'app', + private: true, + intent: { + exclude: ['@tanstack/router#experimental-*'], + }, + }) + process.chdir(root) + + const exitCode = await main([ + 'exclude', + 'remove', + '@tanstack/router#experimental-*', + ]) + const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { + intent?: { exclude?: Array } + } + + expect(exitCode).toBe(0) + expect(pkg.intent?.exclude).toEqual([]) + expect(logSpy).toHaveBeenCalledWith( + 'Removed exclude pattern "@tanstack/router#experimental-*" from package.json intent.exclude.', + ) + }) + + it('prints excludes as JSON', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-exclude-list-json-')) + tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: 'app', + private: true, + intent: { + exclude: ['@tanstack/router#experimental-*', '*#draft-*'], + }, + }) + process.chdir(root) + + const exitCode = await main(['exclude', 'list', '--json']) + const output = logSpy.mock.calls.at(-1)?.[0] + const parsed = JSON.parse(String(output)) as Array + + expect(exitCode).toBe(0) + expect(parsed).toEqual(['@tanstack/router#experimental-*', '*#draft-*']) + }) + + it('fails cleanly on unknown exclude actions', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-exclude-bad-action-')) + tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: 'app', + private: true, + }) + process.chdir(root) + + const exitCode = await main(['exclude', 'enable', '@tanstack/router']) + + expect(exitCode).toBe(1) + expect(errorSpy).toHaveBeenCalledWith( + 'Unknown exclude action: enable. Expected list, add, or remove.', + ) + }) + it('writes skill loading guidance by default and is idempotent', async () => { const root = mkdtempSync(join(realTmpdir, 'intent-cli-install-')) const isolatedGlobalRoot = mkdtempSync( @@ -1073,9 +1180,14 @@ describe('cli commands', () => { }) }) - it('excludes packages from list output with --exclude', async () => { + it('excludes packages from list output with package.json intent.exclude', async () => { const root = mkdtempSync(join(realTmpdir, 'intent-cli-list-exclude-')) tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: 'app', + private: true, + intent: { exclude: ['@tanstack/*devtools*'] }, + }) writeInstalledIntentPackage(root, { name: '@tanstack/query', version: '5.0.0', @@ -1091,12 +1203,7 @@ describe('cli commands', () => { process.chdir(root) - const exitCode = await main([ - 'list', - '--json', - '--exclude', - '@tanstack/*devtools*', - ]) + const exitCode = await main(['list', '--json']) const output = logSpy.mock.calls.at(-1)?.[0] const parsed = JSON.parse(String(output)) as { packages: Array<{ name: string }> @@ -1512,9 +1619,14 @@ describe('cli commands', () => { ) }) - it('fails clearly when loading an excluded package', async () => { + it('fails clearly when loading a package excluded by package.json', async () => { const root = mkdtempSync(join(realTmpdir, 'intent-cli-load-exclude-')) tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: 'app', + private: true, + intent: { exclude: ['@tanstack/*devtools*'] }, + }) writeInstalledIntentPackage(root, { name: '@tanstack/devtools', version: '1.0.0', @@ -1523,12 +1635,7 @@ describe('cli commands', () => { }) process.chdir(root) - const exitCode = await main([ - 'load', - '@tanstack/devtools#panel', - '--exclude', - '@tanstack/*devtools*', - ]) + const exitCode = await main(['load', '@tanstack/devtools#panel']) expect(exitCode).toBe(1) expect(errorSpy).toHaveBeenCalledWith( From 9a29605b4b82a503813b8ed14a80bb27413ca290 Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Sat, 13 Jun 2026 23:28:16 -0700 Subject: [PATCH 5/7] changeset --- .changeset/green-dingos-refuse.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/green-dingos-refuse.md diff --git a/.changeset/green-dingos-refuse.md b/.changeset/green-dingos-refuse.md new file mode 100644 index 0000000..65d00f7 --- /dev/null +++ b/.changeset/green-dingos-refuse.md @@ -0,0 +1,12 @@ +--- +'@tanstack/intent': minor +--- + +Add a persistent `intent exclude` command for managing `package.json#intent.exclude` (`list`, `add`, `remove`), and document it in the CLI/config guides. + +Add notice suppression controls for automation: + +- `--no-notices` on `intent list` and `intent install` +- `INTENT_NO_NOTICES=1` environment variable + +Remove one-off CLI exclude flags from command surfaces (`list/load --exclude`); excludes are now managed via `package.json#intent.exclude` and `intent exclude`. From f6596a017adc6e261205c9afcf78d50a9299eccd Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Sat, 13 Jun 2026 23:34:09 -0700 Subject: [PATCH 6/7] fix knip error --- packages/intent/src/cli-output.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/intent/src/cli-output.ts b/packages/intent/src/cli-output.ts index 6c465d9..58c97b2 100644 --- a/packages/intent/src/cli-output.ts +++ b/packages/intent/src/cli-output.ts @@ -18,9 +18,7 @@ function envSuppressesNotices(): boolean { return value ? TRUE_LIKE_VALUES.has(value) : false } -export function shouldSuppressNotices( - options: NoticeOutputOptions = {}, -): boolean { +function shouldSuppressNotices(options: NoticeOutputOptions = {}): boolean { return options.noNotices === true || envSuppressesNotices() } From ded082f58acefd76b60774c62f6ffd0f74f42a32 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 06:38:15 +0000 Subject: [PATCH 7/7] ci: apply automated fixes --- packages/intent/src/cli-support.ts | 6 +- packages/intent/src/cli.ts | 8 +- packages/intent/src/commands/exclude.ts | 372 ++++++++++++------------ packages/intent/src/commands/install.ts | 12 +- packages/intent/tests/cli.test.ts | 12 +- 5 files changed, 212 insertions(+), 198 deletions(-) diff --git a/packages/intent/src/cli-support.ts b/packages/intent/src/cli-support.ts index d1788d4..a79843d 100644 --- a/packages/intent/src/cli-support.ts +++ b/packages/intent/src/cli-support.ts @@ -95,9 +95,9 @@ export function coreOptionsFromGlobalFlags( } } -export function noticeOptionsFromGlobalFlags( - options: GlobalScanFlags, -): { noNotices?: boolean } { +export function noticeOptionsFromGlobalFlags(options: GlobalScanFlags): { + noNotices?: boolean +} { return { noNotices: options.noNotices || options.notices === false } } diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 8037480..2ccd7f2 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -21,9 +21,7 @@ function createCli(): CAC { 'list', 'Discover intent-enabled packages from the project or workspace', ) - .usage( - 'list [--json] [--debug] [--global] [--global-only] [--no-notices]', - ) + .usage('list [--json] [--debug] [--global] [--global-only] [--no-notices]') .option('--json', 'Output JSON') .option('--debug', 'Print discovery debug details to stderr') .option('--global', 'Include global packages after project packages') @@ -61,9 +59,7 @@ function createCli(): CAC { cli .command('load [use]', 'Load a compact skill use and print its SKILL.md') - .usage( - 'load [--path] [--json] [--debug] [--global] [--global-only]', - ) + .usage('load [--path] [--json] [--debug] [--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') diff --git a/packages/intent/src/commands/exclude.ts b/packages/intent/src/commands/exclude.ts index 71b1df5..2b6f3b3 100644 --- a/packages/intent/src/commands/exclude.ts +++ b/packages/intent/src/commands/exclude.ts @@ -1,180 +1,192 @@ -import { existsSync, readFileSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' -import { fail } from '../cli-error.js' -import { compileExcludePatterns } from '../core/excludes.js' - -export interface ExcludeCommandOptions { - json?: boolean -} - -type ExcludeAction = 'add' | 'list' | 'remove' - -function normalizeAction(action: string | undefined): ExcludeAction { - if (!action) return 'list' - if (action === 'list' || action === 'add' || action === 'remove') return action - fail(`Unknown exclude action: ${action}. Expected list, add, or remove.`) -} - -function getPackageJsonPath(cwd: string): string { - return join(cwd, 'package.json') -} - -function readPackageJson(cwd: string): Record { - const packageJsonPath = getPackageJsonPath(cwd) - if (!existsSync(packageJsonPath)) { - fail(`No package.json found in ${cwd}`) - } - - try { - return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record< - string, - unknown - > - } catch (err) { - fail( - `Failed to parse ${packageJsonPath}: ${err instanceof Error ? err.message : String(err)}`, - ) - } -} - -function readConfiguredExcludes(pkg: Record): Array { - const intent = pkg.intent - if (intent === undefined) return [] - if (!intent || typeof intent !== 'object') { - fail('Invalid package.json: intent must be an object when present.') - } - - const raw = (intent as Record).exclude - if (raw === undefined) return [] - if (!Array.isArray(raw)) { - fail('Invalid package.json: intent.exclude must be an array of strings.') - } - - const excludes: Array = [] - for (const entry of raw) { - if (typeof entry !== 'string') { - fail('Invalid package.json: intent.exclude must contain only strings.') - } - const trimmed = entry.trim() - if (trimmed.length === 0) continue - excludes.push(trimmed) - } - return excludes -} - -function setConfiguredExcludes( - pkg: Record, - excludes: Array, -): void { - const intent = - pkg.intent && typeof pkg.intent === 'object' - ? (pkg.intent as Record) - : {} - - intent.exclude = excludes - pkg.intent = intent -} - -function writePackageJson(cwd: string, pkg: Record): void { - const packageJsonPath = getPackageJsonPath(cwd) - writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8') -} - -function normalizePattern(pattern: string | undefined, action: ExcludeAction): string { - if (!pattern) { - fail(`Missing exclude pattern. Expected: intent exclude ${action} `) - } - const trimmed = pattern.trim() - if (trimmed.length === 0) { - fail(`Missing exclude pattern. Expected: intent exclude ${action} `) - } - return trimmed -} - -function validatePattern(pattern: string): void { - try { - compileExcludePatterns([pattern]) - } catch (err) { - fail( - `Invalid exclude pattern "${pattern}": ${err instanceof Error ? err.message : String(err)}`, - ) - } -} - -function printExcludes(excludes: Array, json?: boolean): void { - if (json) { - console.log(JSON.stringify(excludes, null, 2)) - return - } - - if (excludes.length === 0) { - console.log('No excludes configured.') - return - } - - console.log('Configured excludes:') - for (const pattern of excludes) { - console.log(`- ${pattern}`) - } -} - -export async function runExcludeCommand( - actionArg: string | undefined, - patternArg: string | undefined, - options: ExcludeCommandOptions, -): Promise { - const action = normalizeAction(actionArg) - const cwd = process.cwd() - const pkg = readPackageJson(cwd) - const currentExcludes = readConfiguredExcludes(pkg) - - if (action === 'list') { - if (patternArg) { - fail('Unexpected pattern for list. Use: intent exclude list [--json]') - } - printExcludes(currentExcludes, options.json) - return - } - - const pattern = normalizePattern(patternArg, action) - validatePattern(pattern) - - if (action === 'add') { - if (currentExcludes.includes(pattern)) { - if (options.json) { - printExcludes(currentExcludes, true) - return - } - console.log(`Exclude pattern "${pattern}" is already configured.`) - return - } - - const updated = [...currentExcludes, pattern] - setConfiguredExcludes(pkg, updated) - writePackageJson(cwd, pkg) - if (options.json) { - printExcludes(updated, true) - return - } - console.log(`Added exclude pattern "${pattern}" to package.json intent.exclude.`) - return - } - - const updated = currentExcludes.filter((value) => value !== pattern) - if (updated.length === currentExcludes.length) { - if (options.json) { - printExcludes(currentExcludes, true) - return - } - console.log(`Exclude pattern "${pattern}" is not configured.`) - return - } - - setConfiguredExcludes(pkg, updated) - writePackageJson(cwd, pkg) - if (options.json) { - printExcludes(updated, true) - return - } - console.log(`Removed exclude pattern "${pattern}" from package.json intent.exclude.`) -} +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { fail } from '../cli-error.js' +import { compileExcludePatterns } from '../core/excludes.js' + +export interface ExcludeCommandOptions { + json?: boolean +} + +type ExcludeAction = 'add' | 'list' | 'remove' + +function normalizeAction(action: string | undefined): ExcludeAction { + if (!action) return 'list' + if (action === 'list' || action === 'add' || action === 'remove') + return action + fail(`Unknown exclude action: ${action}. Expected list, add, or remove.`) +} + +function getPackageJsonPath(cwd: string): string { + return join(cwd, 'package.json') +} + +function readPackageJson(cwd: string): Record { + const packageJsonPath = getPackageJsonPath(cwd) + if (!existsSync(packageJsonPath)) { + fail(`No package.json found in ${cwd}`) + } + + try { + return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as Record< + string, + unknown + > + } catch (err) { + fail( + `Failed to parse ${packageJsonPath}: ${err instanceof Error ? err.message : String(err)}`, + ) + } +} + +function readConfiguredExcludes(pkg: Record): Array { + const intent = pkg.intent + if (intent === undefined) return [] + if (!intent || typeof intent !== 'object') { + fail('Invalid package.json: intent must be an object when present.') + } + + const raw = (intent as Record).exclude + if (raw === undefined) return [] + if (!Array.isArray(raw)) { + fail('Invalid package.json: intent.exclude must be an array of strings.') + } + + const excludes: Array = [] + for (const entry of raw) { + if (typeof entry !== 'string') { + fail('Invalid package.json: intent.exclude must contain only strings.') + } + const trimmed = entry.trim() + if (trimmed.length === 0) continue + excludes.push(trimmed) + } + return excludes +} + +function setConfiguredExcludes( + pkg: Record, + excludes: Array, +): void { + const intent = + pkg.intent && typeof pkg.intent === 'object' + ? (pkg.intent as Record) + : {} + + intent.exclude = excludes + pkg.intent = intent +} + +function writePackageJson(cwd: string, pkg: Record): void { + const packageJsonPath = getPackageJsonPath(cwd) + writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8') +} + +function normalizePattern( + pattern: string | undefined, + action: ExcludeAction, +): string { + if (!pattern) { + fail( + `Missing exclude pattern. Expected: intent exclude ${action} `, + ) + } + const trimmed = pattern.trim() + if (trimmed.length === 0) { + fail( + `Missing exclude pattern. Expected: intent exclude ${action} `, + ) + } + return trimmed +} + +function validatePattern(pattern: string): void { + try { + compileExcludePatterns([pattern]) + } catch (err) { + fail( + `Invalid exclude pattern "${pattern}": ${err instanceof Error ? err.message : String(err)}`, + ) + } +} + +function printExcludes(excludes: Array, json?: boolean): void { + if (json) { + console.log(JSON.stringify(excludes, null, 2)) + return + } + + if (excludes.length === 0) { + console.log('No excludes configured.') + return + } + + console.log('Configured excludes:') + for (const pattern of excludes) { + console.log(`- ${pattern}`) + } +} + +export async function runExcludeCommand( + actionArg: string | undefined, + patternArg: string | undefined, + options: ExcludeCommandOptions, +): Promise { + const action = normalizeAction(actionArg) + const cwd = process.cwd() + const pkg = readPackageJson(cwd) + const currentExcludes = readConfiguredExcludes(pkg) + + if (action === 'list') { + if (patternArg) { + fail('Unexpected pattern for list. Use: intent exclude list [--json]') + } + printExcludes(currentExcludes, options.json) + return + } + + const pattern = normalizePattern(patternArg, action) + validatePattern(pattern) + + if (action === 'add') { + if (currentExcludes.includes(pattern)) { + if (options.json) { + printExcludes(currentExcludes, true) + return + } + console.log(`Exclude pattern "${pattern}" is already configured.`) + return + } + + const updated = [...currentExcludes, pattern] + setConfiguredExcludes(pkg, updated) + writePackageJson(cwd, pkg) + if (options.json) { + printExcludes(updated, true) + return + } + console.log( + `Added exclude pattern "${pattern}" to package.json intent.exclude.`, + ) + return + } + + const updated = currentExcludes.filter((value) => value !== pattern) + if (updated.length === currentExcludes.length) { + if (options.json) { + printExcludes(currentExcludes, true) + return + } + console.log(`Exclude pattern "${pattern}" is not configured.`) + return + } + + setConfiguredExcludes(pkg, updated) + writePackageJson(cwd, pkg) + if (options.json) { + printExcludes(updated, true) + return + } + console.log( + `Removed exclude pattern "${pattern}" from package.json intent.exclude.`, + ) +} diff --git a/packages/intent/src/commands/install.ts b/packages/intent/src/commands/install.ts index d624447..020b0e9 100644 --- a/packages/intent/src/commands/install.ts +++ b/packages/intent/src/commands/install.ts @@ -259,12 +259,12 @@ export async function runInstallCommand( ) if (!targetPath) { - printNoActionableSkills( - scanResult.warnings, - scanResult.notices, - noticeOptions, - ) - return + printNoActionableSkills( + scanResult.warnings, + scanResult.notices, + noticeOptions, + ) + return } console.log( diff --git a/packages/intent/tests/cli.test.ts b/packages/intent/tests/cli.test.ts index f696cb1..c7385a1 100644 --- a/packages/intent/tests/cli.test.ts +++ b/packages/intent/tests/cli.test.ts @@ -250,7 +250,9 @@ describe('cli commands', () => { '@tanstack/router#experimental-*', ]) const listExitCode = await main(['exclude']) - const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { + const pkg = JSON.parse( + readFileSync(join(root, 'package.json'), 'utf8'), + ) as { intent?: { exclude?: Array } } const output = logSpy.mock.calls.flat().join('\n') @@ -279,7 +281,9 @@ describe('cli commands', () => { 'remove', '@tanstack/router#experimental-*', ]) - const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { + const pkg = JSON.parse( + readFileSync(join(root, 'package.json'), 'utf8'), + ) as { intent?: { exclude?: Array } } @@ -903,7 +907,9 @@ describe('cli commands', () => { }) it('suppresses notices when INTENT_NO_NOTICES=1 is set', async () => { - const root = mkdtempSync(join(realTmpdir, 'intent-cli-list-env-no-notices-')) + const root = mkdtempSync( + join(realTmpdir, 'intent-cli-list-env-no-notices-'), + ) const isolatedGlobalRoot = mkdtempSync( join(realTmpdir, 'intent-cli-list-env-no-notices-empty-global-'), )