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

just-bash-util

v0.1.10

Published

CLI command framework, config file discovery, and path utilities for just-bash

Readme

just-bash-util

npm GitHub

CLI command framework, config file discovery, and path utilities for just-bash.

Install

npm install just-bash-util just-bash

Modules

just-bash-util/command — CLI framework

Type-safe command trees with fluent builders, inherited options, auto-generated help, and typo suggestions.

import { Bash } from "just-bash";
import { command, o, f, a } from "just-bash-util/command";

const cli = command("mycli", {
  description: "My CLI tool",
});

const serve = cli.command("serve", {
  description: "Start the dev server",
  options: {
    port: o.number().default(3000).alias("p").describe("Port to listen on"),
    host: o.string().describe("Host to bind to"),
    open: f().alias("o").describe("Open browser"),
  },
  args: [a.string().name("entry").describe("Entry file")],
  handler: (args, ctx) => {
    // args is fully typed: { port: number; host: string | undefined; open: boolean; entry: string }
    return { stdout: `Listening on :${args.port}`, stderr: "", exitCode: 0 };
  },
});

const bash = new Bash({ customCommands: [cli.toCommand()] });
await bash.exec("mycli serve app.ts -p 8080");

Commands can also be executed directly without just-bash:

import type { Infer } from "just-bash-util/command";

// Extract handler args type externally (like z.infer)
type ServeArgs = Infer<typeof serve>;

// Execute from CLI tokens
await cli.execute(["serve", "app.ts", "-p", "8080"], ctx);

// Or invoke programmatically with typed args
await serve.invoke({ port: 8080, entry: "app.ts" }, ctx);

Features:

  • Composable command trees — attach pre-existing Command instances with .command(child)
  • Subcommand nesting with automatic option inheritance
  • omitInherited to exclude parent options from specific subcommands
  • --help / -h auto-generated at every level
  • --no-<flag> negation, -abc combined short flags, --key=value syntax, counted flags (-vvv → 3)
  • -- end-of-options separator (remaining tokens go to meta.passthrough without consuming positional args)
  • Environment variable fallbacks for options
  • Levenshtein-based "did you mean?" suggestions for typos
  • transformArgs callback to rewrite tokens before parsing — support non-standard shorthand syntax like -5-n 5
  • defaultSubcommand to name a child as the fallback for bare invocations — supports "dual mode" commands like git stash / git remote
  • Automatic error handling — thrown errors in handlers are caught and returned as clean ExecResult with exitCode: 1

Options and flags

Option keys are written in camelCase and automatically converted to kebab-case for the CLI:

options: {
  allowEmpty: f(),           // CLI: --allow-empty    handler: args.allowEmpty
  dryRun: f().alias("n"),    // CLI: --dry-run / -n   handler: args.dryRun
  message: o.string().alias("m"), // CLI: --message / -m  handler: args.message
}

Flags support counting mode via .count(). Repeated occurrences produce a number instead of a boolean — useful for verbosity levels (-v, -vv, -vvv):

options: {
  verbose: f().alias("v").count().describe("Verbosity level"),
}
// -v → 1, -vv → 2, -vvv → 3, absent → 0
// handler receives args.verbose as number

Short flags (single-dash, single-character) require .alias(). A single-character key like b: f() creates the long flag --b, not the short flag -b. To get -b, use a descriptive key with an alias:

// ✗ b: f()                    → creates --b (long flag), not -b
// ✓ branch: f().alias("b")   → creates --branch and -b

Positional args

Args are required by default. Use .optional() for optional args, and .variadic() to collect remaining positionals into an array. Chain .optional().variadic() for zero-or-more:

args: [
  a.string().name("entry"),                         // required single arg
  a.string().name("file").optional(),               // optional single arg
  a.string().name("files").variadic(),              // required: one or more
  a.string().name("paths").optional().variadic(),   // optional: zero or more → string[]
]

The -- separator

The -- token stops all parsing. Tokens after -- go exclusively into meta.passthrough — they are not consumed as positional args or parsed as flags. Positional args must appear before --:

// mycli checkout main -- README.md
handler: (args, ctx, meta) => {
  args.target;          // "main" (positional arg before --)
  meta.passthrough;     // ["README.md"] (tokens after --)
}

// mycli checkout -- README.md
handler: (args, ctx, meta) => {
  args.target;          // undefined (no positional before --)
  meta.passthrough;     // ["README.md"]
}

Token rewriting with transformArgs

Commands can define a transformArgs callback to rewrite the raw token array before it reaches the parser. This is useful for supporting non-standard shorthand syntax that the parser wouldn't otherwise understand:

cli.command("log", {
  description: "Show commit log",
  transformArgs: (tokens) =>
    tokens.map((t) => (/^-(\d+)$/.test(t) ? `-n${t.slice(1)}` : t)),
  options: {
    maxCount: o.number().alias("n").describe("Limit output to n commits"),
  },
  handler: (args) => {
    // git log -5  →  tokens rewritten to -n5  →  args.maxCount === 5
    return { stdout: `showing ${args.maxCount} commits`, stderr: "", exitCode: 0 };
  },
});

