From b25c493ce030b531b7edb758915514b56216f3a6 Mon Sep 17 00:00:00 2001 From: AmirSa12 Date: Wed, 10 Dec 2025 21:46:43 +0330 Subject: [PATCH] init --- src/cac.ts | 31 ++++++++++++---------------- src/citty.ts | 22 +++++++------------- src/t.ts | 58 +++++++++++++--------------------------------------- 3 files changed, 34 insertions(+), 77 deletions(-) diff --git a/src/cac.ts b/src/cac.ts index 1bf032a..af3941d 100644 --- a/src/cac.ts +++ b/src/cac.ts @@ -26,11 +26,9 @@ export default async function tab( instance: CAC, completionConfig?: CompletionConfig ): Promise { - // Add all commands and their options for (const cmd of [instance.globalCommand, ...instance.commands]) { - if (cmd.name === 'complete') continue; // Skip completion command + if (cmd.name === 'complete') continue; - // Get positional args info from command usage const args = (cmd.rawName.match(/\[.*?\]|<.*?>/g) || []).map((arg) => arg.startsWith('[') ); // true if optional (wrapped in []) @@ -40,22 +38,21 @@ export default async function tab( ? completionConfig : completionConfig?.subCommands?.[cmd.name]; - // Add command to completion using t.ts API + // command const commandName = isRootCommand ? '' : cmd.name; const command = isRootCommand ? t : t.command(commandName, cmd.description || ''); - // Set args for the command + // args (if has positional arguments) if (command) { - // Extract argument names from command usage const argMatches = cmd.rawName.match(/<([^>]+)>|\[\.\.\.([^\]]+)\]/g) || []; const argNames = argMatches.map((match) => { if (match.startsWith('<') && match.endsWith('>')) { - return match.slice(1, -1); // Remove < > + return match.slice(1, -1); } else if (match.startsWith('[...') && match.endsWith(']')) { - return match.slice(4, -1); // Remove [... ] + return match.slice(4, -1); } return match; }); @@ -71,18 +68,17 @@ export default async function tab( }); } - // Add command options + // options for (const option of [...instance.globalCommand.options, ...cmd.options]) { - // Extract short flag from the rawName if it exists (e.g., "-c, --config" -> "c") + // short flag (if exists) const shortFlag = option.rawName.match(/^-([a-zA-Z]), --/)?.[1]; - const argName = option.name; // option.name is already clean (e.g., "config") + const argName = option.name; - // Add option using t.ts API const targetCommand = isRootCommand ? t : command; if (targetCommand) { const handler = commandCompletionConfig?.options?.[argName]; - // Check if option takes a value (has <> or [] in rawName, or is marked as required) + // takes value (if has <> or [] in rawName, or is marked as required) const takesValue = option.required || VALUE_OPTION_RE.test(option.rawName); @@ -98,12 +94,12 @@ export default async function tab( targetCommand.option(argName, option.description || '', handler); } } else if (takesValue) { - // Takes value but no custom handler = value option with no completions + // value option (if takes value but no custom handler) if (shortFlag) { targetCommand.option( argName, option.description || '', - async () => [], // Empty completions + async () => [], shortFlag ); } else { @@ -114,7 +110,7 @@ export default async function tab( ); } } else { - // No custom handler and doesn't take value = boolean flag + // boolean flag (if no custom handler and doesn't take value) if (shortFlag) { targetCommand.option(argName, option.description || '', shortFlag); } else { @@ -153,13 +149,12 @@ export default async function tab( const args: string[] = extra['--'] || []; instance.showHelpOnExit = false; - // Parse current command context + // command context instance.unsetMatchedCommand(); instance.parse([execPath, processArgs[0], ...args], { run: false, }); - // Use t.ts parse method instead of completion.parse return t.parse(args); } } diff --git a/src/citty.ts b/src/citty.ts index bd205cf..6b4d95c 100644 --- a/src/citty.ts +++ b/src/citty.ts @@ -47,17 +47,14 @@ async function handleSubCommands( } const isPositional = isConfigPositional(config); - // Add command using t.ts API const commandName = parentCmd ? `${parentCmd} ${cmd}` : cmd; const command = t.command(commandName, meta.description); // Set args for the command if it has positional arguments if (isPositional && config.args) { - // Add arguments with completion handlers from subCompletionConfig args for (const [argName, argConfig] of Object.entries(config.args)) { const conf = argConfig as ArgDef; if (conf.type === 'positional') { - // Check if this is a variadic argument (required: false for variadic in citty) const isVariadic = conf.required === false; const argHandler = subCompletionConfig?.args?.[argName]; if (argHandler) { @@ -69,7 +66,7 @@ async function handleSubCommands( } } - // Handle nested subcommands recursively + // subcommands (recursive) if (subCommands) { await handleSubCommands( subCommands, @@ -78,11 +75,11 @@ async function handleSubCommands( ); } - // Handle arguments + // args if (config.args) { for (const [argName, argConfig] of Object.entries(config.args)) { const conf = argConfig as ArgDef; - // Extract alias from the config if it exists + // alias (if exists) const shortFlag = typeof conf === 'object' && 'alias' in conf ? Array.isArray(conf.alias) @@ -90,17 +87,17 @@ async function handleSubCommands( : conf.alias : undefined; - // Add option using t.ts API - store without -- prefix + // option (without -- prefix) const handler = subCompletionConfig?.options?.[argName]; if (handler) { - // Has custom handler → value option + // value option (if has custom handler) if (shortFlag) { command.option(argName, conf.description ?? '', handler, shortFlag); } else { command.option(argName, conf.description ?? '', handler); } } else { - // No custom handler → boolean flag + // boolean flag (if no custom handler) if (shortFlag) { command.option(argName, conf.description ?? '', shortFlag); } else { @@ -130,7 +127,7 @@ export default async function tab( const isPositional = isConfigPositional(instance); - // Set args for the root command if it has positional arguments + // args (if has positional arguments) if (isPositional && instance.args) { for (const [argName, argConfig] of Object.entries(instance.args)) { const conf = argConfig as PositionalArgDef; @@ -158,7 +155,6 @@ export default async function tab( for (const [argName, argConfig] of Object.entries(instance.args)) { const conf = argConfig as ArgDef; - // Extract alias (same logic as subcommands) const shortFlag = typeof conf === 'object' && 'alias' in conf ? Array.isArray(conf.alias) @@ -166,17 +162,14 @@ export default async function tab( : conf.alias : undefined; - // Add option using t.ts API - store without -- prefix const handler = completionConfig?.options?.[argName]; if (handler) { - // Has custom handler → value option if (shortFlag) { t.option(argName, conf.description ?? '', handler, shortFlag); } else { t.option(argName, conf.description ?? '', handler); } } else { - // No custom handler → boolean flag if (shortFlag) { t.option(argName, conf.description ?? '', shortFlag); } else { @@ -235,7 +228,6 @@ export default async function tab( assertDoubleDashes(name); const extra = ctx.rawArgs.slice(ctx.rawArgs.indexOf('--') + 1); - // Use t.ts parse method instead of completion.parse return t.parse(extra); } } diff --git a/src/t.ts b/src/t.ts index 7c18537..1610c03 100644 --- a/src/t.ts +++ b/src/t.ts @@ -20,7 +20,6 @@ export type OptionHandler = ( options: OptionsMap ) => void; -// Completion result types export type Completion = { description?: string; value: string; @@ -88,7 +87,6 @@ export class Command { this.description = description; } - // Function overloads for better UX - combined into single signature with optional parameters option( value: string, description: string, @@ -99,7 +97,6 @@ export class Command { let aliasStr: string | undefined; let isBoolean: boolean; - // Parse arguments based on types if (typeof handlerOrAlias === 'function') { handler = handlerOrAlias; aliasStr = alias; @@ -154,7 +151,6 @@ export class RootCommand extends Command { return c; } - // Utility method to strip options from args for command matching private stripOptions(args: string[]): string[] { const parts: string[] = []; let i = 0; @@ -163,18 +159,15 @@ export class RootCommand extends Command { const arg = args[i]; if (arg.startsWith('-')) { - i++; // Skip the option + i++; - // Check if this option expects a value (not boolean) - // We need to check across all commands since we don't know which command context we're in yet let isBoolean = false; - // Check root command options const rootOption = this.findOption(this, arg); if (rootOption) { isBoolean = rootOption.isBoolean ?? false; } else { - // Check all subcommand options + // subcommand options for (const [, command] of this.commands) { const option = this.findOption(command, arg); if (option) { @@ -184,9 +177,9 @@ export class RootCommand extends Command { } } - // Only skip the next argument if this is not a boolean option and the next arg doesn't start with - + // skip the next argument if this is not a boolean option and the next arg doesn't start with - if (!isBoolean && i < args.length && !args[i].startsWith('-')) { - i++; // Skip the option value + i++; } } else { parts.push(arg); @@ -197,7 +190,6 @@ export class RootCommand extends Command { return parts; } - // Find the appropriate command based on args private matchCommand(args: string[]): [Command, string[]] { args = this.stripOptions(args); const parts: string[] = []; @@ -217,46 +209,41 @@ export class RootCommand extends Command { } } - // If no command was matched, use the root command (this) return [matchedCommand || this, remaining]; } - // Determine if we should complete flags private shouldCompleteFlags( lastPrevArg: string | undefined, toComplete: string, endsWithSpace: boolean ): boolean { - // Always complete if the current token starts with a dash if (toComplete.startsWith('-')) { return true; } - // If the previous argument was an option, check if it expects a value + // previous argument was an option, check if it expects a value if (lastPrevArg?.startsWith('-')) { - // Find the option to check if it's boolean let option = this.findOption(this, lastPrevArg); if (!option) { - // Check all subcommand options + // subcommand options for (const [, command] of this.commands) { option = this.findOption(command, lastPrevArg); if (option) break; } } - // If it's a boolean option, don't try to complete its value + // boolean option, don't try to complete its value if (option && option.isBoolean) { return false; } - // Non-boolean options expect values + // non-boolean options expect values return true; } return false; } - // Determine if we should complete commands private shouldCompleteCommands( toComplete: string, endsWithSpace: boolean @@ -264,7 +251,7 @@ export class RootCommand extends Command { return !toComplete.startsWith('-'); } - // Handle flag completion (names and values) + // flag completion (names and values) private handleFlagCompletion( command: Command, previousArgs: string[], @@ -298,7 +285,6 @@ export class RootCommand extends Command { return; } - // Handle flag name completion if (toComplete.startsWith('-')) { const isShortFlag = toComplete.startsWith('-') && !toComplete.startsWith('--'); @@ -324,17 +310,15 @@ export class RootCommand extends Command { } } - // Helper method to find an option by name or alias + // find option by name or alias private findOption(command: Command, optionName: string): Option | undefined { - // Try direct match (with dashes) let option = command.options.get(optionName); if (option) return option; - // Try without dashes (the actual storage format) option = command.options.get(optionName.replace(/^-+/, '')); if (option) return option; - // Try by short alias + // short alias for (const [_name, opt] of command.options) { if (opt.alias && `-${opt.alias}` === optionName) { return opt; @@ -344,7 +328,7 @@ export class RootCommand extends Command { return undefined; } - // Handle command completion + // command completion private handleCommandCompletion(previousArgs: string[], toComplete: string) { const commandParts = this.stripOptions(previousArgs); @@ -365,36 +349,31 @@ export class RootCommand extends Command { } } - // Handle positional argument completion + // positional argument completion private handlePositionalCompletion( command: Command, previousArgs: string[], toComplete: string, endsWithSpace: boolean ) { - // Get the current argument position (subtract command name) + // current argument position (subtract command name) const commandParts = command.value.split(' ').length; const currentArgIndex = Math.max(0, previousArgs.length - commandParts); const argumentEntries = Array.from(command.arguments.entries()); - // If we have arguments defined if (argumentEntries.length > 0) { - // Find the appropriate argument for the current position let targetArgument: Argument | undefined; if (currentArgIndex < argumentEntries.length) { - // We're within the defined arguments const [_argName, argument] = argumentEntries[currentArgIndex]; targetArgument = argument; } else { - // We're beyond the defined arguments, check if the last argument is variadic const lastArgument = argumentEntries[argumentEntries.length - 1][1]; if (lastArgument.variadic) { targetArgument = lastArgument; } } - // If we found a target argument with a handler, use it if ( targetArgument && targetArgument.handler && @@ -412,7 +391,6 @@ export class RootCommand extends Command { } } - // Format and output completion results private complete(toComplete: string) { this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp; @@ -459,7 +437,6 @@ export class RootCommand extends Command { const [matchedCommand] = this.matchCommand(previousArgs); const lastPrevArg = previousArgs[previousArgs.length - 1]; - // 1. Handle flag/option completion if (this.shouldCompleteFlags(lastPrevArg, toComplete, endsWithSpace)) { this.handleFlagCompletion( matchedCommand, @@ -469,31 +446,24 @@ export class RootCommand extends Command { lastPrevArg ); } else { - // Check if we just finished a boolean option with no value expected - // In this case, don't complete anything if (lastPrevArg?.startsWith('-') && toComplete === '' && endsWithSpace) { let option = this.findOption(this, lastPrevArg); if (!option) { - // Check all subcommand options for (const [, command] of this.commands) { option = this.findOption(command, lastPrevArg); if (option) break; } } - // If it's a boolean option followed by empty space, don't complete anything if (option && option.isBoolean) { - // Don't add any completions, just output the directive this.complete(toComplete); return; } } - // 2. Handle command/subcommand completion if (this.shouldCompleteCommands(toComplete, endsWithSpace)) { this.handleCommandCompletion(previousArgs, toComplete); } - // 3. Handle positional arguments - always check for root command arguments if (matchedCommand && matchedCommand.arguments.size > 0) { this.handlePositionalCompletion( matchedCommand,