jan-cli 0.12.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. 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 and remote sha256 values detect corruption or unexpected content changes; they do not authenticate the publisher.


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
# 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

# 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:

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.
  • Local include: paths are confined to the preferred tree; included YAML has the same execution authority as the root file.
  • Remote includes and remote exec.url scripts are fetched over HTTPS (HTTP only with --allow-http / JAN_ALLOW_HTTP) and must declare sha256. Cached objects live under ~/.cache/jan/. Hashes are integrity checks only.
  • jan bundle manifests provide integrity checks only. They do not prove who created a bundle or whether its contents are trustworthy. Bundles cannot contain remote includes — vendor YAML locally first.
  • 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 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:

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 cron --list                         # scripts with time triggers
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:

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).

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 https://example.com/scripts-jan.zip --sha256 <64-hex>
jan use --show
jan use --clear

A remote URL must be an HTTPS jan bundle zip (same layout as jan bundle / jan-install.sh). The zip SHA256 is required. After verification, the tree is cached under ~/.cache/jan/trees/<sha256>/ and that path is preferred.

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
  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]
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
JAN_ALLOW_HTTP Set to 1/true to allow plain HTTP for remote fetches (insecure)
JAN_CACHE_DIR Override cache root for remote objects/trees (default ~/.cache/jan)

Help and discovery

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

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:

Field Type Description
about string Shown in help text
commands map Nested subcommands
exec object Run a program (argv, optional passthrough)
include string or map Load subtree from another YAML file (relative path) or remote {url, sha256}
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 or sections Public assignments and/or private names (see below)
inputs map Named CLI flags (--name) with optional description, required, default; usable as ${{ inputs.name }}
cron string or list Five-field crontab expression(s); jan cron runs the script's run leaf when local time matches
exec.url / exec.sha256 strings Download, verify, and run a remote script (optional argv interpreter prefix)

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
  - url: https://example.com/shared.yaml
    sha256: <64-hex>
commands:
  local:
    about: Defined in this file

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

commands:
  git:
    include: default/git.yaml
  shared:
    include:
      url: https://example.com/git.yaml
      sha256: <64-hex>

Local paths must stay under the preferred tree. Remote includes require HTTPS and sha256. See examples/default.spec.yaml and a personal tree such as dotfiles/jan for real layouts.

Remote exec scripts

commands:
  show-docs:
    about: Run a verified remote script
    exec:
      url: https://example.com/show.py
      sha256: <64-hex>
      argv: ["python3"]   # optional interpreter prefix
      passthrough: true

Without argv, jan runs the cached file directly (executable bit set on Unix). With argv, the cached path is appended after the prefix (e.g. python3 /cache/…).

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.

For inlined POSIX shells (bash/zsh/sh + -c/-lc + script body), jan inserts a $0 placeholder before passthrough args so $1 / "$@" behave like a normal script. A leading -- after the leaf is optional and stripped in that case (jan … run create and jan … run -- create are equivalent).

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.


Time triggers (jan cron)

Put a five-field crontab expression (or a list of them) on a script node or its run child. Then run jan cron from the host crontab (typically every minute) to execute every matching script's run leaf.

commands:
  scripts:
    commands:
      misc:
        commands:
          morning-note:
            about: Append a daily note
            cron: "30 10 * * *"
            # or: cron: ["30 10 * * *", "0 18 * * *"]
            # or: cron: "@hourly"
            commands:
              run:
                exec:
                  argv: ["bash", "-lc", "echo hi >> ~/cron.txt"]
jan cron --list                         # all scripts with schedules
jan cron --dry-run --at "2026-08-05 10:30"
jan cron                                # run matches for the current local minute

Nicknames: @yearly / @annually, @monthly, @weekly, @daily / @midnight, @hourly.


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
  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:
    public:
      ISSUE_EDITOR: vim
    private:
      - GH_TOKEN
  commands:
    run:
      exec:
        argv: ["bash", "-lc", "..."]
        passthrough: true

Environment variables (env):

  • Public — key/value pairs stored in YAML and assigned on the child.
  • Private — names only; jan requires them to be set in the host environment, copies their values into the child, and never reads them from the spec.
  • Legacy shorthand env: { FOO: bar } is treated as public-only.
  • When any public or private entry is present, the child does not inherit the full parent environment. It receives only: a small essential allowlist (PATH, HOME, USER, LANG, TERM, TMPDIR, XDG dirs, …), the private copies, and the public assignments. Spec path/dependencies still control PATH as before.
  • With an empty env, the child inherits the parent environment unchanged (aside from optional PATH prepends).
# shorthand (public only; restricts child env)
env:
  ISSUE_EDITOR: vim

# explicit sections
env:
  public:
    ISSUE_EDITOR: vim
  private:
    - GH_TOKEN

Typed inputs (inputs):

Declare named flags on a script (or leaf). jan parses --name value / --name=value from trailing args, applies defaults, and fails if a required input has no value and no default. Use ${{ inputs.name }} in env.public values and exec.argv strings.

backup:
  about: Backup a path
  inputs:
    path:
      description: Source directory
      required: true
    dest:
      default: ~/Backups
  env:
    public:
      SRC: ${{ inputs.path }}
  commands:
    run:
      exec:
        argv:
          - bash
          - -lc
          - rsync -a "$SRC" "${{ inputs.dest }}"
jan scripts misc backup run --path ~/Documents
jan scripts misc backup run --path ~/Documents --dest /mnt/backup
jan scripts misc backup run --help

Unknown long flags are left for passthrough when enabled; otherwise they error as unexpected trailing args. Use -- to stop input parsing and forward the rest.

At run time, jan:

  1. Merges dependencies, requires, env, inputs, and path from every node along the matched chain (deeper nodes override public env keys and input defs; private names are unioned).
  2. Resolves typed --input flags (and defaults), then interpolates ${{ inputs.* }} in env.public and exec.argv.
  3. Resolves transitive dependencies to other scripts' path directories (cycle-checked).
  4. Prepends those directories, then the script's own path, to PATH.
  5. Fails early if any requires binary is missing, or any env.private variable is unset.
  6. 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:

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/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:

jan use /path/to/dotfiles/jan
jan bundle -o ~/scripts-jan.zip
jan bundle --dry-run
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 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):

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.

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:

alias sum='jan scripts misc sum run'

Portable scripts workflow

See docs/PORTABLE_SCRIPTS.md for the full export/import flow.

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

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

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.