Code Monkey home page Code Monkey logo

termost's Introduction


๐Ÿ’ป Termost

Get the most of your terminal


โœจ Features

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

  • Fluent syntax to express your CLI configurations with instructions such as:
  • 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 message helpers...)

๐Ÿš€ Quickstart

Install the library:

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

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

#!/usr/bin/env node

import { helpers, termost } from "termost";

type ProgramContext = {
	globalFlag: string;
};

type DebugCommandContext = {
	localFlag: string;
};

const program = termost<ProgramContext>("CLI example", {
	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",
});

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) {
			helpers.message(`Hello, I'm the ${argv.command} command`);
			helpers.message(`Context value = ${JSON.stringify(context)}`);
			helpers.message(`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 Global help
bin-name debug --help Local help
bin-name build Subcommand with task example
bin-name debug Subcommand with option and context example

โœ๏ธ Usage

Here's an API overview:

command({ name, description })

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.

#!/usr/bin/env node

import { termost, helpers } from "termost";

const program = termost("Example to showcase the `command` API");

program
	.command({
		name: "build",
		description: "Transpile and bundle in production mode",
	})
	.task({
		handler(context, argv) {
			helpers.message(`๐Ÿ‘‹ Hello, I'm the ${argv.command} command`);
		},
	});

program
	.command({
		name: "watch",
		description: "Rebuild your assets on any code change",
	})
	.task({
		handler(context, argv) {
			helpers.message(`๐Ÿ‘‹ Hello, I'm the ${argv.command} command`, {
				type: "warning",
			});
		},
	});

input({ key, label, type, skip, ...typeParameters })

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

#!/usr/bin/env node

import { termost, helpers } from "termost";

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

const program = termost<ProgramContext>("Example to showcase the `input` API");

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);
		},
	})
	.task({
		handler(context) {
			helpers.message(JSON.stringify(context, null, 4));
		},
	});

option({ key, name, description, defaultValue, skip })

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

#!/usr/bin/env node

import { termost, helpers } from "termost";

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

const program = termost<ProgramContext>("Example to showcase the `option` API");

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",
	})
	.task({
		handler(context) {
			helpers.message(JSON.stringify(context, null, 2));
		},
	});

task({ key, label, handler, skip })

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)
#!/usr/bin/env node

import { helpers, termost } from "../src";

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

const program = termost<ProgramContext>("Example to showcase the `task` API");

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");
		},
	})
	.task({
		key: "execOutput",
		label: "Or even execute external commands thanks to its provided helpers",
		handler() {
			return helpers.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) {
			helpers.message(
				`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).`,
				{ label: "Label & console output" },
			);

			helpers.message(
				`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.`,
				{ label: "Context management" },
			);
		},
	})
	.task({
		handler(context) {
			const content =
				"The `message` helpers can be used to display task content in a nice way";

			helpers.message(content, {
				label: "Output formatting",
			});
			helpers.message(content, { type: "warning" });
			helpers.message(content, { type: "error" });
			helpers.message(content, { type: "success" });
			helpers.message(content, {
				type: "information",
				label: "๐Ÿ‘‹ You can also customize the label",
			});
			helpers.message(
				["I support also", "multilines", "with array input"],
				{
					type: "information",
					label: "๐Ÿ‘‹ You can also customize the label",
				},
			);
			console.log(
				helpers.format(
					"\nYou can also have a total control on the formatting through the `format` helper.",
					{
						color: "white",
						modifiers: ["italic", "strikethrough", "bold"],
					},
				),
			);

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

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


๐Ÿคฉ Built with Termost

  • Quickbundle The zero-configuration bundler powered by ESBuild

๐Ÿ’™ Acknowledgements

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

  • enquirer for managing input internals
  • listr2 for managing task internals

๐Ÿ“– License

MIT

termost's People

Contributors

adbayb avatar dependabot[bot] avatar github-actions[bot] avatar renovate[bot] avatar semantic-release-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

dorucioclea

termost's Issues

Bug: "unable to retrieve automatically the package name and version"

Description

Getting the error:

error: Termost was unable to retrieve automatically the package name and version. To fix it, use `termost({ name, description, version })` to define them manually.
      at I (/$bunfs/root/exporter:21735:11)
      at Z (/$bunfs/root/exporter:21704:47)
      at /$bunfs/root/exporter:22507:15

But when I try to do that I get a TypeScript error and the same error above regardless:

CleanShot 2024-02-24 at 16 32 14@2x

Reproduction

Gist

Environment

Bun 1.0.28
Node 18.17.1

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore: lock file maintenance

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/integrate.yml
  • actions/checkout v3
  • pnpm/action-setup v2
  • actions/setup-node v3
  • actions/checkout v3
  • pnpm/action-setup v2
  • actions/setup-node v3
  • actions/checkout v3
  • pnpm/action-setup v2
  • actions/setup-node v3
.github/workflows/pull_request.yml
  • amannn/action-semantic-pull-request v5
  • marocchino/sticky-pull-request-comment v2
  • marocchino/sticky-pull-request-comment v2
.github/workflows/release.yml
  • actions/checkout v3
  • pnpm/action-setup v2
  • actions/setup-node v3
  • changesets/action v1
npm
examples/command/package.json
  • termost ^0.9.0
  • esbuild-register 3.4.2
examples/default/package.json
  • termost ^0.9.0
  • esbuild-register 3.4.2
examples/input/package.json
  • termost ^0.9.0
  • esbuild-register 3.4.2
examples/option/package.json
  • termost ^0.9.0
  • esbuild-register 3.4.2
examples/task/package.json
  • termost ^0.9.0
  • esbuild-register 3.4.2
package.json
  • @adbayb/eslint-config 1.0.0
  • @adbayb/prettier-config 1.0.0
  • @adbayb/stack 1.4.0
  • @adbayb/ts-config 1.0.0
  • @changesets/changelog-github 0.4.8
  • @changesets/cli 2.26.2
  • @commitlint/cli 17.7.1
  • @commitlint/config-conventional 17.7.0
  • eslint 8.47.0
  • prettier 3.0.1
  • turbo 1.10.12
  • typescript 5.1.6
  • vitest 0.34.1
  • node >=18.0.0
  • pnpm >=8.0.0
  • pnpm 8.6.12
termost/package.json
  • chalk ^4.1.2
  • listr2 ^4.0.5
  • prompts ^2.4.2
  • @types/node 18.17.5
  • @types/prompts 2.4.4
  • quickbundle 1.2.0
  • typescript 5.1.6
  • vitest 0.34.1
nvm
.nvmrc
  • node v18.17.1

  • Check this box to trigger a request for Renovate to run again on this repository

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.