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
57 changes: 0 additions & 57 deletions bin/completion-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,5 @@
// TODO: i do not see any completion functionality in this file. nothing is being provided for the defined commands of these package managers. this is a blocker for release. every each of them should be handled.
import { Completion } from '../src/index.js';
import { execSync } from 'child_process';

const DEBUG = false; // for debugging purposes

function debugLog(...args: any[]) {
if (DEBUG) {
console.error('[DEBUG]', ...args);
}
}

async function checkCliHasCompletions(
cliName: string,
packageManager: string
): Promise<boolean> {
try {
debugLog(`Checking if ${cliName} has completions via ${packageManager}`);
const command = `${packageManager} ${cliName} complete --`;
const result = execSync(command, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
timeout: 1000, // AMIR: we still havin issues with this, it still hangs if a cli doesn't have completions. longer timeout needed for shell completion system (shell → node → package manager → cli)
});
const hasCompletions = !!result.trim();
debugLog(`${cliName} supports completions: ${hasCompletions}`);
return hasCompletions;
} catch (error) {
debugLog(`Error checking completions for ${cliName}:`, error);
return false;
}
}

async function getCliCompletions(
cliName: string,
packageManager: string,
args: string[]
): Promise<string[]> {
try {
const completeArgs = args.map((arg) =>
arg.includes(' ') ? `"${arg}"` : arg
);
const completeCommand = `${packageManager} ${cliName} complete -- ${completeArgs.join(' ')}`;
debugLog(`Getting completions with command: ${completeCommand}`);

const result = execSync(completeCommand, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
timeout: 1000, // same: longer timeout needed for shell completion system (shell → node → package manager → cli)
});

const completions = result.trim().split('\n').filter(Boolean);
debugLog(`Got ${completions.length} completions from ${cliName}`);
return completions;
} catch (error) {
debugLog(`Error getting completions from ${cliName}:`, error);
return [];
}
}

