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
7 changes: 4 additions & 3 deletions bin/cli.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env node

import cac from 'cac';
import { script, Completion } from '../src/index.js';
import { script } from '../src/t.js';
import tab from '../src/cac.js';

import { setupCompletionForPackageManager } from './completion-handlers';
import { PackageManagerCompletion } from './package-manager-completion.js';

const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'];
const shells = ['zsh', 'bash', 'fish', 'powershell'];
Expand All @@ -27,8 +28,8 @@ async function main() {

const dashIndex = process.argv.indexOf('--');
if (dashIndex !== -1) {
// TOOD: there's no Completion anymore
const completion = new Completion();
// Use the new PackageManagerCompletion wrapper
const completion = new PackageManagerCompletion(packageManager);
setupCompletionForPackageManager(packageManager, completion);
const toComplete = process.argv.slice(dashIndex + 1);
await completion.parse(toComplete);
Expand Down
95 changes: 49 additions & 46 deletions bin/completion-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
// 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';

const noopCompletion = async () => [];
import { PackageManagerCompletion } from './package-manager-completion.js';

export function setupCompletionForPackageManager(
packageManager: string,
completion: Completion
completion: PackageManagerCompletion
) {
if (packageManager === 'pnpm') {
setupPnpmCompletions(completion);
Expand All @@ -17,54 +15,59 @@ export function setupCompletionForPackageManager(
setupBunCompletions(completion);
}

// TODO: the core functionality of tab should have nothing related to package managers. even though completion is not there anymore, but this is something to consider.
completion.setPackageManager(packageManager);
// Note: Package manager logic is now handled by PackageManagerCompletion wrapper
}

export function setupPnpmCompletions(completion: Completion) {
completion.addCommand('add', 'Install a package', [], noopCompletion);
completion.addCommand('remove', 'Remove a package', [], noopCompletion);
completion.addCommand(
'install',
'Install all dependencies',
[],
noopCompletion
);
completion.addCommand('update', 'Update packages', [], noopCompletion);
completion.addCommand('exec', 'Execute a command', [], noopCompletion);
completion.addCommand('run', 'Run a script', [], noopCompletion);
completion.addCommand('publish', 'Publish package', [], noopCompletion);
completion.addCommand('test', 'Run tests', [], noopCompletion);
completion.addCommand('build', 'Build project', [], noopCompletion);
export function setupPnpmCompletions(completion: PackageManagerCompletion) {
completion.command('add', 'Install a package');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not see any completion for all of these commands, we should provide for all of them before release. that's what we agreed on as far as i remember.

completion.command('remove', 'Remove a package');
completion.command('install', 'Install dependencies');
completion.command('update', 'Update dependencies');
completion.command('run', 'Run a script');
completion.command('exec', 'Execute a command');
completion.command('dlx', 'Run a package without installing');
completion.command('create', 'Create a new project');
completion.command('init', 'Initialize a new project');
completion.command('publish', 'Publish the package');
completion.command('pack', 'Create a tarball');
completion.command('link', 'Link a package');
completion.command('unlink', 'Unlink a package');
completion.command('outdated', 'Check for outdated packages');
completion.command('audit', 'Run security audit');
completion.command('list', 'List installed packages');
}

export function setupNpmCompletions(completion: Completion) {
completion.addCommand('install', 'Install a package', [], noopCompletion);
completion.addCommand('uninstall', 'Uninstall a package', [], noopCompletion);
completion.addCommand('run', 'Run a script', [], noopCompletion);
completion.addCommand('test', 'Run tests', [], noopCompletion);
completion.addCommand('publish', 'Publish package', [], noopCompletion);
completion.addCommand('update', 'Update packages', [], noopCompletion);
completion.addCommand('start', 'Start the application', [], noopCompletion);
completion.addCommand('build', 'Build project', [], noopCompletion);
export function setupNpmCompletions(completion: PackageManagerCompletion) {
completion.command('install', 'Install a package');
completion.command('uninstall', 'Remove a package');
completion.command('update', 'Update dependencies');
completion.command('run', 'Run a script');
completion.command('exec', 'Execute a command');
completion.command('init', 'Initialize a new project');
completion.command('publish', 'Publish the package');
completion.command('pack', 'Create a tarball');
completion.command('link', 'Link a package');
completion.command('unlink', 'Unlink a package');
}

export function setupYarnCompletions(completion: Completion) {
completion.addCommand('add', 'Add a package', [], noopCompletion);
completion.addCommand('remove', 'Remove a package', [], noopCompletion);
completion.addCommand('run', 'Run a script', [], noopCompletion);
completion.addCommand('test', 'Run tests', [], noopCompletion);
completion.addCommand('publish', 'Publish package', [], noopCompletion);
completion.addCommand('install', 'Install dependencies', [], noopCompletion);
completion.addCommand('build', 'Build project', [], noopCompletion);
export function setupYarnCompletions(completion: PackageManagerCompletion) {
completion.command('add', 'Install a package');
completion.command('remove', 'Remove a package');
completion.command('install', 'Install dependencies');
completion.command('upgrade', 'Update dependencies');
completion.command('run', 'Run a script');
completion.command('exec', 'Execute a command');
completion.command('create', 'Create a new project');
completion.command('init', 'Initialize a new project');
}

export function setupBunCompletions(completion: Completion) {
completion.addCommand('add', 'Add a package', [], noopCompletion);
completion.addCommand('remove', 'Remove a package', [], noopCompletion);
completion.addCommand('run', 'Run a script', [], noopCompletion);
completion.addCommand('test', 'Run tests', [], noopCompletion);
completion.addCommand('install', 'Install dependencies', [], noopCompletion);
completion.addCommand('update', 'Update packages', [], noopCompletion);
completion.addCommand('build', 'Build project', [], noopCompletion);
export function setupBunCompletions(completion: PackageManagerCompletion) {
completion.command('add', 'Install a package');
completion.command('remove', 'Remove a package');
completion.command('install', 'Install dependencies');
completion.command('update', 'Update dependencies');
completion.command('run', 'Run a script');
completion.command('x', 'Execute a command');
completion.command('create', 'Create a new project');
completion.command('init', 'Initialize a new project');
}
115 changes: 115 additions & 0 deletions bin/package-manager-completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { execSync } from 'child_process';
import { RootCommand } from '../src/t.js';

function debugLog(...args: any[]) {
if (process.env.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,
});
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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function and checkCliHasCompletions has so much redundancy and i feel they're heavily similar.

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,
});

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 [];
}
}

/**
* Package Manager Completion Wrapper
*
* This extends RootCommand and adds package manager-specific logic.
* It acts as a layer on top of the core tab library.
*/
export class PackageManagerCompletion extends RootCommand {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not think we need this class, it can be done through some imperative functions and calls.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove it and revisit the implementation. it's a bit hard to understand this implementation for me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and we need tests for the package manager completions.

private packageManager: string;

constructor(packageManager: string) {
super();
this.packageManager = packageManager;
}

// Enhanced parse method with package manager logic
async parse(args: string[]) {
// Handle package manager completions first
if (args.length >= 1 && args[0].trim() !== '') {
const potentialCliName = args[0];
const knownCommands = [...this.commands.keys()];

if (!knownCommands.includes(potentialCliName)) {
const hasCompletions = await checkCliHasCompletions(
potentialCliName,
this.packageManager
);

if (hasCompletions) {
const cliArgs = args.slice(1);
const suggestions = await getCliCompletions(
potentialCliName,
this.packageManager,
cliArgs
);

if (suggestions.length > 0) {
// Print completions directly in the same format as the core library
for (const suggestion of suggestions) {
if (suggestion.startsWith(':')) continue;

if (suggestion.includes('\t')) {
const [value, description] = suggestion.split('\t');
console.log(`${value}\t${description}`);
} else {
console.log(suggestion);
}
}
console.log(':4'); // Shell completion directive (NoFileComp)
return;
}
}
}
}

// Fall back to regular completion logic (shows basic package manager commands)
return super.parse(args);
}
}
10 changes: 3 additions & 7 deletions examples/demo.commander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,9 @@ const completion = tab(program);

// Configure custom completions
for (const command of completion.commands.values()) {
if (command.name === 'lint') {
command.handler = () => {
return [
{ value: 'src/**/*.ts', description: 'TypeScript source files' },
{ value: 'tests/**/*.ts', description: 'Test files' },
];
};
if (command.value === 'lint') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the whole commander api needs a new look.

// Note: Direct handler assignment is not supported in the current API
// Custom completion logic would need to be implemented differently
}

for (const [option, config] of command.options.entries()) {
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "@bombsh/tab",
"version": "0.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./dist/t.js",
"types": "./dist/t.d.ts",
"type": "module",
"bin": {
"tab": "./dist/bin/cli.js"
Expand Down Expand Up @@ -41,9 +41,9 @@
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
"types": "./dist/t.d.ts",
"import": "./dist/t.js",
"require": "./dist/t.cjs"
},
"./citty": {
"types": "./dist/citty.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/bash.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ShellCompDirective } from './';
import { ShellCompDirective } from './t';

export function generate(name: string, exec: string): string {
// Replace '-' and ':' with '_' for variable names
Expand Down
Loading
Loading