Skip to content

fridgeops/pro

Repository files navigation

pro

Crates.io CI Downloads License: MIT

Run commands inside a named profile context. Each profile defines environment variables, setup hooks, preflight checks, and named scripts — all in a single TOML file.

Avoiding context drift

When working across multiple environments (clusters, accounts, tenants), the risk is running a command against the wrong one. pro provides layered defense:

  • Separate KUBECONFIG per profile — structural isolation; no ambient state bleeds between profiles
  • preflight checks — assertions that run before every exec/run; abort if the environment isn't what you expect
  • Active profile trackingpro setup records which profile is active; exec/run hard-fail if you call with a different profile name
  • pro status — see what's active before you run anything

Install

cargo build --release
cp target/release/pro /usr/local/bin/pro

Or via cargo install:

cargo install --path .

Quick start

pro init          # scaffold a pro.toml in the current directory
pro config-help   # show all available TOML keys

Add a profile to pro.toml:

[default]
shell = "sh"

[default.vars]
LOG_LEVEL = "info"

[profiles.dev]
description = "Local development"

[profiles.dev.vars]
APP_ENV = "dev"
API_URL = "http://localhost:3000"
LOG_LEVEL = "debug"

[profiles.dev.scripts]
start = "npm run dev"
test  = "cargo test"

Then:

pro list                    # show available profiles
pro show dev                # show resolved config for dev
pro exec dev -- node app.js # run a command with dev vars
pro run dev start           # run a named script
pro env dev                 # print export KEY=value for each var

Config file

Default search order (first found wins):

  1. ./pro.toml
  2. ./.pro.toml
  3. ~/.config/pro/config.toml

Override with -c / --config:

pro -c ./path/to/pro.toml run dev start

Full example

[default]
shell = "sh"

[default.vars]
LOG_LEVEL = "info"
APP_NAME  = "myapp"
BASE_URL  = "http://localhost"
API_URL   = "${BASE_URL}:3000"   # $VAR / ${VAR} interpolation

[default.scripts]
lint = "eslint ."


[profiles.dev]
description = "Local development"
dir = "./packages/app"           # working directory for scripts/exec

[profiles.dev.vars]
APP_ENV   = "dev"
LOG_LEVEL = "debug"              # overrides default

[profiles.dev.setup]
commands = [
  "npm install",
  "cargo fetch",
]

[profiles.dev.scripts]
start = "npm run dev"
test  = "cargo test"


[profiles.stage]
description = "Staging environment"

[profiles.stage.vars]
APP_ENV = "stage"
API_URL = "https://stage.example.com"

[profiles.stage.scripts]
deploy = "./deploy.sh stage"

Commands

pro list

Print all available profiles (default first).

pro show [profile]

Print the resolved configuration for a profile (defaults merged in). Omit profile to show [default].

pro check [profile]

Run the profile's preflight checks. Exits non-zero if any check fails. Omit profile to check [default].

pro exec [profile] -- <command...>

Run a command with profile environment variables. Preflight runs first in the same subprocess, so env mutations in preflight are visible to the command. Omit profile to use [default].

pro exec -- node app.js                    # default profile
pro exec dev -- npm start                  # dev profile
pro exec dev --setup -- cargo build        # run setup first
pro exec dev --skip-preflight -- cargo run # skip preflight

Flags:

Flag Description
--setup Run setup commands before the command
--skip-preflight Skip preflight checks
--ignore-active Warn instead of failing on active profile mismatch

pro run [profile] [script]

Run a named script from [profiles.<profile>.scripts]. Omit profile to use [default]. Omit script to list available scripts.

pro run                  # list scripts for default profile
pro run start            # run default profile's "start" script
pro run dev start        # run dev profile's "start" script
pro run dev test --setup # run setup first

Flags:

Flag Description
--setup Run setup commands before the script
--skip-preflight Skip preflight checks
--ignore-active Warn instead of failing on active profile mismatch

pro setup [profile]

Run the profile's setup.commands in order. Stops on first failure. Omit profile to run [default] setup.

On success, writes .pro-active next to pro.toml to record the active profile. Add .pro-active to your .gitignore.

pro status

Print the currently active profile (set by pro setup):

active: dev   (/path/to/.pro-active)

Or no active profile set if pro setup has not been run in this directory.

pro env [profile]

Print export KEY=value for each resolved profile variable. Useful for sourcing into a shell session.

eval "$(pro env dev)"

pro init

Scaffold a pro.toml with commented placeholders in the current directory. Fails if one already exists.

pro config-help

Print a reference of all available TOML keys with descriptions.

pro completions <shell>

Print a shell completion script. Supported: bash, zsh, fish, elvish, powershell.

