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-cliProject Structure
my-cli/
├── src/
│ ├── main.ms # Entry point
│ ├── commands/ # Command handlers
│ └── utils/ # Shared utilities
├── build.ms # Build config
└── package.jsonDefining 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-x64Create 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
- Deploy to Lambda - Run your CLI as serverless functions
- CLI Reference - Full msc command documentation
- Showcase - See CLIs built with MetaScript