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
- Quick start
- Spec resolution
- Command-line reference
- Environment variables
- Help and discovery
- YAML spec format
- Script entries: path, dependencies, requires, env
- Merging extra specs
- Audit log
- Built-in commands
- Portable scripts workflow
- Examples in this repo
- Troubleshooting
- License
Installation
From crates.io
You still need a spec tree on disk (see Quick start).
From source (monorepo)
Install a spec bundle (recommended after cargo install)
# Export on a machine that has the monorepo (see Portable scripts workflow)
# Import on any machine
# → unpacks to ~/.config/jan/scripts/
# Remember that directory (XDG config) — then drop the flags forever
Or point jan at any existing tree once:
Quick start
1. Use the demo spec (no install dir required):
2. Use the example git/disk/net tree:
3. Prefer a directory once, then use jan with no flags:
4. Project-local spec — drop jan.yaml or jan.spec.yaml in your repo root:
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):
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).
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):
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/scriptsas the well-known directory.JAN_SPEC_ROOT— entry file name inside that directory (default triesscripts.spec.yaml, thenjan.spec.yaml).
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-dirand--speccannot 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:
Help and discovery
jan uses progressive help: place --help or -h immediately after the subcommand prefix you want to inspect.
Rules:
--helpmust come right after the prefix being queried.jan git r --helpis valid;jan git r rev-parse --helpis not (jan treats trailing--helpon exec leaves as an error).- Leaf nodes with
execand no children print a short note that they run an external program. - Subcommands forwarded to external tools (with
passthrough: true) receive their own--helpwhen 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; darwin → macos) |
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
execand nestedcommands. - A node with
includecannot also defineexecorcommandsin the same map. exec.argvmust 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:
run-tool:
about: Forward args to an external command
exec:
argv:
passthrough: true
Nested subcommands
commands:
android:
about: Android workflows
commands:
skills:
about: Skill tools
commands:
list:
about: List skills
exec:
argv:
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:
passthrough: true
Without passthrough: true, trailing CLI arguments are rejected.
Platform filtering (os)
commands:
ports:
os:
about: List listening ports (Linux only)
exec:
argv:
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:
run:
about: Run the script; forwards args
exec:
argv:
passthrough: true
issue:
path: ../scripts/source/issue
requires:
env:
ISSUE_EDITOR: vim
commands:
run:
exec:
argv:
passthrough: true
At run time, jan:
- Merges
dependencies,requires,env, andpathfrom every node along the matched chain (deeper nodes overrideenvkeys). - Resolves transitive
dependenciesto other scripts'pathdirectories (cycle-checked). - Prepends those directories, then the script's own
path, toPATH. - Fails early if any
requiresbinary 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:
Stdin merge:
|
Overlay format — same as a partial root spec:
commands:
zz_extra:
about: Added at runtime
exec:
argv:
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.
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.
| 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
.yamlfiles (paths relative to spec dir) env.sh— setsJAN_SPEC_DIRandJAN_SPEC_ROOTwhen sourcedmanifest.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.
| 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:
Portable scripts workflow
Move the incubator scripts collection between machines. Full detail: docs/PORTABLE_SCRIPTS.md.
Export (source machine):
Import (target machine):
Optional shell aliases:
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:
Try an inlined script:
Troubleshooting
no spec found / well-known directory does not exist
Install a spec bundle or pass an explicit path:
# or
&&
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.