Skip to main content

oclif vs gluegun vs cleye: CLI Framework Comparison (2026)

·PkgPulse Team

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

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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.