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

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

import { setupCompletionForPackageManager } from './completion-handlers';

const packageManagers = ['npm', 'pnpm', 'yarn', 'bun'];
const shells = ['zsh', 'bash', 'fish', 'powershell'];

async function main() {
const cli = cac('tab');

const args = process.argv.slice(2);
if (args.length >= 2 && args[1] === 'complete') {
const packageManager = args[0];

if (!packageManagers.includes(packageManager)) {
console.error(`Error: Unsupported package manager "${packageManager}"`);
console.error(
`Supported package managers: ${packageManagers.join(', ')}`
);
process.exit(1);
}

const dashIndex = process.argv.indexOf('--');
if (dashIndex !== -1) {
const completion = new Completion();
setupCompletionForPackageManager(packageManager, completion);
const toComplete = process.argv.slice(dashIndex + 1);
await completion.parse(toComplete);
process.exit(0);
} else {
console.error(`Error: Expected '--' followed by command to complete`);
console.error(
`Example: ${packageManager} exec @bombsh/tab ${packageManager} complete -- command-to-complete`
);
process.exit(1);
}
}

cli
.command(
'<packageManager> <shell>',
'Generate shell completion script for a package manager'
)
.action(async (packageManager, shell) => {
if (!packageManagers.includes(packageManager)) {
console.error(`Error: Unsupported package manager "${packageManager}"`);
console.error(
`Supported package managers: ${packageManagers.join(', ')}`
);
process.exit(1);
}

if (!shells.includes(shell)) {
console.error(`Error: Unsupported shell "${shell}"`);
console.error(`Supported shells: ${shells.join(', ')}`);
process.exit(1);
}

generateCompletionScript(packageManager, shell);
});

const completion = tab(cli);

cli.parse();
}

// function generateCompletionScript(packageManager: string, shell: string) {
// const name = packageManager;
// const executable = process.env.npm_execpath
// ? `${packageManager} exec @bombsh/tab ${packageManager}`
// : `node ${process.argv[1]} ${packageManager}`;
// script(shell as any, name, executable);
// }

function generateCompletionScript(packageManager: string, shell: string) {
const name = packageManager;
// this always points at the actual file on disk (TESTING)
const executable = `node ${process.argv[1]} ${packageManager}`;
script(shell as any, name, executable);
}

main().catch(console.error);
123 changes: 123 additions & 0 deletions bin/completion-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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,
completion: Completion
) {
if (packageManager === 'pnpm') {
setupPnpmCompletions(completion);
} else if (packageManager === 'npm') {
setupNpmCompletions(completion);
} else if (packageManager === 'yarn') {
setupYarnCompletions(completion);
} else if (packageManager === 'bun') {
setupBunCompletions(completion);
}

completion.setPackageManager(packageManager);
}

export function setupPnpmCompletions(completion: Completion) {
completion.addCommand('add', 'Install a package', [], async () => []);
Comment thread
AmirSa12 marked this conversation as resolved.
completion.addCommand('remove', 'Remove a package', [], async () => []);
completion.addCommand(
'install',
'Install all dependencies',
[],
async () => []
);
completion.addCommand('update', 'Update packages', [], async () => []);
completion.addCommand('exec', 'Execute a command', [], async () => []);
completion.addCommand('run', 'Run a script', [], async () => []);
completion.addCommand('publish', 'Publish package', [], async () => []);
completion.addCommand('test', 'Run tests', [], async () => []);
completion.addCommand('build', 'Build project', [], async () => []);
}

export function setupNpmCompletions(completion: Completion) {
completion.addCommand('install', 'Install a package', [], async () => []);
completion.addCommand('uninstall', 'Uninstall a package', [], async () => []);
completion.addCommand('run', 'Run a script', [], async () => []);
completion.addCommand('test', 'Run tests', [], async () => []);
completion.addCommand('publish', 'Publish package', [], async () => []);
completion.addCommand('update', 'Update packages', [], async () => []);
completion.addCommand('start', 'Start the application', [], async () => []);
completion.addCommand('build', 'Build project', [], async () => []);
}

export function setupYarnCompletions(completion: Completion) {
completion.addCommand('add', 'Add a package', [], async () => []);
completion.addCommand('remove', 'Remove a package', [], async () => []);
completion.addCommand('run', 'Run a script', [], async () => []);
completion.addCommand('test', 'Run tests', [], async () => []);
completion.addCommand('publish', 'Publish package', [], async () => []);
completion.addCommand('install', 'Install dependencies', [], async () => []);
completion.addCommand('build', 'Build project', [], async () => []);
}

