Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
- The goal of this repository is to create the wavedash cli, which helps game developers upload their assets to the site (similar to steams "steampipe" cli).

## Development
- Environment variables are managed by Doppler. Always use `doppler run --` as a prefix when running cargo commands (build, check, clippy, run, test, etc.). For example: `doppler run -- cargo check`, `doppler run -- cargo clippy`.
- Environment variables are managed by Doppler. Always use `doppler run --` as a prefix when running cargo commands (build, check, clippy, run, test, etc.). For example: `doppler run -- cargo check`, `doppler run -- cargo clippy`.
- To run the CLI locally, use `doppler run -- cargo run <command>`. For example: `doppler run -- cargo run build push`.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dialoguer = "0.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
toml_edit = "0.22"

# Error Handling
anyhow = "1.0"
Expand Down
7 changes: 7 additions & 0 deletions src/builds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ pub async fn handle_build_push(config_path: PathBuf, verbose: bool, message: Opt
anyhow::bail!("Source must be a directory: {}", upload_dir.display());
}

println!(
"Pushing v{} to {} ...",
wavedash_config.version, wavedash_config.branch
);

// Get temporary R2 credentials
let engine_kind = wavedash_config.engine_type()?;
let creds = get_temp_credentials(
Expand Down Expand Up @@ -214,5 +219,7 @@ pub async fn handle_build_push(config_path: PathBuf, verbose: bool, message: Opt
)
.await?;

println!("Build ID: {}", creds.game_build_id);

Ok(())
}
55 changes: 55 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,61 @@ impl WavedashConfig {
Ok(config)
}

pub fn display_summary(&self) -> String {
let engine_line = if let Some(ref godot) = self.godot {
format!("engine = godot (v{})", godot.version)
} else if let Some(ref unity) = self.unity {
format!("engine = unity (v{})", unity.version)
} else if let Some(ref custom) = self.custom {
format!(
"engine = custom (v{}, entrypoint: {})",
custom.version, custom.entrypoint
)
} else {
"engine = (none)".to_string()
};

format!(
"game_id = {}\n\
branch = {}\n\
upload_dir = {}\n\
version = {}\n\
{}",
self.game_id,
self.branch,
self.upload_dir.display(),
self.version,
engine_line,
)
}

/// Update a top-level field in the TOML file using toml_edit for lossless editing.
pub fn update_field(config_path: &PathBuf, key: &str, value: &str) -> Result<String> {
let content = std::fs::read_to_string(config_path).map_err(|e| {
anyhow::anyhow!(
"Failed to read config file at {}: {}",
config_path.display(),
e
)
})?;

let mut doc = content
.parse::<toml_edit::DocumentMut>()
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;

let old_value = doc
.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| anyhow::anyhow!("Key '{}' not found in config", key))?;

doc[key] = toml_edit::value(value);

std::fs::write(config_path, doc.to_string())?;

Ok(old_value)
}

