jan-cli 0.6.0

YAML-defined CLI trees with progressive help, optional exec aliases, merged extra specs, and SQLite audit logging keyed by git branch
Documentation

jan-cli

jan is a Rust CLI that turns a YAML command tree into a discoverable, scriptable interface. Each node can expose nested subcommands (with progressive --help at every level) or delegate to a real program via exec. Invocations can be recorded in SQLite, keyed by git branch, so agents and humans can audit what ran in each checkout.

The binary does not ship a built-in command tree. You provide YAML on disk (project-local, explicit flags, or a well-known install directory). See Installation and Spec resolution.


Table of contents


Installation

From crates.io

cargo install jan-cli

You still need a spec tree on disk (see Quick start).

From source (monorepo)

cd jan-cli
cargo build --release
./target/release/jan --help

Install a spec bundle (recommended after cargo install)

# Export on a machine that has the monorepo (see Portable scripts workflow)
jan --spec-dir /path/to/jan-cli --spec-root scripts.spec.yaml bundle -o ~/scripts-jan.zip

# Import on any machine
bash /path/to/jan-cli/scripts/jan-install.sh ~/scripts-jan.zip
# → unpacks to ~/.config/jan/scripts/

# Remember that directory (XDG config) — then drop the flags forever
jan use ~/.config/jan/scripts
jan --help

Or point jan at any existing tree once:

jan use /path/to/jan-cli --root scripts.spec.yaml
jan scripts misc sum help

Quick start

1. Use the demo spec (no install dir required):

jan --spec examples/demo.spec.yaml --no-log
jan --spec examples/demo.spec.yaml --no-log android skills list
jan --spec examples/demo.spec.yaml --no-log android skills search -- extra args here

2. Use the example git/disk/net tree:

jan --spec-dir examples --spec-root default.spec.yaml --no-log git s
jan --spec-dir examples --spec-root default.spec.yaml --no-log git d -- path/to/file

3. Prefer a directory once, then use jan with no flags:

jan use ~/.config/jan/scripts          # or: jan use /path/to/jan-cli --root scripts.spec.yaml
jan scripts misc sum help
jan scripts misc sum run -- 1,2,3
jan use --show                         # where preference is stored

4. Project-local spec — drop jan.yaml or jan.spec.yaml in your repo root:

cd my-project
jan git s    # resolves ./jan.yaml automatically

Spec resolution

jan loads exactly one root spec per invocation. Resolution stops at the first match (highest priority first):

Priority Source Example
1 --spec-dir / JAN_SPEC_DIR + --spec-root jan --spec-dir . --spec-root scripts.spec.yaml
2 --spec / JAN_SPEC (path to a single YAML file) jan --spec ./my.spec.yaml
3 jan.yaml or jan.spec.yaml in --cwd jan --cwd ~/my-repo git s
4 Preferred directory (jan use) ~/.config/jan-cli/config.json → saved jan-dir
5 Well-known install directory ~/.config/jan/scripts/ (or JAN_INSTALL_DIR)

If nothing matches, jan exits with an error that suggests jan use <dir>.

jan use (preferred directory)

Saves a directory under the XDG config location ($XDG_CONFIG_HOME/jan-cli/config.json, typically ~/.config/jan-cli/config.json):

jan use /path/to/specs                 # auto-detects scripts.spec.yaml / jan.spec.yaml / jan.yaml
jan use /path/to/specs --root custom.yaml
jan use --show
jan use --clear

After jan use, normal commands need no --spec-dir / env sourcing. Flags and project-local jan.yaml still override the preference.

--spec-dir + --spec-root

Use when the spec is split across multiple YAML files linked by include:.

  • --spec-dir — directory containing the entry file and included fragments.
  • --spec-root — entry file name inside that directory (default: jan.spec.yaml).
jan --spec-dir /path/to/jan-cli --spec-root scripts.spec.yaml scripts git gd help

include: paths resolve relative to the including file's parent directory.

--spec / JAN_SPEC

Use when you have a single root YAML file (which may still include: siblings):

jan --spec examples/demo.spec.yaml android skills list
export JAN_SPEC=/abs/path/to/jan.spec.yaml
jan list-things

Relative paths are resolved against --cwd (default: current directory).

Cwd-local jan.yaml / jan.spec.yaml

If neither --spec-dir nor --spec is set, jan looks in --cwd for jan.yaml, then jan.spec.yaml. Useful for per-repo command trees checked into version control.

Well-known directory

Default location (after jan-install.sh or manual copy):

~/.config/jan/scripts/
  scripts.spec.yaml    # preferred entry name
  jan.spec.yaml        # alternate entry name
  generated/scripts/   # included fragments (scripts bundle)
  env.sh               # optional: export JAN_SPEC_DIR / JAN_SPEC_ROOT

Overrides:

  • JAN_INSTALL_DIR — replace ~/.config/jan/scripts as the well-known directory.
  • JAN_SPEC_ROOT — entry file name inside that directory (default tries scripts.spec.yaml, then jan.spec.yaml).
export JAN_INSTALL_DIR=~/my-jan-specs
jan scripts misc sum help   # uses ~/my-jan-specs/scripts.spec.yaml if present

Command-line reference

Global flags apply before subcommands:

jan [GLOBAL OPTIONS] <subcommand> ... [passthrough args]
Flag Env Default Description
--spec-dir DIR JAN_SPEC_DIR Spec directory; entry file is --spec-root
--spec-root NAME jan.spec.yaml Entry YAML file name inside --spec-dir
--spec FILE JAN_SPEC Path to root YAML file (mutually exclusive with --spec-dir)
--cwd DIR . Working directory for subprocesses and git branch detection
--db FILE JAN_DB see Audit log SQLite audit database path
--no-log off Disable audit logging
--branch NAME JAN_BRANCH git rev-parse Git branch label stored in audit log
--extra-spec FILE JAN_EXTRA_SPEC Merge YAML fragment after load (repeatable; env is comma-separated)
--stdin-spec off Read YAML from stdin and merge like --extra-spec
-v, --verbose off Print spec identity, cwd, merged fragments (stderr)
-h, --help Framework help (global flags)
-V, --version Print version

Notes:

  • --spec-dir and --spec cannot be used together.
  • Trailing arguments after the matched command path are forwarded only when the leaf defines exec.passthrough: true.
  • Use -- before passthrough args if they start with -: jan scripts foo run -- --help.

Environment variables

Variable Purpose
JAN_SPEC_DIR Same as --spec-dir (highest-priority spec source)
JAN_SPEC Same as --spec
JAN_SPEC_ROOT Entry file name in well-known or bundled layout (scripts.spec.yaml)
JAN_INSTALL_DIR Well-known spec directory (default ~/.config/jan/scripts)
JAN_CONFIG_DIR Override XDG config dir for jan use preference (default ~/.config/jan-cli)
JAN_EXTRA_SPEC Comma-separated paths merged after load (like repeated --extra-spec)
JAN_DB Audit database path
JAN_BRANCH Override git branch for audit log
JAN_OS Override host OS for os: filtering in specs (linux, macos, windows)
JAN_SCRIPTS_ROOT Base for resolving relative script path entries

env.sh from jan bundle / jan-install.sh typically sets:

export JAN_SPEC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
export JAN_SPEC_ROOT="scripts.spec.yaml"

Help and discovery

jan uses progressive help: place --help or -h immediately after the subcommand prefix you want to inspect.

jan --help                          # global framework flags + top-level subcommands
jan git --help                      # git subtree (with examples/default.spec.yaml)
jan git log --help                  # deeper level
jan --spec examples/demo.spec.yaml android skills --help

Rules:

  • --help must come right after the prefix being queried. jan git r --help is valid; jan git r rev-parse --help is not (jan treats trailing --help on exec leaves as an error).
  • Leaf nodes with exec and no children print a short note that they run an external program.
  • Subcommands forwarded to external tools (with passthrough: true) receive their own --help when you pass it after -- or as trailing args.

YAML spec format

A spec is a YAML document with optional metadata and a commands map. Each command node can define:

Field Type Description
about string Shown in help text
commands map Nested subcommands
exec object Run a program (argv, optional passthrough)
include string Load subtree from another YAML file (relative path)
os list Offer this node only on listed platforms (linux, macos, windows; darwinmacos)
path string Directory prepended to PATH when this script runs
dependencies list Other script names whose path dirs are prepended first
requires list External binaries that must exist on PATH before run
env map Environment variables for the child process

Constraints:

  • A node cannot define both exec and nested commands.
  • A node with include cannot also define exec or commands in the same map.
  • exec.argv must be non-empty; the first element is the program.

Minimal example

metadata:
  name: myapp
  description: |
    Short description shown at the root help level.

commands:
  hello:
    about: Say hello
    exec:
      argv: ["echo", "hello"]

  run-tool:
    about: Forward args to an external command
    exec:
      argv: ["my-tool", "start"]
      passthrough: true

Nested subcommands

commands:
  android:
    about: Android workflows
    commands:
      skills:
        about: Skill tools
        commands:
          list:
            about: List skills
            exec:
              argv: ["echo", "listing"]

Invoke: jan android skills list

include (split large specs)

Root-level include (list) merges top-level command maps from other files:

include:
  - fragments/extra.yaml
commands:
  local:
    about: Defined in this file

Per-command include (string) grafts a file as that command's subtree:

commands:
  git:
    include: default/git.yaml

See examples/default.spec.yaml and scripts.spec.yaml for real layouts.

exec and passthrough

commands:
  diff:
    about: Git diff with forwarded paths and flags
    exec:
      argv: ["git", "diff"]
      passthrough: true
jan diff -- src/foo.rs
jan diff -w --cached

Without passthrough: true, trailing CLI arguments are rejected.

Platform filtering (os)

commands:
  ports:
    os: [linux]
    about: List listening ports (Linux only)
    exec:
      argv: ["ss", "-tlnp"]

Nodes hidden on other platforms are omitted from help and cannot be invoked.


Script entries: path, dependencies, requires, env

Script-style commands (e.g. the auto-generated scripts tree) use extra fields so inlined runners can call sibling scripts and external tools.

trackusage:
  about: Track command usage
  path: ../scripts/source/trackusage
  dependencies:
    - trackusage-impl
  commands:
    help:
      about: Show description
      exec:
        argv: ["bash", "-lc", "cat <<'EOF' ..."]
    run:
      about: Run the script; forwards args
      exec:
        argv: ["bash", "-lc", "... inlined script ..."]
        passthrough: true

issue:
  path: ../scripts/source/issue
  requires: [gh, jq, fzf]
  env:
    ISSUE_EDITOR: vim
  commands:
    run:
      exec:
        argv: ["bash", "-lc", "..."]
        passthrough: true

At run time, jan:

  1. Merges dependencies, requires, env, and path from every node along the matched chain (deeper nodes override env keys).
  2. Resolves transitive dependencies to other scripts' path directories (cycle-checked).
  3. Prepends those directories, then the script's own path, to PATH.
  4. Fails early if any requires binary is missing.

Relative path values are resolved against, in order: --cwd, the spec directory, its parent, then JAN_SCRIPTS_ROOT.

Metadata for the incubator scripts tree is generated from scripts/source/*/script.meta.yaml — see scripts/generate_scripts_jan_spec.py.


Merging extra specs

Overlay additional commands at runtime without editing the base spec:

jan --extra-spec ./local-overrides.yaml my-custom-cmd
jan --extra-spec a.yaml --extra-spec b.yaml   # later files override earlier keys
export JAN_EXTRA_SPEC=/path/a.yaml,/path/b.yaml

Stdin merge:

cat overlay.yaml | jan --stdin-spec extra-leaf

Overlay format — same as a partial root spec:

commands:
  zz_extra:
    about: Added at runtime
    exec:
      argv: ["echo", "merged"]

Later overlays win on key conflicts. After merge, the full tree is validated.


Audit log

By default, every leaf execution is logged to SQLite:

Column Content
ts Unix timestamp
git_branch From --branch, JAN_BRANCH, or git rev-parse in --cwd
cwd Working directory
command_path Matched subcommand chain (e.g. git s)
argv_json Full argv passed to the child process
exit_code Child exit code
spec_root_id Link to which spec tree was used

Default database path:

OS Path
Linux ~/.local/share/jan-cli/audit.db
macOS ~/Library/Application Support/jan-cli/audit.db
Windows %LOCALAPPDATA%\jan-cli\audit.db

Override with --db / JAN_DB, or disable with --no-log.

Useful for agent workflows: each git branch gets a separate audit trail of which jan commands ran.


Built-in commands

These are reserved first-token subcommands (not defined in your YAML):

jan use

Remember a preferred jan directory under XDG config (~/.config/jan-cli/config.json). Works even when no YAML tree is configured yet.

jan use ~/.config/jan/scripts
jan use /path/to/jan-cli --root scripts.spec.yaml
jan use --show
jan use --clear

Afterward, plain jan … loads that tree (unless flags, JAN_SPEC*, or a project-local jan.yaml override it).

jan bundle

Pack every YAML file reachable via include: under the spec anchor into a portable ZIP.

jan --spec-dir . --spec-root scripts.spec.yaml bundle -o ~/scripts-jan.zip
jan --spec-dir . --spec-root scripts.spec.yaml bundle --dry-run
jan --spec-dir . --spec-root scripts.spec.yaml bundle --include-extra -o out.zip
Option Description
-o, --output FILE Output path (default: jan-spec-bundle.zip)
--dry-run List archive paths instead of writing
--include-extra Also pack --extra-spec files if under the anchor

The archive contains:

  • All reachable .yaml files (paths relative to spec dir)
  • env.sh — sets JAN_SPEC_DIR and JAN_SPEC_ROOT when sourced
  • manifest.json — file list, SHA-256 hashes, version metadata

jan alias

Emit shell aliases for script run leaves so you can type sum instead of jan scripts misc sum run.

jan --spec-dir . --spec-root scripts.spec.yaml alias
jan --spec-dir . --spec-root scripts.spec.yaml alias --shell zsh -o ~/.config/jan/scripts/aliases.zsh
Option Description
-o, --output FILE Write to file instead of stdout
--jan-bin NAME Program on the RHS (default: jan)
--spec-dir, --spec-root Embedded in each alias (default: loaded spec identity)
--shell sh|bash|zsh Comment header dialect

Example output:

alias sum='jan --spec-dir "$JAN_SPEC_DIR" --spec-root scripts.spec.yaml scripts misc sum run'

Portable scripts workflow

Move the incubator scripts collection between machines. Full detail: docs/PORTABLE_SCRIPTS.md.

Export (source machine):

python3 jan-cli/scripts/generate_scripts_jan_spec.py
cd jan-cli
cargo build --release
./target/release/jan --spec-dir . --spec-root scripts.spec.yaml bundle -o ~/scripts-jan.zip

Import (target machine):

bash jan-cli/scripts/jan-install.sh ~/scripts-jan.zip
source ~/.config/jan/scripts/env.sh
jan scripts git gd help
jan scripts git gd run

Optional shell aliases:

jan alias --spec-dir "$JAN_SPEC_DIR" --spec-root scripts.spec.yaml --shell zsh \
  -o ~/.config/jan/scripts/aliases.zsh
source ~/.config/jan/scripts/aliases.zsh
sum 1,2,3

Trust: bundles contain executable script source inlined in YAML. Only import bundles you trust. Verify manifest.json SHA-256 hashes if needed.


Examples in this repo

Path Purpose
examples/demo.spec.yaml Minimal tutorial tree (nested subcommands, passthrough, script fields)
examples/default.spec.yaml + examples/default/ Git, disk, net, proc, gradle, rust, android, docker, sys shortcuts
scripts.spec.yaml + generated/scripts/ Full incubator scripts/source tree (auto-generated)
scripts/generate_scripts_jan_spec.py Regenerate generated/scripts/ from scripts/source
scripts/jan-install.sh Unpack bundle to ~/.config/jan/scripts

Try the example git shortcuts:

jan --spec-dir examples --spec-root default.spec.yaml git s
jan --spec-dir examples --spec-root default.spec.yaml git r rev-parse --is-inside-work-tree

Try an inlined script:

jan --spec-dir . --spec-root scripts.spec.yaml scripts files basename run -- /tmp/foo/bar.txt

Troubleshooting

no spec found / well-known directory does not exist

Install a spec bundle or pass an explicit path:

jan --spec examples/demo.spec.yaml --help
# or
jan-install.sh ~/scripts-jan.zip && source ~/.config/jan/scripts/env.sh

use either --spec-dir or --spec, not both

Pick one resolution mode per invocation.

missing required utilities on PATH: fzf

The leaf's requires list names binaries that must be installed. Install them or remove/adjust the spec.

unknown script dependency 'foo'

dependencies must name another command in the same spec that declares a path.

unexpected trailing arguments

Enable exec.passthrough: true on the leaf, or remove extra CLI tokens.

place --help immediately after the subcommand prefix

Move --help earlier: jan git --help, not jan git r --help (unless r is the leaf you want).

Spec include errors

include: targets must exist on disk and stay under the spec directory anchor (for bundle validation). Paths are relative to the including file's directory.


License

MIT — see LICENSE.md.