export function setupBunCompletions(completion: Completion) {
completion.addCommand('add', 'Add a package', [], async () => []);
completion.addCommand('remove', 'Remove a package', [], async () => []);
completion.addCommand('run', 'Run a script', [], async () => []);
completion.addCommand('test', 'Run tests', [], async () => []);
completion.addCommand('install', 'Install dependencies', [], async () => []);
completion.addCommand('update', 'Update packages', [], async () => []);
completion.addCommand('build', 'Build project', [], async () => []);
}
85 changes: 33 additions & 52 deletions examples/demo-cli-cac/demo-cli-cac.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env node

const cac = require('cac');
import cac from 'cac';
import tab from '../../dist/src/cac.js';

const cli = cac('demo-cli-cac');

// Define version and help
Expand Down Expand Up @@ -29,61 +31,40 @@ cli
console.log('Options:', options);
});

// Manual implementation of completion for CAC
if (process.argv[2] === '__complete') {
const args = process.argv.slice(3);
const toComplete = args[args.length - 1] || '';
const previousArgs = args.slice(0, -1);

// Root command completion
if (previousArgs.length === 0) {
console.log('start\tStart the application');
console.log('build\tBuild the application');
console.log('--help\tDisplay help');
console.log('--version\tOutput the version number');
console.log('-c\tSpecify config file');
console.log('--config\tSpecify config file');
console.log('-d\tEnable debugging');
console.log('--debug\tEnable debugging');
process.exit(0);
}

// Subcommand completion
if (previousArgs[0] === 'start') {
console.log('-p\tPort to use');
console.log('--port\tPort to use');
console.log('--help\tDisplay help');
// Set up completion using the cac adapter
const completion = await tab(cli);

// Port value completion if --port is the last arg
if (
previousArgs[previousArgs.length - 1] === '--port' ||
previousArgs[previousArgs.length - 1] === '-p'
) {
console.log('3000\tDefault port');
console.log('8080\tAlternative port');
// custom config for options
for (const command of completion.commands.values()) {
for (const [optionName, config] of command.options.entries()) {
if (optionName === '--port') {
config.handler = () => {
return [
{ value: '3000', description: 'Default port' },
{ value: '8080', description: 'Alternative port' },
];
};
}
process.exit(0);
}

if (previousArgs[0] === 'build') {
console.log('-m\tBuild mode');
console.log('--mode\tBuild mode');
console.log('--help\tDisplay help');
if (optionName === '--mode') {
config.handler = () => {
return [
{ value: 'development', description: 'Development mode' },
{ value: 'production', description: 'Production mode' },
{ value: 'test', description: 'Test mode' },
];
};
}

// Mode value completion if --mode is the last arg
if (
previousArgs[previousArgs.length - 1] === '--mode' ||
previousArgs[previousArgs.length - 1] === '-m'
) {
console.log('development\tDevelopment mode');
console.log('production\tProduction mode');
console.log('test\tTest mode');
if (optionName === '--config') {
config.handler = () => {
return [
{ value: 'config.json', description: 'JSON config file' },
{ value: 'config.js', description: 'JavaScript config file' },
];
};
}
process.exit(0);
}

process.exit(0);
} else {
// Parse CLI args
cli.parse();
}

cli.parse();
1 change: 1 addition & 0 deletions examples/demo-cli-cac/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "Demo CLI using CAC for testing tab completions with pnpm",
"main": "demo-cli-cac.js",
"type": "module",
"bin": {
"demo-cli-cac": "./demo-cli-cac.js"
},
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
{
"name": "tab",
"name": "@bombsh/tab",
"version": "0.0.0",
"description": "",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"bin": {
"tab": "./dist/bin/cli.js"
},
"scripts": {
"test": "vitest",
"type-check": "tsc --noEmit",
"format": "prettier --write .",
"format:check": "prettier --check .",
"build": "tsdown",
"prepare": "pnpm build",
"lint": "eslint src \"./*.ts\""
"lint": "eslint src \"./*.ts\"",
"test-cli": "tsx bin/cli.ts"
},
"files": [
"dist"
Expand All @@ -34,7 +37,6 @@
"vitest": "^2.1.3"
},
"dependencies": {
"examples": "link:./examples",
"mri": "^1.2.0"
},
"exports": {
Expand Down
3 changes: 0 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading