gunshi
repository·main·Indexed 19 days ago
https://github.com/kazupon/gunshiA modern JavaScript command-line library for creating type-safe, declarative, and composable CLIs. It supports Node.js, Deno, and Bun runtimes. The ecosystem includes a monorepo with packages such as @gunshi/bone for minimal footprints, @gunshi/combinators for type-safe argument schemas, @gunshi/definition for command definitions, and plugins for shell completion (@gunshi/plugin-completion) and dry-run support (@gunshi/plugin-dryrun).
What's inside gunshi
- Gunshi is a modern JavaScript command-line library designed to simplify the creation of command-line interfaces (CLIs). It is built to be developer-friendly, flexible, maintainable, and performant, supporting universal runtimes including Node.js, Deno, and Bun.
What is a plugin in Gunshi?
mainA plugin in Gunshi is a modular extension used to add functionality to a CLI application without modifying its core code. Plugins allow for Separation of Concerns (keeping command logic clean by offloading tasks like logging or authentication), Reusability (using the same functionality across different commands or projects), and Composability (combining multiple plugins like authentication, logging, and database plugins into a single CLI lifecycle).Use declarative configuration to define commands
mainGunshi allows you to define CLI commands using a declarative structure via the
definefunction. This approach separates the command's metadata, argument definitions, and usage examples from the actual execution logic (runfunction), making your CLI code more maintainable as complexity grows.A command definition object includes:
- Metadata:
nameanddescriptionto identify the command. - Arguments (
args): A map of argument definitions (type, short flags, descriptions, defaults). - Examples: A string containing usage examples for the help text.
- Run function: An async function that receives a context (
ctx) and executes the command logic.
import { define } from 'gunshi' const command = define({ name: 'command-name', description: 'Command description', args: { // Argument definitions }, examples: 'Example usage', run: ctx => { // Command implementation } })- Metadata:
How context extensions work in Gunshi
mainIn Gunshi, the command context (
ctx) is the central object passed to every command runner. Plugins enhance this context by adding new capabilities through thectx.extensionsproperty.To prevent naming collisions, each plugin registers its extension under a unique identifier (plugin ID) within the
ctx.extensionsobject. This namespacing ensures that different plugins can coexist without overwriting each other's functionality. When a plugin is added to your CLI configuration, its extension becomes available to all commands viactx.extensions[pluginId].import { define } from 'gunshi' import { pluginId as globalId } from '@gunshi/plugin-global' const command = define({ run: ctx => { // Access global plugin extension via its unique ID const globalExtension = ctx.extensions[globalId] // Use extension methods globalExtension.showVersion() globalExtension.showHeader() } })Understand the execution order of multiple Renderer Decorators
mainWhen multiple plugins register renderer decorators, Gunshi builds a chain using a
forloop that iterates through the decorator array from first to last. Each decorator wraps the result of the previous one.Execution Flow
- The base renderer starts as an empty string.
@gunshi/plugin-renderer(a default plugin) wraps the base and provides the actual implementation.- Your custom plugins wrap the previous result in the order they are listed in the
pluginsarray.
If you call
await baseRenderer(ctx)at the start of your decorator, your logic executes after the underlying renderer has finished, but your return value will be the last thing processed in the chain.Example Chain:
Base$\rightarrow$plugin-renderer$\rightarrow$Plugin A$\rightarrow$Plugin B(where Plugin B wraps Plugin A).If Plugin B calls
baseRendererfirst, it waits for Plugin A to finish, then adds its own content. This results in Plugin B's content appearing last in the output.How plugin extensions and CommandContext work together
mainGunshi uses a composition pattern to extend the
CommandContext.- Plugin Registration: During CLI initialization, plugins are executed. If a plugin provides an
extension, it is registered via a uniqueSymbol. - Command Declaration: A command declares which extensions it needs via the
extensionsfield. - Context Creation: When a command is executed,
createCommandContextis called. It identifies the required extensions, runs theirfactoryfunctions using theCommandContextCore(the base context containing arguments and values), and merges the results into a newextproperty on the finalCommandContext. - Type Safety: The
ExtendedCommandtype ensures that therunfunction'sctx.extobject is mapped to the return types of the extension factories, providing full IDE autocompletion and compile-time checks.
- Plugin Registration: During CLI initialization, plugins are executed. If a plugin provides an
Understand the Gunshi Type System
mainGunshi v0.27 uses a type parameter system to provide compile-time safety for CLI applications. It ensures that command arguments and plugin extensions are correctly typed throughout the lifecycle of a command.
Key components include:
GunshiParams: The core type used to define the shape of command arguments (args) and plugin extensions (extensions).- Core Functions:
defineandlazyprovide automatic type inference for standard commands. - Plugin Extensions:
defineWithTypesandlazyWithTypesallow commands to declare expected extensions from plugins that are installed at runtime. - CLI Entry Point: The
clifunction uses type parameters to ensure the entry command is correctly typed.
Understand the rendering architecture and decorator pattern
mainGunshi uses a decorator pattern via
plugin-rendererto handle the display of command information. The rendering flow involves three main components:renderHeader: Displays command header information.renderUsage: Displays command usage instructions.renderValidationErrors: Displays errors encountered during validation.
Important Behavior: Decorators are applied in a chain and follow a LIFO (Last In, First Out) execution order. This means the last plugin registered will be the first one to execute its decorator. This can lead to conflicts where a later plugin overrides the rendering intent of an earlier one if not managed via the
renderingproperty or dedicated control plugins.When to use `define` vs plain objects
mainChoosing between
defineand plain JavaScript objects depends on your environment and needs:Use
definewhen:- You are using TypeScript.
- You want automatic type inference.
- You want IDE autocompletion for the command context.
- You want to catch errors at compile time.
Use plain objects when:
- You are writing plain JavaScript.
- You prefer explicit type annotations.
- You are integrating with existing type definitions.
Understand the relationship between @gunshi/plugin and gunshi/plugin
mainThe APIs and TypeScript type definitions provided by
@gunshi/pluginare identical to those found in thegunshi/pluginentry point of the maingunshipackage.When to use
@gunshi/plugin:- When you are building a standalone plugin package.
- When you want to reduce the installation size of your plugin's dependencies.
When to use
gunshi/plugin:- When you are already using the main
gunshipackage in your project and want to avoid adding an extra dependency.
How CLI hooks and Plugin Decorators interact
mainGunshi uses two different mechanisms for controlling execution:
- CLI-level Hooks: Run before and after the entire command execution process (including the plugin chain). They are defined in the
cli()configuration. - Plugin Decorators: Wrap the command runner itself. Plugins use
decorateCommandto add or modify functionality. Multiple decorators form a chain applied in reverse order (LIFO - last registered, first executed).
Execution Sequence:
onBeforeCommandHook- Plugin Decorator Chain (wraps the runner)
- Command Runner (the actual
runfunction) onAfterCommandHook (if successful)onErrorCommandHook (if any step above fails)
- CLI-level Hooks: Run before and after the entire command execution process (including the plugin chain). They are defined in the
How built-in help and versioning work
mainGunshi provides automatic help generation (e.g., via
--help) and versioning (e.g., via--version). When using the standardcli()function from the maingunshipackage, these features are enabled by default via two built-in plugins:@gunshi/plugin-global: Provides global options like--helpand--version.@gunshi/plugin-renderer: Handles formatted output for help messages, error messages, and usage information.
Note: If you use the lower-level
run()function instead ofcli(), you must manually configure these plugins to enable help and version functionality.