Skip to content

ryangoree/gud-cli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Gud CLI

GitHub NPM Version License: Apache-2.0

Build delightful command-line tools that your users will actually enjoy using.

Gud CLI is a modern TypeScript framework that makes creating interactive CLI applications effortless. Instead of forcing users to memorize complex commands, your CLI can guide them through an intuitive, conversational experience.

npm install @gud/cli

Why Gud CLI?

  • 🎯 User-first design – Missing a required option? Gud CLI automatically prompts for it instead of showing cryptic error messages.
  • πŸ“ Intuitive organization – Commands are just files in folders. Want nested commands? Create nested folders. It's that simple.
  • πŸ”§ TypeScript-powered – Full type safety with intelligent autocompletion for options and parameters.
  • πŸ”Œ Extensible – Plugin system and lifecycle hooks let you customize everything without touching core logic.

Table of Contents

Quick Start

1. Create your CLI entry point

// src/cli.ts
#!/usr/bin/env node
import { run } from '@gud/cli';

// Uses ./commands by default
run();

2. Add your first command

// src/commands/hello.ts
import { command } from '@gud/cli';

export default command({
  description: 'Say hello to someone',
  options: {
    name: {
      description: 'Who to greet',
      type: 'string',
      default: 'World',
    },
  },
  handler: async ({ options, client }) => {
    // Prompts if you pass the prompt option
    const name = await options.name({
      prompt: "What's your name?",
    });
    
    client.log(`Hello, ${name}! πŸ‘‹`);
  },
});

3. Run it

$ tsx src/cli.ts hello
? What's your name? β€Ί Alice
Hello, Alice! πŸ‘‹

What makes it different?

Interactive by default

Traditional CLIs fail hard when options are missing:

$ mycli deploy
Error: Missing required option --environment

Gud CLI can guide users through required options:

$ mycli deploy
? Enter environment β€Ί 
❯ dev
  staging  
  prod

Add the command menu plugin to prompt for missing subcommands too.

File-based routing

Organize commands like you organize code:

commands/
β”œβ”€β”€ hello.ts           # mycli hello
β”œβ”€β”€ users/
β”‚   β”œβ”€β”€ list.ts        # mycli users list
β”‚   β”œβ”€β”€ create.ts      # mycli users create
β”‚   └── [id]/
β”‚       β”œβ”€β”€ show.ts    # mycli users 123 show
β”‚       └── delete.ts  # mycli users 123 delete
└── deploy/
    └── [env].ts       # mycli deploy prod

TypeScript-first

Get full intellisense and type checking:

export default command({
  options: {
    port: { type: 'number', default: 3000 },
    watch: { type: 'boolean' }
  },
  handler: async ({ options }) => {
    const port = await options.port(); // TypeScript knows this is number
    const watch = await options.watch(); // TypeScript knows this is boolean | undefined
  }
});

Examples

Interactive deployment

// commands/deploy.ts
export default command({
  options: {
    environment: {
      type: 'string',
      choices: ['dev', 'staging', 'prod'],
      required: true
    },
    confirm: { type: 'boolean', default: false }
  },
  handler: async ({ options, client }) => {
    // Prompts "Enter environment" because required: true
    const env = await options.environment();
    
    const confirmed = await options.confirm({
      prompt: `Deploy to ${env}?`,
    });
    
    if (!confirmed) {
      client.log('Deployment cancelled');
      return;
    }
    
    client.log(`πŸš€ Deploying to ${env}...`);
  }
});

Parameterized commands

// commands/users/[id]/delete.ts
export default command({
  description: 'Delete a user by ID',
  options: {
    force: { type: 'boolean', description: 'Skip confirmation' }
  },
  handler: async ({ params, options, client }) => {
    const userId = params.id; // 123 from the command: users 123 delete
    const force = await options.force();
    
    if (!force) {
      const confirmed = await client.confirm(
        `Really delete user ${userId}?`
      );
      if (!confirmed) return;
    }
    
    // Delete user logic here
    client.log(`βœ… User ${userId} deleted`);
  }
});

Advanced Features

Plugins

Extend functionality with plugins:

import { run, help, logger } from '@gud/cli';

run({
  plugins: [
    help(), // Adds --help support
    logger(), // Logs command execution
    yourCustomPlugin()
  ]
});

Lifecycle Hooks

Hook into command execution:

import { run } from '@gud/cli';

run({
  hooks: {
    beforeCommand: ({ command, data }) => {
      console.log(`Executing: ${command.commandName}`);
    },
    afterCommand: ({ command, data }) => {
      console.log(`Finished: ${command.commandName}`);
    }
  }
});

Flexible Option Handling

export default command({
  options: {
    username: {
      type: 'string',
      conflicts: ['email'],
    },
    email: {
      type: 'string', 
      conflicts: ['username'],
    }
  },
  handler: async ({ options, client }) => {
    let account = await options.username();

    if (!account) {
      account = await options.email({
        prompt: 'Enter your email',
        validate: (value) => {
          if (!value?.includes('@')) {
            return 'Must be a valid email';
          }
          return true;
        },
      });
    }

    client.log(`Querying account: ${account}`);
  }
});

Built for Scale

Gud CLI grows with your project:

  • Simple scripts: Just run() and a single command file
  • Complex tools: Nested commands, plugins, custom validation
  • Team CLIs: Shared plugins, consistent patterns, full TypeScript support

Whether you're building a quick utility or the next great developer tool, Gud CLI gives you the structure and flexibility you need.

Migration Guide

From Commander.js

// Before (Commander)
program
  .command('hello')
  .option('-n, --name <name>', 'name to greet')
  .action((options) => {
    console.log(`Hello ${options.name || 'World'}`);
  });

// After (Gud CLI)
export default command({
  options: {
    name: {
      alias: ['n'],
      type: 'string',
      description: 'Name to greet',
      default: 'World',
    },
  },
  handler: async ({ options }) => {
    const name = await options.name();
    console.log(`Hello ${name}`);
  },
});

From yargs

// Before (yargs)
yargs(hideBin(process.argv))
  .command(
    'deploy <env>',
    'Deploy to environment',
    {
      env: { describe: 'Environment name', type: 'string' },
    },
    (argv) => {
      console.log(`Deploying to ${argv.env}`);
    },
  );

// After (Gud CLI) - file: commands/deploy/[env].ts
export default command({
  description: 'Deploy to environment',
  handler: async ({ params }) => {
    console.log(`Deploying to ${params.env}`);
  }
});

Community

Reference

About

TypeScript CLI framework with file-based routing.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages