Documentation

Everything you need to build, publish, and promote CLI tools with shipcli.

Getting Started

shipcli is a CLI-as-a-Product toolkit. It helps you scaffold a new CLI project, add shareable output, publish to npm, build cross-platform binaries, and generate landing pages.

Node.js 24 or newer is required. Binary builds additionally require Bun.

Install

npm install -g @shipcli/cli

Create your first CLI

npx @shipcli/create my-cli
cd my-cli

# Run your CLI
npm start -- --help

# Publish to npm
shipcli publish --bump patch

Project Structure

A scaffolded project looks like this:

my-cli/
  package.json          # @shipcli/core + @shipcli/share deps
  tsconfig.json         # Strict TypeScript checks
  tsconfig.build.json   # Compile source to dist/
  shipcli.config.ts     # Typed shipcli defaults
  test/                 # Starter test
  .github/workflows/    # CI workflow
  src/
    cli.ts              # createCLI() entry point
    share-card.ts       # --share image template
    commands/
      index.ts          # Main command

Your CLI imports @shipcli/core at runtime — it's a framework, not a template. This means you get updates by bumping a dependency version.

Project Configuration

Keep repeatable defaults in shipcli.config.ts. Command-line options take precedence, so the same config works locally and in CI.

import { defineConfig } from "@shipcli/core";

export default defineConfig({
  name: "my-cli",
  description: "A useful command-line tool",
  build: {
    entrypoint: "./src/cli.ts",
    outDir: "dist",
    targets: ["macos-arm64", "linux-x64"],
  },
  publish: { bump: "patch", access: "public" },
  share: { enabled: true },
  landing: { outDir: "web" },
});

CLI Commands

shipcli init [name]

Scaffold a new CLI tool. Delegates to npx @shipcli/create.

shipcli init my-cli
shipcli init my-cli --description "A useful CLI" --no-install
shipcli init my-cli --no-git

shipcli publish

Version bump, git tag, and npm publish in one command.

shipcli publish
shipcli publish --bump minor
shipcli publish --dry-run
shipcli publish --skip-git
shipcli publish --access restricted
--bump <type>Version bump type: major, minor, patchpatch
--dry-runRun without actually publishingfalse
--skip-gitSkip git commit and tagfalse
--access <type>npm access levelpublic

A real publish requires a clean git working tree unless --skip-gitis supplied intentionally. Start with --dry-run.

shipcli build

Build cross-platform standalone binaries using Bun.

shipcli build
shipcli build --targets macos-arm64,linux-x64
shipcli build --out-dir bin
shipcli build --entrypoint src/cli.ts
--targets <list>Comma-separated: macos-arm64, macos-x64, linux-x64, linux-arm64, windows-x64all
--out-dir <dir>Output directorydist
--entrypoint <path>TypeScript or JavaScript CLI entry pointpackage metadata, bin, or src/cli.ts

shipcli homebrew

Generate a Homebrew formula from your package.json.

shipcli homebrew
shipcli homebrew --repo owner/repo --output formula.rb

shipcli changelog

Generate a CHANGELOG.md from git commits.

shipcli changelog

shipcli landing init

Scaffold a Next.js landing page with terminal demo, install instructions, and feature showcase.

shipcli landing init
shipcli landing init --name my-cli --description "My awesome CLI"
shipcli landing init --out-dir site
shipcli landing init --force
--name <name>Tool namepackage.json name
--description <desc>Tool descriptionpackage.json description
--out-dir <path>Generated website directoryweb
--forceOverwrite files in an existing web directoryfalse

createCLI

Create a CLI application. Wraps Commander.js with shipcli conventions.

import { createCLI } from "@shipcli/core/cli";

const cli = createCLI({
  name: "my-cli",
  packageName: "my-cli", // npm package used by the update check
  description: "My awesome CLI tool",
  version: "0.1.0",
});

cli.command("run <target>")
  .option("--verbose", "Verbose output")
  .action((target, options) => {
    // your logic
  });

await cli.run();

Automatically adds:

  • --json global flag
  • Auto update check (cached 24h, skipped in CI)
  • Global error handler for uncaught exceptions

Output

Terminal output utilities. All status output goes to stderr so stdout stays clean for piping.

import { phase, status, success, error, fatal, hint } from "@shipcli/core/output";

phase("Analyzing repository");     // ==> Analyzing repository
status("Fetching commits...");     //     Fetching commits...
success("Analysis complete");      // --> Analysis complete
error("Not found", "Check URL");   // Error: Not found
fatal("Cannot continue");          // Error + process.exit(1)
hint("Tip", "Use --verbose");      // Tip: Use --verbose

table(headers, rows)

ANSI-aware table with automatic column padding.

import { table, fmt } from "@shipcli/core/output";

console.log(table(
  ["Name", "Score", "Status"],
  [
    ["react", fmt.score(95), "alive"],
    ["atom", fmt.score(0), "dead"],
  ]
));

box(title, lines)

Unicode box drawing for highlighted output.

