# 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](#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, packages, requires, env](#script-entries-path-dependencies-packages-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.
- 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](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 cron --list # scripts with time triggers
jan packages # cached uv envs for `packages.uv`
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 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`):
```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 |
| `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.
```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 or map | Load subtree from YAML, or link a script file as an exec leaf; local `{path, sha256?}` or remote `{url, sha256}` |
| `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 |
| `packages` | map | Package-manager deps (`uv` list / `project` / `requirements`; `pnpm` reserved) |
| `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) |
| `exec.file` / `exec.sha256` | strings | Run a local script under the jan tree (`sha256` optional) |
**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
- path: fragments/pinned.yaml # optional local pin
sha256: <64-hex>
- url: https://example.com/shared.yaml
sha256: <64-hex>
commands:
local:
about: Defined in this file
```
**Per-command** `include` grafts a file as that command's subtree (YAML) or as an
exec leaf (any other extension). Local includes do **not** require a hash
(convenient while editing); declare `sha256` only when you want an integrity pin.
Remote includes still require HTTPS + `sha256`.
```yaml
commands:
git:
include: default/git.yaml # YAML subtree (unhashed)
pinned:
include:
path: default/git.yaml
sha256: <64-hex> # optional local pin
shared:
include:
url: https://example.com/git.yaml
sha256: <64-hex>
basename:
about: Print last path component
commands:
run:
include:
path: scripts/files/basename.sh # script → exec leaf
argv: [bash] # optional interpreter prefix
passthrough: true
```
After load, jan retains the include as a link on the node (`jan show` prints
`source:`). `jan validate` re-checks any declared local hashes. `jan bundle`
packs linked script files along with YAML.
Local paths must stay under the preferred tree. See `examples/default.spec.yaml`
and a personal tree such as `dotfiles/jan` for real layouts.
### Local / remote `exec` scripts
```yaml
commands:
local-tool:
about: Run a script from the tree
exec:
file: bin/tool.sh # relative to jan use root
argv: [bash] # optional interpreter prefix
sha256: <64-hex> # optional local pin
passthrough: true
show-docs:
about: Run a verified remote script
exec:
url: https://example.com/show.py
sha256: <64-hex> # required for remote
argv: ["python3"]
passthrough: true
```
Without `argv`, jan runs the file/cache path directly (executable bit set on Unix).
With `argv`, the script path is appended after the prefix (e.g. `bash bin/tool.sh`).
`exec.url` and `exec.file` are mutually exclusive.
### `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.
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 single leading `--` after the leaf is always stripped for passthrough (`jan … run create` and `jan … run -- create` are equivalent; use `run -- --` for a literal first `--`).
### 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.
---
## 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.
```yaml
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"]
```
```bash
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, packages, requires, env
Script-style commands use extra fields so runners can call sibling scripts, pull
package deps, and require host tools.
### `packages` (uv, pnpm)
Declare language package deps separately from `dependencies:` (which names other
jan scripts). Identical declarations share a cached env under
`~/.cache/jan/packages/<manager>/<hash>/` (or `$JAN_CACHE_DIR/…`).
```yaml
# Inline pins
analyze:
about: Analyze a file
packages:
uv: [rich==14.1.0, pathspec==0.12.1]
commands:
run:
exec:
argv: ["python3", "-c", "import rich; …"]
passthrough: true
# Requirements file under the jan use root (every line pinned)
ingest:
packages:
uv:
requirements: deps/ingest.txt
exec:
argv: ["python3", "tools/ingest.py"]
# uv project directory (needs pyproject.toml and uv.lock)
train:
packages:
uv:
project: deps/ml
exec:
argv: ["python3", "-m", "train"]
```
On run, jan requires `uv` on PATH, ensures the hashed env, prepends its `bin/`
to the child PATH, and resolves `python` / `python3` from that venv.
`packages.pnpm` mirrors that for Node:
```yaml
# Inline pins
lint:
packages:
pnpm: [typescript@5.6.3, "@types/node@22.7.4"]
exec:
argv: ["tsc", "--noEmit"]
# pnpm project directory (needs package.json and pnpm-lock.yaml)
build:
packages:
pnpm:
project: deps/web
exec:
argv: ["node", "-e", "console.log(require('lodash').VERSION)"]
```
pnpm envs are installed in the cache, never in your tree: an inline list gets a
generated `package.json`, and `project:` copies `package.json` +
`pnpm-lock.yaml` into the env and runs
`pnpm install --frozen-lockfile --ignore-scripts`. On run, jan prepends
`node_modules/.bin` to PATH (so `tsc`, `zx`, … resolve), sets `NODE_PATH` to the
env's `node_modules` (so `require()` finds packages), and resolves argv[0] from
`.bin` when it matches. Workspace (`workspace:`) and local `file:` links are not
supported, because the manifest is installed outside its original directory.
`packages.gradle` resolves pinned Maven coordinates with the stable Gradle
Java Library plugin (Groovy DSL) into jars under
`~/.cache/jan/packages/gradle/<hash>/lib/`, then injects `CLASSPATH` for
`java` / `kotlinc -script`:
```yaml
# Inline pins (group:artifact:version)
greet:
packages:
gradle: [org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.6]
requires: [kotlinc]
exec:
argv:
- bash
- -lc
- 'kotlinc -script -cp "$CLASSPATH" packages/kotlin-greet.kts -- -n Ada'
# Gradle project directory (needs build.gradle(.kts) and gradle.lockfile)
app:
packages:
gradle:
project: deps/jvm
exec:
argv: ["bash", "-lc", "java -cp \"$CLASSPATH\" com.example.Main"]
```
Cold builds need `gradle` on PATH (`--no-daemon`); warm hits reuse the jar
cache without spawning Gradle. Project mode attaches an init script that
copies `runtimeClasspath` into the cache — the project must already apply a
JVM plugin that exposes that configuration (e.g. `java`, `java-library`, or
`org.jetbrains.kotlin.jvm`) and must commit a `gradle.lockfile` from dependency
locking.
#### Every dependency must pin an exact version
The env hash is only meaningful if the declaration resolves to one set of
versions, so jan rejects anything looser:
| `uv` inline list | Each entry needs exactly one `==` (or `===`) version — `pandas==2.0.1`. Extras (`pandas[perf]==2.0.1`) and markers (`…; python_version < "3.12"`) are fine |
| `uv` requirements file | Same rule per line; `-r` / `-e` lines are rejected so the file hash covers everything. Comments, `--index-url`, and `--hash=` options are allowed |
| `uv` project directory | Needs a `uv.lock`; jan installs the exported lock (plus the project with `--no-deps`) instead of re-resolving |
| `pnpm` inline list | Each entry needs a full `name@major.minor.patch` — `typescript@5.6.3`, `@types/node@22.7.4`. Prereleases (`zx@8.1.9-beta.1`) are fine |
| `pnpm` project directory | Needs a `pnpm-lock.yaml`; jan installs it with `--frozen-lockfile`, so ranges in `package.json` are fine |
| `gradle` inline list | Each entry needs `group:artifact:version` with an exact version — `com.google.guava:guava:33.3.1-jre`. No `+`, `*`, Ivy ranges, or `latest.release` |
| `gradle` project directory | Needs a `gradle.lockfile`; jan copies `runtimeClasspath` with `--frozen` semantics via the lockfile digest |
Rejected examples: `pandas`, `pandas>=2`, `pandas~=2.0`, `pandas==2.0.*`,
`pandas>=2,<3`, `typescript`, `typescript@latest`, `typescript@^5.6.3`,
`typescript@5.6`, npm aliases or git/file specs, and Maven dynamics like
`g:a:1.+` or `g:a:[1,2)`. A uv direct reference is
accepted only with a hash fragment
(`pkg @ https://…/pkg-1.0-py3-none-any.whl#sha256=<hex>`).
Unpinned inline entries fail when the YAML loads; file, lockfile, and project
rules are enforced on run and reported by `jan validate`.
Inspect the cached envs with [`jan packages`](#jan-packages).
Worked examples in [`examples/packages.spec.yaml`](examples/packages.spec.yaml) —
prefer it, then:
```bash
jan --cwd examples summarize-sales run # uv + pandas CSV rollup
jan --cwd examples pin-report run # pnpm versions via NODE_PATH
jan --cwd examples pin-report cow # binary from node_modules/.bin
jan --cwd examples kotlin-greet run --name Ada # gradle jars + kotlinc -script
```
### `dependencies` / `requires` / `env`
```yaml
path-demo:
about: Demo script that depends on a sibling for PATH
dependencies:
- path-demo-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).
```yaml
# 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.
```yaml
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 }}"
```
```bash
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:
| `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'
```
### `jan packages`
Inspect the package environments jan has materialized under
`<cache>/packages/<manager>/<hash>/`, cross-referenced with the preferred tree:
```bash
jan packages # same as `jan packages list`
jan packages list --json
jan packages list --pnpm # or --uv / --gradle, to scope to one manager
jan packages list --orphans # envs no script references anymore
jan packages show csv-summary # by script name, chain, or env hash prefix
jan packages show f16a95ee --json
jan packages path csv-summary --python
jan packages path pin-report --modules
jan packages path kotlin-greet --classpath
```
`list` prints one row per env (manager, hash, kind, runtime version, size on
disk, build age, and the command chains that use it) plus a `declared but not
built yet` section for declarations with no env — including ones whose
`project:` / `requirements:` path fails to resolve.
`show` adds the canonical declaration and the installed packages, read from
`*.dist-info` in the venv, `node_modules` in the pnpm env, or jar names under
`lib/` for gradle, so it works without the manager on PATH.
`path` prints a single path for scripting:
```bash
"$(jan packages path csv-summary --python)" -c 'import pandas; print(pandas.__version__)'
NODE_PATH="$(jan packages path pin-report --modules)" node -e "console.log(require('lodash').VERSION)"
CLASSPATH="$(jan packages path kotlin-greet --classpath)" kotlinc -script -cp "$CLASSPATH" packages/kotlin-greet.kts
```
| `--uv` / `--pnpm` / `--gradle` | Scope to one manager (`list`, `show`, `path`) |
| `--json` | Machine-readable output (`list`, `show`) |
| `--orphans` | `list`: only envs no script in the tree references |
| `--bin` | `path`: print the bin directory (venv `bin`, `node_modules/.bin`, or gradle `lib`) |
| `--python` | `path`: print the venv interpreter (uv only) |
| `--modules` | `path`: print the `node_modules` directory (pnpm only) |
| `--lib` | `path`: print the jar `lib` directory (gradle only) |
| `--classpath` | `path`: print the resolved classpath string (gradle only) |
Envs are keyed by declaration, so scripts with identical `packages:` share a
row, and a `help` leaf under a node that declares packages is listed as a user
too (it inherits the declaration). A script selector that resolves to both a uv
and a pnpm env asks you to narrow with `--uv` / `--pnpm`.
---
## 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
# packages.uv + pandas CSV summary
jan use examples --root packages.spec.yaml
jan --cwd examples summarize-sales run
jan --cwd examples summarize-sales run --product gadget
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`.