Skip to content

A zero-dependency CLI framework with monadic principles for creating type-safe, composable command-line interfaces with minimal boilerplate

License

Notifications You must be signed in to change notification settings

supitsdu/climonad.js

Repository files navigation

Climonad Banner

NPM License


A no-nonsense, zero-dependency CLI framework for developers who prefer their tools lean, typed, and composable. Climonad won’t hold your hand—but it won’t get in your way, either.

Most CLI frameworks overpromise and underdeliver. Climonad does neither. It gives you a clean, type-safe foundation to build powerful CLIs your way—without the bloat or boilerplate.

Functional where it counts. Testable by default. Easy to start, endlessly flexible. No fluff. No footguns.


🧠 Why?

Built for developers who want complete control over their CLI tools. It avoids magic, embraces type safety, and scales from tiny scripts to full-featured CLIs without dragging opinions along.


⚙️ Setup In Four Steps

Get rolling fast without hidden complexity. Define what you need, skip what you don’t.

1. Define the CLI App

// cli.ts
import { createCLI } from "climonad"

export const cli = createCLI({
  name: "example-cli",
  description: "An example CLI application",
})

2. Define Your Flags

// flags.ts
import { bool } from "climonad"

export const helpFlag = bool({
  name: "help",
  description: "Display help information",
  default: false,
  aliases: ["h"],
})

export const verboseFlag = bool({
  name: "verbose",
  description: "Verbose mode",
  default: false,
  aliases: ["v"],
})

3. Register Commands

// command.ts
import { cli } from "./cli"

export const runCmd = cli.cmd({
  name: "run",
  description: "Run the application",
  action: async (args) => {
    console.log("Running the application with args:", args)
  },
})

export const testCmd = runCmd.cmd({
  name: "test",
  description: "Test the application",
  action: async (args) => {
    console.log("Testing the application with args:", args)
  },
})

4. Wire It Up

// index.ts
import { cli } from "./cli"
import "./command"
import { helpFlag, verboseFlag } from "./flags"

cli.use(helpFlag, verboseFlag)

cli.run(process.argv.slice(2)).catch((error) => {
  console.error("Error running CLI:", error)
  process.exit(1)
})

🧾 You Control Help Output

Climonad doesn’t guess what your help experience should look like. If users ask for help and you haven’t defined how to show it, that’s on purpose. You’re in charge of what’s printed.

1. Create a Help Reporter

// help.ts
import { CLIHelpConstructor } from "climonad"

export const helpReporter = ({ commands, flags, root }: CLIHelpConstructor) => {
  console.log(`\n${root.name} - ${root.description}\n`)

  if (commands.length > 0) {
    console.log("Commands:")
    for (const cmd of commands) {
      console.log(`  ${cmd.name}  - ${cmd.description}`)
    }
  }

  if (flags.length > 0) {
    console.log("\nFlags:")
    for (const flag of flags) {
      console.log(`  --${flag.name}  - ${flag.description}`)
    }
  }

  console.log("")
}

2. Plug It In

// cli.ts
import { createCLI } from "climonad"
import { helpReporter } from "./help"

export const cli = createCLI({
  name: "example-cli",
  description: "An example CLI application",
  help: true,
  helpReporter,
})

🛎️ Change the Help Flag

Want --assist instead of --help? Easy:

help: "assist"

Now your users run:

example-cli --assist

Warning

If you don't supply a helpReporter, help output won't be shown—even if help is enabled.


💥 Make Errors Yours

Don’t let unhandled errors ruin the CLI experience. Climonad lets you define responses for common CLI mistakes so your tool speaks your language—not ours.

1. Define the Handler

// errors.ts
import { CLIErrorHandler, ErrorCodes } from "climonad"

export const errorHandler = new CLIErrorHandler<ErrorCodes>({
  TOKEN_NOT_FOUND: (token, nodes) => {
    const suggestions = nodes
      ?.filter((node) => node.name.startsWith(token))
      .map((node) => node.name)
      .join(", ")

    return `Unknown token \"${token}\". Did you mean: ${suggestions}?`
  },

  REQ_FLAG_MISSING: (flagName) => `Oops! The flag \"--${flagName}\" is required.`,
})

2. Hook It Up

// cli.ts
import { createCLI } from "climonad"
import { errorHandler } from "./errors"

export const cli = createCLI({
  name: "example-cli",
  description: "An example CLI application",
  errorHandler,
})

🧠 When To Customize

Custom error handlers shine when:

  • You want error messages in a specific tone or language
  • Your CLI targets less technical users
  • You want to plug in logging or telemetry

Note

Unhandled errors fall back to default Climonad messaging.


🧬 Deeply Nested Commands

Most CLI frameworks stop at subcommands. Climonad goes further.

Commands can nest indefinitely, allowing you to structure large command trees in clean, composable layers. Whether you’re building a microservice runner, deployment pipeline, or multi-utility toolkit, you won’t hit a wall.

Example

const root = createCLI({ name: "cli" })

const user = root.cmd({
  name: "user",
  description: "Manage users",
})

const create = user.cmd({
  name: "create",
  description: "Create a user",
  action: () => {
    console.log("User created!")
  },
})

const admin = create.cmd({
  name: "admin",
  description: "Create an admin user",
  action: () => {
    console.log("Admin user created!")
  },
})

Your CLI can now handle:

cli user create admin

Climonad doesn't impose limits on depth or nesting, so you can model your CLI the way your users think—not the way your framework dictates.


🧩 Define Custom Entry Presets

If you want to create reusable validation logic for flags, createPreset is your tool.

Important

Climonad doesn't yet support positional arguments or lists out of the box, but presets give you composable power now.

Example: Hex Color Flag

// presets.ts
import { CLIDefinition, CLIEntryPreset } from "climonad"
import { CLI } from "climonad"
import { createPreset } from "climonad"

export function hex(config: CLIDefinition<string>): CLIEntryPreset<string> {
  return createPreset("flag", config, (input) => {
    const hexRegex = /^#?[0-9A-Fa-f]+$/
    return hexRegex.test(input) ? CLI.Ok(input) : CLI.Error(null)
  })
}

Use It

export const colorFlag = hex({
  name: "color",
  description: "Set the color",
  default: "#000000",
  required: true,
  aliases: ["c"],
})

Presets are the preferred way to express custom validations currently.


🎯 Let’s Be Real

Caution

Climonad is optimized, but no abstraction is free. Here's where performance could take a hit:

  • Deep Command Nesting – While supported, very deep command trees can add processing overhead.
  • Large Token Sets – Matching against many tokens might introduce delays.
  • Complex Requirements – Intricate validation logic may slow down command resolution.

In Practice

Tip

Climonad is built with fast initialization and predictable performance in mind:

  • Time Complexity: O(1) to O(n) depending on input size.
  • Space Complexity: Linear with respect to commands and flags.
  • Runtime: Constant-time lookups and efficient traversal.

It's designed for real-world use—fast enough for scripts, structured enough for full CLI suites.


🤝 Open to Ideas

Whether it’s a sharp bug fix, a novel feature idea, or just feedback on the philosophy—PRs and issues welcome. Check the Contributing Guide before you dive in.


🔐 Responsible Disclosure

To report vulnerabilities or sensitive issues, please read our Security Policy.


📄 License

Released under the MIT License.

About

A zero-dependency CLI framework with monadic principles for creating type-safe, composable command-line interfaces with minimal boilerplate

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks