Skip to content

feat(cli): print a profile's scope as dotenv lines with context --format env - #129

Merged
thebrandonlucas merged 4 commits into
brandon/dev-2956-voltage-cli-commands-ignore-voltage_-variables-under-afrom
brandon/dev-2957-voltage-cli-no-way-to-hand-a-profiles-scope-to-another-tool
Nov 4, 2026
Merged

thebrandonlucas merged 4 commits into
brandon/dev-2956-voltage-cli-commands-ignore-voltage_-variables-under-afrom
brandon/dev-2957-voltage-cli-no-way-to-hand-a-profiles-scope-to-another-tool

Conversation

@thebrandonlucas

@thebrandonlucas thebrandonlucas commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes DEV-2957
Closes #122

Problem

Profiles hold the CLI's scope, but other Voltage tools cannot read them. The voltage-api skill's helper takes scope only from VOLTAGE_* variables or a dotenv file. After switching profiles, a person or agent had to copy the IDs by hand or turn voltage context --json into variables with jq and knowledge of its shape. A forgotten copy left the helper on the old environment.

Behavior

voltage context --format env prints the effective organization, environment, and wallet as dotenv lines, under the names the helper reads (CONFIG_KEYS in its scripts/request.py):

$ voltage context --profile staging --wallet WALLET_ID --format env
VOLTAGE_ORGANIZATION_ID=ORG_ID
VOLTAGE_ENVIRONMENT_ID=ENV_ID
VOLTAGE_WALLET_ID=WALLET_ID
$ voltage context --profile staging --format env > scope.env
Case Result
A value is unset (no wallet, say) Its line is left out, never printed empty, because the helper treats an empty variable as an override. Nothing set prints nothing, exit 0.
More than one environment (repeated --env) error: --format env takes one environment, but 2 are selected, hint Select one with --env ENV_ID., exit 2, nothing on stdout.
With --json, --output, or --output-file, before or after context error: --format cannot be combined with --json (or the flag given), with a hint to redirect stdout, exit 2.
Credentials Never printed. context reads no saved secret, and neither does this format. The docs say the other tool needs a key from elsewhere, for the printed organization and environment.

Values resolve exactly as voltage context resolves them: flags, then the profile, then the VOLTAGE_* variables.

Design notes

  • LocalCommand::Context becomes Context(ContextFlags) with --format <FORMAT>, a ContextFormat value enum with one value, env. An enum leaves room for another format without a new flag.
  • Context::dotenv builds the lines from the resolved Context, so the format cannot drift from voltage context. startup::execute writes them with write_stdout instead of a result envelope.
  • The conflict with --json, --output, and --output-file is checked on the typed flags (ContextFlags::check, called from Invocation::from_matches). A clap conflicts_with on the subcommand flag does not see a global flag given before context, so voltage --json context --format env would have passed.
  • scripts/voltage-staging and scripts/voltage-local pass their state directory as VOLTAGE_CONFIG_DIR, not --config-dir (e43d367). Hints the CLI prints then do not name a flag the wrapper refuses. fix(cli): warn when a profile or flag overrides a set VOLTAGE_* variable #128 makes the same change, so whichever PR merges second merges cleanly.
  • The help for voltage context gains a --format env > scope.env example; the help-examples test parses it.

Try it on staging

Setup: Try a pull request on staging

alias voltage="$PWD/scripts/voltage-staging"
voltage --build
voltage login --account me --credential-store file

Find an organization with voltage organizations list --account me and an environment with voltage environments list --account me --org ORG_ID:

voltage profiles create try-it --account me --org ORG_ID --env ENV_ID
voltage context --profile try-it --format env

Expect: exactly two lines, VOLTAGE_ORGANIZATION_ID=ORG_ID and VOLTAGE_ENVIRONMENT_ID=ENV_ID, and nothing that looks like a token or key. If .env sets VOLTAGE_ORGANIZATION_ID or VOLTAGE_ENVIRONMENT_ID, the values still come from the profile.

Find a wallet with voltage wallets list --profile try-it:

voltage context --profile try-it --wallet WALLET_ID --format env
voltage context --profile try-it --format env --env ENV_ID --env 00000000-0000-4000-8000-000000000000; echo "exit $?"
voltage --json context --profile try-it --format env; echo "exit $?"
voltage context --help
voltage profiles delete try-it

Expect:

  • A third line, VOLTAGE_WALLET_ID=WALLET_ID.
  • --format env takes one environment, but 2 are selected, then exit 2.
  • --format cannot be combined with --json, then exit 2.
  • The help shows the --format env > scope.env example.
  • The delete removes the profile.

Validation

Run on the head after merging master (445e6b5):

  • cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, cargo machete --with-metadata, cargo test --locked (200 unit, 150 integration), and python3 scripts/check-coverage.py all pass.
  • New integration test context_env_format_prints_only_set_scope_and_never_a_credential: exact stdout for a profile plus --wallet, no saved or ambient key in it, only the organization when only --org is set, empty output when nothing is set, and exit 2 with empty stdout for two environments.
  • New unit test context_format_refuses_a_result_format_or_file_before_or_after_the_command: --json before and after context, -o table, and --output-file are each refused with the flag named; context --json alone still parses; --format shell is rejected by clap. The parse test also checks --format env parses to ContextFormat::Env.
  • CRAP (scripts/crap-report.sh, cargo-llvm-cov 0.8.5, knots 1.16.0): 848 of 1060 functions measured, 0 violations. The highest score is 18.10, wait_for_payment in src/api.rs, which this PR does not change. Changed functions: startup::execute 16.0 (was 14.0, at 100% coverage), context::inspect 13.01, Invocation::from_matches 7.01, ContextFlags::check 5.0, Context::dotenv 2.0.
  • Staging, with the PR's binary and an existing profile: --format env printed the organization and environment lines, and a third wallet line with --wallet. Two --env values and --json each exited 2 with the documented message. A login with no scope printed nothing.

Limits

  • When stdout is redirected (> scope.env), an error is reported as JSON on stderr, like every other command whose stdout is not a terminal. It is still exit 2 with nothing written to stdout.
  • With DEV-2956 (Commands ignore VOLTAGE_* variables under a profile without a warning #121) merged as well, --format env under a profile also gets the stderr warning for each ignored VOLTAGE_* variable, which is the case where handing the scope over matters most.

Checklist

  • User-visible change: a guide or reference under docs/ and CHANGELOG.md (Unreleased) are updated. (docs/guides/login-and-profiles.md, docs/guides/scripting.md)
  • The verification gate in docs/development.md passes.
  • The diff was reviewed against its base with the voltage-pr-review skill, and every new error and early-return path has a test.
  • CRAP report: highest 18.10 (wait_for_payment, unchanged); no function at or above 20.
  • Checks not run, and why: the profiles create try-it steps were not run, to avoid changing the shared staging config. The same checks ran with an existing profile.

Stack

Stack #135, layer 4 of 9 (bottom targets master):

# PR Change
1 #97 Staging setup and a Try it section for each PR
2 #120 --all on organizations, environments, and profiles list
3 #128 Warn when a profile or flag overrides a set VOLTAGE_* variable
4 #129 (this PR) voltage context --format env
5 #127 Replace hand-rolled terminal code with crossterm
6 #126 Pick the wallet, organization, or environment to show
7 #130 Prefilled GitHub issue links from voltage feedback and panics
8 #123 voltage environments create
9 #131 Replace flake.nix with a Kaifile

The layers are chained with merge commits, because history is not rewritten. Use squash and merge, bottom first.

…mat env

Other Voltage tools, such as the voltage-api skill's helper, take scope
only from VOLTAGE_* variables or a dotenv file, so handing them a
profile's scope meant copying IDs by hand or parsing `context --json`.
`voltage context --format env` prints the effective organization,
environment, and wallet under those names. It leaves out unset values,
never prints a credential, refuses more than one environment with exit
code 2, and refuses --json, --output, and --output-file before or after
the command.
The hint after `profiles create` repeats --config-dir, which the
staging and local wrappers refuse. Passing the directory as a variable
keeps the hint runnable through the wrappers.
…ariables-under-a' into brandon/dev-2957-voltage-cli-no-way-to-hand-a-profiles-scope-to-another-tool

# Conflicts:
#	CHANGELOG.md
#	src/context.rs

@tee8z tee8z left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved at a64f074. ContextFlags::check on the typed flags catches --json, -o, and --output-file before or after context, which a clap conflicts_with would miss. Context::dotenv builds from the resolved context, leaves out unset values, refuses two environments, and prints no credential. None of the output flags read an environment variable, so the check cannot refuse a run that had no flag.

CRAP on this head: 860 of 1073 functions measured, 0 violations. Highest: 18.10 wait_for_payment (unchanged). CI is green.

@thebrandonlucas
thebrandonlucas merged commit 0915b61 into master Nov 4, 2026
7 checks passed
@thebrandonlucas
thebrandonlucas deleted the brandon/dev-2957-voltage-cli-no-way-to-hand-a-profiles-scope-to-another-tool branch November 4, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No way to hand a profile's scope to another tool

2 participants