# termost

> Get the most of your terminal

Latest version **2.0.0** (published 2026-08-24) · MIT license · 0 weekly downloads

## Install

```sh
npm install termost
pnpm add termost
yarn add termost
bun add termost
```

## Health

**Score 65/100 (B)** — status: active.

Positive: esm support; no vulnerabilities; has provenance; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 2.0.0 |
| Published | 2026-08-24 |
| First published | 2021-08-13 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | ESM |
| Dependencies | 1 |
| Unpacked size | 49.7 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 186 |
| Author | Ayoub Adib <adbayb@gmail.com> (https://twitter.com/adbayb) |
| Maintainers | adbayb |
| Keywords | args, argument, cli, command, message, option, question, task, terminal |

## Links

- npm: https://www.npmjs.com/package/termost
- Repository: git@github.com:adbayb/termost
- Homepage: https://github.com/adbayb/termost/tree/main/termost#readme
- Issues: https://github.com/adbayb/termost/issues
- npm.io page: https://npm.io/package/termost

## Dependencies (1)

- [@clack/prompts](https://npm.io/package/@clack/prompts.md) ^1.7.0

## Alternatives

- [@salesforce/cli](https://npm.io/package/@salesforce/cli.md) — 389.7K weekly downloads
- [@mintlify/cli](https://npm.io/package/@mintlify/cli.md) — 208.9K weekly downloads
- [@grafana/e2e-selectors](https://npm.io/package/@grafana/e2e-selectors.md) — 128.7K weekly downloads
- [mintlify](https://npm.io/package/mintlify.md) — 112.0K weekly downloads
- [@intlayer/cli](https://npm.io/package/@intlayer/cli.md) — 22.8K weekly downloads

## Recent versions

- 2.0.0 (latest) — 2026-08-24
- 2.0.0-next.1787580477295.3c1fc6f (next) — 2026-08-24
- 2.0.0-next.1787580025828.edf2cf4 — 2026-08-24
- 2.0.0-next.1787579253893.aedef71 — 2026-08-24
- 2.0.0-next.1787579169953.c31bdc3 — 2026-08-24
- 2.0.0-next.1787576940627.bf20b1f — 2026-08-24
- 2.0.0-next.1787574052443.9d817ed — 2026-08-24
- 2.0.0-next.1787570142676.db069e9 — 2026-08-24
- 2.0.0-next.1787567046497.4c8df62 — 2026-08-24
- 2.0.0-next.1787564068731.8ad50ed — 2026-08-24
- 1.9.2-next-d1e744c — 2026-08-02
- 1.9.2 — 2026-08-02
- 1.9.1-next-78f246b — 2026-08-02
- 1.9.1-next-95cd956 — 2026-08-02
- 1.9.1-next-5370a3e — 2026-08-01
- … 80 more at https://npm.io/package/termost/versions

## README

<br>
<div align="center">
    <h1>💻 Termost</h1>
    <strong>Get the most of your terminal</strong>
</div>
<br>
<br>

## ✨ Features

Termost allows building command line tools in a minute thanks to its:

- [Fluent](https://en.wikipedia.org/wiki/Fluent_interface) syntax to express your CLI configurations with instructions such as:
    - [Subcommand](examples/command/src/index.ts) support
    - Long and short [option](examples/option/src/index.ts) support
    - [User input](examples/input/src/index.ts) support
    - [Task](examples/task/src/index.ts) support
- Shareable output between instructions
- Auto-generated help and version metadata
- TypeScript support to foster a type-safe API
- Built-in helpers to make stdin/stdout management a breeze (including `exec`, and `createLogger`...)

<br>

## 🚀 Quickstart

Install the library:

```bash
# Npm
npm install termost
# Pnpm
pnpm add termost
# Yarn
yarn add termost
```

Once you're done, you can play with the API:

```ts
#!/usr/bin/env node

import { createLogger, termost } from "termost";
import { name, version } from "../package.json" with { type: "json" }; // Depending on your `package.json` location.

type ProgramContext = {
	globalFlag: string;
};

type DebugCommandContext = {
	localFlag: string;
};

const logger = createLogger({ name: "my-cli" });

const program = termost<ProgramContext>({
	name,
	description: "CLI description",
	version,
	onException(error) {
		console.error(`Error logic ${error.message}`);
	},
	onShutdown() {
		console.log("Clean-up logic");
	},
});

program.option({
	key: "globalFlag",
	name: { long: "global", short: "g" },
	description:
		"A global flag/option example accessible by all commands (key is used to persist the value into the context object)",
	defaultValue: "A default value can be set if no flag is provided by the user",
	validate({ globalFlag }) {
		if (globalFlag === "invalid") return new Error("Invalid input");

		return undefined;
	},
});

program
	.command({
		name: "build",
		description:
			"A custom command example runnable via `bin-name build` (command help available via `bin-name build --help`)",
	})
	.task({
		label: "A label can be displayed to follow the task progress",
		async handler() {
			await fakeBuild();
		},
	});

program
	.command<DebugCommandContext>({
		name: "debug",
		description: "A command to play with Termost capabilities",
	})
	.option({
		key: "localFlag",
		name: "local",
		description: "A local flag accessible only by the `debug` command",
		defaultValue: "local-value",
	})
	.task({
		handler(context, argv) {
			logger.info(`Hello, I'm the ${argv.command} command`);
			logger.info(`Context value = ${JSON.stringify(context)}`);
			logger.info(`Argv value = ${JSON.stringify(argv)}`);
		},
	});

const fakeBuild = async () => {
	return new Promise((resolve) => {
		setTimeout(resolve, 3000);
	});
};
```

Depending on the command, the output will look like this (`bin-name` is the program name automatically retrieved from the `package.json>name`):

| Command                 |                                                       Preview                                                        |
| :---------------------- | :------------------------------------------------------------------------------------------------------------------: |
| `bin-name --help`       | <img alt="Global help" src="https://github.com/adbayb/termost/assets/10498826/ccb55954-5cd1-4528-a98a-0b1fb480447f"> |
| `bin-name debug --help` | <img alt="Local help" src="https://github.com/adbayb/termost/assets/10498826/4127d5d6-4592-496a-b03d-484de4f8a2f7">  |

<br>

## ✍️ Usage

Here's an API overview:

<details>
<summary><b>command({ name, description })</b></summary>
<p>

The `command` API creates a new subcommand context.  
Please note that the root command context is shared across subcommands but subcommand's contexts are scoped and not accessible between each other.

```ts
#!/usr/bin/env node

import { createLogger, termost } from "termost";
import { name, version } from "../package.json" with { type: "json" }; // Depending on your `package.json` location.

const logger = createLogger({ name: "my-cli" });

const program = termost({
	name,
	description: "CLI description",
	version,
});

program
	.command({
		name: "build",
		description: "Transpile and bundle in production mode",
	})
	.task({
		handler(context, argv) {
			logger.info(`👋 Hello, I'm the ${argv.command} command`);
		},
	});

program
	.command({
		name: "watch",
		description: "Rebuild your assets on any code change",
	})
	.task({
		handler(context, argv) {
			logger.warn(`👋 Hello, I'm the ${argv.command} command`);
		},
	});
```

</p>
</details>

<details>
<summary><b>input({ key, label, type, skip, validate, ...typeParameters })</b></summary>
<p>

The `input` API creates an interactive prompt.  
It supports several types:

```ts
#!/usr/bin/env node

import { createLogger, termost } from "termost";
import { name, version } from "../package.json" with { type: "json" }; // Depending on your `package.json` location.

type ProgramContext = {
	input1: "singleOption1" | "singleOption2";
	input2: Array<"multipleOption1" | "multipleOption2">;
	input3: boolean;
	input4: string;
};

const logger = createLogger({ name: "my-cli" });

const program = termost<ProgramContext>({
	name,
	description: "CLI description",
	version,
});

program
	.input({
		type: "select",
		key: "input1",
		label: "What is your single choice?",
		options: ["singleOption1", "singleOption2"],
		defaultValue: "singleOption2",
	})
	.input({
		type: "multiselect",
		key: "input2",
		label: "What is your multiple choices?",
		options: ["multipleOption1", "multipleOption2"],
		defaultValue: ["multipleOption2"],
	})
	.input({
		type: "confirm",
		key: "input3",
		label: "Are you sure to skip next input?",
		defaultValue: false,
	})
	.input({
		type: "text",
		key: "input4",
		label: (context) =>
			`Dynamic input label generated from a contextual value: ${context.input1}`,
		defaultValue: "Empty input",
		skip(context) {
			return Boolean(context.input3);
		},
		validate(context) {
			if (context.input4 === "invalid") return new Error("Invalid input");

			return undefined;
		},
	})
	.task({
		handler(context) {
			logger.info(JSON.stringify(context, null, 4));
		},
	});
```

</p>
</details>

<details>
<summary><b>option({ key, name, description, defaultValue, skip, validate })</b></summary>
<p>

The `option` API defines a contextual CLI option.  
The option value can be accessed through its `key` property from the current context.

```ts
#!/usr/bin/env node

import { createLogger, termost } from "termost";
import { name, version } from "../package.json" with { type: "json" }; // Depending on your `package.json` location.

type ProgramContext = {
	optionWithAlias: number;
	optionWithoutAlias: string;
};

const logger = createLogger({ name: "my-cli" });

const program = termost<ProgramContext>({
	name,
	description: "CLI description",
	version,
});

program
	.option({
		key: "optionWithAlias",
		name: { long: "shortOption", short: "s" },
		description: "Useful CLI flag",
		defaultValue: 0,
	})
	.option({
		key: "optionWithoutAlias",
		name: "longOption",
		description: "Useful CLI flag",
		defaultValue: "defaultValue",
		validate(context) {
			if (context.optionWithoutAlias === "invalid") return new Error("Invalid input");

			return undefined;
		},
	})
	.task({
		handler(context) {
			logger.info(JSON.stringify(context, null, 2));
		},
	});
```

</p>
</details>

<details>
<summary><b>task({ key, label, handler, skip, validate })</b></summary>
<p>

The `task` executes a handler (either a synchronous or an asynchronous one).  
The output can be either:

- Displayed gradually if no `label` is provided
- Displayed until the promise is fulfilled if a `label` property is specified (in the meantime, a spinner with the label is showcased)

```ts
#!/usr/bin/env node

import { createLogger, exec, termost } from "../src";
import { name, version } from "../package.json" with { type: "json" }; // Depending on your `package.json` location.

type ProgramContext = {
	computedFromOtherTaskValues: "big" | "small";
	execOutput: string;
	size: number;
};

const logger = createLogger({ name: "task" });

const program = termost<ProgramContext>({
	name,
	description: "CLI description",
	version,
});

program
	.task({
		key: "size",
		label: "Task with returned value (persisted)",
		async handler() {
			return 45;
		},
	})
	.task({
		label: "Task with side-effect only (no persisted value)",
		async handler() {
			await wait(500);
			// @note: side-effect only handler
		},
	})
	.task({
		key: "computedFromOtherTaskValues",
		label: "Task can also access other persisted task values",
		handler(context) {
			if (context.size > 2000) {
				return Promise.resolve("big");
			}

			return Promise.resolve("small");
		},
		validate(context) {
			if (context.computedFromOtherTaskValues === "big") return new Error("Invalid input");

			return undefined;
		},
	})
	.task({
		key: "execOutput",
		label: "Or even execute external commands thanks to its provided helpers",
		handler() {
			return exec("echo 'Hello from shell'");
		},
	})
	.task({
		label: "A task can be skipped as well",
		async handler() {
			await wait(2000);

			return Promise.resolve("Super long task");
		},
		skip(context) {
			const needOptimization = context.size > 2000;

			return !needOptimization;
		},
	})
	.task({
		label: (context) =>
			`A task can have a dynamic label generated from contextual values: ${context.computedFromOtherTaskValues}`,
		async handler() {},
	})
	.task({
		handler(context) {
			logger.info(
				"output",
				`If you don't specify a label, the handler is executed in "live mode" (the output is not hidden by the label and is displayed gradually).`,
			);
			logger.info(
				"context",
				`A task with a specified "key" can be retrieved here. Size = ${context.size}. If no "key" was specified the task returned value cannot be persisted across program instructions.`,
			);
		},
	})
	.task({
		handler(context) {
			const content = "The logger helper can be used to display task content in a nice way";

				logger.info(content);
				logger.warn(content);
				logger.error(content);
				logger.success(content);
				logger.info("👋 You can also namespace the logger name");
			);

			console.info(JSON.stringify(context, null, 2));
		},
	});

const wait = (delay: number) => {
	return new Promise((resolve) => setTimeout(resolve, delay));
};
```

</p>
</details>

<br>

## 🤩 Built with Termost

- [Quickbundle](https://github.com/adbayb/quickbundle) The zero-configuration transpiler and bundler for the web.

<br>

## 💙 Acknowledgements

This project is built upon solid open-source foundations. We'd like to thank:

- [`@clack/prompts`](https://www.npmjs.com/package/@clack/prompts) for managing `input` and `task` internals.

<br>

## 📖 License

[MIT](https://github.com/adbayb/termost/blob/main/LICENSE "License MIT").

---
_Source: https://npm.io/package/termost · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
