# Portable scripts via jan-cli — implementation summary
This document summarizes work to move the incubator `scripts` collection between machines using `jan-cli`, YAML specs, and built-in `bundle` / `alias` commands.
## Goal
Turn `scripts/source/` into a **portable, categorized `jan` command tree** that can be:
1. Generated from source in the monorepo
2. Exported as a ZIP bundle
3. Imported on another machine (with `jan` installed)
4. Invoked via `jan scripts …` or shell aliases
## Architecture
```text
scripts/source/<name>/ # canonical script dirs (216-ish)
│
▼
generate_scripts_jan_spec.py # Python generator
│
▼
jan-cli/generated/scripts/ # categorized YAML fragments
│
▼
jan-cli/scripts.spec.yaml # entry spec (include index)
│
▼
jan bundle # ZIP + env.sh + manifest.json
│
▼
target machine: unzip, source env.sh, jan / aliases
```
Each script in the tree has:
- **`help`** — prints name, description, optional requires/deps/env
- **`run`** — inlines script dir to a temp folder, runs primary executable, forwards args
Categories (11): `git`, `android`, `docker`, `network`, `media`, `files`, `text`, `tmux`, `time`, `system`, `misc`.
## Files created
| `jan-cli/docs/PORTABLE_SCRIPTS.md` | User guide: export, import, env vars, limitations |
| `jan-cli/scripts/jan-install.sh` | Unpack bundle to `~/.config/jan/scripts` |
| `jan-cli/scripts/generate_scripts_jan_spec.py` | Regenerate YAML from `scripts/source` |
| `jan-cli/scripts.spec.yaml` | Root spec pointing at generated tree |
| `jan-cli/generated/scripts/*.yaml` | Auto-generated category fragments + index |
| `jan-cli/generated/scripts/manifest.json` | Generator manifest (SHA-256 per category file) |
| `jan-cli/src/builtins.rs` | `bundle` and `alias` implementations |
| `jan-cli/src/yaml_closure.rs` | Collect transitive YAML `include:` graph for bundling |
| `jan-cli/xyz_summary.md` | This file |
## Files modified
| `jan-cli/src/lib.rs` | Modules for builtins, yaml_closure, deps; `CommandNode` fields: `path`, `dependencies`, `requires`, `env` |
| `jan-cli/src/runner.rs` | Route `bundle` / `alias` before user command matching |
| `jan-cli/src/deps.rs` | PATH/env/requires at run time; skip missing paths/deps on portable hosts |
| `jan-cli/Cargo.toml` | `zip` dependency |
| `jan-cli/README.md` | Document bundle, alias, portable scripts link |
| `jan-cli/tests/cli_smoke.rs` | Portability integration tests |
## Removed / superseded
- `jan-cli/generated/scripts-flat.yaml` — replaced by categorized `generated/scripts/` layout
## Generator highlights
`scripts/generate_scripts_jan_spec.py`:
- Walks `scripts/source/*/script.meta.yaml`
- Picks primary executable per dir (`<name>.{sh,zsh,py,js,…}`)
- **Inlines entire script directory** (not only the main file) into a bash wrapper
- Resolves **`dependencies`** from `script.meta.yaml` (e.g. `trackusage` → `trackusage-impl`) into `$tmpdir/bin` on PATH
- **Help text** from: `description` / `about` in meta → README `## Description` → script header comments → fallback
- **Category** from meta `category:` or name heuristics
- Sets **`path: ../scripts/source/<name>`** for dev-machine PATH resolution via `deps.rs`
- Writes per-category YAML + index; emits `generated/scripts/manifest.json`
## Built-in commands
### `jan bundle`
- Collects all YAML reachable via `include:` under the spec anchor
- Verifies paths stay under the anchor directory
- Writes ZIP with:
- All YAML (relative paths preserved)
- **`env.sh`** — sets `JAN_SPEC_DIR` (relocatable) and `JAN_SPEC_ROOT`
- **`manifest.json`** — `jan` version, timestamp, optional git SHA, per-file SHA-256 + size
- Flags: `--dry-run`, `-o/--output`, `--include-extra`
- Does not work with embedded default spec (`jan://embedded`)
### `jan alias`
- Finds script `run` leaves in the merged spec
- Emits `alias <canonical-name>='jan --spec-dir "$JAN_SPEC_DIR" --spec-root … scripts <cat> <name> run'`
- Flags: `--jan-bin`, `--spec-dir`, `--spec-root`, `--shell`, `-o/--output`
- Collisions on leaf name: longest chain wins
## Typical workflows
### Regenerate (after editing `scripts/source`)
```bash
python3 jan-cli/scripts/generate_scripts_jan_spec.py
```
### Export
```bash
cd jan-cli
cargo build --release
./target/release/jan --spec-dir . --spec-root scripts.spec.yaml bundle -o ~/scripts-jan.zip
```
### Import
```bash
bash jan-cli/scripts/jan-install.sh ~/scripts-jan.zip
source ~/.config/jan/scripts/env.sh
jan scripts files basename run -- /tmp/foo/bar.txt
```
### Aliases
```bash
jan --spec-dir "$JAN_SPEC_DIR" --spec-root scripts.spec.yaml alias --shell zsh \
-o ~/.config/jan/scripts/aliases.zsh
source ~/.config/jan/scripts/aliases.zsh
```
## Environment variables
| `JAN_SPEC_DIR` | Directory containing `scripts.spec.yaml` and `generated/` |
| `JAN_SPEC_ROOT` | Entry YAML filename (default `scripts.spec.yaml`) |
| `JAN_SPEC` | Alternative: absolute path to entry YAML |
| `JAN_SCRIPTS_ROOT` | Optional base for resolving `path:` in specs |
## Tests added
In `tests/cli_smoke.rs`:
- `scripts_bundle_dry_run_lists_portable_artifacts`
- `scripts_alias_includes_spec_flags_and_sum`
- `scripts_inlined_basename_run`
- `scripts_trackusage_help_lists_dependency`
All `cargo test` in `jan-cli` pass.
## Known limitations
- Target machine must have **`jan`** and per-script tools (`git`, `python3`, `bc`, etc.)
- Bundle is **YAML only** — does not ship the `jan` binary
- Scripts that `source` files **outside** their directory may still fail
- Legacy `scripts/install.sh` / zsh completions are **not** ported
- ~215 scripts generated (dirs without a runnable file are skipped)
- Inlined YAML is ~500KB; acceptable for personal use, may grow
## Trust
Bundles contain executable script source. Only import bundles from trusted sources. `manifest.json` includes SHA-256 hashes for verification.