oclif vs gluegun vs cleye: CLI Framework Comparison (2026)
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
| Feature | oclif | gluegun | cleye |
|---|---|---|---|
| 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.