cli-to-js: Turn Any CLI Into a JavaScript API
SkillDev toolsTurn any CLI tool into a fully typed JavaScript/TypeScript API using cli-to-js
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the cli-to-js: Turn Any CLI Into a JavaScript API skill
What this skill tells your AI
The instructions your AI receives, as published by reason-machines/trending-skills in skills/cli-to-js-api-wrapper/SKILL.md and read by ahel’s review.
Skill by ara.so — Daily 2026 Skills collection.
cli-to-js reads a binary's --help output, parses it into a schema, and returns a fully typed Proxy-based API where subcommands are methods and flags are options. Designed for agent workflows where structured APIs are safer than raw shell strings.
Install
npm install cli-to-js
Core Concepts
convertCliToJs(binary)— runs--help, parses output, returns typed API proxyfromHelpText(binary, text)— same but from a static help string- Every subcommand becomes a method:
api.subcommand({ flag: value }) - Positional args use the
_key:api.command({ _: ["file.txt"] }) - camelCase keys auto-convert to kebab-case flags:
{ dryRun: true }→--dry-run
Flag → CLI Mapping
| 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 |
Basic Usage
import { convertCliToJs } from "cli-to-js";
// Wrap any installed binary
const git = await convertCliToJs("git");
const npm = await convertCliToJs("npm");
// Call subcommands as methods
const result = await git.status();
console.log(result.stdout);
console.log(result.exitCode);
// Pass flags as options
await git.commit({ message: "fix: update logic", all: true });
// → git commit --message "fix: update logic" --all
// Positional arguments via _
const { stdout } = await git.diff({ nameOnly: true, _: ["HEAD~1"] });
const changedFiles = stdout.trim().split("\n");
TypeScript Generics for Full Typing
import { convertCliToJs } from "cli-to-js";
const git = await convertCliToJs<{
commit: { message?: string; all?: boolean; amend?: boolean };
push: { force?: boolean; setUpstream?: string };
diff: { nameOnly?: boolean; stat?: boolean; _?: string[] };
}>("git");
// Fully autocompleted and type-checked
await git.commit({ message: "hello", all: true });
await git.push({ force: true });
// Type error — foobar doesn't exist
await git.push({ foobar: true }); // ❌ compile error
Output Helpers
const git = await convertCliToJs("git");
// .text() — trimmed stdout string
const branch = await git.branch({ showCurrent: true }).text();
// "main"
// .lines() — stdout split into array
const files = await git.diff({ nameOnly: true, _: ["HEAD~1"] }).lines();
// ["src/index.ts", "src/utils.ts"]
// .json<T>() — parse stdout as JSON
const packages = await npm.outdated({ json: true }).json<Record<string, { current: string }>>();
// { "lodash": { current: "4.17.20" }, ... }
// Raw result
const result = await git.log({ oneline: true, n: "5" });
result.stdout; // string
result.stderr; // string
result.exitCode; // number
Validation (Critical for Agent Use)
Validate options before spawning — catches hallucinated flag names with did-you-mean suggestions:
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"?' }]
// Always validate before running in agent workflows
if (errors.length === 0) {
await git.commit({ message: "fix typo" });
} else {
// Use errors[0].suggestion to self-correct
console.log("Suggestion:", errors[0].suggestion);
}
// Validate root command options
const rootErrors = git.$validate({ unknownFlag: true });
Subcommand Parsing
// Eager: parse all subcommands up front
const git = await convertCliToJs("git", { subcommands: true });
const commitFlags = git.$schema.command.subcommands
.find((s) => s.name === "commit")?.flags;
// Lazy: parse one subcommand on demand
const git2 = await convertCliToJs("git");
const commitSchema = await git2.$parse("commit");
console.log(commitSchema.flags);
// Parse all subcommands lazily
await git2.$parse();
Streaming Output
const api = await convertCliToJs("my-tool");
// Callbacks: real-time output + buffered result
const result = await api.build(
{ watch: false },
{
onStdout: (data) => process.stdout.write(data),
onStderr: (data) => process.stderr.write(data),
}
);
// Async iterator via $spawn
const proc = api.$spawn.test({ _: ["--watch"] });
for await (const line of proc) {
console.log(line);
if (line.includes("failed")) proc.kill();
}
console.log("Exit code:", await proc.exitCode);
// Direct spawnCommand
import { spawnCommand } from "cli-to-js";
const dev = spawnCommand("npm", ["run", "dev"]);
for await (const line of dev) {
if (line.includes("ready")) {
console.log("Server started");
break;
}
}
Per-Call Execution Config
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await api.build(
{ minify: true },
{
cwd: "/my/project",
env: { ...process.env, NODE_ENV: "production" },
timeout: 60_000,
signal: controller.signal,
stdio: "inherit", // pass through to terminal for interactive CLIs
}
);
Command Strings (Without Executing)
const git = await convertCliToJs("git");
// Get the shell string instead of running it
git.$command.commit({ message: "deploy", all: true });
// "git commit --message deploy --all"
// Compose into a script
import { script } from "cli-to-js";
const deploy = script(
git.$command.commit({ message: "deploy", all: true }),
git.$command.push({ force: false })
);
console.log(`${deploy}`);
// "git commit --message deploy --all && git push"
deploy.run(); // executes sequentially, stops on failure
From Help Text String
import { fromHelpText } from "cli-to-js";
const helpText = `
Usage: mytool [options]
--output <dir> Output directory
--minify Minify output
--watch Watch for changes
`;
const api = fromHelpText("mytool", helpText, { cwd: "/project" });
await api({ output: "dist", minify: true });
CLI Code Generation
# TypeScript wrapper to stdout
npx cli-to-js git
# Write to file
npx cli-to-js git -o git.ts
# Plain JavaScript
npx cli-to-js git --js -o git.js
# Include per-subcommand flags
npx cli-to-js git --subcommands -o git.ts
# Type declarations only
npx cli-to-js git --dts -o git.d.ts
# Dump raw schema as JSON
npx cli-to-js git --json
Generated files are standalone with zero runtime dependencies on cli-to-js.
Agent Workflow Pattern
import { convertCliToJs } from "cli-to-js";
async function agentTask() {
const git = await convertCliToJs("git", { subcommands: true });
const claude = await convertCliToJs("claude");
// Get changed files
const files = await git.diff({ nameOnly: true, _: ["HEAD~1"] }).lines();
for (const file of files) {
// Validate before calling
const errors = claude.$validate({ print: true, model: "sonnet" });
if (errors.length > 0) {
console.error("Invalid flags:", errors);
continue;
}
const review = await claude({
print: true,
model: "sonnet",
_: [`Review ${file} for bugs`],
});
if (!review.stdout.includes("no issues")) {
console.log(`Issues in ${file}:`, review.stdout);
}
}
}
Schema Inspection
const git = await convertCliToJs("git", { subcommands: true });
// Full parsed schema
console.log(git.$schema);
// { binary: "git", command: { name: "git", flags: [...], subcommands: [...] } }
// List subcommands
git.$schema.command.subcommands.forEach((s) => {
console.log(s.name, s.flags.map((f) => f.name));
});
Common Patterns
Wrap with default config:
const docker = await convertCliToJs("docker", {
cwd: process.env.PROJECT_DIR,
env: { ...process.env, DOCKER_BUILDKIT: "1" },
timeout: 120_000,
});
Root command call (no subcommand):
const result = await api({ version: true });
// or
const result = await api("subcommand", { flag: true });
Interactive CLI passthrough:
const gh = await convertCliToJs("gh");
await gh.auth({ login: true }, { stdio: "inherit" });
Troubleshooting
Binary not found: Ensure the binary is in PATH. Test with which <binary> in terminal.
Help text not parsed correctly: Use fromHelpText with manually fetched help, or set helpFlag to the correct flag (-h, help, etc.):
const api = await convertCliToJs("mytool", { helpFlag: "-h" });
Subcommand flags missing: Subcommand flags only populate when subcommands: true is set or $parse("sub") is called:
await git.$parse("commit"); // now git.$validate("commit", opts) works
Type errors on dynamic subcommands: Pass a generic type to convertCliToJs<T> for per-subcommand option types.
Timeout on slow help output: Increase the help fetch timeout:
const api = await convertCliToJs("slow-tool", { timeout: 30_000 });
Signals
- GitHub stars
- 78
- Forks
- 13
- Last commit
- Jul 2026
ahel recommends instead
Advanced
- Catalog kind
- skill
- Gateway key
cli-to-js-api-wrapper-reason-machines- Source
- github.com/reason-machines/trending-skills