Skip to main content

torvyn_cli/commands/
pack.rs

1//! `torvyn pack` — package as OCI artifact.
2
3use crate::cli::PackArgs;
4use crate::errors::CliError;
5use crate::output::terminal;
6use crate::output::{CommandResult, HumanRenderable, OutputContext};
7use serde::Serialize;
8use std::path::PathBuf;
9
10/// Result of `torvyn pack`.
11#[derive(Debug, Serialize)]
12pub struct PackResult {
13    /// Component or project name.
14    pub name: String,
15    /// Version string.
16    pub version: String,
17    /// Path to the created artifact file.
18    pub artifact_path: PathBuf,
19    /// Size of the artifact in bytes.
20    pub artifact_size_bytes: u64,
21    /// Layers in the packed artifact.
22    pub layers: Vec<PackLayer>,
23}
24
25/// A single layer in the packed artifact.
26#[derive(Debug, Serialize)]
27pub struct PackLayer {
28    /// Layer name (e.g., "component", "contracts", "metadata").
29    pub name: String,
30    /// Size of the layer in bytes.
31    pub size_bytes: u64,
32}
33
34impl HumanRenderable for PackResult {
35    fn render_human(&self, ctx: &OutputContext) {
36        terminal::print_success(ctx, &format!("Packed: {}:{}", self.name, self.version));
37        terminal::print_kv(ctx, "Artifact", &self.artifact_path.display().to_string());
38        terminal::print_kv(
39            ctx,
40            "Size",
41            &terminal::format_bytes(self.artifact_size_bytes),
42        );
43        if !self.layers.is_empty() {
44            eprintln!("  Layers:");
45            for layer in &self.layers {
46                eprintln!(
47                    "    - {} ({})",
48                    layer.name,
49                    terminal::format_bytes(layer.size_bytes)
50                );
51            }
52        }
53    }
54}
55
56/// Execute the `torvyn pack` command.
57///
58/// COLD PATH.
59pub async fn execute(
60    args: &PackArgs,
61    ctx: &OutputContext,
62) -> Result<CommandResult<PackResult>, CliError> {
63    let manifest_path = &args.manifest;
64
65    if !manifest_path.exists() {
66        return Err(CliError::Config {
67            detail: format!("Manifest not found: {}", manifest_path.display()),
68            file: Some(manifest_path.display().to_string()),
69            suggestion: "Run this command from a Torvyn project directory.".into(),
70        });
71    }
72
73    let spinner = ctx.spinner("Checking contracts...");
74
75    // Validate first
76    let manifest_content = std::fs::read_to_string(manifest_path).map_err(|e| CliError::Io {
77        detail: e.to_string(),
78        path: Some(manifest_path.display().to_string()),
79    })?;
80
81    let manifest = torvyn_config::ComponentManifest::from_toml_str(
82        &manifest_content,
83        manifest_path.to_str().unwrap_or("Torvyn.toml"),
84    )
85    .map_err(|errors| CliError::Config {
86        detail: format!("Manifest has {} error(s)", errors.len()),
87        file: Some(manifest_path.display().to_string()),
88        suggestion: "Run `torvyn check` first.".into(),
89    })?;
90
91    if let Some(sp) = &spinner {
92        sp.finish_and_clear();
93    }
94
95    let project_dir = manifest_path.parent().unwrap_or(std::path::Path::new("."));
96
97    let output_dir = args
98        .output
99        .clone()
100        .unwrap_or_else(|| project_dir.join(".torvyn").join("artifacts"));
101
102    std::fs::create_dir_all(&output_dir).map_err(|e| CliError::Io {
103        detail: format!("Cannot create output directory: {e}"),
104        path: Some(output_dir.display().to_string()),
105    })?;
106
107    let name = manifest.torvyn.name.clone();
108    let version = manifest.torvyn.version.clone();
109    let tag = args.tag.clone().unwrap_or_else(|| version.clone());
110
111    // Placeholder: write manifest as minimal artifact
112    // IMPLEMENTATION SPIKE REQUIRED: torvyn_packaging::assemble_artifact API
113    let artifact_filename = format!("{name}-{tag}.tar");
114    let artifact_path = output_dir.join(&artifact_filename);
115
116    let artifact_json = serde_json::json!({
117        "name": name,
118        "version": version,
119        "tag": tag,
120    });
121    std::fs::write(
122        &artifact_path,
123        serde_json::to_string_pretty(&artifact_json).unwrap(),
124    )
125    .map_err(|e| CliError::Io {
126        detail: format!("Failed to write artifact: {e}"),
127        path: Some(artifact_path.display().to_string()),
128    })?;
129
130    let artifact_size = std::fs::metadata(&artifact_path)
131        .map(|m| m.len())
132        .unwrap_or(0);
133
134    let result = PackResult {
135        name,
136        version,
137        artifact_path,
138        artifact_size_bytes: artifact_size,
139        layers: vec![],
140    };
141
142    Ok(CommandResult {
143        success: true,
144        command: "pack".into(),
145        data: result,
146        warnings: vec![],
147    })
148}