diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5d5f65c..3851c3c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -635,7 +635,7 @@ jobs: # asserts contents and version and says nothing about the tag. container: off args: --release --locked --out dist - - name: The wheel is the two binaries, at the version rust/Cargo.toml names + - name: The wheel is the binaries, at the version rust/Cargo.toml names run: | set -euo pipefail version=$(sed -n 's/^version = "\(.*\)"/\1/p' rust/Cargo.toml | head -1) @@ -645,6 +645,7 @@ jobs: /tmp/wheel-check/bin/pip install --quiet "$wheel" test "$(/tmp/wheel-check/bin/dl --version)" = "dl $version" test "$(/tmp/wheel-check/bin/aid --version)" = "aid $version" + test "$(/tmp/wheel-check/bin/dla --version)" = "aid $version" - uses: prefix-dev/setup-pixi@v0.10.2 with: run-install: false diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ed57d465..dedd9460 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -118,8 +118,8 @@ jobs: uses: PyO3/maturin-action@v1 with: # `packaging/wheel/pyproject.toml` is the published project: maturin - # bin bindings over `rust/dl`, whose two bin targets are `dl` and (behind - # the `wheel` feature) `aid`. + # bin bindings over `rust/dl`, whose bin targets are `dl` and (behind + # the `wheel` feature) `aid` and its second name `dla`. working-directory: packaging/wheel # Built in a manylinux container rather than on the runner, because the # tag is computed from the glibc the binaries actually got linked @@ -147,7 +147,7 @@ jobs: run: | set -euo pipefail version='${{ steps.decide.outputs.version }}' - # Exactly one wheel, and it is the two binaries at the version this + # Exactly one wheel, and it is the binaries at the version this # release is being cut at. A mismatch means the version was read from one # place and the binaries built from another, which is the failure mode # single-sourcing exists to remove -- so it stops here, before PyPI. @@ -164,6 +164,7 @@ jobs: /tmp/wheel-check/bin/pip install --quiet "$wheel" test "$(/tmp/wheel-check/bin/dl --version)" = "dl $version" test "$(/tmp/wheel-check/bin/aid --version)" = "aid $version" + test "$(/tmp/wheel-check/bin/dla --version)" = "aid $version" - name: Publish to PyPI if: steps.decide.outputs.publish == 'true' diff --git a/CHANGELOG.md b/CHANGELOG.md index fc10dc61..78117985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`dla` is a second name for `aid`.** It is easier to type: `dl` and one letter. It is the + same program, built from the same source, so it takes the same flags, prints the same + messages and answers `dla --version` as `aid `. The wheel, the conda package and + `./dev.sh` (as `dla-next`) ship it, and bash completion treats it as `aid`. An existing + install gets `dla` completion after it runs `dl --install` again. + ## [0.59.4] - 2026-10-05 ### Fixed diff --git a/README.md b/README.md index e1771f0c..f8f66bc6 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,9 @@ aid [@branch|@] [prompt...] aid resume [] ``` +`dla` is the same program under a name that is easier to type: every `aid` line in this +section works with `dla` in its place. + `aid` is a shortcut, not a second launcher. It rewrites its command line into a `dl` one, so ```bash diff --git a/conda.recipe/recipe.yaml b/conda.recipe/recipe.yaml index 4aad5cf5..1137ae42 100644 --- a/conda.recipe/recipe.yaml +++ b/conda.recipe/recipe.yaml @@ -20,13 +20,14 @@ source: build: number: 0 - # No longer `noarch: python`: as of 0.1.0 the package is two compiled binaries + # No longer `noarch: python`: as of 0.1.0 the package is compiled binaries # for linux-64 and carries no Python at all. script: - # Both binaries come out of the `dl` package -- its own `dl`, and an `aid` - # compiled from `rust/aid/src/main.rs` behind the `wheel` feature, which is + # Every binary comes out of the `dl` package -- its own `dl`, and an `aid` + # (and `dla`, its second name) compiled from `rust/aid/src/main.rs` behind the + # `wheel` feature, which is # the arrangement `rust/dl/Cargo.toml` explains and the one the PyPI wheel - # ships from too. One `cargo install` for both, so the two channels cannot + # ships from too. One `cargo install` for all of them, so the two channels cannot # ship different sets. # # `--locked` is the point of building from a tag: the dependency versions are @@ -64,6 +65,8 @@ tests: # package was built and named under. - test "$(dl --version)" = "dl ${{ version }}" - test "$(aid --version)" = "aid ${{ version }}" + # `dla` is aid under a second name, so it answers as aid. + - test "$(dla --version)" = "aid ${{ version }}" about: homepage: https://github.com/blooop/devlaunch diff --git a/dev.sh b/dev.sh index 36c8286a..5f8f2f7f 100755 --- a/dev.sh +++ b/dev.sh @@ -1,6 +1,7 @@ #!/bin/bash # Development installation script for DevLaunch -# Builds this working tree and installs it as `dl-next` and `aid-next`, beside the +# Builds this working tree and installs it as `dl-next` and `aid-next` (and +# `dla-next`, aid's second name), beside the # released `dl` and `aid` rather than on top of them, so both are on PATH at once # and running the wrong one is not possible by accident. The released build is what # keeps working while this checkout is mid-change; it is installed by pixi global @@ -159,6 +160,7 @@ echo " dl-next --version # prints -dev; plain dl does not" echo " dl-next owner/repo # clone + DevPod workspace (default branch)" echo " dl-next owner/repo@branch # clone + DevPod workspace (specific branch)" echo " aid-next owner/repo@branch # ...with a coding agent started in it" +echo " dla-next owner/repo@branch # the same as aid-next, shorter to type" echo "" echo "Against throwaway state, leaving the real workspace list alone:" echo " XDG_CACHE_HOME=/tmp/dl-scratch/cache dl-next ..." diff --git a/docs/development.md b/docs/development.md index d09916fe..c9c2c1c6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -194,7 +194,7 @@ and `--test lock_wait` kill real process trees, and a SIGKILLed process writes n the `rust` job like everything else, and `pixi run coverage-rust` includes them locally. Inside this repository's devcontainer, `pixi run dl` and `pixi run aid` are `cargo run` over the -working tree; on a host, `./dev.sh` installs it as `dl-next`/`aid-next` beside the released pair. Both +working tree; on a host, `./dev.sh` installs it as `dl-next`/`aid-next`/`dla-next` beside the released builds. All print a `-dev` version so a working-tree build is never mistaken for a released one. See [AGENTS.md](../AGENTS.md). @@ -232,7 +232,7 @@ The two published artifacts are built from the same cargo package, both taking t `rust/Cargo.toml`, the only place it is written down: ```bash -cd packaging/wheel && maturin build --release --locked -o dist # the PyPI wheel: two binaries +cd packaging/wheel && maturin build --release --locked -o dist # the PyPI wheel: dl, aid and dla rattler-build build --experimental --recipe conda.recipe/recipe.yaml # the conda package ``` diff --git a/packaging/wheel/pyproject.toml b/packaging/wheel/pyproject.toml index 75e0b212..8e1452fe 100644 --- a/packaging/wheel/pyproject.toml +++ b/packaging/wheel/pyproject.toml @@ -1,4 +1,4 @@ -# The PyPI artifact: a wheel whose payload is the two compiled binaries. +# The PyPI artifact: a wheel whose payload is the compiled binaries. # # Why this is a second pyproject.toml rather than the repository's own. The root # `pyproject.toml` is the pixi workspace and the tool configuration; since #267 it @@ -30,7 +30,7 @@ description = "DevLaunch - A streamlined CLI for devpod workspaces" readme = "README.md" license = "MIT" authors = [{ name = "Austin Gregg-Smith", email = "blooop@gmail.com" }] -# No dependencies, and no `requires-python`. The wheel carries two statically +# No dependencies, and no `requires-python`. The wheel carries statically # linked binaries; there is no Python in it to run and nothing to import. `dl` # needs `devpod` and `git` on PATH, which no Python metadata can express -- the # conda package is where those are declared (`conda.recipe/recipe.yaml`). diff --git a/pyproject.toml b/pyproject.toml index a26b222c..aca40a17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,9 +5,9 @@ # table whose version was read out of `rust/Cargo.toml` -- served a Python `dl` # that no longer exists. # -# What ships is a linux-64 wheel of the two compiled binaries, built by maturin -# from `packaging/wheel/pyproject.toml`, and a conda package of the same two -# binaries from `conda.recipe/recipe.yaml`. Both read their version from +# What ships is a linux-64 wheel of the compiled binaries (`dl`, `aid`, `dla`), +# built by maturin from `packaging/wheel/pyproject.toml`, and a conda package of +# the same binaries from `conda.recipe/recipe.yaml`. Both read their version from # `rust/Cargo.toml`, which is the one place it lives; a release is one edit. # Neither is built from here, and the `rust` feature below is not what builds them # either: it is for building the crates under `rust/` from inside a container. @@ -148,6 +148,8 @@ default = { features = ["test", "rust"], solve-group = "default" } # does on the host: a build of somebody's tree says so wherever it runs. dl = "cargo run -q --locked --manifest-path rust/Cargo.toml -p dl --bin dl --features dl/dev-build --" aid = "cargo run -q --locked --manifest-path rust/Cargo.toml -p aid --bin aid --features aid/dev-build --" +# `dla` is aid under a second name (rust/aid/Cargo.toml), so it is the same task. +dla = "cargo run -q --locked --manifest-path rust/Cargo.toml -p aid --bin dla --features aid/dev-build --" dev-add-docker = "python scripts/devpod_provider.py docker" dev = { cmd = "devpod up . --ide none && ssh pythontemplate.devpod", depends-on = ["dev-add-docker"] } dev-vs = { cmd = "devpod up . --ide vscode", depends-on = ["dev-add-docker"] } diff --git a/rust/aid/Cargo.toml b/rust/aid/Cargo.toml index 1d897d52..63037555 100644 --- a/rust/aid/Cargo.toml +++ b/rust/aid/Cargo.toml @@ -6,6 +6,29 @@ edition.workspace = true license.workspace = true repository.workspace = true +# Two names for one program. `dla` is `aid` under a name that is easier to type +# (`dl` plus one letter), built from the same `main.rs`, so there is nothing for +# the two to disagree about: same flags, same messages, and `dla --version` still +# answers `aid `. A copy rather than a symlink because a bin-wheel and a +# conda package ship files, and neither can carry a link. Naming the bins at all +# turns off cargo's discovery of `src/main.rs`, which is why `aid` is listed too. +# +# Cargo warns that `main.rs` is "present in multiple build targets". That warning +# is the arrangement, not a mistake in it. The ways around it cost more: a +# `dla.rs` that loads `main.rs` as a module breaks the `crate::` paths in the +# submodules, and `include!` refuses `main.rs`'s `//!` docs. `test = false` +# keeps a plain `cargo test` from running `main.rs`'s unit tests twice; an +# explicit `--bins`, which CI passes, selects `dla` anyway, and the second run +# costs a link and well under a second. +[[bin]] +name = "aid" +path = "src/main.rs" + +[[bin]] +name = "dla" +path = "src/main.rs" +test = false + [dependencies] # The only devlaunch crate named here, and deliberately: aid is a command-line # rewrite that hands the result to dl, so what it can reach is whatever dl makes diff --git a/rust/aid/tests/rewrite.rs b/rust/aid/tests/rewrite.rs index a51ab742..fda56833 100644 --- a/rust/aid/tests/rewrite.rs +++ b/rust/aid/tests/rewrite.rs @@ -212,6 +212,37 @@ impl Run { } } +// =========================================================================== +// dla: aid under a second name +// =========================================================================== + +#[test] +fn dla_answers_exactly_as_aid_does() { + // `dla` is a second bin target over the same `main.rs` (`aid/Cargo.toml`), so + // it is aid in every respect, its own name in `--version` included. The two + // lines here are the ones aid answers without a workspace, which is enough to + // prove the binary is aid's and not some other program wearing the name. + for args in [&["--version"][..], &["--help"][..]] { + let aid = Command::new(env!("CARGO_BIN_EXE_aid")) + .args(args) + .keeping_coverage() + .output() + .expect("the aid binary runs"); + let dla = Command::new(env!("CARGO_BIN_EXE_dla")) + .args(args) + .keeping_coverage() + .output() + .expect("the dla binary runs"); + assert!(aid.status.success(), "aid {args:?}: {aid:?}"); + assert_eq!(dla.status.code(), aid.status.code(), "dla {args:?}"); + assert_eq!( + String::from_utf8_lossy(&dla.stdout), + String::from_utf8_lossy(&aid.stdout), + "dla {args:?}" + ); + } +} + // =========================================================================== // the three answers aid gives on its own // =========================================================================== diff --git a/rust/devlaunch-core/completions/dl.bash b/rust/devlaunch-core/completions/dl.bash index 49516065..5f2f9eb8 100644 --- a/rust/devlaunch-core/completions/dl.bash +++ b/rust/devlaunch-core/completions/dl.bash @@ -11,7 +11,8 @@ # # The same function serves `aid`, whose first argument is a dl workspace spec # too. Only the flag list and what follows the spec differ, so the two places -# that care branch on $cmd rather than the script being copied for aid. +# that care branch on $cmd rather than the script being copied for aid. `dla` is +# aid under a second name and completes as aid. _dl_completion() { local cur prev opts COMPREPLY=() @@ -48,11 +49,14 @@ _dl_completion() { fi fi - # The command being completed: dl or aid. + # The command being completed: dl or aid. `dla` is aid's second name. local cmd="" if (( ${#words[@]} > 0 )); then cmd="${words[0]##*/}" fi + if [[ "$cmd" == dla ]]; then + cmd=aid + fi # The options offered where a workspace spec goes. # @@ -375,4 +379,5 @@ _dl_completion() { # Use -o default for better completion behavior complete -o default -F _dl_completion dl complete -o default -F _dl_completion aid +complete -o default -F _dl_completion dla # end dl completion diff --git a/rust/dl/Cargo.toml b/rust/dl/Cargo.toml index 565fc5a9..b73f73de 100644 --- a/rust/dl/Cargo.toml +++ b/rust/dl/Cargo.toml @@ -23,10 +23,11 @@ serde_json = { workspace = true } skim = { workspace = true } term = { workspace = true } -# The released package ships two binaries, and a maturin bin-wheel ships the bin +# The released package ships three binaries (`dl`, `aid`, and `dla`, which is aid +# under a second name), and a maturin bin-wheel ships the bin # targets of exactly *one* cargo package (maturin 1.14 asks cargo metadata for # `root_package()` and takes its targets; there is no cross-package collection). -# So the package the wheel is built from has to own both bin targets, and this is +# So the package the wheel is built from has to own every bin target, and this is # that package -- see `packaging/wheel/pyproject.toml`. # # Nothing forks: the bin below compiles the very `aid/src/main.rs` the `aid` @@ -58,6 +59,13 @@ name = "aid" path = "../aid/src/main.rs" required-features = ["wheel"] +# `aid`'s second name, for the same reason it has one in `aid/Cargo.toml`. +[[bin]] +name = "dla" +path = "../aid/src/main.rs" +required-features = ["wheel"] +test = false + [dev-dependencies] devlaunch-test-support = { workspace = true } tempfile = { workspace = true } diff --git a/scripts/check_wheel.py b/scripts/check_wheel.py index d91b7b42..460256d9 100755 --- a/scripts/check_wheel.py +++ b/scripts/check_wheel.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 """Check a built devlaunch wheel before anything installs or publishes it. -The wheel is not a Python package any more: since 0.1.0 it is a container for two -compiled binaries, built by maturin from ``packaging/wheel/pyproject.toml``. That +The wheel is not a Python package any more: since 0.1.0 it is a container for +compiled binaries (`dl`, `aid`, and `dla`, which is aid under a second name), built by maturin from ``packaging/wheel/pyproject.toml``. That makes two things worth asserting mechanically, both of which are silent failures otherwise: -* **Both binaries are in there.** They come from *one* cargo package (`rust/dl`, - whose second bin target sits behind the ``wheel`` feature), so a lost feature +* **Every binary is in there.** They come from *one* cargo package (`rust/dl`, + whose `aid` and `dla` bin targets sit behind the ``wheel`` feature), so a lost feature flag or a renamed target ships a wheel with `dl` and no `aid` -- a wheel that installs cleanly and leaves half the tool missing. * **The version in the wheel is the version being released.** It is read from @@ -30,9 +30,10 @@ import zipfile from pathlib import Path -# What the wheel must contain, and all it may contain beyond `.dist-info`: the two +# What the wheel must contain, and all it may contain beyond `.dist-info`: the # binaries, as `.data/scripts/` entries that pip drops into the environment's bin/. -BINARIES = ("dl", "aid") +# `dla` is `aid` under a second name, built from the same source. +BINARIES = ("dl", "aid", "dla") def check(wheel: Path, version: str) -> list[str]: diff --git a/test/test_bash_completion.py b/test/test_bash_completion.py index e58bc853..0707071e 100644 --- a/test/test_bash_completion.py +++ b/test/test_bash_completion.py @@ -310,6 +310,11 @@ def test_aid_completes_the_same_workspaces_as_dl(self): """aid's first argument is a dl workspace spec, so it completes alike.""" assert self.run_completion("aid my-") == self.run_completion("dl my-") + def test_dla_completes_exactly_as_aid(self): + """`dla` is aid's second name, so every line completes as aid's does.""" + for line in ("dla my-", "dla --", "dla my-workspace ", "dla --codex my-"): + assert self.run_completion(line) == self.run_completion("aid" + line[3:]), line + def test_aid_completes_agent_flags(self): """Test completion of aid's own flags.""" completions = self.run_completion("aid --")