Skip to content
Draft
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
101 changes: 60 additions & 41 deletions examples/demo.commander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ program
.option('-v, --verbose', 'enable verbose output');

// Add commands
const devCommand = program
.command('dev')
.description('Start dev server')
.option('-H, --host [host]', `Specify hostname`)
.option('-p, --port <port>', `Specify port`)
.option('-v, --verbose', `Enable verbose logging`)
.option('--quiet', `Suppress output`)
.action((options) => {});
// subcommands of dev
devCommand
.command('start')
.description('Start development server')
.action((options) => {});
devCommand
.command('build')
.description('Build project')
.action((options) => {});

program
.command('serve')
.description('Start the server')
Expand Down Expand Up @@ -60,48 +78,49 @@ program
const completion = tab(program);

// Configure custom completions
for (const command of completion.commands.values()) {
if (command.value === 'lint') {
// Note: Direct handler assignment is not supported in the current API
// Custom completion logic would need to be implemented differently
}
// This needs rewriting with config passed into tab(program)?
// for (const command of completion.commands.values()) {
// if (command.value === 'lint') {
// // 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()) {
if (option === '--port') {
config.handler = () => {
return [
{ value: '3000', description: 'Default port' },
{ value: '8080', description: 'Alternative port' },
];
};
}
if (option === '--host') {
config.handler = () => {
return [
{ value: 'localhost', description: 'Local development' },
{ value: '0.0.0.0', description: 'All interfaces' },
];
};
}
if (option === '--mode') {
config.handler = () => {
return [
{ value: 'development', description: 'Development mode' },
{ value: 'production', description: 'Production mode' },
{ value: 'test', description: 'Test mode' },
];
};
}
if (option === '--config') {
config.handler = () => {
return [
{ value: 'config.json', description: 'JSON config file' },
{ value: 'config.yaml', description: 'YAML config file' },
];
};
}
}
}
// for (const [option, config] of command.options.entries()) {
// if (option === '--port') {
// config.handler = () => {
// return [
// { value: '3000', description: 'Default port' },
// { value: '8080', description: 'Alternative port' },
// ];
// };
// }
// if (option === '--host') {
// config.handler = () => {
// return [
// { value: 'localhost', description: 'Local development' },
// { value: '0.0.0.0', description: 'All interfaces' },
// ];
// };
// }
// if (option === '--mode') {
// config.handler = () => {
// return [
// { value: 'development', description: 'Development mode' },
// { value: 'production', description: 'Production mode' },
// { value: 'test', description: 'Test mode' },
// ];
// };
// }
// if (option === '--config') {
// config.handler = () => {
// return [
// { value: 'config.json', description: 'JSON config file' },
// { value: 'config.yaml', description: 'YAML config file' },
// ];
// };
// }
// }
// }

// Parse command line arguments
program.parse();
44 changes: 44 additions & 0 deletions tests/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,50 @@ my-tool My tool
"
`;

exports[`cli completion tests for commander > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = `
"--port Specify port
:4
"
`;

exports[`cli completion tests for commander > cli option completion tests > should complete option for partial input '{ partial: '-H', expected: '-H' }' 1`] = `
"-H Specify hostname
:4
"
`;

exports[`cli completion tests for commander > cli option completion tests > should complete option for partial input '{ partial: '-p', expected: '-p' }' 1`] = `
"-p Specify port
:4
"
`;

