SkillAgentSearch skills...

Pastel

🎨 Next.js-like framework for CLIs made with Ink

Install / Use

/learn @vadimdemedes/Pastel
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Pastel test

Next.js-like framework for CLIs made with Ink.

Features

  • Create files in commands folder to add commands.
  • Create folders in commands to add subcommands.
  • Define options and arguments via Zod.
  • Full type-safety of options and arguments thanks to Zod.
  • Auto-generated help message for commands, options and arguments.
  • Uses battle-tested Commander package under the hood.

Install

npm install pastel ink react zod

Getting started

Use create-pastel-app to quickly scaffold a Pastel app with TypeScript, linter and tests set up.

npm create pastel-app hello-world
hello-world
<details><summary>Manual setup</summary> <p>
  1. Set up a new project.
mkdir hello-world
cd hello-world
npm init --yes
  1. Install Pastel and TypeScript.
npm install pastel
npm install --save-dev typescript @sindresorhus/tsconfig
  1. Create a tsconfig.json file to set up TypeScript.
{
	"extends": "@sindresorhus/tsconfig",
	"compilerOptions": {
		"outDir": "build",
		"sourceMap": true
	},
	"include": [
		"source"
	]
}
  1. Create a source folder for the source code.
mkdir source
  1. Create a source/cli.ts file with the following code, which will be CLI's entrypoint:
#!/usr/bin/env node
import Pastel from 'pastel';

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

await app.run();
  1. Create source/commands folder for defining CLI's commands.
mkdir source/commands
  1. Create an source/commands/index.tsx file for a default command, with the following code:
import React from 'react';
import {Text} from 'ink';
import zod from 'zod';

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

type Props = {
	options: zod.infer<typeof options>;
};

export default function Index({options}: Props) {
	return <Text>Hello, {options.name}!</Text>;
}
  1. Build your CLI.
npx tsc
  1. Set up an executable file.

9.1. Add bin field to package.json, which points to the compiled version of source/cli.ts file.

	"bin": "build/cli.js"

9.2. Make your CLI available globally.

npm link --global
  1. Run your CLI.
hello-world --name=Jane
Hello, Jane!
hello-world --help
Usage: hello-world [options]

Options:
  --name         Your name
  -v, --version  Show version number
  -h, --help     Show help

</p></details>

Table of contents

Commands

Pastel treats every file in the commands folder as a command, where filename is a command's name (excluding the extension). Files are expected to export a React component, which will be rendered when command is executed.

You can also nest files in folders to create subcommands.

Here's an example, which defines login and logout commands:

commands/
	login.tsx
	logout.tsx

login.tsx

import React from 'react';
import {Text} from 'ink';

export default function Login() {
	return <Text>Logging in</Text>;
}

logout.tsx

import React from 'react';
import {Text} from 'ink';

export default function Logout() {
	return <Text>Logging out</Text>;
}

Given that your executable is named my-cli, you can execute these commands like so:

$ my-cli login
$ my-cli logout

Index commands

Files named index.tsx are index commands. They will be executed by default, when no other command isn't specified.

commands/
	index.tsx
	login.tsx
	logout.tsx

Running my-cli without a command name will execute index.tsx command.

$ my-cli

Index command is useful when you're building a single-purpose CLI, which has only one command. For example, np or fast-cli.

Default commands

Default commands are similar to index commands, because they too will be executed when an explicit command isn't specified. The difference is default commands still have a name, just like any other command, and they'll show up in the help message.

Default commands are useful for creating shortcuts to commands that are used most often.

Let's say there are 3 commands available: deploy, login and logout.

commands/
	deploy.tsx
	login.tsx
	logout.tsx

Each of them can be executed by typing their name.

$ my-cli deploy
$ my-cli login
$ my-cli logout

Chances are, deploy command is going to be used a lot more frequently than login and logout, so it makes sense to make deploy a default command in this CLI.

Export a variable named isDefault from the command file and set it to true to mark that command as a default one.

import React from 'react';
import {Text} from 'ink';

+ export const isDefault = true;

export default function Deploy() {
	return <Text>Deploying...</Text>;
}

Now, running my-cli or my-cli deploy will execute a deploy command.

$ my-cli

Vercel's CLI is a real-world example of this approach, where both vercel and vercel deploy trigger a new deploy of your project.

Subcommands

As your CLI grows and more commands are added, it makes sense to group the related commands together.

To do that, create nested folders in commands folder and put the relevant commands inside to create subcommands. Here's an example for a CLI that triggers deploys and manages domains for your project:

commands/
	deploy.tsx
	login.tsx
	logout.tsx
	domains/
		list.tsx
		add.tsx
		remove.tsx

Commands for managing domains would be executed like so:

$ my-cli domains list
$ my-cli domains add
$ my-cli domains remove

Subcommands can even be deeply nested within many folders.

Aliases

Commands can have an alias, which is usually a shorter alternative name for the same command. Power users prefer aliases instead of full names for commands they use often. For example, most users type npm i instead of npm install.

Any command in Pastel can assign an alias by exporting a variable named alias:

import React from 'react';
import {Text} from 'ink';

+ export const alias = 'i';

export default function Install() {
	return <Text>Installing something...</Text>;
}

Now the same install command can be executed by only typing i:

$ my-cli i

Options

Commands can define options to customize their default behavior or ask for some additional data to run properly. For example, a command that creates a new server might specify options for choosing a server's name, an operating system, memory size or a region where that server should be spin up.

Pastel uses Zod to define, parse and validate command options. Export a variable named options and set a Zod object schema. Pastel will parse that schema and automatically set these options up. When command is executed, option values are passed via options prop to your component.

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

export const options = zod.object({
	name: zod.string().describe('Server name'),
	os: zod.enum(['Ubuntu', 'Debian']).describe('Operating system'),
	memory: zod.number().describe('Memory size'),
	region: zod.enum(['waw', 'lhr', 'nyc']).describe('Region'),
});

type Props = {
	options: zod.infer<typeof options>;
};

export default function Deploy({options}: Props) {
	return (
		<Text>
			Deploying a server named "{options.name}" running {options.os} with memory
			size of {options.memory} MB in {options.region} region
		</Text>
	);
}

With options set up, here's an example deploy command:

$ my-cli deploy --name=Test --os=Ubuntu --memory=1024 --region=waw
Deploying a server named "Test" running Ubuntu with memory size of 1024 MB in waw region.

Help message is auto-generated for you as well.

$ my-cli deploy --help
Usage: my-cli deploy [options]

Options:
  --name         Server name
  --os           Operating system (choices: "Ubuntu", "Debian")
  --memory       Memory size
  --region       Region
  -v, --version  Show version number
  -h, --help     Show help

Types

Pastel only supports string, number, boolean, enum, array and set types for defining options.

String

Example that defines a --name string option:

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

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

type Props = {
	options: zod.
View on GitHub
GitHub Stars2.4k
CategoryDevelopment
Updated46m ago
Forks43

Languages

TypeScript

Security Score

100/100

Audited on Mar 31, 2026

No findings