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
188 changes: 188 additions & 0 deletions .github/workflows/rebase-global-packages-current-main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Refresh global packages onto current main

on:
push:
branches:
- agent/global-zed-packages

permissions:
contents: write

concurrency:
group: refresh-global-packages-current-main
cancel-in-progress: false

jobs:
refresh:
if: github.actor != 'github-actions[bot]'
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- name: Check out the feature branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
persist-credentials: true

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30
with:
toolchain: stable
components: rustfmt,clippy

- name: Merge current main and preserve the semantic union
shell: bash
run: |
set -euo pipefail
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git fetch origin main

set +e
git merge --no-commit --no-ff origin/main
merge_status=$?
set -e
if [[ $merge_status -ne 0 && $merge_status -ne 1 ]]; then
exit "$merge_status"
fi

# Current main owns the modern dispatcher, cli_model, environment,
# Nix, fetch, managed-install, and Git-submodule composition. Start
# from those exact files, then add the independent global surface.
git checkout --theirs -- src/lib.rs src/main.rs src/completion.rs

python3 - <<'PY'
from pathlib import Path

lib = Path('src/lib.rs')
text = lib.read_text()
needle = 'pub mod git_submodules;\n'
if 'pub mod global;\n' not in text:
if needle not in text:
raise SystemExit('lib.rs insertion point not found')
text = text.replace(needle, needle + 'pub mod global;\n', 1)
lib.write_text(text)

main = Path('src/main.rs')
text = main.read_text()
use_needle = 'use zed_cli::git_submodules as submodules;\n'
if 'use zed_cli::global;\n' not in text:
if use_needle not in text:
raise SystemExit('main.rs import insertion point not found')
text = text.replace(use_needle, use_needle + 'use zed_cli::global;\n', 1)

dispatch_needle = ''' if let Err(error) = zed_cli::flags::normalize_global_boolean_environment(&args) {
eprintln!("error: {error:#}");
std::process::exit(2);
}
'''
dispatch = ''' if let Err(error) = zed_cli::flags::normalize_global_boolean_environment(&args) {
eprintln!("error: {error:#}");
std::process::exit(2);
}
if let Some(result) = global::dispatch(args.clone()) {
match result {
Ok(0) => return,
Ok(code) => std::process::exit(code),
Err(error) => {
eprintln!("error: {error:#}");
std::process::exit(1);
}
}
}
'''
if 'global::dispatch(args.clone())' not in text:
if dispatch_needle not in text:
raise SystemExit('main.rs dispatch insertion point not found')
text = text.replace(dispatch_needle, dispatch, 1)
main.write_text(text)

completion = Path('src/completion.rs')
text = completion.read_text()
old_import = 'use crate::{dev, fetch, git_submodules, nix_export_plan};\n'
new_import = 'use crate::{dev, fetch, git_submodules, global, nix_export_plan};\n'
if old_import in text:
text = text.replace(old_import, new_import, 1)
elif new_import not in text:
raise SystemExit('completion.rs import insertion point not found')

old_fn = '''fn root_command() -> clap::Command {
git_submodules::augment_root_command(nix_export_plan::augment_root_command(
fetch::augment_root_command(dev::augment_root_command(cli_model::command())),
))
}
'''
new_fn = '''pub(crate) fn root_command() -> clap::Command {
global::augment_root_command(git_submodules::augment_root_command(
nix_export_plan::augment_root_command(fetch::augment_root_command(
dev::augment_root_command(cli_model::command()),
)),
))
}
'''
if old_fn in text:
text = text.replace(old_fn, new_fn, 1)
elif 'global::augment_root_command' not in text:
raise SystemExit('completion.rs command composition not found')

for command in ['"global",', '"bin-dir",']:
if command not in text:
marker = ' "overtake",\n'
if marker not in text:
raise SystemExit('completion command test insertion point not found')
text = text.replace(marker, marker + f' {command}\n', 1)
if '"--global-bin-dir",' not in text:
marker = ' "--install-mode",\n'
if marker not in text:
raise SystemExit('completion option test insertion point not found')
text = text.replace(marker, marker + ' "--global-bin-dir",\n', 1)
completion.write_text(text)

global_rs = Path('src/global.rs')
text = global_rs.read_text()
text = text.replace(
'use clap::{Args, CommandFactory, Parser, Subcommand};',
'use clap::{Args, Parser, Subcommand};',
)
text = text.replace(
'use crate::{dev, interactive, manifestless};',
'use crate::{interactive, manifestless};',
)
old = '''fn print_root_help() -> Result<()> {
let mut command = dev::augment_root_command(augment_root_command(crate::cli::Cli::command()));
command.print_help().context("printing zed help")?;
println!();
Ok(())
}
'''
new = '''fn print_root_help() -> Result<()> {
let mut command = crate::completion::root_command();
command.print_help().context("printing zed help")?;
println!();
Ok(())
}
'''
if old in text:
text = text.replace(old, new, 1)
elif 'crate::completion::root_command()' not in text:
raise SystemExit('global.rs root help integration point not found')
global_rs.write_text(text)
PY

