Skip to content

Latest commit

 

History

History
2072 lines (1591 loc) · 77 KB

File metadata and controls

2072 lines (1591 loc) · 77 KB

API Reference

Detailed reference for functions and types provided by politty.

Functions

defineCommand

Defines a command.

// Simplified for readability: real overloads take TArgsSchema, TResult, and
// TGlobalArgs, and infer the resolved args passed to setup/run/cleanup from
// them — there's no user-facing generic for it, so it's shown as `unknown`.
function defineCommand<TArgsSchema, TResult>(config: {
  name: string;
  description?: string;
  args?: TArgsSchema;
  aliases?: string[];
  subCommands?: SubCommandsRecord;
  setup?: (context: SetupContext) => void | Promise<void>;
  run?: (args: unknown) => TResult;
  cleanup?: (context: CleanupContext) => void | Promise<void>;
  notes?: string;
}): Command<TArgsSchema, unknown, TResult>;

Parameters

Name Type Description
config object Command configuration

config properties:

Property Type Description
name string Command name (required)
description? string Command description
args? TArgsSchema Argument schema (Zod schema)
aliases? string[] Alternative names for the command
subCommands? SubCommandsRecord Subcommands (supports lazy loading)
setup? (context: SetupContext) => void | Promise<void> Initialization hook
run? (args: unknown) => TResult Main process
cleanup? (context: CleanupContext) => void | Promise<void> Cleanup hook
notes? string Additional notes (shown in help and docs)
examples? Example[] Usage examples (shown in help and docs)

Example

import { z } from "zod";
import { defineCommand, arg } from "politty";

const command = defineCommand({
  name: "my-cli",
  description: "CLI tool description",
  args: z.object({
    input: arg(z.string(), { positional: true }),
  }),
  setup: ({ args }) => {
    /* initialization */
  },
  run: (args) => {
    /* main process */
  },
  cleanup: ({ args, error }) => {
    /* cleanup */
  },
});

runMain

Executes a command as the CLI entry point. Signal handling (SIGINT, SIGTERM) is automatically enabled, and process.exit is called on termination.

async function runMain(command: Command, options?: MainOptions): Promise<never>;

Parameters

Name Type Description
command Command Command to execute
options MainOptions Execution options (optional)

Return Value

Promise<never> - This function does not return as it calls process.exit.

Example

import { defineCommand, runMain } from "politty";

const command = defineCommand({
  name: "my-cli",
  run: () => console.log("Hello!"),
});

// Basic usage
runMain(command);

// With version
runMain(command, { version: "1.0.0" });

// Debug mode
runMain(command, { version: "1.0.0", debug: true });

runCommand

Executes a command programmatically. Ideal for testing purposes. Does not call process.exit and does not handle signals.

async function runCommand<TResult>(
  command: Command,
  argv: string[],
  options?: RunCommandOptions,
): Promise<RunResult<TResult>>;

Parameters

Name Type Description
command Command Command to execute
argv string[] Command-line arguments
options RunCommandOptions Execution options (optional)

Return Value

Promise<RunResult<TResult>> - Execution result

Example

import { defineCommand, runCommand } from "politty";

const command = defineCommand({
  name: "my-cli",
  run: () => ({ success: true }),
});

// Usage in tests
const result = await runCommand(command, ["--verbose", "input.txt"]);
console.log(result.exitCode);
console.log(result.result);

arg

Attaches metadata to a Zod schema.

function arg<T extends z.ZodType>(schema: T, meta: ArgMeta): T;

Parameters

Name Type Description
schema z.ZodType Zod schema
meta ArgMeta Argument metadata

Return Value

The same Zod schema (chainable)

Example

import { z } from "zod";
import { arg } from "politty";

// Positional argument
const input = arg(z.string(), {
  positional: true,
  description: "Input file",
});

// Option with alias
const verbose = arg(z.boolean().default(false), {
  alias: "v",
  description: "Verbose output",
});

// Option with placeholder
const output = arg(z.string(), {
  alias: "o",
  description: "Output file",
  placeholder: "FILE", // Shows as --output <FILE> in help
});

generateHelp

Generates help text for a command.

function generateHelp(command: Command, options: HelpOptions): string;

Parameters

Name Type Description
command Command Command to generate help for
options HelpOptions Help generation options

Return Value

Formatted help text


generateHelpData

Generates a structured, JSON-serializable representation of a command's help — the machine-readable counterpart of generateHelp. Contains the same field metadata (name, description, positionals, options, discriminated-union/union variants, global options, examples, notes) as plain data with no ANSI styling, plus the full recursive subcommand tree (there is no --help/--help-all depth distinction for structured help). This is what backs the --help-json flag that runMain/runCommand handle automatically; call it directly only when building your own help/docs tooling.

function generateHelpData(command: Command, options?: HelpDataOptions): HelpData;

Parameters

Name Type Description
command Command Command to generate help data for
options HelpDataOptions { descriptions?, context?, agentHelp? }

Return Value

A HelpData object. lazy() subcommands are resolved from their synchronous meta without loading them; a legacy () => Promise<Command> subcommand (registered without lazy()) appears as { name, unresolved: true } since no synchronous metadata exists for it.

Example

$ my-cli --help-json
{"name":"my-cli","commandPath":[],"description":"...","usage":{...},"options":[...],"subcommands":[...]}

extractFields

Extracts field information from a schema.

function extractFields(schema: ArgsSchema): ExtractedFields;

Example

import { z } from "zod";
import { extractFields, arg } from "politty";

const schema = z.object({
  name: arg(z.string(), { positional: true }),
  verbose: arg(z.boolean().default(false), { alias: "v" }),
});

const extracted = extractFields(schema);
// extracted.fields contains information about each field

enableCompileCache

Enables the Node.js on-disk compile cache (V8 code cache) for the current process. Every politty package exports it from its own dependency-free compile-cache subpath, so a minimal bin shim can call it before the real CLI graph is imported — import it from the package your CLI depends on. Never throws; no-ops on runtimes without module.enableCompileCache (Node.js < 22.8.0). The NODE_COMPILE_CACHE environment variable always takes precedence over the derived directory.

runMain calls this automatically (see MainOptions.compileCache), but only modules imported after that point benefit — see Faster Startup (Compile Cache) for the bin-shim pattern that covers the whole CLI.

function enableCompileCache(options?: string | { programName?: string; cacheDir?: string }): {
  enabled: boolean;
  directory?: string;
};

Example

#!/usr/bin/env node
// bin.ts — keep this file's static imports minimal.
// Replace the placeholder with the politty package your CLI depends on.
import { enableCompileCache } from "<your-politty-package>/compile-cache";

enableCompileCache("my-cli");
// => cache in ${XDG_CACHE_HOME:-$HOME/.cache}/my-cli/node-compile-cache
await import("./cli.js");

generateCompileCacheShim

Generates one compile-cache bin shim per bin entry (or per explicit path) as executable files, so they can be produced by a postbuild/prepack script instead of living in source. Also available as the politty generate-shim CLI command.

