Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 13 additions & 18 deletions src/cac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@ export default async function tab(
instance: CAC,
completionConfig?: CompletionConfig
): Promise<RootCommand> {
// 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 [])
Expand All @@ -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;
});
Expand All @@ -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);

Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
}
Expand Down
22 changes: 7 additions & 15 deletions src/citty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -69,7 +66,7 @@ async function handleSubCommands(
}
}

// Handle nested subcommands recursively
// subcommands (recursive)
if (subCommands) {
await handleSubCommands(
subCommands,
Expand All @@ -78,29 +75,29 @@ 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)
? conf.alias[0]
: 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 {
Expand Down Expand Up @@ -130,7 +127,7 @@ export default async function tab<TArgs extends ArgsDef>(

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;
Expand Down Expand Up @@ -158,25 +155,21 @@ export default async function tab<TArgs extends ArgsDef>(
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)
? conf.alias[0]
: 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 {
Expand Down Expand Up @@ -235,7 +228,6 @@ export default async function tab<TArgs extends ArgsDef>(
assertDoubleDashes(name);

const extra = ctx.rawArgs.slice(ctx.rawArgs.indexOf('--') + 1);
// Use t.ts parse method instead of completion.parse
return t.parse(extra);
}
}
Expand Down
Loading
Loading