# The workflow is a one-shot semantic merge helper, not product code.
rm -f .github/workflows/rebase-global-packages-current-main.yml

cargo fmt --all
cargo test --locked --test global_cli
cargo test --locked global::tests
cargo test --locked completion::tests
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo test --all-targets --all-features --locked

git add -A
if git diff --cached --quiet; then
echo 'No refresh commit required.'
exit 0
fi
git commit -m 'Merge current main into global package profiles'
git push origin HEAD:agent/global-zed-packages
1 change: 1 addition & 0 deletions .zpkg.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version = 1
45 changes: 45 additions & 0 deletions .zpkg.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[package]
org = "zed-pkg"
name = "zed-cli"
version = "0.1.0"
description = "The zed universal package-manager CLI"
license = "MIT"
keywords = ["package-manager", "cli", "polyglot", "vcs", "global-tools"]
language = "rust"

[package.repository]
vcs = "git"
url = "https://github.com/zed-pkg/zed-cli"

[build]
command = "cargo build --release --locked"
outputs = ["target/release/zed"]

[bin]
zed = "target/release/zed"

[publish]
include_readme = true
tag_format = "v{version}"
smoke_test = "test -x \"$ZED_PKG_TEST_TARGET/target/release/zed\" && \"$ZED_PKG_TEST_TARGET/target/release/zed\" --version"
exclude = [
".env",
".env.*",
".direnv/**",
".zed/**",
".zed-pack/**",
"target/**",
"tmp/**",
"**/*.log",
".DS_Store",
]

[publish.native]
registry = "crates-io"
package = "zed-cli"

[install]
adapter = "none"

[scripts]
test = "cargo test --all-targets --locked"
114 changes: 114 additions & 0 deletions docs/global-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Global executable packages

Zed keeps project dependencies project-local by default. A command-line tool is different: its executable should normally be available from the user's `PATH`, independent of any one repository.

Use the explicit global package namespace:

```sh
zed global install acme/tool
zed global install acme/tool@^2
```

The npm/cargo-style spelling is an exact compatibility route to the same implementation:

```sh
zed install --global acme/tool
```

## Storage model

Each requested top-level package gets an isolated profile:

```text
$ZED_PKG_HOME/global/profiles/<org>/<name>/
├── .zpkg.lock
├── .zed-global-profile.json
└── zed_modules/
├── <org>/<name>/
└── .bin/<executable>
```

A profile resolves and locks its own complete dependency graph. Two global tools may therefore use incompatible versions of the same transitive package without forcing one global dependency solution.

The package artifact still lives once in Zed's content-addressed store. A profile normally symlinks package trees from that store, while exposed executables are independent, executable copies owned by the global installer.

## PATH directory

On Unix-like systems, executables are copied to `~/.local/bin` by default. On Windows, Zed uses a per-user directory below the local application-data root. Print the exact path with:

```sh
zed global bin-dir
```

Override it for one invocation or permanently:

```sh
zed --global-bin-dir "$HOME/bin" global install acme/tool
export ZED_PKG_GLOBAL_BIN_DIR="$HOME/bin"
```

Zed reports when the selected directory is not on `PATH`; it never silently edits shell startup files or the Windows user environment.

## Builds and executable declarations

A package exposes commands through its root manifest:

```toml
[build]
command = "cargo build --release --locked"
outputs = ["target/release/acme"]

[bin]
acme = "target/release/acme"
```

Package build hooks execute author-supplied code and remain opt-in:

```sh
zed global install acme/tool --allow-build
```

Prebuilt packages do not need a build hook. Their `[bin]` values may point directly at executable files already present in the artifact.

## Ownership and collisions

Zed records the package owner and SHA-256 hash of every executable it places in the global bin directory.

- An unrelated existing command is never overwritten.
- Two installed packages exposing the same command name fail closed.
- Uninstall removes an executable only when its current bytes still match the version Zed installed.
- A command changed after installation is retained with a warning rather than deleted.

These rules make a shared directory such as `~/.local/bin` safe to use alongside manually installed tools and other package managers.

## Lifecycle

```sh
# Inspect profiles and their commands
zed global list

# Re-materialize every exact lock after moving machines or clearing the store
zed global install --frozen --allow-build

# Re-materialize one exact profile
zed global install --frozen acme/tool --allow-build

# Remove one profile and its still-owned commands
zed global uninstall acme/tool
zed uninstall --global acme/tool

# Remove all Zed-managed global profiles
zed global uninstall
```

The global lock serializes profile and PATH mutations across terminals. Package downloads and builds continue to use the normal store and build-cache locks.

## Installing Zed with Zed

The `zed-pkg/zed-cli` repository is itself a Zed package. Once a bootstrap `zed` binary is available, it can install or upgrade the CLI through the same contract:

```sh
zed global install zed-pkg/zed-cli --allow-build
```

The resulting `zed` executable is managed in the configured global bin directory; the package source and build output remain reproducibly locked in the isolated `zed-pkg/zed-cli` profile.
Loading
Loading