All options are optional: the output paths default to the bin paths in the nearest package.json, each entry to the first of ./cli.js, ./cli.mjs, ./index.js, ./index.mjs existing next to its shim, and each program name to the shim's bin name (falling back to the package name without its scope — with a warning when an explicit out path cannot be matched to a bin entry; pass program to override). Multiple entry values pair in order with the bin entries, or with out values of the same count. Refuses to overwrite an existing file it did not generate. The generated shims are ES modules: a .js output requires a "type": "module" package; use .mjs otherwise.

The specifier the shim imports enableCompileCache from is the compile-cache subpath of the politty package this function was reached through: each package exports its own binding, so the shim names the package you imported. Importing it means that package is installed, so the specifier resolves from the shim it writes into your package. Pass compileCacheSpecifier when the helper is reached some other way (a re-export from your own package, an import map, ...).

function generateCompileCacheShim(options?: {
  /** Specifier(s) the shim imports, relative to the shim file (default: conventional built module next to each shim) */
  entry?: string | string[];
  /** Output path(s); count must match entry when both are given (default: bin paths in package.json) */
  out?: string | string[];
  /** Program name for the cache directory, applied to all shims (default: derived per shim from package.json) */
  program?: string;
  /** Base directory for paths and package.json (default: process.cwd()) */
  cwd?: string;
  /** Specifier the shim imports enableCompileCache from (default: the compile-cache subpath of the politty package this function came from) */
  compileCacheSpecifier?: string;
}): Array<{
  outputPath: string;
  program: string;
  entry: string;
  compileCacheSpecifier: string;
}>;

Example

# package.json: "bin": { "my-cli": "./dist/bin.js" },
#               "postbuild": "politty generate-shim --entry ./cli.js"
politty generate-shim --entry ./cli.js
# => Generated compile-cache shim: dist/bin.js (program: my-cli, entry: ./cli.js,
#    compile-cache: <the politty package providing this bin>/compile-cache)

# Multiple bins: one --entry per bin, paired in declaration order
politty generate-shim --entry ./cli-a.js --entry ./cli-b.js

detectAgent

Detects whether the process runs under an AI coding agent. Exported from the agent subpath of every politty package (e.g. politty/agent, @politty/valibot/agent). runMain/runCommand call it once per run to drive agentHelp, args.$agent and the global setup/cleanup agent.

function detectAgent(
  env?: Record<string, string | undefined>, // default: process.env
  options?: {
    isTTY?: boolean; // default: process.stdout.isTTY
    fileExists?: (path: string) => boolean; // default: fs.existsSync
  },
): AgentInfo | undefined;

Detection follows the language-agnostic spec of vercel/detect-agent (vendored agents.json, Apache-2.0):

  1. POLITTY_NO_AGENT set to any non-empty value → undefined. Use it when a human types into an agent's terminal and wants the normal help.
  2. AI_AGENT non-empty after trimming → an agent, with the trimmed value as rawId.
  3. The spec's agents are checked in order and the first match wins (e.g. CLAUDECODE=1 → claude_code, CODEX_THREAD_ID → codex_cli, CURSOR_AGENT → cursor-cli).

id is normalized to the spec's name even when AI_AGENT carries a custom value: AI_AGENT itself when it is a known name, otherwise the first spec agent whose markers match, falling back to the custom AI_AGENT value when none does. rawId is what the detect-agent spec itself reports. Claude Code, for example, sets AI_AGENT=claude-code_<version>_agent alongside CLAUDECODE=1, so id is "claude_code" while rawId is the versioned value.

It is synchronous and never throws.

import { detectAgent } from "politty/agent";

detectAgent({ CLAUDECODE: "1" }); // { id: "claude_code", rawId: "claude_code" }
detectAgent({ AI_AGENT: "claude-code_2-1-284_agent", CLAUDECODE: "1" });
// => { id: "claude_code", rawId: "claude-code_2-1-284_agent" }
detectAgent({ AI_AGENT: "my-agent" }); // { id: "my-agent", rawId: "my-agent" }

Branch on a specific agent with id (agent?.id === "claude_code"). Spec names may change when upstream renames them.


Shell Completion

withCompletionCommand

Wraps a command with shell completion support. Adds a completion subcommand, a hidden __complete command for dynamic completion, and a hidden __refresh-completion command used by the on-disk cache auto-refresh path. See Shell Completion for details on the refresh flow and the POLITTY_NO_COMPLETION_REFRESH opt-out.

function withCompletionCommand<T extends AnyCommand>(
  command: T,
  options?: string | WithCompletionOptions,
): T;

Parameters

Name Type Description
command AnyCommand Command to wrap
options string | WithCompletionOptions Program name or options object

WithCompletionOptions:

Property Type Description
programName string? Override program name (defaults to command.name)
globalArgsSchema ArgsSchema? Global args schema for deriving global options in completion
cacheDir string? Hardcode the on-disk cache directory used by the rc loader and the runMain background refresh (overrides XDG default)
programVersion string? Program version embedded in the script header
bundledWorker BundledWorkerOptions? Package-relative bundled worker lookup configuration for dispatcher mode

Example

import { defineCommand, runMain, withCompletionCommand } from "politty";

const mainCommand = withCompletionCommand(
  defineCommand({
    name: "mycli",
    subCommands: {/* ... */},
  }),
);

// Now includes:
// - mycli completion bash|zsh|fish [--install] [--loader] [--instructions]
// - mycli __complete --shell <shell> -- <args>
// - mycli __refresh-completion <shell>  (hidden; spawned by the rc loader / runMain hook)
// - mycli __completion-worker-path <shell>  (hidden; prints a bundled worker path)

runMain(mainCommand);

generateCompletion

Generates a shell completion script for a command.

function generateCompletion(command: AnyCommand, options: CompletionOptions): CompletionResult;

Parameters

Name Type Description
command AnyCommand Command to generate
options CompletionOptions Generation options

CompletionOptions:

Property Type Description
shell ShellType Target shell: "bash", "zsh", or "fish"
programName string Program name as invoked
includeSubcommands boolean? Include subcommand completions (default: true)
includeDescriptions boolean? Include descriptions (default: true)
mode CompletionMode? Defaults to static (self-contained) for this direct API. Pass dispatcher for runtime resolution — it requires the CLI to register the hidden __complete / __refresh-completion commands via withCompletionCommand / createCompletionCommand. The completion <shell> subcommand defaults to dispatcher.
globalArgsSchema ArgsSchema? Global args schema for deriving global options in completion
binPath string? Path to the binary whose mtime is the freshness signature (defaults to process.argv[1])
programVersion string? Program version embedded in the script header
cacheDir string? Cache directory hardcoded into the generated rc loader (defaults to XDG cache dir resolved at runtime)
bundledWorker BundledWorkerOptions? Package-relative bundled worker lookup configuration for dispatcher mode

Return Value

interface CompletionResult {
  script: string; // The completion script
  shell: ShellType; // Shell type
  installInstructions: string; // Installation instructions
}

Example

import { generateCompletion } from "politty/completion";

const result = generateCompletion(command, {
  shell: "bash",
  programName: "mycli",
});

console.log(result.script);

This direct API emits a self-contained static script by default. For runtime dispatcher completion, pass mode: "dispatcher" and make sure the command tree has the hidden runtime commands registered (use withCompletionCommand / createCompletionCommand, which the completion <shell> subcommand relies on).


generateBundledCompletionWorker

Generates a bundled worker artifact for dispatcher completion and validates that it is usable by the built CLI package.

