feat(cli): --version/-V + in-app version command + release guard (ADR-0054)

Cargo.toml version is the single source of truth, surfaced by a
--version/-V CLI flag and an in-app `version` command (both via
cli::version_text -> cli.version_line). release.yaml gains a guard that
fails the release unless the v* tag equals v<CARGO_PKG_VERSION>, keeping
--version, the release name, and the asset in lockstep. New app command
wired across grammar/REGISTRY/dispatch/usage/help/hint-corpus/keys; 6
test-first tests. Also fixes a stale "macOS deferred" comment in
release.yaml. ADR-0054 + README index + plan-doc step 1.
This commit is contained in:
claude@clouddev1
2026-06-16 15:57:54 +00:00
parent fe9d58e037
commit c30a6114b9
12 changed files with 215 additions and 4 deletions
+29
View File
@@ -1868,6 +1868,12 @@ impl App {
self.note_hint_for_recent_error();
Vec::new()
}
// ADR-0054: the in-app twin of `--version`. Reports the same
// single source of truth (`CARGO_PKG_VERSION`, via cli::version_text).
AppCommand::Version => {
self.note_system(crate::cli::version_text());
Vec::new()
}
AppCommand::Rebuild => vec![Action::PrepareRebuild],
AppCommand::Save => self.handle_save_command(false),
AppCommand::SaveAs => self.handle_save_command(true),
@@ -5737,6 +5743,29 @@ mod tests {
));
}
// ── ADR-0054: in-app `version` command ──────────────────────────
#[test]
fn version_command_parses_to_app_version() {
use crate::dsl::{parse_command, AppCommand, Command};
assert!(matches!(
parse_command("version"),
Ok(Command::App(AppCommand::Version))
));
}
#[test]
fn version_command_emits_the_cargo_version() {
let mut app = App::new();
type_str(&mut app, "version");
submit(&mut app);
assert!(
output_contains(&app, env!("CARGO_PKG_VERSION")),
"in-app `version` should print CARGO_PKG_VERSION: {}",
error_lines(&app),
);
}
#[test]
fn hint_command_with_no_recent_error_shows_getting_started() {
let mut app = App::new();
+49
View File
@@ -30,6 +30,10 @@ pub struct Args {
/// `--help` / `-h`: print usage to stdout and exit. The
/// runtime checks this flag before doing any other work.
pub help: bool,
/// `--version` / `-V`: print the version (`CARGO_PKG_VERSION`,
/// the single source of truth — ADR-0054) and exit. Checked
/// alongside `--help` before any other work.
pub version: bool,
/// `--no-undo`: disable the auto-snapshot / undo machinery for
/// this run (ADR-0006 Amendment 1). When set, no snapshots are
/// taken — zero per-command overhead — and `undo` / `redo`
@@ -62,6 +66,17 @@ pub fn help_text() -> String {
crate::t!("help.cli_banner")
}
/// Version line printed by `--version` / `-V` and the in-app `version`
/// command (ADR-0054).
///
/// `CARGO_PKG_VERSION` is the single source of truth — it equals the `v*`
/// release tag (the release CI guards that), so what the binary reports
/// always matches the downloaded artifact.
#[must_use]
pub fn version_text() -> String {
crate::t!("cli.version_line", version = env!("CARGO_PKG_VERSION"))
}
#[derive(Debug)]
pub enum ArgsError {
MissingValue(&'static str),
@@ -129,6 +144,7 @@ impl Args {
let mut project_path: Option<PathBuf> = None;
let mut resume = false;
let mut help = false;
let mut version = false;
let mut no_undo = false;
let mut mode: Option<Mode> = None;
// Demonstration mode (ADR-0047): the env var is the default,
@@ -143,6 +159,9 @@ impl Args {
"--help" | "-h" => {
help = true;
}
"--version" | "-V" => {
version = true;
}
"--resume" => {
resume = true;
}
@@ -208,6 +227,7 @@ impl Args {
project_path,
resume,
help,
version,
no_undo,
mode,
demo,
@@ -475,4 +495,33 @@ mod tests {
let err = Args::parse(["--bogus", "/some/path"]).unwrap_err();
assert!(matches!(&err, ArgsError::Unknown(s) if s == "--bogus"), "got: {err:?}");
}
// ---- ADR-0054: --version / -V ----
#[test]
fn version_long_flag_parses() {
assert!(Args::parse(["--version"]).unwrap().version);
}
#[test]
fn version_short_flag_parses() {
assert!(Args::parse(["-V"]).unwrap().version);
}
#[test]
fn version_defaults_off() {
assert!(!Args::parse(std::iter::empty::<&str>()).unwrap().version);
}
#[test]
fn version_text_carries_the_cargo_version() {
// The binary's self-reported version IS Cargo.toml's (the
// single source of truth, ADR-0054) — and the release CI guards
// that the `v*` tag equals it.
let text = version_text();
assert!(
text.contains(env!("CARGO_PKG_VERSION")),
"version line should embed CARGO_PKG_VERSION; got {text:?}"
);
}
}
+5
View File
@@ -557,6 +557,10 @@ pub enum AppCommand {
/// (the buffer is empty post-submit). The live-input surface is
/// the F1 keybinding, handled in `App::handle_key`, not here.
Hint,
/// Print the application version (ADR-0054): the in-app twin of the
/// `--version` / `-V` CLI flag. Emits `CARGO_PKG_VERSION` — the same
/// single source of truth — into the output panel.
Version,
/// Rebuild `playground.db` from `project.yaml` + data/, with
/// confirmation modal.
Rebuild,
@@ -1019,6 +1023,7 @@ impl Command {
AppCommand::Quit => "quit",
AppCommand::Help { .. } => "help",
AppCommand::Hint => "hint",
AppCommand::Version => "version",
AppCommand::Rebuild => "rebuild",
AppCommand::Save => "save",
AppCommand::SaveAs => "save as",
+12
View File
@@ -174,6 +174,10 @@ const fn build_rebuild(_path: &MatchedPath, _source: &str) -> Result<Command, Va
Ok(Command::App(AppCommand::Rebuild))
}
const fn build_version(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
Ok(Command::App(AppCommand::Version))
}
const fn build_undo(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
Ok(Command::App(AppCommand::Undo))
}
@@ -294,6 +298,14 @@ pub static REBUILD: CommandNode = CommandNode {
hint_ids: &["rebuild"],
usage_ids: &["parse.usage.rebuild"],};
pub static VERSION: CommandNode = CommandNode {
entry: Word::keyword("version"),
shape: EMPTY_SEQ,
ast_builder: build_version,
help_id: Some("app.version"),
hint_ids: &["version"],
usage_ids: &["parse.usage.version"],};
pub static SAVE: CommandNode = CommandNode {
entry: Word::keyword("save"),
shape: SAVE_AS_OPT,
+1
View File
@@ -791,6 +791,7 @@ pub static REGISTRY: &[(&CommandNode, CommandCategory)] = &[
(&app::QUIT, CommandCategory::Simple),
(&app::HELP, CommandCategory::Simple),
(&app::HINT, CommandCategory::Simple),
(&app::VERSION, CommandCategory::Simple),
(&app::REBUILD, CommandCategory::Simple),
(&app::SAVE, CommandCategory::Simple),
(&app::NEW, CommandCategory::Simple),
+5
View File
@@ -181,6 +181,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("help.app.quit", &[]),
("help.app.help", &[]),
("help.app.hint", &[]),
("help.app.version", &[]),
("help.app.rebuild", &[]),
("help.app.save", &[]),
("help.app.new", &[]),
@@ -272,6 +273,8 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("hint.cmd.help.concept", &[]),
("hint.cmd.hint.what", &[]),
("hint.cmd.hint.example", &[]),
("hint.cmd.version.what", &[]),
("hint.cmd.version.example", &[]),
("hint.cmd.rebuild.what", &[]),
("hint.cmd.rebuild.example", &[]),
("hint.cmd.rebuild.concept", &[]),
@@ -486,6 +489,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("parse.usage.mode", &[]),
("parse.usage.new", &[]),
("parse.usage.quit", &[]),
("parse.usage.version", &[]),
("parse.usage.rebuild", &[]),
("parse.usage.redo", &[]),
("parse.usage.replay", &[]),
@@ -580,6 +584,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("cli.multiple_paths", &["first", "second"]),
("cli.resume_with_path", &[]),
("cli.unknown_argument", &["arg"]),
("cli.version_line", &["version"]),
(
"archive.export_sequence_exhausted",
&["project", "target_dir", "limit"],
+12
View File
@@ -162,6 +162,10 @@ error:
# ---- Help text (CLI banner + in-app `help` command) ------------------
# ---- CLI argument-parsing errors (stderr before TUI starts) ---------
cli:
# Version line for `--version` / `-V` and the in-app `version` command
# (ADR-0054). `{version}` is `CARGO_PKG_VERSION` — the single source of
# truth, equal to the `v*` release tag (release CI guards the match).
version_line: "rdbms-playground {version}"
missing_value: "missing value for --{flag}"
invalid_value: "invalid value for --{flag}: {value} (expected one of: {expected})"
unknown_argument: "unknown argument: {arg}"
@@ -186,6 +190,7 @@ help:
Options:
-h, --help Print this help and exit.
-V, --version Print the version and exit.
--theme <light|dark> Override theme (default: auto-detect).
--data-dir <PATH> Use PATH as the data root instead of
the OS-standard location for this run.
@@ -210,6 +215,7 @@ help:
App-level commands (typed inside the app, available in both modes):
quit Exit cleanly.
version Print the application version.
mode simple|advanced Switch input mode.
help Show this list of commands in-app.
save Save the current temp project under a
@@ -258,6 +264,8 @@ help:
help <command> — detailed help for one command (e.g. `help insert`)
hint: |-
hint — explain the most recent error (press F1 for a hint on what you're typing)
version: |-
version — print the application version (same as the `--version` command-line flag)
rebuild: |-
rebuild — rebuild the project database from project.yaml + data/ (with confirmation)
save: |-
@@ -425,6 +433,9 @@ hint:
hint:
what: "Explain the most recent error — or, pressing F1 while typing, the command you're building."
example: "hint"
version:
what: "Print the application version."
example: "version"
rebuild:
what: "Rebuild the project database from its saved text files."
example: "rebuild"
@@ -874,6 +885,7 @@ parse:
quit: "quit"
help: "help [<command>]"
hint: "hint"
version: "version"
rebuild: "rebuild"
save: "save | save as"
new: "new"
+6 -1
View File
@@ -1,6 +1,6 @@
use std::process::ExitCode;
use rdbms_playground::cli::{help_text, Args};
use rdbms_playground::cli::{help_text, version_text, Args};
use rdbms_playground::{logging, runtime};
fn main() -> ExitCode {
@@ -22,6 +22,11 @@ fn main() -> ExitCode {
}
};
if args.version {
println!("{}", version_text());
return ExitCode::SUCCESS;
}
if args.help {
print!("{}", help_text());
return ExitCode::SUCCESS;