diff --git a/.changeset/thin-rings-behave.md b/.changeset/thin-rings-behave.md new file mode 100644 index 0000000..319cb55 --- /dev/null +++ b/.changeset/thin-rings-behave.md @@ -0,0 +1,7 @@ +--- +'@tanstack/intent': patch +--- + +Refactored the CLI to use `cac`, replacing the previous hand-rolled parsing and dispatch logic with a more structured command system. + +This update also fixes monorepo workflow generation behavior related to `setup-github-actions`, improving repo/package fallback handling and ensuring generated workflow watch paths are monorepo-aware. diff --git a/docs/cli/intent-setup.md b/docs/cli/intent-setup.md index 2effee9..39ef9b0 100644 --- a/docs/cli/intent-setup.md +++ b/docs/cli/intent-setup.md @@ -24,7 +24,9 @@ npx @tanstack/intent@latest setup-github-actions - 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` + - Applies variable substitution (`PACKAGE_NAME`, `PACKAGE_LABEL`, `PAYLOAD_PACKAGE`, `REPO`, `DOCS_PATH`, `SRC_PATH`, `WATCH_PATHS`) + - Detects the workspace root in monorepos and writes repo-level workflows there + - Generates monorepo-aware watch paths for package `src/` and docs directories - Skips files that already exist at destination ## Required `files` entries @@ -42,6 +44,7 @@ npx @tanstack/intent@latest setup-github-actions ## Notes - `setup-github-actions` skips existing files +- In monorepos, run `setup-github-actions` from either the repo root or a package directory; Intent writes workflows to the workspace root ## Related diff --git a/packages/intent/package.json b/packages/intent/package.json index 1d58c4f..f0d40b6 100644 --- a/packages/intent/package.json +++ b/packages/intent/package.json @@ -27,6 +27,7 @@ "meta" ], "dependencies": { + "cac": "^6.7.14", "yaml": "^2.7.0" }, "devDependencies": { @@ -37,6 +38,7 @@ "scripts": { "prepack": "npm run build", "build": "tsdown src/index.ts src/cli.ts src/setup.ts src/intent-library.ts src/library-scanner.ts --format esm --dts", + "test:smoke": "pnpm run build && node dist/cli.mjs --help > /dev/null", "test:lib": "vitest run --exclude 'tests/integration/**'", "test:integration": "vitest run tests/integration/", "test:types": "tsc --noEmit" diff --git a/packages/intent/src/cli-error.ts b/packages/intent/src/cli-error.ts new file mode 100644 index 0000000..ffe8ecd --- /dev/null +++ b/packages/intent/src/cli-error.ts @@ -0,0 +1,18 @@ +const CLI_FAILURE = Symbol('CliFailure') + +export type CliFailure = { + readonly [CLI_FAILURE]: true + message: string + exitCode: number +} + +// Throws a structured CliFailure (not an Error) — this represents an expected +// user-facing failure, not an internal bug. Stack traces are intentionally +// omitted since these are anticipated exit paths (bad input, missing files, etc). +export function fail(message: string, exitCode = 1): never { + throw { [CLI_FAILURE]: true as const, message, exitCode } satisfies CliFailure +} + +export function isCliFailure(value: unknown): value is CliFailure { + return !!value && typeof value === 'object' && CLI_FAILURE in value +} diff --git a/packages/intent/src/cli-support.ts b/packages/intent/src/cli-support.ts new file mode 100644 index 0000000..1535d40 --- /dev/null +++ b/packages/intent/src/cli-support.ts @@ -0,0 +1,86 @@ +import { existsSync, readFileSync } from 'node:fs' +import { dirname, join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' +import { fail } from './cli-error.js' +import type { ScanResult, StalenessReport } from './types.js' + +export function printWarnings(warnings: Array): void { + if (warnings.length === 0) return + + console.log('Warnings:') + for (const warning of warnings) { + console.log(` ⚠ ${warning}`) + } +} + +export function getMetaDir(): string { + const thisDir = dirname(fileURLToPath(import.meta.url)) + return join(thisDir, '..', 'meta') +} + +export async function scanIntentsOrFail(): Promise { + const { scanForIntents } = await import('./scanner.js') + + try { + return scanForIntents() + } catch (err) { + fail(err instanceof Error ? err.message : String(err)) + } +} + +function readPackageName(root: string): string { + try { + const pkgJson = JSON.parse( + readFileSync(join(root, 'package.json'), 'utf8'), + ) as { + name?: unknown + } + return typeof pkgJson.name === 'string' + ? pkgJson.name + : relative(process.cwd(), root) || 'unknown' + } catch { + return relative(process.cwd(), root) || 'unknown' + } +} + +export async function resolveStaleTargets( + targetDir?: string, +): Promise<{ reports: Array }> { + const resolvedRoot = targetDir + ? join(process.cwd(), targetDir) + : process.cwd() + const { checkStaleness } = await import('./staleness.js') + + if (existsSync(join(resolvedRoot, 'skills'))) { + return { + reports: [ + await checkStaleness(resolvedRoot, readPackageName(resolvedRoot)), + ], + } + } + + const { findPackagesWithSkills, findWorkspaceRoot } = + await import('./setup.js') + const workspaceRoot = findWorkspaceRoot(resolvedRoot) + if (workspaceRoot) { + const packageDirs = findPackagesWithSkills(workspaceRoot) + if (packageDirs.length > 0) { + return { + reports: await Promise.all( + packageDirs.map((packageDir) => + checkStaleness(packageDir, readPackageName(packageDir)), + ), + ), + } + } + } + + const staleResult = await scanIntentsOrFail() + return { + reports: await Promise.all( + staleResult.packages.map((pkg) => + checkStaleness(pkg.packageRoot, pkg.name), + ), + ), + } +} diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index 0c74230..3af0449 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -1,751 +1,163 @@ #!/usr/bin/env node -import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs' -import { dirname, join, relative, sep } from 'node:path' +import { realpathSync } from 'node:fs' import { fileURLToPath } from 'node:url' -import { INSTALL_PROMPT } from './install-prompt.js' -import type { ScanResult } from './types.js' - -function getMetaDir(): string { - const thisDir = dirname(fileURLToPath(import.meta.url)) - return join(thisDir, '..', 'meta') -} - -type CliFailure = { - message: string - exitCode: number -} - -function fail(message: string, exitCode = 1): never { - throw { message, exitCode } satisfies CliFailure -} - -function isCliFailure(value: unknown): value is CliFailure { - return ( - !!value && - typeof value === 'object' && - 'message' in value && - typeof value.message === 'string' && - 'exitCode' in value && - typeof value.exitCode === 'number' - ) -} - -async function scanIntentsOrFail(): Promise { - const { scanForIntents } = await import('./scanner.js') - - try { - return scanForIntents() - } catch (err) { - fail((err as Error).message) - } -} - -function printWarnings(warnings: Array): void { - if (warnings.length === 0) return - - console.log('Warnings:') - for (const warning of warnings) { - console.log(` ⚠ ${warning}`) - } -} - -function formatScanCoverage(result: ScanResult): string { - const coverage: Array = [] - - if (result.nodeModules.local.scanned) coverage.push('project node_modules') - if (result.nodeModules.global.scanned) coverage.push('global node_modules') - - return coverage.join(', ') -} - -function printVersionConflicts(result: ScanResult): void { - if (result.conflicts.length === 0) return - - console.log('\nVersion conflicts:\n') - for (const conflict of result.conflicts) { - console.log(` ${conflict.packageName} -> using ${conflict.chosen.version}`) - console.log(` chosen: ${conflict.chosen.packageRoot}`) - - for (const variant of conflict.variants) { - if (variant.packageRoot === conflict.chosen.packageRoot) continue - console.log( - ` also found: ${variant.version} at ${variant.packageRoot}`, - ) - } - - console.log() - } -} - -function buildValidationFailure( - errors: Array<{ file: string; message: string }>, - warnings: Array, -): string { - const lines = ['', `❌ Validation failed with ${errors.length} error(s):`, ''] - - for (const { file, message } of errors) { - lines.push(` ${file}: ${message}`) - } - - if (warnings.length > 0) { - lines.push('', '⚠ Packaging warnings:') - for (const warning of warnings) { - lines.push(` ${warning}`) - } - } - - return lines.join('\n') -} - -async function cmdList(args: Array): Promise { - const { computeSkillNameWidth, printSkillTree, printTable } = - await import('./display.js') - const jsonOutput = args.includes('--json') - const result = await scanIntentsOrFail() - - if (jsonOutput) { - console.log(JSON.stringify(result, null, 2)) - return - } - - const scanCoverage = formatScanCoverage(result) - - if (result.packages.length === 0) { - console.log('No intent-enabled packages found.') - if (scanCoverage) console.log(`Scanned: ${scanCoverage}`) - if (result.warnings.length > 0) { - console.log() - printWarnings(result.warnings) - } - return - } - - const totalSkills = result.packages.reduce( - (sum, p) => sum + p.skills.length, - 0, - ) - console.log( - `\n${result.packages.length} intent-enabled packages, ${totalSkills} skills (${result.packageManager})\n`, - ) - if (scanCoverage) { - console.log( - `Scanned: ${scanCoverage}${result.nodeModules.global.scanned ? ' (local packages take precedence)' : ''}\n`, +import { type CAC, cac } from 'cac' +import { fail, isCliFailure } from './cli-error.js' +import { + getMetaDir, + resolveStaleTargets, + scanIntentsOrFail, +} from './cli-support.js' +import { runEditPackageJsonCommand } from './commands/edit-package-json.js' +import { runInstallCommand } from './commands/install.js' +import { runListCommand } from './commands/list.js' +import { runMetaCommand } from './commands/meta.js' +import { runScaffoldCommand } from './commands/scaffold.js' +import { runSetupGithubActionsCommand } from './commands/setup-github-actions.js' +import { runStaleCommand } from './commands/stale.js' +import { runValidateCommand } from './commands/validate.js' + +function createCli(): CAC { + const cli = cac('intent') + cli.usage(' [options]') + + cli + .command('list', 'Discover intent-enabled packages') + .usage('list [--json]') + .option('--json', 'Output JSON') + .example('list') + .example('list --json') + .action(async (options: { json?: boolean }) => { + await runListCommand(options, scanIntentsOrFail) + }) + + cli + .command('meta [name]', 'List meta-skills, or print one by name') + .usage('meta [name]') + .example('meta') + .example('meta domain-discovery') + .action(async (name?: string) => { + await runMetaCommand(name, getMetaDir()) + }) + + cli + .command('validate [dir]', 'Validate skill files') + .usage('validate [dir]') + .example('validate') + .example('validate packages/query/skills') + .action(async (dir?: string) => { + await runValidateCommand(dir) + }) + + cli + .command( + 'install', + 'Print a skill that guides your coding agent to set up skill-to-task mappings', + ) + .usage('install') + .action(() => { + runInstallCommand() + }) + + cli + .command('scaffold', 'Print maintainer scaffold prompt') + .usage('scaffold') + .action(() => { + runScaffoldCommand(getMetaDir()) + }) + + cli + .command('stale [dir]', 'Check skills for staleness') + .usage('stale [dir] [--json]') + .option('--json', 'Output JSON') + .example('stale') + .example('stale packages/query') + .example('stale --json') + .action( + async (targetDir: string | undefined, options: { json?: boolean }) => { + await runStaleCommand(targetDir, options, resolveStaleTargets) + }, ) - } - - // Summary table - const rows = result.packages.map((pkg) => [ - pkg.name, - pkg.version, - String(pkg.skills.length), - pkg.intent.requires?.join(', ') || '–', - ]) - printTable(['PACKAGE', 'VERSION', 'SKILLS', 'REQUIRES'], rows) - - printVersionConflicts(result) - - // Skills detail - const allSkills = result.packages.map((p) => p.skills) - const nameWidth = computeSkillNameWidth(allSkills) - const showTypes = result.packages.some((p) => p.skills.some((s) => s.type)) - - console.log(`\nSkills:\n`) - for (const pkg of result.packages) { - console.log(` ${pkg.name}`) - printSkillTree(pkg.skills, { nameWidth, showTypes }) - console.log() - } - - console.log(`Feedback:`) - console.log( - ` Submit feedback on skill usage to help maintainers improve the skills.`, - ) - console.log( - ` Load: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md`, - ) - console.log() - - printWarnings(result.warnings) -} - -async function cmdMeta(args: Array): Promise { - const { parseFrontmatter } = await import('./utils.js') - const metaDir = getMetaDir() - - if (!existsSync(metaDir)) { - fail('Meta-skills directory not found.') - } - - if (args.length > 0) { - const name = args[0]! - if (name.includes('..') || name.includes('/') || name.includes('\\')) { - fail(`Invalid meta-skill name: "${name}"`) - } - const skillFile = join(metaDir, name, 'SKILL.md') - if (!existsSync(skillFile)) { - fail( - `Meta-skill "${name}" not found. Run \`intent meta\` to list available meta-skills.`, - ) - } - try { - console.log(readFileSync(skillFile, 'utf8')) - } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - fail(`Failed to read meta-skill "${name}": ${msg}`) - } - return - } - - const entries = readdirSync(metaDir, { withFileTypes: true }) - .filter((e) => e.isDirectory()) - .filter((e) => existsSync(join(metaDir, e.name, 'SKILL.md'))) - - if (entries.length === 0) { - console.log('No meta-skills found.') - return - } - - console.log('Meta-skills (for library maintainers):\n') - - for (const entry of entries) { - const skillFile = join(metaDir, entry.name, 'SKILL.md') - const fm = parseFrontmatter(skillFile) - let description = '' - if (typeof fm?.description === 'string') { - description = fm.description.replace(/\s+/g, ' ').trim() - } - - const shortDesc = - description.length > 60 ? description.slice(0, 57) + '...' : description - console.log(` ${entry.name.padEnd(28)} ${shortDesc}`) - } - - console.log(`\nUsage: load the SKILL.md into your AI agent conversation.`) - console.log(`Path: node_modules/@tanstack/intent/meta//SKILL.md`) -} - -function collectPackagingWarnings(root: string): Array { - const pkgJsonPath = join(root, 'package.json') - if (!existsSync(pkgJsonPath)) return [] - - let pkgJson: Record - try { - pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) - } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - return [`Could not parse package.json: ${msg}`] - } - - const warnings: Array = [] - - const devDeps = pkgJson.devDependencies as Record | undefined - if (!devDeps?.['@tanstack/intent']) { - warnings.push('@tanstack/intent is not in devDependencies') - } - - 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 - if (Array.isArray(files)) { - if (!files.includes('skills')) { - warnings.push( - '"skills" is not in the "files" array — skills won\'t be published', - ) - } - // 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 + cli + .command( + 'edit-package-json', + 'Update package.json files so skills are published', + ) + .usage('edit-package-json') + .action(async () => { + await runEditPackageJsonCommand(process.cwd()) + }) + + cli + .command( + 'setup-github-actions', + 'Copy Intent CI workflow templates into .github/workflows/', + ) + .usage('setup-github-actions') + .action(async () => { + await runSetupGithubActionsCommand(process.cwd(), getMetaDir()) + }) + + cli + .command('help [command]', 'Display help for a command') + .action((commandName?: string) => { + if (!commandName) { + cli.outputHelp() + return } - return false - })() - if (!isMonorepoPkg && !files.includes('!skills/_artifacts')) { - warnings.push( - '"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily', + const command = cli.commands.find((candidate) => + candidate.isMatched(commandName), ) - } - } - - return warnings -} - -function resolvePackageRoot(startDir: string): string { - let dir = startDir - - while (true) { - if (existsSync(join(dir, 'package.json'))) { - return dir - } - - const next = dirname(dir) - if (next === dir) { - return startDir - } - - dir = next - } -} - -function readPackageName(root: string): string { - try { - const pkgJson = JSON.parse( - readFileSync(join(root, 'package.json'), 'utf8'), - ) as { - name?: unknown - } - return typeof pkgJson.name === 'string' - ? pkgJson.name - : relative(process.cwd(), root) || 'unknown' - } catch { - return relative(process.cwd(), root) || 'unknown' - } -} - -async function resolveStaleTargets(targetDir?: string) { - const resolvedRoot = targetDir - ? join(process.cwd(), targetDir) - : process.cwd() - const { checkStaleness } = await import('./staleness.js') - if (existsSync(join(resolvedRoot, 'skills'))) { - return { - reports: [ - await checkStaleness(resolvedRoot, readPackageName(resolvedRoot)), - ], - } - } - - const { findPackagesWithSkills, findWorkspaceRoot } = - await import('./setup.js') - const workspaceRoot = findWorkspaceRoot(resolvedRoot) - if (workspaceRoot) { - const packageDirs = findPackagesWithSkills(workspaceRoot) - if (packageDirs.length > 0) { - return { - reports: await Promise.all( - packageDirs.map((packageDir) => - checkStaleness(packageDir, readPackageName(packageDir)), - ), - ), - } - } - } - - const staleResult = await scanIntentsOrFail() - return { - reports: await Promise.all( - staleResult.packages.map((pkg) => - checkStaleness(pkg.packageRoot, pkg.name), - ), - ), - } -} - -async function cmdValidate(args: Array): Promise { - const [{ parse: parseYaml }, { findSkillFiles }] = await Promise.all([ - import('yaml'), - import('./utils.js'), - ]) - const targetDir = args[0] ?? 'skills' - const skillsDir = join(process.cwd(), targetDir) - const packageRoot = resolvePackageRoot(skillsDir) - - if (!existsSync(skillsDir)) { - fail(`Skills directory not found: ${skillsDir}`) - } - - interface ValidationError { - file: string - message: string - } - - const errors: Array = [] - const skillFiles = findSkillFiles(skillsDir) - - if (skillFiles.length === 0) { - fail('No SKILL.md files found') - } - - for (const filePath of skillFiles) { - const rel = relative(process.cwd(), filePath) - const content = readFileSync(filePath, 'utf8') - const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/) - - if (!match) { - errors.push({ file: rel, message: 'Missing or invalid frontmatter' }) - continue - } - - if (!match[1]) { - errors.push({ file: rel, message: 'Missing YAML frontmatter' }) - continue - } - - let fm: Record - try { - fm = parseYaml(match[1]) as Record - } catch { - errors.push({ file: rel, message: 'Invalid YAML frontmatter' }) - continue - } - - if (!fm.name) - errors.push({ file: rel, message: 'Missing required field: name' }) - if (!fm.description) - errors.push({ file: rel, message: 'Missing required field: description' }) - - // Validate name matches directory path - if (typeof fm.name === 'string') { - const expectedPath = relative(skillsDir, filePath) - .replace(/[/\\]SKILL\.md$/, '') - .split(sep) - .join('/') - if (fm.name !== expectedPath) { - errors.push({ - file: rel, - message: `name "${fm.name}" does not match directory path "${expectedPath}"`, - }) - } - } - - // Description character limit - if (typeof fm.description === 'string' && fm.description.length > 1024) { - errors.push({ - file: rel, - message: `Description exceeds 1024 character limit (${fm.description.length} chars)`, - }) - } - - // Framework skills must have requires - if (fm.type === 'framework' && !Array.isArray(fm.requires)) { - errors.push({ - file: rel, - message: 'Framework skills must have a "requires" field', - }) - } - - // Line count - const lineCount = content.split(/\r?\n/).length - if (lineCount > 500) { - errors.push({ - file: rel, - message: `Exceeds 500 line limit (${lineCount} lines). Rewrite for conciseness: move API tables to references/, trim verbose examples, and remove content an agent already knows. Do not simply raise the limit.`, - }) - } - } - - const artifactsDir = join(skillsDir, '_artifacts') - if (existsSync(artifactsDir)) { - const requiredArtifacts = [ - 'domain_map.yaml', - 'skill_spec.md', - 'skill_tree.yaml', - ] - - for (const fileName of requiredArtifacts) { - const artifactPath = join(artifactsDir, fileName) - if (!existsSync(artifactPath)) { - errors.push({ - file: relative(process.cwd(), artifactPath), - message: 'Missing required artifact', - }) - continue - } - - const content = readFileSync(artifactPath, 'utf8') - if (content.trim().length === 0) { - errors.push({ - file: relative(process.cwd(), artifactPath), - message: 'Artifact file is empty', - }) - continue - } - - if (fileName.endsWith('.yaml')) { - try { - parseYaml(content) - } catch { - errors.push({ - file: relative(process.cwd(), artifactPath), - message: 'Invalid YAML in artifact file', - }) - } + if (!command) { + fail(`Unknown command: ${commandName}`) } - } - } - - const warnings = collectPackagingWarnings(packageRoot) - - if (errors.length > 0) { - fail(buildValidationFailure(errors, warnings)) - } - - console.log(`✅ Validated ${skillFiles.length} skill files — all passed`) - if (warnings.length > 0) console.log() - printWarnings(warnings) -} - -function cmdScaffold(): void { - const metaDir = getMetaDir() - const metaSkillPath = (name: string) => join(metaDir, name, 'SKILL.md') - - const prompt = `You are helping a library maintainer scaffold Intent skills. - -Run the three meta skills below **one at a time, in order**. For each step: -1. Load the SKILL.md file specified -2. Follow its instructions completely -3. Present outputs to the maintainer for review -4. Do NOT proceed to the next step until the maintainer confirms - -## Before you start - -Gather this context yourself (do not ask the maintainer — agents should never -ask for information they can discover): -1. Read package.json for library name, repository URL, and homepage/docs URL -2. Detect if this is a monorepo (look for workspaces field, packages/ directory, lerna.json) -3. Use skills/ as the default skills root -4. For monorepos: - - Domain map artifacts go at the REPO ROOT: _artifacts/ - - Skills go INSIDE EACH PACKAGE: packages//skills/ - - Identify which packages are client-facing (usually client SDKs and primary framework adapters) - ---- -## Step 1 — Domain Discovery + command.outputHelp() + }) -Load and follow: ${metaSkillPath('domain-discovery')} + cli.help() -This produces: domain_map.yaml and skill_spec.md in the artifacts directory. -Domain discovery covers the WHOLE library (one domain map even for monorepos). - -**STOP. Review outputs with the maintainer before continuing.** - ---- - -## Step 2 — Tree Generator - -Load and follow: ${metaSkillPath('tree-generator')} - -This produces: skill_tree.yaml in the artifacts directory. -For monorepos, each skill entry should include a \`package\` field. - -**STOP. Review outputs with the maintainer before continuing.** - ---- - -## Step 3 — Generate Skills - -Load and follow: ${metaSkillPath('generate-skill')} - -This produces: individual SKILL.md files. -- Single-repo: skills///SKILL.md -- Monorepo: packages//skills///SKILL.md - ---- - -## After all skills are generated - -1. Run \`intent validate\` in each package directory -2. Commit skills/ and artifacts -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) -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -export const USAGE = `TanStack Intent CLI - -Usage: - intent list [--json] Discover intent-enabled packages - intent meta [name] List meta-skills, or print one by name - 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 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` - -const HELP_BY_COMMAND: Record = { - list: `${USAGE} - -Examples: - intent list - intent list --json`, - meta: `intent meta [name] - -List shipped meta-skills, or print a single meta-skill by name. - -Examples: - intent meta - intent meta domain-discovery`, - validate: `intent validate [dir] - -Validate SKILL.md files in the target directory. - -Examples: - intent validate - intent validate packages/query/skills`, - install: `intent install - -Print the install prompt used to set up skill-to-task mappings.`, - scaffold: `intent scaffold - -Print the guided maintainer prompt for generating skills.`, - stale: `intent stale [dir] [--json] - -Check installed skills for version and source drift. - -Examples: - intent stale - intent stale packages/query - intent stale --json`, - 'edit-package-json': `intent edit-package-json - -Update package.json files so skills are published.`, - 'setup-github-actions': `intent setup-github-actions - -Copy Intent CI workflow templates into .github/workflows/.`, -} - -function isHelpFlag(arg: string | undefined): boolean { - return arg === '-h' || arg === '--help' -} - -function printHelp(command?: string): void { - if (!command) { - console.log(`${USAGE} - -Run \`intent help \` for details on a specific command.`) - return - } - - console.log(HELP_BY_COMMAND[command] ?? USAGE) + return cli } export async function main(argv: Array = process.argv.slice(2)) { - const command = argv[0] - const commandArgs = argv.slice(1) - try { - if (!command || isHelpFlag(command)) { - printHelp() - return 0 - } + const cli = createCli() - if (command === 'help') { - printHelp(commandArgs[0]) + if (argv.length === 0) { + cli.outputHelp() return 0 } - if (isHelpFlag(commandArgs[0])) { - printHelp(command) + // cac expects process.argv format: first two entries (binary + script) are ignored + cli.parse(['intent', 'intent', ...argv], { run: false }) + + if (cli.options.help) { return 0 } - switch (command) { - case 'list': - await cmdList(commandArgs) - return 0 - case 'meta': - await cmdMeta(commandArgs) - return 0 - case 'validate': - await cmdValidate(commandArgs) - return 0 - case 'install': { - console.log(INSTALL_PROMPT) - return 0 - } - case 'scaffold': { - cmdScaffold() - return 0 - } - case 'stale': { - const jsonStale = commandArgs.includes('--json') - const targetDir = commandArgs.find((arg) => !arg.startsWith('-')) - const { reports } = await resolveStaleTargets(targetDir) - - if (reports.length === 0) { - console.log('No intent-enabled packages found.') - return 0 - } - - if (jsonStale) { - console.log(JSON.stringify(reports, null, 2)) - return 0 - } - - for (const report of reports) { - const driftLabel = report.versionDrift - ? ` [${report.versionDrift} drift]` - : '' - const vLabel = - report.skillVersion && report.currentVersion - ? ` (${report.skillVersion} → ${report.currentVersion})` - : '' - console.log(`${report.library}${vLabel}${driftLabel}`) - - const stale = report.skills.filter((s) => s.needsReview) - if (stale.length === 0) { - console.log(' All skills up-to-date') - } else { - for (const skill of stale) { - console.log(` ⚠ ${skill.name}: ${skill.reasons.join(', ')}`) - } - } - console.log() - } - return 0 - } - case 'edit-package-json': { - const { runEditPackageJsonAll } = await import('./setup.js') - runEditPackageJsonAll(process.cwd()) - return 0 - } - case 'setup-github-actions': { - const { runSetupGithubActions } = await import('./setup.js') - runSetupGithubActions(process.cwd(), getMetaDir()) - return 0 - } - default: - printHelp() - return command ? 1 : 0 + if (!cli.matchedCommand) { + cli.outputHelp() + return 1 } + + await cli.runMatchedCommand() + return 0 } catch (err) { if (isCliFailure(err)) { console.error(err.message) return err.exitCode } + if (err instanceof Error) { + console.error(err.message) + return 1 + } + throw err } } diff --git a/packages/intent/src/commands/edit-package-json.ts b/packages/intent/src/commands/edit-package-json.ts new file mode 100644 index 0000000..39a7e07 --- /dev/null +++ b/packages/intent/src/commands/edit-package-json.ts @@ -0,0 +1,4 @@ +export async function runEditPackageJsonCommand(root: string): Promise { + const { runEditPackageJsonAll } = await import('../setup.js') + runEditPackageJsonAll(root) +} diff --git a/packages/intent/src/install-prompt.ts b/packages/intent/src/commands/install.ts similarity index 97% rename from packages/intent/src/install-prompt.ts rename to packages/intent/src/commands/install.ts index 6803823..91fa79a 100644 --- a/packages/intent/src/install-prompt.ts +++ b/packages/intent/src/commands/install.ts @@ -53,3 +53,7 @@ skills: - Keep entries concise - this block is read on every agent task - Preserve all content outside the block tags unchanged - If the user is on Deno, note that this setup is best-effort today and relies on npm interop` + +export function runInstallCommand(): void { + console.log(INSTALL_PROMPT) +} diff --git a/packages/intent/src/commands/list.ts b/packages/intent/src/commands/list.ts new file mode 100644 index 0000000..5a12d8a --- /dev/null +++ b/packages/intent/src/commands/list.ts @@ -0,0 +1,103 @@ +import { printWarnings } from '../cli-support.js' +import type { ScanResult } from '../types.js' + +function formatScanCoverage(result: ScanResult): string { + const coverage: Array = [] + + if (result.nodeModules.local.scanned) coverage.push('project node_modules') + if (result.nodeModules.global.scanned) coverage.push('global node_modules') + + return coverage.join(', ') +} + +function printVersionConflicts(result: ScanResult): void { + if (result.conflicts.length === 0) return + + console.log('\nVersion conflicts:\n') + for (const conflict of result.conflicts) { + console.log(` ${conflict.packageName} -> using ${conflict.chosen.version}`) + console.log(` chosen: ${conflict.chosen.packageRoot}`) + + for (const variant of conflict.variants) { + if (variant.packageRoot === conflict.chosen.packageRoot) continue + console.log( + ` also found: ${variant.version} at ${variant.packageRoot}`, + ) + } + + console.log() + } +} + +export async function runListCommand( + options: { json?: boolean }, + scanIntentsOrFail: () => Promise, +): Promise { + const { computeSkillNameWidth, printSkillTree, printTable } = + await import('../display.js') + const result = await scanIntentsOrFail() + + if (options.json) { + console.log(JSON.stringify(result, null, 2)) + return + } + + const scanCoverage = formatScanCoverage(result) + + if (result.packages.length === 0) { + console.log('No intent-enabled packages found.') + if (scanCoverage) console.log(`Scanned: ${scanCoverage}`) + if (result.warnings.length > 0) { + console.log() + printWarnings(result.warnings) + } + return + } + + const totalSkills = result.packages.reduce( + (sum, pkg) => sum + pkg.skills.length, + 0, + ) + console.log( + `\n${result.packages.length} intent-enabled packages, ${totalSkills} skills (${result.packageManager})\n`, + ) + if (scanCoverage) { + console.log( + `Scanned: ${scanCoverage}${result.nodeModules.global.scanned ? ' (local packages take precedence)' : ''}\n`, + ) + } + + const rows = result.packages.map((pkg) => [ + pkg.name, + pkg.version, + String(pkg.skills.length), + pkg.intent.requires?.join(', ') || '–', + ]) + printTable(['PACKAGE', 'VERSION', 'SKILLS', 'REQUIRES'], rows) + + printVersionConflicts(result) + + const allSkills = result.packages.map((pkg) => pkg.skills) + const nameWidth = computeSkillNameWidth(allSkills) + const showTypes = result.packages.some((pkg) => + pkg.skills.some((skill) => skill.type), + ) + + console.log(`\nSkills:\n`) + for (const pkg of result.packages) { + console.log(` ${pkg.name}`) + printSkillTree(pkg.skills, { nameWidth, showTypes }) + console.log() + } + + console.log('Feedback:') + console.log( + ' Submit feedback on skill usage to help maintainers improve the skills.', + ) + console.log( + ' Load: node_modules/@tanstack/intent/meta/feedback-collection/SKILL.md', + ) + console.log() + + printWarnings(result.warnings) +} diff --git a/packages/intent/src/commands/meta.ts b/packages/intent/src/commands/meta.ts new file mode 100644 index 0000000..3be577e --- /dev/null +++ b/packages/intent/src/commands/meta.ts @@ -0,0 +1,62 @@ +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' +import { fail } from '../cli-error.js' + +export async function runMetaCommand( + name: string | undefined, + metaDir: string, +): Promise { + if (!existsSync(metaDir)) { + fail('Meta-skills directory not found.') + } + + if (name) { + if (name.includes('..') || name.includes('/') || name.includes('\\')) { + fail(`Invalid meta-skill name: "${name}"`) + } + + const skillFile = join(metaDir, name, 'SKILL.md') + if (!existsSync(skillFile)) { + fail( + `Meta-skill "${name}" not found. Run \`intent meta\` to list available meta-skills.`, + ) + } + + try { + console.log(readFileSync(skillFile, 'utf8')) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + fail(`Failed to read meta-skill "${name}": ${msg}`) + } + + return + } + + const { parseFrontmatter } = await import('../utils.js') + const entries = readdirSync(metaDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .filter((entry) => existsSync(join(metaDir, entry.name, 'SKILL.md'))) + + if (entries.length === 0) { + console.log('No meta-skills found.') + return + } + + console.log('Meta-skills (for library maintainers):\n') + + for (const entry of entries) { + const skillFile = join(metaDir, entry.name, 'SKILL.md') + const fm = parseFrontmatter(skillFile) + let description = '' + if (typeof fm?.description === 'string') { + description = fm.description.replace(/\s+/g, ' ').trim() + } + + const shortDesc = + description.length > 60 ? `${description.slice(0, 57)}...` : description + console.log(` ${entry.name.padEnd(28)} ${shortDesc}`) + } + + console.log('\nUsage: load the SKILL.md into your AI agent conversation.') + console.log('Path: node_modules/@tanstack/intent/meta//SKILL.md') +} diff --git a/packages/intent/src/commands/scaffold.ts b/packages/intent/src/commands/scaffold.ts new file mode 100644 index 0000000..cd86f67 --- /dev/null +++ b/packages/intent/src/commands/scaffold.ts @@ -0,0 +1,73 @@ +import { join } from 'node:path' + +export function runScaffoldCommand(metaDir: string): void { + function metaSkillPath(name: string): string { + return join(metaDir, name, 'SKILL.md') + } + + const prompt = `You are helping a library maintainer scaffold Intent skills. + +Run the three meta skills below **one at a time, in order**. For each step: +1. Load the SKILL.md file specified +2. Follow its instructions completely +3. Present outputs to the maintainer for review +4. Do NOT proceed to the next step until the maintainer confirms + +## Before you start + +Gather this context yourself (do not ask the maintainer — agents should never +ask for information they can discover): +1. Read package.json for library name, repository URL, and homepage/docs URL +2. Detect if this is a monorepo (look for workspaces field, packages/ directory, lerna.json) +3. Use skills/ as the default skills root +4. For monorepos: + - Domain map artifacts go at the REPO ROOT: _artifacts/ + - Skills go INSIDE EACH PACKAGE: packages//skills/ + - Identify which packages are client-facing (usually client SDKs and primary framework adapters) + +--- + +## Step 1 — Domain Discovery + +Load and follow: ${metaSkillPath('domain-discovery')} + +This produces: domain_map.yaml and skill_spec.md in the artifacts directory. +Domain discovery covers the WHOLE library (one domain map even for monorepos). + +**STOP. Review outputs with the maintainer before continuing.** + +--- + +## Step 2 — Tree Generator + +Load and follow: ${metaSkillPath('tree-generator')} + +This produces: skill_tree.yaml in the artifacts directory. +For monorepos, each skill entry should include a \`package\` field. + +**STOP. Review outputs with the maintainer before continuing.** + +--- + +## Step 3 — Generate Skills + +Load and follow: ${metaSkillPath('generate-skill')} + +This produces: individual SKILL.md files. +- Single-repo: skills///SKILL.md +- Monorepo: packages//skills///SKILL.md + +--- + +## After all skills are generated + +1. Run \`intent validate\` in each package directory +2. Commit skills/ and artifacts +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) +} diff --git a/packages/intent/src/commands/setup-github-actions.ts b/packages/intent/src/commands/setup-github-actions.ts new file mode 100644 index 0000000..a663592 --- /dev/null +++ b/packages/intent/src/commands/setup-github-actions.ts @@ -0,0 +1,7 @@ +export async function runSetupGithubActionsCommand( + root: string, + metaDir: string, +): Promise { + const { runSetupGithubActions } = await import('../setup.js') + runSetupGithubActions(root, metaDir) +} diff --git a/packages/intent/src/commands/stale.ts b/packages/intent/src/commands/stale.ts new file mode 100644 index 0000000..f216ec0 --- /dev/null +++ b/packages/intent/src/commands/stale.ts @@ -0,0 +1,43 @@ +import type { StalenessReport } from '../types.js' + +export async function runStaleCommand( + targetDir: string | undefined, + options: { json?: boolean }, + resolveStaleTargets: ( + targetDir?: string, + ) => Promise<{ reports: Array }>, +): Promise { + const { reports } = await resolveStaleTargets(targetDir) + + if (reports.length === 0) { + console.log('No intent-enabled packages found.') + return + } + + if (options.json) { + console.log(JSON.stringify(reports, null, 2)) + return + } + + for (const report of reports) { + const driftLabel = report.versionDrift + ? ` [${report.versionDrift} drift]` + : '' + const vLabel = + report.skillVersion && report.currentVersion + ? ` (${report.skillVersion} → ${report.currentVersion})` + : '' + console.log(`${report.library}${vLabel}${driftLabel}`) + + const stale = report.skills.filter((skill) => skill.needsReview) + if (stale.length === 0) { + console.log(' All skills up-to-date') + } else { + for (const skill of stale) { + console.log(` ⚠ ${skill.name}: ${skill.reasons.join(', ')}`) + } + } + + console.log() + } +} diff --git a/packages/intent/src/commands/validate.ts b/packages/intent/src/commands/validate.ts new file mode 100644 index 0000000..636256d --- /dev/null +++ b/packages/intent/src/commands/validate.ts @@ -0,0 +1,250 @@ +import { existsSync, readFileSync } from 'node:fs' +import { dirname, join, relative, sep } from 'node:path' +import { fail } from '../cli-error.js' +import { printWarnings } from '../cli-support.js' + +interface ValidationError { + file: string + message: string +} + +function buildValidationFailure( + errors: Array, + warnings: Array, +): string { + const lines = ['', `❌ Validation failed with ${errors.length} error(s):`, ''] + + for (const { file, message } of errors) { + lines.push(` ${file}: ${message}`) + } + + if (warnings.length > 0) { + lines.push('', '⚠ Packaging warnings:') + for (const warning of warnings) { + lines.push(` ${warning}`) + } + } + + return lines.join('\n') +} + +function isInsideMonorepo(root: string): boolean { + 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 +} + +function collectPackagingWarnings(root: string): Array { + const pkgJsonPath = join(root, 'package.json') + if (!existsSync(pkgJsonPath)) return [] + + let pkgJson: Record + try { + pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + return [`Could not parse package.json: ${msg}`] + } + + const warnings: Array = [] + + const devDeps = pkgJson.devDependencies as Record | undefined + if (!devDeps?.['@tanstack/intent']) { + warnings.push('@tanstack/intent is not in devDependencies') + } + + 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 + if (Array.isArray(files)) { + if (!files.includes('skills')) { + warnings.push( + '"skills" is not in the "files" array — skills won\'t be published', + ) + } + + // In monorepos, _artifacts lives at repo root, not under packages — + // the negation pattern is a no-op and shouldn't be added. + const isMonorepoPkg = isInsideMonorepo(root) + + if (!isMonorepoPkg && !files.includes('!skills/_artifacts')) { + warnings.push( + '"!skills/_artifacts" is not in the "files" array — artifacts will be published unnecessarily', + ) + } + } + + return warnings +} + +function resolvePackageRoot(startDir: string): string { + let dir = startDir + + while (true) { + if (existsSync(join(dir, 'package.json'))) { + return dir + } + + const next = dirname(dir) + if (next === dir) { + return startDir + } + + dir = next + } +} + +export async function runValidateCommand(dir?: string): Promise { + const [{ parse: parseYaml }, { findSkillFiles }] = await Promise.all([ + import('yaml'), + import('../utils.js'), + ]) + const targetDir = dir ?? 'skills' + const skillsDir = join(process.cwd(), targetDir) + const packageRoot = resolvePackageRoot(skillsDir) + + if (!existsSync(skillsDir)) { + fail(`Skills directory not found: ${skillsDir}`) + } + + const errors: Array = [] + const skillFiles = findSkillFiles(skillsDir) + + if (skillFiles.length === 0) { + fail('No SKILL.md files found') + } + + for (const filePath of skillFiles) { + const rel = relative(process.cwd(), filePath) + const content = readFileSync(filePath, 'utf8') + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/) + + if (!match) { + errors.push({ file: rel, message: 'Missing or invalid frontmatter' }) + continue + } + + if (!match[1]) { + errors.push({ file: rel, message: 'Missing YAML frontmatter' }) + continue + } + + let fm: Record + try { + fm = parseYaml(match[1]) as Record + } catch (err) { + const detail = err instanceof Error ? err.message : String(err) + errors.push({ file: rel, message: `Invalid YAML frontmatter: ${detail}` }) + continue + } + + if (!fm.name) { + errors.push({ file: rel, message: 'Missing required field: name' }) + } + if (!fm.description) { + errors.push({ file: rel, message: 'Missing required field: description' }) + } + + if (typeof fm.name === 'string') { + const expectedPath = relative(skillsDir, filePath) + .replace(/[/\\]SKILL\.md$/, '') + .split(sep) + .join('/') + if (fm.name !== expectedPath) { + errors.push({ + file: rel, + message: `name "${fm.name}" does not match directory path "${expectedPath}"`, + }) + } + } + + if (typeof fm.description === 'string' && fm.description.length > 1024) { + errors.push({ + file: rel, + message: `Description exceeds 1024 character limit (${fm.description.length} chars)`, + }) + } + + if (fm.type === 'framework' && !Array.isArray(fm.requires)) { + errors.push({ + file: rel, + message: 'Framework skills must have a "requires" field', + }) + } + + const lineCount = content.split(/\r?\n/).length + if (lineCount > 500) { + errors.push({ + file: rel, + message: `Exceeds 500 line limit (${lineCount} lines). Rewrite for conciseness: move API tables to references/, trim verbose examples, and remove content an agent already knows. Do not simply raise the limit.`, + }) + } + } + + const artifactsDir = join(skillsDir, '_artifacts') + if (existsSync(artifactsDir)) { + const requiredArtifacts = [ + 'domain_map.yaml', + 'skill_spec.md', + 'skill_tree.yaml', + ] + + for (const fileName of requiredArtifacts) { + const artifactPath = join(artifactsDir, fileName) + if (!existsSync(artifactPath)) { + errors.push({ + file: relative(process.cwd(), artifactPath), + message: 'Missing required artifact', + }) + continue + } + + const content = readFileSync(artifactPath, 'utf8') + if (content.trim().length === 0) { + errors.push({ + file: relative(process.cwd(), artifactPath), + message: 'Artifact file is empty', + }) + continue + } + + if (fileName.endsWith('.yaml')) { + try { + parseYaml(content) + } catch (err) { + const detail = err instanceof Error ? err.message : String(err) + errors.push({ + file: relative(process.cwd(), artifactPath), + message: `Invalid YAML in artifact file: ${detail}`, + }) + } + } + } + } + + const warnings = collectPackagingWarnings(packageRoot) + + if (errors.length > 0) { + fail(buildValidationFailure(errors, warnings)) + } + + console.log(`✅ Validated ${skillFiles.length} skill files — all passed`) + if (warnings.length > 0) console.log() + printWarnings(warnings) +} diff --git a/packages/intent/src/intent-library.ts b/packages/intent/src/intent-library.ts index c468674..3922db8 100644 --- a/packages/intent/src/intent-library.ts +++ b/packages/intent/src/intent-library.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { computeSkillNameWidth, printSkillTree, printTable } from './display.js' -import { INSTALL_PROMPT } from './install-prompt.js' +import { INSTALL_PROMPT } from './commands/install.js' import { scanLibrary } from './library-scanner.js' import type { LibraryScanResult } from './library-scanner.js' diff --git a/packages/intent/src/setup.ts b/packages/intent/src/setup.ts index 8ff32f0..0a2ae23 100644 --- a/packages/intent/src/setup.ts +++ b/packages/intent/src/setup.ts @@ -38,6 +38,37 @@ interface TemplateVars { WATCH_PATHS: string } +function isGenericWorkspaceName(name: string, root: string): boolean { + const normalized = name.trim().toLowerCase() + return ( + normalized.length === 0 || + normalized === 'unknown' || + normalized === 'root' || + normalized === 'workspace' || + normalized === 'monorepo' || + normalized === basename(root).toLowerCase() + ) +} + +function deriveWorkspacePackageName( + root: string, + repo: string, + packageDirs: Array, +): string { + const repoName = repo.split('/').filter(Boolean).pop() || basename(root) + + for (const packageDir of packageDirs) { + const pkgJson = readPackageJson(packageDir) + const pkgName = typeof pkgJson.name === 'string' ? pkgJson.name : null + if (pkgName?.startsWith('@')) { + const scope = pkgName.split('/')[0] + return `${scope}/${repoName}` + } + } + + return repoName +} + // --------------------------------------------------------------------------- // Variable detection from package.json // --------------------------------------------------------------------------- @@ -129,30 +160,45 @@ function buildWatchPaths(root: string, packageDirs: Array): string { function detectVars(root: string, packageDirs?: Array): TemplateVars { const pkgJson = readPackageJson(root) - const name = typeof pkgJson.name === 'string' ? pkgJson.name : 'unknown' + const rawName = typeof pkgJson.name === 'string' ? pkgJson.name : 'unknown' const docs = typeof (pkgJson.intent as Record | undefined)?.docs === 'string' ? ((pkgJson.intent as Record).docs as string) : 'docs/' - const repo = detectRepo(pkgJson, name.replace(/^@/, '').replace(/\//, '/')) const isMonorepo = packageDirs !== undefined - const packageLabel = - isMonorepo && name === 'unknown' ? `${basename(root)} workspace` : name + const monorepoFallbackPkg = packageDirs?.[0] + ? readPackageJson(packageDirs[0]) + : null + const repo = detectRepo( + pkgJson, + detectRepo(monorepoFallbackPkg ?? {}, basename(root)), + ) + + let packageName = rawName + if (isMonorepo && isGenericWorkspaceName(rawName, root)) { + packageName = deriveWorkspacePackageName(root, repo, packageDirs) + } - // Best-guess src path from common monorepo patterns - const shortName = name.replace(/^@[^/]+\//, '') - let srcPath = `packages/${shortName}/src/**` - if (existsSync(join(root, 'src'))) { + // Derive srcPath: monorepos use a wildcard; single packages use the short name or fall back to root src/ + const shortName = packageName.replace(/^@[^/]+\//, '') + let srcPath = isMonorepo + ? 'packages/*/src/**' + : `packages/${shortName}/src/**` + if (!isMonorepo && existsSync(join(root, 'src'))) { srcPath = 'src/**' } + const docsPath = isMonorepo ? 'packages/*/docs/**' : docs + return { - PACKAGE_NAME: name, - PACKAGE_LABEL: packageLabel, - PAYLOAD_PACKAGE: packageLabel, + PACKAGE_NAME: packageName, + PACKAGE_LABEL: packageName, + PAYLOAD_PACKAGE: packageName, REPO: repo, - DOCS_PATH: docs.endsWith('**') ? docs : docs.replace(/\/$/, '') + '/**', + DOCS_PATH: docsPath.endsWith('**') + ? docsPath + : docsPath.replace(/\/$/, '') + '/**', SRC_PATH: srcPath, WATCH_PATHS: isMonorepo ? buildWatchPaths(root, packageDirs) diff --git a/packages/intent/tests/cli.test.ts b/packages/intent/tests/cli.test.ts index 756f590..252b081 100644 --- a/packages/intent/tests/cli.test.ts +++ b/packages/intent/tests/cli.test.ts @@ -11,8 +11,8 @@ import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { INSTALL_PROMPT } from '../src/install-prompt.js' -import { main, USAGE } from '../src/cli.js' +import { INSTALL_PROMPT } from '../src/commands/install.js' +import { main } from '../src/cli.js' const thisDir = dirname(fileURLToPath(import.meta.url)) const metaDir = join(thisDir, '..', 'meta') @@ -41,19 +41,28 @@ function writeSkillMd(dir: string, frontmatter: Record): void { let originalCwd: string let logSpy: ReturnType +let infoSpy: ReturnType let errorSpy: ReturnType let tempDirs: Array +function getHelpOutput(): string { + return [...infoSpy.mock.calls, ...logSpy.mock.calls] + .map((call) => String(call[0] ?? '')) + .join('') +} + beforeEach(() => { originalCwd = process.cwd() tempDirs = [] logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}) errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { process.chdir(originalCwd) logSpy.mockRestore() + infoSpy.mockRestore() errorSpy.mockRestore() for (const dir of tempDirs) { if (existsSync(dir)) { @@ -107,35 +116,54 @@ describe('intent meta', () => { describe('cli commands', () => { it('prints top-level help when no command is provided', async () => { const exitCode = await main([]) + const output = getHelpOutput() expect(exitCode).toBe(0) - expect(logSpy.mock.calls[0]?.[0]).toContain(USAGE) - expect(logSpy.mock.calls[0]?.[0]).toContain('Run `intent help `') + expect(output).toContain('Usage:') + expect(output).toContain('$ intent [options]') + expect(output).toContain('Commands:') }) it('prints top-level help for --help', async () => { const exitCode = await main(['--help']) + const output = getHelpOutput() expect(exitCode).toBe(0) - expect(logSpy.mock.calls[0]?.[0]).toContain('Run `intent help `') + expect(output).toContain('Usage:') + expect(output).toContain('$ intent [options]') + }) + + it('prints top-level help for unknown commands', async () => { + const exitCode = await main(['wat']) + const output = getHelpOutput() + + expect(exitCode).toBe(1) + expect(output).toContain('Usage:') + expect(output).toContain('Commands:') }) it('prints command help for help subcommands', async () => { const exitCode = await main(['help', 'validate']) + const output = getHelpOutput() expect(exitCode).toBe(0) - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining('intent validate [dir]'), - ) + expect(output).toContain('$ intent validate [dir]') + }) + + it('fails cleanly for unknown help subcommands', async () => { + const exitCode = await main(['help', 'wat']) + + expect(exitCode).toBe(1) + expect(errorSpy).toHaveBeenCalledWith('Unknown command: wat') }) it('prints command help when --help is passed after a subcommand', async () => { const exitCode = await main(['list', '--help']) + const output = getHelpOutput() expect(exitCode).toBe(0) - expect(logSpy).toHaveBeenCalledWith( - expect.stringContaining('intent list --json'), - ) + expect(output).toContain('$ intent list [--json]') + expect(output).toContain('--json') }) it('prints the install prompt', async () => { @@ -145,6 +173,62 @@ describe('cli commands', () => { expect(logSpy).toHaveBeenCalledWith(INSTALL_PROMPT) }) + it('prints the scaffold prompt', async () => { + const exitCode = await main(['scaffold']) + const output = String(logSpy.mock.calls[0]?.[0]) + + expect(exitCode).toBe(0) + expect(output).toContain('## Step 1') + expect(output).toContain('meta/domain-discovery/SKILL.md') + }) + + it('updates package.json for skill publishing', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-edit-package-json-')) + tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: 'pkg', + version: '1.0.0', + }) + + process.chdir(root) + + const exitCode = await main(['edit-package-json']) + const pkg = JSON.parse( + readFileSync(join(root, 'package.json'), 'utf8'), + ) as { + keywords?: Array + files?: Array + } + const output = logSpy.mock.calls.flat().join('\n') + + expect(exitCode).toBe(0) + expect(pkg.keywords).toContain('tanstack-intent') + expect(pkg.files).toContain('skills') + expect(pkg.files).toContain('!skills/_artifacts') + expect(output).toContain('Added keywords: "tanstack-intent"') + }) + + it('copies github workflow templates', async () => { + const root = mkdtempSync(join(realTmpdir, 'intent-cli-setup-gha-')) + tempDirs.push(root) + writeJson(join(root, 'package.json'), { + name: '@scope/pkg', + version: '1.0.0', + intent: { version: 1, repo: 'scope/pkg', docs: 'docs/' }, + }) + + process.chdir(root) + + const exitCode = await main(['setup-github-actions']) + const workflowsDir = join(root, '.github', 'workflows') + const output = logSpy.mock.calls.flat().join('\n') + + expect(exitCode).toBe(0) + expect(existsSync(workflowsDir)).toBe(true) + expect(output).toContain('Copied workflow:') + expect(output).toContain('Template variables applied:') + }) + it('lists installed intent packages as json', async () => { const root = mkdtempSync(join(realTmpdir, 'intent-cli-list-')) tempDirs.push(root) diff --git a/packages/intent/tests/setup.test.ts b/packages/intent/tests/setup.test.ts index c74940b..0e37ad2 100644 --- a/packages/intent/tests/setup.test.ts +++ b/packages/intent/tests/setup.test.ts @@ -255,7 +255,7 @@ describe('runSetupGithubActions', () => { writeFileSync( join(monoRoot, 'package.json'), JSON.stringify( - { name: '@tanstack/router', private: true, workspaces: ['packages/*'] }, + { name: 'root', private: true, workspaces: ['packages/*'] }, null, 2, ), @@ -295,9 +295,11 @@ describe('runSetupGithubActions', () => { 'utf8', ) expect(notifyContent).toContain('package: @tanstack/router') + expect(notifyContent).toContain('repo: TanStack/router') expect(notifyContent).toContain("- 'packages/router/docs/**'") expect(notifyContent).toContain("- 'packages/router/src/**'") expect(notifyContent).toContain("- 'packages/start/src/**'") + expect(notifyContent).not.toContain('packages/root/src/**') const checkContent = readFileSync( join(monoRoot, '.github', 'workflows', 'check-skills.yml'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e95ad1b..1849c7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,16 +23,16 @@ importers: version: 1.2.0 '@tanstack/eslint-config': specifier: 0.4.0 - version: 0.4.0(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 0.4.0(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@types/node': specifier: ^25.0.7 version: 25.0.9 eslint: specifier: ^9.39.2 - version: 9.39.2(jiti@2.6.1) + version: 9.39.4(jiti@2.6.1) eslint-plugin-unused-imports: specifier: ^4.3.0 - version: 4.3.0(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) + version: 4.3.0(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) happy-dom: specifier: ^20.1.0 version: 20.3.1 @@ -69,6 +69,9 @@ importers: packages/intent: dependencies: + cac: + specifier: ^6.7.14 + version: 6.7.14 yaml: specifier: ^2.7.0 version: 2.8.2 @@ -347,8 +350,8 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/config-helpers@0.4.2': @@ -359,8 +362,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@10.0.1': @@ -372,8 +375,8 @@ packages: eslint: optional: true - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -1430,6 +1433,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -1450,8 +1458,8 @@ packages: ajv: optional: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} @@ -2094,8 +2102,8 @@ packages: resolution: {integrity: sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2870,6 +2878,9 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@7.4.6: resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} engines: {node: '>=10'} @@ -4364,18 +4375,18 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -4387,25 +4398,25 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.3': + '@eslint/eslintrc@3.3.5': dependencies: - ajv: 6.12.6 + ajv: 6.14.0 debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 3.1.2 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@10.0.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint/js@10.0.1(eslint@9.39.4(jiti@2.6.1))': optionalDependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) - '@eslint/js@9.39.2': {} + '@eslint/js@9.39.4': {} '@eslint/object-schema@2.1.7': {} @@ -4828,19 +4839,19 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@9.39.2(jiti@2.6.1))': + '@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@typescript-eslint/types': 8.56.1 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 picomatch: 4.0.3 - '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.8(acorn@8.16.0)': dependencies: - acorn: 8.15.0 + acorn: 8.16.0 '@svitejs/changesets-changelog-github-compact@1.2.0': dependencies: @@ -4853,16 +4864,16 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tanstack/eslint-config@0.4.0(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@tanstack/eslint-config@0.4.0(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint/js': 10.0.1(eslint@9.39.2(jiti@2.6.1)) - '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-n: 17.24.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@eslint/js': 10.0.1(eslint@9.39.4(jiti@2.6.1)) + '@stylistic/eslint-plugin': 5.10.0(eslint@9.39.4(jiti@2.6.1)) + eslint: 9.39.4(jiti@2.6.1) + eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-n: 17.24.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) globals: 17.4.0 - typescript-eslint: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - vue-eslint-parser: 10.4.0(eslint@9.39.2(jiti@2.6.1)) + typescript-eslint: 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - '@typescript-eslint/utils' - eslint-import-resolver-node @@ -4925,15 +4936,15 @@ snapshots: dependencies: '@types/node': 25.0.9 - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(typescript@5.9.3) @@ -4941,14 +4952,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.56.1 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -4971,13 +4982,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -5002,13 +5013,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/types': 8.56.1 '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5580,6 +5591,8 @@ snapshots: acorn@8.15.0: {} + acorn@8.16.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -5594,7 +5607,7 @@ snapshots: optionalDependencies: ajv: 8.18.0 - ajv@6.12.6: + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 @@ -6188,9 +6201,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.39.2(jiti@2.6.1)): + eslint-compat-utils@0.5.1(eslint@9.39.4(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) semver: 7.7.3 eslint-import-context@0.1.9(unrs-resolver@1.11.1): @@ -6200,19 +6213,19 @@ snapshots: optionalDependencies: unrs-resolver: 1.11.1 - eslint-plugin-es-x@7.8.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-es-x@7.8.0(eslint@9.39.4(jiti@2.6.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - eslint: 9.39.2(jiti@2.6.1) - eslint-compat-utils: 0.5.1(eslint@9.39.2(jiti@2.6.1)) + eslint: 9.39.4(jiti@2.6.1) + eslint-compat-utils: 0.5.1(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@typescript-eslint/types': 8.53.0 comment-parser: 1.4.4 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.1.1 @@ -6220,16 +6233,16 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - supports-color - eslint-plugin-n@17.24.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + eslint-plugin-n@17.24.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) enhanced-resolve: 5.18.4 - eslint: 9.39.2(jiti@2.6.1) - eslint-plugin-es-x: 7.8.0(eslint@9.39.2(jiti@2.6.1)) + eslint: 9.39.4(jiti@2.6.1) + eslint-plugin-es-x: 7.8.0(eslint@9.39.4(jiti@2.6.1)) get-tsconfig: 4.13.0 globals: 15.15.0 globrex: 0.1.2 @@ -6239,11 +6252,11 @@ snapshots: transitivePeerDependencies: - typescript - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint-scope@8.4.0: dependencies: @@ -6256,21 +6269,21 @@ snapshots: eslint-visitor-keys@5.0.0: {} - eslint@9.39.2(jiti@2.6.1): + eslint@9.39.4(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 + '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 + ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 @@ -6289,7 +6302,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -7139,6 +7152,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + minimatch@7.4.6: dependencies: brace-expansion: 2.0.2 @@ -7985,9 +8002,9 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.16.0) '@types/estree': 1.0.8 - acorn: 8.15.0 + acorn: 8.16.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 @@ -8163,13 +8180,13 @@ snapshots: typescript: 5.9.3 yaml: 2.8.2 - typescript-eslint@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/utils': 8.56.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8388,10 +8405,10 @@ snapshots: - tsx - yaml - vue-eslint-parser@10.4.0(eslint@9.39.2(jiti@2.6.1)): + vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.4(jiti@2.6.1) eslint-scope: 8.4.0 eslint-visitor-keys: 5.0.0 espree: 11.0.0