Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ec3baff
feat: harden global executable package profiles
ORESoftware Aug 4, 2026
e2176a8
ci: format and verify hardened global packages once
ORESoftware Aug 4, 2026
11bc0de
ci: materialize rustfmt before validation
ORESoftware Aug 4, 2026
355d9e1
style: format hardened global package profiles
github-actions[bot] Aug 4, 2026
a95defb
ci: split global package gates and emit format evidence
ORESoftware Aug 4, 2026
b74e3d6
ci: retain focused global-profile test diagnostics
ORESoftware Aug 4, 2026
483f360
fix: preserve current-main module exports
ORESoftware Aug 4, 2026
2fdf85b
chore: sync Windows cfg hygiene into global package profiles
ORESoftware Aug 4, 2026
13743a7
chore: sync canonical archive and Windows fixes into global profiles
ORESoftware Aug 4, 2026
7b3a9c7
chore: sync canonical Nix fixtures into global profiles
ORESoftware Aug 4, 2026
575d122
chore: sync formatted canonical fixtures into global profiles
ORESoftware Aug 4, 2026
1e2ae0e
chore: sync merged Git-submodule contract into global profiles
ORESoftware Aug 4, 2026
7ace18d
chore: stage offline frozen prefetch repair
ORESoftware Aug 4, 2026
204c5e1
ci: validate offline frozen prefetch repair
ORESoftware Aug 4, 2026
02ae507
fix: make frozen prefetch lock-authoritative for local artifacts
ORESoftware Aug 4, 2026
695d3c8
test: certify registry-free frozen prefetch replay
ORESoftware Aug 4, 2026
a4d5125
chore: remove completed offline prefetch patch helper
ORESoftware Aug 4, 2026
bcef6f0
chore: remove completed offline prefetch validation workflow
ORESoftware Aug 4, 2026
9fa5065
style: apply rustfmt to frozen offline prefetch
ORESoftware Aug 4, 2026
a2d9b55
merge: preserve global profiles and canonical asdf env routes
ORESoftware Aug 4, 2026
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
184 changes: 184 additions & 0 deletions .github/workflows/global-packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: Global executable package profiles

on:
pull_request:
paths:
- .zpkg.toml
- .zpkg.lock
- docs/global-packages.md
- src/global.rs
- src/lib.rs
- src/main.rs
- src/completion.rs
- src/ops.rs
- tests/global_cli.rs
- .github/workflows/global-packages.yml
push:
branches: [main]
paths:
- .zpkg.toml
- .zpkg.lock
- docs/global-packages.md
- src/global.rs
- src/lib.rs
- src/main.rs
- src/completion.rs
- src/ops.rs
- tests/global_cli.rs
- .github/workflows/global-packages.yml

permissions:
contents: read

concurrency:
group: global-packages-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

defaults:
run:
shell: bash

jobs:
certify:
name: Global profiles (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Check out exact candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
show-progress: false

- name: Install Rust validation toolchain
run: |
set -euo pipefail
rustup toolchain install stable --profile minimal --component rustfmt,clippy
rustup default stable

- name: Check formatting
id: formatting
continue-on-error: true
run: |
set -euo pipefail
cargo fmt --all --check

- name: Run global profile unit tests
id: unit
continue-on-error: true
run: |
set -euo pipefail
mkdir -p diagnostics
cargo test --quiet --locked --lib global::tests -- --nocapture \
2>&1 | tee "diagnostics/global-unit-tests-${RUNNER_OS}.log"

- name: Upload focused unit-test diagnostics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: global-unit-tests-${{ runner.os }}-${{ github.run_id }}-${{ github.run_attempt }}
path: diagnostics/
if-no-files-found: error
retention-days: 3

- name: Enforce global profile unit tests
if: always() && steps.unit.outcome == 'failure'
run: |
echo 'focused global profile unit tests failed; see retained diagnostics.' >&2
exit 1

- name: Run compiled global CLI tests
run: |
set -euo pipefail
cargo test --locked --test global_cli

- name: Run strict Clippy
run: |
set -euo pipefail
cargo clippy --locked --all-targets --all-features -- -D warnings

- name: Build and invoke the packaged command
run: |
set -euo pipefail
cargo build --release --locked --bin zed
if [[ "$RUNNER_OS" == "Windows" ]]; then
./target/release/zed.exe --version
else
./target/release/zed --version
fi

- name: Run the complete repository suite on Linux
if: matrix.os == 'ubuntu-24.04'
run: |
set -euo pipefail
cargo test --all-targets --all-features --locked

- name: Produce exact rustfmt evidence
if: always() && runner.os == 'Linux' && steps.formatting.outcome == 'failure'
run: |
set -euo pipefail
cargo fmt --all
python3 - <<'PY'
import pathlib
import shutil
import subprocess

allowed = {
'src/completion.rs',
'src/global.rs',
'src/lib.rs',
'src/main.rs',
'src/ops.rs',
'tests/global_cli.rs',
}
changed = {
line.strip()
for line in subprocess.check_output(
['git', 'diff', '--name-only'], text=True
).splitlines()
if line.strip()
}
unexpected = changed - allowed
if unexpected:
raise SystemExit(
'rustfmt changed files outside the reviewed feature boundary: '
+ ', '.join(sorted(unexpected))
)
if not changed:
raise SystemExit('format check failed but rustfmt produced no changed files')
root = pathlib.Path('format-evidence')
for name in sorted(changed):
destination = root / name
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(name, destination)
(root / 'changed-files.txt').write_text('\n'.join(sorted(changed)) + '\n')
PY
git diff --binary -- \
src/completion.rs \
src/global.rs \
src/lib.rs \
src/main.rs \
src/ops.rs \
tests/global_cli.rs \
> format-evidence/rustfmt.patch
find format-evidence -type f -print0 | sort -z | xargs -0 sha256sum > format-evidence/SHA256SUMS

- name: Upload rustfmt evidence
if: always() && runner.os == 'Linux' && steps.formatting.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: global-package-rustfmt-${{ github.run_id }}-${{ github.run_attempt }}
path: format-evidence/
if-no-files-found: error
retention-days: 3

- name: Enforce formatting result
if: always() && steps.formatting.outcome == 'failure'
run: |
echo 'cargo fmt --all --check failed; see the Linux rustfmt evidence artifact.' >&2
exit 1
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