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
864 changes: 667 additions & 197 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ name = "ag"
path = "src/main.rs"

[dependencies]
clap = { version = "4.5.48", features = ["derive"] }
clap = { version = "4.5.48", features = ["derive", "env"] }
reqwest = { version = "0.11", features = ["json", "multipart", "stream"] }
zip = "0.6"
tokio = { version = "1.0", features = ["full"] }
tokio = { version = "1.40", features = ["full"] }
tempfile = "3.0"
chrono = "0.4"
indicatif = "0.17"
futures-util = "0.3"
open = "5.3.2"
axum = { version = "0.8.6", features = ["json"] }
tower-http = { version = "0.5", features = ["cors"] }
urlencoding = "2.1.3"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
jsonwebtoken = {version = "10.2.0", features = ["aws_lc_rs"] }
thiserror = "2.0.17"
6 changes: 4 additions & 2 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod pipeline;
pub mod project;
pub mod status;
pub mod system;
pub mod user;

pub use pipeline::{PipelineAction, handle_pipeline_command};
pub use project::{ProjectAction, handle_project_command};
pub use status::{StatusAction, handle_status_command};
pub use system::SystemAction;
pub use user::UserAction;
175 changes: 69 additions & 106 deletions src/commands/status.rs → src/commands/system.rs
Original file line number Diff line number Diff line change
@@ -1,86 +1,21 @@
//! Status command implementation
//!
//! This module provides functionality to check the status of installed binaries
//! and other system information.
use std::fs;

use crate::utils::{AppConfig, get_binaries_status, get_binary_version_by_name};
use clap::Subcommand;
use std::fs;

/// Status-related subcommands
use crate::utils::{AppConfig, get_binaries_status, get_binary_version_by_name};

