# inquirerer

<p align="center" width="100%">
    <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
</p>

<p align="center" width="100%">
  <a href="https://github.com/constructive-io/dev-utils/actions/workflows/ci.yml">
    <img height="20" src="https://github.com/constructive-io/dev-utils/actions/workflows/ci.yml/badge.svg" />
  </a>
  <a href="https://github.com/constructive-io/dev-utils/blob/main/LICENSE">
    <img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
  </a>
  <a href="https://www.npmjs.com/package/inquirerer"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/dev-utils?filename=packages%2Finquirerer%2Fpackage.json"></a>
</p>

A powerful, TypeScript-first library for building beautiful command-line interfaces.Create interactive CLI tools with ease using intuitive prompts, validation, and rich user experiences.

## Installation

```bash
npm install inquirerer
```

## Features

- 🔌 **CLI Builder** - Build command-line utilties fast
- 🖊 **Multiple Question Types** - Support for text, autocomplete, checkbox, and confirm questions
- 🤖 **Non-Interactive Mode** - Fallback to defaults for CI/CD environments, great for testing
- ✅ **Smart Validation** - Built-in pattern matching, custom validators, and sanitizers
- 🔀 **Conditional Logic** - Show/hide questions based on previous answers
- 🎨 **Interactive UX** - Fuzzy search, keyboard navigation, and visual feedback
- 🔄 **Dynamic Defaults** - Auto-populate defaults from git config, date/time, or custom resolvers

## Table of Contents

- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
  - [TypeScript Support](#typescript-support)
  - [Question Types](#question-types)
  - [Non-Interactive Mode](#non-interactive-mode)
- [API Reference](#api-reference)
  - [Inquirerer Class](#inquirerer-class)
  - [Question Types](#question-types-1)
    - [Text Question](#text-question)
    - [Number Question](#number-question)
    - [Confirm Question](#confirm-question)
    - [Boolean Question](#boolean-question)
    - [JSON Question](#json-question)
    - [List Question](#list-question)
    - [Autocomplete Question](#autocomplete-question)
    - [Checkbox Question](#checkbox-question)
  - [Advanced Question Options](#advanced-question-options)
  - [Positional Arguments](#positional-arguments)
  - [Alias](#alias)
- [Real-World Examples](#real-world-examples)
  - [Project Setup Wizard](#project-setup-wizard)
  - [Configuration Builder](#configuration-builder)
  - [CLI with Commander Integration](#cli-with-commander-integration)
  - [Dynamic Dependencies](#dynamic-dependencies)
  - [Custom Validation](#custom-validation)
- [Dynamic Defaults with Resolvers](#dynamic-defaults-with-resolvers)
  - [Built-in Resolvers](#built-in-resolvers)
  - [Custom Resolvers](#custom-resolvers)
  - [Resolver Examples](#resolver-examples)
- [CLI Helper](#cli-helper)
- [CLI Utilities](#cli-utilities)
  - [Package Information](#package-information)
  - [Argument Parsing](#argument-parsing)
  - [Error Handling](#error-handling)
- [UI Components](#ui-components)
  - [Spinner](#spinner)
  - [Progress Bar](#progress-bar)
  - [Streaming Text](#streaming-text)
  - [Custom UI with UIEngine](#custom-ui-with-uiengine)
- [Testing](#testing)
- [Developing](#developing)

## Quick Start

```typescript
import { Inquirerer } from 'inquirerer';

const prompter = new Inquirerer();

const answers = await prompter.prompt({}, [
  {
    type: 'text',
    name: 'username',
    message: 'What is your username?',
    required: true
  },
  {
    type: 'confirm',
    name: 'newsletter',
    message: 'Subscribe to our newsletter?',
    default: true
  }
]);

console.log(answers);
// { username: 'john_doe', newsletter: true }
```

## Core Concepts

### TypeScript Support

Import types for full type safety:

```typescript
import {
  Inquirerer,
  Question,
  TextQuestion,
  NumberQuestion,
  ConfirmQuestion,
  BooleanQuestion,
  JsonQuestion,
  ListQuestion,
  AutocompleteQuestion,
  CheckboxQuestion,
  InquirererOptions,
  DefaultResolverRegistry,
  registerDefaultResolver,
  resolveDefault
} from 'inquirerer';

interface UserConfig {
  name: string;
  age: number;
  newsletter: boolean;
}

const answers = await prompter.prompt<UserConfig>({}, questions);
// answers is typed as UserConfig
```

### Question Types

All questions support these base properties:

```typescript
interface BaseQuestion {
  name: string;           // Property name in result object
  type: string;           // Question type
  _?: boolean;            // Mark as positional argument (can be passed without --name flag)
  alias?: string | string[];  // Short flag alias(es) for CLI (e.g., 'w' for --workspace)
  message?: string;       // Prompt message to display
  description?: string;   // Additional context
  default?: any;          // Default value
  defaultFrom?: string;   // Dynamic default from resolver (e.g., 'git.user.name')
  setFrom?: string;       // Auto-set value from resolver, bypassing prompt entirely
  optionsFrom?: string;   // Dynamic options from another answer's value
  useDefault?: boolean;   // Skip prompt and use default
  required?: boolean;     // Validation requirement
  skipPrompt?: boolean;   // Skip prompting entirely (field still in man pages / CLI flags)
  validate?: (input: any, answers: any) => boolean | Validation;
  sanitize?: (input: any, answers: any) => any;
  pattern?: string;       // Regex pattern for validation
  dependsOn?: string[];   // Question dependencies
  when?: (answers: any) => boolean;  // Conditional display
}
```

#### Skipping Prompts

Use `skipPrompt: true` to skip interactive prompting for a question entirely. The field is omitted from the answers object unless the user explicitly passes it via a CLI flag. This is useful for fields with backend-managed defaults where the CLI should not prompt, but should still allow overrides.

```typescript
const questions: Question[] = [
  {
    type: 'text',
    name: 'username',
    message: 'Username',
    required: true
  },
  {
    type: 'text',
    name: 'status',
    message: 'Account status',
    skipPrompt: true  // Won't prompt, but user can pass --status active
  }
];

const result = await prompter.prompt({}, questions);
// { username: 'john' }  — status is not included

const result2 = await prompter.prompt({ status: 'active' }, questions);
// { username: 'john', status: 'active' }  — CLI flag override works
```

Key behaviors:
- The question still appears in generated man pages
- CLI flag overrides (e.g. `--status active`) still work
- The field is simply left out of the answers if not provided
- Different from `when`: `skipPrompt` is unconditional, while `when` depends on other answers
- Different from `useDefault`: `skipPrompt` does not apply a default value

### Non-Interactive Mode

When running in CI/CD or without a TTY, inquirerer automatically falls back to default values:

```typescript
const prompter = new Inquirerer({
  noTty: true,  // Force non-interactive mode
  useDefaults: true  // Use defaults without prompting
});
```

## API Reference

### Inquirerer Class

#### Constructor Options

```typescript
interface InquirererOptions {
  noTty?: boolean;                     // Disable interactive mode
  input?: Readable;                    // Input stream (default: process.stdin)
  output?: Writable;                   // Output stream (default: process.stdout)
  useDefaults?: boolean;               // Skip prompts and use defaults
  globalMaxLines?: number;             // Max lines for list displays (default: 10)
  mutateArgs?: boolean;                // Mutate argv object (default: true)
  resolverRegistry?: DefaultResolverRegistry;  // Custom resolver registry
}

const prompter = new Inquirerer(options);
```

#### Methods

```typescript
// Main prompt method
prompt<T>(argv: T, questions: Question[], options?: PromptOptions): Promise<T>

// Generate man page documentation
generateManPage(info: ManPageInfo): string

// Clean up resources
close(): void
exit(): void
```

#### Managing Multiple Instances

When working with multiple `Prompter` instances that share the same input stream (typically `process.stdin`), only one instance should be actively prompting at a time. Each instance attaches its own keyboard listener, so having multiple active instances will cause duplicate or unexpected keypress behavior.

**Best practices:**

1. **Reuse a single instance** - Create one `Prompter` instance and reuse it for all prompts:
   ```typescript
   const prompter = new Inquirerer();
   
   // Use the same instance for multiple prompt sessions
   const answers1 = await prompter.prompt({}, questions1);
   const answers2 = await prompter.prompt({}, questions2);
   
   prompter.close(); // Clean up when done
   ```

2. **Close before creating another** - If you need separate instances, close the first before using the second:
   ```typescript
   const prompter1 = new Prompter();
   const answers1 = await prompter1.prompt({}, questions1);
   prompter1.close(); // Important: close before creating another
   
   const prompter2 = new Prompter();
   const answers2 = await prompter2.prompt({}, questions2);
   prompter2.close();
   ```

### Question Types

#### Text Question

Collect string input from users.

```typescript
{
  type: 'text',
  name: 'projectName',
  message: 'What is your project name?',
  default: 'my-app',
  required: true,
  pattern: '^[a-z0-9-]+$',  // Regex validation
  validate: (input) => {
    if (input.length < 3) {
      return { success: false, reason: 'Name must be at least 3 characters' };
    }
    return true;
  }
}
```

#### Number Question

Collect numeric input.

```typescript
{
  type: 'number',
  name: 'port',
  message: 'Which port to use?',
  default: 3000,
  validate: (input) => {
    if (input < 1 || input > 65535) {
      return { success: false, reason: 'Port must be between 1 and 65535' };
    }
    return true;
  }
}
```

#### Confirm Question

Yes/no questions.

```typescript
{
  type: 'confirm',
  name: 'useTypeScript',
  message: 'Use TypeScript?',
  default: true  // Default to 'yes'
}
```

#### Boolean Question

Alias for `confirm` — provides a semantic name for boolean fields. Behaves identically to `confirm` (y/n prompt).

```typescript
{
  type: 'boolean',
  name: 'isActive',
  message: 'Is this record active?',
  default: true
}
```

Useful when generating CLI prompts from schema types where the field type is `Boolean` rather than a yes/no confirmation.

#### JSON Question

Collect structured JSON input. Validates input with `JSON.parse()` — invalid JSON returns `null`.

```typescript
{
  type: 'json',
  name: 'metadata',
  message: 'Enter metadata',
  default: { key: 'value' }
}
```

The prompt displays a `(JSON)` hint. Users enter raw JSON strings:
```bash
$ Enter metadata (JSON)
> {"email":"user@example.com","role":"admin"}
```

In non-interactive mode, returns the `default` value if provided, otherwise `undefined`.

#### List Question

Select one option from a list (no search).

```typescript
{
  type: 'list',
  name: 'license',
  message: 'Choose a license',
  options: ['MIT', 'Apache-2.0', 'GPL-3.0', 'BSD-3-Clause'],
  default: 'MIT',
  maxDisplayLines: 5
}
```

#### Autocomplete Question

Select with fuzzy search capabilities.

```typescript
{
  type: 'autocomplete',
  name: 'framework',
  message: 'Choose your framework',
  options: [
    { name: 'React', value: 'react' },
    { name: 'Vue.js', value: 'vue' },
    { name: 'Angular', value: 'angular' },
    { name: 'Svelte', value: 'svelte' }
  ],
  allowCustomOptions: true,  // Allow user to enter custom value
  maxDisplayLines: 8
}
```

#### Checkbox Question

Multi-select with search.

```typescript
{
  type: 'checkbox',
  name: 'features',
  message: 'Select features to include',
  options: [
    'Authentication',
    'Database',
    'API Routes',
    'Testing',
    'Documentation'
  ],
  default: ['Authentication', 'API Routes'],
  returnFullResults: false,  // Only return selected items
  required: true
}
```

With `returnFullResults: true`, returns all options with selection status:

```typescript
[
  { name: 'Authentication', value: 'Authentication', selected: true },
  { name: 'Database', value: 'Database', selected: false },
  // ...
]
```

### Advanced Question Options

#### Custom Validation

```typescript
{
  type: 'text',
  name: 'email',
  message: 'Enter your email',
  pattern: '^[^@]+@[^@]+\\.[^@]+$',
  validate: (email, answers) => {
    // Custom async validation possible
    if (email.endsWith('@example.com')) {
      return {
        success: false,
        reason: 'Please use a real email address'
      };
    }
    return { success: true };
  }
}
```

#### Value Sanitization

```typescript
{
  type: 'text',
  name: 'tags',
  message: 'Enter tags (comma-separated)',
  sanitize: (input) => {
    return input.split(',').map(tag => tag.trim());
  }
}
```

#### Conditional Questions

```typescript
const questions: Question[] = [
  {
    type: 'confirm',
    name: 'useDatabase',
    message: 'Do you need a database?',
    default: false
  },
  {
    type: 'list',
    name: 'database',
    message: 'Which database?',
    options: ['PostgreSQL', 'MySQL', 'MongoDB', 'SQLite'],
    when: (answers) => answers.useDatabase === true  // Only show if useDatabase is true
  }
];
```

#### Question Dependencies

Ensure questions appear in the correct order:

```typescript
[
  {
    type: 'checkbox',
    name: 'services',
    message: 'Select services',
    options: ['Auth', 'Storage', 'Functions']
  },
  {
    type: 'text',
    name: 'authProvider',
    message: 'Which auth provider?',
    dependsOn: ['services'],  // Wait for services question
    when: (answers) => {
      const selected = answers.services.find(s => s.name === 'Auth');
      return selected?.selected === true;
    }
  }
]
```

### Positional Arguments

The `_` property allows you to name positional parameters, enabling users to pass values without flags. This is useful for CLI tools where the first few arguments have obvious meanings.

#### Basic Usage

```typescript
const questions: Question[] = [
  {
    _: true,
    name: 'database',
    type: 'text',
    message: 'Database name',
    required: true
  }
];

const argv = minimist(process.argv.slice(2));
const result = await prompter.prompt(argv, questions);
```

Now users can run either:
```bash
node myprogram.js mydb1
# or equivalently:
node myprogram.js --database mydb1
```

#### Multiple Positional Arguments

Positional arguments are assigned in declaration order:

```typescript
const questions: Question[] = [
  { _: true, name: 'source', type: 'text', message: 'Source file' },
  { name: 'verbose', type: 'confirm', default: false },
  { _: true, name: 'destination', type: 'text', message: 'Destination file' }
];

// Running: node copy.js input.txt output.txt --verbose
// Results in: { source: 'input.txt', destination: 'output.txt', verbose: true }
```

#### Named Arguments Take Precedence

When both positional and named arguments are provided, named arguments win and the positional slot is preserved for the next positional question:

```typescript
const questions: Question[] = [
  { _: true, name: 'foo', type: 'text' },
  { _: true, name: 'bar', type: 'text' },
  { _: true, name: 'baz', type: 'text' }
];

// Running: node myprogram.js pos1 pos2 --bar named-bar
// Results in: { foo: 'pos1', bar: 'named-bar', baz: 'pos2' }
```

In this example, `bar` gets its value from the named flag, so the two positional values go to `foo` and `baz`.

### Alias

The `alias` property allows you to define short flag alternatives for question names, making CLI usage more convenient. Users can pass values using either the full name or any defined alias.

#### Basic Usage

```typescript
const questions: Question[] = [
  {
    name: 'workspace',
    type: 'confirm',
    alias: 'w',
    message: 'Create workspace?'
  },
  {
    name: 'outputDir',
    type: 'text',
    alias: 'o',
    message: 'Output directory',
    default: './dist'
  }
];

const argv = minimist(process.argv.slice(2));
const result = await prompter.prompt(argv, questions);
```

Now users can run either:
```bash
node myprogram.js --workspace --outputDir ./build
# or equivalently:
node myprogram.js -w -o ./build
```

#### Multiple Aliases

You can define multiple aliases for a single question using an array:

```typescript
{
  name: 'verbose',
  type: 'confirm',
  alias: ['v', 'V'],  // Both -v and -V work
  message: 'Enable verbose output?'
}
```

#### Alias Priority

When both the main name and an alias are provided, the main name takes precedence:

```typescript
// Running: node myprogram.js --workspace --w=false
// Results in: { workspace: true }  (--workspace wins)
```

When multiple aliases are provided in argv, the first alias in the definition order is used:

```typescript
{
  name: 'workspace',
  type: 'text',
  alias: ['w', 'ws']
}

// Running: node myprogram.js -w first --ws second
// Results in: { workspace: 'first' }  (-w is checked first)
```

#### Positional with Options

Positional arguments work with list, autocomplete, and checkbox questions. The value is mapped through the options:

```typescript
const questions: Question[] = [
  {
    _: true,
    name: 'framework',
    type: 'list',
    options: [
      { name: 'React', value: 'react' },
      { name: 'Vue', value: 'vue' }
    ]
  }
];

// Running: node setup.js React
// Results in: { framework: 'react' }
```

## Real-World Examples

### Project Setup Wizard

```typescript
import { Inquirerer, Question } from 'inquirerer';
import minimist from 'minimist';

const argv = minimist(process.argv.slice(2));
const prompter = new Inquirerer();

const questions: Question[] = [
  {
    type: 'text',
    name: 'projectName',
    message: 'Project name',
    required: true,
    pattern: '^[a-z0-9-]+$'
  },
  {
    type: 'text',
    name: 'description',
    message: 'Project description',
    default: 'My awesome project'
  },
  {
    type: 'confirm',
    name: 'typescript',
    message: 'Use TypeScript?',
    default: true
  },
  {
    type: 'autocomplete',
    name: 'framework',
    message: 'Choose a framework',
    options: ['React', 'Vue', 'Svelte', 'None'],
    default: 'React'
  },
  {
    type: 'checkbox',
    name: 'tools',
    message: 'Additional tools',
    options: ['ESLint', 'Prettier', 'Jest', 'Husky'],
    default: ['ESLint', 'Prettier']
  }
];

const config = await prompter.prompt(argv, questions);
console.log('Creating project with:', config);
```

Run interactively:
```bash
node setup.js
```

Or with CLI args:
```bash
node setup.js --projectName=my-app --typescript --framework=React
```

### Configuration Builder

```typescript
interface AppConfig {
  port: number;
  host: string;
  ssl: boolean;
  sslCert?: string;
  sslKey?: string;
  database: string;
  logLevel: string;
}

const questions: Question[] = [
  {
    type: 'number',
    name: 'port',
    message: 'Server port',
    default: 3000,
    validate: (port) => port > 0 && port < 65536
  },
  {
    type: 'text',
    name: 'host',
    message: 'Server host',
    default: '0.0.0.0'
  },
  {
    type: 'confirm',
    name: 'ssl',
    message: 'Enable SSL?',
    default: false
  },
  {
    type: 'text',
    name: 'sslCert',
    message: 'SSL certificate path',
    when: (answers) => answers.ssl === true,
    required: true
  },
  {
    type: 'text',
    name: 'sslKey',
    message: 'SSL key path',
    when: (answers) => answers.ssl === true,
    required: true
  },
  {
    type: 'list',
    name: 'database',
    message: 'Database type',
    options: ['PostgreSQL', 'MySQL', 'SQLite'],
    default: 'PostgreSQL'
  },
  {
    type: 'list',
    name: 'logLevel',
    message: 'Log level',
    options: ['error', 'warn', 'info', 'debug'],
    default: 'info'
  }
];

const config = await prompter.prompt<AppConfig>(argv, questions);

// Write config to file
fs.writeFileSync('config.json', JSON.stringify(config, null, 2));
```

### CLI with Commander Integration

```typescript
import { CLI, CommandHandler } from 'inquirerer';
import { Question } from 'inquirerer';

const handler: CommandHandler = async (argv, prompter, options) => {
  const questions: Question[] = [
    {
      type: 'text',
      name: 'name',
      message: 'What is your name?',
      required: true
    },
    {
      type: 'number',
      name: 'age',
      message: 'What is your age?',
      validate: (age) => age >= 0 && age <= 120
    }
  ];

  const answers = await prompter.prompt(argv, questions);
  console.log('Hello,', answers.name);
};

const cli = new CLI(handler, {
  version: 'myapp@1.0.0',
  minimistOpts: {
    alias: {
      n: 'name',
      a: 'age',
      v: 'version'
    }
  }
});

await cli.run();
```

### Dynamic Dependencies

```typescript
const questions: Question[] = [
  {
    type: 'checkbox',
    name: 'cloud',
    message: 'Select cloud services',
    options: ['AWS', 'Azure', 'GCP'],
    returnFullResults: true
  },
  {
    type: 'text',
    name: 'awsRegion',
    message: 'AWS Region',
    dependsOn: ['cloud'],
    when: (answers) => {
      const aws = answers.cloud?.find(c => c.name === 'AWS');
      return aws?.selected === true;
    },
    default: 'us-east-1'
  },
  {
    type: 'text',
    name: 'azureLocation',
    message: 'Azure Location',
    dependsOn: ['cloud'],
    when: (answers) => {
      const azure = answers.cloud?.find(c => c.name === 'Azure');
      return azure?.selected === true;
    },
    default: 'eastus'
  },
  {
    type: 'text',
    name: 'gcpZone',
    message: 'GCP Zone',
    dependsOn: ['cloud'],
    when: (answers) => {
      const gcp = answers.cloud?.find(c => c.name === 'GCP');
      return gcp?.selected === true;
    },
    default: 'us-central1-a'
  }
];

const config = await prompter.prompt({}, questions);
```

### Custom Validation

```typescript
const questions: Question[] = [
  {
    type: 'text',
    name: 'username',
    message: 'Choose a username',
    required: true,
    pattern: '^[a-zA-Z0-9_]{3,20}$',
    validate: async (username) => {
      // Simulate API call to check availability
      const available = await checkUsernameAvailability(username);
      if (!available) {
        return {
          success: false,
          reason: 'Username is already taken'
        };
      }
      return { success: true };
    }
  },
  {
    type: 'text',
    name: 'password',
    message: 'Choose a password',
    required: true,
    validate: (password) => {
      if (password.length < 8) {
        return {
          success: false,
          reason: 'Password must be at least 8 characters'
        };
      }
      if (!/[A-Z]/.test(password)) {
        return {
          success: false,
          reason: 'Password must contain an uppercase letter'
        };
      }
      if (!/[0-9]/.test(password)) {
        return {
          success: false,
          reason: 'Password must contain a number'
        };
      }
      return { success: true };
    }
  },
  {
    type: 'text',
    name: 'confirmPassword',
    message: 'Confirm password',
    required: true,
    dependsOn: ['password'],
    validate: (confirm, answers) => {
      if (confirm !== answers.password) {
        return {
          success: false,
          reason: 'Passwords do not match'
        };
      }
      return { success: true };
    }
  }
];
```

## Dynamic Defaults with Resolvers

The `defaultFrom` feature allows you to automatically populate question defaults from dynamic sources like git configuration, environment variables, date/time values, or custom resolvers. This eliminates repetitive boilerplate code for common default values.

### Quick Example

```typescript
import { Inquirerer } from 'inquirerer';

const questions = [
  {
    type: 'text',
    name: 'authorName',
    message: 'Author name?',
    defaultFrom: 'git.user.name'  // Auto-fills from git config
  },
  {
    type: 'text',
    name: 'authorEmail',
    message: 'Author email?',
    defaultFrom: 'git.user.email'  // Auto-fills from git config
  },
  {
    type: 'text',
    name: 'npmUser',
    message: 'NPM username?',
    defaultFrom: 'npm.whoami'  // Auto-fills from npm whoami
  },
  {
    type: 'text',
    name: 'copyrightYear',
    message: 'Copyright year?',
    defaultFrom: 'date.year'  // Auto-fills current year
  }
];

const prompter = new Inquirerer();
const answers = await prompter.prompt({}, questions);
```

### Built-in Resolvers

Prompter comes with several built-in resolvers ready to use:

#### Git Configuration

| Resolver | Description | Example Output |
|----------|-------------|----------------|
| `git.user.name` | Git global user name | `"John Doe"` |
| `git.user.email` | Git global user email | `"john@example.com"` |

#### NPM

| Resolver | Description | Example Output |
|----------|-------------|----------------|
| `npm.whoami` | Currently logged in npm user | `"johndoe"` |

#### Date & Time

| Resolver | Description | Example Output |
|----------|-------------|----------------|
| `date.year` | Current year | `"2025"` |
| `date.month` | Current month (zero-padded) | `"11"` |
| `date.day` | Current day (zero-padded) | `"23"` |
| `date.iso` | ISO date (YYYY-MM-DD) | `"2025-11-23"` |
| `date.now` | ISO timestamp | `"2025-11-23T15:30:45.123Z"` |
| `date.timestamp` | Unix timestamp (ms) | `"1732375845123"` |

#### Workspace (nearest package.json)

| Resolver | Description | Example Output |
|----------|-------------|----------------|
| `workspace.name` | Repo slug from `repository` URL (fallback: `package.json` `name`) | `"dev-utils"` |
| `workspace.repo.name` | Repo name from `repository` URL | `"dev-utils"` |
| `workspace.repo.organization` | Repo org/owner from `repository` URL | `"constructive-io"` |
| `workspace.organization.name` | Alias for `workspace.repo.organization` | `"constructive-io"` |
| `workspace.license` | License field from `package.json` | `"MIT"` |
| `workspace.author` | Author name from `package.json` | `"Constructive"` |
| `workspace.author.name` | Author name from `package.json` | `"Constructive"` |
| `workspace.author.email` | Author email from `package.json` | `"email@example.org"` |

### Priority Order

When resolving default values, inquirerer follows this priority:

1. **CLI Arguments** - Values passed via command line (highest priority)
2. **`setFrom`** - Auto-set values (bypasses prompt entirely)
3. **`defaultFrom`** - Dynamically resolved default values
4. **`default`** - Static default values
5. **`undefined`** - No default available

```typescript
{
  type: 'text',
  name: 'author',
  defaultFrom: 'git.user.name',  // Try git first
  default: 'Anonymous'            // Fallback if git not configured
}
```

### `setFrom` vs `defaultFrom`

Both `setFrom` and `defaultFrom` use resolvers to get values, but they behave differently:

| Feature | `defaultFrom` | `setFrom` |
|---------|---------------|-----------|
| Sets value as | Default (user can override) | Final value (no prompt) |
| User prompted? | Yes, with pre-filled default | No, question is skipped |
| Use case | Suggested values | Auto-computed values |

**`defaultFrom`** - The resolved value becomes the default, but the user is still prompted and can change it:

```typescript
{
  type: 'text',
  name: 'authorName',
  message: 'Author name?',
  defaultFrom: 'git.user.name'  // User sees "Author name? [John Doe]" and can change it
}
```

**`setFrom`** - The resolved value is set directly and the question is skipped entirely:

```typescript
{
  type: 'text',
  name: 'year',
  message: 'Copyright year?',
  setFrom: 'date.year'  // Automatically set to "2025", no prompt shown
}
```

#### When to use each

Use `defaultFrom` when:
- The value is a suggestion the user might want to change
- User confirmation is desired

Use `setFrom` when:
- The value should be computed automatically
- No user input is needed (e.g., timestamps, computed fields)
- You want to reduce the number of prompts

#### Combined example

```typescript
const questions = [
  {
    type: 'text',
    name: 'authorName',
    message: 'Author name?',
    defaultFrom: 'git.user.name'  // User can override
  },
  {
    type: 'text',
    name: 'createdAt',
    setFrom: 'date.iso'  // Auto-set, no prompt
  },
  {
    type: 'text',
    name: 'copyrightYear',
    setFrom: 'date.year'  // Auto-set, no prompt
  }
];

// User only sees prompt for authorName
// createdAt and copyrightYear are set automatically
```

### Custom Resolvers

Register your own custom resolvers for project-specific needs:

```typescript
import { registerDefaultResolver } from 'inquirerer';

// Register a resolver for current directory name
registerDefaultResolver('cwd.name', () => {
  return process.cwd().split('/').pop();
});

// Register a resolver for environment variable
registerDefaultResolver('env.user', () => {
  return process.env.USER;
});

// Use in questions
const questions = [
  {
    type: 'text',
    name: 'projectName',
    message: 'Project name?',
    defaultFrom: 'cwd.name',
    default: 'my-project'
  },
  {
    type: 'text',
    name: 'author',
    message: 'Author?',
    defaultFrom: 'env.user'
  }
];
```

### Instance-Specific Resolvers

For isolated resolver registries, use a custom resolver registry per Prompter instance:

```typescript
import { DefaultResolverRegistry, Prompter } from 'inquirerer';

const customRegistry = new DefaultResolverRegistry();

// Register resolvers specific to this instance
customRegistry.register('app.name', () => 'my-app');
customRegistry.register('app.port', () => 3000);

const prompter = new Inquirerer({
  resolverRegistry: customRegistry  // Use custom registry
});

const questions = [
  {
    type: 'text',
    name: 'appName',
    defaultFrom: 'app.name'
  },
  {
    type: 'number',
    name: 'port',
    defaultFrom: 'app.port'
  }
];

const answers = await prompter.prompt({}, questions);
```

### Resolver Examples

#### System Information

```typescript
import os from 'os';
import { registerDefaultResolver } from 'inquirerer';

registerDefaultResolver('system.hostname', () => os.hostname());
registerDefaultResolver('system.username', () => os.userInfo().username);

const questions = [
  {
    type: 'text',
    name: 'hostname',
    message: 'Hostname?',
    defaultFrom: 'system.hostname'
  }
];
```

#### Conditional Defaults

```typescript
registerDefaultResolver('app.port', () => {
  return process.env.NODE_ENV === 'production' ? 80 : 3000;
});

const questions = [
  {
    type: 'number',
    name: 'port',
    message: 'Port?',
    defaultFrom: 'app.port'
  }
];
```

### Error Handling

Resolvers fail silently by default. If a resolver throws an error or returns `undefined`, inquirerer falls back to the static `default` value (if provided):

```typescript
{
  type: 'text',
  name: 'author',
  defaultFrom: 'git.user.name',  // May fail if git not configured
  default: 'Anonymous',           // Used if resolver fails
  required: true
}
```

For debugging, set `DEBUG=inquirerer` to see resolver errors:

```bash
DEBUG=inquirerer node your-cli.js
```

### Real-World Use Case

```typescript
import { Inquirerer, registerDefaultResolver } from 'inquirerer';

// Register a resolver for current directory name
registerDefaultResolver('cwd.name', () => {
  return process.cwd().split('/').pop();
});

const questions = [
  {
    type: 'text',
    name: 'projectName',
    message: 'Project name?',
    defaultFrom: 'cwd.name',
    required: true
  },
  {
    type: 'text',
    name: 'author',
    message: 'Author?',
    defaultFrom: 'git.user.name',
    required: true
  },
  {
    type: 'text',
    name: 'email',
    message: 'Email?',
    defaultFrom: 'git.user.email',
    required: true
  },
  {
    type: 'text',
    name: 'year',
    message: 'Copyright year?',
    defaultFrom: 'date.year'
  }
];

const prompter = new Inquirerer();
const config = await prompter.prompt({}, questions);
```

With git configured, the prompts will show:

```bash
Project name? (my-project-dir)
Author? (John Doe)
Email? (john@example.com)
Copyright year? (2025)
```

All defaults automatically populated from git config, directory name, and current date!

## CLI Helper

The `CLI` class provides integration with command-line argument parsing:

```typescript
import { CLI, CommandHandler, CLIOptions } from 'inquirerer';

const options: Partial<CLIOptions> = {
  version: 'myapp@1.0.0',
  minimistOpts: {
    alias: {
      v: 'version',
      h: 'help'
    },
    boolean: ['help', 'version'],
    string: ['name', 'output']
  }
};

const handler: CommandHandler = async (argv, prompter) => {
  if (argv.help) {
    console.log('Usage: myapp [options]');
    process.exit(0);
  }

  const answers = await prompter.prompt(argv, questions);
  // Handle answers
};

const cli = new CLI(handler, options);
await cli.run();
```

## CLI Utilities

inquirerer provides a complete set of utilities for building CLI applications, so you can import everything from a single package.

### Package Information

Get information about your CLI's package.json for version display and other metadata:

```typescript
import { getPackageJson, getPackageVersion, getPackageName } from 'inquirerer';

// Get the full package.json object
const pkg = getPackageJson(__dirname);
console.log(`${pkg.name}@${pkg.version}`);

// Or use the convenience helpers
if (argv.version) {
  console.log(getPackageVersion(__dirname));
  process.exit(0);
}

const toolName = getPackageName(__dirname);
console.log(`Welcome to ${toolName}!`);
```

### Argument Parsing

Parse command-line arguments and extract subcommands:

```typescript
import { parseArgv, extractFirst } from 'inquirerer';

const argv = parseArgv(process.argv);
const { first, newArgv } = extractFirst(argv);

// Running: mycli generate --output ./dist
// first = 'generate'
// newArgv = { output: './dist', _: [] }

switch (first) {
  case 'generate':
    await handleGenerate(newArgv);
    break;
  case 'init':
    await handleInit(newArgv);
    break;
  default:
    console.log('Unknown command');
}
```

### Error Handling

Exit gracefully with error messages and optional cleanup:

```typescript
import { cliExitWithError, CliExitOptions } from 'inquirerer';

try {
  await riskyOperation();
} catch (error) {
  await cliExitWithError(error, {
    context: { operation: 'build', target: 'production' },
    beforeExit: async () => {
      await cleanup();
    },
    logger: customLogger // optional, defaults to console
  });
}
```

### Complete CLI Example

Here's a complete example using all the CLI utilities:

```typescript
import { 
  CLI, 
  CLIOptions, 
  Inquirerer,
  extractFirst, 
  cliExitWithError, 
  getPackageVersion,
  ParsedArgs 
} from 'inquirerer';

const options: Partial<CLIOptions> = {
  minimistOpts: {
    alias: { v: 'version', h: 'help' },
    boolean: ['help', 'version']
  }
};

const handler = async (argv: Partial<ParsedArgs>, prompter: Inquirerer) => {
  if (argv.version) {
    console.log(getPackageVersion(__dirname));
    process.exit(0);
  }

  const { first, newArgv } = extractFirst(argv);

  try {
    switch (first) {
      case 'init':
        await handleInit(newArgv, prompter);
        break;
      case 'build':
        await handleBuild(newArgv, prompter);
        break;
      default:
        console.log('Usage: mycli <command> [options]');
        console.log('Commands: init, build');
    }
  } catch (error) {
    await cliExitWithError(error, {
      context: { command: first }
    });
  }
};

const cli = new CLI(handler, options);
await cli.run();
```

## UI Components

inquirerer includes a set of UI components for building rich terminal interfaces beyond simple prompts. These are useful for showing progress, loading states, and streaming output.

### Spinner

Show an animated spinner while performing async operations:

```typescript
import { createSpinner } from 'inquirerer';

const spinner = createSpinner('Loading packages...');
spinner.start();

const data = await fetchPackages();

spinner.succeed('Loaded 42 packages');
// Or: spinner.fail('Failed to load'), spinner.warn('Warning'), spinner.info('Info')
```

**Spinner styles:**

```typescript
import { createSpinner, SPINNER_STYLES } from 'inquirerer';

// Use different spinner styles
const spinner = createSpinner('Processing...', {
  frames: SPINNER_STYLES.dots,    // ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
  // frames: SPINNER_STYLES.line,  // - \ | /
  // frames: SPINNER_STYLES.arc,   // ◜ ◠ ◝ ◞ ◡ ◟
  // frames: SPINNER_STYLES.circle // ◐ ◓ ◑ ◒
});
```

**Update text while spinning:**

```typescript
spinner.start();
spinner.text('Step 1: Downloading...');
await download();
spinner.text('Step 2: Installing...');
await install();
spinner.succeed('Done!');
```

### Progress Bar

Show progress for operations with known completion:

```typescript
import { createProgress } from 'inquirerer';

const progress = createProgress('Installing dependencies');
progress.start();

for (let i = 0; i < packages.length; i++) {
  await installPackage(packages[i]);
  progress.update((i + 1) / packages.length);
}

progress.complete('All packages installed');
// Or: progress.error('Installation failed')
```

**Increment progress:**

```typescript
const progress = createProgress('Processing files');
progress.start();

for (const file of files) {
  await processFile(file);
  progress.increment(1 / files.length);
}

progress.complete();
```

### Streaming Text

Display streaming output like AI chat responses:

```typescript
import { createStream } from 'inquirerer';

const stream = createStream({ showCursor: true });
stream.start();

for await (const token of llmResponse) {
  stream.append(token);
}

stream.done();
```

**With line prefix:**

```typescript
const stream = createStream({ prefix: '> ' });
stream.start();
stream.appendLine('First line of response');
stream.appendLine('Second line of response');
stream.done();
```

### Custom UI with UIEngine

For fully custom interactive UIs, use the `UIEngine` directly:

```typescript
import { UIEngine, Key } from 'inquirerer';

interface MyState {
  items: string[];
  selectedIndex: number;
}

const engine = new UIEngine();

const result = await engine.run<MyState, string>({
  initialState: {
    items: ['Option A', 'Option B', 'Option C'],
    selectedIndex: 0
  },
  
  render: (state) => [
    'Select an option:',
    ...state.items.map((item, i) => 
      i === state.selectedIndex ? `> ${item}` : `  ${item}`
    )
  ],
  
  onEvent: (event, state) => {
    if (event.type === 'key') {
      switch (event.key) {
        case Key.UP:
          return { 
            state: { 
              ...state, 
              selectedIndex: Math.max(0, state.selectedIndex - 1) 
            } 
          };
        case Key.DOWN:
          return { 
            state: { 
              ...state, 
              selectedIndex: Math.min(state.items.length - 1, state.selectedIndex + 1) 
            } 
          };
        case Key.ENTER:
          return { 
            state, 
            done: true, 
            value: state.items[state.selectedIndex] 
          };
      }
    }
    return { state };
  }
});

console.log('You selected:', result);
```

**With animations (tick events):**

```typescript
const engine = new UIEngine();

await engine.run({
  initialState: { frame: 0, message: 'Loading' },
  tickInterval: 100, // Trigger tick event every 100ms
  
  render: (state) => {
    const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
    return [`${frames[state.frame % frames.length]} ${state.message}...`];
  },
  
  onEvent: (event, state) => {
    if (event.type === 'tick') {
      return { state: { ...state, frame: state.frame + 1 } };
    }
    if (event.type === 'key' && event.key === Key.ENTER) {
      return { state, done: true };
    }
    return { state };
  }
});
```

## Testing

For testing CLI applications built with `inquirerer`, use the companion
package [`@inquirerer/test`](https://www.npmjs.com/package/@inquirerer/test)
(also in this repo). It provides everything you need to mock stdin/stdout,
queue keypresses and readline responses, capture and snapshot output, and
even drive the built executable as a subprocess for end-to-end tests —
without you having to hand-roll any of that scaffolding.

```bash
npm install --save-dev @inquirerer/test
```

**In-process testing** (mock streams):

```typescript
import { createTestEnvironment, KEY_SEQUENCES } from '@inquirerer/test';
import { Inquirerer } from 'inquirerer';

const env = createTestEnvironment();
env.sendKey(KEY_SEQUENCES.ENTER);

const prompter = new Inquirerer(env.options);
const result = await prompter.prompt({}, [
  { name: 'confirm', type: 'confirm', message: 'Continue?' }
]);

expect(result.confirm).toBe(true);
expect(env.getOutput()).toContain('Continue?');
```

**Subprocess (E2E) testing** — drive the actual built CLI:

```typescript
import { runCli } from '@inquirerer/test';

const { stdout, exitCode } = await runCli('node', [CLI_ENTRY, 'search', 'hello']);
expect(exitCode).toBe(0);
expect(stdout).toContain('1 result');
```

See the [@inquirerer/test README](https://github.com/constructive-io/dev-utils/tree/main/packages/inquirerer-test#readme) for the full API, including `createTestFixture` (temp-dir + commands runner), key-sequence constants, and snapshot normalisers.

## Developing

**Run the demos:**

```bash
cd packages/inquirerer
pnpm dev:spinner   # Spinner styles demo
pnpm dev:chat      # Streaming text demo  
pnpm dev:upgrade   # Interactive upgrade UI demo
pnpm dev:prompts   # All prompt types demo
```

---

## Development

### Setup

1. Clone the repository:

```bash
git clone https://github.com/constructive-io/dev-utils.git
```

2. Install dependencies:

```bash
cd dev-utils
pnpm install
pnpm build
```

3. Test the package of interest:

```bash
cd packages/<packagename>
pnpm test:watch
```

## Credits

**🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**

## Disclaimer

AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.

No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
