Skip to content

kiyo-e/hono-cli-adapter

Repository files navigation

hono-cli-adapter

Tiny library to adapt CLI arguments into a POST request for a Hono app — without touching stdout. It lets you build a small custom CLI around an existing Hono application, while keeping this package a pure, testable library.

Status: minimal but usable. ESM-only.

Why

  • Keep your CLI thin: printing, JSON formatting, and flags live in your own small bin.
  • Library has no side effects: never writes to stdout/stderr.
  • Strong constraints: POST-only philosophy for predictable calls from the shell.
  • Reserved flags: easily exclude your CLI-only flags from HTTP query strings.
  • Default env: process.env is merged by default.
  • Env merging: combine process.env + options.env + repeated --env KEY=VALUE flags (later wins).
  • Body tokens: pass -- key=value pairs to send JSON payloads.

Install

npm install hono-cli-adapter

Peer/runtime expectations:

  • Node 18+ (global WHATWG fetch)
  • Hono app instance with app.fetch(req, env) available

Quick Start

Create a tiny CLI that wraps your existing Hono app. Your CLI controls output and flags; the adapter only builds the request and calls app.fetch.

Default (recommended): one‑liner bin that prints and exits for you.

#!/usr/bin/env node
// my-cli.ts (ESM)
import { cli } from 'hono-cli-adapter'
import { app } from './dist/app.js' // your Hono app export

// Pass a process-like object (for argv/stdout/exit) or omit.
await cli(app) // or simply: await cli(app)

Pure/no‑stdout variant when you want to fully control printing and exiting yourself:

#!/usr/bin/env node
import { runCli } from 'hono-cli-adapter'
import { app } from './dist/app.js'

const { code, lines } = await runCli(app, process) // or runCli(app)
for (const l of lines) console.log(l)
process.exit(code)

API

Exported from src/index.ts:

type BeforeFetchFn = (req: Request, argv: minimist.ParsedArgs) => Promise<Request | void> | Request | void
type OpenApiSpec = Record<string, unknown>
type OpenApiProvider = OpenApiSpec | (() => OpenApiSpec | Promise<OpenApiSpec>)

type AdapterOptions = {
  base?: string
  env?: Record<string, unknown>
  openapi?: OpenApiProvider
  reservedKeys?: string[]
  beforeFetch?: BeforeFetchFn | Record<string, BeforeFetchFn>
}

// High-level (recommended for CLI bins)
type RunCliResult = { code: number; lines: string[]; req?: Request; res?: Response }
type ProcLike = { argv?: any[]; stdout?: { write?: (s: string) => unknown }; exit?: (code?: number) => unknown }
function cli(app: any, argvOrProcess?: string[] | ProcLike | AdapterOptions, options?: AdapterOptions): Promise<number>
function runCli(app: any, argvOrProcess?: string[] | ProcLike | AdapterOptions, options?: AdapterOptions): Promise<RunCliResult>

// Mid-level
function adaptAndFetch(
  app: any,
  argvRaw?: string[] /* defaults to process.argv.slice(2) */,
  options?: AdapterOptions
): Promise<{ req: Request; res: Response }>

// Request building
function buildUrlFromArgv(argv: minimist.ParsedArgs, options?: AdapterOptions): URL
function buildRequestFromArgv(argv: minimist.ParsedArgs, options?: AdapterOptions): Request
function parseEnvFlags(envFlags: string | string[] | undefined): Record<string, string>
function parseBodyTokens(tokens: string | string[] | undefined): Record<string, string>
function commandFromArgv(argv: minimist.ParsedArgs, options?: AdapterOptions): string | undefined

// Route helpers
function listPostRoutes(app: any): string[]
function routePathToCommandSegments(routePath: string): string[]
function buildCommandExample(routePath: string, cmdBase: string): string
function buildCommandExamples(routes: string[], cmdBase: string): string[]
function detectCommandBase(argv0?: string, argv1?: string): string
function listRoutesWithExamples(app: any, cmdBase?: string): { routes: string[]; examples: string[] }
function listCommandExamples(app: any, cmdBase?: string): string[]
function listHelpLinesFromOpenApi(openapi: any, cmdBase?: string): string[]