import { box } from "@shipcli/core/output";

console.log(box("Results", [
  "Total: 42",
  "Passed: 40",
  "Failed: 2",
]));

progressBar(current, total)

import { progressBar } from "@shipcli/core/output";

console.log(progressBar(7, 10));
// █████████████████████░░░░░░░░░ 70%

Formatting

Color and style helpers via fmt.

import { fmt } from "@shipcli/core/output";

fmt.app("my-cli")      // cyan
fmt.cmd("npm install")  // bold cyan
fmt.key("name")         // green
fmt.val("42")           // yellow
fmt.dim("optional")     // dimmed
fmt.bold("important")   // bold
fmt.url("https://...")   // underline cyan
fmt.score(85)           // green (80+), yellow (50-79), red (<50)
fmt.grade("A")          // green (A/B), yellow (C), red (D/F)

Config

Persistent configuration stored at ~/.toolname/config.json. Supports dot-notation and migrations.

import { Config } from "@shipcli/core/config";

const config = new Config("my-cli").load();

config.set("github.token", "ghp_...");
config.get("github.token");  // "ghp_..."
config.has("github.token");  // true
config.delete("github.token");
config.save();

Migrations

Transform config between versions:

const config = new Config("my-cli", {
  migrations: [
    (data) => {
      // Rename old key
      if (data.apiKey) {
        data.token = data.apiKey;
        delete data.apiKey;
      }
      return data;
    },
  ],
}).load();

Spinner

Lightweight spinner for async operations. Wraps nanospinner.

import { spinner } from "@shipcli/core/spinner";

const s = spinner("Loading...").start();

// On success:
s.success({ text: "Done!" });

// On error:
s.error({ text: "Failed" });

Share

Generate shareable OG images (1200x630 PNG) for social media from your CLI output.

import { share, type SatoriElement } from "@shipcli/share";

interface ReportData {
  title: string;
}

// Template uses Satori h() format
function myCard(data: ReportData): SatoriElement {
  return {
    type: "div",
    props: {
      style: {
        display: "flex",
        width: "100%",
        height: "100%",
        background: "#0f0f0f",
        color: "#e5e5e5",
        padding: "48px",
      },
      children: {
        type: "div",
        props: { children: data.title }
      }
    }
  };
}

const reportData: ReportData = { title: "Repository health: good" };

await share(myCard, reportData, {
  toolName: "my-cli",
  filename: "output.png",
});

Satori renders the element tree to SVG, then resvg converts to PNG. Inter is provided by a local package dependency, so rendering works offline.

Add --share to your CLI command to let users generate shareable cards:

cli.command("analyze <target>")
  .option("--share", "Generate shareable image")
  .action(async (target, options) => {
    const result = analyze(target);
    console.log(renderOutput(result));

    if (options.share) {
      const { share } = await import("@shipcli/share");
      await share(myCard, result, { toolName: "my-cli", filename: "result.png" });
    }
  });

Build

Build and distribution pipeline. Used by the shipcli CLI commands.

npm Publish

import { publish } from "@shipcli/build/npm-publish";

publish({
  cwd: process.cwd(),
  bump: "minor",       // major | minor | patch
  dryRun: false,
  skipGit: false,
  access: "public",
});

Binary Build

Compile standalone executables using bun build --compile.

import { build } from "@shipcli/build/binary";

build({
  cwd: process.cwd(),
  outDir: "dist",
  targets: [
    { name: "macos-arm64", bun: "bun-darwin-arm64", label: "macOS (Apple Silicon)" },
    { name: "linux-x64", bun: "bun-linux-x64", label: "Linux (x64)" },
  ],
});

Homebrew Formula

import { generateFormula } from "@shipcli/build/homebrew";

generateFormula({
  cwd: process.cwd(),
  repo: "owner/repo",
  output: "formula.rb",
});

Changelog

import { generateChangelog } from "@shipcli/build/changelog";

generateChangelog({ cwd: process.cwd() });
// → Writes CHANGELOG.md grouped by Features / Fixes / Other

Landing

Scaffold a Next.js landing page for your CLI tool.

shipcli landing init

Generates a web/ directory with:

  • Animated terminal demo component
  • Click-to-copy installation tabs for npm and npx
  • Feature showcase grid
  • "Built with shipcli" footer (flywheel)
  • Responsive navigation for workflow, output, and installation sections
  • Tailwind CSS + dark mode
  • SEO meta tags and Open Graph

Deploy to Vercel with cd web && vercel.

Packages

@shipcli/coreCLI framework: output, config, commands, spinner
@shipcli/shareShareable OG image generation
@shipcli/buildnpm publish, binary builds, homebrew formulas
@shipcli/landingNext.js landing page scaffolding
@shipcli/createnpx @shipcli/create scaffolding
@shipcli/clishipcli CLI orchestrator

Packages are authored in strict TypeScript and published as ESM JavaScript with TypeScript declarations and source maps.

Browse the packages on npm or read the release changelog.