Skip to content
Merged
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
15 changes: 14 additions & 1 deletion .github/workflows/asdf-interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ on:
paths:
- "src/asdf_environment.rs"
- "src/bin/zed-asdf.rs"
- "src/cli.rs"
- "src/main.rs"
- "src/lib.rs"
- "tests/asdf_environment_cli.rs"
- "tests/asdf_main_cli.rs"
- "docs/asdf-interop.md"
- "README.md"
- ".github/workflows/asdf-interop.yml"
- "Cargo.toml"
- "Cargo.lock"
Expand All @@ -16,8 +20,13 @@ on:
paths:
- "src/asdf_environment.rs"
- "src/bin/zed-asdf.rs"
- "src/cli.rs"
- "src/main.rs"
- "src/lib.rs"
- "tests/asdf_environment_cli.rs"
- "tests/asdf_main_cli.rs"
- "docs/asdf-interop.md"
- "README.md"
- ".github/workflows/asdf-interop.yml"
- "Cargo.toml"
- "Cargo.lock"
Expand Down Expand Up @@ -70,8 +79,12 @@ jobs:
set -euo pipefail
cargo test --locked --lib asdf_environment::tests
cargo test --locked --test asdf_environment_cli
cargo test --locked --test asdf_main_cli

- name: Clippy (strict)
run: |
set -euo pipefail
cargo clippy --locked --bin zed-asdf --test asdf_environment_cli -- -D warnings
cargo clippy --locked --bin zed --bin zed-asdf --test asdf_environment_cli --test asdf_main_cli -- -D warnings

- name: Prove import and verification did not mutate the checkout
run: git diff --exit-code
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ 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 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 |
| `zed pack` | Build the pruned, deterministic `tar.gz` artifact |
| `zed release plan [--json]` | Print the credential-free Zed, native-registry, and forge-package release set derived from `.zpkg.toml` |
Expand Down
9 changes: 9 additions & 0 deletions docs/asdf-interop.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ separate from the main `zed env` dispatcher until the shared mise command branch
lands; the reusable implementation lives in `zed_cli::asdf_environment` so the
later wiring step does not need to reimplement parsing or validation.

## Canonical `zed env` commands

```console
zed env import asdf [--config .tool-versions] [--lock .zed/asdf.lock.toml] [--frozen] [--json]
zed env verify asdf [--config .tool-versions] [--lock .zed/asdf.lock.toml] --frozen [--json]
```

The staged `zed-asdf` executable remains a focused adapter-test surface, but the supported user interface is the typed `zed env` dispatcher. Both routes call the same Rust module and preserve the same read-only, project-local, no-execution contract.

## Commands

```text
Expand Down
42 changes: 27 additions & 15 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ impl From<CompletionShell> for clap_complete::Shell {
pub enum EnvironmentManagerArg {
/// Import or verify project-local mise configuration.
Mise,
/// Import or verify project-local asdf configuration and Zed-owned provenance.
Asdf,
}

#[derive(Debug, Subcommand)]
Expand Down Expand Up @@ -663,21 +665,31 @@ mod tests {

#[test]
fn environment_import_and_verify_are_typed() {
for action in ["import", "verify"] {
let cli = Cli::try_parse_from([
"zed",
"env",
action,
"mise",
"--config",
"mise.toml",
"--lock",
"mise.lock",
"--frozen",
"--json",
])
.unwrap();
assert!(matches!(cli.cmd, Cmd::Env { .. }));
for manager in ["mise", "asdf"] {
for action in ["import", "verify"] {
let cli = Cli::try_parse_from([
"zed",
"env",
action,
manager,
"--config",
if manager == "mise" {
"mise.toml"
} else {
".tool-versions"
},
"--lock",
if manager == "mise" {
"mise.lock"
} else {
".zed/asdf.lock.toml"
},
"--frozen",
"--json",
])
.unwrap();
assert!(matches!(cli.cmd, Cmd::Env { .. }));
}
}
}

Expand Down
51 changes: 38 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use zed_cli::asdf_environment;
use zed_cli::auth;
use zed_cli::cli::EnvCmd;
use zed_cli::cli::{AuthCmd, CacheCmd, Cli, Cmd, OrgCmd, ReleaseCmd, StoreCmd};
use zed_cli::cli::{
AuthCmd, CacheCmd, Cli, Cmd, EnvironmentManagerArg, OrgCmd, ReleaseCmd, StoreCmd,
};
use zed_cli::completion;
use zed_cli::config::Config;
use zed_cli::dev;
Expand Down Expand Up @@ -132,27 +135,49 @@ fn run(cli: Cli) -> anyhow::Result<()> {
Cmd::Uninstall { specs } => ops::uninstall(&cwd, &cfg, &specs),
Cmd::Env { cmd } => match cmd {
EnvCmd::Import {
manager: _,
manager,
config,
lock,
frozen,
json,
} => {
let imported =
environment::import_mise(&cwd, config.as_deref(), lock.as_deref(), frozen)?;
environment::print_import(&imported, json)
}
} => match manager {
EnvironmentManagerArg::Mise => {
let imported =
environment::import_mise(&cwd, config.as_deref(), lock.as_deref(), frozen)?;
environment::print_import(&imported, json)
}
EnvironmentManagerArg::Asdf => {
let imported = asdf_environment::import_asdf(
&cwd,
config.as_deref(),
lock.as_deref(),
frozen,
)?;
asdf_environment::print_import(&imported, json)
}
},
EnvCmd::Verify {
manager: _,
manager,
config,
lock,
frozen,
json,
} => {
let imported =
environment::import_mise(&cwd, config.as_deref(), lock.as_deref(), frozen)?;
environment::print_verification(&imported, json)
}
} => match manager {
EnvironmentManagerArg::Mise => {
let imported =
environment::import_mise(&cwd, config.as_deref(), lock.as_deref(), frozen)?;
environment::print_verification(&imported, json)
}
EnvironmentManagerArg::Asdf => {
let imported = asdf_environment::import_asdf(
&cwd,
config.as_deref(),
lock.as_deref(),
frozen,
)?;
asdf_environment::print_verification(&imported, json)
}
},
},
Cmd::Completions { shell } => {
completion::print(shell.into());
Expand Down
104 changes: 104 additions & 0 deletions tests/asdf_main_cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_zed"))
}

fn checksum(digit: char) -> String {
digit.to_string().repeat(64)
}

fn revision(digit: char) -> String {
digit.to_string().repeat(40)
}

fn write_fixture(root: &Path) {
fs::write(
root.join(".tool-versions"),
"nodejs 22.11.0\npython 3.12.4\n",
)
.unwrap();
fs::create_dir_all(root.join(".zed")).unwrap();
fs::write(
root.join(".zed/asdf.lock.toml"),
format!(
r#"schema = 1

[plugins.nodejs]
version = "22.11.0"
url = "https://github.com/asdf-vm/asdf-nodejs.git"
revision = "{}"
sha256 = "{}"
platforms = ["x86_64-linux"]

[plugins.python]
version = "3.12.4"
url = "https://github.com/danhper/asdf-python.git"
revision = "{}"
sha256 = "{}"
"#,
revision('1'),
checksum('a'),
revision('2'),
checksum('b'),
),
)
.unwrap();
}

#[test]
fn canonical_env_dispatch_imports_and_verifies_asdf_read_only() {
let temp = tempfile::tempdir().unwrap();
write_fixture(temp.path());
let config_before = fs::read(temp.path().join(".tool-versions")).unwrap();
let lock_before = fs::read(temp.path().join(".zed/asdf.lock.toml")).unwrap();

for action in ["import", "verify"] {
let output = Command::new(binary())
.current_dir(temp.path())
.env("PATH", "")
.args(["env", action, "asdf", "--frozen", "--json"])
.output()
.unwrap();
assert!(
output.status.success(),
"{action} stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
if action == "verify" {
assert_eq!(value["manager"], "asdf");
assert_eq!(value["verified"], true);
} else {
assert_eq!(value["tools"].as_object().unwrap().len(), 2);
}
}

assert_eq!(
fs::read(temp.path().join(".tool-versions")).unwrap(),
config_before
);
assert_eq!(
fs::read(temp.path().join(".zed/asdf.lock.toml")).unwrap(),
lock_before
);
}

#[test]
fn canonical_env_dispatch_fails_closed_without_frozen_provenance() {
let temp = tempfile::tempdir().unwrap();
fs::write(temp.path().join(".tool-versions"), "nodejs 22.11.0\n").unwrap();
let output = Command::new(binary())
.current_dir(temp.path())
.env("PATH", "")
.args(["env", "verify", "asdf", "--frozen"])
.output()
.unwrap();
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stderr)
.contains("frozen asdf import requires `.zed/asdf.lock.toml`")
);
}
Loading