Skip to content

Commit 2379752

Browse files
authored
Merge pull request #33 from 100monkeys-ai/feat/delegate-foundry-client-publish-7078516248368222698
feat(cli): delegate forge publish to foundry-client
2 parents b1bf201 + 60ffbd5 commit 2379752

6 files changed

Lines changed: 118 additions & 20 deletions

File tree

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/commands/publish.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use anyhow::Result;
44
use clap::Args;
5+
use crate::config::CliConfig;
56

67
#[derive(Debug, Args)]
78
pub struct PublishArgs {
@@ -11,12 +12,17 @@ pub struct PublishArgs {
1112
}
1213

1314
pub async fn run(args: PublishArgs) -> Result<()> {
14-
use foundry_client::publish::{publish_package, PublishOptions};
15+
let config = CliConfig::load();
16+
let registry_url = config.registry_url.unwrap_or_else(|| "https://registry.forgejs.com".to_string());
1517

16-
publish_package(PublishOptions {
18+
let options = foundry_client::publish::PublishOptions {
19+
dir: camino::Utf8PathBuf::from("."),
1720
dry_run: args.dry_run,
18-
})
19-
.await?;
21+
registry_url,
22+
auth_token: config.auth_token,
23+
};
24+
25+
foundry_client::publish::publish_package(options).await?;
2026

2127
Ok(())
2228
}

foundry/client/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ thiserror = { workspace = true }
1616
anyhow = { workspace = true }
1717
tracing = { workspace = true }
1818
camino = { workspace = true }
19-
reqwest = { workspace = true }
19+
reqwest = { workspace = true, features = ["multipart"] }
2020
blake3 = { workspace = true }
2121
indexmap = { workspace = true }
2222
tokio = { workspace = true }
@@ -27,6 +27,7 @@ oxc_ast = { workspace = true }
2727
oxc_span = { workspace = true }
2828
oxc_allocator = { workspace = true }
2929
chrono = { workspace = true }
30+
walkdir = "2.5.0"
3031

3132
[dev-dependencies]
3233
serial_test = "3.4.0"

foundry/client/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ pub mod migrate;
4040
pub mod publish;
4141
pub mod registry_client;
4242
pub mod resolver;
43+
pub mod publish;

foundry/client/src/publish.rs

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,76 @@
1-
//! Publishing logic for the Foundry registry.
2-
31
use crate::error::FoundryError;
2+
use crate::manifest::foundry_toml::parse_foundry_toml;
3+
use crate::registry_client::RegistryClient;
4+
use flate2::write::GzEncoder;
5+
use flate2::Compression;
6+
use std::fs;
47
use tracing::info;
58

6-
/// Options for the publish command.
7-
#[derive(Debug, Clone)]
89
pub struct PublishOptions {
9-
/// Perform a dry run without actually publishing.
10+
pub dir: camino::Utf8PathBuf,
1011
pub dry_run: bool,
12+
pub registry_url: String,
13+
pub auth_token: Option<String>,
1114
}
1215

13-
/// Publish a package to the Foundry registry.
1416
pub async fn publish_package(options: PublishOptions) -> Result<(), FoundryError> {
15-
// TODO: Implementation of package publishing:
16-
// 1. Read foundry.toml
17-
// 2. Validate package
18-
// 3. Create tarball
19-
// 4. Sign package
20-
// 5. Upload to registry
17+
let manifest_path = options.dir.join("foundry.toml");
18+
let manifest = parse_foundry_toml(&manifest_path)?;
19+
20+
let name_parts: Vec<&str> = manifest.package.name.split('/').collect();
21+
if name_parts.len() != 2 {
22+
return Err(FoundryError::ManifestParse {
23+
path: manifest_path.to_string(),
24+
message: "package name must be in the format 'author/name'".to_string(),
25+
});
26+
}
27+
let author = name_parts[0];
28+
let name = name_parts[1];
29+
30+
info!("Publishing {}@{}...", manifest.package.name, manifest.package.version);
2131

2232
if options.dry_run {
23-
info!("Dry run: would publish package to registry");
24-
} else {
25-
info!("Publishing package to registry...");
33+
info!("Dry run complete. No package published.");
34+
return Ok(());
2635
}
2736

37+
let auth_token = options.auth_token.ok_or(FoundryError::AuthRequired)?;
38+
39+
let mut tar_gz = Vec::new();
40+
let enc = GzEncoder::new(&mut tar_gz, Compression::default());
41+
let mut tar = tar::Builder::new(enc);
42+
43+
// Pack the directory
44+
for entry in walkdir::WalkDir::new(&options.dir).into_iter().filter_map(|e: Result<walkdir::DirEntry, walkdir::Error>| e.ok()) {
45+
let path = entry.path();
46+
if path.is_file() {
47+
let relative_path = path.strip_prefix(&options.dir).unwrap();
48+
49+
let should_skip = relative_path.components().any(|c| {
50+
if let std::path::Component::Normal(os_str) = c {
51+
let s = os_str.to_string_lossy();
52+
s == ".git" || s == ".forge" || s == "node_modules"
53+
} else {
54+
false
55+
}
56+
});
57+
58+
if should_skip {
59+
continue;
60+
}
61+
62+
let mut file = fs::File::open(path)?;
63+
tar.append_file(relative_path, &mut file)?;
64+
}
65+
}
66+
let enc = tar.into_inner()?;
67+
enc.finish()?;
68+
69+
let manifest_content = fs::read_to_string(&manifest_path)?;
70+
71+
let client = RegistryClient::new(options.registry_url, Some(auth_token));
72+
client.publish(author, name, manifest_content, tar_gz).await?;
73+
74+
info!("Successfully published {}@{}", manifest.package.name, manifest.package.version);
2875
Ok(())
2976
}

foundry/client/src/registry_client.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,35 @@ impl RegistryClient {
107107

108108
Ok(bytes.to_vec())
109109
}
110+
111+
pub async fn publish(&self, author: &str, name: &str, manifest: String, tarball: Vec<u8>) -> Result<(), FoundryError> {
112+
let url = format!("{}/packages/{}/{}", self.base_url, author, name);
113+
114+
let token = self.auth_token.as_ref().ok_or(FoundryError::AuthRequired)?;
115+
116+
let manifest_part = reqwest::multipart::Part::text(manifest)
117+
.mime_str("text/plain;charset=utf-8")
118+
.map_err(|_| FoundryError::Registry("Invalid mime type for manifest".to_string()))?;
119+
120+
let tarball_part = reqwest::multipart::Part::bytes(tarball)
121+
.mime_str("application/x-tar")
122+
.map_err(|_| FoundryError::Registry("Invalid mime type for tarball".to_string()))?;
123+
124+
let form = reqwest::multipart::Form::new()
125+
.part("manifest", manifest_part)
126+
.part("tarball", tarball_part);
127+
128+
let response: reqwest::Response = self.http.post(&url)
129+
.bearer_auth(token)
130+
.multipart(form)
131+
.send()
132+
.await?;
133+
134+
if !response.status().is_success() {
135+
let error_text = response.text().await.unwrap_or_default();
136+
return Err(FoundryError::Registry(format!("Failed to publish package: {}", error_text)));
137+
}
138+
139+
Ok(())
140+
}
110141
}

0 commit comments

Comments
 (0)