Termost

repository·main·Indexed 41 days ago

https://github.com/adbayb/termost

A fluent, type-safe TypeScript library for building command-line interfaces (CLIs). Termost simplifies subcommand management, interactive user inputs, option parsing, and task execution with built-in support for shared context and formatted logging. Version 2.0.0 migrates prompt and task runner internals to @clack/prompts.

Tokens
8.4K
Snippets
33
Records
38
Agent score
51%

What's inside Termost

  1. How widgets and connectors work together

    main

    Termost uses a fluent interface to build CLI tools. You initialize a program using termost<T>(), where T is your ProgramContext type. You then chain instructions like .option(), .command(), .input(), and .task() to define the CLI behavior.

    Key concepts:

    • Context: A shared object that persists values across instructions. Values are stored using the key provided in options, inputs, or tasks.
    • Subcommands: Created via .command(). Subcommands have their own scoped context, but they can access the root command's context.
    • Tasks: The execution units of your CLI. They can perform side effects or return values that are persisted into the context if a key is provided.
    • Instructions: Each method call (option, input, task) adds a step to the CLI lifecycle.
    const program = termost<ProgramContext>({
    	name: 'my-cli',
    	description: 'CLI description',
    	version: '1.0.0',
    });
    
    program
    	.option({ key: 'flag', name: 'f', description: 'desc' })
    	.task({ handler: () => {} });
  2. Migrate from termost 1.x.x to 2.0.0

    main

    When upgrading from version 1.x.x to 2.0.0, several breaking changes require manual updates to your implementation. The underlying prompt and task runner libraries have changed from enquirer and listr2 to @clack/prompts. While high-level APIs like input, option, and task should remain functional without changes, the helpers namespace has been removed, and logging mechanisms have been unified.

    npm install termost@2.0.0
  3. Expose instruction output to Context using InstructionKey

    main

    By default, the return value of an instruction is not automatically added to the command's Context. To make an instruction's output available in the Context object for subsequent instructions, you must use the InstructionKey pattern by providing a key in the return object.

    export type InstructionKey<Key> = {
    	/**
    	 * Makes the method output available in the context object. By default, if no provided key, the
    	 * output is not included in the context.
    	 */
    	key: Key;
    };
  4. Build a CLI using the Termost fluent interface

    main

    The Termost type defines a fluent interface used to build CLI commands, inputs, options, and tasks. You can chain these methods to define the structure and behavior of your CLI.

    Available Methods

    • command(parameters): Attaches a new sub-command to the program. This returns a new Termost instance that includes the values defined by the command, allowing for type-safe composition.
    • input(parameters): Adds an interactive input instruction. This marks the command as having interactive instructions (hasInteractiveInstruction: true).
    • option(parameters): Adds a command-line option. Unlike input or task, adding an option does not mark a command as having interactive instructions.
    • task(parameters): Adds a task instruction to the command.

    Each method returns this (the Termost instance) to allow for chaining, except for command, which returns a specialized Termost type to support type-safe value accumulation.

    const cli = termost({ name: 'app', description: 'desc' })
      .command({ name: 'serve', description: 'start server' })
      .option({ name: 'port', description: 'port to listen on' })
      .input({ name: 'username', description: 'your name' })
      .task({ name: 'deploy', description: 'deploying...' });
  5. Use `node:util` for text styling instead of `helpers.format`

    main

    In version 2.0.0, helpers.format is removed. For text styling (such as bold or italic), use the styleText function from the built-in node:util module.

    - import { format } from "termost";
    - console.log(format.bold("My styled message"));
    + import { styleText } from "node:util";
    + console.log(styleText(["bold", "italic"], "My styled message"));
  6. Replace the `helpers` namespace with direct exports

    main

    In version 2.0.0, the helpers namespace is removed. You must now import createLogger and exec directly from the termost package.

    - import { helpers, termost } from "termost";
    + import { createLogger, exec, termost } from "termost";
    
    - const log = helpers.createLogger("my-cli");
    + const log = createLogger({ name: "my-cli" });
    
    - helpers.exec("echo 'Hello from shell'");
    + exec("echo 'Hello from shell'");
  7. Use createLogger for formatted output

    main

    Termost provides a createLogger helper to manage stdout/stderr with consistent formatting and namespacing.

    import { createLogger } from "termost";
    
    const logger = createLogger({ name: "my-cli" });
    
    logger.info("Info message");
    logger.warn("Warning message");
    logger.error("Error message");
    logger.success("Success message");
  8. Create subcommands with command()

    main

    The command({ name, description }) API creates a new subcommand context. The root command context is shared across all subcommands, but subcommand contexts are scoped and cannot be accessed by other subcommands.

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

    main

    The helpers.message and helpers.format methods are no longer available in version 2.0.0. They have been replaced by a unified logger built on @clack/prompts. To log messages, instantiate a logger using createLogger and call the appropriate level method (e.g., .info()).

    - helpers.message("info", "Hello world");
    + const log = createLogger({ name: "my-cli", level: "info" });
    + log.info("Hello world");
  10. Define CLI options with option()

    main

    The option({ key, name, description, defaultValue, skip, validate }) API defines a contextual CLI option. The value provided by the user is persisted in the program context using the provided key.

    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;
    		},
    	});
  11. Create interactive prompts with input()

    main

    The input({ key, label, type, skip, validate, ...typeParameters }) API creates an interactive prompt. Supported types include select, multiselect, confirm, and text.

    Values are persisted to the context via the key property.

    program
    	.input({
    		type: "select",
    		key: "input1",
    		label: "What is your single choice?",
    		options: ["singleOption1", "singleOption2"],
    		defaultValue: "singleOption2",
    	})
    	.input({
    		type: "text",
    		key: "input4",
    		label: (context) => `Dynamic label: ${context.input1}`,
    		defaultValue: "Empty input",
    		skip(context) {
    			return Boolean(context.input3);
    		},
    		validate(context) {
    			if (context.input4 === "invalid") return new Error("Invalid input");
    			return undefined;
    		},
    	});