function generateBundledCompletionWorker(
  options: GenerateBundledCompletionWorkerOptions,
): Promise<GenerateBundledCompletionWorkerResult>;

Parameters

Property Type Description
bin string CLI binary or built JS entry file to invoke
programName string Program name expected in worker metadata
shell ShellType Worker shell to generate
outputPath string? Output path, defaulting to dist/completion/<shell>-worker.<ext>
verify boolean? Also verify that __completion-worker-path <shell> resolves to outputPath
cwd string? Working directory for relative paths
env Record<string, string | undefined>? Extra environment passed to the target binary
quiet boolean? Suppress the success message

Example

import { generateBundledCompletionWorker } from "politty/completion";

await generateBundledCompletionWorker({
  bin: "dist/cli/index.mjs",
  programName: "mycli",
  shell: "zsh",
  verify: true,
});

The package-script equivalent is:

politty generate-worker --bin dist/cli/index.mjs --program mycli --shell zsh --verify

createDynamicCompleteCommand

Creates the hidden __complete command for dynamic completion.

function createDynamicCompleteCommand(
  rootCommand: AnyCommand,
  programName?: string,
  globalArgsSchema?: ArgsSchema,
): Command;

Usage

The __complete command is automatically added by withCompletionCommand. It can be invoked directly:

# Get completions for "mycli build --"
mycli __complete --shell fish -- build --

# Output (tab-separated: value\tdescription)
--watch	Watch mode
--output	Output directory
:4

The last line (:N) is a directive that tells the shell how to handle completions:

  • :0 - Default
  • :4 - Filter by prefix
  • :16 - File completion
  • :32 - Directory completion

parseCompletionContext

Parses a partial command line to determine what kind of completion is needed.

function parseCompletionContext(
  argv: string[],
  rootCommand: AnyCommand,
  globalArgsSchema?: ArgsSchema,
): CompletionContext;

Return Value

interface CompletionContext {
  subcommandPath: string[]; // e.g., ["plugin", "add"]
  currentCommand: AnyCommand; // Resolved command
  currentWord: string; // Current partial word
  previousWord: string; // Previous word
  completionType: CompletionType; // What to complete
  targetOption?: CompletableOption; // For option-value completion
  positionalIndex?: number; // For positional completion
  options: CompletableOption[]; // Available options
  subcommands: string[]; // Available subcommands
  positionals: CompletablePositional[];
  usedOptions: Set<string>; // Already used options in the current frame (cleared on subcommand descent)
  hasAnyOptionBeenUsed?: boolean; // Any option used anywhere in the invocation (never cleared)
  parsedArgs: Record<string, unknown>; // Other arg values (for dynamic resolvers)
  previousValues: string[]; // Prior values for the option/positional being completed
}

type CompletionType = "subcommand" | "option-name" | "option-value" | "positional";

generateCandidates

Generates completion candidates based on context. Async because dynamic resolvers may return promises.

function generateCandidates(
  context: CompletionContext,
  options: { shell: "bash" | "zsh" | "fish" },
): Promise<CandidateResult>;

Return Value

interface CandidateResult {
  candidates: CompletionCandidate[];
  directive: number; // Bitwise flags
}

interface CompletionCandidate {
  value: string;
  description?: string;
  type?: "option" | "subcommand" | "value" | "file" | "directory";
}

CompletionMeta

Completion configuration for arguments.

interface CompletionMeta {
  /** Completion type */
  type?: "file" | "directory" | "none";
  /** Custom completion (mutually exclusive: choices | shellCommand | resolve | expand) */
  custom?: {
    /** Static choices */
    choices?: string[];
    /** Shell command for dynamic values */
    shellCommand?: string;
    /** In-process JS resolver (see DynamicCompletionResolver) */
    resolve?: DynamicCompletionResolver;
    /** Pre-enumerated completion baked into the static shell script (see ExpandCompletion) */
    expand?: ExpandCompletion;
  };
  /** File extension filters (for type: "file") */
  extensions?: string[];
}

DynamicCompletionResolver

Callback invoked at completion time inside the __complete command.

type DynamicCompletionResolver = (
  ctx: DynamicCompletionContext,
) => DynamicCompletionResult | Promise<DynamicCompletionResult>;

interface DynamicCompletionContext {
  /** Word being completed (`--field=` inline prefix is stripped before this is set). */
  currentWord: string;
  /** Target shell formatting requested by the caller. */
  shell: "bash" | "zsh" | "fish";
  /** Best-effort parsed values of OTHER args (camelCase keys, raw strings). */
  parsedArgs: Readonly<Record<string, unknown>>;
  /** Values already supplied for the same option/positional being completed. */
  previousValues: readonly string[];
  /** Subcommand path from root (e.g. ["api"]). */
  subcommandPath: readonly string[];
}

interface DynamicCompletionResult {
  candidates: Array<string | { value: string; description?: string }>;
  /** Optional override; defaults to FilterPrefix | NoFileCompletion. */
  directive?: number;
}

Specifying more than one of choices, shellCommand, resolve, or expand on the same field throws at command-definition time. Dispatcher scripts delegate every completion request to <program> __complete --shell <shell>, resolving the executable currently visible on PATH at TAB time. Static shell scripts automatically delegate to <program> __complete --shell <shell> when a field uses resolve. With expand, dispatcher mode calls enumerate for the already typed dependency values inside __complete; static mode computes the candidate table at script-generation time and inlines it into the script.


ExpandCompletion

Pre-enumerated value completion. Use when candidates can be computed from a finite set of sibling arg values (each must have a static choices or enum schema). Dispatcher mode calls enumerate from __complete for the dependency values already typed on the command line. Static mode walks the cartesian product of the dependsOn values at script-generation time, calls enumerate for each combination, and bakes the resulting table into the static shell script.

interface ExpandCompletion {
  /**
   * Sibling arg names (camelCase, same command) whose runtime values
   * drive the candidate set. Each must have a static `choices` or enum
   * schema. Chaining `expand` specs is not supported.
   */
  dependsOn: readonly string[];
  /**
   * Pure function called once per combination of dependsOn values at
   * script-generation time. Returns the candidates for that combination.
   * Strings imply no description; objects carry an optional description
   * surfaced by zsh and fish.
   */
  enumerate: (
    deps: Readonly<Record<string, string>>,
  ) => ReadonlyArray<string | { value: string; description?: string }>;
}

Properties and constraints:

  • dependsOn must be non-empty and may not reference the field itself or any sibling without a static value set. Validation errors throw at command-definition time with the offending field name.
  • In dispatcher mode, enumerate runs synchronously inside __complete for the currently typed dependency values. In static mode, it runs when the user invokes <program> completion <shell> --static; if it throws, the error is wrapped with the field name and the offending deps snapshot.
  • bash emits one prefix-scalar variable per table entry (<base>__<encKey>=<candidates>) so the generated script runs on Bash 3.2 (macOS default /bin/bash) without associative arrays; zsh emits one global associative array per spec (typeset -gA); fish emits an inline switch (no associative arrays). bash drops descriptions; zsh uses value:description for _describe; fish uses tab-separated value\tdescription.

Example

import { z } from "zod";
import { arg, defineCommand } from "politty";

const command = defineCommand({
  name: "deploy",
  args: z.object({
    // File completion with extension filter
    config: arg(z.string(), {
      completion: { type: "file", extensions: ["json", "yaml"] },
    }),

    // Directory completion
    outputDir: arg(z.string(), {
      completion: { type: "directory" },
    }),

    // Static choices
    env: arg(z.string(), {
      completion: { custom: { choices: ["dev", "staging", "prod"] } },
    }),

    // Dynamic from shell command
    branch: arg(z.string().optional(), {
      completion: { custom: { shellCommand: "git branch --format='%(refname:short)'" } },
    }),

    // Auto-detected from z.enum()
    format: arg(z.enum(["json", "yaml"]), {}),
  }),
  run: () => {},
});

validatePositionalConfig

Validates whether the positional argument configuration is valid.

function validatePositionalConfig(extracted: ExtractedFields): void;

Throws PositionalConfigError if the configuration is invalid.


formatValidationErrors

Formats validation errors into a user-friendly string.

function formatValidationErrors(errors: ValidationError[]): string;

Types

Command

Type for a defined command. run is a function when the command is runnable and undefined for a subcommand-only parent (RunnableCommand/NonRunnableCommand in the actual types — flattened here into one interface for readability).

interface Command<TArgsSchema, TArgs, TResult> {
  /** Command name (required) */
  name: string;
  description?: string;
  /** Alternative names for this command (used as subcommand aliases) */
  aliases?: string[];
  /** Argument schema; `undefined` for a command with no args */
  args: TArgsSchema;
  /** Also accepts `lazy()`-loaded and async-function subcommands (see SubCommandsRecord below) */
  subCommands?: SubCommandsRecord;
  setup?: (context: SetupContext<TArgs>) => void | Promise<void>;
  /** A function when the command is runnable; `undefined` for subcommand-only parents */
  run?: (args: TArgs) => TResult;
  cleanup?: (context: CleanupContext<TArgs>) => void | Promise<void>;
  /** Additional notes */
  notes?: string;
  /** Usage examples */
  examples?: Example[];
}

SubCommandsRecord

The type of Command.subCommands — maps a subcommand name to a command, an async-function loader (see Lazy Loading), or a lazy()-loaded command.

type SubCommandsRecord = Record<string, SubCommandValue>;

type SubCommandValue = AnyCommand | (() => Promise<AnyCommand>) | LazyCommand;

/** Carries synchronous metadata (for help/completion) alongside a deferred loader */
interface LazyCommand {
  /** Internal brand used by the `isLazyCommand` type guard; set by `lazy()` */
  readonly __politty_lazy__: true;
  readonly meta: AnyCommand;
  readonly load: () => Promise<AnyCommand>;
}

ArgSource

The return type of args.$source(name) — where a resolved arg value came from. See Defining Arguments for usage.

type ArgSource = "cli" | "env" | "default";
  • "cli": an explicit CLI token (flag or positional)
  • "env": a field.env fallback (no CLI token was provided)
  • "default": neither of the above (e.g. a schema default, or a value resolved by a prompt handler)

RunInvocation

The shape of args.$invocation — the CLI name a command was invoked with. See Knowing which name run was invoked with for usage.

interface RunInvocation {
  /** The name or alias actually typed on the CLI to reach this command */
  name: string;
  /** Canonical name `name` is an alias for; undefined when not an alias */
  aliasFor?: string;
}

aliasFor is set only when name is one of the command's aliases; it's undefined when invoked by the command's canonical name (or run directly, with no subcommand routing involved).


AgentInfo

The shape of args.$agent, the global setup/cleanup agent, and detectAgent()'s result. See AI Coding Agents for usage.

type KnownAgentId = "claude_code" | "codex_cli" | "cursor" | ...; // the spec's agent names

interface AgentInfo {
  /** The spec's name for the agent; the custom AI_AGENT value when no spec agent matches */
  id: KnownAgentId | (string & {});
  /** The trimmed AI_AGENT value when non-empty, otherwise the same as id */
  rawId: string;
}

Example

Type for defining command usage examples.

interface Example {
  /** Command arguments (e.g., "config.json" or "--loud Alice") */
  cmd: string;
  /** Description of the example */
  desc: string;
  /** Expected output (for documentation, optional) */
  output?: string;
}

ArgMeta

Type for argument metadata (union type).

type ArgMeta = RegularArgMeta | BuiltinOverrideArgMeta;

BaseArgMeta

Base metadata common to all argument types.

interface BaseArgMeta {
  /** Argument description */
  description?: string;
  /** Treat as positional argument; `{ named: true }` also accepts it as `--<name>` */
  positional?: boolean | { named?: boolean };
  /** Placeholder for help display */
  placeholder?: string;
  /**
   * Environment variable name (single or array).
   * If array, the first element takes priority.
   * CLI arguments always take priority over environment variables.
   */
  env?: string | string[];
  /** Shell completion configuration */
  completion?: CompletionMeta;
  /** Interactive prompt configuration (see [Interactive Prompts](./interactive-prompts.md)) */
  prompt?: PromptMeta;
}

RegularArgMeta

Metadata for regular arguments.

interface RegularArgMeta extends BaseArgMeta {
  /**
   * Alias name(s).
   * - 1-char string  → short alias (`-v`)
   * - >1-char string → long alias (`--long-name`, e.g. `--to-be` for `--tobe`)
   * - array          → multiple aliases of either kind
   */
  alias?: string | string[];
  /**
   * Alias name(s) accepted by the parser but hidden from help,
   * generated docs, and shell completion (e.g. legacy names).
   */
  hiddenAlias?: string | string[];
}

BuiltinOverrideArgMeta

Metadata for overriding built-in aliases (-h, -H).

interface BuiltinOverrideArgMeta extends BaseArgMeta {
  /** Built-in alias to override ('h' or 'H'); may be combined with extra aliases */
  alias: "h" | "H" | Array<"h" | "H" | string>;
  /** Hidden aliases (accepted but not surfaced in help/docs/completion) */
  hiddenAlias?: string | string[];
  /** Must be true to override built-in alias */
  overrideBuiltinAlias: true;
}

Reserved built-in names

A field's cliName (its kebab-case name, derived from the field key) or any of its long aliases (alias/hiddenAlias entries longer than one character) cannot be help, help-all, help-json, or version — those long flags are always intercepted by parseArgs before schema parsing, regardless of which field produced the name, so an unguarded collision would make that field permanently unreachable. defineCommand itself only builds the command object and does not validate. For a collision in the command's own args schema, the collision is instead caught by:

  • parseArgs() — throws ReservedAliasError synchronously.
  • runCommand() — catches that throw internally and returns it as { success: false, error } rather than throwing to the caller.
  • runMain() — never returns a result at all (its signature is Promise<never>); it catches the same failure internally, reports the error, and calls process.exit() with the corresponding non-zero exit code.
  • the explicit validateCommand() — never throws; collects it (with every other schema error) into the returned { valid: false, errors }.