Key behaviors:

  • Reserved keys default to ['_', '--', 'base', 'env'] and are removed from the query string. Add your own via options.reservedKeys.
  • runCli/cli also reserve global flags automatically: json, list, help, base, env.
  • If options.openapi is provided, --help uses it to include per-parameter details (type, required/optional, description, default) and --list includes --param examples from the spec.
  • Environment precedence when calling app.fetch(req, env):
    1. process.env (base, included by default)
    2. options.env (overrides base)
    3. --env KEY=VALUE flags (highest precedence)
  • Tokens after -- like key=value become a JSON body.
  • buildRequestFromArgv and adaptAndFetch use POST only.
  • listPostRoutes is best-effort and relies on Hono’s internal shape. It enumerates POST paths only.

Usage Patterns

Two ways to integrate (besides the default cli above):

  1. High-level: let the adapter parse argvRaw for the common flags --base, --env.
const { res } = await adaptAndFetch(app, process.argv.slice(2), { reservedKeys: ['json'] })
  1. Lower-level: parse your own flags, then build the request yourself.
import minimist from 'minimist'
import { buildRequestFromArgv, parseEnvFlags, listRoutesWithExamples, detectCommandBase } from 'hono-cli-adapter'

const argv = minimist(process.argv.slice(2), { string: ['env'], '--': true })
const req = buildRequestFromArgv(argv, { base: '/v1', reservedKeys: ['json'] })
const res = await app.fetch(req, { ...parseEnvFlags(argv.env) })

// Show routes plus runnable examples (you print; library does not):
const { routes, examples } = listRoutesWithExamples(app, detectCommandBase())
console.log('POST routes:')
for (const p of routes) console.log('  POST ' + p)
console.log('\nCommand examples:')
for (const ex of examples) console.log('  ' + ex)

OpenAPI-powered help

If you have an OpenAPI spec (e.g., from hono-openapi + describeRoute), pass it to runCli to enrich --help:

const openapi = await getOpenApiSpecSomehow()
const { lines } = await runCli(app, process, { openapi })
for (const l of lines) console.log(l)

Hooks and command detection

Tweak the outgoing Request before sending:

import fs from 'node:fs/promises'
import minimist from 'minimist'
import { adaptAndFetch, commandFromArgv } from 'hono-cli-adapter'

const argv = minimist(process.argv.slice(2), { '--': true })
const cmd = commandFromArgv(argv) // safe: no direct argv._ access

await adaptAndFetch(app, process.argv.slice(2), {
  beforeFetch: {
    upload: async (req, argv) => {
      if (argv.file) {
        const buf = await fs.readFile(argv.file)
        const headers = new Headers(req.headers)
        headers.set('content-type', 'application/octet-stream')
        return new Request(req, { body: buf, headers })
      }
    }
  }
})

Example Projects

This repo ships runnable examples:

  • example/basic/ — minimal Hono app + CLI (example/basic/README.md)
  • example/slaq/ — Slack-focused CLI example built on this adapter

Quick taste:

npm run build
node example/basic/cli.mjs --list   # prints runnable command examples only
node example/basic/cli.mjs --help   # adds a Flags section
node example/basic/cli.mjs hello Taro         # -> "Hello, Taro!"
node example/basic/cli.mjs env API_KEY --env API_KEY=secret-123   # -> "API_KEY=secret-123"

Single-file binary via Bun:

npm run build:example:bin
./example/basic/bin/hono-example --list
./example/basic/bin/hono-example --help   # shows flags

Design Notes

  • POST-only now; adding other methods can be an additive future feature when/if needed.
  • The adapter never writes to stdout — your CLI formats results (plain text, JSON, etc.).
  • runCli automatically excludes the global CLI flags from the query. Add more via reservedKeys when needed.

Compatibility

  • ESM only. If you need CJS, transpile on your side.
  • Node 18+ recommended. If you don’t have global fetch, polyfill (e.g. undici).

Caveats

  • listPostRoutes depends on Hono internals and may break if they change. Consider maintaining your own list if you need strong guarantees.

Development

npm i
npm run build

That’s it — minimal surface, focused on the adapter behavior.

License

MIT © K.Endo

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors