Skip to main content

torvyn_cli/commands/
publish.rs

1//! `torvyn publish` — publish to a registry.
2
3use crate::cli::PublishArgs;
4use crate::errors::CliError;
5use crate::output::terminal;
6use crate::output::{CommandResult, HumanRenderable, OutputContext};
7use serde::Serialize;
8
9/// Result of `torvyn publish`.
10#[derive(Debug, Serialize)]
11pub struct PublishResult {
12    /// Registry URL or local path.
13    pub registry: String,
14    /// Full artifact reference (registry/name:tag).
15    pub reference: String,
16    /// Content digest (sha256).
17    pub digest: String,
18    /// Whether this was a dry run.
19    pub dry_run: bool,
20}
21
22impl HumanRenderable for PublishResult {
23    fn render_human(&self, ctx: &OutputContext) {
24        if self.dry_run {
25            terminal::print_success(ctx, "Dry run: publish would succeed");
26            terminal::print_kv(ctx, "Registry", &self.registry);
27            terminal::print_kv(ctx, "Reference", &self.reference);
28        } else {
29            terminal::print_success(ctx, &format!("Published: {}", self.reference));
30            terminal::print_kv(ctx, "Digest", &self.digest);
31        }
32    }
33}
34
35/// Execute the `torvyn publish` command.
36///
37/// COLD PATH.
38pub async fn execute(
39    args: &PublishArgs,
40    ctx: &OutputContext,
41) -> Result<CommandResult<PublishResult>, CliError> {
42    // Determine artifact path
43    let artifact_path = match &args.artifact {
44        Some(path) => {
45            if !path.exists() {
46                return Err(CliError::Packaging {
47                    detail: format!("Artifact not found: {}", path.display()),
48                    suggestion: "Run `torvyn pack` first.".into(),
49                });
50            }
51            path.clone()
52        }
53        None => {
54            // Find latest artifact in .torvyn/artifacts/
55            let artifacts_dir = std::path::PathBuf::from(".torvyn/artifacts");
56            if !artifacts_dir.exists() {
57                return Err(CliError::Packaging {
58                    detail: "No artifacts found. Run `torvyn pack` first.".into(),
59                    suggestion: "Run `torvyn pack` to create an artifact, then `torvyn publish`."
60                        .into(),
61                });
62            }
63
64            find_latest_artifact(&artifacts_dir).ok_or_else(|| CliError::Packaging {
65                detail: "No artifact files found in .torvyn/artifacts/".into(),
66                suggestion: "Run `torvyn pack` first.".into(),
67            })?
68        }
69    };
70
71    let registry = args
72        .registry
73        .clone()
74        .unwrap_or_else(|| "local:.torvyn/registry".into());
75
76    let spinner = ctx.spinner(&format!("Publishing to {registry}..."));
77
78    // For Phase 0: local directory "registry" only
79    // IMPLEMENTATION SPIKE REQUIRED: OCI push API
80    let is_local = registry.starts_with("local:");
81
82    if let Some(sp) = &spinner {
83        sp.finish_and_clear();
84    }
85
86    if args.dry_run {
87        let result = PublishResult {
88            registry: registry.clone(),
89            reference: format!("{registry}/artifact:latest"),
90            digest: "sha256:dry-run".into(),
91            dry_run: true,
92        };
93
94        return Ok(CommandResult {
95            success: true,
96            command: "publish".into(),
97            data: result,
98            warnings: vec![],
99        });
100    }
101
102    // Local publish: copy artifact to registry directory
103    if is_local {
104        let local_dir = registry
105            .strip_prefix("local:")
106            .unwrap_or(".torvyn/registry");
107        let registry_dir = std::path::PathBuf::from(local_dir);
108        std::fs::create_dir_all(&registry_dir).map_err(|e| CliError::Io {
109            detail: format!("Cannot create local registry directory: {e}"),
110            path: Some(registry_dir.display().to_string()),
111        })?;
112
113        let dest = registry_dir.join(
114            artifact_path
115                .file_name()
116                .unwrap_or(std::ffi::OsStr::new("artifact.tar")),
117        );
118        std::fs::copy(&artifact_path, &dest).map_err(|e| CliError::Io {
119            detail: format!("Failed to copy artifact to local registry: {e}"),
120            path: Some(dest.display().to_string()),
121        })?;
122
123        let digest = format!("sha256:{:x}", {
124            use std::collections::hash_map::DefaultHasher;
125            use std::hash::{Hash, Hasher};
126            let mut hasher = DefaultHasher::new();
127            artifact_path.hash(&mut hasher);
128            hasher.finish()
129        });
130
131        let result = PublishResult {
132            registry: registry.clone(),
133            reference: format!("{}/{}", registry, dest.display()),
134            digest,
135            dry_run: false,
136        };
137
138        return Ok(CommandResult {
139            success: true,
140            command: "publish".into(),
141            data: result,
142            warnings: vec![],
143        });
144    }
145
146    // Remote publish placeholder
147    let result = PublishResult {
148        registry: registry.clone(),
149        reference: format!("{registry}/artifact:latest"),
150        digest: "sha256:placeholder".into(),
151        dry_run: false,
152    };
153
154    Ok(CommandResult {
155        success: true,
156        command: "publish".into(),
157        data: result,
158        warnings: vec![],
159    })
160}
161
162/// Find the most recently modified artifact in a directory.
163fn find_latest_artifact(dir: &std::path::Path) -> Option<std::path::PathBuf> {
164    std::fs::read_dir(dir)
165        .ok()?
166        .filter_map(|e| e.ok())
167        .filter(|e| {
168            e.path()
169                .extension()
170                .map(|ext| ext == "tar")
171                .unwrap_or(false)
172        })
173        .max_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()))
174        .map(|e| e.path())
175}