# 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.
> [!WARNING]
> A jan tree is executable code, not configuration data. `jan use` makes that
> tree the persistent source of commands for later invocations, and its YAML can
> launch arbitrary programs with your user account's permissions. Only use trees
> you have reviewed and trust. Bundle manifest hashes detect corruption; they do
> not authenticate the bundle's publisher.
---
## Table of contents
- [Installation](#installation)
- [Security model](#security-model)
- [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)
- [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
# Prefer a reviewed personal tree (canonical: dotfiles/jan)
./target/release/jan use /path/to/dotfiles/jan
./target/release/jan --help
```
### Install a trusted spec bundle
```bash
# Export a reviewed personal tree (canonical: dotfiles/jan)
jan use /path/to/dotfiles/jan
jan bundle -o ~/scripts-jan.zip
# Import on any machine
bash /path/to/jan-cli/scripts/jan-install.sh ~/scripts-jan.zip
# → verifies archive structure and manifest hashes, then unpacks
# to ~/.config/jan/scripts/; this does not authenticate the publisher
# Review the extracted YAML before making it active.
jan use ~/.config/jan/scripts
jan --help
```
Or point `jan` at any existing tree once:
```bash
jan use /path/to/dotfiles/jan
jan scripts misc sum help
```
---
## Security model
- A preferred tree can execute arbitrary commands and set child environment
variables. `jan` does not sandbox commands or reduce their privileges.
- `jan use` persists an absolute tree path. Future `jan` commands use the current
contents at that path without a signature, content lock, or per-run prompt.
- `include:` paths are confined to the preferred tree, but included YAML has the
same execution authority as the root file.
- `jan bundle` manifests provide integrity checks only. They do not prove who
created a bundle or whether its contents are trustworthy.
- The SQLite audit database is mutable local telemetry and may contain sensitive
command-line arguments. It is not a tamper-proof security log.
See [SECURITY.md](SECURITY.md) for the detailed threat model and open findings.
---
## Quick start
**1. Review and trust a tree, prefer it once, then use `jan` with no path flags:**
```bash
jan use /path/to/dotfiles/jan
# or: jan use ~/.config/jan/scripts
jan --help # lists live subcommands from that tree
jan list # script leaves
jan scripts misc sum help
jan scripts misc sum run -- 1,2,3
jan use --show # where preference is stored
```
**2. Demo / examples trees in this repo:**
```bash
jan use examples --root demo.spec.yaml
jan --help
jan android skills list
jan use examples --root default.spec.yaml
jan git s
```
---
## 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`).
```bash
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
```
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`):
```text
~/.config/jan/scripts/
scripts.spec.yaml
scripts.yaml # category index (flat layout)
git.yaml …
env.sh # notes that you should `jan use` this directory
```
---
## Command-line reference
```
jan [GLOBAL OPTIONS] <subcommand> ... [passthrough args]
```
| `-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](#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
| `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.
```bash
jan --help # live top-level subcommands + built-ins
jan scripts --help # progressive help for a prefix
jan scripts misc sum help # many script leaves also expose a `help` action
```
**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.
---
ptional `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 a personal tree such as `dotfiles/jan` 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
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:
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.
5. Resolves `exec.argv[0]` on the `PATH` jan itself inherited, before the prepends above apply, so a spec `path` directory cannot substitute its own `bash` or `python3` for the system one. Directories from `path`/`dependencies` are used only for programs the host does not provide, and an `argv[0]` containing a `/` is used verbatim.
Relative `path` values are resolved against, in order: `--cwd`, the spec directory, its parent, then `JAN_SCRIPTS_ROOT`.
Personal utilities live as inlined YAML under a preferred directory such as `dotfiles/jan`. Edit that tree directly; the old `generate_scripts_jan_spec.py` helper is retired.
---
## 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 use`
Remember a preferred jan directory under XDG config (`~/.config/jan-cli/config.json`). Works even when no YAML tree is configured yet.
```bash
jan use ~/.config/jan/scripts
jan use /path/to/dotfiles/jan
jan use --show
jan use --clear
```
Afterward, plain `jan …` and `jan --help` load that tree.
### `jan bundle`
Pack the reachable YAML tree into a ZIP:
```bash
jan use /path/to/dotfiles/jan
jan bundle -o ~/scripts-jan.zip
jan bundle --dry-run
```
| `-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 to `jan use` the unpack directory
- `manifest.json` — file list and SHA-256 hashes
### `jan alias`
Emit shell aliases for executable leaves (assumes `jan use` is already configured):
```bash
jan alias
jan alias --shell zsh -o ~/.config/jan/scripts/aliases.zsh
```
Alias generation rejects names outside `[A-Za-z_][A-Za-z0-9_-]*` and
shell-quotes every generated command argument.
| `--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:
```bash
alias sum='jan scripts misc sum run'
```
---
## Portable scripts workflow
See [docs/PORTABLE_SCRIPTS.md](docs/PORTABLE_SCRIPTS.md) for the full export/import flow.
```bash
cargo build --release
./target/release/jan use /path/to/dotfiles/jan
./target/release/jan bundle -o ~/scripts-jan.zip
# On the target machine:
bash scripts/jan-install.sh ~/scripts-jan.zip
jan use ~/.config/jan/scripts
jan --help
jan alias --shell zsh -o ~/.config/jan/scripts/aliases.zsh
```
---
## Examples in this repo
```bash
jan use examples --root demo.spec.yaml
jan android skills list
jan use examples --root default.spec.yaml
jan git s
jan git r rev-parse --is-inside-work-tree
jan use /path/to/dotfiles/jan
jan scripts files basename run -- /tmp/foo/bar.txt
jan list --category files
```
---
## Troubleshooting
### `no preferred jan directory configured`
```bash
jan use /path/to/jan-cli --root scripts.spec.yaml
jan --help
```
### `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 from the root directory selected by `jan use`.
Absolute paths, `..` components, and symlinks that escape that root are rejected
both during normal loading and by `jan bundle`.
---
## License
MIT — see `LICENSE.md`.