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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.1] - 2026-08-20

### Changed

- **`dl --help` puts the verbs and the examples above the options table.** clap's default
layout renders every flag between the usage line and the `after_help` block, so the half of
this CLI clap cannot describe from its own arguments — the workspace verbs, the examples, the
suffix-flag and retired-word notes, the environment variables — sat below fourteen options
nobody opened `--help` to read. The examples and the verb list now come straight after the
usage line (`before_help`, plus a `help_template` that is clap's own default with
`{before-help}` moved above `{all-args}`), `Environment:` stays last, and a unit test pins
the order. Note that clap already does this for a CLI whose verbs are subcommands — it writes
`Commands:` before `Options:` — and dl's verbs are positional words only because the grammar
is workspace-first, so this is the layout clap would have produced anyway. Same flags, same
verbs, same text, for `-h` and `--help` alike.
- **The README is ordered the same way, for the same reason.** The options table, the workspace
id derivation and the purge/prune/reconcile and disk-accounting detail used to come before
the things someone actually types, and `## Examples` sat 1200 lines down. What a reader needs
on the way in — features, install, usage with the examples beside it, workspace and global
commands, `aid` — is now the top half; a rule and a note mark where reference begins; and the
header carries a two-row index. The `dl --ls` table was split out of `## Global Commands`
from the 500 lines of cleanup detail beneath it, now `## Cleaning up: purge, prune,
reconcile`. One duplicated example block (`## Workspace Sources`) was folded into
`### Examples`. Every existing anchor still resolves.

## [0.2.0] - 2026-08-20

