# Pastel: a file-based command framework for Ink CLIs

> Pastel maps a commands folder to a Commander program and renders each command as an Ink React component, with options and arguments validated by Zod. It is for TypeScript developers who want a structured CLI, not a single script.

**vadimdemedes/pastel** — 🎨 Next.js-like framework for CLIs made with Ink

- Repository: https://github.com/vadimdemedes/pastel
- Website: https://term.ink/pastel
- Stars: 2,408 · Forks: 43
- Language: TypeScript
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/vadimdemedes-pastel

## The problem Pastel solves in an Ink CLI

Ink renders React components to the terminal, but it does not decide how a CLI is structured. A developer who wants several commands, each with its own options, ends up writing argument parsing, help output and command dispatch by hand. Pastel moves that structure into the filesystem. Every file in the commands folder becomes a command, and the filename becomes the command name. The README describes the framework as a "Next.js-like framework for CLIs made with Ink," and the comparison is about routing, not rendering. A file named login.tsx under commands produces a login command; a folder named domains containing list.tsx, add.tsx and remove.tsx produces domains list, domains add and domains remove. The intended user is a TypeScript developer who already knows React and Ink and wants the command surface to grow by adding files rather than by editing a central registry. If your tool is a single command with a couple of flags, the framework adds a dependency tree for very little return.

## How a commands folder becomes a Commander program

The mechanism has three parts. Pastel reads the commands directory, Commander parses the process arguments, and Ink renders the React component exported by the matched command file. According to the package.json, Commander 14 is a direct dependency, so the parsing layer is the same one many Node CLIs already use. Zod supplies the schema layer: a command file exports an options object built with zod.object, and each field carries a describe call that becomes the option description in the help output. The README shows the generated help for the scaffolded hello-world app, listing --name, -v, --version and -h, --help, which means version and help are supplied by the framework rather than written by the developer. Type safety follows from Zod inference: the README types the component props as zod.infer<typeof options>, so a renamed option breaks the build instead of failing at runtime. Commands can also export isDefault set to true, which makes that command run when no explicit command is given while still appearing in the help message. The README points to Vercel's CLI as a real-world example of that pattern, where both vercel and vercel deploy start a deploy.

## Installing Pastel and running a first command

The README gives a scaffold command and a manual path. The scaffold is the shorter route and produces a project with TypeScript, a linter and tests already configured.

```bash
npm create pastel-app hello-world
cd hello-world
```

The manual setup starts with a project and the runtime packages. Note that the install line includes ink, react and zod, because Pastel declares them as peer dependencies rather than bundling them: the package.json lists ink >=6.0.0, react >=19.0.0 and zod >=4.0.0, and the engines field requires Node >=20.

```bash
npm install pastel ink react zod
```

The entrypoint creates a Pastel instance and runs it. The importMeta field is what lets Pastel locate the commands folder relative to the compiled entry file.

```js
#!/usr/bin/env node
import Pastel from 'pastel';

const app = new Pastel({
	importMeta: import.meta,
});

await app.run();
```

A default command lives at source/commands/index.tsx and exports both an options schema and a React component. The schema field here is a required string, described as "Your name".

```tsx
import React from 'react';
import {Text} from 'ink';
import zod from 'zod';

export const options = zod.object({
	name: zod.string().describe('Your name'),
});

export default function Index({options}: {options: zod.infer<typeof options>}) {
	return <Text>Hello, {options.name}!</Text>;
}
```

After compiling with npx tsc, the README adds a bin field pointing at build/cli.js, runs npm link --global, and then hello-world --name=Jane prints Hello, Jane!. Running hello-world --help prints the usage block with the option, the version flag and the help flag.

## Where Pastel stops being the right tool