export function setupCompletionForPackageManager(
packageManager: string,
Expand Down
2 changes: 1 addition & 1 deletion examples/demo.cac.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cac from 'cac';
import tab from '../src/cac';
import type { Command, Option, OptionsMap } from '../src/t';
import type { Option, OptionsMap } from '../src/t';

const cli = cac('vite');

Expand Down
5 changes: 2 additions & 3 deletions examples/demo.t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ t.command('dev build', 'Build project');
t.command('dev start', 'Start development server');

// Copy command with multiple arguments
const copyCmd = t
.command('copy', 'Copy files')
t.command('copy', 'Copy files')
.argument('source', function (complete) {
complete('src/', 'Source directory');
complete('dist/', 'Distribution directory');
Expand All @@ -110,7 +109,7 @@ const copyCmd = t
});

// Lint command with variadic arguments
const lintCmd = t.command('lint', 'Lint project').argument(
t.command('lint', 'Lint project').argument(
'files',
function (complete) {
complete('main.ts', 'Main file');
Expand Down
4 changes: 2 additions & 2 deletions src/cac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as powershell from './powershell';
import type { CAC } from 'cac';
import { assertDoubleDashes } from './shared';
import { CompletionConfig } from './shared';
import t from './t';
import t, { RootCommand } from './t';

const execPath = process.execPath;
const processArgs = process.argv.slice(1);
Expand All @@ -22,7 +22,7 @@ function quoteIfNeeded(path: string): string {
export default async function tab(
instance: CAC,
completionConfig?: CompletionConfig
): Promise<any> {
): Promise<RootCommand> {
// Add all commands and their options
for (const cmd of [instance.globalCommand, ...instance.commands]) {
if (cmd.name === 'complete') continue; // Skip completion command
Expand Down
58 changes: 2 additions & 56 deletions src/citty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import type {
} from 'citty';
import { generateFigSpec } from './fig';
import { CompletionConfig, assertDoubleDashes } from './shared';
import { OptionHandler, Command, Option, OptionsMap } from './t';
import t from './t';
import t, { RootCommand } from './t';

function quoteIfNeeded(path: string) {
return path.includes(' ') ? `'${path}'` : path;
Expand All @@ -32,59 +31,6 @@ function isConfigPositional<T extends ArgsDef>(config: CommandDef<T>) {
);
}

// Convert Handler from index.ts to OptionHandler from t.ts
function convertOptionHandler(handler: any): OptionHandler {
return function (
this: Option,
complete: (value: string, description: string) => void,
options: OptionsMap,
previousArgs?: string[],
toComplete?: string,
endsWithSpace?: boolean
) {
// For short flags with equals sign and a value, don't complete (citty behavior)
// Check if this is a short flag option and if the toComplete looks like a value
if (
this.alias &&
toComplete &&
toComplete !== '' &&
!toComplete.startsWith('-')
) {
// This might be a short flag with equals sign and a value
// Check if the previous args contain a short flag with equals sign
if (previousArgs && previousArgs.length > 0) {
const lastArg = previousArgs[previousArgs.length - 1];
if (lastArg.includes('=')) {
const [flag, value] = lastArg.split('=');
if (flag.startsWith('-') && !flag.startsWith('--') && value !== '') {
return; // Don't complete short flags with equals sign and value
}
}
}
}

// Call the old handler with the proper context
const result = handler(
previousArgs || [],
toComplete || '',
endsWithSpace || false
);

if (Array.isArray(result)) {
result.forEach((item: any) =>
complete(item.value, item.description || '')
);
} else if (result && typeof result.then === 'function') {
// Handle async handlers
result.then((items: any[]) => {
items.forEach((item: any) =>
complete(item.value, item.description || '')
);
});
}
};
}

async function handleSubCommands(
subCommands: SubCommandsDef,
parentCmd?: string,
Expand Down Expand Up @@ -169,7 +115,7 @@ async function handleSubCommands(
export default async function tab<TArgs extends ArgsDef>(
instance: CommandDef<TArgs>,
completionConfig?: CompletionConfig
): Promise<any> {
): Promise<RootCommand> {
const meta = await resolve(instance.meta);

if (!meta) {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Completion as CompletionItem } from './t';

const DEBUG = false;

function debugLog(...args: any[]) {
function debugLog(...args: unknown[]) {
if (DEBUG) {
console.error('[DEBUG]', ...args);
}
Expand Down
26 changes: 8 additions & 18 deletions src/t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,7 @@ export class Command {
this.description = description;
}

// Function overloads for better UX
option(value: string, description: string): Command;
option(value: string, description: string, alias: string): Command;
option(value: string, description: string, handler: OptionHandler): Command;
option(
value: string,
description: string,
handler: OptionHandler,
alias: string
): Command;
// Function overloads for better UX - combined into single signature with optional parameters
option(
value: string,
description: string,
Expand Down Expand Up @@ -211,22 +202,23 @@ export class RootCommand extends Command {
args = this.stripOptions(args);
const parts: string[] = [];
let remaining: string[] = [];
let matched: Command = this;
let matchedCommand: Command | null = null;

for (let i = 0; i < args.length; i++) {
const k = args[i];
parts.push(k);
const potential = this.commands.get(parts.join(' '));

if (potential) {
matched = potential;
matchedCommand = potential;
} else {
remaining = args.slice(i, args.length);
break;
}
}

return [matched, remaining];
// If no command was matched, use the root command (this)
return [matchedCommand || this, remaining];
}

// Determine if we should complete flags
Expand Down Expand Up @@ -282,12 +274,10 @@ export class RootCommand extends Command {
) {
// Handle flag value completion
let optionName: string | undefined;
let valueToComplete = toComplete;

if (toComplete.includes('=')) {
const [flag, value] = toComplete.split('=');
const [flag] = toComplete.split('=');
optionName = flag;
valueToComplete = value || '';
} else if (lastPrevArg?.startsWith('-')) {
optionName = lastPrevArg;
}
Expand Down Expand Up @@ -350,7 +340,7 @@ export class RootCommand extends Command {
if (option) return option;

// Try by short alias
for (const [name, opt] of command.options) {
for (const [_name, opt] of command.options) {
if (opt.alias && `-${opt.alias}` === optionName) {
return opt;
}
Expand Down Expand Up @@ -399,7 +389,7 @@ export class RootCommand extends Command {

if (currentArgIndex < argumentEntries.length) {
// We're within the defined arguments
const [argName, argument] = argumentEntries[currentArgIndex];
const [_argName, argument] = argumentEntries[currentArgIndex];
targetArgument = argument;
} else {
// We're beyond the defined arguments, check if the last argument is variadic
Expand Down
Loading