velvet

The rope between your terminal and your app.

Define prompts in YAML (or use the lib/DSL in Crystal). Collect clean, typed input. Emit JSON. Your app never writes argparse again.


Install

git clone https://github.com/you/velvet
cd velvet
shards install
shards build --release
# binary at bin/velvet

Usage

# Generate a new YAML wizard from shorthand field specs
velvet new "Deploy config" app_name replicas:int frontend@one_of=vanilla,vue dry_run@confirm

# Interactive wizard
velvet run deploy.yml

# Non-interactive — parse flags against schema
velvet parse deploy.yml -- --environment staging --replicas 3 --dry-run

# Validate a wizard file
velvet validate deploy.yml

# Run the DSL deploy example
crystal run examples/dsl.deploy.cr

The new command writes a normalized .yml file in the current directory.

Pipe into your app:

config=$(velvet run deploy.yml)
myapp <<< "$config"

# or
velvet run deploy.yml | myapp deploy

Your app just reads JSON from stdin — no argparse, no type coercion, no required-field checks.


Generator field shorthand

Supported shorthand format:

field   := id | id:cast | id@ui | id:cast@ui | id:cast@ui=val,val
cast    := str | string | int | float | bool
ui      := input | select | multi | confirm
values  := csv, valid only with select and multi

Examples:

app_name
app_name:str
replicas:int
replicas:int@select=1,2,4,8
frontend@select=vanilla,vue
dry_run@confirm
tags@multi=cache,metrics

Defaults and implications:

Built-in UI aliases:

Custom aliases can be registered from Crystal code while the API evolves:

Velvet::Generator.set_ui_alias("pick", "select")
Velvet::Generator.set_ui_alias("many", "multi")

Reset aliases back to defaults:

Velvet::Generator.reset_ui_aliases

DSL has matching user-facing aliases:

w.input "replicas", "Number of replicas", cast: Velvet::Cast::Int
w.one_of "environment", "Environment", ["dev", "prod"]
w.multi "tags", "Tags", ["cache", "metrics"]
w.any_of "tags", "Tags", ["cache", "metrics"]
w.field "tags", "Tags", ui: "multiselect", options: ["cache", "metrics"]

Wizard file

name: "Deploy config"

fields:
  - id: environment
    type: select
    label: "Target environment"
    options: [dev, staging, production]
    default: dev

  - id: replicas
    type: input
    label: "Number of replicas"
    cast: int
    required: true
    validate:
      min: 1
      max: 20

  - id: dry_run
    type: confirm
    label: "Dry run?"
    default: false

  - id: tags
    type: multiselect
    label: "Feature flags"
    options: [cache, metrics, tracing]
    required: false

Field types

| type | description | | ------------- | ---------------------------------------- | | input | free text, optionally cast and validated | | select | pick one from a list | | multiselect | pick many | | confirm | yes/no boolean |

Cast types

string (default), int, float, bool

For select and multiselect fields, cast is also supported and is applied in both run and parse flows.

Validation

validate:
  min: 1
  max: 100
  pattern: "^[a-z-]+$"

Output

Always clean JSON to stdout, errors to stderr.

{
  "environment": "staging",
  "replicas": 3,
  "dry_run": false,
  "tags": ["cache", "metrics"]
}

Types are cast before emission — consumers always get typed values.


Exit codes

| code | meaning | | ----- | --------------------- | | 0 | success | | 1 | validation error | | 2 | config/schema error | | 130 | user aborted (Ctrl+C) |