diff --git a/bin/cli.ts b/bin/cli.ts index cf4b414..27827a7 100644 --- a/bin/cli.ts +++ b/bin/cli.ts @@ -30,7 +30,7 @@ async function main() { if (dashIndex !== -1) { // Use the new PackageManagerCompletion wrapper const completion = new PackageManagerCompletion(packageManager); - setupCompletionForPackageManager(packageManager, completion); + await setupCompletionForPackageManager(packageManager, completion); const toComplete = process.argv.slice(dashIndex + 1); await completion.parse(toComplete); process.exit(0); diff --git a/bin/completion-handlers.ts b/bin/completion-handlers.ts index b0815cb..28b6c97 100644 --- a/bin/completion-handlers.ts +++ b/bin/completion-handlers.ts @@ -1,457 +1,33 @@ -import { PackageManagerCompletion } from './package-manager-completion.js'; -import { readFileSync } from 'fs'; - -// Helper functions for dynamic completions -function getPackageJsonScripts(): string[] { - try { - const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); - return Object.keys(packageJson.scripts || {}); - } catch { - return []; - } -} - -function getPackageJsonDependencies(): string[] { - try { - const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); - const deps = { - ...packageJson.dependencies, - ...packageJson.devDependencies, - ...packageJson.peerDependencies, - ...packageJson.optionalDependencies, - }; - return Object.keys(deps); - } catch { - return []; - } -} - -// Common completion handlers -const scriptCompletion = async (complete: any) => { - const scripts = getPackageJsonScripts(); - scripts.forEach((script) => complete(script, `Run ${script} script`)); -}; - -const dependencyCompletion = async (complete: any) => { - const deps = getPackageJsonDependencies(); - deps.forEach((dep) => complete(dep, '')); -}; - -export function setupCompletionForPackageManager( +/** + * Main entry point for package manager completion handlers + * Delegates to specific package manager handlers + */ + +import type { PackageManagerCompletion } from './package-manager-completion.js'; +import { setupPnpmCompletions } from './handlers/pnpm-handler.js'; +import { setupNpmCompletions } from './handlers/npm-handler.js'; +import { setupYarnCompletions } from './handlers/yarn-handler.js'; +import { setupBunCompletions } from './handlers/bun-handler.js'; + +export async function setupCompletionForPackageManager( packageManager: string, completion: PackageManagerCompletion -) { - if (packageManager === 'pnpm') { - setupPnpmCompletions(completion); - } else if (packageManager === 'npm') { - setupNpmCompletions(completion); - } else if (packageManager === 'yarn') { - setupYarnCompletions(completion); - } else if (packageManager === 'bun') { - setupBunCompletions(completion); +): Promise { + switch (packageManager) { + case 'pnpm': + await setupPnpmCompletions(completion); + break; + case 'npm': + await setupNpmCompletions(completion); + break; + case 'yarn': + await setupYarnCompletions(completion); + break; + case 'bun': + await setupBunCompletions(completion); + break; + default: + // silently ignore unknown package managers + break; } } - -export function setupPnpmCompletions(completion: PackageManagerCompletion) { - // Package management - const addCmd = completion.command('add', 'Install packages'); - addCmd.option('save-dev', 'Save to devDependencies', 'D'); - addCmd.option('save-optional', 'Save to optionalDependencies', 'O'); - addCmd.option('save-exact', 'Save exact version', 'E'); - addCmd.option('global', 'Install globally', 'g'); - addCmd.option('workspace', 'Install to workspace'); - addCmd.option('filter', 'Filter packages'); - - const removeCmd = completion.command('remove', 'Remove packages'); - removeCmd.argument('package', dependencyCompletion); - removeCmd.option('global', 'Remove globally', 'g'); - - const installCmd = completion.command('install', 'Install dependencies'); - installCmd.option('frozen-lockfile', 'Install with frozen lockfile'); - installCmd.option('prefer-frozen-lockfile', 'Prefer frozen lockfile'); - installCmd.option('production', 'Install production dependencies only'); - installCmd.option('dev', 'Install dev dependencies only'); - installCmd.option('optional', 'Include optional dependencies'); - installCmd.option('filter', 'Filter packages'); - - const updateCmd = completion.command('update', 'Update dependencies'); - updateCmd.argument('package', dependencyCompletion); - updateCmd.option('latest', 'Update to latest versions'); - updateCmd.option('global', 'Update global packages', 'g'); - updateCmd.option('interactive', 'Interactive update', 'i'); - - // Script execution - const runCmd = completion.command('run', 'Run scripts'); - runCmd.argument('script', scriptCompletion, true); - runCmd.option('parallel', 'Run scripts in parallel'); - runCmd.option('stream', 'Stream output'); - runCmd.option('filter', 'Filter packages'); - - const execCmd = completion.command('exec', 'Execute commands'); - execCmd.option('filter', 'Filter packages'); - execCmd.option('parallel', 'Run in parallel'); - execCmd.option('stream', 'Stream output'); - - completion.command('dlx', 'Run package without installing'); - - // Project management - completion.command('create', 'Create new project'); - completion.command('init', 'Initialize project'); - - // Publishing - const publishCmd = completion.command('publish', 'Publish package'); - publishCmd.option('tag', 'Publish with tag'); - publishCmd.option('access', 'Set access level'); - publishCmd.option('otp', 'One-time password'); - publishCmd.option('dry-run', 'Dry run'); - - const packCmd = completion.command('pack', 'Create tarball'); - packCmd.option('pack-destination', 'Destination directory'); - - // Linking - const linkCmd = completion.command('link', 'Link packages'); - linkCmd.option('global', 'Link globally', 'g'); - - const unlinkCmd = completion.command('unlink', 'Unlink packages'); - unlinkCmd.option('global', 'Unlink globally', 'g'); - - // Information - const listCmd = completion.command('list', 'List packages'); - listCmd.option('depth', 'Max depth'); - listCmd.option('global', 'List global packages', 'g'); - listCmd.option('long', 'Show extended information'); - listCmd.option('parseable', 'Parseable output'); - listCmd.option('json', 'JSON output'); - - const outdatedCmd = completion.command('outdated', 'Check outdated packages'); - outdatedCmd.option('global', 'Check global packages', 'g'); - outdatedCmd.option('long', 'Show extended information'); - - const auditCmd = completion.command('audit', 'Security audit'); - auditCmd.option('fix', 'Automatically fix vulnerabilities'); - auditCmd.option('json', 'JSON output'); - - // Workspace commands - completion.command('workspace', 'Workspace commands'); - - // Store management - completion.command('store', 'Store management'); - completion.command('store status', 'Store status'); - completion.command('store prune', 'Prune store'); - completion.command('store path', 'Store path'); - - // Configuration - completion.command('config', 'Configuration'); - completion.command('config get', 'Get config value'); - completion.command('config set', 'Set config value'); - completion.command('config delete', 'Delete config value'); - completion.command('config list', 'List config'); - - // Other useful commands - completion.command('why', 'Explain why package is installed'); - completion.command('rebuild', 'Rebuild packages'); - completion.command('root', 'Print root directory'); - completion.command('bin', 'Print bin directory'); - completion.command('start', 'Run start script'); - completion.command('test', 'Run test script'); - completion.command('restart', 'Run restart script'); - completion.command('stop', 'Run stop script'); -} - -export function setupNpmCompletions(completion: PackageManagerCompletion) { - // Package management - const installCmd = completion.command('install', 'Install packages'); - installCmd.option('save', 'Save to dependencies', 'S'); - installCmd.option('save-dev', 'Save to devDependencies', 'D'); - installCmd.option('save-optional', 'Save to optionalDependencies', 'O'); - installCmd.option('save-exact', 'Save exact version', 'E'); - installCmd.option('global', 'Install globally', 'g'); - installCmd.option('production', 'Production install'); - installCmd.option('only', 'Install only specific dependencies'); - - const uninstallCmd = completion.command('uninstall', 'Remove packages'); - uninstallCmd.argument('package', dependencyCompletion); - uninstallCmd.option('save', 'Remove from dependencies', 'S'); - uninstallCmd.option('save-dev', 'Remove from devDependencies', 'D'); - uninstallCmd.option('global', 'Remove globally', 'g'); - - const updateCmd = completion.command('update', 'Update packages'); - updateCmd.argument('package', dependencyCompletion); - updateCmd.option('global', 'Update global packages', 'g'); - - // Script execution - const runCmd = completion.command('run', 'Run scripts'); - runCmd.argument('script', scriptCompletion, true); - - const runScriptCmd = completion.command('run-script', 'Run scripts'); - runScriptCmd.argument('script', scriptCompletion, true); - - completion.command('exec', 'Execute command'); - - // Project management - const initCmd = completion.command('init', 'Initialize project'); - initCmd.option('yes', 'Use defaults', 'y'); - initCmd.option('scope', 'Set scope'); - - // Publishing - const publishCmd = completion.command('publish', 'Publish package'); - publishCmd.option('tag', 'Publish with tag'); - publishCmd.option('access', 'Set access level'); - publishCmd.option('otp', 'One-time password'); - publishCmd.option('dry-run', 'Dry run'); - - completion.command('pack', 'Create tarball'); - - // Linking - completion.command('link', 'Link packages'); - completion.command('unlink', 'Unlink packages'); - - // Information - const listCmd = completion.command('list', 'List packages'); - listCmd.option('depth', 'Max depth'); - listCmd.option('global', 'List global packages', 'g'); - listCmd.option('long', 'Show extended information'); - listCmd.option('parseable', 'Parseable output'); - listCmd.option('json', 'JSON output'); - - const lsCmd = completion.command('ls', 'List packages (alias)'); - lsCmd.option('depth', 'Max depth'); - lsCmd.option('global', 'List global packages', 'g'); - - const outdatedCmd = completion.command('outdated', 'Check outdated packages'); - outdatedCmd.option('global', 'Check global packages', 'g'); - - const auditCmd = completion.command('audit', 'Security audit'); - auditCmd.option('fix', 'Fix vulnerabilities'); - auditCmd.option('json', 'JSON output'); - - // Configuration - completion.command('config', 'Configuration'); - completion.command('config get', 'Get config value'); - completion.command('config set', 'Set config value'); - completion.command('config delete', 'Delete config value'); - completion.command('config list', 'List config'); - - // Other commands - completion.command('version', 'Bump version'); - completion.command('view', 'View package info'); - completion.command('search', 'Search packages'); - completion.command('whoami', 'Display username'); - completion.command('login', 'Login to registry'); - completion.command('logout', 'Logout from registry'); - completion.command('adduser', 'Add user'); - completion.command('owner', 'Manage package owners'); - completion.command('deprecate', 'Deprecate package'); - completion.command('dist-tag', 'Manage distribution tags'); - completion.command('cache', 'Manage cache'); - completion.command('completion', 'Tab completion'); - completion.command('explore', 'Browse package'); - completion.command('docs', 'Open documentation'); - completion.command('repo', 'Open repository'); - completion.command('bugs', 'Open bug tracker'); - completion.command('help', 'Get help'); - completion.command('root', 'Print root directory'); - completion.command('prefix', 'Print prefix'); - completion.command('bin', 'Print bin directory'); - completion.command('fund', 'Fund packages'); - completion.command('find-dupes', 'Find duplicate packages'); - completion.command('dedupe', 'Deduplicate packages'); - completion.command('prune', 'Remove extraneous packages'); - completion.command('rebuild', 'Rebuild packages'); - completion.command('start', 'Run start script'); - completion.command('stop', 'Run stop script'); - completion.command('test', 'Run test script'); - completion.command('restart', 'Run restart script'); -} - -export function setupYarnCompletions(completion: PackageManagerCompletion) { - // Package management - const addCmd = completion.command('add', 'Add packages'); - addCmd.option('dev', 'Add to devDependencies', 'D'); - addCmd.option('peer', 'Add to peerDependencies', 'P'); - addCmd.option('optional', 'Add to optionalDependencies', 'O'); - addCmd.option('exact', 'Add exact version', 'E'); - addCmd.option('tilde', 'Add with tilde range', 'T'); - - const removeCmd = completion.command('remove', 'Remove packages'); - removeCmd.argument('package', dependencyCompletion); - - const installCmd = completion.command('install', 'Install dependencies'); - installCmd.option('frozen-lockfile', 'Install with frozen lockfile'); - installCmd.option('prefer-offline', 'Prefer offline'); - installCmd.option('production', 'Production install'); - installCmd.option('pure-lockfile', 'Pure lockfile'); - installCmd.option('focus', 'Focus install'); - installCmd.option('har', 'Save HAR file'); - - const upgradeCmd = completion.command('upgrade', 'Upgrade packages'); - upgradeCmd.argument('package', dependencyCompletion); - upgradeCmd.option('latest', 'Upgrade to latest'); - upgradeCmd.option('pattern', 'Upgrade pattern'); - upgradeCmd.option('scope', 'Upgrade scope'); - - const upgradeInteractiveCmd = completion.command( - 'upgrade-interactive', - 'Interactive upgrade' - ); - upgradeInteractiveCmd.option('latest', 'Show latest versions'); - - // Script execution - const runCmd = completion.command('run', 'Run scripts'); - runCmd.argument('script', scriptCompletion, true); - - completion.command('exec', 'Execute command'); - - // Project management - completion.command('create', 'Create new project'); - const initCmd = completion.command('init', 'Initialize project'); - initCmd.option('yes', 'Use defaults', 'y'); - initCmd.option('private', 'Create private package', 'p'); - - // Publishing - const publishCmd = completion.command('publish', 'Publish package'); - publishCmd.option('tag', 'Publish with tag'); - publishCmd.option('access', 'Set access level'); - publishCmd.option('new-version', 'Set new version'); - - const packCmd = completion.command('pack', 'Create tarball'); - packCmd.option('filename', 'Output filename'); - - // Linking - completion.command('link', 'Link packages'); - completion.command('unlink', 'Unlink packages'); - - // Information - const listCmd = completion.command('list', 'List packages'); - listCmd.option('depth', 'Max depth'); - listCmd.option('pattern', 'Filter pattern'); - - completion.command('info', 'Show package info'); - completion.command('outdated', 'Check outdated packages'); - const auditCmd = completion.command('audit', 'Security audit'); - auditCmd.option('level', 'Minimum severity level'); - - // Workspace commands - completion.command('workspace', 'Workspace commands'); - completion.command('workspaces', 'Workspaces commands'); - completion.command('workspaces info', 'Workspace info'); - completion.command('workspaces run', 'Run in workspaces'); - - // Configuration - completion.command('config', 'Configuration'); - completion.command('config get', 'Get config value'); - completion.command('config set', 'Set config value'); - completion.command('config delete', 'Delete config value'); - completion.command('config list', 'List config'); - - // Cache management - completion.command('cache', 'Cache management'); - completion.command('cache list', 'List cache'); - completion.command('cache dir', 'Cache directory'); - completion.command('cache clean', 'Clean cache'); - - // Other commands - completion.command('version', 'Show version'); - completion.command('versions', 'Show all versions'); - completion.command('why', 'Explain installation'); - completion.command('owner', 'Manage owners'); - completion.command('team', 'Manage teams'); - completion.command('login', 'Login to registry'); - completion.command('logout', 'Logout from registry'); - completion.command('tag', 'Manage tags'); - completion.command('global', 'Global packages'); - completion.command('bin', 'Print bin directory'); - completion.command('dir', 'Print modules directory'); - completion.command('licenses', 'List licenses'); - completion.command('generate-lock-entry', 'Generate lock entry'); - completion.command('check', 'Verify package tree'); - completion.command('import', 'Import from npm'); - completion.command('install-peerdeps', 'Install peer dependencies'); - completion.command('autoclean', 'Clean unnecessary files'); - completion.command('policies', 'Policies'); - completion.command('start', 'Run start script'); - completion.command('test', 'Run test script'); - completion.command('node', 'Run node'); -} - -export function setupBunCompletions(completion: PackageManagerCompletion) { - // Package management - const addCmd = completion.command('add', 'Add packages'); - addCmd.option('development', 'Add to devDependencies', 'd'); - addCmd.option('optional', 'Add to optionalDependencies'); - addCmd.option('exact', 'Add exact version', 'E'); - addCmd.option('global', 'Install globally', 'g'); - - const removeCmd = completion.command('remove', 'Remove packages'); - removeCmd.argument('package', dependencyCompletion); - removeCmd.option('global', 'Remove globally', 'g'); - - const installCmd = completion.command('install', 'Install dependencies'); - installCmd.option('production', 'Production install'); - installCmd.option('frozen-lockfile', 'Use frozen lockfile'); - installCmd.option('dry-run', 'Dry run'); - installCmd.option('force', 'Force install'); - installCmd.option('silent', 'Silent install'); - - const updateCmd = completion.command('update', 'Update packages'); - updateCmd.argument('package', dependencyCompletion); - updateCmd.option('global', 'Update global packages', 'g'); - - // Script execution and running - const runCmd = completion.command('run', 'Run scripts'); - runCmd.argument('script', scriptCompletion, true); - runCmd.option('silent', 'Silent output'); - runCmd.option('bun', 'Use bun runtime'); - - const xCmd = completion.command('x', 'Execute packages'); - xCmd.option('bun', 'Use bun runtime'); - - // Bun-specific commands - completion.command('dev', 'Development server'); - completion.command('build', 'Build project'); - completion.command('test', 'Run tests'); - - // Project management - completion.command('create', 'Create new project'); - const initCmd = completion.command('init', 'Initialize project'); - initCmd.option('yes', 'Use defaults', 'y'); - - // Publishing - const publishCmd = completion.command('publish', 'Publish package'); - publishCmd.option('tag', 'Publish with tag'); - publishCmd.option('access', 'Set access level'); - publishCmd.option('otp', 'One-time password'); - - completion.command('pack', 'Create tarball'); - - // Linking - completion.command('link', 'Link packages'); - completion.command('unlink', 'Unlink packages'); - - // Information - const listCmd = completion.command('list', 'List packages'); - listCmd.option('global', 'List global packages', 'g'); - - completion.command('outdated', 'Check outdated packages'); - completion.command('audit', 'Security audit'); - - // Configuration - completion.command('config', 'Configuration'); - - // Bun runtime commands - completion.command('bun', 'Run with Bun runtime'); - completion.command('node', 'Node.js compatibility'); - completion.command('upgrade', 'Upgrade Bun'); - completion.command('completions', 'Generate completions'); - completion.command('discord', 'Open Discord'); - completion.command('help', 'Show help'); - - // File operations - completion.command('install.cache', 'Cache operations'); - completion.command('pm', 'Package manager operations'); - - // Other commands - completion.command('start', 'Run start script'); - completion.command('stop', 'Run stop script'); - completion.command('restart', 'Run restart script'); -} diff --git a/bin/completions/completion-producers.ts b/bin/completions/completion-producers.ts new file mode 100644 index 0000000..cb03067 --- /dev/null +++ b/bin/completions/completion-producers.ts @@ -0,0 +1,21 @@ +import type { Complete } from '../../src/t.js'; +import { + getPackageJsonScripts, + getPackageJsonDependencies, +} from '../utils/package-json-utils.js'; + +// provides completions for npm scripts from package.json.. like: start,dev,build +export const packageJsonScriptCompletion = async ( + complete: Complete +): Promise => { + getPackageJsonScripts().forEach((script) => + complete(script, `Run ${script} script`) + ); +}; + +// provides completions for package dependencies from package.json.. for commands like remove `pnpm remove ` +export const packageJsonDependencyCompletion = async ( + complete: Complete +): Promise => { + getPackageJsonDependencies().forEach((dep) => complete(dep, '')); +}; diff --git a/bin/handlers/bun-handler.ts b/bin/handlers/bun-handler.ts new file mode 100644 index 0000000..225b049 --- /dev/null +++ b/bin/handlers/bun-handler.ts @@ -0,0 +1,5 @@ +import type { PackageManagerCompletion } from '../package-manager-completion.js'; + +export async function setupBunCompletions( + completion: PackageManagerCompletion +): Promise {} diff --git a/bin/handlers/npm-handler.ts b/bin/handlers/npm-handler.ts new file mode 100644 index 0000000..e4528e9 --- /dev/null +++ b/bin/handlers/npm-handler.ts @@ -0,0 +1,5 @@ +import type { PackageManagerCompletion } from '../package-manager-completion.js'; + +export async function setupNpmCompletions( + completion: PackageManagerCompletion +): Promise {} diff --git a/bin/handlers/pnpm-handler.ts b/bin/handlers/pnpm-handler.ts new file mode 100644 index 0000000..6e9ad11 --- /dev/null +++ b/bin/handlers/pnpm-handler.ts @@ -0,0 +1,233 @@ +import { promisify } from 'node:util'; +import child_process from 'node:child_process'; + +const exec = promisify(child_process.exec); +const { execSync } = child_process; +import type { PackageManagerCompletion } from '../package-manager-completion.js'; +import { Command, Option } from '../../src/t.js'; + +interface LazyCommand extends Command { + _lazyCommand?: string; + _optionsLoaded?: boolean; + optionsRaw?: Map; +} + +import { + packageJsonScriptCompletion, + packageJsonDependencyCompletion, +} from '../completions/completion-producers.js'; +import { + stripAnsiEscapes, + measureIndent, + parseAliasList, + COMMAND_ROW_RE, + OPTION_ROW_RE, + OPTION_HEAD_RE, + type ParsedOption, +} from '../utils/text-utils.js'; + +// regex to detect options section in help text +const OPTIONS_SECTION_RE = /^\s*Options:/i; + +// we parse the pnpm help text to extract commands and their descriptions! +export function parsePnpmHelp(helpText: string): Record { + const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); + + // we find the earliest description column across command rows. + let descColumnIndex = Number.POSITIVE_INFINITY; + for (const line of helpLines) { + const rowMatch = line.match(COMMAND_ROW_RE); + if (!rowMatch) continue; + const descColumnIndexOnThisLine = line.indexOf(rowMatch[2]); + if ( + descColumnIndexOnThisLine >= 0 && + descColumnIndexOnThisLine < descColumnIndex + ) { + descColumnIndex = descColumnIndexOnThisLine; + } + } + if (!Number.isFinite(descColumnIndex)) return {}; + + // we fold rows, and join continuation lines aligned to descColumnIndex or deeper. + type PendingRow = { names: string[]; desc: string } | null; + let pendingRow: PendingRow = null; + + const commandMap = new Map(); + const flushPendingRow = () => { + if (!pendingRow) return; + const desc = pendingRow.desc.trim(); + for (const name of pendingRow.names) commandMap.set(name, desc); + pendingRow = null; + }; + + for (const line of helpLines) { + if (OPTIONS_SECTION_RE.test(line)) break; // we stop at options + + // we match the command row + const rowMatch = line.match(COMMAND_ROW_RE); + if (rowMatch) { + flushPendingRow(); + pendingRow = { + names: parseAliasList(rowMatch[1]), + desc: rowMatch[2].trim(), + }; + continue; + } + + // we join continuation lines aligned to descColumnIndex or deeper + if (pendingRow) { + const indentWidth = measureIndent(line); + if (indentWidth >= descColumnIndex && line.trim()) { + pendingRow.desc += ' ' + line.trim(); + } + } + } + // we flush the pending row and return the command map + flushPendingRow(); + + return Object.fromEntries(commandMap); +} + +// now we get the pnpm commands from the main help output +export async function getPnpmCommandsFromMainHelp(): Promise< + Record +> { + try { + const { stdout } = await exec('pnpm --help', { + encoding: 'utf8', + timeout: 500, + maxBuffer: 4 * 1024 * 1024, + }); + return parsePnpmHelp(stdout); + } catch { + return {}; + } +} + +// here we parse the pnpm options from the help text +export function parsePnpmOptions( + helpText: string, + { flagsOnly = true }: { flagsOnly?: boolean } = {} +): ParsedOption[] { + // we strip the ANSI escapes from the help text + const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/); + + // we find the earliest description column among option rows we care about + let descColumnIndex = Number.POSITIVE_INFINITY; + for (const line of helpLines) { + const optionMatch = line.match(OPTION_ROW_RE); + if (!optionMatch) continue; + if (flagsOnly && optionMatch.groups?.val) continue; // skip value-taking options, we will add them manually with their value + const descColumnIndexOnThisLine = line.indexOf(optionMatch.groups!.desc); + if ( + descColumnIndexOnThisLine >= 0 && + descColumnIndexOnThisLine < descColumnIndex + ) { + descColumnIndex = descColumnIndexOnThisLine; + } + } + if (!Number.isFinite(descColumnIndex)) return []; + + // we fold the option rows and join the continuations + const optionsOut: ParsedOption[] = []; + let pendingOption: ParsedOption | null = null; + + const flushPendingOption = () => { + if (!pendingOption) return; + pendingOption.desc = pendingOption.desc.trim(); + optionsOut.push(pendingOption); + pendingOption = null; + }; + + // we match the option row + for (const line of helpLines) { + const optionMatch = line.match(OPTION_ROW_RE); + if (optionMatch) { + if (flagsOnly && optionMatch.groups?.val) continue; + flushPendingOption(); + pendingOption = { + short: optionMatch.groups?.short || undefined, + long: optionMatch.groups!.long, + desc: optionMatch.groups!.desc.trim(), + }; + continue; + } + + // we join the continuations + if (pendingOption) { + const indentWidth = measureIndent(line); + const startsNewOption = OPTION_HEAD_RE.test(line); + if (indentWidth >= descColumnIndex && line.trim() && !startsNewOption) { + pendingOption.desc += ' ' + line.trim(); + } + } + } + // we flush the pending option + flushPendingOption(); + + return optionsOut; +} + +// we load the dynamic options synchronously when requested ( separated from the command loading ) +export function loadDynamicOptionsSync( + cmd: LazyCommand, + command: string +): void { + try { + const stdout = execSync(`pnpm ${command} --help`, { + encoding: 'utf8', + timeout: 500, + }); + + const parsedOptions = parsePnpmOptions(stdout, { flagsOnly: true }); + + for (const { long, short, desc } of parsedOptions) { + const alreadyDefined = cmd.optionsRaw?.get?.(long); + if (!alreadyDefined) cmd.option(long, desc, short); + } + } catch (_err) {} +} + +// we setup the lazy option loading for a command + +function setupLazyOptionLoading(cmd: LazyCommand, command: string): void { + cmd._lazyCommand = command; + cmd._optionsLoaded = false; + + const optionsStore = cmd.options; + cmd.optionsRaw = optionsStore; + + Object.defineProperty(cmd, 'options', { + get() { + if (!this._optionsLoaded) { + this._optionsLoaded = true; + loadDynamicOptionsSync(this, this._lazyCommand); // block until filled + } + return optionsStore; + }, + configurable: true, + }); +} + +export async function setupPnpmCompletions( + completion: PackageManagerCompletion +): Promise { + try { + const commandsWithDescriptions = await getPnpmCommandsFromMainHelp(); + + for (const [command, description] of Object.entries( + commandsWithDescriptions + )) { + const cmd = completion.command(command, description); + + if (['remove', 'rm', 'update', 'up'].includes(command)) { + cmd.argument('package', packageJsonDependencyCompletion); + } + if (command === 'run') { + cmd.argument('script', packageJsonScriptCompletion, true); + } + + setupLazyOptionLoading(cmd, command); + } + } catch (_err) {} +} diff --git a/bin/handlers/yarn-handler.ts b/bin/handlers/yarn-handler.ts new file mode 100644 index 0000000..5827efe --- /dev/null +++ b/bin/handlers/yarn-handler.ts @@ -0,0 +1,5 @@ +import type { PackageManagerCompletion } from '../package-manager-completion.js'; + +export async function setupYarnCompletions( + completion: PackageManagerCompletion +): Promise {} diff --git a/bin/utils/package-json-utils.ts b/bin/utils/package-json-utils.ts new file mode 100644 index 0000000..094d8e1 --- /dev/null +++ b/bin/utils/package-json-utils.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'fs'; + +export function getPackageJsonScripts(): string[] { + try { + const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); + return Object.keys(packageJson.scripts || {}); + } catch { + return []; + } +} + +export function getPackageJsonDependencies(): string[] { + try { + const packageJson = JSON.parse(readFileSync('package.json', 'utf8')); + const deps = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + ...packageJson.peerDependencies, + ...packageJson.optionalDependencies, + }; + return Object.keys(deps); + } catch { + return []; + } +} diff --git a/bin/utils/text-utils.ts b/bin/utils/text-utils.ts new file mode 100644 index 0000000..6c4c929 --- /dev/null +++ b/bin/utils/text-utils.ts @@ -0,0 +1,35 @@ +// regex for parsing help text +export const ANSI_ESCAPE_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g; + +// Command row: <>=2 spaces> +// e.g. " install, i Install all dependencies" +export const COMMAND_ROW_RE = /^\s+([a-z][a-z\s,-]*?)\s{2,}(\S.*)$/i; + +// Option row (optional value part captured in (?)): +// [indent][-x, ]--long[ | [value]] <>=2 spaces> +export const OPTION_ROW_RE = + /^\s*(?:-(?[A-Za-z]),\s*)?--(?[a-z0-9-]+)(?\s+(?:<[^>]+>|\[[^\]]+\]))?\s{2,}(?\S.*)$/i; + +// we detect the start of a new option head (used to stop continuation) +export const OPTION_HEAD_RE = /^\s*(?:-[A-Za-z],\s*)?--[a-z0-9-]+/i; + +// we remove the ANSI escape sequences from a string +export const stripAnsiEscapes = (s: string): string => + s.replace(ANSI_ESCAPE_RE, ''); + +// measure the indentation level of a string +export const measureIndent = (s: string): number => + (s.match(/^\s*/) || [''])[0].length; + +// parse a comma-separated list of aliases +export const parseAliasList = (s: string): string[] => + s + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + +export type ParsedOption = { + long: string; + short?: string; + desc: string; +}; diff --git a/src/t.ts b/src/t.ts index 65eda28..cbfc6c9 100644 --- a/src/t.ts +++ b/src/t.ts @@ -12,7 +12,7 @@ export const ShellCompDirective = { export type OptionsMap = Map; -type Complete = (value: string, description: string) => void; +export type Complete = (value: string, description: string) => void; export type OptionHandler = ( this: Option,