citty-test-utils
Version:
Unified testing framework for CLI applications with auto-detecting local/cleanroom execution, vitest config integration, and simplified scenario DSL.
195 lines (173 loc) • 5.51 kB
JavaScript
/**
* @fileoverview CLI Entry Point Resolver
* @description Resolves CLI entry points with support for custom paths
*/
import { existsSync, statSync } from 'fs'
import { resolve, extname } from 'path'
import { SmartCLIDetector } from './smart-cli-detector.js'
/**
* CLI Entry Point Resolver
* Supports both auto-detection and explicit file selection
*/
export class CLIEntryResolver {
constructor(options = {}) {
this.options = {
verbose: options.verbose || false,
...options,
}
this.detector = new SmartCLIDetector({ verbose: this.options.verbose })
}
/**
* Resolve CLI entry point from various sources
* @param {Object} options - Resolution options
* @returns {Promise<Object>} Resolved CLI path and metadata
*/
async resolveCLIEntry(options = {}) {
const {
entryFile,
cliFile,
cliPath,
verbose = this.options.verbose,
} = options
// Priority 1: Explicit --entry-file or --cli-file flag
const explicitPath = entryFile || cliFile
if (explicitPath) {
return this.resolveExplicitPath(explicitPath, verbose)
}
// Priority 2: Legacy --cli-path (with auto-detection fallback)
if (cliPath && cliPath !== 'src/cli.mjs') {
return this.resolveExplicitPath(cliPath, verbose)
}
// Priority 3: Auto-detection
return this.autoDetectCLI(verbose)
}
/**
* Resolve explicitly specified CLI file
* @param {string} filePath - Explicit file path
* @param {boolean} verbose - Enable verbose output
* @returns {Object} Resolution result
*/
resolveExplicitPath(filePath, verbose) {
if (verbose) {
console.log(`🔍 Resolving explicit CLI entry: ${filePath}`)
}
// Resolve to absolute path
const resolvedPath = resolve(filePath)
// Validate file exists
if (!existsSync(resolvedPath)) {
throw new Error(
`CLI entry file not found: ${resolvedPath}\n\n` +
`Suggestion: Use --entry-file with a valid path:\n` +
` $ ctu analyze --entry-file ./path/to/your/cli.js\n`
)
}
// Validate it's a file
const stat = statSync(resolvedPath)
if (!stat.isFile()) {
throw new Error(
`CLI entry path is not a file: ${resolvedPath}\n\n` +
`Suggestion: Provide a JavaScript/TypeScript file:\n` +
` $ ctu analyze --entry-file ./src/cli.mjs\n`
)
}
// Validate it's a JavaScript file
const ext = extname(resolvedPath)
const validExtensions = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']
if (!validExtensions.includes(ext)) {
throw new Error(
`CLI entry file must be JavaScript/TypeScript: ${resolvedPath}\n` +
`Found extension: ${ext}\n` +
`Valid extensions: ${validExtensions.join(', ')}\n\n` +
`Suggestion: Provide a .js, .mjs, or .ts file:\n` +
` $ ctu analyze --entry-file ./src/cli.mjs\n`
)
}
if (verbose) {
console.log(`✅ Resolved CLI entry: ${resolvedPath}`)
}
return {
cliPath: resolvedPath,
detectionMethod: 'explicit',
confidence: 'high',
source: 'user-specified',
validated: true,
}
}
/**
* Auto-detect CLI entry point
* @param {boolean} verbose - Enable verbose output
* @returns {Promise<Object>} Detection result
*/
async autoDetectCLI(verbose) {
if (verbose) {
console.log('🔍 Starting smart CLI auto-detection...')
}
const detected = await this.detector.detectCLI({ verbose })
if (!detected || !detected.cliPath) {
throw new Error(
`No CLI entry point found via auto-detection.\n\n` +
`Suggestions:\n` +
`1. Use --entry-file to specify the CLI file explicitly:\n` +
` $ ctu analyze --entry-file ./path/to/your/cli.js\n\n` +
`2. Add a "bin" field to your package.json:\n` +
` "bin": {\n` +
` "your-cli": "./path/to/cli.js"\n` +
` }\n\n` +
`3. Use a conventional CLI file location:\n` +
` - src/cli.mjs\n` +
` - bin/cli.js\n` +
` - cli.js\n`
)
}
if (verbose) {
console.log(`✅ Auto-detected CLI: ${detected.cliPath}`)
console.log(` Detection method: ${detected.detectionMethod}`)
console.log(` Confidence: ${detected.confidence}`)
}
return {
...detected,
source: 'auto-detected',
validated: true,
}
}
/**
* Validate resolved CLI path
* @param {string} cliPath - CLI path to validate
* @returns {Object} Validation result
*/
validateCLI(cliPath) {
return this.detector.validateCLI(cliPath)
}
}
/**
* Convenience function to resolve CLI entry point
* @param {Object} options - Resolution options
* @returns {Promise<string>} Resolved CLI path
*/
export async function resolveCLIEntry(options) {
const resolver = new CLIEntryResolver(options)
const result = await resolver.resolveCLIEntry(options)
return result.cliPath
}
/**
* Add CLI entry arguments to command definition
* @returns {Object} CLI entry arguments
*/
export function getCLIEntryArgs() {
return {
'entry-file': {
type: 'string',
description: 'Path to CLI entry file (default: auto-detect)',
},
'cli-file': {
type: 'string',
description: 'Alias for --entry-file',
},
'cli-path': {
type: 'string',
description: '[Deprecated] Path to CLI file (use --entry-file instead)',
default: 'src/cli.mjs',
},
}
}