xy

command module
v0.0.0-...-2914b19 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 4, 2026 License: MIT Imports: 13 Imported by: 0

README

xy

A stateful CLI template runner. Merge config files, render commands as Go templates, execute them, and chain outputs — all with a persistent SQLite audit trail.

Why xy?

Multi-step CLI workflows often need values from config files and outputs from previous commands. Without xy, you either write long bash scripts where the actual commands are 5% of the file, or copy-paste commands and manually substitute values.

xy sits in between: it gives you config merging, templating, and state — without the boilerplate.

Without xy With xy
200-line bash script per task 1 xy command per step
Config in .env files, sourced with set -a YAML/JSON files, deep-merged in order
Values via $VARIABLE substitution Go templates: {{ .namespace }}
No audit trail Every command's stdout/stderr/exitcode/duration in SQLite
Error handling boilerplate --strict (fail on error), --retry (retry with timeout)
Hard to chain outputs --saveas captures result, key reads it back

Install

# With Go 1.23+
go install github.com/chitta-pardeshi/xy@latest

# Or build from source
git clone https://github.com/chitta-pardeshi/xy.git
cd xy
go build -o xy .
cp xy /usr/local/bin/

# Verify
xy --init && echo "xy works"

Single binary, no dependencies. ~15MB. Linux/Mac/Windows (WSL).

Quick Start

# Initialize state (creates .xy/state.db)
xy --init

# Load config from YAML files (merged in order, later wins)
xy common.yaml prod.yaml --saveas cfg

# Run a command using config values
xy -- 'echo "Deploying to {{ key "cfg" "environment" }}"'

# Save command output for later use
xy --saveas disk -- 'df -h / --output=avail | tail -1'

# Use saved output in next command
xy -- 'echo "Available disk: {{ key "disk" "stdout" | trim }}"'

# Conditional execution
xy --condition '{{ eq (key "disk" "exitcode") "0" }}' \
   -- 'echo "Disk check passed"'

# Retry until success
xy --retry 5s --timeout 60s --strict \
   -- 'curl -sf http://localhost:8080/health'

# Generate a report from all saved state
xy --template report.tmpl --output report.txt

Command Reference

Syntax
xy [files...] [options] [-- command...]
Modes
Mode Syntax Purpose
Init xy --init Create fresh .xy/state.db
Save context xy file1.yaml file2.json --saveas name Merge files, save to SQLite
Run command xy [files...] [options] -- command Render template, execute, optionally save
Report xy --template file [--output file] Render template file against all saved state
Options
Option Description Example
--init Initialize fresh SQLite state in .xy/ xy --init
--set key=value Override a config value (repeatable) --set env=prod --set timeout=600
--saveas name Save command result (or merged context) to SQLite --saveas deploy
--strict Exit non-zero if command fails --strict
--condition expr Only run if template expression is truthy --condition '{{ eq (key "v" "exitcode") "0" }}'
--retry interval Retry command at interval until success --retry 5s
--timeout duration Max time for retries --timeout 300s
--template file Render a template file (report mode) --template report.tmpl
--output file Write rendered output to file (default: stdout) --output report.txt
Config Files

xy auto-detects file format by extension:

Extension Format
.yaml, .yml YAML
.json JSON

Multiple files are deep-merged in order (later file wins):

# base.yaml has env: staging, timeout: 30
# prod.yaml has env: production, replicas: 3

xy base.yaml prod.yaml --saveas cfg
# Result: env=production, timeout=30, replicas=3

--set overrides are applied last:

xy base.yaml prod.yaml --set env=canary --saveas cfg
# env is now "canary"

Dotted paths create nested keys:

xy --set database.host=db.example.com
# creates: {"database": {"host": "db.example.com"}}

Template Functions

Config values (dot context)

Values from merged YAML/JSON files are accessed via dot notation:

{{ .environment }}       → production
{{ .database.host }}     → db.example.com
{{ .replicas }}          → 3
key — read saved state

key retrieves values saved by --saveas from SQLite.

For command results (--saveas with -- command):

{{ key "varname" "stdout" }}     → command stdout
{{ key "varname" "stderr" }}     → command stderr
{{ key "varname" "exitcode" }}   → exit code (as string: "0", "1", etc.)
{{ key "varname" "command" }}    → the rendered command that was executed
{{ key "varname" "duration" }}   → how long it took (e.g., "2.34s")
{{ key "varname" "timestamp" }}  → when it ran (RFC3339)

For saved contexts (--saveas without command):

{{ key "cfg" "environment" }}    → field from merged YAML/JSON
{{ key "cfg" "replicas" }}       → "3" (always string)
set — save inline
{{ set "myvar" "some value" }}   → saves to SQLite, outputs nothing
Sprig v3

All Sprig v3 functions are available. Common ones:

Function Example Result
trim {{ key "v" "stdout" | trim }} Remove whitespace
default {{ key "v" "stdout" | default "none" }} Fallback value
upper / lower {{ upper .env }} PROD
b64enc / b64dec {{ b64enc .password }} Base64 encode
contains {{ if contains "error" .msg }} String search
eq / ne / gt / lt {{ eq (key "v" "exitcode") "0" }} Comparison
ternary {{ ternary "yes" "no" .flag }} If/else
now {{ now | date "2006-01-02" }} Current date
join {{ list "a" "b" | join "," }} a,b
toYaml {{ toYaml .data }} Render as YAML

State Management

How it works
xy --init                          → creates .xy/state.db (SQLite, WAL mode)
xy ... --saveas name -- command    → saves result to "results" table
xy file.yaml --saveas name         → saves merged context to "contexts" table
xy -- '{{ key "name" "field" }}'   → reads from either table
What gets saved

