+
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ A powerful CLI parser built with TypeScript and Zod for type-safe command-line i
- 🔀 **Smart conversion**: Automatic kebab-case to camelCase conversion.
- 🏷️ **Alias support**: Define short aliases for any option.
- 📦 **Array handling**: Automatic normalization of single values to arrays.
- 🎯 **Default commands**: Set a default command to run when no command is specified.
- ❓ **Help message**: Automatic help generation for commands and options.
- ⚠️ **Error handling**: Clear, actionable error messages.
- 📦 **ESM support**: Modern ES modules with full TypeScript support.
Expand Down Expand Up @@ -58,6 +59,7 @@ const config = defineConfig({
commands: {
greet: greetCommand,
},
defaultCommand: greetCommand,
});

// Process command line arguments
Expand All @@ -80,10 +82,37 @@ my-cli greet --help
# Run commands
my-cli greet --name Alice
my-cli greet -n Bob --loud

# Run default command
my-cli --name Alice
my-cli greet --name Alice
```

### Advanced Features

#### Default Commands

You can specify a default command that will be executed when no command is provided:

```javascript
const config = defineConfig({
meta: {
name: 'my-app',
version: '1.0.0',
},
commands: {
start: startCommand,
build: buildCommand,
},
defaultCommand: startCommand,
});
```

With this configuration:
- `my-app` will run the `startCommand`
- `my-app --help` will still show the help message
- `my-app build` will run the `build` command

#### Commands with Arguments

```javascript
Expand Down Expand Up @@ -176,6 +205,7 @@ Define the CLI configuration.

- `meta`: CLI metadata (name, version, description)
- `commands`: Object mapping command names to definitions
- `defaultCommand`: Optional default command definition to run when no command is specified

#### `processConfig(config, args)`

Expand Down
57 changes: 57 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,61 @@ describe('index', () => {
const result = processConfig(config, ['test', '--files', 'single.txt']);
expect(result.options).toEqual({ files: ['single.txt'] });
});

it('should use default command when no command is specified', () => {
const defaultCommand = defineCommand({
description: 'Default command',
options: defineOptions(
z.object({
name: z.string().default('world'),
}),
),
action: vi.fn(),
});

const config = defineConfig({
commands: {
greet: defaultCommand,
},
defaultCommand,
});

const result = processConfig(config, ['--name', 'Alice']);
expect(result.command).toBe(defaultCommand);
expect(result.options).toEqual({ name: 'Alice' });
expect(result.args).toEqual([]);
});

it('should still show help when --help is passed with default command', () => {
const defaultCommand = defineCommand({
description: 'Default command',
action: vi.fn(),
});

const config = defineConfig({
commands: {
greet: defaultCommand,
},
defaultCommand,
});

expect(() => processConfig(config, ['--help'])).toThrow('process.exit called');
expect(console.log).toHaveBeenCalled();
});

it('should throw error for unknown command even with default command configured', () => {
const defaultCommand = defineCommand({
description: 'Default command',
action: vi.fn(),
});

const config = defineConfig({
commands: {
greet: defaultCommand,
},
defaultCommand,
});

expect(() => processConfig(config, ['unknown'])).toThrow(/Unknown command:.*unknown/);
});
});
68 changes: 46 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,45 @@ function validateOptions<T extends z.ZodObject<any> = z.ZodObject<any>>(
return optionsDef.schema.parse(normalizedOptions);
}

/**
* Processes a command with its options and arguments.
* Validates options using the command's schema and parses arguments if provided.
*
* @param command - The command definition to process
* @param parsedFlags - Parsed flags from command line
* @param args - Arguments to validate and process
* @returns Processed result containing command, options, and arguments
* @throws Error for validation failures
*/
function processCommandExecution<TCommand extends CommandDefinition<any, any>>(
command: TCommand,
parsedFlags: Record<string, any>,
args: string[],
): ProcessResult<TCommand> {
// Process command options
const options = validateOptions(parsedFlags, command.options);

// Validate args if schema is provided
let validatedArgs: any = args;
if (command.args) {
try {
validatedArgs = command.args.parse(args);
} catch (error) {
if (error instanceof z.ZodError) {
const issues = error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join(', ');
throw new Error(`Argument validation failed: ${issues}`);
}
throw error;
}
}

return {
command,
options,
args: validatedArgs,
} as ProcessResult<TCommand>;
}

/**
* Main entry point for processing CLI configuration and arguments.
* Parses command line arguments, validates options, and returns the processed result.
Expand Down Expand Up @@ -450,6 +489,11 @@ export function processConfig<TCommands extends Record<string, CommandDefinition
// Show help and exit successfully
displayHelp(config.commands, config.meta);
process.exit(0);
} else if (config.defaultCommand) {
// Use default command when no command is specified
return processCommandExecution(config.defaultCommand, parsedFlags, commandArgs) as ProcessResult<
TCommands[keyof TCommands]
>;
} else {
// Show help and throw error
displayHelp(config.commands, config.meta);
Expand All @@ -471,28 +515,8 @@ export function processConfig<TCommands extends Record<string, CommandDefinition
process.exit(0);
}

// Process command options
const options = validateOptions(parsedFlags, command.options);

// Validate args if schema is provided
let validatedArgs: any = remainingArgs;
if (command.args) {
try {
validatedArgs = command.args.parse(remainingArgs);
} catch (error) {
if (error instanceof z.ZodError) {
const issues = error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join(', ');
throw new Error(`Argument validation failed: ${issues}`);
}
throw error;
}
}

return {
command,
options,
args: validatedArgs,
} as ProcessResult<TCommands[keyof TCommands]>;
// Process the command
return processCommandExecution(command, parsedFlags, remainingArgs) as ProcessResult<TCommands[keyof TCommands]>;
}

// Export main functions and types from config
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface DefineConfig<TCommands extends Record<string, CommandDefinition
description?: string;
};
commands: TCommands;
defaultCommand?: CommandDefinition<any, any>;
}

export interface ProcessResult<TCommand extends CommandDefinition<any, any> = CommandDefinition<any, any>> {
Expand Down
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载