Quickly and easily create interactive CLI prototypes.
Enabling you to rapidly iterate, share a CLI concept with colleagues or do user testing.

A "dummy" prototype of a task management CLI. You can see more interaction examples under components
Proto* is a tool for creating interactive CLI prototypes using a simple JSON configuration file. It generates a website that emulates a terminal, with only the CLI defined in the configuration file being available. It's designed for quick and easy creation of CLI prototypes, enabling rapid iterations.
The tool automatically deploys to GitHub Pages, allowing prototypes to be easily shared. This makes it suitable for user testing, as users can access the prototype through a simple link rather than having to install anything.
Prototypes can also be shared as a URL without forking or deploying anything.
-
Create a new repository using this repository as a template.
-
Clone the newly created repository:
git clone https://github.com/your-username/cli-prototype.git cd cli-prototype -
Install dependencies:
pnpm install
-
Start the development server:
pnpm dev
-
Open the provided URL in your browser to interact with your CLI prototype.
Edit packages/playground/src/commands.json to define your CLI prototype. The development server hot-reloads on changes.
Run the test suites:
# Install Playwright browsers (first time only)
pnpm exec playwright install --with-deps chromium
# Unit tests
pnpm test:unit
# End-to-end tests
pnpm test:e2eThis project deploys automatically to GitHub Pages via GitHub Actions.
-
In your GitHub repository, go to Settings > Pages.
-
Under "Source", select GitHub Actions.
-
On the home page of your repository, click the settings icon of the "About" section.
-
Under website, check "Use your GitHub Pages website".
-
Your GitHub Pages link will now show up in the "About" section.
-
The site deploys automatically on pushes to the main branch. A green checkmark confirms a successful deployment.
Normally, creating a prototype means forking this repo, editing commands.json, and deploying your own GitHub Pages site. URL sharing removes that overhead entirely: you write (or generate) a commands.json, encode it, and get a link that loads your prototype on the main project's hosted playground. No fork, no deploy, no server.
This is especially useful when a coding agent generates the JSON for you. An agent can produce a commands.json, encode it, and hand you a link you can open in the browser immediately.
The encoding works by compressing the JSON config and packing it into the URL hash. Since the hash never leaves the browser, nothing is sent to a server.
From the playground: press Ctrl+Shift+L to copy a share URL to the clipboard.
From the command line using the @dgtlntv/protostar-codec package:
cat commands.json | pnpm exec protostar-encode
# → https://dgtlntv.github.io/protostar/#p1=<payload>Open the resulting URL and the encoded prototype boots instead of the bundled demo.
You can integrate Protostar into your own JavaScript projects. The published bundle is self-contained, no shim aliases or polyfills required.
pnpm add @dgtlntv/protostarimport "@xterm/xterm/css/xterm.css"
import "@dgtlntv/protostar/styles.css"
import { Protostar } from "@dgtlntv/protostar"
import commandsData from "./commands.json"
document.addEventListener("DOMContentLoaded", () => {
const protostar = new Protostar(
document.getElementById("terminal"),
commandsData
)
protostar.start()
})The only file you need to change to customize your CLI prototype is packages/playground/src/commands.json. The general schema is:
{
// Welcome message displayed when the CLI loads
welcome: "Welcome to My CLI! Type 'help' for available commands.",
// Global variables that can be read and written across commands
variables: {
username: "dgtlntv",
isLoggedIn: "false",
},
// The commands available in the CLI
commands: {},
}welcomeis optional. It defines a welcome message shown when the CLI loads.variablesis optional. Use it to define variables that can be set and checked by commands, enabling prescribed command sequences.commandsdefines the available CLI commands.
The command name is the key of the command object:
{
commands: {
register: {
// Command content
},
},
}Shown in the automatically generated help message.
{
commands: {
register: {
description: "This command registers a new account with the service.",
},
},
}Call the same command with a different name. Can be a string or array of strings.
{
commands: {
register: {
alias: ["enrol", "signup"],
},
},
}Provides example usages in the help message. Can be a single [command, description] or an array.
{
commands: {
register: {
example: [
"register email@example.com",
"Register a new account with the email@example.com email.",
],
},
},
}Required positional arguments use <email>, optional ones use [username].
{
commands: {
"register <email> [username]": {
// Command content
},
},
}Use | for aliases: <email | username>. Use .. for variadic: [..socialUrls].
Under positional, describe each argument:
| Field | Description |
|---|---|
| alias | Alternative name(s) for the positional argument. |
| choices | An array of valid values for this positional argument. |
| default | The default value if not provided. |
| demandOption | Marks the argument as required. If a string, used as the error message. |
| description | A short description. |
| type | Expected data type (boolean, number, string). |
{
commands: {
"register <email>": {
positional: {
email: {
alias: "username",
demandOption: true,
description: "The email to register your account with",
type: "string",
},
},
},
},
}Under options, define the flags (e.g., --flag) for a command:
| Field | Description |
|---|---|
| alias | Alternative name(s) for the option. |
| choices | An array of valid values. |
| default | The default value if not provided. |
| defaultDescription | A description of the default value. |
| demandOption | Marks the option as required. If a string, used as the error message. |
| description | A short description. |
| group | Group for related options in help output. |
| hidden | If true, hidden from help output. |
| nargs | Number of arguments consumed by this option. |
| requiresArg | If true, must be specified with a value. |
| type | Expected data type (boolean, number, string). |
{
commands: {
register: {
options: {
password: {
alias: ["pwd", "pw"],
demandOption: true,
description: "The password for your account",
group: "Login credentials",
hidden: false,
nargs: 1,
requiresArg: true,
type: "string",
},
},
},
},
}Nest commands under commands to create sub-commands:
{
commands: {
register: {
commands: {
user: {
// Command content
},
serviceaccount: {
// Command content
},
},
},
},
}Under handler, define the response to a command. The handler accepts one component or an array of components that run in sequence.
| Component | Description |
|---|---|
| text | Prints text to the terminal. |
| progressBar | Renders a progress bar. |
| spinner | Renders an animated spinner. |
| table | Renders a table. |
| conditional | Evaluates a condition and executes a component based on the result. |
| variable | Saves a value to a global variable. |
| autoComplete | Prompt that auto-completes as the user types. |
| basicAuth | Prompt for username and password authentication. |
| confirm | Prompt to confirm or deny a statement. |
| form | Prompt for multiple values on a single screen. |
| input | Prompt for user input. |
| invisible | Prompt for user input, hiding it from the terminal. |
| list | Prompt returning a list of values from comma-separated input. |
| multiSelect | Prompt allowing selection of multiple items from a list. |
| number | Prompt that takes a number as input. |
| password | Prompt that masks user input. |
| select | Prompt for selecting from a list of options. |
| sort | Prompt for sorting items in a list. |
| toggle | Prompt for toggling between two values. |
Single component:
{
commands: {
register: {
handler: {
component: "text",
output: "Registered successfully",
},
},
},
}Array of components:
{
commands: {
register: {
handler: [
{
component: "text",
output: "Registering in progress...",
duration: 5000,
},
{
component: "text",
output: "Registered successfully",
},
],
},
},
}A component corresponds to something that happens as a reaction to a command. Multiple components can be chained to run in sequence.
Prints text to the terminal, optionally waiting for a duration afterward.
| Field | Required | Description |
|---|---|---|
| output | Yes | The text to print. |
| duration | No | Duration in ms to wait after printing. Also accepts "random" (100ms-3000ms). |
{
component: "text",
output: "Registering in progress...",
duration: 2000,
}Renders a progress bar showing task completion over time.
| Field | Required | Description |
|---|---|---|
| output | Yes | Text displayed alongside the progress bar. |
| duration | Yes | Duration in ms for the bar to complete. Also accepts "random" (100ms-3000ms). |
{
component: "progressBar",
output: "Installing dependencies...",
duration: 2000,
}Displays an animated spinner indicating an ongoing process.
| Field | Required | Description |
|---|---|---|
| output | Yes | Text or array of texts displayed alongside the spinner. |
| duration | Yes | Duration in ms. Also accepts "random" (100ms-3000ms). |
| conclusion | No | How the spinner concludes: stop, success, or fail. |
{
component: "spinner",
output: ["Processing", "Please wait", "Almost done"],
duration: 2000,
conclusion: "succeed",
}Renders a formatted table.
| Field | Required | Description |
|---|---|---|
| output | Yes | A 2D array representing table data (including headers). |
| colWidths | No | Array of column widths. If not set, columns hug their content up to the terminal width. |
{
component: "table",
output: [
["Name", "Age", "City"],
["John", "30", "New York"],
["Alice", "25", "London"],
],
colWidths: [10, 5, 15],
}Branches logic based on a condition evaluated against global variables.
| Field | Required | Description |
|---|---|---|
| output | Yes | Object containing if, then, and optionally else fields. |
The output object:
| Field | Required | Description |
|---|---|---|
| if | Yes | Condition string to evaluate. |
| then | Yes | Component to execute if true. Can be another conditional. |
| else | No | Component to execute if false. Can be another conditional. |
{
component: "conditional",
output: {
if: "isLoggedIn == 'true'",
then: {
component: "text",
output: "Welcome back!",
},
else: {
component: "text",
output: "Please log in first.",
},
},
}Sets global variables that can be used across commands. The variable must be declared in the top-level variables object.
| Field | Required | Description |
|---|---|---|
| output | Yes | Object where keys are variable names and values are strings. |
{
component: "variable",
output: {
username: "john_doe",
isLoggedIn: "true",
},
}Prompt that auto-completes as the user types.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Message displayed with the prompt. |
| choices | Yes | List of items for selection. |
| limit | No | Number of choices visible on-screen. |
| initial | No | Index of the initial selection. |
| multiple | No | Allow multiple selections. |
| footer | No | Muted hint message. |
{
component: "autoComplete",
name: "query",
message: "Search for a fruit:",
choices: ["Apple", "Banana", "Cherry", "Date", "Elderberry"],
limit: 3,
footer: "Use arrow keys to navigate",
}Prompts for username and password authentication.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Message displayed with the prompt. |
| username | Yes | Username to compare against. |
| password | Yes | Password to compare against. |
| showPassword | No | Whether to show the password. |
{
component: "basicAuth",
name: "auth",
message: "Please enter your credentials:",
username: "admin",
password: "secret",
showPassword: false,
}Prompts to confirm or deny with a Y/n keystroke.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Question to confirm or deny. |
| initial | No | Initial value (true or false). |
{
component: "confirm",
name: "confirmDelete",
message: "Are you sure you want to delete this item?",
initial: false,
}Prompts for multiple values on a single screen.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the form results. |
| message | Yes | Message displayed with the form. |
| choices | Yes | Array of form fields (see below). |
Each choice:
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the field. |
| message | Yes | Label for the field. |
| initial | No | Initial placeholder value. |
{
component: "form",
name: "userInfo",
message: "Please enter your information:",
choices: [
{ name: "username", message: "Username:", initial: "user123" },
{ name: "email", message: "Email:" },
],
}Prompts for text input.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Prompt message. |
| initial | No | Initial placeholder value. |
{
component: "input",
name: "username",
message: "What's your name?",
initial: "Anonymous",
}Prompts for input that is completely hidden from the terminal.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Prompt message. |
{
component: "invisible",
name: "password",
message: "Enter your password:",
}Prompts for a comma-separated list of values.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Prompt message. |
{
component: "list",
name: "tags",
message: "Enter tags (comma-separated):",
}Allows selection of multiple items from a list.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the results. |
| message | Yes | Message displayed with the prompt. |
| choices | Yes | Array of selectable options (see below). |
| limit | No | Number of choices visible on-screen. |
Each choice:
| Field | Required | Description |
|---|---|---|
| name | Yes | Display text. |
| value | Yes | Value returned if selected. |
{
component: "multiSelect",
name: "features",
message: "Select desired features:",
choices: [
{ name: "Auto-save", value: "autosave" },
{ name: "Dark mode", value: "darkmode" },
{ name: "Notifications", value: "notifications" },
],
limit: 2,
}Prompts for a numeric input.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Prompt message. |
{
component: "number",
name: "age",
message: "Enter your age:",
}Prompts for a password, masking the input.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Prompt message. |
{
component: "password",
name: "newPassword",
message: "Enter new password:",
}Prompts for selecting a single item from a list.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Message displayed with the prompt. |
| choices | Yes | List of options (strings or objects). |
Choices as objects:
| Field | Required | Description |
|---|---|---|
| name | Yes | Display text. |
| value | Yes | Value returned if selected. |
{
component: "select",
name: "favoriteColor",
message: "Choose your favorite color:",
choices: [
{ name: "Red", value: "red" },
{ name: "Blue", value: "blue" },
{ name: "Green", value: "green" },
],
}Prompts for sorting items in a list.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the sorted result. |
| message | Yes | Message displayed with the prompt. |
| choices | Yes | List of items to sort. |
{
component: "sort",
name: "taskOrder",
message: "Sort these tasks by priority:",
choices: [
"Fix bugs",
"Implement new feature",
"Write documentation",
"Refactor code",
],
}Prompts for toggling between two values using Left/Right arrow keys.
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the result. |
| message | Yes | Message displayed with the prompt. |
| enabled | Yes | Label for the enabled state. |
| disabled | Yes | Label for the disabled state. |
{
component: "toggle",
name: "notificationsEnabled",
message: "Enable notifications?",
enabled: "Yes",
disabled: "No",
}

















