askeroo
v0.0.0-alpha.16
Published
A modern CLI prompt library with flow control, history navigation, and conditional prompts
Maintainers
Readme
Askeroo
A modern CLI prompt library with flow control, back navigation, and conditional fields.
[!WARNING] This library is still in alpha and things might change before release.
Features
- Works great out of the box, with key prompts, like text, radio, multi
- Create your own bespoke prompts using a highly flexible framework
- Runs prompts in a flow which you can configure and customise to suit your needs
- Stateful navigation that preserves user's inputs and supports back navigation
- Return structured data your way with imperative-style functions
- Write dynamic branching with groups and conditionals
- Run tasks with progress tracking, parallel or sequential execution, and error handling
- Display notes with support for markdown and chalk syntax
- Cancel event listeners for cleanup when users exit with Ctrl+C
Installation
npm i askerooQuick Start
Askeroo comes with a default runtime and set of prompts that you can use out of the box.
import { ask, group, text, confirm, note } from "askeroo";
const flow = async () => {
// Display notes
await note("[Hello world!]{bgBlue}");
// Call prompts on their own
const nickname = await text({ label: "Nickname" });
// Group prompts together
const profile = await group(async () => {
const first = await text({ label: "First name" });
const last = await text({ label: "Last name" });
return { first, last };
});
// Create conditional inputs
const prefs = await group(
async () => {
const role = await text({ label: "Role (user/admin)" });
if (role === "admin") {
const code = await text({ label: "Access code" });
return { role, code };
}
const news = await confirm({ label: "Subscribe to newsletter?" });
return { role, news };
},
{ label: "Preferences" } // Optional group labels
);
// Return structured data your way
return { nickname, profile, prefs };
};
const result = await ask(flow);
console.log(result);How does Askeroo work?
Askeroo is built around the concept of a flow, where each prompt is executed in sequence. Some prompts can auto-submit, while others wait for user input, and each prompt can appear in different states—before activation, while active, and after completion. Prompts are also dynamic: they can be updated or changed at any point within your flow function.
When you call ask(flow), Askeroo sets up a runtime that runs your flow function multiple times using a replay mechanism. This allows for a smooth and interactive experience:
- First run: The flow executes, prompts are shown one by one, and answers are stored.
- User navigates back: The runtime replays the flow, automatically filling in previous answers.
- User changes an answer: The runtime clears any answers that came after the changed prompt and continues from there.
- Repeat: This process continues until the user completes the flow.
From the user's perspective, this feels like a seamless way to move back and forth between prompts. Behind the scenes, Askeroo is replaying your flow function as needed to determine which prompts should be shown at each step.
// Your flow function runs multiple times
const flow = async () => {
const role = await radio({
label: "Select your role",
options: ["Developer", "Designer"],
});
if (role === "Developer") {
const language = await radio({
label: "Preferred language",
options: ["TypeScript", "JavaScript"],
});
return { role, language };
}
const tool = await radio({
label: "Design tool",
options: ["Figma", "Sketch"],
});
return { role, tool };
};Flow:
Select your role: Developer
↓
Preferred language: TypeScript
↓
User presses back ⬆
↓
Select your role: Designer
↓
Design tool: Figma
↓
CompleteThe flow replays automatically when the user navigates back, clearing subsequent answers and showing the appropriate prompts based on the new selection.
Running Flows
Run prompt flows
ask(flow: FlowFunction, options: FlowOpts): Promise<T>Executes a prompt flow with replay and navigation support.
const result = await ask(async () => { const name = await text({ message: "Name", }); return { name }; });Options
interface FlowOpts { allowBack?: boolean; }Flow API
The flow function receives an API object with the following properties:
onCancel(callback: () => void): Register a callback to be called when the flow is cancelled (e.g., via Ctrl+C)
const result = await ask(async ({ text, confirm, onCancel }) => { // Register cleanup callback onCancel(() => { console.log("Flow cancelled! Cleaning up..."); // Close connections, remove temp files, etc. }); const name = await text({ label: "Name" }); const confirmed = await confirm({ label: "Confirm?" }); return { name, confirmed }; });Cancellation Behavior:
- First Ctrl+C: Triggers
onCancelcallbacks for graceful cleanup. A hint message"(Press Ctrl+C again to force quit)"is displayed. - Second Ctrl+C: Forces immediate exit with
process.exit(1), bypassing any cleanup. Useful if cleanup is stuck or taking too long.
Multiple cancel callbacks can be registered, and they will all be called when the flow is cancelled:
await ask(async ({ onCancel }) => { // Create temp file const tempFile = "temp.json"; fs.writeFileSync(tempFile, "{}"); // Register multiple cleanup callbacks onCancel(() => { console.log("Removing temp file..."); fs.unlinkSync(tempFile); }); onCancel(() => { console.log("Closing database connection..."); db.close(); }); // ... rest of flow });Group prompts
group(flow: () => Promise<T>, options: GroupOpts): Promise<T>Group prompts visually and control their behaviour together.
Prompts
text(options: TextOpts)Show a text input.
confirm(options: ConfirmOpts)Show a confirmation with choice of yes or no.
radio(options: RadioOpts)Show a single-choice selection from multiple options.
multi(options: MultiOpts)Show a multi-choice selection allowing multiple options.
note(MarkdownString)Show a note using markdown.
component(options: ComponentOpts)Render a React component using Ink.
tasks(taskList: Task[], options?: TasksOpts)Execute a list of tasks with progress indication and error handling.
Create Custom Prompts
Askeroo uses automatic plugin registration - when you create a prompt with createPrompt, it registers itself when imported. This means:
- ✅ No manual registration needed
- ✅ Only bundle the prompts you actually use
- ✅ Custom prompts work exactly like built-in ones
import React, { useState } from "react";
import { Text, useInput } from "ink";
import { createPrompt } from "askeroo";
// Define your options interface
export interface CustomOptions {
label: string;
placeholder?: string;
}
// Create and export the plugin - it auto-registers when imported!
export const customField = createPrompt<CustomOptions, string>({
type: "custom-field",
component: ({ node, options, events }: any) {
const [value, setValue] = useState("");
useInput((input, key) => {
if (key.return) {
events.onSubmit?.(value);
} else if (input) {
setValue((prev) => prev + input);
}
});
// Handle different states
if (node.state === "completed") {
return (
<Text>
{options.label}: <Text color="blue">{node.completedValue}</Text>
</Text>
);
}
if (node.state === "disabled") {
return <Text dimColor>{options.label}: ...</Text>;
}
return (
<Text>
{options.label}: {value}
</Text>
);
}
});Usage
import { ask } from "askeroo";
import { customField } from "./custom-field.js"; // Auto-registers when imported!
const result = await ask(async () => {
const input = await customField({
label: "Enter something",
});
return { input };
});Examples
Run the included example:
npm run exampleTry npm run build && npm run example test-run
Development
npm install
npm run build
npm run dev # Watch modeLicense
MIT