The callback receives a mutable copy of the tokens and returns the rewritten array. It runs only in execute() (token-based invocation) — invoke() takes typed args directly, so there's nothing to transform.

Default subcommand

Commands with a "dual mode" pattern (e.g. git stash implicitly pushes, git remote lists remotes) can name a child as the fallback for bare invocations. Explicit subcommands still route normally, and typos still get "did you mean?" suggestions:

const stash = cli.command("stash", {
  description: "Stash changes",
  defaultSubcommand: "push",  // bare "stash" or "stash -m wip" → routes to push
});
stash.command("push", { /* options, handler */ });
stash.command("pop", { /* args, handler */ });

defaultSubcommand and handler are mutually exclusive.

Attaching pre-existing commands

.command() also accepts an existing Command instance. This lets you define commands independently and attach them later — useful for splitting a CLI across files, building reusable command modules, or composing subtrees from libraries:

import { command, o, f, a } from "just-bash-util/command";

// Defined independently (e.g. in another file or package)
const lint = command("lint", {
  description: "Run linters",
  options: { fix: f().describe("Auto-fix problems") },
  handler: (args) => ({ stdout: args.fix ? "fixed" : "checked", stderr: "", exitCode: 0 }),
});

// Attach to the main CLI
const cli = command("mycli", { description: "My CLI" });
cli.command(lint);
// mycli lint --fix  →  "fixed"

Subtrees with children are attached as a unit — the entire tree comes along:

const db = command("db", { description: "Database operations" });
db.command("migrate", { description: "Run migrations", handler: /* ... */ });
db.command("seed", { description: "Seed data", handler: /* ... */ });

cli.command(db);
// mycli db migrate
// mycli db seed

If a command is already attached to another parent, it is automatically detached from the old parent first.

just-bash-util/config — Config file discovery

Cosmiconfig-style config search that walks up the directory tree, trying conventional filenames at each level. Comments and trailing commas are supported out of the box.

import { searchConfig } from "just-bash-util/config";

// Walks up from cwd trying: .myapprc, .myapprc.json, myapp.config.json
const result = await searchConfig(ctx, { name: "myapp" });
if (result) {
  result.config; // parsed config object
  result.filepath; // absolute path to the file that matched
  result.isEmpty; // true if config is null/undefined/empty object
}

// Find nearest package.json and return its full contents
const pkg = await searchConfig(ctx, { name: "package", searchPlaces: ["package.json"] });

// Extract a tool-specific property from package.json
const result2 = await searchConfig(ctx, {
  name: "myapp",
  searchPlaces: ["package.json", ".myapprc", ".myapprc.json"],
  packageJsonProp: "myapp",
});

Layered / cascading configs — pass merge: true to collect configs from every directory level and deep-merge them (closest wins):

const result = await searchConfig(ctx, { name: "myapp", merge: true });
// e.g. /project/.myapprc.json  → { indent: 2, rules: { semi: "error" } }
//      /.myapprc.json           → { indent: 4, color: true, rules: { semi: "warn" } }
// result.config                → { indent: 2, color: true, rules: { semi: "error" } }

Use stopWhen for ESLint-style root: true cascading stops:

const result = await searchConfig(ctx, {
  name: "myapp",
  merge: true,
  stopWhen: (cfg) => cfg.root === true,
});

Also exports loadConfig for loading a known file path directly (e.g. when the user passes --config ./path), and findUp for locating files by name up the directory tree.

just-bash-util/path — Path utilities

Pure POSIX path operations with no Node.js dependency.

import {
  join,
  resolve,
  dirname,
  basename,
  extname,
  relative,
  parse,
  normalize,
  parsePackageSpecifier,
} from "just-bash-util/path";

join("src", "utils", "index.ts"); // "src/utils/index.ts"
dirname("/project/src/index.ts"); // "/project/src"
basename("src/index.ts", ".ts"); // "index"
relative("/a/b/c", "/a/d"); // "../../d"

parsePackageSpecifier("@vue/shared/dist"); // { name: "@vue/shared", subpath: "./dist" }
parsePackageSpecifier("lodash/merge"); // { name: "lodash", subpath: "./merge" }

join vs resolvejoin concatenates segments and normalizes; an absolute second argument is kept as-is (appended, not replacing). resolve processes right-to-left and stops at the first absolute path, like Node's path.resolve (but without prepending cwd when no absolute segment exists):

join("/repo", "/file.txt");    // "/repo/file.txt" — concatenates with /
resolve("/repo", "/file.txt"); // "/file.txt"      — absolute segment wins
resolve("/repo", "file.txt");  // "/repo/file.txt"
resolve("a", "b");             // "a/b"            — stays relative (no cwd)

Peer dependencies

Requires just-bash ^2.9.6 — provides the CommandContext and ExecResult types used throughout.

Status

This project is in early development. Test coverage exists but is limited — expect gaps, especially around edge cases. Contributions and bug reports are welcome.

Disclaimer

This project is not affiliated with, endorsed by, or associated with Vercel or the just-bash project.

License

MIT