npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cli-to-js

v0.0.5

Published

Turn any CLI tool into a Node.js API. Reverse commander.

Readme

cli-to-js

Warning: This project is very experimental. APIs may change without notice.

Turn any CLI into a JavaScript API, automatically.

Give it a binary name, it reads --help, and hands you back a fully typed object where subcommands are methods, flags are options, and everything just works.

import { convertCliToJs } from "cli-to-js";

const git = await convertCliToJs("git");
const claude = await convertCliToJs("claude");

// Get the files that changed in the last commit
const { stdout } = await git.diff({ nameOnly: true, _: ["HEAD~1"] });
const changedFiles = stdout.trim().split("\n");

// Ask Claude to review each changed file
for (const file of changedFiles) {
  const review = await claude({
    print: true,
    model: "sonnet",
    _: [`Review ${file} for bugs and suggest fixes`],
  });

  if (review.stdout.includes("no issues")) continue;
  console.log(`${file}:`, review.stdout);
}

Why this matters for agents.

Agents need to call CLI tools, but they work best with structured APIs, not raw shell strings.

cli-to-js lets an agent introspect any binary on the system, get a typed interface, and call it safely. $validate catches hallucinated flag names before spawning a process and returns did-you-mean suggestions the agent can self-correct from in a single retry.

$spawn returns a standard async iterator, so streaming and piping is just a for await loop.

Install

npm install cli-to-js

Quick start

convertCliToJs runs --help on the binary, parses the output into a schema, and returns a Proxy-based API where every subcommand is a method and every flag is an option.

import { convertCliToJs } from "cli-to-js";

const api = await convertCliToJs("my-tool");

// Subcommand as a method
const result = await api.build({ output: "dist", minify: true });
// → my-tool build --output dist --minify

console.log(result.stdout);
console.log(result.exitCode);

Here's how JS option keys map to CLI flags:

| JS option | CLI output | | ------------------------- | ------------------------- | | { verbose: true } | --verbose | | { verbose: false } | (omitted) | | { output: "file.txt" } | --output file.txt | | { dryRun: true } | --dry-run | | { v: true } | -v | | { include: ["a", "b"] } | --include a --include b | | { _: ["file.txt"] } | file.txt |

TypeScript

The API is fully typed out of the box. Every subcommand returns Promise<CommandResult>, and $schema, $parse, $spawn are all properly typed. No codegen needed.

For per-subcommand option types, pass a generic:

const git = await convertCliToJs<{
  commit: { message?: string; all?: boolean; amend?: boolean };
  push: { force?: boolean; setUpstream?: string };
}>("git");

git.commit({ message: "hello" }); // message autocompletes as string
git.push({ foobar: true }); // type error

Or generate a .d.ts from the parsed schema:

npx cli-to-js git --dts --subcommands -o git.d.ts

From a help text string

If you already have the help text, skip the binary lookup:

import { fromHelpText } from "cli-to-js";

const api = fromHelpText("my-tool", helpTextString);
await api.build({ watch: true });

Subcommand parsing

By default, only the root --help is parsed. Enable subcommands to also parse every subcommand's help text and populate its flags in the schema:

const git = await convertCliToJs("git", { subcommands: true });

// Schema now includes each subcommand's flags
const commitFlags = git.$schema.command.subcommands.find((s) => s.name === "commit")?.flags;

Or parse on demand:

const git = await convertCliToJs("git");

// Parse one subcommand lazily
const commitSchema = await git.$parse("commit");
console.log(commitSchema.flags);

// Parse all discovered subcommands
await git.$parse();

Handles commander-style aliases (init|setup, add|install). The primary name is used.

Validation

Validate options against the parsed schema before running a command. Returns an array of structured errors (empty means valid).

const git = await convertCliToJs("git", { subcommands: true });

const errors = git.$validate("commit", { massage: "fix typo" });
// => [{ kind: "unknown-flag", name: "massage", suggestion: "message",
//       message: 'Unknown flag "massage". Did you mean "message"?' }]

if (errors.length === 0) {
  await git.commit({ message: "fix typo" });
}

Checks for unknown flags (with Levenshtein-based suggestions), type mismatches (boolean vs value-taking), missing required positionals, and too many positionals.

For root command validation, pass options directly:

const errors = git.$validate({ unknownFlag: true });

For subcommand validation, the subcommand must be enriched first (via subcommands: true or $parse("name")).

Or use validateOptions directly with any ParsedCommand:

import { validateOptions } from "cli-to-js";

const errors = validateOptions(schema.command, { verbose: "wrong" });

Output parsing

Every command returns a CommandPromise with .text(), .lines(), and .json() for typed output:

const branch = await git.branch({ showCurrent: true }).text();
// "main"

const files = await git.diff({ nameOnly: true, _: ["HEAD~1"] }).lines();
// ["src/index.ts", "src/utils.ts"]

const packages = await npm.outdated({ json: true }).json<Record<string, { current: string }>>();
// { "lodash": { current: "4.17.20" }, ... }

You can also use the raw CommandResult directly:

