This README is provided in English and 中文 (Chinese).
语言切换:下方包含 English 和 中文,内容一致。
License: SeeLICENSEat the repository root.
lyenv is a simple, robust, and language-agnostic environment manager based on a directory layout. It lets you:
- Create and activate isolated workspaces (
bin/,plugins/,workspace/,.lyenv/). - Install and run plugins implemented in any language (Python, Node.js, C/C++, etc.), via:
- shell executor (run command lines and capture logs),
- stdio executor (exchange JSON over stdin/stdout, enabling structured results and configuration mutations).
- Merge structured configuration from plugins (
mutations) into:- global config (
lyenv.yaml), - plugin-local config (
plugins/<INSTALL_NAME>/config.yamlor.jsonby extension).
- global config (
- Record per-command JSON Lines logs and a global dispatch log for observability.
- Resolve plugins by name from a Plugin Center (Monorepo) with subdirs, optionally using archive + SHA‑256 integrity verification.
Why this matters:
- Language-agnostic: Any tool that can run in shell or read/write JSON over stdio is supported.
- Reproducible & Traceable: Directory-based design, JSON logs, and dispatch records simplify auditing and debugging.
- Structured orchestration:
stdioenables returning structured results and safe config mutations, avoiding brittle text scraping. - Scalable distribution: Plugin Center can host many plugins as subdirectories or prebuilt archives with SHA‑256 verification.
make build
# or
go build -o ./dist/lyenv ./cmd/lyenv./dist/lyenv create ./my-env
./dist/lyenv init ./my-env
cd ./my-env
eval "$("./../dist/lyenv" activate)"After create:
Directory structure:
.lyenv/, .lyenv/logs, .lyenv/registry, bin/, cache/, plugins/, workspace/.
lyenv.yaml defaults (center URL included):
env:
name: "default"
platform: "auto"
path:
bin: "./bin"
cache: "./cache"
workspace: "./workspace"
plugins:
installed: []
registry_url: "https://raw.githubusercontent.com/systemnb/lyenv-plugin-center/main/index.yaml"
registry_format: "yaml"
default_version_strategy: "latest"
config:
use_container: false
pkg_manager: "auto"
network:
proxy_url: "" # set to e.g. http://127.0.0.1:7890 if neededRegistry file initialized: .lyenv/registry/installed.yaml:
plugins: []All program outputs are in English. Below are the major commands.
lyenv create <DIR>
# Create a new environment with default folders and config
lyenv init <DIR>
# Verify/repair an existing environment (idempotent)
lyenv activate
# Print shell snippet; typically: eval "$(lyenv activate)"
# The snippet is robust in non-interactive shells (checks PS1 existence)lyenv config set <KEY> <VALUE> [--type=string|int|float|bool|json]
# Set a value under lyenv.yaml (dot path with type enforcement)
lyenv config get <KEY>
# Read a value by dot path
lyenv config dump [<KEY>] <FILE>
# Dump entire config or a specific key to FILE (YAML/JSON by extension)
lyenv config load <FILE> [--merge=override|append|keep]
# Load YAML or JSON overlay into lyenv.yaml with merge strategy
lyenv config importjson <FILE> <JSON_KEY> [--to=<CONFIG_KEY>] [--type=...] [--merge=...] [--input=1]
# Import from JSON file (dot path) into lyenv.yaml
lyenv config importyaml <FILE> <YAML_KEY> [--to=<CONFIG_KEY>] [--type=...] [--merge=...] [--input=1]
# Import from YAML file (dot path) into lyenv.yamllyenv plugin center sync
# Cache center index into .lyenv/registry/index.yaml or .json
lyenv plugin search <KEYWORDS...>
# Search plugin center by name/description keywordsCenter index default: https://raw.githubusercontent.com/systemnb/lyenv-plugin-center/main/index.yaml
You can override via:
lyenv config set plugins.registry_url <URL> --type=stringlyenv plugin add <PATH> [--name=<INSTALL_NAME>]
# Install local directory plugin under custom install name
lyenv plugin install <NAME|PATH> [--name=<INSTALL_NAME>] [--repo=<org/repo>] [--ref=<branch|tag|commit|version>] [--source=<url>] [--proxy=<url>]
# Install from local path, remote repo, source archive or center name
# - NAME only: resolve from center; prefer archive+sha256 if present, else monorepo subpath
lyenv plugin update <INSTALL_NAME> [--repo=<org/repo>] [--ref=<branch|tag|commit|version>] [--source=<url>] [--proxy=<url>]
# Update installed plugin (git/center source)
lyenv plugin info <INSTALL_NAME|LOGICAL_NAME>
# Show manifest details, resolved directory and shims
lyenv plugin list [--json]
# List installed plugins (JSON for machine-readable)
lyenv plugin remove <INSTALL_NAME> [--force]
# Uninstall plugin and remove related shims
# If shell still resolves shim name after removal, run: hash -rNotes:
- Shims bind to the install name (physical directory under plugins/).
- Shims prefer env var
LYENV_BINpath; fallback to lyenv in PATH. - Windows shims
.cmd/.ps1also supported (generation carried but tested here on Linux).
lyenv run <PLUGIN> <COMMAND> [--merge=override|append|keep] [--timeout=<sec>] [--fail-fast|--keep-going] [-- ...args]
# Execute plugin command
# Examples:
lyenv run testtools run --merge=override --keep-going
lyenv run testtools slow --timeout=5 --fail-fast- shell: Runs
bash -c "<program + args>". Captures stdout/stderr into JSON Lines logs. - stdio: Sends a JSON request to stdin; expects JSON response with:
status(e.g., ok),logs(array of strings echoed to console),artifacts(array of paths),mutations:global(merged into lyenv.yaml),plugin(merged into plugin-local config; original format preserved YAML/JSON by extension).
Multi-step: Compose multiple steps (shell/stdio mixed) with continue_on_error. Global --keep-going overrides per-step; --fail-fast stops on first error.
Timeout: Global deadline (sec). Uses exec.CommandContext so child processes are canceled when deadline is reached.
Plugin manifests support both formats and the following fields (common subset):
name(string, required)version(string, required)expose(array of shim names, required)config.local_file(optional path to plugin-local config; YAML or JSON by extension)commands: array of command specs:name(string, required, unique)summary(string)- Either:
- Single command:
executor(shell or stdio)program(string; command or plugin-relative path)args(array of strings)workdir(string, plugin-relative or absolute)env(map of string environment variables)use_stdio(bool; for stdio)
- Or multi-step:
steps: array of sub-commands with same fields per step, pluscontinue_on_error(bool)
- Single command:
entry: optional default stdio entry:type: "stdio"path(string)args(array of strings)
- shell: best for simple commands without structured return. Logs are captured automatically.
- stdio: best for structured exchange:
- Request JSON includes
action,args,paths,system,config,merge_strategy,started_at. - Response JSON can include
mutationsto be merged safely by core with specified strategy (override/append/keep).
- Request JSON includes
Install/update normalize permissions:
- Directories: 0755,
- Regular files: 0644,
- Files with shebang (
#!/...): 0755.
Logs:
- Per plugin command:
plugins/<INSTALL_NAME>/logs/YYYY-MM-DD/<COMMAND>-<TIMESTAMP>.log(JSON Lines: info, stdout, stderr, etc.). - Global dispatch log:
.lyenv/logs/dispatch.log.
A single repository hosts many plugins in plugins/<NAME>/. The center index (YAML/JSON) maps <NAME> to either:
repo,ref,subpath(monorepo checkout),- or
versions[<ver>].source(ZIP/TGZ URL) +versions[<ver>].sha256for archive distribution.
Center index example (YAML):
apiVersion: v1
updatedAt: 2026-01-01T12:00:00Z
plugins:
tester:
desc: "Mixed steps demo"
repo: "systemnb/lyenv-plugin-center"
subpath: "plugins/tester"
ref: "main"
shims: ["tctl"]
versions:
"0.1.0":
source: "https://raw.githubusercontent.com/systemnb/lyenv-plugin-center/main/artifacts/tester-0.1.0.zip"
sha256: "<64 hex sha>"
shims: ["tctl"]- NAME only: lyenv will resolve from center:
- If
source+sha256present → download, verify SHA‑256, extract, install. - Else → clone monorepo (shallow) and copy subpath.
- If
Center repo workflow (PR-based) generates:
artifacts/<NAME>-<VERSION>.zipwith all plugin files,index.yamlwith source and sha256 entries,- creates a PR; upon merging, source raw URLs are valid.
We provide a workflow (.github/workflows/e2e.yml) that:
- Checks out and builds lyenv,
- Sets up Python (PyYAML) for YAML parsing,
- Adds
dist/to PATH, - Runs
scripts/full_e2e_test.sh:- Environment create/init/activate,
- Center sync/search,
- Install (center name),
- Run (shell+stdio, multi-step),
- Verify logs/mutations,
- Timeout/fail-fast,
- Uninstall and shim removal,
- Local plugin add/run/remove,
- Archive+SHA‑256 validation (if center provides),
- Config dump/load (JSON),
- Dispatch log inspection,
- Uploads logs as artifacts.
- Shim still present after removal: flush shell cache
hash -r; checktype -a <shim>/which -a <shim>for other instances in PATH. fork/exec ... no such file or directoryfor stdio script:- Ensure the script has executable bit (
chmod +x) and LF line endings, - Proper shebang (
#!/usr/bin/env python3), python3in PATH (considermanifest.env.PATH).
- Ensure the script has executable bit (
- Timeouts:
context deadline exceededindicates global deadline; increase--timeoutor reduce step durations. - Center index missing archive entries: run center CI to generate
artifacts/*.zipandindex.yamlwithsource+sha256, then merge PR.
- Add plugins to center repo under
plugins/<NAME>/withmanifest.yaml|yml|jsonand required files. - Center CI will generate artifacts and index via PR.
- For local development:
lyenv plugin add ./plugins/<NAME> --name=<INSTALL_NAME> lyenv run <INSTALL_NAME> <COMMAND>
This project is licensed under the terms in LICENSE.