Skip to main content

spec_driven_docs/commands/
payload.rs

1//! `payload` subcommand: runtime-shape.
2//!
3//! The diagnostic form of the release seam, and the first proof that the
4//! seam answers for a release this binary does not carry. It reads and
5//! never writes into a target: the only thing it can leave behind is a
6//! verified archive in the cache.
7
8use crate::cli::payload::PayloadArgs;
9use crate::context::AppContext;
10use crate::domain::paths::UserEnv;
11use crate::error::AppError;
12use crate::output;
13use crate::release::crates_io::CratesIoResolver;
14use crate::release::embedded::EmbeddedReleaseBundle;
15use crate::release::{Provenance, ReleaseBundle, ReleaseManifest, ReleaseResolver, Role, Selector};
16
17/// Report what one release carries.
18///
19/// # Errors
20///
21/// [`AppError::Usage`] for a version that is not semantic and for
22/// `--offline` with no cache root, and the resolver's refusals.
23pub fn run(_ctx: &AppContext, args: &PayloadArgs) -> Result<(), AppError> {
24    let manifest = match selector(args.release.as_deref())? {
25        Selector::Embedded => EmbeddedReleaseBundle::new().manifest()?,
26        chosen => {
27            let cache = UserEnv::from_process()
28                .user_paths()
29                .ok_or_else(|| AppError::Usage("no cache root resolves".to_string()))?
30                .bundle_cache
31                .path;
32            let resolver = CratesIoResolver::new(&cache).offline(args.offline);
33            resolver.resolve(&chosen)?.bundle.manifest()?
34        }
35    };
36    if args.json {
37        return output::json(&manifest);
38    }
39    render(&manifest);
40    Ok(())
41}
42
43/// What the caller asked for.
44fn selector(release: Option<&str>) -> Result<Selector, AppError> {
45    match release {
46        None | Some("embedded") => Ok(Selector::Embedded),
47        Some("latest") => Ok(Selector::Latest),
48        Some(version) => version.parse().map(Selector::Exact).map_err(|_| {
49            AppError::Usage(format!(
50                "--release takes embedded, latest, or a semantic version; {version} is none of those"
51            ))
52        }),
53    }
54}
55
56fn render(manifest: &ReleaseManifest) {
57    let provenance = match manifest.provenance {
58        Provenance::Native => "native".to_string(),
59        Provenance::LegacyAdapted => manifest.descriptor_sha256.as_ref().map_or_else(
60            || "legacy-adapted".to_string(),
61            |digest| format!("legacy-adapted from descriptor {digest}"),
62        ),
63    };
64    output::line(format!("spec-driven-docs {}", manifest.version));
65    output::line(format!("payload schema: {}", manifest.payload_schema));
66    output::line(format!("provenance: {provenance}"));
67    output::line(format!("payload digest: {}", manifest.payload_sha256));
68    let payload = manifest
69        .artifacts
70        .iter()
71        .filter(|artifact| artifact.role == Role::Payload)
72        .count();
73    let metadata = manifest.artifacts.len() - payload;
74    output::line(format!("artifacts: {payload} payload, {metadata} metadata"));
75}