/// System-related subcommands
#[derive(Subcommand, Debug)]
pub enum StatusAction {
/// Show status of all installed binaries
Binaries,
pub enum SystemAction {
/// Show overall system status
System,
}

/// Handle status commands
pub async fn handle_status_command(action: StatusAction, config: &AppConfig) {
match action {
StatusAction::Binaries => show_binaries_status(config).await,
StatusAction::System => show_system_status(config).await,
}
Status,
}

/// Display the status of all managed binaries
async fn show_binaries_status(config: &AppConfig) {
println!("Binary Status");
println!("=============");
println!();

let bin_dir = config.agnostic_dir.join("bin");
let binaries = get_binaries_status(&bin_dir);

if binaries.is_empty() {
println!("No managed binaries found.");
return;
}

for binary in &binaries {
let status_icon = if binary.is_ready() {
"[READY]"
} else {
"[MISSING]"
};
let size_info = match binary.size {
Some(size) => format_file_size(size),
None => "N/A".to_string(),
};

println!("{} {}", status_icon, binary.name);
println!(" Path: {}", binary.path.display());
println!(" Exists: {}", if binary.exists { "Yes" } else { "No" });
println!(
" Executable: {}",
if binary.executable { "Yes" } else { "No" }
);
println!(" Size: {}", size_info);

// Show version info for ready binaries
if binary.is_ready() {
let bin_dir = &config.agnostic_dir.join("bin");
match get_binary_version_by_name(&binary.name, bin_dir).await {
Ok(version) => println!(" Version: {}", version),
Err(_) => println!(" Version: Unknown"),
}
impl SystemAction {
pub async fn handle(self, config: &AppConfig) {
match self {
Self::Status => show_system_status(config).await,
}

println!();
}

// Summary
let ready_count = binaries.iter().filter(|b| b.is_ready()).count();
let total_count = binaries.len();

if ready_count == total_count {
println!("All {} binaries are ready", total_count);
} else {
println!(
"Warning: {} of {} binaries are ready",
ready_count, total_count
);
}
}

Expand Down Expand Up @@ -109,50 +44,24 @@ async fn show_system_status(config: &AppConfig) {

// Subdirectories
println!("Subdirectories");
let subdirs = ["bin"];
let subdirs = ["bin", "user"];
for subdir in subdirs {
let path = config.agnostic_dir.join(subdir);
let exists = path.exists();
let status = if exists { "[EXISTS]" } else { "[MISSING]" };

println!(" {} {} - {}", status, subdir, path.display());

if exists {
if let Ok(entries) = fs::read_dir(&path) {
let count = entries.count();
println!(" Items: {}", count);
}
if exists && let Ok(entries) = fs::read_dir(&path) {
let count = entries.count();
println!(" Items: {}", count);
}
}
println!();

// Binary status summary
println!("Binary Dependencies");
let bin_dir = config.agnostic_dir.join("bin");
let binaries = get_binaries_status(&bin_dir);
let ready_count = binaries.iter().filter(|b| b.is_ready()).count();
let total_count = binaries.len();

if total_count == 0 {
println!(" No binaries managed");
} else if ready_count == total_count {
println!(" All {} binaries ready", total_count);
} else {
println!(
" Warning: {} of {} binaries ready",
ready_count, total_count
);
}

for binary in binaries {
let status = if binary.is_ready() {
"Ready"
} else {
"Not Ready"
};
println!(" {} - {}", binary.name, status);
}
println!();
show_binaries_status(config).await;

// System information
println!("System Information");
Expand All @@ -169,6 +78,60 @@ async fn show_system_status(config: &AppConfig) {
}
}

/// Display the status of all managed binaries
async fn show_binaries_status(config: &AppConfig) {
let bin_dir = config.agnostic_dir.join("bin");
let binaries = get_binaries_status(&bin_dir);

if binaries.is_empty() {
println!("No managed binaries found.");
return;
}

for binary in &binaries {
let status_icon = if binary.is_ready() {
"[READY]"
} else {
"[MISSING]"
};
let size_info = match binary.size {
Some(size) => format_file_size(size),
None => "N/A".to_string(),
};

println!(" {} {}", status_icon, binary.name);
println!(" Path: {}", binary.path.display());
println!(" Exists: {}", if binary.exists { "Yes" } else { "No" });
println!(
" Executable: {}",
if binary.executable { "Yes" } else { "No" }
);
println!(" Size: {}", size_info);

// Show version info for ready binaries
if binary.is_ready() {
let bin_dir = &config.agnostic_dir.join("bin");
match get_binary_version_by_name(&binary.name, bin_dir).await {
Ok(version) => println!(" Version: {}", version),
Err(_) => println!(" Version: Unknown"),
}
}

println!();
}

// Summary
let ready_count = binaries.iter().filter(|b| b.is_ready()).count();
let total_count = binaries.len();

if ready_count != total_count {
println!(
"Warning: {} of {} binaries are ready",
ready_count, total_count
);
}
}

/// Format file size in human-readable format
fn format_file_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
Expand Down
98 changes: 98 additions & 0 deletions src/commands/user/login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::sync::Arc;

use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::post};
use open::that;
use tokio::{net::TcpListener, sync::watch};

use crate::{commands::UserAction, utils::AppConfig, utils::AuthTokens};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ShutdownSignal {
NotTriggered,
Triggered,
}

struct LoginAppState {
config: AppConfig,
shutdown_tx: watch::Sender<ShutdownSignal>,
}

impl UserAction {
pub(super) async fn handle_login(
self,
config: &AppConfig,
) -> Result<(), Box<dyn std::error::Error>> {
let (shutdown_tx, mut shutdown_rx) = watch::channel(ShutdownSignal::NotTriggered);

let state = Arc::new(LoginAppState {
shutdown_tx,
config: config.clone(),
});

let listener = TcpListener::bind("127.0.0.1:0").await?;
let local_addr = listener.local_addr()?;
let port = local_addr.port();

let redirect_uri = format!("http://localhost:{}", port);
let login_url = format!(
"https://app.agnostic.tech/login?redirectTo={}",
urlencoding::encode(&redirect_uri)
);

println!("Opening browser: {}", login_url);
if let Err(e) = that(&login_url) {
if config.verbose {
eprintln!("Failed to open browser: {}", e);
}
eprintln!("Please manually open: {}", login_url);
}

// Build router with shutdown sender
let app = Router::new()
.route("/", post(handle_callback))
.layer(tower_http::cors::CorsLayer::permissive())
.with_state(state);

if config.verbose {
println!("HTTP server listening at {}", redirect_uri);
}

tokio::select! {
result = axum::serve(listener, app) => {
if let Err(e) = result {
eprintln!("Server error: {}", e);
}
}
_ = shutdown_rx.wait_for(|&signal| signal == ShutdownSignal::Triggered) => {
println!("Authentication successful!");
}
}

if config.verbose {
println!("Shutting down HTTP server.");
}

Ok(())
}
}

async fn handle_callback(
State(state): State<Arc<LoginAppState>>,
Json(payload): Json<AuthTokens>,
) -> impl IntoResponse {
if !payload.is_valid_token_type() {
eprintln!("Invalid token_type: {}", payload.token_type());
return StatusCode::BAD_REQUEST;
}

let auth_file = state.config.agnostic_dir.join("user/auth.json");
if payload.save(&auth_file).is_ok() {
if state.config.verbose {
println!("Tokens saved to {:?}", auth_file);
}
let _ = state.shutdown_tx.send(ShutdownSignal::Triggered);
return StatusCode::NO_CONTENT;
}

StatusCode::INTERNAL_SERVER_ERROR
}
17 changes: 17 additions & 0 deletions src/commands/user/logout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::{error::Error, fs};

use crate::{commands::UserAction, utils::AppConfig};

impl UserAction {
pub(super) async fn handle_logout(self, config: &AppConfig) -> Result<(), Box<dyn Error>> {
let auth_json = config.agnostic_dir.join("user/auth.json");
if auth_json.try_exists()? {
fs::remove_file(auth_json)?;
println!("auth.json file removed");
}

println!("User logged out...");

Ok(())
}
}
34 changes: 34 additions & 0 deletions src/commands/user/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
mod login;
mod logout;
mod status;
mod user;

use clap::Subcommand;

use crate::utils::AppConfig;

#[derive(Subcommand, Debug)]
pub enum UserAction {
Login,
Logout,
Status,
}

impl UserAction {
pub async fn handle(self, config: &AppConfig) {
match self {
Self::Login => self
.handle_login(config)
.await
.expect("Unable to handle login command"),
Self::Logout => self
.handle_logout(config)
.await
.expect("Unable to handle logout command"),
Self::Status => self
.handle_status(config)
.await
.expect("Unable to handle status command"),
}
}
}
Loading