const result = await git.status();
result.stdout; // raw string
result.exitCode; // number

Command strings

$command returns the shell string instead of executing:

git.$command.commit({ message: "fix", all: true });
// "git commit --message fix --all"

git.$command.push({ force: true });
// "git push --force"

Compose multiple commands into a runnable script with script():

import { script } from "cli-to-js";

const deploy = script(git.$command.commit({ message: "deploy", all: true }), git.$command.push());

deploy.run(); // executes sequentially, stops on failure
console.log(`${deploy}`); // "git commit --message deploy --all && git push"

Streaming

Callbacks

Get real-time output while still receiving the buffered result:

const result = await api.build(
  { watch: true },
  {
    onStdout: (data) => process.stdout.write(data),
    onStderr: (data) => process.stderr.write(data),
  },
);

Async iterator

spawnCommand and $spawn return a CommandProcess with raw streams and an async iterator that yields stdout lines:

const proc = api.$spawn.test({ _: ["--watch"] });

for await (const line of proc) {
  console.log(line);
}

console.log("exited with:", await proc.exitCode);

Or use spawnCommand directly:

import { spawnCommand } from "cli-to-js";

const proc = spawnCommand("npm", ["run", "dev"]);
for await (const line of proc) {
  if (line.includes("ready")) console.log("Server is up");
}

stdio inherit

Pass stdio through to the parent terminal for interactive CLIs:

await api.login({}, { stdio: "inherit" });

Per-call config

Every method accepts an optional second argument for execution config:

const controller = new AbortController();

await api.build(
  {},
  {
    cwd: "/my/project",
    env: { NODE_ENV: "production" },
    timeout: 60_000,
    signal: controller.signal,
  },
);

CLI

Generate a standalone JS/TS wrapper for any CLI tool:

npx cli-to-js git                          # TypeScript to stdout
npx cli-to-js git -o git.ts               # write to file
npx cli-to-js git --js -o git.js          # plain JavaScript
npx cli-to-js git --subcommands -o git.ts  # include per-subcommand flags
npx cli-to-js git --dts -o git.d.ts       # generate type declarations only
npx cli-to-js git --json                   # dump raw schema as JSON

The generated code is standalone. It embeds a tiny runtime (spawn + options-to-args) and has zero dependencies on cli-to-js. Drop it into any project and it just works.

API

convertCliToJs<T>(binary, options?)

Runs --help, parses the output, returns the API proxy. Accepts an optional generic T for per-subcommand option types.

| Option | Type | Default | Description | | ------------- | ------------ | ---------- | ------------------------------------------ | | helpFlag | string | "--help" | Flag to get help text | | timeout | number | 10000 | Timeout for help text fetch (ms) | | cwd | string | - | Default working directory for all commands | | env | ProcessEnv | - | Default environment for all commands | | subcommands | boolean | false | Parse all subcommand help texts eagerly |

fromHelpText<T>(binary, helpText, options?)

Same as convertCliToJs but from a static help text string. Accepts cwd and env options.

API proxy (CliApi<T>)

The returned proxy is both callable and has subcommand methods:

| Access | Description | | ---------------------------- | ---------------------------------------- | | api.sub({ flag: val }) | Run subcommand, returns CommandPromise | | api.sub(opts).text() | Run and get trimmed stdout | | api.sub(opts).lines() | Run and get stdout as string[] | | api.sub(opts).json<T>() | Run and parse stdout as JSON | | api("sub", { flag: val }) | Run subcommand by name | | api({ flag: val }) | Run root command | | api.$schema | Parsed CliSchema | | api.$validate(opts) | Validate options against root schema | | api.$validate("sub", opts) | Validate options against subcommand | | api.$command.sub(opts) | Get shell string without executing | | api.$spawn.sub(opts) | Spawn subcommand, get CommandProcess | | api.$parse("sub") | Lazily parse a subcommand's help text | | api.$parse() | Parse all subcommand help texts |

RunConfig

| Option | Type | Default | Description | | ---------- | ------------------------ | ---------- | ------------------------- | ---------- | | timeout | number | 30000 | Command timeout (ms) | | signal | AbortSignal | - | Abort signal | | cwd | string | - | Working directory | | env | ProcessEnv | - | Environment variables | | stdio | "pipe" | "inherit" | "pipe" | stdio mode | | onStdout | (data: string) => void | - | Real-time stdout callback | | onStderr | (data: string) => void | - | Real-time stderr callback |

Color output (FORCE_COLOR, CLICOLOR_FORCE) is auto-detected. It's enabled when streaming callbacks are provided and the parent process is connected to a TTY. To force it manually, pass env: { ...process.env, FORCE_COLOR: "1" }.

CommandResult

interface CommandResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

CommandProcess

Returned by spawnCommand and $spawn:

interface CommandProcess {
  stdin: Writable | null;
  stdout: Readable | null;
  stderr: Readable | null;
  pid: number | undefined;
  kill: (signal?) => boolean;
  exitCode: Promise<number>;
  [Symbol.asyncIterator](): AsyncIterableIterator<string>;
}

License

MIT © Million Software, Inc.