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. Point it at a directory once with jan use <DIR>, then jan --help lists the live subcommands from that tree.
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
- 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/ and runs `jan use` on that dir
Or point jan at any existing tree once:
Quick start
1. Prefer a directory once, then use jan with no path flags:
# or: jan use ~/.config/jan/scripts
2. Demo / examples trees in this repo:
Spec resolution
jan loads the YAML tree from the preferred directory saved by jan use (stored in ~/.config/jan-cli/config.json, overridable with JAN_CONFIG_DIR).
The directory is expected to be a properly formatted jan tree (entry YAML plus any include: fragments). After jan use, run jan --help to list live subcommands.
A common install layout (after jan-install.sh):
~/.config/jan/scripts/
scripts.spec.yaml
generated/scripts/ # included fragments (scripts bundle)
env.sh # notes that you should `jan use` this directory
Command-line reference
jan [GLOBAL OPTIONS] <subcommand> ... [passthrough args]
| Flag | Env | Default | Description |
|---|---|---|---|
-h, --help |
— | — | List live subcommands from the preferred tree (+ built-ins) |
-V, --version |
— | — | Print version |
-v, --verbose |
— | off | Print preferred directory resolution (stderr) |
--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 |
Notes:
- 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_CONFIG_DIR |
Override XDG config dir for jan use preference (default ~/.config/jan-cli) |
JAN_DB |
Default audit database path |
JAN_BRANCH |
Default --branch |
JAN_OS |
Override platform filter id for os: in YAML (linux, macos, windows, …) |
JAN_INSTALL_DIR |
Default unpack directory for jan-install.sh only |
JAN_SCRIPTS_ROOT |
Base for resolving relative script path entries |
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.
ptional 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.
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 … and jan --help load that tree.
jan bundle
Pack the reachable YAML tree into a ZIP:
| Flag | Description |
|---|---|
-o, --output |
Output zip path (default: jan-spec-bundle.zip) |
--dry-run |
List files instead of writing a zip |
Archive contents:
- All YAML files reachable via
include:under the preferred directory env.sh— reminds you tojan usethe unpack directorymanifest.json— file list and SHA-256 hashes
jan alias
Emit shell aliases for executable leaves (assumes jan use is already configured):
| Flag | Description |
|---|---|
--jan-bin |
Program name on the RHS (default: jan) |
--shell |
Header dialect: sh, bash, or zsh |
-o |
Write to a file instead of stdout |
Example line:
Portable scripts workflow
See docs/PORTABLE_SCRIPTS.md for the full export/import flow.
# On the target machine:
Examples in this repo
Troubleshooting
no preferred jan directory configured
missing required utilities on PATH: fzf
Install the named tool or remove it from the node's requires: list.
unknown script dependency 'foo'
The dependencies: entry must match another command name in the same tree that has a path:.
unexpected trailing arguments
The leaf does not set exec.passthrough: true. Add it, or drop the extra args.
place --help immediately after the subcommand prefix
Put --help right after the prefix you want help for, not after passthrough argv.
Spec include errors
include: paths are resolved relative to the including file's parent directory and must stay under the preferred jan directory for jan bundle.
License
MIT — see LICENSE.md.