The framework assumes Node 20 or newer, and the README does not document a fallback for older runtimes. That is a hard floor for anyone distributing a CLI to environments pinned to an earlier Node. The peer dependency ranges are the second constraint: Pastel does not install Ink, React or Zod for you, so a project already on React 18 or Zod 3 has to upgrade those before Pastel will work. The rendering model is the third. Because commands render Ink components, a command that mostly shells out and prints plain lines still pays for the React and Ink runtime, and output that needs to be piped into another program has to be handled with care since Ink is built for interactive terminal rendering. The README is also explicit about what it does not cover: it documents commands, options, arguments, aliases, custom app settings and the API, but there is no section on testing a Pastel app, on packaging for distribution beyond npm link, or on what happens when two command files resolve to the same name. For a long-running interactive TUI, Ink alone plus your own argument parsing may be less machinery.

## Pastel compared with Commander on its own

Commander is the closest comparison because Pastel uses it internally. With Commander directly, you create a program, call .command() for each command, attach .option() calls, and write the action handlers. Everything about the CLI lives in one file or a small set of files you wire together yourself. Pastel inverts that: the file tree is the command tree, and the option definitions are Zod schemas attached to the component that renders the command. The difference shows up when the CLI grows. Adding a subcommand in Commander means editing the program setup; in Pastel it means creating a file in a nested folder. The cost of the Pastel approach is the extra runtime and the peer dependency set. Commander alone has no React, no Ink and no Zod requirement, and its own package.json has no Node 20 floor of this kind. If your commands produce plain text and you do not want a component tree, Commander is the smaller commitment. If your commands already render Ink interfaces, Pastel removes the dispatch code you would otherwise write.

## Maintenance, licence and upgrade cost

The last push to the repository was on 2026-03-21, the same day as the v4.0.1 release, and the repository is not archived. The release history shows a major version roughly every year to eighteen months: v3.0.0 in June 2024, v4.0.0 in October 2025, v4.0.1 in March 2026. Major bumps are the upgrade events to plan for. Pastel is published under the MIT licence, which permits commercial and closed-source use; the peer dependencies you install alongside it carry their own licences, so a distributed CLI inherits the terms of Ink, React, Zod and Commander as well. Because Pastel is a thin layer over Commander, a major Pastel release that changes how options are declared would still leave the underlying Commander behaviour intact, but the README does not document a migration path between major versions, so pinning a version and reading the release notes before upgrading is the practical approach. This is a description of the licence text, not legal advice.

## Conclusion

Pastel fits a TypeScript team that already ships React and wants a multi-command CLI whose help text and option parsing come from the command files themselves. Skip it if your tool is one command with no subcommands, if you want to avoid React and Ink in your dependency tree, or if you need to run on Node below 20. Before adopting, check that your Node, Ink, React and Zod versions satisfy the peer dependency ranges, and read the API section of the README for Custom app if you need to change the program name, description or version.

## FAQ

### What Node version does Pastel require?

The package.json engines field requires Node >=20, and the README does not document support for earlier versions. Pastel also declares ink >=6.0.0, react >=19.0.0 and zod >=4.0.0 as peer dependencies, so those must be installed alongside it.

### How do I add a new command to a Pastel CLI?

Create a file in the commands folder; the filename becomes the command name. The file exports a React component as its default export, and nested folders create subcommands such as domains list and domains add.

### How does Pastel define command options and arguments?

Options are declared with a Zod schema exported as options from the command file, and each field's describe call becomes the option description in the generated help message. The README states that arguments are also defined via Zod, with string, number and enum types documented.

### Can a Pastel command run when no command name is given?

Yes. A file named index.tsx is an index command and runs by default. Alternatively, a named command can export isDefault set to true, which makes it run when no explicit command is specified while still appearing in the help message.

## Sources

- [License: MIT](https://github.com/vadimdemedes/pastel/blob/main/LICENSE)
- [Project website](https://term.ink/pastel)
- [README](https://github.com/vadimdemedes/pastel/blob/main/README.md)
- [Releases](https://github.com/vadimdemedes/pastel/releases)
- [vadimdemedes/pastel on GitHub](https://github.com/vadimdemedes/pastel)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/vadimdemedes-pastel
