From 0ef743489a9a1b737f88de4cb9ed84d84ee5241a Mon Sep 17 00:00:00 2001 From: The64thRealm <36716817+The64thRealm@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:19:17 -0700 Subject: [PATCH] cli attaches upload source --- src/builds.rs | 6 ++- src/config.rs | 27 ++++++++++ src/dev/mod.rs | 15 ++++-- src/main.rs | 137 +++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 176 insertions(+), 9 deletions(-) diff --git a/src/builds.rs b/src/builds.rs index aa86651..c47c21a 100644 --- a/src/builds.rs +++ b/src/builds.rs @@ -1,5 +1,5 @@ use crate::auth::AuthManager; -use crate::config::{self, WavedashConfig}; +use crate::config::{self, UploadSource, WavedashConfig}; use crate::file_staging::FileStaging; use anyhow::Result; use serde::{Deserialize, Serialize}; @@ -43,6 +43,7 @@ struct BuildUploadInfo<'a> { entrypoint_params: Option, message: Option<&'a str>, build_size_bytes: u64, + upload_source: UploadSource, } async fn get_temp_credentials( @@ -59,6 +60,7 @@ async fn get_temp_credentials( let mut request_body = serde_json::json!({ "buildSizeBytes": info.build_size_bytes, + "uploadSource": info.upload_source.as_label(), }); if let Some(eng) = info.engine { @@ -133,6 +135,7 @@ pub async fn handle_build_push( config_path: PathBuf, verbose: bool, message: Option, + upload_source: UploadSource, ) -> Result<()> { // Load wavedash.toml config let wavedash_config = WavedashConfig::load(&config_path)?; @@ -177,6 +180,7 @@ pub async fn handle_build_push( entrypoint_params: wavedash_config.executable_entrypoint_params()?, message: message.as_deref(), build_size_bytes: total_bytes, + upload_source, }, &api_key, ) diff --git a/src/config.rs b/src/config.rs index 0686a3f..5d0a336 100644 --- a/src/config.rs +++ b/src/config.rs @@ -29,6 +29,7 @@ //! project config at all. use anyhow::Result; +use clap::ValueEnum; use colored::Colorize; use directories::BaseDirs; use serde::Deserialize; @@ -478,6 +479,25 @@ impl EngineKind { } } +// The API's third source, `WEB`, has no variant here: it's set server-side and +// rejected from any client that claims it. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] +pub enum UploadSource { + #[default] + #[value(skip)] + Cli, + GodotPlugin, +} + +impl UploadSource { + pub fn as_label(&self) -> &'static str { + match self { + UploadSource::Cli => "CLI", + UploadSource::GodotPlugin => "GODOT_PLUGIN", + } + } +} + /// Resolve a game_id from, in precedence order: the `--game-id` flag, /// `WAVEDASH_GAME_ID`, then `game_id` in the wavedash.toml at `config_path`. /// Errors include the config path so the user knows which file we tried to read. @@ -1805,4 +1825,11 @@ mod tests { Some(serde_json::json!({ "executable": "game.swf" })) ); } + + #[test] + fn upload_source_labels_are_the_ones_the_api_accepts() { + assert_eq!(UploadSource::default(), UploadSource::Cli); + assert_eq!(UploadSource::Cli.as_label(), "CLI"); + assert_eq!(UploadSource::GodotPlugin.as_label(), "GODOT_PLUGIN"); + } } diff --git a/src/dev/mod.rs b/src/dev/mod.rs index 27b13d5..3a5b0f5 100644 --- a/src/dev/mod.rs +++ b/src/dev/mod.rs @@ -6,7 +6,7 @@ use serde::Deserialize; use walkdir::WalkDir; use crate::auth::{generate_state, AuthManager}; -use crate::config::{self, EngineKind, WavedashConfig}; +use crate::config::{self, EngineKind, UploadSource, WavedashConfig}; use crate::file_staging::FileStaging; mod server; @@ -23,13 +23,16 @@ async fn create_local_build( engine: Option<&str>, engine_version: Option<&str>, entrypoint: Option<&str>, + upload_source: UploadSource, api_key: &str, ) -> Result { let client = config::create_http_client()?; let api_host = config::get("api_host")?; let url = format!("{}/api/games/{}/builds/create-local", api_host, game_id); - let mut request_body = serde_json::json!({}); + let mut request_body = serde_json::json!({ + "uploadSource": upload_source.as_label(), + }); if let Some(eng) = engine { request_body["engine"] = serde_json::json!(eng); } @@ -54,7 +57,12 @@ async fn create_local_build( const DEFAULT_CONFIG: &str = "./wavedash.toml"; -pub async fn handle_dev(config_path: Option, verbose: bool, no_open: bool) -> Result<()> { +pub async fn handle_dev( + config_path: Option, + verbose: bool, + no_open: bool, + upload_source: UploadSource, +) -> Result<()> { let auth_manager = AuthManager::new()?; let api_key = auth_manager .get_api_key() @@ -128,6 +136,7 @@ pub async fn handle_dev(config_path: Option, verbose: bool, no_open: bo engine_kind.map(|e| e.as_label()), wavedash_config.engine_version()?, entrypoint.as_deref(), + upload_source, &api_key, ) .await?; diff --git a/src/main.rs b/src/main.rs index 37e350d..0f16c07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ use builds::handle_build_push; use clap::{Parser, Subcommand}; use clear_playtest_data::{handle_clear_playtest_data, ClearPlaytestDataArgs}; use colored::Colorize; -use config::resolve_game_id; +use config::{resolve_game_id, UploadSource}; use dev::handle_dev; use init::{ handle_init, handle_project_create, handle_project_list, handle_team_create, handle_team_list, @@ -87,6 +87,13 @@ enum Commands { help = "Don't automatically open the browser; just print the local URL" )] no_open: bool, + #[arg( + long = "upload-source", + value_enum, + hide = true, + help = "Attribute the build to the tool running the CLI instead of the CLI itself" + )] + upload_source: Option, }, #[command( about = "Publish an uploaded build to wavedash.com", @@ -245,6 +252,13 @@ enum BuildCommands { config: PathBuf, #[arg(short = 'm', long = "message", help = "Build message")] message: Option, + #[arg( + long = "upload-source", + value_enum, + hide = true, + help = "Attribute the build to the tool running the CLI instead of the CLI itself" + )] + upload_source: Option, }, } @@ -588,12 +602,32 @@ async fn run() -> Result<()> { } } Commands::Build { action } => match action { - BuildCommands::Push { config, message } => { - handle_build_push(config, cli.verbose, message).await?; + BuildCommands::Push { + config, + message, + upload_source, + } => { + handle_build_push( + config, + cli.verbose, + message, + upload_source.unwrap_or_default(), + ) + .await?; } }, - Commands::Dev { config, no_open } => { - handle_dev(config, cli.verbose, no_open).await?; + Commands::Dev { + config, + no_open, + upload_source, + } => { + handle_dev( + config, + cli.verbose, + no_open, + upload_source.unwrap_or_default(), + ) + .await?; } Commands::Publish { config, @@ -832,4 +866,97 @@ mod tests { checked ); } + + #[test] + fn upload_source_is_hidden_and_only_offers_the_godot_plugin() { + fn walk(cmd: &clap::Command, path: &[String], found: &mut Vec) { + if let Some(arg) = cmd + .get_arguments() + .find(|arg| arg.get_long() == Some("upload-source")) + { + let command = path.join(" "); + assert!( + arg.is_hide_set(), + "`{}` lists --upload-source in its help", + command + ); + let values: Vec = arg + .get_possible_values() + .into_iter() + .map(|value| value.get_name().to_string()) + .collect(); + assert_eq!( + values, + ["godot-plugin"], + "`{}` offers a source other than the Godot plugin's", + command + ); + found.push(command); + } + + for sub in cmd.get_subcommands() { + let mut sub_path = path.to_vec(); + sub_path.push(sub.get_name().to_string()); + walk(sub, &sub_path, found); + } + } + + let mut found = Vec::new(); + walk(&Cli::command(), &["wavedash".to_string()], &mut found); + found.sort(); + + assert_eq!( + found, + ["wavedash build push", "wavedash dev"], + "every command that creates a build row should be able to name its source" + ); + } + + #[test] + fn upload_source_parses_the_plugin_and_defaults_to_the_cli() { + fn push_source(argv: &[&str]) -> Option { + let parsed = Cli::try_parse_from(argv).expect("should parse"); + let Some(Commands::Build { + action: BuildCommands::Push { upload_source, .. }, + }) = parsed.command + else { + panic!("`{:?}` did not parse as `build push`", argv); + }; + upload_source + } + + assert_eq!( + push_source(&[ + "wavedash", + "build", + "push", + "--upload-source", + "godot-plugin" + ]), + Some(UploadSource::GodotPlugin) + ); + assert_eq!(push_source(&["wavedash", "build", "push"]), None); + assert_eq!( + push_source(&["wavedash", "build", "push"]).unwrap_or_default(), + UploadSource::Cli + ); + + // Not `try_parse_from`: `expect_err` needs the Ok type to be `Debug`, and + // `Cli` isn't — the fix is here, not a `derive(Debug)` on `Cli`. + for rejected in ["cli", "CLI", "web", "WEB", "godot", "GODOT_PLUGIN", ""] { + let err = Cli::command() + .try_get_matches_from(["wavedash", "build", "push", "--upload-source", rejected]) + .expect_err(&format!( + "`build push` accepted --upload-source {:?}", + rejected + )); + assert_eq!( + err.kind(), + clap::error::ErrorKind::InvalidValue, + "--upload-source {:?} was rejected for the wrong reason: {}", + rejected, + err + ); + } + } }