This does not apply to a globalArgs schema, for a foreground invocation. runCommand()/runMain() validate globalArgs up front, before the try/catch that produces { success: false, error } (or, for runMain(), before the report-and-process.exit() path) is even entered, so a reserved-name collision there becomes a rejected promise from runCommand()/runMain() itself instead — both are async, so the throw surfaces as a promise rejection, not a synchronous throw at the call site — rather than a returned failure result. runMain() skips this validation entirely, however, when it detects an internal (__*) subcommand invocation (e.g. shell completion's dispatcher): it deliberately runs without the caller's globalArgs/setup/cleanup/prompt on that path, so a globalArgs collision is not rejected there.

Unlike the short aliases -h/-H (see BuiltinOverrideArgMeta above), this collision has no override: since --help/--help-all/--help-json/ --version are always resolved as the built-in before any field is consulted, overrideBuiltinAlias: true would not actually make the field reachable — it would just suppress the error while leaving the field permanently shadowed. Rename the field or its alias instead (e.g. appVersion/targetVersion instead of version).

Known gap, shared with the pre-existing -h/-H reservation: several early-exit paths run before a command's own schema is ever extracted or validated, so this check doesn't apply to them for that command:

  • parseArgs() matches and dispatches to a subcommand before extracting or validating the current command's own schema, so invoking a parent command in a way that routes straight to a subcommand (e.g. cli sub ...) skips this check for the parent's own fields in that call.
  • runMain()'s plugin dispatch (the onUnknownSubcommand option) runs before parseArgs()/runCommandInternal() for a root command without its own run, so a root field collision is not rejected on that path either if the first positional isn't a known subcommand name.

validateCommand() is unaffected by either gap — it walks and validates every command's schema regardless of subcommand routing or plugin dispatch — so it still catches such a collision (e.g. in a test or a CI validation step).

Another known gap, also pre-existing and not specific to this reservation: for a union/discriminatedUnion args schema, every check in this file (including this one) only inspects the schema's flattened, first-occurrence-wins fields list, not each variant's own field metadata. If the same field name appears in multiple variants with different aliases (e.g. plain in one variant, aliased to version in another), a later variant's collision can be missed here even though the help renderer — which iterates each variant's own fields directly — would still advertise the unreachable alias.


Logger

Logger interface for CLI output.

interface Logger {
  /** Output message to stdout */
  log(message: string): void;
  /** Output message to stderr */
  error(message: string): void;
}

MainOptions

Type for options passed to runMain.

interface MainOptions {
  /** Command version */
  version?: string;
  /** Enable debug mode (show stack traces on errors) */
  debug?: boolean;
  /** Capture console and process.stdout/stderr output during execution (default: false) */
  captureLogs?: boolean;
  /** Skip command definition validation (useful in production when already tested) */
  skipValidation?: boolean;
  /** Custom logger (default: console) */
  logger?: Logger;
  /** Prompt resolver for interactive missing-arg prompts */
  prompt?: PromptResolver;
  /**
   * Guidance shown above help output only when an AI coding agent is
   * detected (Markdown). See "AI Coding Agents" in advanced-features.md.
   */
  agentHelp?:
    | string
    | ((context: { agent: AgentInfo; commandPath: readonly string[] }) => string | undefined);
  /**
   * Node.js on-disk compile cache control (default: derive the cache
   * directory from the command name). Pass a string to use a custom
   * directory, or `false` to disable. See "Faster Startup" in recipes.md.
   */
  compileCache?: boolean | string;
}

RunCommandOptions

Type for options passed to runCommand.

interface RunCommandOptions {
  /** Enable debug mode (show stack traces on errors) */
  debug?: boolean;
  /** Capture console and process.stdout/stderr output during execution (default: false) */
  captureLogs?: boolean;
  /** Skip command definition validation (useful in production when already tested) */
  skipValidation?: boolean;
  /** Custom logger (default: console) */
  logger?: Logger;
  /** Same as `MainOptions.agentHelp` */
  agentHelp?: MainOptions["agentHelp"];
}

RunResult

Type for command execution result (discriminated union).

type RunResult<T> = RunResultSuccess<T> | RunResultFailure;

RunResultSuccess

Execution result on success.

interface RunResultSuccess<T = unknown> {
  /** Indicates success */
  success: true;
  /** Return value from run function */
  result: T | undefined;
  /** Error (not present on success) */
  error?: never;
  /** Exit code (always 0 on success) */
  exitCode: 0;
  /** Logs collected during execution */
  logs: CollectedLogs;
}

RunResultFailure

Execution result on failure.

interface RunResultFailure {
  /** Indicates failure */
  success: false;
  /** Return value from run function (not present on failure) */
  result?: never;
  /** Error that occurred */
  error: Error;
  /** Exit code (non-zero) */
  exitCode: number;
  /** Logs collected during execution */
  logs: CollectedLogs;
}

CollectedLogs

Logs collected during execution.

interface CollectedLogs {
  /** All recorded log entries */
  entries: LogEntry[];
}

LogEntry

A single log entry.

interface LogEntry {
  /** Log message */
  message: string;
  /** Time when logged */
  timestamp: Date;
  /** Log level */
  level: LogLevel;
  /** Output stream */
  stream: LogStream;
}

LogLevel

Type for log levels.

type LogLevel = "log" | "info" | "debug" | "warn" | "error";

LogStream

Type for output streams.

type LogStream = "stdout" | "stderr";

SetupContext

Type for context passed to the setup hook.

interface SetupContext<TArgs> {
  /** Parsed and validated arguments */
  args: TArgs;
}

CleanupContext

Type for context passed to the cleanup hook.

interface CleanupContext<TArgs> {
  /** Parsed and validated arguments */
  args: TArgs;
  /** Error that occurred during execution (if any) */
  error?: Error;
}

Note: The run function receives the parsed arguments args directly, not a context object.


HelpOptions

Type for options passed to generateHelp.

interface HelpOptions {
  /** Show subcommand list */
  showSubcommands?: boolean;
  /** Show subcommand options */
  showSubcommandOptions?: boolean;
  /** Custom descriptions for built-in options */
  descriptions?: BuiltinOptionDescriptions;
  /** Command hierarchy context */
  context?: CommandContext;
}

BuiltinOptionDescriptions

Type for customizing built-in option descriptions.

interface BuiltinOptionDescriptions {
  /** Description for --help option */
  help?: string;
  /** Description for --help-all option */
  helpAll?: string;
  /** Description for --help-json option */
  helpJson?: string;
  /** Description for --version option */
  version?: string;
}

HelpDataOptions

Type for options passed to generateHelpData.

interface HelpDataOptions {
  /** Custom descriptions for the --help/--help-all/--help-json/--version built-in options */
  descriptions?: BuiltinOptionDescriptions;
  /** Command hierarchy context */
  context?: CommandContext;
}

HelpData

Structured, JSON-serializable representation of a command's help, returned by generateHelpData.

interface HelpData {
  /** Command name (command.name) */
  name: string;
  /** Full path from the root command. Empty at the true root; also empty when called directly on a non-root command without a context.commandPath */
  commandPath: string[];
  /** Root command name; undefined whenever context.rootName is not supplied (no context at all, or a context that omits rootName) */
  rootName?: string;
  /** Root command version, when provided to runMain/runCommand */
  version?: string;
  /** Canonical subcommand name, set only when this command was reached via an alias */
  aliasFor?: string;
  /** Command description */
  description?: string;
  /** Command aliases (as a subcommand) */
  aliases?: string[];
  /** Structured usage line */
  usage: HelpUsageData;
  /** Descriptions of the built-in --help/--help-all/--help-json/--version options */
  builtinOptions: { help: string; helpAll: string; helpJson: string; version?: string };
  /** Args schema shape: a plain object, or a union/discriminated-union of variants */
  schemaType?: "object" | "discriminatedUnion" | "union" | "xor" | "intersection";
  /** Positional arguments */
  positionals: HelpFieldData[];
  /** Non-positional options; for discriminatedUnion/union/xor, only fields common to every variant/option */
  options: HelpFieldData[];
  /** Discriminator field name (discriminatedUnion schemas only) */
  discriminator?: string;
  /** Per-variant fields (discriminatedUnion schemas only) */
  variants?: HelpVariantData[];
  /** Per-option fields (union/xor schemas only) */
  unionOptions?: HelpVariantData[];
  /** Global options shared across all commands, when a global args schema is defined */
  globalOptions?: HelpFieldData[];
  /** Subcommands, recursively resolved */
  subcommands?: HelpSubcommandData[];
  /** Example usages */
  examples?: Example[];
  /** Additional notes (raw Markdown, unrendered) */
  notes?: string;
}

HelpFieldData

Type for a single positional or option field within HelpData.

interface HelpFieldData {
  /** Field name (camelCase, as defined in the schema) */
  name: string;
  /** CLI option name (kebab-case) */
  cliName: string;
  /** Whether this is a positional argument */
  positional: boolean;
  /** Whether this is accepted as a long option (`--cli-name`), including a named positional */
  named: boolean;
  /** Whether this argument is required */
  required: boolean;
  /** Detected type from schema */
  type: "string" | "number" | "boolean" | "array" | "unknown";
  /** Aliases for this option (short: 1 char, long: multi-char) */
  alias?: string[];
  /** Argument description */
  description?: string;
  /** Placeholder shown in help */
  placeholder?: string;
  /** Environment variable name(s) to read value from */
  env?: string | string[];
  /** Default value, if any */
  defaultValue?: unknown;
  /** Enum values, if detected from schema */
  enumValues?: string[];
  /** Negation configuration */
  negation?: string | boolean;
  /** Derived negation flag name (no -- prefix), or undefined when hidden */
  negationDisplay?: string;
  /** Description shown for the negation option */
  negationDescription?: string;
}

HelpVariantData

A named group of fields sharing a discriminated-union variant or a plain union option.

interface HelpVariantData {
  /** Discriminator value this variant matches (discriminated unions only) */
  discriminatorValue?: string;
  /** Variant/option description */
  description?: string;
  /** Fields unique to this variant/option, including its own positional fields (duplicated from the top-level positionals) */
  fields: HelpFieldData[];
}

HelpUsageData

Structured usage information, content-equivalent to the text Usage: line.

interface HelpUsageData {
  /** Command name as shown in usage (includes root name for subcommands) */
  commandName: string;
  /** Whether global options are defined */
  hasGlobalOptions: boolean;
  /** Whether this command defines any (non-positional) options */
  hasOptions: boolean;
  /** Whether a subcommand token is expected, and whether it's optional */
  subcommand?: "required" | "optional";
  /** Positional arguments in order */
  positionals: Array<{ name: string; required: boolean }>;
}

HelpSubcommandData

A subcommand entry in HelpData.subcommands.

interface HelpSubcommandData {
  /** Subcommand name (key under subCommands) */
  name: string;
  /** Subcommand aliases */
  aliases?: string[];
  /** Set when this subcommand is a legacy async factory registered without lazy(), so no data is available */
  unresolved?: true;
  /** Full structured help for this subcommand, recursively. Absent when unresolved is true. */
  data?: HelpData;
}

CommandContext

Context for command hierarchy.

interface CommandContext {
  /** Full command path (e.g., ["config", "get"]) */
  commandPath?: string[];
  /** Root command name */
  rootName?: string;
  /** Root command version */
  rootVersion?: string;
}

ExtractedFields

Type for field information extracted from a schema.

interface ExtractedFields {
  /** All field definitions */
  fields: ResolvedFieldMeta[];
  /** Original schema */
  schema: ArgsSchema;
  /** Schema type */
  schemaType: "object" | "discriminatedUnion" | "union" | "xor" | "intersection";
  /** Discriminator key (for discriminatedUnion) */
  discriminator?: string;
  /** Variants (for discriminatedUnion) */
  variants?: Array<{
    discriminatorValue: string;
    fields: ResolvedFieldMeta[];
    description?: string;
  }>;
  /** Options (for union) */
  unionOptions?: ExtractedFields[];
  /** Schema description */
  description?: string;
}

ResolvedFieldMeta

Type for resolved field metadata.

interface ResolvedFieldMeta {
  /** Field name (camelCase, as defined in schema) */
  name: string;
  /** CLI option name (kebab-case, used on command line) */
  cliName: string;
  /** Aliases (1-char = short `-v`, multi-char = long `--to-be`) */
  alias?: string[];
  /** Aliases accepted by parser but hidden from help/docs/completion */
  hiddenAlias?: string[];
  /** Description */
  description?: string;
  /** Whether positional argument */
  positional: boolean;
  /** Whether accepted as a long option (`--cli-name`); true for options and `{ positional: { named: true } }` */
  named: boolean;
  /** Placeholder */
  placeholder?: string;
  /** Environment variable name (single or array) */
  env?: string | string[];
  /** Whether required */
  required: boolean;
  /** Default value */
  defaultValue?: unknown;
  /** Detected type */
  type: "string" | "number" | "boolean" | "array" | "unknown";
  /** Original Zod schema */
  schema: z.ZodType;
  /** True if overriding built-in alias (-h, -H) */
  overrideBuiltinAlias?: true;
}

ValidationError

Type for validation errors.

interface ValidationError {
  /** Path where error occurred */
  path: (string | number)[];
  /** Error message */
  message: string;
}

ValidationResult

Type for validation result.

type ValidationResult<T> =
  { success: true; data: T } | { success: false; errors: ValidationError[] };

PositionalConfigError

Error class representing positional argument configuration errors.

class PositionalConfigError extends Error {
  name: "PositionalConfigError";
}

DuplicateAliasError

Error class representing duplicate alias errors.

class DuplicateAliasError extends Error {
  name: "DuplicateAliasError";
}

DuplicateFieldError

Error class representing duplicate field name errors.

class DuplicateFieldError extends Error {
  name: "DuplicateFieldError";
}

ReservedAliasError

Error class representing reserved alias usage errors.

class ReservedAliasError extends Error {
  name: "ReservedAliasError";
}

CommandValidationError

Type for command definition validation errors.

interface CommandValidationError {
  /** Error type */
  type: "positional" | "duplicateAlias" | "duplicateField" | "reservedAlias";
  /** Error message */
  message: string;
}

CommandValidationResult

Type for command definition validation result.

type CommandValidationResult =
  { success: true } | { success: false; errors: CommandValidationError[] };

Skill Management (politty/skill)

Validates SKILL.md files against the Agent Skills specification and installs them by populating .agents/skills/<name> and each agent-specific directory (e.g. .claude/skills/<name>) from the source (typically node_modules/<pkg>/skills/<name>). The materialization is controlled by mode: "symlink" (default) symlinks the source into place and throws with guidance to retry with "copy" on filesystems without symlink support (e.g. Windows without Developer Mode); "copy" always copies. Agent-specific slots route through the canonical .agents/skills/<name> so one sync swaps all hops at once. Source SKILL.md must pre-declare metadata["politty-cli"] = "{package}:{cliName}"; the skills add / skills sync subcommands verify the stamp before installing, and skills remove / skills sync refuse to delete skills owned by another tool. installSkill itself does not validate ownership — programmatic callers that bypass withSkillCommand are responsible for that check. The installer never writes to SKILL.md.

withSkillCommand

Wraps a command with a skills subcommand for managing SKILL.md-based agent skills.

function withSkillCommand<T extends AnyCommand>(
  command: T,
  options: SkillCommandOptions,
): WithSkillCommand<T>;

Throws if command.subCommands.skills already exists.

Parameters

Name Type Description
command AnyCommand Command to wrap
options SkillCommandOptions Skill command options

SkillCommandOptions:

Property Type Description
sourceDir string Source directory containing SKILL.md files (symlinks within the source tree are followed)
package string npm package name that owns these skills. Combined with the command name as "{package}:{cliName}" and compared against each source SKILL.md's metadata["politty-cli"] stamp; mismatches are refused
mode InstallMode Install materialization strategy ("symlink" | "copy"). Defaults to "symlink" — symlink the source into place; install throws with guidance to retry with "copy" on filesystems without symlink support (e.g. Windows without Developer Mode)
cwd string? Install-root directory used by every skills subcommand. Default: walk up from process.cwd() to the first ancestor containing .git/ or package.json, falling back to process.cwd(). Pass an absolute (or cwd-relative) path to override — e.g. a CLI-specific config dir
globalArgs ArgsSchema? The same schema passed to runMain/runCommand's globalArgs. When it already declares a same-named non-positional boolean verbose/json field, skills add/skills sync's --verbose and skills list's --json are omitted from their own schema automatically, so the host's global flag of the same name takes priority — no manual configuration. A same-named non-boolean field doesn't trigger this (the local flag stays declared), but politty itself throws FieldTypeConflictError at parse time for a same-named field with conflicting definitions across globalArgs and a command's own schema — rename or remove the conflicting global field instead
flags SkillFlagOverrides? Per-flag overrides: exclude.alias (rename/disable skills sync --exclude's short alias, default "x") and verbose.alias (rename/disable skills add/skills sync --verbose's short alias — a collision independent of globalArgs's field-name auto-detection, e.g. the host's -v belongs to an unrelated flag)
commandMap { add?, remove? }? Primary name + aliases for skills add/skills remove. In each array the first element becomes the dispatched name, the rest become aliases. Default: add: ["add", "install"], remove: ["remove", "uninstall"]
unknownKeys UnknownKeysMode? Unknown-flag handling for add/sync/remove/list's own schemas ("strict" | "strip" | "passthrough"). Default "strip" (warn and drop the value, matching politty's own z.object()); "passthrough" matches z.object().passthrough() — no warning, but the value is kept on the parsed args under its raw CLI name instead of dropped. Applied uniformly to all four; never rejects a value that legitimately arrives via globalArgs
descriptionAppend string | false Hint appended to the wrapped command's description so --help advertises the skills subcommand. Default: a one-line auto-generated hint. Pass a string to override the hint, or false to leave the description untouched

