Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion .cli-flags.toml
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,46 @@ aliases = ["un"]
help = "Remove materialized dependencies while retaining the lockfile."

[commands.env]
help = "Import and verify project-local developer environments."
help = "Import, verify, and export project-local developer environments."

[commands.env.commands.import]
help = "Import manager-native state as a normalized EnvironmentPlan."

[commands.env.commands.verify]
help = "Verify config/lock coverage and normalized environment identity."


[commands.env.commands.export]
help = "Project a schema-v2 EnvironmentPlan into deterministic manager configuration."


[commands.env.commands.export.flags.env_plan]
env = "ZED_PKG_ENV_PLAN"
aliases = ["plan"]
type = "string"
help = "Project-local schema-v2 environment plan path."

[commands.env.commands.export.flags.env_output]
env = "ZED_PKG_ENV_OUTPUT"
aliases = ["output"]
type = "string"
default = ".mise.toml"
help = "Project-local manager export path."

[commands.env.commands.export.flags.env_check]
env = "ZED_PKG_ENV_CHECK"
aliases = ["check"]
type = "bool"
default = "false"
help = "Verify the deterministic manager projection without writing."

[commands.env.commands.export.flags.env_write]
env = "ZED_PKG_ENV_WRITE"
aliases = ["write"]
type = "bool"
default = "false"
help = "Transactionally write a Zed-owned manager projection."

[commands.completions]
help = "Generate Bash or Zsh completion from the typed CLI model."

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ registry hosts both on S3/Cloudflare R2.
| `zed uninstall [<org>/<name> ...]` (`zed un`) | Transactionally remove all or selected materialized packages while retaining the manifest and lockfile for a frozen reinstall |
| `zed env import mise [--config PATH] [--lock PATH] [--frozen] [--json]` | Import the supported project-local mise tool/lock subset as the shared normalized `EnvironmentPlan`; never loads parent/global config or executes hooks |
| `zed env verify mise [--config PATH] [--lock PATH] --frozen [--json]` | Fail closed on missing lock coverage, drift, malformed checksums, unsupported semantics, or non-portable frozen state and report the stable plan digest |
| `zed env export mise --plan PATH [--output .mise.toml] [--check|--write] [--json]` | Deterministically project a schema-v2 environment plan into conflict-safe project-local mise TOML without invoking mise or executing project code |
| `zed env import asdf [--config .tool-versions] [--lock .zed/asdf.lock.toml] [--frozen] [--json]` | Import project-local asdf selections and optional immutable plugin/artifact provenance without invoking asdf or plugin code |
| `zed env verify asdf [--config .tool-versions] [--lock .zed/asdf.lock.toml] --frozen [--json]` | Verify exact asdf tool, plugin revision, artifact SHA-256, platform, and normalized plan identity without reading parent/global configuration |
| `zed find <query>` | Search the registry |
Expand Down
122 changes: 122 additions & 0 deletions docs/mise-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Deterministic mise export

`zed env export mise` projects a schema-v2 Zed environment plan into a
project-local mise configuration without invoking mise, loading global state,
or executing project code.

```sh
# Print deterministic TOML without writing files.
zed env export mise --plan zed-env.toml

# Verify that the checked-in manager view is current.
zed env export mise --plan zed-env.toml --output .mise.toml --check

# Create or update a Zed-owned manager view transactionally.
zed env export mise --plan zed-env.toml --output .mise.toml --write
```

`--check` and `--write` are mutually exclusive. Without either flag, the
command prints the deterministic TOML document to standard output.

## Supported projection

The first write-capable slice exports only state with an exact, tested mise
representation:

- ordered single- and multi-version tools;
- backend-qualified tool keys and core-tool aliases;
- `version`, portable project-relative `path`, `prefix`, and `ref` selectors;
- typed backend options and tool OS constraints;
- scalar environment values;
- typed project and task variables;
- task descriptions, aliases, ordered command arrays, dependencies,
post-dependencies, readiness dependencies, environment, variables,
task-local tools, directories, sources, outputs, one shell string, usage,
confirmation, cache, timeout, and output flags; and
- lockfile platform settings.

The exporter preserves multi-version and command order. Set-like platform
lists are sorted and deduplicated by the normalized environment contract.

## Fail-closed boundaries

The command rejects fields rather than silently omitting them when no certified
mapping exists. Current explicit boundaries include:

- activation hooks other than `activation = "none"`;
- environment-plan system packages;
- imported manager provenance and lock identity;
- extension maps;
- resolved versions, immutable sources, and artifact checksums that belong in
`mise.lock` rather than `mise.toml`;
- complex top-level or task environment tables/arrays, which mise may interpret
as directives rather than literal values;
- grouped and structured task invocations until the current mise run-entry wire
form is certified; and
- shell program-plus-argument vectors, because the current mise task field is
one shell string.

Diagnostics include the exact environment-plan path that cannot be represented.
Complete current `mise.lock` export is tracked independently so config export
cannot accidentally erase provenance.

## Secrets

The plan contract does not yet expose a portable secret-reference type. Export
therefore rejects literal environment, variable, and tool-option names that
look credential-bearing at any nesting depth, including password, secret, token,
private/access key, API key, credential, and authorization names. The generated file, sidecar,
logs, and diagnostics never print rejected values.

## Ownership and conflicts

Write mode records deterministic ownership in:

```text
.zed/mise-export-state.json
```

The sidecar contains only schema version, project-relative plan/output paths,
and SHA-256 identities. It contains no timestamps, random identifiers,
absolute paths, credentials, or environment values.

The exporter follows these rules:

1. A missing output can be created.
2. An existing file whose bytes already equal the deterministic projection can
be adopted safely.
3. A differing existing file with no matching Zed ownership record is treated
as hand-authored and is never overwritten.
4. A Zed-owned file whose current digest differs from the recorded digest is a
user edit and is never overwritten.
5. A Zed-owned unchanged file may be replaced only by the same project-relative
plan recorded in the sidecar.

Output and state changes share `ProjectTransaction`, including crash recovery
and rollback. Plan, output, state, and staging identities are compared with
portable ASCII case-folding so a Linux-generated ownership file cannot become
ambiguous on Windows or macOS. Project, output, and state paths reject absolute/home/drive/UNC,
parent traversal, and symlink escape.

## Determinism

The plan identity is:

```text
SHA256("zed-pkg:mise-export-plan:v1\0" || canonical-environment-plan-json)
```

The output identity is SHA-256 over the exact generated TOML bytes. Print,
check, and write modes all render the normalized semantic plan, so set-like
presentation order cannot diverge under one plan digest. Repeated generation of the
same normalized plan is byte-identical across supported platforms.

## Next gates

- Bind the complete current `mise.lock` contract to export and verification.
- Translate portable manager identities into native `EnvironmentLock`.
- Certify structured task run entries and sandbox capabilities.
- Add conflict-aware import → export → import semantic identity tests on Linux,
macOS, and Windows.

Tracking: DEN-1462, DEN-1461, DEN-1481.
64 changes: 62 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ impl From<CompletionShell> for clap_complete::Shell {

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum EnvironmentManagerArg {
/// Import or verify project-local mise configuration.
/// Import, verify, or export project-local mise configuration.
Mise,
/// Import or verify project-local asdf configuration and Zed-owned provenance.
Asdf,
Expand Down Expand Up @@ -432,6 +432,26 @@ pub enum EnvCmd {
#[arg(long, env = "ZED_PKG_ENV_JSON")]
json: bool,
},
/// Export a schema-v2 EnvironmentPlan as deterministic mise TOML.
Export {
#[arg(value_enum)]
manager: EnvironmentManagerArg,
/// Project-local schema-v2 EnvironmentPlan (.toml or .json).
#[arg(long, env = "ZED_PKG_ENV_PLAN")]
plan: PathBuf,
/// Project-local mise output path.
#[arg(long, env = "ZED_PKG_ENV_OUTPUT", default_value = ".mise.toml")]
output: PathBuf,
/// Verify that the output already equals the deterministic projection.
#[arg(long, env = "ZED_PKG_ENV_CHECK")]
check: bool,
/// Transactionally create/update a Zed-owned manager view.
#[arg(long, env = "ZED_PKG_ENV_WRITE")]
write: bool,
/// Emit a machine-readable export result.
#[arg(long, env = "ZED_PKG_ENV_JSON")]
json: bool,
},
/// Verify manager config/lock coverage and the normalized plan digest.
Verify {
#[arg(value_enum)]
Expand Down Expand Up @@ -548,7 +568,7 @@ mod tests {

use clap::{CommandFactory, Parser};

use super::{AuthCmd, Cli, Cmd};
use super::{AuthCmd, Cli, Cmd, EnvCmd};

#[test]
fn flat_and_nested_auth_spellings_dispatch_identically() {
Expand Down Expand Up @@ -693,6 +713,46 @@ mod tests {
}
}

#[test]
fn environment_export_is_typed_and_rejects_ambiguous_write_modes() {
let cli = Cli::try_parse_from([
"zed",
"env",
"export",
"mise",
"--plan",
"zed-env.toml",
"--output",
".mise.toml",
"--check",
"--json",
])
.unwrap();
assert!(matches!(cli.cmd, Cmd::Env { .. }));

assert!(matches!(
Cli::try_parse_from([
"zed",
"env",
"export",
"mise",
"--plan",
"zed-env.toml",
"--check",
"--write",
])
.unwrap()
.cmd,
Cmd::Env {
cmd: EnvCmd::Export {
check: true,
write: true,
..
}
}
));
}

#[test]
fn completion_shells_are_typed_positionals() {
for shell in ["bash", "zsh"] {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod interactive;
pub mod lock_waiter;
pub mod managed_install;
pub mod manifestless;
pub mod mise_export;
pub mod mise_lock;
pub mod nix_bundle_write;
pub mod nix_export_bundle;
Expand Down
26 changes: 26 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use zed_cli::fetch;
use zed_cli::git_submodules as submodules;
use zed_cli::global;
use zed_cli::managed_install;
use zed_cli::mise_export::{self, MiseExportMode};
use zed_cli::nix_bundle_write;
use zed_cli::nix_export_plan;
use zed_cli::ops;
Expand Down Expand Up @@ -224,6 +225,31 @@ fn run(cli: Cli) -> anyhow::Result<()> {
asdf_environment::print_import(&imported, json)
}
},
EnvCmd::Export {
manager: EnvironmentManagerArg::Mise,
plan,
output,
check,
write,
json,
} => {
if check && write {
anyhow::bail!("the arguments '--check' and '--write' cannot be used together");
}
let mode = if check {
MiseExportMode::Check
} else if write {
MiseExportMode::Write
} else {
MiseExportMode::Print
};
let exported = mise_export::export_mise(&cwd, &plan, &output, mode)?;
mise_export::print_export(&exported, json)
}
EnvCmd::Export {
manager: EnvironmentManagerArg::Asdf,
..
} => anyhow::bail!("asdf export is not implemented; use `zed env export mise`"),
EnvCmd::Verify {
manager,
config,
Expand Down
Loading