# 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](#installation) and [Spec resolution](#spec-resolution).
---
## Table of contents
- [Installation](#installation)
- [Quick start](#quick-start)
- [Spec resolution](#spec-resolution)
- [Command-line reference](#command-line-reference)
- [Environment variables](#environment-variables)
- [Help and discovery](#help-and-discovery)
- [YAML spec format](#yaml-spec-format)
- [Script entries: path, dependencies, requires, env](#script-entries-path-dependencies-requires-env)
- [Merging extra specs](#merging-extra-specs)
- [Audit log](#audit-log)
- [Built-in commands](#built-in-commands)
- [Portable scripts workflow](#portable-scripts-workflow)
- [Examples in this repo](#examples-in-this-repo)
- [Troubleshooting](#troubleshooting)
- [License](#license)
---
## Installation
### From crates.io
```bash
cargo install jan-cli
```
You still need a **spec tree** on disk (see [Quick start](#quick-start)).
### From source (monorepo)
```bash
cd jan-cli
cargo build --release
./target/release/jan --help
```
### Install a spec bundle (recommended after `cargo install`)
```bash
# 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/
source ~/.config/jan/scripts/env.sh
jan --help
```
---
## Quick start
**1. Use the demo spec (no install dir required):**
```bash
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:**
```bash
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. After installing a scripts bundle to `~/.config/jan/scripts`:**
```bash
source ~/.config/jan/scripts/env.sh # sets JAN_SPEC_DIR, JAN_SPEC_ROOT
jan scripts misc sum help
jan scripts misc sum run -- 1,2,3
```
**4. Project-local spec** — drop `jan.yaml` or `jan.spec.yaml` in your repo root:
```bash
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):
| 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 | **Well-known directory** | `~/.config/jan/scripts/` |
If nothing matches, `jan` exits with an error that points at install docs.
### `--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`).
```bash
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):
```bash
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):
```text
~/.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`).
```bash
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]
```
| `--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](#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
| `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_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:
```bash
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.
```bash
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:
| `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 `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
```yaml
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
```yaml
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:
```yaml
include:
- fragments/extra.yaml
commands:
local:
about: Defined in this file
```
**Per-command** `include` (string) grafts a file as that command's subtree:
```yaml
commands:
git:
include: default/git.yaml
```
See `examples/default.spec.yaml` and `scripts.spec.yaml` for real layouts.
### `exec` and passthrough
```yaml
commands:
diff:
about: Git diff with forwarded paths and flags
exec:
argv: ["git", "diff"]
passthrough: true
```
```bash
jan diff -- src/foo.rs
jan diff -w --cached
```
Without `passthrough: true`, trailing CLI arguments are rejected.
### Platform filtering (`os`)
```yaml
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.
```yaml
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:
```bash
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:**
```bash
Overlay format — same as a partial root spec:
```yaml
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:
| `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:**
| 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 bundle`
Pack every YAML file reachable via `include:` under the spec anchor into a portable ZIP.
```bash
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
```
| `-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`.
```bash
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
```
| `-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:
```sh
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`](docs/PORTABLE_SCRIPTS.md).
**Export (source machine):**
```bash
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
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:**
```bash
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
| `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:**
```bash
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:**
```bash
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:
```bash
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`.