This guide explains the core concepts you need to create CLI tools with politty.
politty uses Zod schemas to define positional arguments and named options (flags) within a single object.
Positional arguments are determined by position rather than name. Specify { positional: true } in the arg() options.
Important Rule: The order of definitions within z.object directly determines the argument order.
There are two ways to define argument metadata (description, positional flag, alias, etc.):
arg(): Wrap the schema with a helper function. This is the standard approach..meta(): Chain directly on the Zod schema. When using this method,import "politty/augment";is recommended for TypeScript type support.
import "politty/augment"; // Required for .meta() type support
import { defineCommand, arg } from "politty";
import { z } from "zod";
const command = defineCommand({
args: z.object({
// Method 1: Using arg()
source: arg(z.string(), {
positional: true,
description: "Source file",
}),
// Method 2: Using .meta()
// Requires import "politty/augment"
destination: z.string().meta({
positional: true,
description: "Destination file",
}),
}),
// ...
});The examples below primarily use arg(), but you can write the same with .meta().
$ my-cli src.txt dest.txt- Required before optional: You cannot define a required positional argument after an optional one.
- ✅
required→optional - ❌
optional→required
- ✅
- Arrays must be last: Array positional arguments (e.g.,
z.array(z.string())) can be defined but must be last. They receive all remaining arguments. - No arrays with optional: When using array positional arguments, you cannot combine them with other optional positional arguments (to avoid ambiguity).
Arguments without { positional: true } are treated as named options (flags).
args: z.object({
// --name="value"
name: arg(z.string(), { description: "Name" }),
// --verbose or -v (boolean flag)
verbose: arg(z.boolean().default(false), {
alias: "v",
description: "Enable verbose logging",
}),
});- Boolean flags: Their presence alone is treated as
true(e.g.,--verbose). - Aliases: Use
aliasto define short forms like-v. Multi-character entries become additional long options (e.g.alias: "to-be"accepts both--tobeand--to-be). Pass an array to combine short and long aliases:alias: ["v", "loud"]. - Hidden aliases: Use
hiddenAliasfor names the parser should accept but that should not appear in help, generated docs, or shell completion (typical use: legacy or deprecated names). - Default values: Use Zod's
.default()to set fallback values.
Using z.array() allows the same option to be specified multiple times.
args: z.object({
include: arg(z.array(z.string()), {
alias: "i",
description: "Files to include",
}),
});$ my-cli --include file1.txt -i file2.txt
# args.include = ["file1.txt", "file2.txt"]Since politty is built on Zod, you get powerful validation features out of the box.
Command-line arguments are strings by default. Use z.coerce for automatic type conversion.
args: z.object({
// Convert "123" to number 123
port: arg(z.coerce.number().default(3000)),
// Convert "2023-01-01" to Date object
date: arg(z.coerce.date()),
});You can also use Zod's refine methods and more.
args: z.object({
email: arg(z.string().email()),
age: arg(z.coerce.number().min(18).max(100)),
url: arg(z.string().url()),
});Validation errors are automatically caught and displayed to users in a readable format.
defineCommand supports three lifecycle hooks:
setup: Runs before the main process. Useful for initializing resources (DB connections, loading config).run: The main command process.cleanup: Runs afterruncompletes, even if an error occurred. Ideal for closing connections or deleting temporary files.
const command = defineCommand({
setup: async ({ args }) => {
console.log("Setting up...");
},
run: async (args) => {
console.log("Running...");
// throw new Error("Oops"); // cleanup still runs even if error here
},
cleanup: async ({ args, error }) => {
console.log("Cleaning up...");
if (error) console.error("An error occurred during execution:", error);
},
});The execution order setup → run → cleanup is always guaranteed.