Building CLI Tools with oclif: Lessons from a Deprecated GitHub Search Tool
Hook
Sometimes the best way to learn a framework is by studying a tool that's been deprecated—especially when its successor borrowed the same patterns.
Context
Before GitHub released their official CLI in 2020, developers who wanted to search GitHub from the terminal were stuck with either curl commands against raw API endpoints or cobbling together scripts with tools like jq. The GitHub web interface had robust search, but switching contexts from terminal to browser killed productivity for developers living in the command line.
gh-search-cli emerged to fill this gap, built by a developer who needed programmatic access to GitHub's search API without the ceremony of writing custom API clients. Rather than reinvent the wheel, it leveraged oclif—Heroku's battle-tested CLI framework used by their own tooling. The tool mapped GitHub's search qualifiers (language, stars, date ranges) to CLI flags, handling authentication, pagination, and output formatting. When GitHub's official CLI arrived with native search commands, the maintainer deprecated gh-search-cli, but its architecture remains instructive for anyone building API-wrapping CLIs.
Technical Insight
The core architectural decision behind gh-search-cli was using oclif's command-per-endpoint pattern. Rather than building a monolithic CLI with complex routing, oclif promotes a structure where each command is a class extending a base Command type. Here's how gh-search-cli structured its repository search:
import {Command, flags} from '@oclif/command'
import {Octokit} from '@octokit/rest'
export default class Repositories extends Command {
static flags = {
language: flags.string({char: 'l', description: 'filter by language'}),
stars: flags.string({char: 's', description: 'filter by stars (e.g. ">1000")'}),
sort: flags.enum({options: ['stars', 'forks', 'updated'], default: 'best-match'}),
json: flags.boolean({description: 'output JSON'}),
}
static args = [{name: 'query', required: true}]
async run() {
const {args, flags} = this.parse(Repositories)
const octokit = new Octokit({auth: this.config.token})
let q = args.query
if (flags.language) q += ` language:${flags.language}`
if (flags.stars) q += ` stars:${flags.stars}`
const {data} = await octokit.search.repos({q, sort: flags.sort})
if (flags.json) {
this.log(JSON.stringify(data, null, 2))
} else {
data.items.forEach(repo => {
this.log(`${repo.full_name} (⭐ ${repo.stargazers_count})`)
this.log(` ${repo.description}`)
})
}
}
}
This pattern scales beautifully. Adding a new search type means creating a new command file, not modifying routing logic. Oclif automatically generates help text from the flags definition, maintains consistent argument parsing, and handles error boundaries.
The authentication strategy deserves attention. Rather than forcing users to manage tokens manually, gh-search-cli used oclif's hook system to intercept commands and ensure credentials exist:
// hooks/init.ts
import {Hook} from '@oclif/config'
import {prompt} from 'inquirer'
import * as Conf from 'conf'
const hook: Hook<'init'> = async function (opts) {
const config = new Conf({projectName: 'gh-search-cli'})
if (!config.get('token')) {
const {token} = await prompt([{
type: 'password',
name: 'token',
message: 'Enter your GitHub personal access token:'
}])
config.set('token', token)
}
opts.config.token = config.get('token')
}
export default hook
This init hook runs before any command executes, ensuring the token exists and injecting it into the config object accessible to all commands. The token persists using the conf library, which handles cross-platform config file storage (AppData on Windows, ~/.config on Linux).
The tool also demonstrates smart query building. GitHub's search API uses a query string syntax where modifiers like language:typescript are appended to search terms. Rather than exposing this syntax directly to users, gh-search-cli translated friendly CLI flags into the proper query format. A command like gh-search repos "rest api" --language typescript --stars ">100" becomes the query string rest api language:typescript stars:>100 before hitting the API.
For output, the dual-mode approach (human-readable vs JSON) is particularly elegant. The --json flag transforms the tool from an end-user utility into a scriptable component in larger pipelines. You could pipe results to jq for filtering, feed them into spreadsheet imports, or chain multiple searches with bash scripting—all without parsing formatted text.
Gotcha
The elephant in the room: this tool is deprecated and you shouldn't use it. The maintainer explicitly states that GitHub's official CLI has superseded it. But beyond the deprecation, the architecture reveals inherent limitations worth understanding.
GitHub's Search API caps results at 1,000 items per query, regardless of pagination. If you're searching a popular language or topic with tens of thousands of results, you'll never see beyond result 1,000. The tool doesn't work around this—it couldn't without breaking API terms of service. For exhaustive data collection, you need GitHub's GraphQL API or database dumps, not the search endpoint. Additionally, rate limiting hits hard: 30 requests per minute for authenticated users. A script running multiple searches sequentially will quickly exhaust the quota, and gh-search-cli doesn't implement backoff or retry logic.
The authentication model, while convenient, stores tokens in plaintext in the config directory. On shared systems or compromised machines, this is a security risk. Modern CLIs like the official gh use OAuth device flow with token encryption, but gh-search-cli predates those patterns. There's also no token rotation, revocation handling, or permission scope validation—you might grant excessive permissions without realizing it.
Verdict
Use if: You're learning oclif and want a well-structured reference implementation showing command organization, hook usage, and API integration patterns. The codebase is small enough to read in an afternoon but complete enough to demonstrate production patterns. Also consider if you're on ancient GitHub Enterprise instances that don't support the official CLI (pre-2.20), though upgrading is the better solution. Skip if: You need actual GitHub search functionality—use the official gh CLI instead with commands like gh search repos, gh search issues, and gh search code. It's actively maintained, features OAuth authentication, supports GitHub's latest API capabilities, and integrates with other gh extensions. There's zero practical reason to choose gh-search-cli for production work unless you're literally unable to install the official tool.