Skip to main content

Guide

oclif vs gluegun vs cleye: CLI Framework 2026

Compare oclif, gluegun, and cleye for building CLI tools in Node.js. Command routing, plugin systems, flag parsing, interactive prompts, and which CLI.

·PkgPulse Team·
0

TL;DR

oclif is the enterprise-grade CLI framework from Salesforce — powers the Heroku CLI, Salesforce CLI, and many others. Plugin system, auto-generated help, command classes with typed flags. gluegun is the developer-friendly toolkit — ships with prompts, filesystem utilities, template generation, and a plugin system out of the box. cleye is the modern, minimal TypeScript-first flag parser and command router — no frills, zero runtime dependencies, excellent type inference. In 2026: oclif for large multi-command CLIs, cleye for small-to-medium tools, gluegun if you need code generation in your CLI.

Key Takeaways

  • oclif: ~300K weekly downloads — Salesforce, class-based commands, plugin ecosystem, generators
  • gluegun: ~100K weekly downloads — all-in-one toolkit, template generation, prompts, spinner built in
  • cleye: ~100K weekly downloads — minimal, TypeScript-first, zero deps, excellent flag type inference
  • oclif uses class-based commands — each command is a class with a static run() method
  • gluegun ships with file system, HTTP, prompts, print, semver utilities — batteries included
  • cleye focuses purely on flag parsing and command routing — bring your own prompts/spinner

When to Use a CLI Framework

When NOT to use a framework:
  Simple one-command scripts → commander.js or yargs is fine
  Internal dev scripts → plain node:parseArgs or even process.argv

When a framework helps:
  Multi-command CLIs (git-style: mycli init, mycli deploy, mycli status)
  CLIs with plugin systems (third parties can add commands)
  CLIs that ship with generators/scaffolding (like create-react-app)
  Team-built CLIs with multiple maintainers and help text requirements

oclif

oclif — enterprise CLI framework (Salesforce):

Create a new CLI

npx oclif generate mycli
cd mycli

# Project structure:
# src/commands/         ← Each file = one command
# src/hooks/            ← Lifecycle hooks
# src/index.ts          ← Entry point
# bin/run.js            ← Executable

Command structure

// src/commands/packages/compare.ts
import { Command, Flags, Args } from "@oclif/core"

export default class PackagesCompare extends Command {
  static description = "Compare two npm packages on PkgPulse"

  static examples = [
    "$ mycli packages compare react vue",
    "$ mycli packages compare react vue --format json",
  ]

  static args = {
    packageA: Args.string({ required: true, description: "First package" }),
    packageB: Args.string({ required: true, description: "Second package" }),
  }

  static flags = {
    format: Flags.string({
      char: "f",
      options: ["table", "json"],
      default: "table",
      description: "Output format",
    }),
    limit: Flags.integer({
      char: "l",
      default: 10,
      description: "Number of metrics to show",
    }),
    verbose: Flags.boolean({
      char: "v",
      description: "Verbose output",
    }),
  }

  async run(): Promise<void> {
    const { args, flags } = await this.parse(PackagesCompare)

    this.log(`Comparing ${args.packageA} vs ${args.packageB}...`)

    const data = await this.fetchComparison(args.packageA, args.packageB)

    if (flags.format === "json") {
      this.log(JSON.stringify(data, null, 2))
    } else {
      this.printTable(data, flags.limit)
    }
  }

  private async fetchComparison(a: string, b: string) {
    // ...
  }

  private printTable(data: unknown, limit: number) {
    // oclif has built-in table utilities
  }
}

Auto-generated help

$ mycli packages compare --help
Compare two npm packages on PkgPulse

USAGE
  $ mycli packages compare PACKAGEA PACKAGEB

ARGUMENTS
  PACKAGEA  First package
  PACKAGEB  Second package

FLAGS
  -f, --format=<option>  [default: table] Output format
                         <options: table|json>
  -l, --limit=<value>    [default: 10] Number of metrics to show
  -v, --verbose          Verbose output

EXAMPLES
  $ mycli packages compare react vue
  $ mycli packages compare react vue --format json

Plugin system

// oclif supports third-party plugins that add commands:
// package.json:
{
  "oclif": {
    "plugins": ["@pkgpulse/oclif-plugin-health"]
  }
}

// The plugin adds commands automatically:
// $ mycli health check react
// $ mycli health score react --threshold 80

gluegun

gluegun — batteries-included CLI toolkit:

Setup

npm install gluegun

CLI entry point

// src/cli.ts
import { build } from "gluegun"

const cli = build()
  .brand("pkgpulse")
  .src(__dirname)
  .plugins("./node_modules", { matching: "pkgpulse-*", hidden: true })
  .help()
  .version()
  .create()

export { cli }

Command structure

// src/commands/compare.ts
import type { GluegunCommand } from "gluegun"

