#![forbid(unsafe_code)]
use shipwright_manifest::{ExecutableKind, Language, ManifestError, VersionOutput};
use std::io::Write;
#[derive(Debug, thiserror::Error)]
pub enum CliError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("manifest: {0}")]
Manifest(#[from] ManifestError),
}
#[derive(Debug, Clone, Copy)]
pub struct BuildInfo<'a> {
pub git_sha: Option<&'a str>,
pub git_dirty: Option<bool>,
pub build_time: Option<&'a str>,
pub target: Option<&'a str>,
pub toolchain: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VersionMode {
NotRequested,
Plain,
Json,
}
impl VersionMode {
pub fn from_args<I, S>(args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut has_version = false;
let mut has_json = false;
for a in args {
match a.as_ref() {
"--version" | "-V" => has_version = true,
"--json" => has_json = true,
_ => {}
}
}
match (has_version, has_json) {
(true, true) => Self::Json,
(true, false) => Self::Plain,
_ => Self::NotRequested,
}
}
}
pub fn write_plain<W: Write>(mut w: W, name: &str, version: &str) -> std::io::Result<()> {
writeln!(w, "{name} {version}")
}
#[derive(Debug, Clone)]
pub struct VersionSpec<'a> {
pub name: &'a str,
pub version: &'a str,
pub kind: ExecutableKind,
pub language: Language,
pub product: Option<&'a str>,
pub capabilities: &'a [&'a str],
pub build: BuildInfo<'a>,
}
pub fn write_json<W: Write>(mut w: W, spec: &VersionSpec<'_>) -> Result<(), CliError> {
let mut out = VersionOutput::new(
spec.name,
spec.version,
spec.kind.clone(),
spec.language.clone(),
)?;
if let Some(sha) = spec.build.git_sha {
out.git_sha = Some(sha.to_string());
}
if let Some(dirty) = spec.build.git_dirty {
out.git_dirty = Some(dirty);
}
if let Some(t) = spec.build.build_time {
out.build_time = Some(t.to_string());
}
if let Some(t) = spec.build.target {
out.target = Some(t.to_string());
}
if let Some(t) = spec.build.toolchain {
out.toolchain = Some(t.to_string());
}
out.capabilities = spec
.capabilities
.iter()
.copied()
.map(str::to_string)
.collect();
if let Some(p) = spec.product {
out.product = Some(p.to_string());
}
serde_json::to_writer(&mut w, &out).map_err(ManifestError::from)?;
writeln!(w)?;
Ok(())
}
pub fn dispatch<I, S, W>(args: I, out: W, spec: &VersionSpec<'_>) -> Result<bool, CliError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
W: Write,
{
match VersionMode::from_args(args) {
VersionMode::NotRequested => Ok(false),
VersionMode::Plain => {
write_plain(out, spec.name, spec.version)?;
Ok(true)
}
VersionMode::Json => {
write_json(out, spec)?;
Ok(true)
}
}
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::missing_docs_in_private_items
)]
mod tests {
use super::{write_json, write_plain, BuildInfo, VersionMode, VersionSpec};
use shipwright_manifest::{ExecutableKind, Language};
#[test]
fn plain_matches_contract() {
let mut buf: Vec<u8> = Vec::new();
write_plain(&mut buf, "deslop-lsp", "0.4.2").expect("write");
assert_eq!(buf, b"deslop-lsp 0.4.2\n");
}
#[test]
fn json_conforms() {
let mut buf: Vec<u8> = Vec::new();
write_json(
&mut buf,
&VersionSpec {
name: "deslop-lsp",
version: "0.4.2",
kind: ExecutableKind::Lsp,
language: Language::Rust,
product: Some("deslop"),
capabilities: &["lsp", "stdio"],
build: BuildInfo {
git_sha: Some("7a9b3e1"),
git_dirty: Some(false),
build_time: Some("2026-04-22T10:14:55Z"),
target: Some("aarch64-apple-darwin"),
toolchain: Some("rustc 1.84.0"),
},
},
)
.expect("write json");
let s = std::str::from_utf8(&buf).expect("utf8");
assert!(s.contains("\"name\":\"deslop-lsp\""), "name: {s}");
assert!(s.contains("\"version\":\"0.4.2\""), "version: {s}");
assert!(s.contains("\"kind\":\"lsp\""), "kind: {s}");
assert!(s.contains("\"language\":\"rust\""), "language: {s}");
assert!(s.contains("\"manifestVersion\":1"), "mv: {s}");
assert!(s.ends_with('\n'));
}
#[test]
fn mode_parsing() {
assert_eq!(
VersionMode::from_args(["prog"].iter().copied()),
VersionMode::NotRequested
);
assert_eq!(
VersionMode::from_args(["prog", "--version"].iter().copied()),
VersionMode::Plain
);
assert_eq!(
VersionMode::from_args(["prog", "--version", "--json"].iter().copied()),
VersionMode::Json
);
assert_eq!(
VersionMode::from_args(["prog", "--json", "--version"].iter().copied()),
VersionMode::Json
);
}
}