pro completions zsh > ~/.zfunc/_pro

The [default] section

Values in [default] are inherited by all profiles. Profile values win on conflict; setup.commands and preflight.commands lists are concatenated (default runs first).

[default.vars]
LOG_LEVEL = "info"

[profiles.dev.vars]
LOG_LEVEL = "debug"   # overrides default

Merge behavior:

Field Behavior
vars key-level merge; profile wins on conflict
scripts key-level merge; profile wins on conflict
setup.commands concatenated; default runs first
preflight.commands concatenated; default runs first
shell profile wins; falls back to default; then sh
dir profile wins; falls back to default

Use "default" as a profile name to target [default] directly:

pro show default
pro run default lint

Setup

setup.commands run in order when you call pro setup. Stop on first failure. Use setup for one-time or session-start work: authentication, dependency installation, tool version pinning.

[profiles.prod.setup]
commands = [
  "kubelogin convert-kubeconfig -l azurecli",
  "npm install",
]

A command that exactly matches a script name runs that script's body:

[profiles.dev.scripts]
install = "npm install && cargo fetch"

[profiles.dev.setup]
commands = ["install"]   # runs the install script body

Preflight

preflight.commands run before every exec and run in the same subprocess as the command. This means env mutations in preflight are visible to the command that follows — useful for shell-env-based version managers.

[profiles.dev]
shell = "bash"

[profiles.dev.preflight]
commands = [
  "source ~/.nvm/nvm.sh",
  "nvm use 20",             # modifies PATH; exec'd command sees it
]

Like setup commands, a preflight command that exactly matches a script name runs that script's body:

[profiles.prod.scripts]
check-context = "kubectl config current-context | grep -q prod-cluster"

[profiles.prod.preflight]
commands = ["check-context"]

Skip with --skip-preflight. Run standalone with pro check [profile].

Working directory (dir)

Set dir to run scripts, setup commands, and exec'd processes in a specific directory. ~ is expanded. Inherited from [default] and overridable per profile.

[profiles.frontend]
dir = "./packages/web"

[profiles.frontend.scripts]
dev = "npm run dev"   # runs from ./packages/web

Var interpolation

Use $VAR or ${VAR} in var values. References are expanded against profile vars (resolved first) and the current process environment.

[default.vars]
BASE = "http://localhost"
API  = "${BASE}:8080"       # → http://localhost:8080
BIN  = "$HOME/bin"          # → /Users/you/bin

Shell selection

Controls how scripts, setup commands, and preflight commands run. All three use the profile shell and share a subprocess during exec/run.

Value Invocation
sh (default on Unix) sh -c "<cmd>"
bash bash -c "<cmd>"
nu nu --no-config-file -c "<cmd>"
other <shell> -c "<cmd>"

Environment variables

Profile vars are merged on top of the current process environment. Profile values override; everything else (including PATH) is preserved. All TOML types convert to strings:

PORT    = 3000    # → "3000"
DEBUG   = true    # → "true"
TIMEOUT = 2.5     # → "2.5"

Best practices for Kubernetes and cloud environments

Use separate KUBECONFIG files per profile

Point each profile at its own kubeconfig file for structural isolation — no ambient context state can bleed across profiles.

[profiles.dev.vars]
KUBECONFIG = "~/.kube/configs/dev.yaml"
NAMESPACE  = "myapp-dev"

[profiles.prod.vars]
KUBECONFIG = "~/.kube/configs/prod.yaml"
NAMESPACE  = "myapp-prod"

Use setup for auth, preflight for validation

Run authentication once with pro setup. Use preflight to assert the environment is correct before every command.

[profiles.prod.scripts]
auth          = "kubelogin convert-kubeconfig -l azurecli"
check-context = "kubectl config current-context | grep -q prod-cluster"
check-account = "az account show --query id -o tsv | grep -q $AZURE_SUBSCRIPTION_ID"

[profiles.prod.setup]
commands = ["auth"]

[profiles.prod.preflight]
commands = ["check-context", "check-account"]
pro setup prod               # authenticate once; records active=prod
pro exec prod -- kubectl get pods -n $NAMESPACE
pro run prod deploy          # preflight validates context before every run

Active tracking catches context drift

pro setup records the active profile in .pro-active. exec and run refuse to proceed if the active profile doesn't match:

error: active profile is "prod" but exec was called with "dev"
hint: run `pro setup dev` to switch, or pass --ignore-active to proceed anyway
pro status                   # active: prod   (/path/to/.pro-active)
pro exec dev -- ...          # hard fails — run pro setup dev first

Exit codes

Code Meaning
0 Success
1 General failure
2 Config error
3 Profile not found
child code Propagated from child process

About

Profile based shell management

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors