Pre-AlphaMetaScript is in early design phase. The compiler is not yet available.Join Discord for updates
MetaScript

Build a CLI Tool

Learn how to build a high-performance CLI tool that compiles to a tiny native binary.

Why MetaScript for CLI?

  • Fast cold starts: <50ms startup time
  • Small binaries: 500KB - 2MB typical
  • No runtime: Single file deployment
  • Cross-platform: Compile for Linux, macOS, Windows

Getting Started

Create the Project

npm create metascript@latest my-cli -- --template cli
cd my-cli

Project Structure

my-cli/
├── src/
│   ├── main.ms          # Entry point
│   ├── commands/        # Command handlers
│   └── utils/           # Shared utilities
├── build.ms             # Build config
└── package.json

Defining Commands

Basic Command

import { cli, Command } from "@metascript/cli"

const greet = new Command("greet")
  .description("Greet someone")
  .argument("<name>", "Name to greet")
  .option("-l, --loud", "Use uppercase")
  .action((name, options) => {
    const message = `Hello, ${name}!`
    console.log(options.loud ? message.toUpperCase() : message)
  })

cli.addCommand(greet)
cli.parse(process.argv)

Subcommands

const user = new Command("user")
  .description("User management commands")

user.addCommand(
  new Command("create")
    .argument("<email>", "User email")
    .action((email) => {
      console.log(`Creating user: ${email}`)
    })
)

user.addCommand(
  new Command("delete")
    .argument("<id>", "User ID")
    .option("-f, --force", "Skip confirmation")
    .action((id, options) => {
      if (!options.force) {
        // Prompt for confirmation
      }
      console.log(`Deleting user: ${id}`)
    })
)

cli.addCommand(user)

Interactive Prompts

import { prompt } from "@metascript/cli"

const answers = await prompt([
  {
    type: "text",
    name: "name",
    message: "What is your name?",
  },
  {
    type: "select",
    name: "color",
    message: "Favorite color?",
    choices: ["Red", "Green", "Blue"],
  },
  {
    type: "confirm",
    name: "save",
    message: "Save settings?",
  },
])

console.log(answers)

Progress and Spinners

import { spinner, progress } from "@metascript/cli"

// Spinner for indeterminate progress
const spin = spinner("Loading...")
spin.start()
await someAsyncWork()
spin.stop("Done!")

// Progress bar for known progress
const bar = progress("Downloading", { total: 100 })
for (let i = 0; i <= 100; i++) {
  bar.update(i)
  await sleep(50)
}
bar.done()

Colored Output

import { colors } from "@metascript/cli"

console.log(colors.green("Success!"))
console.log(colors.red("Error!"))
console.log(colors.yellow("Warning!"))
console.log(colors.bold.blue("Important"))

Configuration Files

Reading Config

import { config } from "@metascript/cli"

// Looks for: .myapprc, .myapprc.json, myapp.config.ts
const settings = await config.load("myapp", {
  defaults: {
    verbose: false,
    outputDir: "./dist",
  },
})

Writing Config

await config.save("myapp", {
  verbose: true,
  outputDir: "./build",
})

Building for Distribution

Compile to Native

# Build for current platform
msc build --target=c

# Cross-compile
msc build --target=c --platform=linux-x64
msc build --target=c --platform=darwin-arm64
msc build --target=c --platform=windows-x64

Create npm Package

// package.json
{
  "name": "my-cli",
  "bin": {
    "my-cli": "./dist/my-cli"
  },
  "files": ["dist/"],
  "scripts": {
    "build": "msc build --target=c",
    "prepublish": "npm run build"
  }
}

Example: File Counter

A complete example CLI that counts files:

import { cli, Command, colors, spinner } from "@metascript/cli"
import { fs } from "@metascript/core"

const count = new Command("count")
  .description("Count files in a directory")
  .argument("[dir]", "Directory to count", ".")
  .option("-r, --recursive", "Count recursively")
  .option("-e, --ext <ext>", "Filter by extension")
  .action(async (dir, options) => {
    const spin = spinner(`Counting files in ${dir}...`)
    spin.start()

    let count = 0
    const entries = options.recursive
      ? await fs.walk(dir)
      : await fs.readdir(dir)

    for (const entry of entries) {
      if (entry.isFile()) {
        if (!options.ext || entry.name.endsWith(options.ext)) {
          count++
        }
      }
    }

    spin.stop(colors.green(`Found ${count} files`))
  })

cli.name("filecount")
   .version("1.0.0")
   .addCommand(count)
   .parse(process.argv)

Next Steps