const command: GluegunCommand = {
  name: "compare",
  description: "Compare two npm packages",
  run: async (toolbox) => {
    const { print, prompt, filesystem, http, parameters, strings } = toolbox

    const { packageA, packageB } = await prompt.ask([
      {
        type: "input",
        name: "packageA",
        message: "First package:",
        initial: parameters.first,
      },
      {
        type: "input",
        name: "packageB",
        message: "Second package:",
        initial: parameters.second,
      },
    ])

    const spinner = print.spin(`Fetching ${packageA} vs ${packageB}...`)

    try {
      const api = http.create({ baseURL: "https://api.pkgpulse.com" })
      const result = await api.get(`/compare?a=${packageA}&b=${packageB}`)
      spinner.succeed("Done!")

      print.table(
        [
          ["Metric", packageA, packageB],
          ["Downloads/wk", result.data.a.downloads, result.data.b.downloads],
          ["Health score", result.data.a.score, result.data.b.score],
          ["Stars", result.data.a.stars, result.data.b.stars],
        ],
        { format: "markdown" }
      )
    } catch (error) {
      spinner.fail(`Error: ${error.message}`)
      process.exit(1)
    }
  },
}

export default command

Code generation (gluegun's killer feature)

// src/commands/init.ts — scaffold a new project:
import type { GluegunCommand } from "gluegun"

const command: GluegunCommand = {
  name: "init",
  run: async (toolbox) => {
    const { template, filesystem, print, prompt } = toolbox

    const { projectName } = await prompt.ask({
      type: "input",
      name: "projectName",
      message: "Project name:",
    })

    // Generate files from templates:
    await template.generate({
      template: "package.json.ejs",
      target: `${projectName}/package.json`,
      props: { name: projectName, version: "1.0.0" },
    })

    await template.generate({
      template: "index.ts.ejs",
      target: `${projectName}/src/index.ts`,
      props: { name: projectName },
    })

    filesystem.dir(`${projectName}/src`)
    print.success(`✓ Created ${projectName}`)
  },
}
<!-- src/templates/package.json.ejs -->
{
  "name": "<%= props.name %>",
  "version": "<%= props.version %>",
  "scripts": {
    "build": "tsup",
    "dev": "tsx src/index.ts"
  }
}

cleye

cleye — TypeScript-first flag parsing:

Basic usage

import { cli } from "cleye"

const argv = cli({
  name: "pkgpulse",
  version: "1.0.0",

  parameters: ["<packageA>", "<packageB>"],  // Required positional args

  flags: {
    format: {
      type: String,
      alias: "f",
      description: "Output format",
      default: "table",
    },
    limit: {
      type: Number,
      alias: "l",
      description: "Number of metrics to show",
      default: 10,
    },
    verbose: {
      type: Boolean,
      alias: "v",
      description: "Verbose output",
    },
  },
})

// Fully typed — TypeScript knows the types:
const packageA: string = argv._.packageA   // string
const packageB: string = argv._.packageB   // string
const format: string = argv.flags.format   // string
const limit: number = argv.flags.limit     // number
const verbose: boolean = argv.flags.verbose // boolean

Sub-commands

import { cli } from "cleye"

const argv = cli({
  name: "pkgpulse",
  commands: [compareCommand, searchCommand, initCommand],
})

// compare command:
import { command } from "cleye"

const compareCommand = command(
  {
    name: "compare",
    parameters: ["<packageA>", "<packageB>"],
    flags: {
      format: { type: String, default: "table" },
    },
  },
  (argv) => {
    const { packageA, packageB } = argv._
    const { format } = argv.flags

    console.log(`Comparing ${packageA} vs ${packageB} (${format})`)
  }
)

Type inference in action

import { cli } from "cleye"

// cleye infers types from the type property:
const argv = cli({
  flags: {
    count: { type: Number },           // argv.flags.count: number
    tags: { type: [String] },          // argv.flags.tags: string[]
    dryRun: { type: Boolean },         // argv.flags.dryRun: boolean
    config: { type: String, default: ".config.json" },  // string (not string | undefined)
  },
})

// TypeScript catches this at compile time:
// argv.flags.count.toFixed(2)    ✅
// argv.flags.count.split(",")    ❌ Property 'split' does not exist on type 'number'

Feature Comparison

Featureoclifglueguncleye
Multi-command routing
Plugin system✅ (first-class)
Code generation / templates✅ (generators)✅ (built-in)
Interactive prompts✅ (via plugins)✅ (built-in)❌ (bring your own)
Spinner / progress
File system utilities
HTTP client✅ (apisauce)
TypeScript flag types✅ (best-in-class)
Auto help generation
Zero dependencies
Weekly downloads~300K~100K~100K

When to Use Each

Choose oclif if:

  • Building a large, production CLI distributed to thousands of users
  • Need a plugin ecosystem (third parties extend your CLI)
  • Salesforce/Heroku-style multi-command hierarchy
  • Want auto-generated help, update notifications, and command testing

Choose gluegun if:

  • Building a scaffolding or code generation tool (like create-app)
  • Need interactive prompts, spinners, and filesystem in one package
  • Want an opinionated toolkit vs assembling individual packages

Choose cleye if:

  • Want the best TypeScript flag type inference
  • Building a smaller CLI (1-10 commands) with zero dependencies
  • Plan to bring your own prompt/spinner libraries (clack, ora, etc.)
  • Codebase values minimal dependencies

Also consider:

  • commander.js — most battle-tested, vast documentation, slightly more verbose
  • yargs — great for complex flag parsing with automatic help
  • cac — minimal commander alternative, good TypeScript support

Testing CLI Applications

CLI testing is an under-discussed dimension when choosing a framework, because the ergonomics differ significantly across the three options. Oclif ships with a dedicated testing helper, @oclif/test, that wraps Mocha and provides test.command() to invoke your commands programmatically and assert on stdout, stderr, and exit codes. This is purpose-built for oclif's class-based command architecture — you instantiate the command, pass mock arguments, and assert on the structured output without spawning a child process. For large CLIs with many commands, this test isolation keeps your test suite fast and avoids global state leakage between test cases.

Gluegun commands are pure functions that receive a toolbox object, which makes unit testing straightforward: you mock the toolbox properties (print, filesystem, prompt, http) and call the command's run function directly. The downside is that your mocks must replicate the full toolbox shape, which can be verbose. In practice, most gluegun projects settle on a thin integration test using execa to spawn the actual CLI binary — simpler to write but slower to run.

Cleye's minimal footprint means there is no framework-owned test helper at all. Because cleye only handles argument parsing, your command logic sits in plain functions that receive typed arguments. This is the easiest to test in isolation: call the function with a mock argv object and assert on whatever your function produces. The CLI entry point itself can be tested with execa or Node's built-in child_process.spawn. The absence of a testing layer is a feature here — you're not locked into any test runner.

Distribution and Package Setup

Publishing and distributing your CLI is where the frameworks diverge sharply. Oclif's generator sets up the full publishing pipeline: @oclif/core handles command discovery via the file system, package.json oclif config block registers the bin name, and oclif manifest generates a manifest used by the auto-update and help system. Oclif CLIs are commonly distributed via npm install, Homebrew tap, or Oclif's own install scripts. The manifest system means users get accurate help and completion even without running the command first.

Gluegun CLIs are published as normal npm packages with a bin entry in package.json. The build step compiles TypeScript and resolves the template directory into the output. Distribution is simple — npm publish and users npm install -g your package. Gluegun's lack of a manifest means auto-generated help is slightly less sophisticated, but for most scaffolding tools this is a non-issue.

Cleye is the most vanilla: it's just argument parsing, so your distribution strategy is entirely your own. This is ideal for CLIs distributed as part of a monorepo (e.g., packages/my-cli) where you want zero framework overhead in the published artifact. Many teams combine cleye with tsup for bundling and pkg or @vercel/ncc for producing a self-contained binary. Because cleye adds no runtime framework dependency, the resulting binary is smaller and starts faster than oclif or gluegun equivalents.

Error Handling and User-Facing Error Messages

The quality of error messages in a CLI is often what separates professional tooling from throwaway scripts. Users who receive cryptic stack traces when they mistype a flag are less likely to trust or adopt your tool. Oclif handles many common error cases automatically: passing an unknown flag triggers a formatted error with a suggestion (Did you mean --format?), missing required arguments produce a descriptive usage message, and flag value validation (for flags with options: ["json", "table"]) outputs the list of valid values alongside the error.

Performance and Startup Time Considerations

CLI startup time matters more than application startup time because users invoke CLIs interactively and notice latency above 200ms. Oclif's class-based command discovery scans the filesystem at startup to find command files, loads plugins, and reads configuration from the manifest. This discovery cost is front-loaded on every invocation. For large CLIs like the Heroku CLI (which oclif powers), this startup overhead is mitigated by lazy-loading commands: the manifest pre-indexes all commands, so only the invoked command's file is loaded at runtime. Without lazy loading, a CLI with 50 commands that requires all of them at startup can add 300-500ms before the command begins executing.

Gluegun's startup cost depends on how many built-in toolbox extensions are loaded. The full build().src() initialization chain loads prompts, HTTP, filesystem, print, template, and semver utilities regardless of which command is invoked. For scaffolding CLIs where users invoke my-cli init once and the full startup overhead is acceptable, this is fine. For interactive CLIs used dozens of times per hour, it is worth profiling whether the unused toolbox extensions (particularly the HTTP client and template system) add measurable overhead.

Cleye has the lowest startup cost of the three by a significant margin — it is a single JavaScript file with zero runtime dependencies that parses process.argv synchronously. There is no command discovery, no plugin loading, and no extension initialization. The first line of your CLI's actual business logic executes faster with cleye than with any framework-based alternative. This makes cleye particularly well-suited for CLIs distributed as part of CI pipelines where startup time contributes to total job duration, and for CLIs packaged as single binaries using tools like @vercel/ncc where minimizing module initialization overhead directly reduces binary size.

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on oclif v4.x, gluegun v5.x, and cleye v2.x.

Compare CLI and developer tooling packages on PkgPulse →

See also: cac vs meow vs arg 2026 and cosmiconfig vs lilconfig vs conf, archiver vs adm-zip vs JSZip (2026).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.