Skip to content

Commit 63fed1c

Browse files
committed
feat(deployments): streamline deployment process and enhance user experience
- Updated deployment commands to remove confirmation prompts, allowing for automatic deployments when the `--yes` flag is used. - Added documentation for deployment procedures, emphasizing the use of Bun over Node.js. - Introduced a new section in the setup command to manage the downloading of the GeoLite2-City.mmdb database, including progress feedback and error handling. - Enhanced the onboarding experience by allowing users to skip Git provider connection during setup, improving flexibility for project creation. This commit significantly improves the deployment workflow by simplifying user interactions and providing clearer guidance during setup and deployment processes.
1 parent 2e124f6 commit 63fed1c

File tree

13 files changed

+711
-84
lines changed

13 files changed

+711
-84
lines changed

apps/temps-cli/CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
44
alwaysApply: false
55
---
66

7+
## Deployment
8+
9+
When deploying this application, do not ask for confirmation. Proceed with deployments automatically.
10+
11+
## Bun Usage
12+
713
Default to using Bun instead of Node.js.
814

915
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`

apps/temps-cli/src/commands/deploy/deploy-image.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { setupClient, client } from '../../lib/api-client.js'
33
import { watchDeployment } from '../../lib/deployment-watcher.jsx'
44
import { getProjectBySlug, getProject, getEnvironments } from '../../api/sdk.gen.js'
55
import type { EnvironmentResponse } from '../../api/types.gen.js'
6-
import { promptSelect, promptConfirm, promptText } from '../../ui/prompts.js'
6+
import { promptSelect, promptText } from '../../ui/prompts.js'
77
import {
88
startSpinner,
99
succeedSpinner,
@@ -212,19 +212,6 @@ export async function deployImage(options: DeployImageOptions): Promise<void> {
212212
)
213213
newline()
214214

215-
// Confirm deployment (skip if --yes flag)
216-
if (!options.yes) {
217-
const confirmed = await promptConfirm({
218-
message: 'Start deployment?',
219-
default: true,
220-
})
221-
222-
if (!confirmed) {
223-
info('Deployment cancelled')
224-
return
225-
}
226-
}
227-
228215
// Trigger deployment
229216
startSpinner('Starting deployment...')
230217

apps/temps-cli/src/commands/deploy/deploy-static.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { setupClient, client } from '../../lib/api-client.js'
33
import { watchDeployment } from '../../lib/deployment-watcher.jsx'
44
import { getProjectBySlug, getProject, getEnvironments } from '../../api/sdk.gen.js'
55
import type { EnvironmentResponse } from '../../api/types.gen.js'
6-
import { promptSelect, promptConfirm } from '../../ui/prompts.js'
6+
import { promptSelect } from '../../ui/prompts.js'
77
import {
88
startSpinner,
99
succeedSpinner,
@@ -221,19 +221,6 @@ export async function deployStatic(options: DeployStaticOptions): Promise<void>
221221
)
222222
newline()
223223

224-
// Confirm deployment (skip if --yes flag)
225-
if (!options.yes) {
226-
const confirmed = await promptConfirm({
227-
message: 'Start deployment?',
228-
default: true,
229-
})
230-
231-
if (!confirmed) {
232-
info('Deployment cancelled')
233-
return
234-
}
235-
}
236-
237224
// Prepare the file for upload
238225
startSpinner('Preparing static bundle...')
239226

apps/temps-cli/src/commands/deploy/deploy.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
getLastDeployment,
88
} from '../../api/sdk.gen.js'
99
import type { EnvironmentResponse, DeploymentResponse } from '../../api/types.gen.js'
10-
import { promptSelect, promptText, promptConfirm } from '../../ui/prompts.js'
10+
import { promptSelect, promptText } from '../../ui/prompts.js'
1111
import { startSpinner, succeedSpinner, failSpinner, updateSpinner } from '../../ui/spinner.js'
1212
import { success, info, warning, newline, icons, colors, header, keyValue, box } from '../../ui/output.js'
1313

@@ -134,18 +134,6 @@ export async function deploy(options: DeployOptions): Promise<void> {
134134
)
135135
newline()
136136

137-
if (!options.yes) {
138-
const confirmed = await promptConfirm({
139-
message: 'Start deployment?',
140-
default: true,
141-
})
142-
143-
if (!confirmed) {
144-
info('Deployment cancelled')
145-
return
146-
}
147-
}
148-
149137
// Start deployment
150138
startSpinner('Starting deployment...')
151139

crates/temps-auth/src/temps_middleware.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ impl AuthMiddleware {
118118
let host_without_port = host.split(':').next().unwrap_or(host);
119119

120120
// Log for debugging
121-
tracing::info!(
121+
tracing::debug!(
122122
"Auth middleware: host={}, host_without_port={}, demo_mode_header={}, path={}",
123123
host,
124124
host_without_port,
@@ -135,7 +135,7 @@ impl AuthMiddleware {
135135
.check_demo_mode(host_without_port, demo_mode_header, demo_user_id)
136136
.await
137137
{
138-
tracing::info!(
138+
tracing::debug!(
139139
"Demo mode: authenticated as user {} (id={})",
140140
demo_user.email,
141141
demo_user.id
@@ -409,7 +409,7 @@ impl AuthMiddleware {
409409

410410
// Check if demo mode header is set by proxy (proxy already validated the host)
411411
if demo_mode_header {
412-
tracing::info!(
412+
tracing::debug!(
413413
"Demo mode detected via X-Temps-Demo-Mode header (host: {})",
414414
host_without_port
415415
);
@@ -432,7 +432,7 @@ impl AuthMiddleware {
432432
return None;
433433
}
434434

435-
tracing::info!(
435+
tracing::debug!(
436436
"Demo mode detected: host {} matches expected demo host {}",
437437
host_without_port,
438438
expected_demo_host
@@ -449,7 +449,7 @@ impl AuthMiddleware {
449449
} else {
450450
crate::permissions::Role::User
451451
};
452-
tracing::info!(
452+
tracing::debug!(
453453
"Demo mode: authenticated as selected user (id={}, email={}, role={:?})",
454454
user.id,
455455
user.email,
@@ -473,7 +473,7 @@ impl AuthMiddleware {
473473
// Default: Find or create demo user
474474
match self.user_service.find_or_create_demo_user().await {
475475
Ok(demo_user) => {
476-
tracing::info!(
476+
tracing::debug!(
477477
"Demo mode: auto-authenticated as demo user (id={}, email={})",
478478
demo_user.id,
479479
demo_user.email

crates/temps-cli/src/commands/setup.rs

Lines changed: 177 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use argon2::password_hash::{rand_core::OsRng, SaltString};
1010
use argon2::{Argon2, PasswordHasher};
1111
use clap::Args;
1212
use colored::Colorize;
13+
use indicatif::{ProgressBar, ProgressStyle};
1314
use rand::Rng;
1415
use rustls::crypto::CryptoProvider;
1516
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
@@ -211,6 +212,11 @@ pub struct SetupCommand {
211212
/// Used when running behind a reverse proxy or load balancer
212213
#[arg(long, env = "TEMPS_EXTERNAL_URL")]
213214
pub external_url: Option<String>,
215+
216+
/// Skip downloading GeoLite2-City.mmdb database from GitHub
217+
/// By default, the database is downloaded automatically if not present
218+
#[arg(long, default_value = "false")]
219+
pub skip_geolite2_download: bool,
214220
}
215221

216222
fn generate_secure_password() -> String {
@@ -680,20 +686,151 @@ fn ask_confirmation(prompt: &str) -> anyhow::Result<bool> {
680686
Ok(response == "y" || response == "yes")
681687
}
682688

683-
/// Check if GeoLite2-City.mmdb exists and warn if missing
684-
/// This database is required to run the application but not for setup
685-
fn check_geolite2_database(data_dir: &PathBuf) {
689+
/// URL for downloading GeoLite2-City.mmdb from GitHub
690+
const GEOLITE2_DOWNLOAD_URL: &str =
691+
"https://raw.githubusercontent.com/gotempsh/temps/refs/heads/main/crates/temps-cli/GeoLite2-City.mmdb";
692+
693+
/// Download GeoLite2-City.mmdb from GitHub with progress bar
694+
async fn download_geolite2_database(data_dir: &PathBuf) -> anyhow::Result<()> {
695+
use futures::StreamExt;
696+
697+
let geo_db_path = data_dir.join("GeoLite2-City.mmdb");
698+
699+
println!();
700+
println!(
701+
" {} {}",
702+
"📥".bright_cyan(),
703+
"Downloading GeoLite2-City.mmdb...".bright_white()
704+
);
705+
println!(
706+
" {} {}",
707+
"🔗".bright_blue(),
708+
GEOLITE2_DOWNLOAD_URL.bright_cyan()
709+
);
710+
println!();
711+
712+
// Create HTTP client
713+
let client = reqwest::Client::new();
714+
let response = client
715+
.get(GEOLITE2_DOWNLOAD_URL)
716+
.send()
717+
.await
718+
.map_err(|e| anyhow::anyhow!("Failed to start download: {}", e))?;
719+
720+
// Check response status
721+
if !response.status().is_success() {
722+
return Err(anyhow::anyhow!(
723+
"Failed to download GeoLite2 database: HTTP {}",
724+
response.status()
725+
));
726+
}
727+
728+
// Get content length for progress bar
729+
let total_size = response.content_length().unwrap_or(0);
730+
731+
// Create progress bar with beautiful styling
732+
let pb = if total_size > 0 {
733+
let pb = ProgressBar::new(total_size);
734+
pb.set_style(
735+
ProgressStyle::default_bar()
736+
.template(" {spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}) {msg}")
737+
.unwrap()
738+
.progress_chars("█▓▒░ "),
739+
);
740+
pb.set_message("downloading...");
741+
pb
742+
} else {
743+
// Unknown size - use spinner
744+
let pb = ProgressBar::new_spinner();
745+
pb.set_style(
746+
ProgressStyle::default_spinner()
747+
.template(" {spinner:.green} [{elapsed_precise}] {bytes} ({bytes_per_sec}) {msg}")
748+
.unwrap(),
749+
);
750+
pb.set_message("downloading...");
751+
pb
752+
};
753+
754+
// Create temporary file for download
755+
let temp_path = geo_db_path.with_extension("mmdb.tmp");
756+
let mut file = fs::File::create(&temp_path)
757+
.map_err(|e| anyhow::anyhow!("Failed to create temporary file: {}", e))?;
758+
759+
// Stream the download with progress updates
760+
let mut stream = response.bytes_stream();
761+
let mut downloaded: u64 = 0;
762+
763+
while let Some(chunk_result) = stream.next().await {
764+
let chunk = chunk_result.map_err(|e| anyhow::anyhow!("Download error: {}", e))?;
765+
766+
use std::io::Write;
767+
file.write_all(&chunk)
768+
.map_err(|e| anyhow::anyhow!("Failed to write to file: {}", e))?;
769+
770+
downloaded += chunk.len() as u64;
771+
pb.set_position(downloaded);
772+
}
773+
774+
// Finish progress bar
775+
pb.finish_with_message("complete!");
776+
777+
// Rename temp file to final destination
778+
fs::rename(&temp_path, &geo_db_path)
779+
.map_err(|e| anyhow::anyhow!("Failed to finalize download: {}", e))?;
780+
781+
println!();
782+
println!(
783+
" {} {} {}",
784+
"✅".bright_green(),
785+
"Downloaded to:".bright_white(),
786+
geo_db_path.display().to_string().bright_cyan()
787+
);
788+
789+
// Verify file size is reasonable (> 1MB, typical size is ~60MB)
790+
let metadata = fs::metadata(&geo_db_path)?;
791+
let size_mb = metadata.len() as f64 / 1024.0 / 1024.0;
792+
println!(
793+
" {} {} {:.1} MB",
794+
"📊".bright_blue(),
795+
"File size:".bright_white(),
796+
size_mb
797+
);
798+
799+
if metadata.len() < 1_000_000 {
800+
warn!(
801+
"Downloaded file seems too small ({} bytes), may be corrupted",
802+
metadata.len()
803+
);
804+
println!(
805+
" {} {}",
806+
"⚠️ ".bright_yellow(),
807+
"Warning: File seems small, may be corrupted".bright_yellow()
808+
);
809+
}
810+
811+
println!();
812+
813+
Ok(())
814+
}
815+
816+
/// Check if GeoLite2-City.mmdb exists
817+
/// Returns true if database exists, false if it needs to be downloaded
818+
fn check_geolite2_database(data_dir: &PathBuf) -> bool {
686819
let geo_db_path = data_dir.join("GeoLite2-City.mmdb");
687820
let current_dir_path = PathBuf::from("./GeoLite2-City.mmdb");
688821

689822
// Check both locations
690823
if geo_db_path.exists() || current_dir_path.exists() {
691824
print_success("GeoLite2 database found");
692-
return;
825+
return true;
693826
}
694827

695-
// Database not found - show warning with download instructions
696-
print_warning("GeoLite2 database not found");
828+
false
829+
}
830+
831+
/// Show warning when GeoLite2 database is missing and download was skipped
832+
fn show_geolite2_missing_warning(data_dir: &PathBuf) {
833+
print_warning("GeoLite2 database not found (download skipped)");
697834
println!();
698835
println!(
699836
"{}",
@@ -709,10 +846,17 @@ fn check_geolite2_database(data_dir: &PathBuf) {
709846
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".bright_yellow()
710847
);
711848
println!();
849+
println!(
850+
" {} {} {}",
851+
"💡".bright_yellow(),
852+
"Quick fix:".bright_white().bold(),
853+
"Run setup without --skip-geolite2-download".bright_cyan()
854+
);
855+
println!();
712856
println!(
713857
" {} {}",
714858
"📥".bright_cyan(),
715-
"Download instructions:".bright_white().bold()
859+
"Or download manually:".bright_white().bold()
716860
);
717861
println!(
718862
" {}",
@@ -1013,12 +1157,36 @@ impl SetupCommand {
10131157

10141158
print_success("Encryption key configured");
10151159

1016-
// Check for GeoLite2 database (warning only - not required for setup)
1017-
check_geolite2_database(&data_dir);
1160+
// Check for GeoLite2 database
1161+
let geolite2_exists = check_geolite2_database(&data_dir);
10181162

10191163
// Create tokio runtime for async operations
10201164
let rt = tokio::runtime::Runtime::new()?;
10211165

1166+
// Download GeoLite2 database if not present (unless skipped)
1167+
if !geolite2_exists {
1168+
if self.skip_geolite2_download {
1169+
// User explicitly skipped download - show warning
1170+
show_geolite2_missing_warning(&data_dir);
1171+
} else {
1172+
// Download by default
1173+
print_section("GeoLite2 Database Download");
1174+
match rt.block_on(download_geolite2_database(&data_dir)) {
1175+
Ok(()) => {
1176+
print_success("GeoLite2 database downloaded successfully");
1177+
}
1178+
Err(e) => {
1179+
print_error(&format!("Failed to download GeoLite2 database: {}", e));
1180+
println!(
1181+
" {} You can manually download the database later.",
1182+
"ℹ️ ".bright_blue()
1183+
);
1184+
println!();
1185+
}
1186+
}
1187+
}
1188+
}
1189+
10221190
// Establish database connection (this also runs migrations)
10231191
print_section("Database Setup");
10241192
print_substep("Checking database connectivity...");

0 commit comments

Comments
 (0)