Skip to main content

max / makenotwork

MNW synckit-client/bento/mnw-cli/pom: adopt lint block, fix clippy, fmt Adopt the shared clippy::pedantic block and reach green under 'cargo clippy --all-targets -- -D warnings': format!-push to write!, #[must_use] on builders, Option<&T> over &Option<T>, borrow unused-by-value params, name concrete Default types, epsilon float compares. Scoped #[allow]s (with reasons) only for serde-bound field names, rhai's boxed error type, and a serde skip_serializing_if signature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 15:01 UTC
Commit: 28eedb8ab7fda843637a96f3c8c75128095a9bf5
Parent: 1dce76f
151 files changed, +1803 insertions, -1181 deletions
@@ -37,3 +37,35 @@ tempfile = "3.20"
37 37 tower = { version = "0.5", features = ["util"] }
38 38 http-body-util = "0.1"
39 39 reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] }
40 +
41 + [lints.rust]
42 + unused = "warn"
43 + unreachable_pub = "warn"
44 +
45 + [lints.clippy]
46 + pedantic = { level = "warn", priority = -1 }
47 + # Allow-list tuned from a measured breakdown across server/multithreaded/pter
48 + # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything
49 + # else in `pedantic` stays a warning. Keep this block identical across repos.
50 + module_name_repetitions = "allow"
51 + # Doc lints. No docs-completeness push is underway.
52 + missing_errors_doc = "allow"
53 + missing_panics_doc = "allow"
54 + doc_markdown = "allow"
55 + # Numeric casts. Endemic and mostly intentional in size and byte math.
56 + cast_possible_truncation = "allow"
57 + cast_sign_loss = "allow"
58 + cast_precision_loss = "allow"
59 + cast_possible_wrap = "allow"
60 + cast_lossless = "allow"
61 + # Subjective structure and style nags. High churn, low signal.
62 + must_use_candidate = "allow"
63 + too_many_lines = "allow"
64 + struct_excessive_bools = "allow"
65 + similar_names = "allow"
66 + items_after_statements = "allow"
67 + single_match_else = "allow"
68 + # Frequent false-positives in TUI and router-heavy code.
69 + match_same_arms = "allow"
70 + unnecessary_wraps = "allow"
71 + type_complexity = "allow"
@@ -156,7 +156,7 @@ impl RecipeCtx {
156 156 /// empty list: preflight then cannot claim a version is a duplicate, and
157 157 /// `cargo publish` still refuses one, so the check degrades to advisory
158 158 /// rather than blocking a release on registry availability.
159 - fn published_versions(&self, name: &str) -> Vec<String> {
159 + fn published_versions(name: &str) -> Vec<String> {
160 160 let url = format!("https://crates.io/api/v1/crates/{name}");
161 161 let Ok(out) = std::process::Command::new("curl")
162 162 .args([
@@ -328,20 +328,17 @@ impl RecipeCtx {
328 328 fn step_budget(&self, step: Step) -> std::time::Duration {
329 329 self.cfg
330 330 .step_timeout_secs
331 - .map(std::time::Duration::from_secs)
332 - .unwrap_or_else(|| default_step_budget(step))
331 + .map_or_else(|| default_step_budget(step), std::time::Duration::from_secs)
333 332 }
334 333
335 334 /// The current step's wall-clock deadline (or a default if no step is open,
336 335 /// which only happens before the first `step()` โ€” commands then run under an
337 336 /// implicit `Build` step opened by `ensure_step`).
338 337 fn step_deadline(&self) -> std::time::Instant {
339 - self.current
340 - .lock()
341 - .unwrap()
342 - .as_ref()
343 - .map(|s| s.deadline)
344 - .unwrap_or_else(|| std::time::Instant::now() + self.step_budget(Step::Build))
338 + self.current.lock().unwrap().as_ref().map_or_else(
339 + || std::time::Instant::now() + self.step_budget(Step::Build),
340 + |s| s.deadline,
341 + )
345 342 }
346 343
347 344 /// Drive `fut` on the runtime, but stop early on two conditions the recipe
@@ -367,10 +364,10 @@ impl RecipeCtx {
367 364 };
368 365 tokio::select! {
369 366 r = &mut fut => r,
370 - _ = tokio::time::sleep_until(deadline.into()) => {
367 + () = tokio::time::sleep_until(deadline.into()) => {
371 368 Err(anyhow::anyhow!("`{what}` exceeded its per-step deadline"))
372 369 }
373 - _ = watch => {
370 + () = watch => {
374 371 Err(anyhow::anyhow!("build superseded by a newer request; aborting `{what}`"))
375 372 }
376 373 }
@@ -444,8 +441,7 @@ impl RecipeCtx {
444 441 .lock()
445 442 .unwrap()
446 443 .as_ref()
447 - .map(|s| s.step)
448 - .unwrap_or(Step::Build)
444 + .map_or(Step::Build, |s| s.step)
449 445 }
450 446
451 447 /// The capability-scoped executor for `name`, or an error if the host isn't
@@ -541,6 +537,12 @@ impl RecipeCtx {
541 537
542 538 // ----- error bridging: anyhow -> Rhai runtime error -----
543 539
540 + // Rhai host functions return `Result<_, Box<EvalAltResult>>` by convention, so
541 + // this bridge must yield the boxed form to be usable with `.map_err(rhai_err)`.
542 + #[allow(
543 + clippy::unnecessary_box_returns,
544 + reason = "rhai's error type is used boxed throughout its host-function API"
545 + )]
544 546 fn rhai_err(e: impl std::fmt::Display) -> Box<EvalAltResult> {
545 547 Box::new(EvalAltResult::ErrorRuntime(
546 548 e.to_string().into(),
@@ -638,7 +640,10 @@ pub fn version_from_repo(repo: &str, version_path: Option<&str>) -> Result<Versi
638 640 let path = root.join(vp);
639 641 let raw = std::fs::read_to_string(&path)
640 642 .with_context(|| format!("reading version file {}", path.display()))?;
641 - let ver = if vp.ends_with(".json") {
643 + let ver = if std::path::Path::new(vp)
644 + .extension()
645 + .is_some_and(|e| e.eq_ignore_ascii_case("json"))
646 + {
642 647 version_from_tauri_json(&raw)?
643 648 } else {
644 649 version_from_cargo_toml(&raw)?
@@ -724,7 +729,10 @@ pub fn check_version_consistency(
724 729 let path = root.join(vp);
725 730 let raw = std::fs::read_to_string(&path)
726 731 .with_context(|| format!("reading version file {}", path.display()))?;
727 - consider(vp, &raw, vp.ends_with(".json"))?;
732 + let is_json = std::path::Path::new(vp)
733 + .extension()
734 + .is_some_and(|e| e.eq_ignore_ascii_case("json"));
735 + consider(vp, &raw, is_json)?;
728 736 }
729 737 let tauri_conf = root.join("src-tauri").join("tauri.conf.json");
730 738 if version_path != Some("src-tauri/tauri.conf.json") && tauri_conf.exists() {
@@ -893,7 +901,7 @@ fn resolve_artifact_match(listing: &str, glob: &str, required: bool) -> Result<S
893 901
894 902 /// Build a Rhai engine with the host API bound to `ctx`. Sandboxed: recipes
895 903 /// touch the outside world only through these functions.
896 - pub fn build_engine(ctx: Arc<RecipeCtx>) -> Engine {
904 + pub fn build_engine(ctx: &Arc<RecipeCtx>) -> Engine {
897 905 let mut engine = Engine::new();
898 906 // Defensive caps โ€” recipes are first-party but bound the blast radius.
899 907 engine.set_max_operations(5_000_000);
@@ -1088,8 +1096,7 @@ pub fn build_engine(ctx: Arc<RecipeCtx>) -> Engine {
1088 1096 .args(["ls-remote", url])
1089 1097 .env("GIT_TERMINAL_PROMPT", "0")
1090 1098 .output()
1091 - .map(|o| o.status.success())
1092 - .unwrap_or(false)
1099 + .is_ok_and(|o| o.status.success())
1093 1100 });
1094 1101
1095 1102 // Ask the publishing host whether cargo has credentials, rather
@@ -1102,10 +1109,9 @@ pub fn build_engine(ctx: Arc<RecipeCtx>) -> Engine {
1102 1109 test -s \"${CARGO_HOME:-$HOME/.cargo}/credentials.toml\" \
1103 1110 || test -s \"${CARGO_HOME:-$HOME/.cargo}/credentials\"",
1104 1111 )
1105 - .map(|(code, _)| code == 0)
1106 - .unwrap_or(false);
1112 + .is_ok_and(|(code, _)| code == 0);
1107 1113
1108 - let published = ctx.published_versions(&meta.name);
1114 + let published = RecipeCtx::published_versions(&meta.name);
1109 1115 let problems = crate_publish_problems(&meta, clonable, &published, creds);
1110 1116 if !problems.is_empty() {
1111 1117 return Err(format!(
@@ -1242,7 +1248,7 @@ pub fn build_engine(ctx: Arc<RecipeCtx>) -> Engine {
1242 1248 artifact: &str,
1243 1249 meta: Map|
1244 1250 -> Result<String, Box<EvalAltResult>> {
1245 - ctx.publish(channel, app, target, version, artifact, meta)
1251 + ctx.publish(channel, app, target, version, artifact, &meta)
1246 1252 .map_err(rhai_err)
1247 1253 },
1248 1254 );
@@ -1254,7 +1260,7 @@ pub fn build_engine(ctx: Arc<RecipeCtx>) -> Engine {
1254 1260 // transport โ€” the only security session where the Developer ID key is
1255 1261 // usable (design ยง7 "THE WALL"). Capability-gated by the host's `sign`
1256 1262 // grant. ---
1257 - register_macos_fns(&mut engine, &ctx);
1263 + register_macos_fns(&mut engine, ctx);
1258 1264
1259 1265 engine
1260 1266 }
@@ -1292,7 +1298,7 @@ impl RecipeCtx {
1292 1298 let entry = entry?;
1293 1299 let name = entry.file_name().to_string_lossy().into_owned();
1294 1300 assert_artifact_version(&name, &self.version)?;
1295 - if entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
1301 + if entry.file_type().is_ok_and(|t| t.is_file()) {
1296 1302 let digest = sha256_file(&entry.path())?;
1297 1303 self.artifact_hashes.lock().unwrap().insert(name, digest);
1298 1304 }
@@ -1359,7 +1365,7 @@ impl RecipeCtx {
1359 1365 target: &str,
1360 1366 version: &str,
1361 1367 artifact: &str,
1362 - meta: Map,
1368 + meta: &Map,
1363 1369 ) -> Result<String> {
1364 1370 // Never let a superseded build ship. This is the last and most important
1365 1371 // cooperative-cancel checkpoint: even if a long-running step finished
@@ -1773,7 +1779,7 @@ mod tests {
1773 1779 Arc::new(AtomicBool::new(false)),
1774 1780 None,
1775 1781 ));
1776 - let engine = build_engine(ctx);
1782 + let engine = build_engine(&ctx);
1777 1783 engine.eval::<String>("feature_flags()").unwrap()
1778 1784 }
1779 1785
@@ -2140,7 +2146,7 @@ mod tests {
2140 2146 // whole matrix so a new Step variant can't silently get a 0 budget.
2141 2147 for step in Step::ALL {
2142 2148 assert!(
2143 - default_step_budget(step) >= std::time::Duration::from_secs(60),
2149 + default_step_budget(step) >= std::time::Duration::from_mins(1),
2144 2150 "{step} budget must be a sane ceiling",
2145 2151 );
2146 2152 }
@@ -24,7 +24,7 @@ pub fn channel() -> EventTx {
24 24 /// Send an event without caring whether anyone is listening. `StepLogChunk` goes
25 25 /// to the high-rate log channel, everything else to the status channel.
26 26 pub fn emit(tx: &EventTx, event: Event) {
27 - eventbus::emit(tx, event)
27 + eventbus::emit(tx, event);
28 28 }
29 29
30 30 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -78,7 +78,7 @@ async fn main() -> Result<()> {
78 78 match &api_token {
79 79 Some(_) => tracing::info!("build endpoints require a bearer token"),
80 80 None => {
81 - tracing::warn!(%addr, "BENTO_API_TOKEN unset; build endpoints are UNAUTHENTICATED (loopback bind)")
81 + tracing::warn!(%addr, "BENTO_API_TOKEN unset; build endpoints are UNAUTHENTICATED (loopback bind)");
82 82 }
83 83 }
84 84
@@ -105,7 +105,7 @@ async fn main() -> Result<()> {
105 105 let cfg = app_state.cfg.clone();
106 106 let pool = app_state.pool.clone();
107 107 tokio::spawn(async move {
108 - let mut tick = tokio::time::interval(std::time::Duration::from_secs(6 * 60 * 60));
108 + let mut tick = tokio::time::interval(std::time::Duration::from_hours(6));
109 109 loop {
110 110 tick.tick().await; // fires immediately, then every 6h
111 111 let active: bento_daemon::retention::ActiveVersions =
@@ -119,7 +119,7 @@ async fn main() -> Result<()> {
119 119 .collect();
120 120 let cfg = cfg.clone();
121 121 let _ = tokio::task::spawn_blocking(move || {
122 - bento_daemon::retention::prune_once(&cfg, &active)
122 + bento_daemon::retention::prune_once(&cfg, &active);
123 123 })
124 124 .await;
125 125 }
@@ -122,7 +122,7 @@ impl OtaRegistry {
122 122 }
123 123
124 124 pub fn get(&self, channel: &str) -> Option<&dyn OtaBackend> {
125 - self.backends.get(channel).map(|b| b.as_ref())
125 + self.backends.get(channel).map(std::convert::AsRef::as_ref)
126 126 }
127 127
128 128 /// The standard registry: `tauri-mnw` wired to the MNW OTA endpoint.
@@ -145,12 +145,12 @@ pub struct TauriMnwBackend {
145 145 }
146 146
147 147 impl OtaBackend for TauriMnwBackend {
148 - fn id(&self) -> &str {
148 + fn id(&self) -> &'static str {
149 149 "tauri-mnw"
150 150 }
151 151
152 152 fn supports(&self, target: Target) -> bool {
153 - use crate::domain::Platform::*;
153 + use crate::domain::Platform::{Linux, Macos, Windows};
154 154 // Tauri's updater covers the desktop trio; mobile rides testflight/play.
155 155 matches!(target.platform, Macos | Linux | Windows)
156 156 }
@@ -34,7 +34,7 @@ fn prune_root(root: &Path, label: &str, active: &ActiveVersions) {
34 34 let Ok(apps) = std::fs::read_dir(root) else {
35 35 return;
36 36 };
37 - for app in apps.filter_map(|e| e.ok()) {
37 + for app in apps.filter_map(std::result::Result::ok) {
38 38 let app_dir = app.path();
39 39 if app_dir.is_dir() {
40 40 let app_name = app.file_name().to_string_lossy().into_owned();
@@ -50,7 +50,7 @@ fn prune_app_dir(app_dir: &Path, app_name: &str, label: &str, active: &ActiveVer
50 50 // Only consider subdirectories whose name parses as a semver โ€” anything
51 51 // else (a stray file, a non-version dir) is left untouched.
52 52 let mut versions: Vec<(Version, String, PathBuf)> = entries
53 - .filter_map(|e| e.ok())
53 + .filter_map(std::result::Result::ok)
54 54 .filter(|e| e.path().is_dir())
55 55 .filter_map(|e| {
56 56 let name = e.file_name().to_string_lossy().into_owned();
@@ -70,10 +70,10 @@ fn prune_app_dir(app_dir: &Path, app_name: &str, label: &str, active: &ActiveVer
70 70 }
71 71 match std::fs::remove_dir_all(&path) {
72 72 Ok(()) => {
73 - tracing::info!(%ver, dir = %path.display(), "retention: pruned old {label} version")
73 + tracing::info!(%ver, dir = %path.display(), "retention: pruned old {label} version");
74 74 }
75 75 Err(e) => {
76 - tracing::warn!(error = %e, dir = %path.display(), "retention: could not prune {label} dir")
76 + tracing::warn!(error = %e, dir = %path.display(), "retention: could not prune {label} dir");
77 77 }
78 78 }
79 79 }
@@ -106,7 +106,7 @@ mod tests {
106 106
107 107 let mut kept: Vec<String> = std::fs::read_dir(root.join("demo"))
108 108 .unwrap()
109 - .filter_map(|e| e.ok())
109 + .filter_map(std::result::Result::ok)
110 110 .map(|e| e.file_name().to_string_lossy().into_owned())
111 111 .collect();
112 112 kept.sort();
@@ -413,18 +413,18 @@ async fn run_target(
413 413 // runtime worker.
414 414 let ctx_run = ctx.clone();
415 415 let outcome = tokio::task::spawn_blocking(move || {
416 - let engine = engine::build_engine(ctx_run.clone());
416 + let engine = engine::build_engine(&ctx_run);
417 417 let res = engine.run(&recipe_src);
418 418 let last_step = ctx_run.current_step();
419 419 match &res {
420 - Ok(_) => {
420 + Ok(()) => {
421 421 let _ = ctx_run.finish_step(Status::Ok);
422 422 }
423 423 Err(_) => {
424 424 let _ = ctx_run.finish_step(Status::Failed);
425 425 }
426 426 }
427 - res.map(|_| ()).map_err(|e| (last_step, e.to_string()))
427 + res.map_err(|e| (last_step, e.to_string()))
428 428 })
429 429 .await;
430 430
@@ -544,11 +544,11 @@ async fn fail_target(
544 544 /// reach the blocking bodies, so a build was marked failed while codesign kept
545 545 /// running on the mac.
546 546 async fn finalize_build(state: AppState, build_id: i64, mut set: tokio::task::JoinSet<()>) {
547 - const MAX_WAIT: std::time::Duration = std::time::Duration::from_secs(6 * 60 * 60);
547 + const MAX_WAIT: std::time::Duration = std::time::Duration::from_hours(6);
548 548 let deadline = tokio::time::Instant::now() + MAX_WAIT;
549 549 loop {
550 550 match tokio::time::timeout_at(deadline, set.join_next()).await {
551 - Ok(Some(Ok(()))) => continue,
551 + Ok(Some(Ok(()))) => {}
552 552 Ok(Some(Err(e))) => {
553 553 // A panic in run_target itself (outside its inner spawn_blocking,
554 554 // which is already caught). Cancellation = a superseding build.
@@ -652,7 +652,7 @@ fn collected_artifacts(state: &AppState, app: &AppId, version: &Version) -> Vec<
652 652 let Ok(rd) = std::fs::read_dir(&dir) else {
653 653 return Vec::new();
654 654 };
655 - rd.filter_map(|e| e.ok())
655 + rd.filter_map(std::result::Result::ok)
656 656 .map(|e| e.file_name().to_string_lossy().into_owned())
657 657 .collect()
658 658 }
@@ -166,7 +166,7 @@ pub fn build_syncs(topo: &Topology) -> ExecutorMap {
166 166 #[cfg(test)]
167 167 mod tests {
168 168 use super::*;
169 - use ops_exec::Action;
169 + use ops_exec::{Action, SyncOpts};
170 170
171 171 fn host(toml_host: &str) -> Host {
172 172 // Only a host is under test here, so parse just the `[[host]]` table.
@@ -213,7 +213,7 @@ mod tests {
213 213 .pull_glob(
214 214 "/nonexistent/*.dmg",
215 215 std::path::Path::new("/tmp"),
216 - &Default::default(),
216 + &SyncOpts::default(),
217 217 )
218 218 .await
219 219 .expect_err("nothing matches, so this must error either way");
@@ -231,7 +231,7 @@ mod tests {
231 231 .pull_glob(
232 232 "/x/*.dmg",
233 233 std::path::Path::new("/tmp"),
234 - &Default::default(),
234 + &SyncOpts::default(),
235 235 )
236 236 .await
237 237 .expect_err("AgentRpc has no glob form");
@@ -264,7 +264,7 @@ mod tests {
264 264 .pull_file(
265 265 std::path::Path::new("/home/max/.tauri/passwords.env"),
266 266 std::path::Path::new("/tmp/out"),
267 - &Default::default(),
267 + &SyncOpts::default(),
268 268 )
269 269 .await
270 270 .expect_err("a path outside pull_root must be denied");
@@ -280,7 +280,7 @@ mod tests {
280 280 .pull_file(
281 281 std::path::Path::new("/home/max/Code/Apps/goingson/x.dmg"),
282 282 std::path::Path::new("/tmp/out"),
283 - &Default::default(),
283 + &SyncOpts::default(),
284 284 )
285 285 .await
286 286 .expect_err("no pull_root โ‡’ no pulls");
@@ -31,6 +31,8 @@
31 31 //! naming what is missing, rather than the `ok` that a build-level status alone
32 32 //! would imply.
33 33
34 + use std::fmt::Write as _;
35 +
34 36 use chrono::{DateTime, Utc};
35 37 use ops_status::{Action, Condition, Field, Method, Node, Payload, Status, Value};
36 38 use serde_json::json;
@@ -183,10 +185,10 @@ fn release_completeness(app: &AppStatusView, build: &BuildView) -> Option<Condit
183 185 app.declared_targets.len()
184 186 );
185 187 if !failed.is_empty() {
186 - detail.push_str(&format!("; failed: {}", failed.join(", ")));
188 + write!(detail, "; failed: {}", failed.join(", ")).unwrap();
187 189 }
188 190 if !missing.is_empty() {
189 - detail.push_str(&format!("; never ran: {}", missing.join(", ")));
191 + write!(detail, "; never ran: {}", missing.join(", ")).unwrap();
190 192 }
191 193
192 194 Some(Condition {
@@ -35,6 +35,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
35 35 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
36 36
37 37 [[package]]
38 + name = "aws-lc-rs"
39 + version = "1.17.3"
40 + source = "registry+https://github.com/rust-lang/crates.io-index"
41 + checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
42 + dependencies = [
43 + "aws-lc-sys",
44 + "zeroize",
45 + ]
46 +
47 + [[package]]
48 + name = "aws-lc-sys"
49 + version = "0.43.0"
50 + source = "registry+https://github.com/rust-lang/crates.io-index"
51 + checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c"
52 + dependencies = [
53 + "cc",
54 + "cmake",
55 + "dunce",
56 + "fs_extra",
57 + "pkg-config",
58 + ]
59 +
60 + [[package]]
38 61 name = "base64"
39 62 version = "0.22.1"
40 63 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -80,6 +103,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
80 103 checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
81 104 dependencies = [
82 105 "find-msvc-tools",
106 + "jobserver",
107 + "libc",
83 108 "shlex",
84 109 ]
85 110
@@ -96,6 +121,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
96 121 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
97 122
98 123 [[package]]
124 + name = "cmake"
125 + version = "0.1.58"
126 + source = "registry+https://github.com/rust-lang/crates.io-index"
127 + checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
128 + dependencies = [
129 + "cc",
130 + ]
131 +
132 + [[package]]
133 + name = "combine"
134 + version = "4.6.7"
135 + source = "registry+https://github.com/rust-lang/crates.io-index"
136 + checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
137 + dependencies = [
138 + "bytes",
139 + "memchr",
140 + ]
141 +
142 + [[package]]
143 + name = "core-foundation"
144 + version = "0.10.1"
145 + source = "registry+https://github.com/rust-lang/crates.io-index"
146 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
147 + dependencies = [
148 + "core-foundation-sys",
149 + "libc",
150 + ]
151 +
152 + [[package]]
153 + name = "core-foundation-sys"
154 + version = "0.8.7"
155 + source = "registry+https://github.com/rust-lang/crates.io-index"
156 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
157 +
158 + [[package]]
99 159 name = "displaydoc"
100 160 version = "0.2.6"
101 161 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -107,6 +167,12 @@ dependencies = [
107 167 ]
108 168
109 169 [[package]]
170 + name = "dunce"
171 + version = "1.0.5"
172 + source = "registry+https://github.com/rust-lang/crates.io-index"
173 + checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
174 +
175 + [[package]]
110 176 name = "equivalent"
111 177 version = "1.0.2"
112 178 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -150,6 +216,12 @@ dependencies = [
150 216 ]
151 217
152 218 [[package]]
219 + name = "fs_extra"
220 + version = "1.3.0"
221 + source = "registry+https://github.com/rust-lang/crates.io-index"
222 + checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
223 +
224 + [[package]]
153 225 name = "futures-channel"
154 226 version = "0.3.32"
155 227 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -348,7 +420,6 @@ dependencies = [
348 420 "tokio",
349 421 "tokio-rustls",
350 422 "tower-service",
351 - "webpki-roots",
352 423 ]
353 424
354 425 [[package]]
@@ -508,6 +579,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
508 579 checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
509 580
510 581 [[package]]
582 + name = "jni"
583 + version = "0.22.4"
584 + source = "registry+https://github.com/rust-lang/crates.io-index"
585 + checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
586 + dependencies = [
587 + "cfg-if",
588 + "combine",
589 + "jni-macros",
590 + "jni-sys",
591 + "log",
592 + "simd_cesu8",
593 + "thiserror",
594 + "walkdir",
595 + "windows-link",
596 + ]
597 +
598 + [[package]]
599 + name = "jni-macros"
600 + version = "0.22.4"
601 + source = "registry+https://github.com/rust-lang/crates.io-index"
602 + checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
603 + dependencies = [
604 + "proc-macro2",
605 + "quote",
606 + "rustc_version",
607 + "simd_cesu8",
608 + "syn",
609 + ]
610 +
611 + [[package]]
612 + name = "jni-sys"
613 + version = "0.4.1"
614 + source = "registry+https://github.com/rust-lang/crates.io-index"
615 + checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
616 + dependencies = [
617 + "jni-sys-macros",
618 + ]
619 +
620 + [[package]]
621 + name = "jni-sys-macros"
622 + version = "0.4.1"
623 + source = "registry+https://github.com/rust-lang/crates.io-index"
624 + checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
625 + dependencies = [
626 + "quote",
627 + "syn",
628 + ]
629 +
630 + [[package]]
631 + name = "jobserver"
632 + version = "0.1.35"
633 + source = "registry+https://github.com/rust-lang/crates.io-index"
634 + checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
635 + dependencies = [
636 + "getrandom 0.4.2",
637 + "libc",
638 + ]
639 +
640 + [[package]]
511 641 name = "js-sys"
512 642 version = "0.3.99"
513 643 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -603,6 +733,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
603 733 checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
604 734
605 735 [[package]]
736 + name = "openssl-probe"
737 + version = "0.2.1"
738 + source = "registry+https://github.com/rust-lang/crates.io-index"
739 + checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
740 +
741 + [[package]]
606 742 name = "ops-exec"
607 743 version = "0.1.0"
608 744 dependencies = [
@@ -631,6 +767,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
631 767 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
632 768
633 769 [[package]]
770 + name = "pkg-config"
771 + version = "0.3.33"
772 + source = "registry+https://github.com/rust-lang/crates.io-index"
773 + checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
774 +
775 + [[package]]
634 776 name = "potential_utf"
635 777 version = "0.1.5"
636 778 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -693,6 +835,7 @@ version = "0.11.14"
693 835 source = "registry+https://github.com/rust-lang/crates.io-index"
694 836 checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
695 837 dependencies = [
838 + "aws-lc-rs",
696 839 "bytes",
697 840 "getrandom 0.3.4",
698 841 "lru-slab",
@@ -791,9 +934,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
791 934
792 935 [[package]]
793 936 name = "reqwest"
794 - version = "0.12.28"
937 + version = "0.13.4"
795 938 source = "registry+https://github.com/rust-lang/crates.io-index"
796 - checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
939 + checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
797 940 dependencies = [
798 941 "base64",
799 942 "bytes",
@@ -812,6 +955,7 @@ dependencies = [
812 955 "quinn",
813 956 "rustls",
814 957 "rustls-pki-types",
958 + "rustls-platform-verifier",
815 959 "serde",
816 960 "serde_json",
817 961 "serde_urlencoded",
@@ -827,7 +971,6 @@ dependencies = [
827 971 "wasm-bindgen-futures",
828 972 "wasm-streams",
829 973 "web-sys",
830 - "webpki-roots",
831 974 ]
832 975
833 976 [[package]]
@@ -851,6 +994,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
851 994 checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
852 995
853 996 [[package]]
997 + name = "rustc_version"
998 + version = "0.4.1"
999 + source = "registry+https://github.com/rust-lang/crates.io-index"
1000 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
1001 + dependencies = [
1002 + "semver",
1003 + ]
1004 +
1005 + [[package]]
854 1006 name = "rustix"
855 1007 version = "1.1.4"
856 1008 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -869,8 +1021,8 @@ version = "0.23.40"
869 1021 source = "registry+https://github.com/rust-lang/crates.io-index"
870 1022 checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
871 1023 dependencies = [
1024 + "aws-lc-rs",
872 1025 "once_cell",
873 - "ring",
874 1026 "rustls-pki-types",
875 1027 "rustls-webpki",
876 1028 "subtle",
@@ -878,6 +1030,18 @@ dependencies = [
878 1030 ]
879 1031
880 1032 [[package]]
1033 + name = "rustls-native-certs"
1034 + version = "0.8.4"
1035 + source = "registry+https://github.com/rust-lang/crates.io-index"
1036 + checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
1037 + dependencies = [
1038 + "openssl-probe",
1039 + "rustls-pki-types",
1040 + "schannel",
1041 + "security-framework",
1042 + ]
1043 +
1044 + [[package]]
881 1045 name = "rustls-pki-types"
882 1046 version = "1.14.1"
883 1047 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -888,11 +1052,39 @@ dependencies = [
888 1052 ]
889 1053
890 1054 [[package]]
1055 + name = "rustls-platform-verifier"
1056 + version = "0.7.0"
1057 + source = "registry+https://github.com/rust-lang/crates.io-index"
1058 + checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
1059 + dependencies = [
1060 + "core-foundation",
1061 + "core-foundation-sys",
1062 + "jni",
1063 + "log",
1064 + "once_cell",
1065 + "rustls",
1066 + "rustls-native-certs",
1067 + "rustls-platform-verifier-android",
1068 + "rustls-webpki",
1069 + "security-framework",
1070 + "security-framework-sys",
1071 + "webpki-root-certs",
1072 + "windows-sys 0.52.0",
1073 + ]
1074 +
1075 + [[package]]
1076 + name = "rustls-platform-verifier-android"
1077 + version = "0.1.1"
1078 + source = "registry+https://github.com/rust-lang/crates.io-index"
1079 + checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
1080 +
1081 + [[package]]
891 1082 name = "rustls-webpki"
892 1083 version = "0.103.13"
893 1084 source = "registry+https://github.com/rust-lang/crates.io-index"
894 1085 checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
895 1086 dependencies = [
1087 + "aws-lc-rs",
896 1088 "ring",
897 1089 "rustls-pki-types",
898 1090 "untrusted",
@@ -911,6 +1103,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
911 1103 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
912 1104
913 1105 [[package]]
1106 + name = "same-file"
1107 + version = "1.0.6"
1108 + source = "registry+https://github.com/rust-lang/crates.io-index"
1109 + checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1110 + dependencies = [
1111 + "winapi-util",
1112 + ]
1113 +
1114 + [[package]]
1115 + name = "schannel"
1116 + version = "0.1.29"
1117 + source = "registry+https://github.com/rust-lang/crates.io-index"
1118 + checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
1119 + dependencies = [
1120 + "windows-sys 0.61.2",
1121 + ]
1122 +
1123 + [[package]]
1124 + name = "security-framework"
1125 + version = "3.7.0"
1126 + source = "registry+https://github.com/rust-lang/crates.io-index"
1127 + checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
1128 + dependencies = [
1129 + "bitflags",
1130 + "core-foundation",
1131 + "core-foundation-sys",
1132 + "libc",
1133 + "security-framework-sys",
1134 + ]
1135 +
1136 + [[package]]
1137 + name = "security-framework-sys"
1138 + version = "2.17.0"
1139 + source = "registry+https://github.com/rust-lang/crates.io-index"
1140 + checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
1141 + dependencies = [
1142 + "core-foundation-sys",
1143 + "libc",
1144 + ]
1145 +
1146 + [[package]]
914 1147 name = "semver"
915 1148 version = "1.0.28"
916 1149 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1006,6 +1239,22 @@ dependencies = [
1006 1239 ]
1007 1240
1008 1241 [[package]]
1242 + name = "simd_cesu8"
1243 + version = "1.2.0"
1244 + source = "registry+https://github.com/rust-lang/crates.io-index"
1245 + checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
1246 + dependencies = [
1247 + "rustc_version",
1248 + "simdutf8",
1249 + ]
1250 +
1251 + [[package]]
1252 + name = "simdutf8"
1253 + version = "0.1.5"
1254 + source = "registry+https://github.com/rust-lang/crates.io-index"
1255 + checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
1256 +
1257 + [[package]]
1009 1258 name = "slab"
1010 1259 version = "0.4.12"
1011 1260 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1381,6 +1630,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1381 1630 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
1382 1631
1383 1632 [[package]]
1633 + name = "walkdir"
1634 + version = "2.5.0"
1635 + source = "registry+https://github.com/rust-lang/crates.io-index"
1636 + checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1637 + dependencies = [
1638 + "same-file",
1639 + "winapi-util",
1640 + ]
1641 +
1642 + [[package]]
1384 1643 name = "want"
1385 1644 version = "0.3.1"
1386 1645 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1492,9 +1751,9 @@ dependencies = [
1492 1751
1493 1752 [[package]]
1494 1753 name = "wasm-streams"
1495 - version = "0.4.2"
1754 + version = "0.5.0"
1496 1755 source = "registry+https://github.com/rust-lang/crates.io-index"
1497 - checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
1756 + checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
1498 1757 dependencies = [
1499 1758 "futures-util",
1500 1759 "js-sys",
@@ -1536,15 +1795,24 @@ dependencies = [
1536 1795 ]
1537 1796
1538 1797 [[package]]
1539 - name = "webpki-roots"
1540 - version = "1.0.7"
1798 + name = "webpki-root-certs"
1799 + version = "1.0.9"
1541 1800 source = "registry+https://github.com/rust-lang/crates.io-index"
1542 - checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
1801 + checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
1543 1802 dependencies = [
1544 1803 "rustls-pki-types",
1545 1804 ]
1546 1805
1547 1806 [[package]]
1807 + name = "winapi-util"
1808 + version = "0.1.11"
1809 + source = "registry+https://github.com/rust-lang/crates.io-index"
1810 + checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
1811 + dependencies = [
1812 + "windows-sys 0.52.0",
1813 + ]
1814 +
1815 + [[package]]
1548 1816 name = "windows-link"
1549 1817 version = "0.2.1"
1550 1818 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -21,3 +21,35 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
21 21
22 22 [dev-dependencies]
23 23 tempfile = "3.20"
24 +
25 + [lints.rust]
26 + unused = "warn"
27 + unreachable_pub = "warn"
28 +
29 + [lints.clippy]
30 + pedantic = { level = "warn", priority = -1 }
31 + # Allow-list tuned from a measured breakdown across server/multithreaded/pter
32 + # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything
33 + # else in `pedantic` stays a warning. Keep this block identical across repos.
34 + module_name_repetitions = "allow"
35 + # Doc lints. No docs-completeness push is underway.
36 + missing_errors_doc = "allow"
37 + missing_panics_doc = "allow"
38 + doc_markdown = "allow"
39 + # Numeric casts. Endemic and mostly intentional in size and byte math.
40 + cast_possible_truncation = "allow"
41 + cast_sign_loss = "allow"
42 + cast_precision_loss = "allow"
43 + cast_possible_wrap = "allow"
44 + cast_lossless = "allow"
45 + # Subjective structure and style nags. High churn, low signal.
46 + must_use_candidate = "allow"
47 + too_many_lines = "allow"
48 + struct_excessive_bools = "allow"
49 + similar_names = "allow"
50 + items_after_statements = "allow"
51 + single_match_else = "allow"
52 + # Frequent false-positives in TUI and router-heavy code.
53 + match_same_arms = "allow"
54 + unnecessary_wraps = "allow"
55 + type_complexity = "allow"
@@ -189,8 +189,7 @@ async fn run_step(
189 189 "{label} failed (exit {}): {}",
190 190 out.status
191 191 .code()
192 - .map(|c| c.to_string())
193 - .unwrap_or_else(|| "signal".into()),
192 + .map_or_else(|| "signal".into(), |c| c.to_string()),
194 193 failure_tail(&out),
195 194 );
196 195 Ok(out)
@@ -19,3 +19,35 @@ anyhow = "1"
19 19 bytes = "1"
20 20 synckit-client = { path = "../shared/synckit-client", default-features = false }
21 21 uuid = "1"
22 +
23 + [lints.rust]
24 + unused = "warn"
25 + unreachable_pub = "warn"
26 +
27 + [lints.clippy]
28 + pedantic = { level = "warn", priority = -1 }
29 + # Allow-list tuned from a measured breakdown across server/multithreaded/pter
30 + # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything
31 + # else in `pedantic` stays a warning. Keep this block identical across repos.
32 + module_name_repetitions = "allow"
33 + # Doc lints. No docs-completeness push is underway.
34 + missing_errors_doc = "allow"
35 + missing_panics_doc = "allow"
36 + doc_markdown = "allow"
37 + # Numeric casts. Endemic and mostly intentional in size and byte math.
38 + cast_possible_truncation = "allow"
39 + cast_sign_loss = "allow"
40 + cast_precision_loss = "allow"
41 + cast_possible_wrap = "allow"
42 + cast_lossless = "allow"
43 + # Subjective structure and style nags. High churn, low signal.
44 + must_use_candidate = "allow"
45 + too_many_lines = "allow"
46 + struct_excessive_bools = "allow"
47 + similar_names = "allow"
48 + items_after_statements = "allow"
49 + single_match_else = "allow"
50 + # Frequent false-positives in TUI and router-heavy code.
51 + match_same_arms = "allow"
52 + unnecessary_wraps = "allow"
53 + type_complexity = "allow"
M mnw-cli/src/api.rs +97 -58
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
4 4
5 5 /// User info returned from the SSH key lookup endpoint.
6 6 #[derive(Debug, Clone, Deserialize, Serialize)]
7 - pub struct UserInfo {
7 + pub(crate) struct UserInfo {
8 8 pub user_id: String,
9 9 pub username: String,
10 10 pub display_name: Option<String>,
@@ -20,7 +20,11 @@ pub struct UserInfo {
20 20
21 21 /// A creator's project with item count and revenue.
22 22 #[derive(Debug, Clone, Deserialize, Serialize)]
23 - pub struct Project {
23 + #[allow(
24 + clippy::struct_field_names,
25 + reason = "project_type mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract"
26 + )]
27 + pub(crate) struct Project {
24 28 pub id: String,
25 29 pub slug: String,
26 30 pub title: String,
@@ -32,7 +36,11 @@ pub struct Project {
32 36
33 37 /// An item within a project.
34 38 #[derive(Debug, Clone, Deserialize, Serialize)]
35 - pub struct Item {
39 + #[allow(
40 + clippy::struct_field_names,
41 + reason = "item_type mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract"
42 + )]
43 + pub(crate) struct Item {
36 44 pub id: String,
37 45 pub title: String,
38 46 pub item_type: String,
@@ -43,7 +51,7 @@ pub struct Item {
43 51
44 52 /// Period comparison stats for the creator.
45 53 #[derive(Debug, Clone, Deserialize, Serialize)]
46 - pub struct CreatorStats {
54 + pub(crate) struct CreatorStats {
47 55 pub current_revenue_cents: i64,
48 56 pub previous_revenue_cents: i64,
49 57 pub current_sales: i64,
@@ -57,7 +65,7 @@ pub struct CreatorStats {
57 65 /// Response from the create-item internal endpoint.
58 66 #[derive(Debug, Deserialize)]
59 67 #[allow(dead_code)]
60 - pub struct ItemCreated {
68 + pub(crate) struct ItemCreated {
61 69 pub item_id: String,
62 70 pub project_id: String,
63 71 }
@@ -65,7 +73,7 @@ pub struct ItemCreated {
65 73 /// Response from the presign-upload internal endpoint.
66 74 #[derive(Debug, Deserialize)]
67 75 #[allow(dead_code)]
68 - pub struct PresignResponse {
76 + pub(crate) struct PresignResponse {
69 77 pub upload_url: String,
70 78 pub s3_key: String,
71 79 pub expires_in: u64,
@@ -77,7 +85,7 @@ pub struct PresignResponse {
77 85 /// fine for a small file and unacceptable for a multi-GB one; the multipart path
78 86 /// holds one part at a time. It is also the only path that can carry a file past
79 87 /// S3's 5 GiB single-PUT ceiling, which is what the tier limits allow for.
80 - pub const MULTIPART_THRESHOLD_BYTES: u64 = 64 * 1024 * 1024;
88 + pub(crate) const MULTIPART_THRESHOLD_BYTES: u64 = 64 * 1024 * 1024;
81 89
82 90 /// How many presigned part URLs to request at a time. Must not exceed the
83 91 /// server's own window cap.
@@ -85,7 +93,7 @@ const PART_URL_WINDOW: u32 = 100;
85 93
86 94 /// An opened multipart upload session.
87 95 #[derive(Debug, Clone, Deserialize)]
88 - pub struct MultipartStart {
96 + pub(crate) struct MultipartStart {
89 97 pub upload_id: String,
90 98 pub s3_key: String,
91 99 pub part_size: u64,
@@ -95,7 +103,7 @@ pub struct MultipartStart {
95 103
96 104 /// One presigned part target, with the exact length the signature binds.
97 105 #[derive(Debug, Clone, Deserialize)]
98 - pub struct MultipartPartUrl {
106 + pub(crate) struct MultipartPartUrl {
99 107 pub part_number: i32,
100 108 pub content_length: u64,
101 109 pub url: String,
@@ -108,7 +116,7 @@ struct MultipartPartsResponse {
108 116
109 117 /// Full item detail returned from the get/update endpoints.
110 118 #[derive(Debug, Clone, Deserialize, Serialize)]
111 - pub struct ItemDetail {
119 + pub(crate) struct ItemDetail {
112 120 pub id: String,
113 121 pub title: String,
114 122 pub description: Option<String>,
@@ -130,7 +138,11 @@ pub struct ItemDetail {
130 138
131 139 /// A version of an item.
132 140 #[derive(Debug, Clone, Deserialize, Serialize)]
133 - pub struct Version {
141 + #[allow(
142 + clippy::struct_field_names,
143 + reason = "version_number mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract"
144 + )]
145 + pub(crate) struct Version {
134 146 pub id: String,
135 147 pub version_number: String,
136 148 pub changelog: Option<String>,
@@ -143,7 +155,7 @@ pub struct Version {
143 155
144 156 /// A blog post summary.
145 157 #[derive(Debug, Clone, Deserialize, Serialize)]
146 - pub struct BlogPost {
158 + pub(crate) struct BlogPost {
147 159 pub id: String,
148 160 pub title: String,
149 161 pub slug: String,
@@ -155,7 +167,7 @@ pub struct BlogPost {
155 167
156 168 /// A promo code.
157 169 #[derive(Debug, Clone, Deserialize, Serialize)]
158 - pub struct PromoCode {
170 + pub(crate) struct PromoCode {
159 171 pub id: String,
160 172 pub code: String,
161 173 pub code_purpose: String,
@@ -170,7 +182,7 @@ pub struct PromoCode {
170 182
171 183 /// A license key.
172 184 #[derive(Debug, Clone, Deserialize, Serialize)]
173 - pub struct LicenseKey {
185 + pub(crate) struct LicenseKey {
174 186 pub id: String,
175 187 pub key_code: String,
176 188 pub activation_count: i32,
@@ -181,7 +193,7 @@ pub struct LicenseKey {
181 193
182 194 /// Response from the storage-info internal endpoint.
183 195 #[derive(Debug, Clone, Deserialize, Serialize)]
184 - pub struct StorageInfo {
196 + pub(crate) struct StorageInfo {
185 197 pub storage_used_bytes: i64,
186 198 pub max_storage_bytes: i64,
187 199 pub allows_file_uploads: bool,
@@ -189,7 +201,7 @@ pub struct StorageInfo {
189 201
190 202 /// A revenue bucket for analytics timeseries.
191 203 #[derive(Debug, Clone, Deserialize, Serialize)]
192 - pub struct AnalyticsBucket {
204 + pub(crate) struct AnalyticsBucket {
193 205 pub label: String,
194 206 pub revenue_cents: i64,
195 207 pub sales_count: i64,
@@ -197,7 +209,7 @@ pub struct AnalyticsBucket {
197 209
198 210 /// Per-project revenue summary.
199 211 #[derive(Debug, Clone, Deserialize, Serialize)]
200 - pub struct ProjectRevenue {
212 + pub(crate) struct ProjectRevenue {
201 213 pub id: String,
202 214 pub title: String,
203 215 pub revenue_cents: i64,
@@ -205,7 +217,7 @@ pub struct ProjectRevenue {
205 217
206 218 /// Analytics response with timeseries, comparison, and top projects.
207 219 #[derive(Debug, Clone, Deserialize, Serialize)]
208 - pub struct AnalyticsData {
220 + pub(crate) struct AnalyticsData {
209 221 pub buckets: Vec<AnalyticsBucket>,
210 222 pub current_revenue_cents: i64,
211 223 pub previous_revenue_cents: i64,
@@ -218,7 +230,7 @@ pub struct AnalyticsData {
218 230
219 231 /// A seller transaction.
220 232 #[derive(Debug, Clone, Deserialize, Serialize)]
221 - pub struct Transaction {
233 + pub(crate) struct Transaction {
222 234 pub id: String,
223 235 pub item_title: Option<String>,
224 236 pub amount_cents: i32,
@@ -229,14 +241,14 @@ pub struct Transaction {
229 241
230 242 /// CSV export result.
231 243 #[derive(Debug, Clone, Deserialize, Serialize)]
232 - pub struct ExportResult {
244 + pub(crate) struct ExportResult {
233 245 pub csv: String,
234 246 pub row_count: usize,
235 247 }
236 248
237 249 /// A registered SSH key.
238 250 #[derive(Debug, Clone, Deserialize, Serialize)]
239 - pub struct SshKeyInfo {
251 + pub(crate) struct SshKeyInfo {
240 252 pub id: String,
241 253 pub label: String,
242 254 pub fingerprint: String,
@@ -245,7 +257,7 @@ pub struct SshKeyInfo {
245 257
246 258 /// A tag on an item or from search.
247 259 #[derive(Debug, Clone, Deserialize, Serialize)]
248 - pub struct TagInfo {
260 + pub(crate) struct TagInfo {
249 261 pub id: String,
250 262 pub name: String,
251 263 pub slug: String,
@@ -255,14 +267,14 @@ pub struct TagInfo {
255 267 /// Result of a broadcast send.
256 268 #[derive(Debug, Deserialize)]
257 269 #[allow(dead_code)] // wire contract: every field the endpoint returns, read or not
258 - pub struct BroadcastResult {
270 + pub(crate) struct BroadcastResult {
259 271 pub success: bool,
260 272 pub recipient_count: usize,
261 273 }
262 274
263 275 /// A subscription tier.
264 276 #[derive(Debug, Clone, Deserialize, Serialize)]
265 - pub struct TierInfo {
277 + pub(crate) struct TierInfo {
266 278 pub id: String,
267 279 pub name: String,
268 280 pub description: String,
@@ -272,7 +284,7 @@ pub struct TierInfo {
272 284
273 285 /// A collection.
274 286 #[derive(Debug, Clone, Deserialize, Serialize)]
275 - pub struct CollectionInfo {
287 + pub(crate) struct CollectionInfo {
276 288 pub id: String,
277 289 pub slug: String,
278 290 pub title: String,
@@ -283,7 +295,7 @@ pub struct CollectionInfo {
283 295
284 296 /// Custom domain info.
285 297 #[derive(Debug, Clone, Deserialize, Serialize)]
286 - pub struct DomainInfo {
298 + pub(crate) struct DomainInfo {
287 299 pub id: String,
288 300 pub domain: String,
289 301 pub verified: bool,
@@ -294,14 +306,14 @@ pub struct DomainInfo {
294 306 /// Domain verification result.
295 307 #[derive(Debug, Deserialize)]
296 308 #[allow(dead_code)] // wire contract: every field the endpoint returns, read or not
297 - pub struct DomainVerifyResult {
309 + pub(crate) struct DomainVerifyResult {
298 310 pub verified: bool,
299 311 pub message: String,
300 312 }
301 313
302 314 /// Response from the git authorize endpoint.
303 315 #[derive(Debug, Deserialize)]
304 - pub struct GitAuthResponse {
316 + pub(crate) struct GitAuthResponse {
305 317 pub repo_path: String,
306 318 }
307 319
@@ -342,7 +354,7 @@ async fn empty_response(resp: reqwest::Response, context: &str) -> anyhow::Resul
342 354
343 355 /// Client for calling MNW internal API endpoints.
344 356 #[derive(Clone)]
345 - pub struct MnwApiClient {
357 + pub(crate) struct MnwApiClient {
346 358 http: reqwest::Client,
347 359 base_url: String,
348 360 service_token: String,
@@ -352,7 +364,7 @@ pub struct MnwApiClient {
352 364 }
353 365
354 366 impl MnwApiClient {
355 - pub fn new(base_url: String, service_token: String) -> Self {
367 + pub(crate) fn new(base_url: String, service_token: String) -> Self {
356 368 let http = reqwest::Client::builder()
357 369 .timeout(std::time::Duration::from_secs(5))
358 370 .build()
@@ -368,7 +380,7 @@ impl MnwApiClient {
368 380
369 381 /// Record the actor assertion for the authenticated session. Subsequent
370 382 /// internal calls forward it so the server can verify the acting identity.
371 - pub fn set_actor_token(&mut self, token: String) {
383 + pub(crate) fn set_actor_token(&mut self, token: String) {
372 384 self.actor_token = Some(token);
373 385 }
374 386
@@ -379,7 +391,10 @@ impl MnwApiClient {
379 391
380 392 /// Look up a user by SSH key fingerprint.
381 393 /// Returns `Ok(Some(info))` if found, `Ok(None)` if not found.
382 - pub async fn lookup_ssh_key(&self, fingerprint: &str) -> anyhow::Result<Option<UserInfo>> {
394 + pub(crate) async fn lookup_ssh_key(
395 + &self,
396 + fingerprint: &str,
397 + ) -> anyhow::Result<Option<UserInfo>> {
383 398 let url = format!("{}/api/internal/ssh-key-lookup", self.base_url);
384 399 let resp = self
385 400 .http
@@ -403,7 +418,7 @@ impl MnwApiClient {
403 418 }
404 419
405 420 /// Fetch all projects for a creator with item counts and revenue.
406 - pub async fn get_projects(&self, user_id: &str) -> anyhow::Result<Vec<Project>> {
421 + pub(crate) async fn get_projects(&self, user_id: &str) -> anyhow::Result<Vec<Project>> {
407 422 let url = format!("{}/api/internal/creator/projects", self.base_url);
408 423 let resp = self
409 424 .http
@@ -418,7 +433,7 @@ impl MnwApiClient {
418 433 }
419 434
420 435 /// Create a new project.
421 - pub async fn create_project(
436 + pub(crate) async fn create_project(
422 437 &self,
423 438 user_id: &str,
424 439 title: &str,
@@ -447,7 +462,7 @@ impl MnwApiClient {
447 462 }
448 463
449 464 /// Fetch items in a project.
450 - pub async fn get_project_items(
465 + pub(crate) async fn get_project_items(
451 466 &self,
452 467 project_id: &str,
453 468 user_id: &str,
@@ -469,7 +484,11 @@ impl MnwApiClient {
469 484 }
470 485
471 486 /// Fetch period comparison stats for a creator.
472 - pub async fn get_stats(&self, user_id: &str, range: &str) -> anyhow::Result<CreatorStats> {
487 + pub(crate) async fn get_stats(
488 + &self,
489 + user_id: &str,
490 + range: &str,
491 + ) -> anyhow::Result<CreatorStats> {
473 492 let url = format!("{}/api/internal/creator/stats", self.base_url);
474 493 let resp = self
475 494 .http
@@ -484,7 +503,7 @@ impl MnwApiClient {
484 503 }
485 504
486 505 /// Fetch storage usage and limits for a creator.
487 - pub async fn get_storage_info(&self, user_id: &str) -> anyhow::Result<StorageInfo> {
506 + pub(crate) async fn get_storage_info(&self, user_id: &str) -> anyhow::Result<StorageInfo> {
488 507 let url = format!("{}/api/internal/creator/storage", self.base_url);
489 508 let resp = self
490 509 .http
@@ -499,7 +518,7 @@ impl MnwApiClient {
499 518 }
500 519
501 520 /// Create an item in a project.
502 - pub async fn create_item(
521 + pub(crate) async fn create_item(
503 522 &self,
504 523 user_id: &str,
505 524 project_id: &str,
@@ -527,7 +546,7 @@ impl MnwApiClient {
527 546 }
528 547
529 548 /// Get a presigned S3 upload URL.
530 - pub async fn presign_upload(
549 + pub(crate) async fn presign_upload(
531 550 &self,
532 551 user_id: &str,
533 552 item_id: &str,
@@ -555,7 +574,7 @@ impl MnwApiClient {
555 574 }
556 575
557 576 /// Confirm a completed S3 upload.
558 - pub async fn confirm_upload(
577 + pub(crate) async fn confirm_upload(
559 578 &self,
560 579 user_id: &str,
561 580 item_id: &str,
@@ -586,7 +605,7 @@ impl MnwApiClient {
586 605 }
587 606
588 607 /// Fetch full item detail.
589 - pub async fn get_item_detail(
608 + pub(crate) async fn get_item_detail(
590 609 &self,
591 610 user_id: &str,
592 611 item_id: &str,
@@ -605,7 +624,7 @@ impl MnwApiClient {
605 624 }
606 625
607 626 /// Update item fields. Only non-None fields are changed.
608 - pub async fn update_item(
627 + pub(crate) async fn update_item(
609 628 &self,
610 629 user_id: &str,
611 630 item_id: &str,
@@ -642,7 +661,7 @@ impl MnwApiClient {
642 661 }
643 662
644 663 /// Delete an item permanently.
645 - pub async fn delete_item(&self, user_id: &str, item_id: &str) -> anyhow::Result<()> {
664 + pub(crate) async fn delete_item(&self, user_id: &str, item_id: &str) -> anyhow::Result<()> {
646 665 let url = format!("{}/api/internal/creator/items/{}", self.base_url, item_id);
647 666 let resp = self
648 667 .http
@@ -657,7 +676,11 @@ impl MnwApiClient {
657 676 }
658 677
659 678 /// Publish an item (set is_public=true).
660 - pub async fn publish_item(&self, user_id: &str, item_id: &str) -> anyhow::Result<ItemDetail> {
679 + pub(crate) async fn publish_item(
680 + &self,
681 + user_id: &str,
682 + item_id: &str,
683 + ) -> anyhow::Result<ItemDetail> {
661 684 let url = format!(
662 685 "{}/api/internal/creator/items/{}/publish",
663 686 self.base_url, item_id
@@ -675,7 +698,11 @@ impl MnwApiClient {
675 698 }
676 699
677 700 /// Unpublish an item (set is_public=false).
678 - pub async fn unpublish_item(&self, user_id: &str, item_id: &str) -> anyhow::Result<ItemDetail> {
701 + pub(crate) async fn unpublish_item(
702 + &self,
703 + user_id: &str,
704 + item_id: &str,
705 + ) -> anyhow::Result<ItemDetail> {
679 706 let url = format!(
680 707 "{}/api/internal/creator/items/{}/unpublish",
681 708 self.base_url, item_id
@@ -693,7 +720,7 @@ impl MnwApiClient {
693 720 }
694 721
695 722 /// Fetch versions for an item.
696 - pub async fn get_item_versions(
723 + pub(crate) async fn get_item_versions(
697 724 &self,
698 725 user_id: &str,
699 726 item_id: &str,
@@ -717,7 +744,7 @@ impl MnwApiClient {
717 744 // โ”€โ”€ Multipart upload session (large files) โ”€โ”€
718 745
719 746 /// Open a multipart upload session and get the part geometry.
720 - pub async fn multipart_start(
747 + pub(crate) async fn multipart_start(
721 748 &self,
722 749 item_id: &str,
723 750 file_type: &str,
@@ -810,7 +837,11 @@ impl MnwApiClient {
810 837
811 838 /// Release the parts of an abandoned session. Incomplete multipart uploads
812 839 /// bill for their parts until aborted.
813 - pub async fn multipart_abort(&self, s3_key: &str, upload_id: &str) -> anyhow::Result<()> {
840 + pub(crate) async fn multipart_abort(
841 + &self,
842 + s3_key: &str,
843 + upload_id: &str,
844 + ) -> anyhow::Result<()> {
814 845 let url = format!("{}/api/internal/upload/multipart/abort", self.base_url);
815 846 let resp = self
816 847 .http
@@ -835,7 +866,7 @@ impl MnwApiClient {
835 866 /// does not leave parts billing indefinitely โ€” the single abort site means a
836 867 /// future failure path added inside cannot forget to.
837 868 #[allow(clippy::too_many_arguments)]
838 - pub async fn upload_file_multipart(
869 + pub(crate) async fn upload_file_multipart(
839 870 &self,
840 871 item_id: &str,
841 872 file_type: &str,
@@ -993,7 +1024,7 @@ impl MnwApiClient {
993 1024 }
994 1025
995 1026 /// Upload a file to S3 using a presigned URL.
996 - pub async fn upload_to_s3(
1027 + pub(crate) async fn upload_to_s3(
997 1028 &self,
998 1029 presigned_url: &str,
999 1030 file_path: &std::path::Path,
@@ -1023,7 +1054,7 @@ impl MnwApiClient {
1023 1054 // โ”€โ”€ Blog posts โ”€โ”€
1024 1055
1025 1056 /// List blog posts for a project.
1026 - pub async fn list_blog_posts(
1057 + pub(crate) async fn list_blog_posts(
1027 1058 &self,
1028 1059 user_id: &str,
1029 1060 project_id: &str,
@@ -1045,7 +1076,7 @@ impl MnwApiClient {
1045 1076 }
1046 1077
1047 1078 /// Create a blog post, optionally scheduled for future publication.
1048 - pub async fn create_blog_post(
1079 + pub(crate) async fn create_blog_post(
1049 1080 &self,
1050 1081 user_id: &str,
1051 1082 project_id: &str,
@@ -1078,7 +1109,11 @@ impl MnwApiClient {
1078 1109 }
1079 1110
1080 1111 /// Delete a blog post.
1081 - pub async fn delete_blog_post(&self, user_id: &str, post_id: &str) -> anyhow::Result<()> {
1112 + pub(crate) async fn delete_blog_post(
1113 + &self,
1114 + user_id: &str,
1115 + post_id: &str,
1116 + ) -> anyhow::Result<()> {
1082 1117 let url = format!("{}/api/internal/creator/blog/{}", self.base_url, post_id);
1083 1118 let resp = self
1084 1119 .http
@@ -1095,7 +1130,7 @@ impl MnwApiClient {
1095 1130 // โ”€โ”€ Promo codes โ”€โ”€
1096 1131
1097 1132 /// List promo codes for a creator.
1098 - pub async fn list_promo_codes(&self, user_id: &str) -> anyhow::Result<Vec<PromoCode>> {
1133 + pub(crate) async fn list_promo_codes(&self, user_id: &str) -> anyhow::Result<Vec<PromoCode>> {
1099 1134 let url = format!("{}/api/internal/creator/promo-codes", self.base_url);
1100 1135 let resp = self
1101 1136 .http
@@ -1110,7 +1145,7 @@ impl MnwApiClient {
1110 1145 }
1111 1146
1112 1147 /// Create a promo code.
1113 - pub async fn create_promo_code(
1148 + pub(crate) async fn create_promo_code(
1114 1149 &self,
1115 1150 user_id: &str,
1116 1151 code: &str,
@@ -1147,7 +1182,11 @@ impl MnwApiClient {
1147 1182 }
1148 1183
1149 1184 /// Delete a promo code.
1150 - pub async fn delete_promo_code(&self, user_id: &str, code_id: &str) -> anyhow::Result<()> {
1185 + pub(crate) async fn delete_promo_code(
1186 + &self,
1187 + user_id: &str,
1188 + code_id: &str,
1189 + ) -> anyhow::Result<()> {
1151 1190 let url = format!(
1152 1191 "{}/api/internal/creator/promo-codes/{}",
1153 1192 self.base_url, code_id
@@ -1167,7 +1206,7 @@ impl MnwApiClient {
1167 1206 // โ”€โ”€ License keys โ”€โ”€
1168 1207
1169 1208 /// List license keys for an item.
1170 - pub async fn list_license_keys(
1209 + pub(crate) async fn list_license_keys(
1171 1210 &self,
1172 1211 user_id: &str,
1173 1212 item_id: &str,
@@ -1189,7 +1228,7 @@ impl MnwApiClient {
1189 1228 }
1190 1229
1191 1230 /// Generate a new license key for an item.
1192 - pub async fn generate_license_key(
1231 + pub(crate) async fn generate_license_key(
Lines truncated
@@ -4,6 +4,8 @@
4 4 //! dispatches to this module. All commands write output to a byte buffer and
5 5 //! return it for the SSH channel.
6 6
7 + use std::fmt::Write as _;
8 +
7 9 use crate::api::{MnwApiClient, UserInfo};
8 10 use crate::format;
9 11 use crate::staging;
@@ -29,7 +31,7 @@ pub(crate) fn sanitize_api_error(e: &anyhow::Error) -> String {
29 31 }
30 32
31 33 /// Execute a non-interactive command and return the output bytes.
32 - pub async fn execute(command_line: &str, user: &UserInfo, api: &MnwApiClient) -> Vec<u8> {
34 + pub(crate) async fn execute(command_line: &str, user: &UserInfo, api: &MnwApiClient) -> Vec<u8> {
33 35 let parts: Vec<&str> = command_line.split_whitespace().collect();
34 36 if parts.is_empty() {
35 37 return help_text();
@@ -142,14 +144,16 @@ async fn cmd_projects(user: &UserInfo, api: &MnwApiClient, json: bool) -> Vec<u8
142 144 for p in &projects {
143 145 let status = if p.is_public { "public" } else { "draft" };
144 146 let revenue = format::format_cents(p.revenue_cents);
145 - out.push_str(&format!(
147 + write!(
148 + out,
146 149 "{:<30} {:<12} {:<8} {:<6} {:<10}\r\n",
147 150 truncate(&p.title, 29),
148 151 p.project_type,
149 152 status,
150 153 p.item_count,
151 154 revenue,
152 - ));
155 + )
156 + .unwrap();
153 157 }
154 158 out.into_bytes()
155 159 }
@@ -165,28 +169,34 @@ async fn cmd_analytics(user: &UserInfo, api: &MnwApiClient, range: &str, json: b
165 169 }
166 170
167 171 let mut out = String::new();
168 - out.push_str(&format!("Analytics ({range})\r\n\r\n"));
172 + write!(out, "Analytics ({range})\r\n\r\n").unwrap();
169 173
170 174 let rev = format::format_cents(data.current_revenue_cents);
171 175 let prev_rev = format::format_cents(data.previous_revenue_cents);
172 - out.push_str(&format!("Revenue: {rev} (prev: {prev_rev})\r\n"));
173 - out.push_str(&format!(
176 + write!(out, "Revenue: {rev} (prev: {prev_rev})\r\n").unwrap();
177 + write!(
178 + out,
174 179 "Sales: {} (prev: {})\r\n",
175 180 data.current_sales, data.previous_sales
176 - ));
177 - out.push_str(&format!(
181 + )
182 + .unwrap();
183 + write!(
184 + out,
178 185 "Followers: {} (prev: {})\r\n",
179 186 data.current_followers, data.previous_followers
180 - ));
187 + )
188 + .unwrap();
181 189
182 190 if !data.top_projects.is_empty() {
183 191 out.push_str("\r\nTop Projects:\r\n");
184 192 for p in &data.top_projects {
185 - out.push_str(&format!(
193 + write!(
194 + out,
186 195 " {:<30} {}\r\n",
187 196 truncate(&p.title, 29),
188 197 format::format_cents(p.revenue_cents)
189 - ));
198 + )
199 + .unwrap();
190 200 }
191 201 }
192 202
@@ -215,13 +225,15 @@ async fn cmd_transactions(user: &UserInfo, api: &MnwApiClient, json: bool) -> Ve
215 225 let title = tx.item_title.as_deref().unwrap_or("--");
216 226 let amount = format::format_cents(tx.amount_cents as i64);
217 227 let date = tx.created_at.get(..10).unwrap_or(&tx.created_at);
218 - out.push_str(&format!(
228 + write!(
229 + out,
219 230 "{:<30} {:<10} {:<12} {:<12}\r\n",
220 231 truncate(title, 29),
221 232 amount,
222 233 tx.status,
223 234 date,
224 - ));
235 + )
236 + .unwrap();
225 237 }
226 238 out.into_bytes()
227 239 }
@@ -253,7 +265,7 @@ async fn cmd_promo_list(user: &UserInfo, api: &MnwApiClient, json: bool) -> Vec<
253 265 out.push_str("\r\n");
254 266 for c in &codes {
255 267 let discount = match (c.discount_type.as_deref(), c.discount_value) {
256 - (Some("percentage"), Some(v)) => format!("{}% off", v),
268 + (Some("percentage"), Some(v)) => format!("{v}% off"),
257 269 (Some("fixed"), Some(v)) => format!("${}.{:02} off", v / 100, v % 100),
258 270 _ => "Free".to_string(),
259 271 };
@@ -266,13 +278,15 @@ async fn cmd_promo_list(user: &UserInfo, api: &MnwApiClient, json: bool) -> Vec<
266 278 Some(max) => format!("{}/{}", c.use_count, max),
267 279 None => c.use_count.to_string(),
268 280 };
269 - out.push_str(&format!(
281 + write!(
282 + out,
270 283 "{:<20} {:<12} {:<20} {:<10}\r\n",
271 284 truncate(&c.code, 19),
272 285 discount,
273 286 truncate(scope, 19),
274 287 uses,
275 - ));
288 + )
289 + .unwrap();
276 290 }
277 291 out.into_bytes()
278 292 }
@@ -326,13 +340,15 @@ async fn cmd_blog_list(user: &UserInfo, api: &MnwApiClient, slug: &str, json: bo
326 340 for p in &posts {
327 341 let status = if p.is_published { "published" } else { "draft" };
328 342 let date = p.created_at.get(..10).unwrap_or(&p.created_at);
329 - out.push_str(&format!(
343 + write!(
344 + out,
330 345 "{:<30} {:<20} {:<10} {:<12}\r\n",
331 346 truncate(&p.title, 29),
332 347 truncate(&p.slug, 19),
333 348 status,
334 349 date,
335 - ));
350 + )
351 + .unwrap();
336 352 }
337 353 out.into_bytes()
338 354 }
@@ -374,13 +390,15 @@ async fn cmd_collections(user: &UserInfo, api: &MnwApiClient, json: bool) -> Vec
374 390 out.push_str("\r\n");
375 391 for c in &collections {
376 392 let status = if c.is_public { "public" } else { "draft" };
377 - out.push_str(&format!(
393 + write!(
394 + out,
378 395 "{:<25} {:<25} {:<8} {:<6}\r\n",
379 396 truncate(&c.title, 24),
380 397 truncate(&c.slug, 24),
381 398 status,
382 399 c.item_count,
383 - ));
400 + )
401 + .unwrap();
384 402 }
385 403 out.into_bytes()
386 404 }
@@ -396,7 +414,7 @@ async fn cmd_domain_show(user: &UserInfo, api: &MnwApiClient) -> Vec<u8> {
396 414 if !d.verified
397 415 && let Some(ref instr) = d.instructions
398 416 {
399 - out.push_str(&format!("{}\r\n", instr));
417 + write!(out, "{instr}\r\n").unwrap();
400 418 }
401 419 out.into_bytes()
402 420 }
@@ -413,7 +431,7 @@ async fn cmd_domain_add(user: &UserInfo, api: &MnwApiClient, domain: &str) -> Ve
413 431 Ok(d) => {
414 432 let mut out = format!("Domain added: {}\r\n", d.domain);
415 433 if let Some(ref instr) = d.instructions {
416 - out.push_str(&format!("{}\r\n", instr));
434 + write!(out, "{instr}\r\n").unwrap();
417 435 }
418 436 out.push_str("Run `domain verify` after adding the DNS record.\r\n");
419 437 out.into_bytes()
@@ -437,7 +455,7 @@ async fn cmd_domain_remove(user: &UserInfo, api: &MnwApiClient) -> Vec<u8> {
437 455 }
438 456
439 457 /// Execute a pipe-mode file upload (called from handler after stdin EOF).
440 - pub async fn execute_pipe_upload(
458 + pub(crate) async fn execute_pipe_upload(
441 459 api: &MnwApiClient,
442 460 upload: crate::ssh::handler::PipeUpload,
443 461 ) -> anyhow::Result<String> {
@@ -518,7 +536,7 @@ pub async fn execute_pipe_upload(
518 536 ))
519 537 }
520 538
521 - pub fn help_text() -> Vec<u8> {
539 + pub(crate) fn help_text() -> Vec<u8> {
522 540 b"Usage: ssh cli.makenot.work <command>\r\n\
523 541 \r\n\
524 542 Commands:\r\n\
@@ -3,7 +3,7 @@
3 3 use std::path::PathBuf;
4 4
5 5 /// Application configuration.
6 - pub struct Config {
6 + pub(crate) struct Config {
7 7 /// Port to listen on for SSH connections.
8 8 pub port: u16,
9 9 /// MNW API base URL (e.g., "http://localhost:3000").
@@ -20,7 +20,7 @@ pub struct Config {
20 20
21 21 impl Config {
22 22 /// Load configuration from environment variables.
23 - pub fn from_env() -> anyhow::Result<Self> {
23 + pub(crate) fn from_env() -> anyhow::Result<Self> {
24 24 let port: u16 = std::env::var("SSH_PORT")
25 25 .unwrap_or_else(|_| "2222".to_string())
26 26 .parse()
@@ -1,7 +1,7 @@
1 1 //! Shared formatting utilities used across TUI screens and commands.
2 2
3 3 /// Format a revenue amount in cents. Returns "$0" for zero.
4 - pub fn format_cents(cents: i64) -> String {
4 + pub(crate) fn format_cents(cents: i64) -> String {
5 5 if cents == 0 {
6 6 "$0".to_string()
7 7 } else {
@@ -10,7 +10,7 @@ pub fn format_cents(cents: i64) -> String {
10 10 }
11 11
12 12 /// Format an item price in cents. Returns "Free" for zero.
13 - pub fn format_price(cents: i32) -> String {
13 + pub(crate) fn format_price(cents: i32) -> String {
14 14 if cents == 0 {
15 15 "Free".to_string()
16 16 } else {
@@ -19,7 +19,7 @@ pub fn format_price(cents: i32) -> String {
19 19 }
20 20
21 21 /// Human-readable creator tier label.
22 - pub fn format_tier(tier: &str) -> &str {
22 + pub(crate) fn format_tier(tier: &str) -> &str {
23 23 match tier {
24 24 "basic" => "Basic",
25 25 "small_files" => "Small Files",
@@ -30,7 +30,7 @@ pub fn format_tier(tier: &str) -> &str {
30 30 }
31 31
32 32 /// Human-readable project type label.
33 - pub fn format_project_type(pt: &str) -> &str {
33 + pub(crate) fn format_project_type(pt: &str) -> &str {
34 34 match pt {
35 35 "software" => "Software",
36 36 "music" => "Music",
@@ -45,7 +45,7 @@ pub fn format_project_type(pt: &str) -> &str {
45 45 }
46 46
47 47 /// Human-readable item type label.
48 - pub fn format_item_type(it: &str) -> &str {
48 + pub(crate) fn format_item_type(it: &str) -> &str {
49 49 match it {
50 50 "audio" => "Audio",
51 51 "text" => "Text",
@@ -50,8 +50,8 @@ async fn main() -> anyhow::Result<()> {
50 50 // Spawn hourly cleanup task for stale staging files (24h TTL)
51 51 let cleanup_dir = config.staging_dir.clone();
52 52 tokio::spawn(async move {
53 - let ttl = std::time::Duration::from_secs(24 * 60 * 60);
54 - let mut interval = tokio::time::interval(std::time::Duration::from_secs(60 * 60));
53 + let ttl = std::time::Duration::from_hours(24);
54 + let mut interval = tokio::time::interval(std::time::Duration::from_hours(1));
55 55 loop {
56 56 interval.tick().await;
57 57 staging::cleanup_stale(&cleanup_dir, ttl).await;
@@ -92,7 +92,7 @@ async fn main() -> anyhow::Result<()> {
92 92 result = server.run_on_address(Arc::new(ssh_config), addr) => {
93 93 result?;
94 94 }
95 - _ = shutdown_signal() => {
95 + () = shutdown_signal() => {
96 96 tracing::info!("shutdown signal received, stopping");
97 97 }
98 98 }
@@ -18,10 +18,10 @@ const ALLOWED_TARGETS: &[&str] = &["linux", "darwin", "windows"];
18 18 const ALLOWED_ARCHS: &[&str] = &["x86_64", "aarch64"];
19 19
20 20 /// Entry point for the `ota` subcommand. `rest` is everything after `ota`.
21 - pub async fn run(rest: &[String]) -> Result<()> {
21 + pub(crate) async fn run(rest: &[String]) -> Result<()> {
22 22 match rest.first().map(String::as_str) {
23 23 Some("publish") => publish(&rest[1..]).await,
24 - Some("-h") | Some("--help") | None => {
24 + Some("-h" | "--help") | None => {
25 25 print_usage();
26 26 Ok(())
27 27 }
@@ -352,12 +352,10 @@ async fn authenticate_oauth(client: &SyncKitClient, key: &str) -> Result<()> {
352 352 open_browser(&url);
353 353 println!(" waiting for the authorization redirect (5 min timeout)...");
354 354
355 - let (code, got_state) = tokio::time::timeout(
356 - std::time::Duration::from_secs(300),
357 - wait_for_code(&listener),
358 - )
359 - .await
360 - .context("timed out waiting for OAuth authorization")??;
355 + let (code, got_state) =
356 + tokio::time::timeout(std::time::Duration::from_mins(5), wait_for_code(&listener))
357 + .await
358 + .context("timed out waiting for OAuth authorization")??;
361 359
362 360 if got_state.as_deref() != Some(state.as_str()) {
363 361 bail!("OAuth state mismatch โ€” aborting (possible CSRF or a stale redirect)");
@@ -385,7 +383,7 @@ async fn wait_for_code(listener: &tokio::net::TcpListener) -> Result<(String, Op
385 383 .next()
386 384 .and_then(|line| line.split_whitespace().nth(1))
387 385 .unwrap_or("");
388 - let query = target.split_once('?').map(|(_, q)| q).unwrap_or("");
386 + let query = target.split_once('?').map_or("", |(_, q)| q);
389 387
390 388 let (mut code, mut state, mut oauth_err) = (None, None, None);
391 389 for pair in query.split('&') {
@@ -472,7 +470,7 @@ mod tests {
472 470 "https://example.test",
473 471 ]
474 472 .iter()
475 - .map(|s| s.to_string())
473 + .map(std::string::ToString::to_string)
476 474 .collect()
477 475 }
478 476
@@ -13,12 +13,12 @@ const MAX_FAILURES: usize = 10;
13 13 const WINDOW_SECS: u64 = 60;
14 14 const PRUNE_THRESHOLD: usize = 1000;
15 15
16 - pub struct AuthRateLimiter {
16 + pub(crate) struct AuthRateLimiter {
17 17 failures: Mutex<HashMap<IpAddr, Vec<Instant>>>,
18 18 }
19 19
20 20 impl AuthRateLimiter {
21 - pub fn new() -> Self {
21 + pub(crate) fn new() -> Self {
22 22 Self {
23 23 failures: Mutex::new(HashMap::new()),
24 24 }
@@ -26,9 +26,11 @@ impl AuthRateLimiter {
26 26
27 27 /// Returns `true` if the IP is allowed to attempt auth.
28 28 /// Returns `false` if the IP has exceeded the failure threshold.
29 - pub fn check(&self, ip: IpAddr) -> bool {
29 + pub(crate) fn check(&self, ip: IpAddr) -> bool {
30 30 let mut map = self.failures.lock().unwrap();
31 - let cutoff = Instant::now() - std::time::Duration::from_secs(WINDOW_SECS);
31 + let cutoff = Instant::now()
32 + .checked_sub(std::time::Duration::from_secs(WINDOW_SECS))
33 + .unwrap();
32 34
33 35 if let Some(times) = map.get_mut(&ip) {
34 36 times.retain(|t| *t > cutoff);
@@ -39,12 +41,14 @@ impl AuthRateLimiter {
39 41 }
40 42
41 43 /// Record a failed auth attempt for the given IP.
42 - pub fn record_failure(&self, ip: IpAddr) {
44 + pub(crate) fn record_failure(&self, ip: IpAddr) {
43 45 let mut map = self.failures.lock().unwrap();
44 46
45 47 // Prune stale entries when map grows large
46 48 if map.len() > PRUNE_THRESHOLD {
47 - let cutoff = Instant::now() - std::time::Duration::from_secs(WINDOW_SECS);
49 + let cutoff = Instant::now()
50 + .checked_sub(std::time::Duration::from_secs(WINDOW_SECS))
51 + .unwrap();
48 52 map.retain(|_, times| {
49 53 times.retain(|t| *t > cutoff);
50 54 !times.is_empty()
M pom/src/api.rs +31 -29
M pom/src/config.rs +26 -27
M pom/src/display.rs +26 -24
M pom/src/lib.rs +1 -1
M pom/src/main.rs +2 -2
M pom/src/peer.rs +41 -32
M pom/src/types.rs +12 -14