Generated Subcommands

Command Description
skills sync [--exclude name] Remove orphans and reinstall all skills owned by this CLI
skills add [name ...] Install one or more named skills, or all when no name given
skills remove [name] Remove skill(s) owned by this CLI
skills list [--json] List available skills from source

Example

import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineCommand, runMain } from "politty";
import { withSkillCommand } from "politty/skill";

const sourceDir = resolve(dirname(fileURLToPath(import.meta.url)), "../skills");

const cli = withSkillCommand(
  defineCommand({
    name: "my-agent",
    subCommands: {/* ... */},
  }),
  { sourceDir, package: "@my-agent/skills" },
);

runMain(cli);

scanSourceDir

Scans a directory for SKILL.md files.

function scanSourceDir(sourceDir: string): ScanResult;

If the directory itself contains a SKILL.md, it is treated as a single-skill source; the parent-dir name match is skipped in that case. Otherwise, each subdirectory with a SKILL.md is validated; the subdirectory name must equal the frontmatter name. Symlinked skill directories and symlinked SKILL.md files are followed (npm packages already execute arbitrary JS on install, so refusing symlinks here would not raise the trust boundary).

ScanResult:

interface ScanResult {
  skills: DiscoveredSkill[];
  errors: ScanError[];
}

interface ScanError {
  path: string;
  reason: ScanErrorReason;
  message: string;
  /**
   * Parsed frontmatter `name`, populated only for `name-mismatch`.
   * Lets consumers like `sync`'s orphan-retention guard protect an
   * install at either the directory basename or the frontmatter name.
   */
  skillName?: string;
}