Every --saveas with a command stores:

Field Description
stdout Command standard output
stderr Command standard error
exitcode Exit code (0 = success)
command The rendered command string
duration Execution time (e.g., "2.34s")
timestamp When it ran (RFC3339 UTC)

Every --saveas without a command stores the merged YAML/JSON map.

Persistence

State persists across xy invocations in the same directory. This is what enables chaining:

xy --saveas step1 -- 'echo 10.0.0.1'                      # saves stdout
xy -- 'echo "Server IP is {{ key "step1" "stdout" | trim }}"'  # reads it back

xy --init clears all state (fresh start).

Concurrency

SQLite is opened in WAL mode with busy_timeout=5000ms. Multiple xy processes can safely read/write concurrently.

Execution Control

--strict

Fail immediately if the command exits non-zero:

# Without --strict: saves exitcode=1, continues
xy --saveas check -- 'exit 1'

# With --strict: xy itself exits with error
xy --strict -- 'exit 1'
# xy: exit code 1
# stderr: ...
--condition

Gate execution on a template expression. If it evaluates to false, 0, or empty string, the command is skipped:

# Only proceed if prereqs passed
xy --saveas prereq -- './check-prereqs.sh'

xy --condition '{{ eq (key "prereq" "exitcode") "0" }}' \
   --saveas deploy --strict \
   -- 'make deploy'

If the condition is false, no command runs and no --saveas is written.

--retry / --timeout

Retry a command at a fixed interval until it succeeds (exit 0) or timeout:

# Poll every 5s, give up after 5 minutes
xy --retry 5s --timeout 300s --strict \
   -- 'curl -sf http://localhost:8080/health'

The last attempt's result is saved (if --saveas is used). On timeout with --strict, xy exits with error.

Command wrapping

Everything after -- is joined into a single string and executed via bash -c. This means pipes, redirects, loops, and subshells all work:

xy -- 'echo hello | tr a-z A-Z'
xy -- 'for i in 1 2 3; do echo $i; done'
xy -- 'cat /etc/hosts | grep localhost | wc -l'

Report Generation

Render a template file against all saved state:

# To stdout
xy --template report.tmpl

# To file
xy --template report.tmpl --output report.txt

# From stdin (heredoc)
xy --template /dev/stdin <<'EOF'
Environment: {{ key "cfg" "environment" }}
Deploy: exit={{ key "deploy" "exitcode" }}, took {{ key "deploy" "duration" }}
EOF

Templates have access to all saved results and contexts via key.

Example: Multi-Step Deploy Workflow

# 1. Init + load config
xy --init
xy base.yaml prod.yaml \
   --set version=2.1.0 \
   --saveas cfg

# 2. Check prereqs
xy --saveas ctx   --strict -- 'kubectl config current-context'
xy --saveas ns    --strict -- 'kubectl get ns {{ key "cfg" "namespace" }} -o jsonpath={.status.phase}'

# 3. Deploy
xy --saveas deploy --strict \
   -- 'helm install myapp ./chart --namespace {{ key "cfg" "namespace" }} --set image.tag={{ key "cfg" "version" }}'

# 4. Wait for rollout
xy --saveas wait --strict --retry 5s --timeout 300s \
   -- 'kubectl rollout status deployment/myapp -n {{ key "cfg" "namespace" }}'

# 5. Verify
xy --saveas health --strict \
   -- 'curl -sf http://{{ key "cfg" "endpoint" }}/health'

# 6. Report
xy --template /dev/stdin <<'EOF'
Deploy Report
  Version:  {{ key "cfg" "version" }}
  Deploy:   exit={{ key "deploy" "exitcode" }}, took {{ key "deploy" "duration" }}
  Rollout:  {{ key "wait" "duration" }}
  Health:   {{ key "health" "exitcode" }}
EOF

Architecture

                    +-------------+
                    |  YAML/JSON  |  config files
                    |   files     |  (merged in order)
                    +------+------+
                           |
                    +------v------+
                    |  --set k=v  |  overrides
                    +------+------+
                           |
  +------------------------v------------------------+
  |                       xy                        |
  |                                                 |
  |  +----------+  +-----------+  +--------------+  |
  |  |  Config  |  |  Sprig v3 |  |   bash -c    |  |
  |  |  Merge   |->|  Template |->|   Execute    |  |
  |  |          |  |  Render   |  |              |  |
  |  +----------+  +-----------+  +------+-------+  |
  |                                      |          |
  |                             +--------v-------+  |
  |                             |   SQLite DB    |  |
  |                             |   (WAL mode)   |  |
  |                             |                |  |
  |                             | results table  |  |
  |                             | contexts table |  |
  |                             +----------------+  |
  +-------------------------------------------------+

Comparison

Feature xy Bash script Makefile Argo Workflows
Single binary yes N/A yes K8s cluster needed
Config merge YAML/JSON deep merge source .env variables parameters
Templates Sprig v3 (Helm-compatible) $VAR substitution $() Go templates
State between steps SQLite (key/set) temp files / env vars none artifacts
Retry/timeout built-in manual loop manual built-in
Conditional --condition (template expr) if/then ifeq when
Audit trail every step in SQLite none none K8s events
Portable any machine with bash any machine needs make needs K8s

Contributing

Contributions are welcome. Please open an issue to discuss larger changes before submitting a PR.

# Run tests before submitting
go test -v ./...

License

MIT

Documentation

The Go Gopher

There is no documentation for this package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL