@x12i/ask-cli
Accept phrases like "install, build and test all @x12i packages" and resolve them to structured command plans — deterministic catalog matching, offline, testable.
import { createAskCli } from "@x12i/ask-cli"; const askCli = createAskCli({ catalog: [{ id: "xnpm.install.all", phrases: ["install all packages", "install everything"], command: { base: "xnpm", args: [] }, approval: "never", }], }); const result = await askCli.resolve({ input: "install everything", context: { commandName: "xnpm" }, }); if (result.matched && result.command) { // validate, prompt for approval if needed, execute }
npm install @x12i/ask-cli
Every call to resolve() runs the same pipeline. No network, no model, no state — pure function, fully testable.
& → and, everything → all, action reorder{scope}, {repoName}, etc. Extract and validate valuesThe library resolves. The host CLI validates, confirms, and executes. That boundary is intentional — it means the library is testable in isolation and your CLI keeps full control.
Handles the real-world mess of user input: mixed case, punctuation, synonyms (everything / all), conjunction variants (& / and), and action word reordering.
Exact phrase matching runs first for speed and confidence. Slot patterns handle parameterized input like "all @x12i packages" where @x12i is a typed {scope} slot.
Five built-in types: scope, packageName, repoName, enum, string. Each validates on extraction — unknown or malformed values fail the match cleanly.
Each catalog record declares its approval policy: never, always, or when-destructive. The host CLI reads this and renders the confirmation prompt — the library never prompts directly.
Unmatched input returns a structured miss result with suggestions from the catalog. It never falls back to arbitrary command execution. No match = no action, always.
Pass a validate hook to run your own checks on the resolved command before it's returned — check context, permissions, state — without forking the library.
No side effects. resolve() is a pure async function. Pass a catalog, pass an input string, assert the result. No mocking, no CLI harness, no process spawning.
Optional pre-match alias map expands shorthand consistently before matching — "x12i packages" → "all @x12i/* packages" — so your catalog stays clean.
Fully typed catalog records, resolve results, slot values, and hooks. AskCliCatalogRecord and AskCliResolveResult are exported for use in host CLI types.
Each catalog record maps one intent to phrases, a resolved command, human-readable explanation, and an approval policy. The library owns the matching; your CLI owns what happens next.
{ id: "xnpm.publish.scope", intent: "publish", phrases: [ "publish all {scope} packages", "release {scope} and push", ], slots: { scope: { type: "scope" }, }, command: { base: "xnpm", args: ["--filter", "{scope}/*", "--build", "--test", "--publish", "--push"], }, explanation: [ "Select packages matching {scope}/*", "Run install, build, and test", "Publish in dependency-aware order", "Push git changes", ], risks: ["Publish is irreversible", "Push is irreversible"], approval: "always", }
Built-in slot types
| Type | Validates |
|---|---|
| scope | npm scope — must start with @ |
| packageName | valid npm package name, scoped or unscoped |
| repoName | alphanumeric + hyphens, no slashes |
| enum | value must be in the declared allowed list |
| string | any non-empty string — least restrictive |
Approval policy
| Value | When the host should prompt |
|---|---|
| never | safe to run immediately — no confirmation needed |
| always | always ask, regardless of what the command does |
| when-destructive | prompt only when the resolved command touches publish, push, or remote creation |
ask subcommands.
Any CLI that wants a natural-language entry point can ship one catalog-backed ask subcommand. The library handles the resolution; each CLI handles validation and execution.
// 1. resolve — library's job const result = await askCli.resolve({ input, context }); if (!result.matched) { showSuggestions(result.suggestions); // host renders return; } // 2. explain + confirm — host CLI's job showPlan(result.explanation, result.risks); if (requiresApproval(result.approval)) { const ok = await prompt("Execute? [y/N]"); if (!ok) return; } // 3. execute — host CLI's job await run(result.command);
Install as a dependency in any Node.js CLI package. No peer dependencies, no runtime requirements beyond Node.js ≥ 20.
Node.js ≥ 20 required · npmjs.com/package/@x12i/ask-cli ↗