pub fn engine_type(&self) -> Result<EngineKind> {
match (
self.godot.is_some(),
Expand Down
47 changes: 47 additions & 0 deletions src/config_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use anyhow::Result;
use std::path::PathBuf;

use crate::config::WavedashConfig;

pub fn handle_config_show(config_path: PathBuf) -> Result<()> {
let config = WavedashConfig::load(&config_path)?;
println!("{}", config.display_summary());
Ok(())
}

pub fn handle_config_set(config_path: PathBuf, key: String, value: String) -> Result<()> {
let supported_keys = ["branch", "version", "upload_dir", "game_id"];
if !supported_keys.contains(&key.as_str()) {
anyhow::bail!(
"Unsupported key '{}'. Supported keys: {}",
key,
supported_keys.join(", ")
);
}

// Validate
match key.as_str() {
"version" => {
let parts: Vec<&str> = value.split('.').collect();
if parts.len() != 3 || parts.iter().any(|p| p.parse::<u32>().is_err()) {
anyhow::bail!("Version must be in major.minor.patch format (e.g. 1.2.3)");
}
}
"game_id" => {
if value.is_empty() {
anyhow::bail!("game_id must be non-empty");
}
}
"upload_dir" => {
let path = PathBuf::from(&value);
if !path.exists() {
eprintln!("Warning: directory '{}' does not exist yet", value);
}
}
_ => {}
}

let old_value = WavedashConfig::update_field(&config_path, &key, &value)?;
println!("Updated {}: {} → {}", key, old_value, value);
Ok(())
}
59 changes: 59 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod auth;
mod builds;
mod config;
mod config_cmd;
mod dev;
mod file_staging;
mod init;
mod updater;
mod version_cmd;

use anyhow::Result;
use auth::{login_with_browser, AuthManager, AuthSource};
Expand Down Expand Up @@ -69,6 +71,18 @@ enum Commands {
#[arg(long = "no-open", help = "Don't open the sandbox URL in the browser")]
no_open: bool,
},
#[command(about = "Show or update wavedash.toml configuration")]
Config {
#[command(subcommand)]
action: Option<ConfigCommands>,
#[arg(
short = 'c',
long = "config",
help = "Path to wavedash.toml config file",
default_value = "./wavedash.toml"
)]
config: PathBuf,
},
#[command(about = "Initialize a new wavedash.toml config file")]
Init {
#[arg(long)]
Expand All @@ -80,6 +94,18 @@ enum Commands {
},
#[command(about = "Check for and install updates")]
Update,
#[command(about = "Version management")]
Version {
#[command(subcommand)]
action: VersionCommands,
#[arg(
short = 'c',
long = "config",
help = "Path to wavedash.toml config file",
default_value = "./wavedash.toml"
)]
config: PathBuf,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -107,6 +133,26 @@ enum BuildCommands {
},
}

#[derive(Subcommand)]
enum ConfigCommands {
#[command(about = "Set a config field value")]
Set {
#[arg(help = "Config key (branch, version, upload_dir, game_id)")]
key: String,
#[arg(help = "New value")]
value: String,
},
}

#[derive(Subcommand)]
enum VersionCommands {
#[command(about = "Bump the version (patch, minor, or major)")]
Bump {
#[arg(value_enum, default_value = "patch")]
level: version_cmd::BumpLevel,
},
}

#[tokio::main]
async fn main() -> Result<()> {
// Install rustls crypto provider for TLS
Expand Down Expand Up @@ -180,6 +226,14 @@ async fn main() -> Result<()> {
Commands::Dev { config, no_open } => {
handle_dev(config, cli.verbose, no_open).await?;
}
Commands::Config { action, config } => match action {
Some(ConfigCommands::Set { key, value }) => {
config_cmd::handle_config_set(config, key, value)?;
}
None => {
config_cmd::handle_config_show(config)?;
}
},
Commands::Init {
game_id,
branch,
Expand All @@ -190,6 +244,11 @@ async fn main() -> Result<()> {
Commands::Update => {
updater::run_update().await?;
}
Commands::Version { action, config } => match action {
VersionCommands::Bump { level } => {
version_cmd::handle_version_bump(config, level)?;
}
},
}

// Wait for background update check to complete
Expand Down
43 changes: 43 additions & 0 deletions src/version_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use anyhow::Result;
use std::path::PathBuf;

use crate::config::WavedashConfig;

#[derive(Clone, clap::ValueEnum)]
pub enum BumpLevel {
Patch,
Minor,
Major,
}

pub fn handle_version_bump(config_path: PathBuf, level: BumpLevel) -> Result<()> {
let config = WavedashConfig::load(&config_path)?;
let old_version = &config.version;

let parts: Vec<u32> = old_version
.split('.')
.map(|p| {
p.parse::<u32>()
.map_err(|_| anyhow::anyhow!("Invalid version format: {}", old_version))
})
.collect::<Result<Vec<_>>>()?;

if parts.len() != 3 {
anyhow::bail!(
"Version must be in major.minor.patch format, got: {}",
old_version
);
}

let (major, minor, patch) = (parts[0], parts[1], parts[2]);

let new_version = match level {
BumpLevel::Patch => format!("{}.{}.{}", major, minor, patch + 1),
BumpLevel::Minor => format!("{}.{}.0", major, minor + 1),
BumpLevel::Major => format!("{}.0.0", major + 1),
};

WavedashConfig::update_field(&config_path, "version", &new_version)?;
println!("Bumped version: {} → {}", old_version, new_version);
Ok(())
}