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 @@
.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 @@ -57,51 +75,52 @@
});

// Initialize tab completion
const completion = tab(program);

Check failure on line 78 in examples/demo.commander.ts

View workflow job for this annotation

GitHub Actions / Lint and Type Check

'completion' is declared but its value is never read.

// 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
53 changes: 29 additions & 24 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,21 @@ function runCommand(command: string): Promise<string> {
});
}

const cliTools = ['t', 'citty', 'cac', 'commander'];
// const cliTools = ['t', 'citty', 'cac', 'commander'];
const cliTools = ['commander']; // TEMP For quick testing of commander-specific behavior, uncomment this line and comment out the line above.

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 +44,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 +59,32 @@ 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.
// (Which I think is simple behaviour! Do not know whether option allowed to be specified more than once...)
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 +93,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 +122,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 +232,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
Loading