### Added
Expand Down
951 changes: 481 additions & 470 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/rust-rewrite-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ Nothing diverges silently.
|---|---|---|
| 1 | Reserved verb words (`stop`, `prune`, …) win over bare workspace specs; verb-first with no target opens the fuzzy selector. Python parses `dl stop` as a spec. | Owner requirement folded into #251 §6. |
| 2 | clap strictness: unknown flags are rejected; no argparse-style prefix abbreviation. | Correctness aim winning over accidental laxity. |
| 3 | `--help` layout is clap's, not the hand-rolled text. | Generated help feeds the README rule. |
| 3 | `--help` layout is clap's, not the hand-rolled text — reordered so the verbs and examples land above the options table. | Generated help feeds the README rule. |
| 4 | Failure output: no Python tracebacks, ever; typed errors rendered as one-line diagnostics with the same exit codes. OS-level reasons inside diagnostics use Rust's phrasing (`Permission denied (os error 13)`) rather than Python's `[Errno 13] …`, and JSON type names in refusals may differ (`int` where Python said `float`). | #251 §5. |
| 5 | Startup/perf deltas asserted nowhere; `bench.yml` stays outside the gate. | Perf is not a parity dimension. |
| 6 | The fuzzy selector is embedded skim, not spawned fzf; `iterfzf`/fzf are no longer runtime dependencies. With no terminal (`dl < /dev/null`, workspaces present) the picker declines silently where fzf wrote `inappropriate ioctl for device` to stderr; stdout and exit 1 are identical. | Removes a launch-failure class (#251 §6). |
Expand Down
10 changes: 5 additions & 5 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ members = [
# The single source of the version (docs/rust-rewrite-plan.md: cutover ships
# 0.1.0, version read from Cargo.toml).
[workspace.package]
version = "0.2.0"
version = "0.2.1"
edition = "2024"
license = "MIT"
repository = "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/blooop/devlaunch"
Expand Down
2 changes: 1 addition & 1 deletion rust/devlaunch-test-support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ pub use response::Response;
mod tests {
#[test]
fn versioned_with_the_workspace() {
assert_eq!(env!("CARGO_PKG_VERSION"), "0.2.0");
assert_eq!(env!("CARGO_PKG_VERSION"), "0.2.1");
}
}
91 changes: 70 additions & 21 deletions rust/dl/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,28 @@ pub(crate) enum GrammarError {
// `--version` is dispatched like every other command, so the summary that
// prints it lives with the rest of the rendering rather than in clap.
disable_version_flag = true,
// One epilogue for both `-h` and `--help`: the verbs are a grammar clap
// cannot describe from its own arguments — they are positional words — so a
// help text without this block would not document half the CLI, and
// README's "regenerate against `dl --help`" rule would carry the hole
// forward.
after_help = EPILOGUE,
// Two hand-written blocks for both `-h` and `--help`: the verbs are a grammar
// clap cannot describe from its own arguments — they are positional words — so
// a help text without them would not document half the CLI, and README's
// "regenerate against `dl --help`" rule would carry the hole forward.
//
// `GRAMMAR` is `before_help` rather than `after_help` so that it lands above
// the options table, which is what the template below moves it there for: the
// flags are the part of this CLI a person reaches for least, and clap's default
// layout puts all of them between the usage line and the verbs and examples
// somebody opened `--help` to find. The same reordering the README got, for the
// same reason.
before_help = GRAMMAR,
after_help = ENVIRONMENT,
// clap's own default template with `{before-help}` moved from the top to just
// above `{all-args}`. Every marker here is one the default already uses — the
// order is the only change — because an unknown marker is not a compile error,
// it renders literally.
help_template = "\
{about-with-newline}
{usage-heading} {usage}

{before-help}{all-args}{after-help}",
)]
pub(crate) struct Cli {
/// The workspace to open, and optionally the verb to apply: `dl owner/repo`,
Expand Down Expand Up @@ -413,7 +429,21 @@ pub(crate) struct Cli {

/// The half of the grammar clap's own argument list cannot show: the verbs are
/// positional words, so nothing in the options table names them.
const EPILOGUE: &str = "Workspace commands (dl <workspace> <verb>, or dl <verb> <workspace>):
///
/// Rendered above that options table (`before_help` plus the reordered
/// `help_template` on [`Cli`]), examples first, because this is the half somebody
/// opened `--help` to read.
const GRAMMAR: &str = "Examples:
dl Pick a workspace interactively
dl blooop/devlaunch Open it on its default branch
dl blooop/devlaunch@fix/123 Open it on that branch, creating it if needed
dl ./my-project Open a local folder
dl blooop/devlaunch -- make test Run one command inside the workspace
dl blooop/devlaunch stop Stop it
dl stop blooop-devlaunch-main-1a2b Stop it by workspace id
dl --ls --json Every workspace, machine-readable

Workspace commands (dl <workspace> <verb>, or dl <verb> <workspace>):
up Start it without attaching
stop Stop it
rm Delete it. Refuses if its clone holds
Expand Down Expand Up @@ -442,21 +472,15 @@ at the end of it rather than rewriting its front. Whatever the suffix beat is na
on stderr before anything is removed, so a deliberate one and a slip do not look
alike.

'prune' was a second spelling of the rm verb and is retired: it collided with
--prune above, which removes clone directories and no workspace at all. Typing it
says so and names both.

Examples:
dl Pick a workspace interactively
dl blooop/devlaunch Open it on its default branch
dl blooop/devlaunch@fix/123 Open it on that branch, creating it if needed
dl ./my-project Open a local folder
dl blooop/devlaunch -- make test Run one command inside the workspace
dl blooop/devlaunch stop Stop it
dl stop blooop-devlaunch-main-1a2b Stop it by workspace id
dl --ls --json Every workspace, machine-readable
'prune' was a second spelling of the rm verb and is retired: it collided with the
--prune flag below, which removes clone directories and no workspace at all. Typing
it says so and names both.";

Environment:
/// What no flag and no verb names: the variables that change a launch.
///
/// Last, below the options table, because reading it is what somebody does once
/// and then puts in a shell profile.
const ENVIRONMENT: &str = "Environment:
DEVLAUNCH_TIMING=1|json Write a timing summary to stderr
DEVLAUNCH_NO_GH_TOKEN=1 Do not forward the host's gh login
DEVLAUNCH_DOTFILES_ON_ATTACH=1 Refresh dotfiles before every attach";
Expand Down Expand Up @@ -1425,4 +1449,29 @@ mod tests {
assert_eq!(argv_without_devcontainer(&owned), expected, "{argv:?}");
}
}

/// Divergence row 3 licenses clap's layout; *which* clap layout is still a
/// decision, and this is it — the verbs and examples somebody opened `--help`
/// to find come above the options table, not below it.
///
/// `help_template` is a string clap resolves at render time, so a marker that
/// stops resolving is not a compile error — it renders as its own literal text
/// and the section it stood for goes missing, which is what every `find` below
/// is also checking for.
#[test]
fn help_puts_the_grammar_above_the_options_table() {
use clap::CommandFactory;

let help = Cli::command().render_long_help().to_string();
let examples = help.find("Examples:").expect("no examples");
let verbs = help.find("Workspace commands").expect("no verb list");
let options = help.find("Options:").expect("no options table");
let environment = help.find("Environment:").expect("no environment");

assert!(examples < verbs, "{help}");
assert!(verbs < options, "{help}");
assert!(options < environment, "{help}");
assert!(help.starts_with("Open a devcontainer"), "{help}");
assert!(help.contains("Usage: dl "), "{help}");
}
}