// Runtime tuple of every scan error reason (for exhaustive iteration).
const SCAN_ERROR_REASONS = [
  "parse-failed",
  "name-mismatch",
  "read-failed",
  "missing-source",
] as const;
type ScanErrorReason = (typeof SCAN_ERROR_REASONS)[number];

installSkill

Populates .agents/skills/<name> from skill.sourcePath (typically node_modules/<pkg>/skills/<name>) and each agent-specific directory (e.g. .claude/skills/<name>) from the canonical slot. The materialization is controlled by options.mode:

  • "symlink" (default) — symlink the source into place. On symlinkSync failure (e.g. Windows without Developer Mode, or other filesystems that refuse symlinks) throws an error whose message names the path pair, the underlying cause, and tells the caller to retry with mode: "copy". The original error is attached via the ES2022 cause option.
  • "copy" — recursive copy only; works anywhere. Requires the source SKILL.md to carry a metadata["politty-cli"] stamp (any value): without one, the call throws an actionable error. This is necessary because clearInstallSlot only rm-rf's a real directory when its on-disk stamp matches, so a second copy-mode install of an unstamped source would fail with "Refusing to replace non-symlink".

Never writes to the source SKILL.md; the ownership stamp is authored by the skill package, not rewritten at install time.

function installSkill(skill: DiscoveredSkill, cwd?: string, options?: InstallSkillOptions): void;

interface InstallSkillOptions {
  /** Install materialization strategy. Default: `"symlink"`. */
  mode?: InstallMode;
}

type InstallMode = "symlink" | "copy";

Callers that wrap installSkill directly should validate skill.frontmatter.metadata?.["politty-cli"] against their expected "{package}:{cliName}" before calling; the primitive does not compare ownership itself. withSkillCommand's skills add / skills sync do this automatically. In mode: "copy" the primitive additionally requires some politty-cli stamp to be present on the source (see above), so the same caller-side ownership check naturally satisfies that precondition.


uninstallSkill

Removes a skill's symlinks at .agents/skills/<name> and each agent-specific directory. Real directories (copy-mode installs) are removed only when options.expectedOwnership is provided and the directory's SKILL.md carries that stamp — unstamped or foreign real directories are always left alone. The skills remove / skills sync subcommands always pass expectedOwnership after their own ownership check; direct programmatic callers get the conservative symlink-only default.

function uninstallSkill(name: string, cwd?: string, options?: UninstallSkillOptions): void;

