torvyn_cli/commands/
publish.rs1use crate::cli::PublishArgs;
4use crate::errors::CliError;
5use crate::output::terminal;
6use crate::output::{CommandResult, HumanRenderable, OutputContext};
7use serde::Serialize;
8
9#[derive(Debug, Serialize)]
11pub struct PublishResult {
12 pub registry: String,
14 pub reference: String,
16 pub digest: String,
18 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
35pub async fn execute(
39 args: &PublishArgs,
40 ctx: &OutputContext,
41) -> Result<CommandResult<PublishResult>, CliError> {
42 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 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 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 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(®istry_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 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
162fn 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}