exports[`cli completion tests for commander > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = `
":4
"
`;

exports[`cli completion tests for commander > cli option value handling > should handle unknown options with no completions 1`] = `":4"`;

exports[`cli completion tests for commander > cli option value handling > should not show duplicate options 1`] = `
"--version output the version number
--config specify config file
--debug enable debugging
--verbose enable verbose output
:4
"
`;

exports[`cli completion tests for commander > should complete cli options 1`] = `
"dev Start dev server
serve Start the server
build Build the project
deploy Deploy the application
lint Lint source files
:4
"
`;

exports[`cli completion tests for t > --config option tests > should complete --config option values 1`] = `
"vite.config.ts Vite config file
vite.config.js Vite config file
Expand Down
49 changes: 26 additions & 23 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,17 @@ function runCommand(command: string): Promise<string> {
const cliTools = ['t', 'citty', 'cac', 'commander'];

describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
// For Commander, we need to skip most of the tests since it handles completion differently
// Commander has not been refactored yet for new way of passing in custom completion handlers.
const shouldSkipTest = cliTool === 'commander';

// Commander uses a different command structure for completion
// TODO: why commander does that? our convention is the -- part which should be always there.
const commandPrefix =
cliTool === 'commander'
? `pnpm tsx examples/demo.${cliTool}.ts complete`
: `pnpm tsx examples/demo.${cliTool}.ts complete --`;
const commandPrefix = `pnpm tsx examples/demo.${cliTool}.ts complete --`;

it.runIf(!shouldSkipTest)('should complete cli options', async () => {
it('should complete cli options', async () => {
const output = await runCommand(`${commandPrefix}`);
expect(output).toMatchSnapshot();
});

describe.runIf(!shouldSkipTest)('cli option completion tests', () => {
describe('cli option completion tests', () => {
const optionTests = [
{ partial: '--p', expected: '--port' },
{ partial: '-p', expected: '-p' }, // Test short flag completion
Expand All @@ -48,7 +43,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
);
});

describe.runIf(!shouldSkipTest)('cli option exclusion tests', () => {
describe('cli option exclusion tests', () => {
const alreadySpecifiedTests = [
{ specified: '--config', shouldNotContain: '--config' },
];
Expand All @@ -63,24 +58,31 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
);
});

describe.runIf(!shouldSkipTest)('cli option value handling', () => {
it('should resolve port value correctly', async () => {
const command = `${commandPrefix} dev --port=3`;
const output = await runCommand(command);
expect(output).toMatchSnapshot();
});
describe('cli option value handling', () => {
it.runIf(!shouldSkipTest)(
'should resolve port value correctly',
async () => {
const command = `${commandPrefix} dev --port=3`;
const output = await runCommand(command);
expect(output).toMatchSnapshot();
}
);

// Note: on all frameworks, --config is suggested again, which is inconsistent with test title.
it('should not show duplicate options', async () => {
const command = `${commandPrefix} --config vite.config.js --`;
const output = await runCommand(command);
expect(output).toMatchSnapshot();
});

it('should resolve config option values correctly', async () => {
const command = `${commandPrefix} --config vite.config`;
const output = await runCommand(command);
expect(output).toMatchSnapshot();
});
it.runIf(!shouldSkipTest)(
'should resolve config option values correctly',
async () => {
const command = `${commandPrefix} --config vite.config`;
const output = await runCommand(command);
expect(output).toMatchSnapshot();
}
);

it('should handle unknown options with no completions', async () => {
const command = `${commandPrefix} --unknownoption`;
Expand All @@ -89,7 +91,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
});
});

describe.runIf(!shouldSkipTest)('boolean option handling', () => {
describe('boolean option handling', () => {
it('should complete subcommands and arguments after boolean options', async () => {
const command = `${commandPrefix} dev --verbose ""`;
const output = await runCommand(command);
Expand Down Expand Up @@ -118,7 +120,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
it('should not interfere with option completion after boolean options', async () => {
const command = `${commandPrefix} dev --verbose --h`;
const output = await runCommand(command);
// Should complete subcommands that start with 's' even after a boolean option
// Should complete options that start with '--h' even after a boolean option
expect(output).toContain('--host');
});
});
Expand Down Expand Up @@ -228,6 +230,7 @@ describe.each(cliTools)('cli completion tests for %s', (cliTool) => {
expect(output).toMatchSnapshot();
});

// Note: on all frameworks, --config is suggested again, which is inconsistent with test title.
it('should not suggest --config after it has been used', async () => {
const command = `${commandPrefix} --config vite.config.ts --`;
const output = await runCommand(command);
Expand Down