interface UninstallSkillOptions {
  /**
   * If provided, real directories are removed when their SKILL.md's
   * `metadata["politty-cli"]` equals this stamp. Without it, only
   * symlinks are unlinked.
   */
  expectedOwnership?: string;
}

readInstalledOwnership

Returns metadata["politty-cli"] from the installed SKILL.md at .agents/skills/<name>/SKILL.md. For symlink-mode installs this reads through to the source package's authored stamp; for copy-mode installs it reads the stamp captured in the local copy. Returns null if the skill is not installed, the canonical symlink is broken (source package uninstalled), or the stamp is absent/malformed. Other read errors (e.g. EACCES) are surfaced rather than swallowed, so remove / sync do not misinterpret a transient failure as "not installed" and clobber user data.

function readInstalledOwnership(name: string, cwd?: string): string | null;

hasInstalledSkill

Returns true when .agents/skills/<name>/SKILL.md resolves to a readable file (through a valid symlink or directly), false when the path is absent or the canonical symlink is broken. Use together with readInstalledOwnership to distinguish its two null cases — "not installed" (safe to install fresh) vs. "installed but unstamped" (a legacy or manual install that should not be silently clobbered).

function hasInstalledSkill(name: string, cwd?: string): boolean;

parseSkillMd

Parses a SKILL.md content string and validates its frontmatter against the Agent Skills specification.

function parseSkillMd(content: string): ParsedSkillMd | null;

interface ParsedSkillMd {
  frontmatter: SkillFrontmatter;
  body: string;
  rawContent: string;
}

Returns null if the frontmatter is missing or fails validation.


parseFrontmatter

Parses YAML frontmatter from a markdown string. Tolerates a leading UTF-8 BOM. Returns { data: {}, body: content } when no fence is found. When a fence is present but the YAML inside fails to parse, data falls back to {} and parseError contains the underlying YAML error message — scanSourceDir includes this in the parse-failed ScanError so users see the actual cause instead of a downstream "name: …" validation failure.

function parseFrontmatter(content: string): {
  data: Record<string, unknown>;
  body: string;
  parseError?: string;
};

OWNERSHIP_METADATA_KEY

String constant "politty-cli". The metadata key under which the {package}:{cliName} ownership stamp is declared in each source SKILL.md (politty itself never writes this field).

const OWNERSHIP_METADATA_KEY: "politty-cli";

DiscoveredSkill

A skill found in a source directory.

interface DiscoveredSkill {
  frontmatter: SkillFrontmatter;
  sourcePath: string;
  rawContent: string;
}

SkillFrontmatter

Parsed SKILL.md frontmatter, validated against the Agent Skills specification.

type SkillFrontmatter = {
  name: string;
  description: string;
  license?: string;
  compatibility?: string;
  metadata?: Record<string, string>;
  "allowed-tools"?: string;
  // unknown top-level keys round-trip via .passthrough()
};

Prompt Types

For full usage details, see Interactive Prompts.

PromptMeta

Prompt metadata for interactive input when a value is missing.

interface PromptMeta {
  /** Prompt message shown to the user. Defaults to the field's description or name. */
  message?: string;
  /** Explicit prompt type. Overrides auto-detection from schema/completion. */
  type?: PromptType;
  /** Choices for select prompt. Overrides enum values from schema. */
  choices?: Array<string | { label: string; value: string }>;
  /** Whether to enable prompting for this field (default: true when prompt is set) */
  enabled?: boolean;
}

PromptType

Available prompt input types.

type PromptType = "text" | "password" | "confirm" | "select" | "file" | "directory";

PromptResolver

Async callback to resolve missing argument values interactively. Provided by adapter subpath modules.

type PromptResolver = (
  rawArgs: Record<string, unknown>,
  extracted: ExtractedFields,
) => Promise<Record<string, unknown>>;

PromptAdapter

Adapter interface for prompt rendering. Implement this to use a custom prompt library.

interface PromptAdapter {
  text(config: { message: string; placeholder?: string }): Promise<string | symbol>;
  password(config: { message: string }): Promise<string | symbol>;
  confirm(config: { message: string }): Promise<boolean | symbol>;
  select(config: {
    message: string;
    options: Array<{ label: string; value: string }>;
  }): Promise<string | symbol>;
  isCancelled(value: unknown): boolean;
}

Exports

// Core
export { defineCommand } from "./core/command.js";
export { runMain, runCommand } from "./core/runner.js";
export { arg, type ArgMeta } from "./core/arg-registry.js";
export {
  extractFields,
  getUnknownKeysMode,
  toKebabCase,
  type ExtractedFields,
  type ResolvedFieldMeta,
  type UnknownKeysMode,
} from "./core/schema-extractor.js";

// Utilities
export {
  generateHelp,
  generateHelpData,
  type BuiltinOptionDescriptions,
  type CommandContext,
  type HelpData,
  type HelpDataOptions,
  type HelpFieldData,
  type HelpOptions,
  type HelpSubcommandData,
  type HelpUsageData,
  type HelpVariantData,
} from "./output/help-generator.js";
export { isColorEnabled, logger, setColorEnabled, styles, symbols } from "./output/logger.js";

// Types
export type {
  AnyCommand,
  ArgsSchema,
  CleanupContext,
  CollectedLogs,
  Command,
  CommandBase,
  Example,
  LogEntry,
  Logger,
  LogLevel,
  LogStream,
  MainOptions,
  NonRunnableCommand,
  PromptResolver,
  RunCommandOptions,
  RunnableCommand,
  RunResult,
  RunResultFailure,
  RunResultSuccess,
  SetupContext,
  SubCommandsRecord,
  SubCommandValue,
} from "./types.js";

// Command definition validation
export {
  DuplicateAliasError,
  DuplicateFieldError,
  formatCommandValidationErrors,
  PositionalConfigError,
  ReservedAliasError,
  validateCommand,
  validateDuplicateAliases,
  validateDuplicateFields,
  validatePositionalConfig,
  validateReservedAliases,
  type CommandValidationError,
  type CommandValidationResult,
} from "./validator/command-validator.js";

// Validation
export { formatValidationErrors } from "./validator/args-validator.js";
export type { ValidationError, ValidationResult } from "./validator/args-validator.js";

// Prompt (subpath exports)
// import { prompt } from "politty/prompt/clack";
// import { prompt } from "politty/prompt/inquirer";

politty/skill

export { withSkillCommand } from "./skill/index.js";
export {
  hasInstalledSkill,
  installSkill,
  uninstallSkill,
  readInstalledOwnership,
  OWNERSHIP_METADATA_KEY,
} from "./skill/installer.js";
export { parseFrontmatter, parseSkillMd } from "./skill/frontmatter.js";
export type { ParsedSkillMd } from "./skill/frontmatter.js";
export { scanSourceDir } from "./skill/scanner.js";
export { SCAN_ERROR_REASONS } from "./skill/types.js";
export type {
  DiscoveredSkill,
  InstallMode,
  InstallSkillOptions,
  ScanError,
  ScanErrorReason,
  ScanResult,
  SkillCommandOptions,
  SkillFlagOverrides,
  SkillFrontmatter,
  UninstallSkillOptions,
  WithSkillCommand,
} from "./skill/types.js";