diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 14110ea..59ad49f 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -5,11 +5,15 @@ # Matrix (D1, cross-built from Linux x86_64 via cargo-zigbuild): # x86_64-unknown-linux-musl aarch64-unknown-linux-musl (static, D2) # x86_64-pc-windows-gnu aarch64-pc-windows-gnullvm (standalone .exe) -# macOS is deferred — its arboard/AppKit link needs Apple's SDK (see ADR-ci-001). -# D3 package-manager manifests layer on later. +# The two macOS targets are built separately by the dispatched +# release-macos.yaml (native Tart runner; ADR-ci-003 amendment), uploading to +# the same release. D3 package-manager manifests layer on later. # # Tests run once (host) before the matrix, so a tag can never publish untested -# code, even one pointing at a commit that was never gated on a branch. +# code, even one pointing at a commit that was never gated on a branch. The +# version guard (ADR-0054) refuses to publish a tag whose vX.Y.Z disagrees with +# Cargo.toml's version, keeping `--version`, the release name, and the asset in +# lockstep. name: release on: push: @@ -23,6 +27,22 @@ jobs: image: git.lazyeval.net/oli/rdbms-playground-ci:latest steps: - uses: actions/checkout@v4 + - name: version guard — tag must equal Cargo.toml version (ADR-0054) + shell: bash + env: + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + # CARGO_PKG_VERSION is the single source of truth; the binary reports + # it via --version / the `version` command. Parse it from cargo + # metadata (node is in the CI image; avoids assuming jq). + VER=$(nix develop -c cargo metadata --no-deps --format-version 1 \ + | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).packages[0].version))') + echo "tag=$TAG cargo=$VER" + if [ "$TAG" != "v$VER" ]; then + echo "ERROR: release tag '$TAG' != 'v$VER' (Cargo.toml). Bump Cargo.toml to the release version, commit, then retag (ADR-0054)." >&2 + exit 1 + fi - name: test run: nix develop -c cargo test --no-fail-fast diff --git a/docs/adr/0054-release-versioning-and-version-surfaces.md b/docs/adr/0054-release-versioning-and-version-surfaces.md new file mode 100644 index 0000000..c2a0378 --- /dev/null +++ b/docs/adr/0054-release-versioning-and-version-surfaces.md @@ -0,0 +1,71 @@ +# ADR-0054: Release versioning policy + version surfaces (`--version` / `version`) + +## Status + +Accepted — **implemented 2026-06-16** (plan: +`docs/plans/20260616-public-availability.md`, step 1). First step on the +road to public availability. Adds a `--version` / `-V` CLI flag and an +in-app `version` command, both reporting `CARGO_PKG_VERSION`, plus a +release-CI guard that the `v*` tag equals that version. No prior issue or +`requirements.md` item existed for this — it was an untracked gap. + +## Context + +Before this, `Cargo.toml` carried `version = "0.1.0"`, the binary exposed +**no** way to report its version, and `release.yaml` named assets from the +**git tag** (`rdbms-playground--`) while building from +`Cargo.toml`. Tag and crate version were **decoupled**: tagging `v0.2.0` +would publish an asset named `…-v0.2.0-…` containing a binary that (had it +been able to say) reported `0.1.0`. On the way to public availability — +where users download a versioned artifact and file bug reports against "the +version I'm running" — that drift is a correctness problem. + +## Decision + +1. **`Cargo.toml` `version` is the single source of truth.** This is the + idiomatic Rust position and avoids making `Cargo.toml` lie. The version + is read at compile time via `env!("CARGO_PKG_VERSION")`; no build-time + injection of the tag into the crate. + +2. **Two user-facing surfaces, one source:** + - **`--version` / `-V`** — CLI flag (hand-rolled parser, mirrors + `--help`): prints and exits before any other work (`main.rs`). + - **`version`** — an in-app app command (REGISTRY node `app::VERSION`, + `AppCommand::Version`), emitting the same line into the output panel. + Both go through `cli::version_text()` → the catalog key + `cli.version_line` (`"rdbms-playground {version}"`), so there is exactly + one rendered string and one version source. + +3. **Release-CI discipline.** `release.yaml`'s pre-build `test` job gains a + **version guard**: it parses `CARGO_PKG_VERSION` from `cargo metadata` + and **fails the release** unless the pushed tag equals `v`. + So `--version`, the release name, and the downloaded asset are always in + lockstep — enforced by the machine, not by memory. + +4. **The release ritual:** bump `Cargo.toml` → commit → tag `v` → push the tag. The guard rejects any deviation. + +### Rejected / deferred +- **Inject the tag into the build** (tag as source of truth): fiddly with + cargo and makes `Cargo.toml` a placeholder/lie. Rejected. +- **Git-hash + build-date enrichment** (a `build.rs` so dev builds read + `0.2.0 (a1b2c3d)`): useful for bug reports, but not needed for the + tag↔release↔`--version` consistency this ADR is about. Deferred; can be + added behind the same `version_text()` seam without changing the policy. +- **UI placement beyond the `version` command** (status-bar string, etc.): + the command + `help` listing is enough for now (user decision). + +## Consequences + +- A release can no longer ship a binary whose self-reported version + disagrees with its tag/filename. +- Cutting a release now *requires* a `Cargo.toml` bump commit — a small, + deliberate step (and a natural place to update a changelog later). +- New keys: `cli.version_line` (+ `help.app.version`, `parse.usage.version`, + `hint.cmd.version.what`/`.example`); a new REGISTRY command means the + comprehensiveness coverage test now also requires `hint.cmd.version`, + which is supplied. Tested: CLI flag parse (`--version`/`-V`/default-off), + `version_text()` carries `CARGO_PKG_VERSION`, the in-app command parses to + `AppCommand::Version` and emits the version. +- This is step 1 of `docs/plans/20260616-public-availability.md`; the + installer (`install.sh`) and package-manager work (D3) build on top. diff --git a/docs/adr/README.md b/docs/adr/README.md index c831b28..3f042b3 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -66,3 +66,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0051 — Bottom keybinding strip: context- and state-aware](0051-context-state-aware-keybinding-strip.md) — **Accepted + implemented 2026-06-13 (issue #27)**, closes Gitea **#27**. Repurposes the bottom status line into a **keystrokes-only, state-selected** strip (builds on ADR-0046 nav focus, ADR-0003 modes, ADR-0049 the #29 readline keys it now advertises, ADR-0022 the completion memo). A pure `status_bar_bindings(app) -> Vec<(key,label)>` chooses the strip by **priority, first match wins**: (1) **sidebar focus** → `Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input`; (2) **completion memo live** (`last_completion`) → `Tab/Shift-Tab cycle · Esc cancel · Enter run`; (3) **history navigation** (new `App::is_browsing_history()` exposing the private `history_cursor`) → `↑↓ browse · Esc clear · Enter run`; (4) **editing** (input non-empty) → `Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run` (surfaces the #29 keys, closing ADR-0049's deferred advertisement); (5) **default** (empty) → `Ctrl-O sidebar · Tab complete · ↑ history · Enter run`. Priority is correct because Up clears the completion memo and Tab cancels history nav, so states 2/3 never co-occur, and the five are exhaustive for Input focus. **Typed-command words leave the strip** (`mode advanced`/`mode simple` switch, `:` one-shot) and **mode discovery moves to the empty-input hint** (`resolve_hint_lines`), **simple mode only**: `\`mode advanced\` for SQL` (the verb "type" omitted — the prompt implies it; advanced mode shows **no** pointer per a post-trial user decision — a switcher knows how they got there and `help` covers the way back). The one-shot's old `Backspace cancel one-shot` label is subsumed by the editing state (behaviour intact). Forks all user-chosen: **editing state shows the #29 keys** (vs unadvertised); **`Ctrl-C quit` omitted** from the strip (vs always shown); **no width-drop machinery** — the longest strip (~65 cols) fits all supported widths, so a **width-budget unit test** keeps it lean by construction instead (the user's own observation). Catalog: 12 new `shortcut.*` labels + the `panel.hint_mode_advanced` string added to `en-US.yaml`+`keys.rs` (validator-checked 1:1), 5 now-dead strip strings removed. **Modal-aware strip is OOS** (pre-existing: a modal owns the keyboard and carries its own hints; the strip under it is unchanged-in-kind, not worsened). Tests: 9 Tier-1 unit (per-state key sets — completion/history driven through real key events; width budget; mode-pointer presence/absence), 1 Tier-3 rewritten (`status_bar_is_keystroke_only_and_state_aware`), 15 full-panel snapshots re-accepted (reviewed — strip/hint only). **2467 pass / 0 fail / 0 skip (1 ignored), clippy clean.** OOS: modal-aware strip; a full-key cheatsheet overlay; Ctrl-K/U advertisement (editing strip shows the highest-value subset within the width budget) - [ADR-0052 — Mode-tagged history for cross-mode recall](0052-mode-tagged-history-cross-mode-recall.md) — **Accepted + implemented 2026-06-13 (issue #30)**, closes Gitea **#30** — the feature (advanced history reusable in simple mode) **and** the bug in its comment (the `:` one-shot prefix lost across sessions). **Amends ADR-0034** (status field gains a `:adv` tag; **journaling moves from the worker to the dispatch layer**), **ADR-0015 §5/§6** (history.log leaves the worker transaction — `commit-db-last` now scopes yaml/csv/db only), and **ADR-0040** (a success-path journal-write failure is best-effort, not fatal); references ADR-0003. **Root cause:** history carried no mode, and the in-memory ring stored the raw `:select 1` while the worker journalled the *stripped* `select 1`, so the `:` was lost on disk. **Fix:** record the submission mode per entry as a **`:adv` suffix on the status token** (`ok`/`ok:adv`/`err`/`err:adv`) — `source` stays last + canonical so replay is unaffected; the in-memory ring (still `Vec`) stores advanced entries in their `: `-prefixed simple-mode runnable form (a leading `:` unambiguously marks advanced since simple DSL never starts with `:`); recall **strips the `:` in advanced mode** (runs as bare SQL) and keeps it in simple mode (runs via the one-shot escape); hydration reconstructs the `: `-prefix from the tag, so cross-session = in-session. **The architectural turn (user's call):** the first draft kept journaling in the worker + threaded the mode down (~30-site plumbing); on review the user asked why the journal is written deep in the worker when the *failure* path already journals at the top of the chain — it shouldn't (history.log is a journal, not state). So **success journaling moved up** to `spawn_dsl_dispatch` / `run_replay` / the app-command sites (next to the failure path), the worker's `finalize_persistence` now writes only yaml/csv, and the journal write became **best-effort** (the command is already committed — consistent with the failure path; a rare disk-full leaves a committed command unjournalled, state intact). **App commands** journal simple (dispatched outside the spawn) and `submit` excludes them from the ring's advanced flag, so `undo`/`mode advanced` recall bare. Forks user-chosen: status-tag format (vs 4th field / `:`-in-source); unified scope; **dispatch-layer best-effort journaling** (vs worker-coupled-fatal). Two `/runda` passes (the second drove the relocation + app-command exclusion). Tests: the 15 worker-level journaling tests retired (worker no longer journals — yaml/csv/operation checks kept), re-covered at the new layer (history.rs status-tag + `:`-reconstruct; app.rs recall matrix; the #30 cross-session regression in `iteration6`; replay tests cover `run_replay` journaling). **2471 pass / 0 fail / 0 skip (1 ignored), clippy clean.** replay re-journaling mode-fidelity (a replayed advanced line re-journals simple — not a regression). **Follow-up done 2026-06-14:** the vestigial worker `source` plumbing was fully unwound (compiler-guided, no behaviour change) — `_source` removed from `finalize_persistence`/`do_rebuild_from_text`, the three `*_request` wrappers inlined+deleted, the dead `source` param dropped from the ~30 forwarding worker handlers, and the `source` field removed from the `DescribeTable`/`QueryData`/`RunSelect` requests + their `DatabaseHandle` methods (~164 mostly-test call sites); the only worker `source` left is the snapshot/undo label (see ADR-0052 *Consequences*) - [ADR-0053 — Contextual `hint` command and keybinding](0053-contextual-hint-command-and-keybinding.md) — **Accepted, implemented 2026-06-15** (Phases A–D; closes **A1** + requirements **H2**). Settles the `hint` slot ADR-0003 left "ADR pending"; closes the last open piece of **A1** and tracks requirements **H2**. **Two surfaces:** an **F1 keybinding** that renders a deep hint for the *live* partial input without submitting (the primary path — a submitted `hint` command can't see the buffer it would help with, since Enter empties it), and a submitted **`hint` command** that expands on the *most recent error*. **No topic argument** (contextual only — `help ` already owns explicit reference). Introduces a **tier-3 teaching layer**, deeper than the existing tier-1 (colour / error headline) and tier-2 (ambient one-liner; and the error `hint:`, which is shown **by default** since `Verbosity::Verbose` is the default — `messages short` is the opt-*out*); without it `hint` would just duplicate what's already on screen. Tier-3 content lives in the catalogue under `hint.cmd.` (per command form) and `hint.err.` (per error/diagnostic class), each a structured `what`/`example`/`concept` block rendered via a new `note_hint*` family with `OutputStyleClass::Hint`. **Keyed per-form via a new `hint_ids: &[&str]` field on `CommandNode` mirroring `usage_ids`** (revised in Phase B): a per-*node* key proved too coarse — `add`/`drop`/`show`/`create` are each one node spanning many forms, and a live-input hint for `add 1:n relationship` must be specific to relationships; `hint_key_for_input_in_mode` reuses `usage_key_for_input_in_mode`'s form-word disambiguation, and covers the advanced-SQL forms whose `usage_ids` are empty. Not keyed off `help_id` (it is `None` on the advanced-SQL nodes purely to dedup the `help` list; that parallel gap is issue **#36**). **Clause-concept hints** (`on delete` actions, constraint slots, `with pk`, cardinality) are a recorded **deferred extension** (`hint.concept.`, issue **#37**) — per-form is the right tier-3 granularity, with position-awareness owned by tier-2 + the live `Next:` line. Runtime `translate_error` classes resolve via stored `last_error_hint_key` (`hint` command / empty-F1). (The second route — pre-submit `diagnostic.*` read live from the walker on the F1 path — is **deferred**, issue **#38**: `Diagnostic` carries no class key.) Adds `AppCommand::Hint`, a `HINT` grammar node + REGISTRY entry, the `hint_ids` field, and `last_error_hint_key`; F1 is a read-only overlay (buffer + completion memo untouched). **Content is the bulk of the work** (the mechanism is ~a day): v1 scope = ~37 command forms + 9 runtime error classes (comprehensive for those, ~57 blocks), authored **exemplars-first** (voice approved in this ADR's `/runda` review, then mass-authored in batches), enforced by a **comprehensiveness coverage test**, with graceful fall-back to tier-2 if a key is ever missing. The **pre-submit-diagnostic route + ~33 `diagnostic.*` blocks were deferred** (issue **#38**) — `Diagnostic` carries no class key, so the route needs a broad change for marginal value (tier-2 already surfaces diagnostics; many duplicate runtime classes). Forks user-chosen: two-surface model; **F1** (vs `?` / a chord); no-arg; comprehensive-for-commands-and-errors scope; exemplars-first; diagnostics deferred. OOS: per-topic `hint ` (rejected — overlaps `help`); always-on tier-3 (rejected — keeps ambient terse); non-`en-US` locales + success-command teaching (deferred); clause-concept hints (issue #37); the diagnostic route (issue #38); the `help`-side advanced-SQL gap (issue #36) +- [ADR-0054 — Release versioning policy + version surfaces (`--version` / `version`)](0054-release-versioning-and-version-surfaces.md) — **Accepted + implemented 2026-06-16** (plan: `docs/plans/20260616-public-availability.md`, step 1 on the road to public availability; no prior issue/`requirements.md` item — an untracked gap). Fixes the **tag↔crate-version decoupling**: `Cargo.toml` built `0.1.0` while `release.yaml` named assets from the git tag, so a binary could report a version different from the asset it shipped in. **Decision:** `Cargo.toml` `version` is the **single source of truth** (read via `env!("CARGO_PKG_VERSION")`, no tag-injection); two surfaces report it through one `cli::version_text()` → catalog `cli.version_line` — a **`--version` / `-V`** CLI flag (mirrors `--help`, prints+exits in `main.rs`) and an in-app **`version`** command (REGISTRY node `app::VERSION`, `AppCommand::Version`, emits via `note_system`); and a **release-CI version guard** (`release.yaml` `test` job parses `cargo metadata` and **fails the release** unless the `v*` tag equals `v`). Release ritual: bump `Cargo.toml` → commit → tag → push. New keys `cli.version_line` + `help.app.version` + `parse.usage.version` + `hint.cmd.version.{what,example}` (the new REGISTRY command pulls in the comprehensiveness coverage gate). Rejected: tag-as-source (makes Cargo.toml lie). Deferred: git-hash/build-date enrichment (behind the same `version_text()` seam); UI placement beyond the command. Tested test-first: CLI parse (`--version`/`-V`/default-off), `version_text()` carries `CARGO_PKG_VERSION`, the in-app command parses + emits. Also corrected a stale `release.yaml` header comment ("macOS is deferred" → built by the dispatched `release-macos.yaml`). diff --git a/src/app.rs b/src/app.rs index 1db2815..efba2fa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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(); diff --git a/src/cli.rs b/src/cli.rs index 57f9a23..9796431 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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 = None; let mut resume = false; let mut help = false; + let mut version = false; let mut no_undo = false; let mut mode: Option = 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:?}" + ); + } } diff --git a/src/dsl/command.rs b/src/dsl/command.rs index 66d1933..db0e23e 100644 --- a/src/dsl/command.rs +++ b/src/dsl/command.rs @@ -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", diff --git a/src/dsl/grammar/app.rs b/src/dsl/grammar/app.rs index 814151a..a4f116f 100644 --- a/src/dsl/grammar/app.rs +++ b/src/dsl/grammar/app.rs @@ -174,6 +174,10 @@ const fn build_rebuild(_path: &MatchedPath, _source: &str) -> Result Result { + Ok(Command::App(AppCommand::Version)) +} + const fn build_undo(_path: &MatchedPath, _source: &str) -> Result { 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, diff --git a/src/dsl/grammar/mod.rs b/src/dsl/grammar/mod.rs index 337b039..ff2bc23 100644 --- a/src/dsl/grammar/mod.rs +++ b/src/dsl/grammar/mod.rs @@ -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), diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index 341ee54..bd40341 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -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"], diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index 50cc352..41caaf8 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -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 Override theme (default: auto-detect). --data-dir 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 — 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 []" hint: "hint" + version: "version" rebuild: "rebuild" save: "save | save as" new: "new" diff --git a/src/main.rs b/src/main.rs index dc9c6e2..af54091 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; diff --git a/tests/typing_surface/mod.rs b/tests/typing_surface/mod.rs index 31e7229..bf4d88a 100644 --- a/tests/typing_surface/mod.rs +++ b/tests/typing_surface/mod.rs @@ -251,6 +251,7 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String { AppCommand::Quit => "App(Quit)".into(), AppCommand::Help { .. } => "App(Help)".into(), AppCommand::Hint => "App(Hint)".into(), + AppCommand::Version => "App(Version)".into(), AppCommand::Rebuild => "App(Rebuild)".into(), AppCommand::Save => "App(Save)".into(), AppCommand::SaveAs => "App(SaveAs)".into(),