@smbcloud / cli / commits / 98bfe4e

wip

Seto Elkahfi committed May 4, 2025 at 00:01 UTC 98bfe4e8782bcc4a3a3404e65534f331656c6ae5
14 files changed +200 -131
Cargo.lock
+60
@@ -1575,6 +1575,15 @@ dependencies = [
1575 "syn 2.0.101",
1576 ]
1577
1578 +[[package]]
1579 +name = "serde_spanned"
1580 +version = "0.6.8"
1581 +source = "registry+https://github.com/rust-lang/crates.io-index"
1582 +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
1583 +dependencies = [
1584 + "serde",
1585 +]
1586 +
1587 [[package]]
1588 name = "serde_urlencoded"
1589 version = "0.7.1"
@@ -1659,6 +1668,7 @@ dependencies = [
1668 "spinners",
1669 "ssh2-config",
1670 "tokio",
1671 + "toml",
1672 "tracing",
1673 "tracing-bunyan-formatter",
1674 "tracing-subscriber",
@@ -2056,6 +2066,47 @@ dependencies = [
2066 "tokio",
2067 ]
2068
2069 +[[package]]
2070 +name = "toml"
2071 +version = "0.8.22"
2072 +source = "registry+https://github.com/rust-lang/crates.io-index"
2073 +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
2074 +dependencies = [
2075 + "serde",
2076 + "serde_spanned",
2077 + "toml_datetime",
2078 + "toml_edit",
2079 +]
2080 +
2081 +[[package]]
2082 +name = "toml_datetime"
2083 +version = "0.6.9"
2084 +source = "registry+https://github.com/rust-lang/crates.io-index"
2085 +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
2086 +dependencies = [
2087 + "serde",
2088 +]
2089 +
2090 +[[package]]
2091 +name = "toml_edit"
2092 +version = "0.22.26"
2093 +source = "registry+https://github.com/rust-lang/crates.io-index"
2094 +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
2095 +dependencies = [
2096 + "indexmap",
2097 + "serde",
2098 + "serde_spanned",
2099 + "toml_datetime",
2100 + "toml_write",
2101 + "winnow",
2102 +]
2103 +
2104 +[[package]]
2105 +name = "toml_write"
2106 +version = "0.1.1"
2107 +source = "registry+https://github.com/rust-lang/crates.io-index"
2108 +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
2109 +
2110 [[package]]
2111 name = "tower"
2112 version = "0.5.2"
@@ -2676,6 +2727,15 @@ version = "0.53.0"
2727 source = "registry+https://github.com/rust-lang/crates.io-index"
2728 checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
2729
2730 +[[package]]
2731 +name = "winnow"
2732 +version = "0.7.9"
2733 +source = "registry+https://github.com/rust-lang/crates.io-index"
2734 +checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3"
2735 +dependencies = [
2736 + "memchr",
2737 +]
2738 +
2739 [[package]]
2740 name = "wit-bindgen-rt"
2741 version = "0.39.0"
smbcloud-cli/Cargo.toml
+1
@@ -38,6 +38,7 @@ smbcloud-networking-project = { workspace = true }
38 smbcloud-utils = { workspace = true }
39 spinners = { workspace = true }
40 tokio = { workspace = true, features = ["full"] }
41 +toml = { workspace = true }
42 tracing = { workspace = true, features = ["log"] }
43 tracing-bunyan-formatter = { workspace = true }
44 tracing-subscriber = { workspace = true, features = ["registry", "env-filter"] }
smbcloud-cli/src/account/lib.rs
+11 -1
@@ -9,7 +9,7 @@ use smbcloud_networking::{
9 GH_OAUTH_CLIENT_ID, GH_OAUTH_REDIRECT_HOST, GH_OAUTH_REDIRECT_PORT, PATH_AUTHORIZE,
10 },
11 environment::Environment,
12 - smb_base_url_builder,
12 + smb_base_url_builder, smb_token_file_path,
13 };
14 use spinners::Spinner;
15 use std::{
@@ -239,3 +239,13 @@ pub async fn save_token(env: Environment, response: &Response) -> Result<()> {
239 None => Err(anyhow!("Failed to get token. Probably a backend issue.")),
240 }
241 }
242 +
243 +pub async fn protected_request(env: Environment) -> Result<()> {
244 + // Check if token file exists
245 + if smb_token_file_path(env).is_none() {
246 + return Err(anyhow!(
247 + "Please authorize your account first with `smb account login` command."
248 + ));
249 + }
250 + Ok(())
251 +}
\ No newline at end of file
smbcloud-cli/src/account/login/process.rs
+12 -16
@@ -3,7 +3,7 @@ use crate::{
3 lib::{authorize_github, save_token},
4 signup::{do_signup, SignupMethod},
5 },
6 - cli::CommandResult,
6 + cli::CommandResult, ui::{fail_symbol, succeed_message, succeed_symbol},
7 };
8 use anyhow::{anyhow, Result};
9 use console::{style, Term};
@@ -36,7 +36,7 @@ pub async fn process_login(env: Environment) -> Result<CommandResult> {
36 spinners::Spinners::SimpleDotsScrolling,
37 style("Loading...").green().bold().to_string(),
38 ),
39 - symbol: "✅".to_owned(),
39 + symbol: succeed_symbol(),
40 msg: "You are already logged in. Please logout first.".to_owned(),
41 });
42 }
@@ -80,12 +80,12 @@ pub async fn process_logout(env: Environment) -> Result<CommandResult> {
80 spinners::Spinners::SimpleDotsScrolling,
81 style("Cancel operation.").green().bold().to_string(),
82 ),
83 - symbol: "✅".to_owned(),
83 + symbol: succeed_symbol(),
84 msg: "Doing nothing.".to_owned(),
85 });
86 }
87
88 - let mut spinner = Spinner::new(
88 + let spinner = Spinner::new(
89 spinners::Spinners::SimpleDotsScrolling,
90 style("Logging you out...").green().bold().to_string(),
91 );
@@ -93,15 +93,11 @@ pub async fn process_logout(env: Environment) -> Result<CommandResult> {
93 // Call backend
94 match do_process_logout(env).await {
95 Ok(_) => {
96 - spinner.stop_and_persist("✅", "Done.".to_owned());
96 fs::remove_file(token_path)?;
97 Ok(CommandResult {
99 - spinner: Spinner::new(
100 - spinners::Spinners::SimpleDotsScrolling,
101 - style("Loading...").green().bold().to_string(),
102 - ),
103 - symbol: "✅".to_owned(),
104 - msg: "You are now logged out!".to_owned(),
98 + spinner,
99 + symbol: succeed_symbol(),
100 + msg: succeed_message("You are logged out!"),
101 })
102 }
103 Err(e) => Err(anyhow!("{e}")),
@@ -112,7 +108,7 @@ pub async fn process_logout(env: Environment) -> Result<CommandResult> {
108 spinners::Spinners::SimpleDotsScrolling,
109 style("Loading...").green().bold().to_string(),
110 ),
115 - symbol: "😏".to_owned(),
111 + symbol: fail_symbol(),
112 msg: "You are not logged in.".to_owned(),
113 })
114 }
@@ -380,8 +376,8 @@ async fn do_process_login(env: Environment, args: LoginArgs) -> Result<CommandRe
376 spinners::Spinners::SimpleDotsScrolling,
377 style("Loading...").green().bold().to_string(),
378 ),
383 - symbol: "✅".to_owned(),
384 - msg: "You are now logged in!".to_owned(),
379 + symbol: succeed_symbol(),
380 + msg: succeed_message("You are logged in!"),
381 })
382 }
383 StatusCode::NOT_FOUND => {
@@ -391,7 +387,7 @@ async fn do_process_login(env: Environment, args: LoginArgs) -> Result<CommandRe
387 spinners::Spinners::SimpleDotsScrolling,
388 style("Account not found.").green().bold().to_string(),
389 ),
394 - symbol: "✅".to_owned(),
390 + symbol: style("✔").green().to_string(),
391 msg: "Please signup!".to_owned(),
392 })
393 }
@@ -444,7 +440,7 @@ async fn send_reset_password(env: Environment, user: Option<User>) -> Result<Com
440 );
441 return Ok(CommandResult {
442 spinner,
447 - symbol: "✅".to_owned(),
443 + symbol: style("✔").green().to_string(),
444 msg: "Doing nothing.".to_owned(),
445 });
446 }
smbcloud-cli/src/cli/mod.rs
+12 -8
@@ -9,6 +9,12 @@ pub struct CommandResult {
9 pub msg: String,
10 }
11
12 +impl CommandResult {
13 + pub fn stop_and_persist(mut self) {
14 + self.spinner.stop_and_persist(&self.symbol, self.msg);
15 + }
16 +}
17 +
18 #[derive(Parser)]
19 #[clap(author, version, about)]
20 pub struct Cli {
@@ -32,20 +38,18 @@ pub enum Commands {
38 command: account::cli::Commands,
39 },
40
41 + #[clap(about = "Login to your account.")]
42 + Login {},
43 +
44 #[clap(about = "Manage your projects. Add, delete, edit. Need authentication.")]
45 Project {
46 #[clap(subcommand)]
47 command: project::cli::Commands,
48 },
49 +
50 #[clap(about = "Initialize project. Requires an smbCloud account.")]
41 - Init {
42 - /// Project name
43 - #[clap(short, long, required = false)]
44 - name: Option<String>,
45 - /// Project description
46 - #[clap(short, long, required = false)]
47 - description: Option<String>,
48 - },
51 + Init { },
52 +
53 #[clap(about = "Deploy project. It will use deploy.sh script in the .smb folder.")]
54 Deploy {},
55 }
smbcloud-cli/src/deploy/git.rs
+2 -4
@@ -3,7 +3,7 @@ use console::style;
3 use git2::{Remote, Repository};
4 use spinners::Spinner;
5
6 -pub async fn remote_deployment_setup(repo: &Repository) -> Result<Remote> {
6 +pub async fn remote_deployment_setup(repo: &Repository, repo_name: String) -> Result<Remote> {
7 let mut spinner = Spinner::new(
8 spinners::Spinners::SimpleDotsScrolling,
9 style("Setting up remote deployment...")
@@ -17,7 +17,7 @@ pub async fn remote_deployment_setup(repo: &Repository) -> Result<Remote> {
17 Err(_) => {
18 spinner.stop_and_persist("😩", "Remote deployment is not setup. Will setup remote deployment.".to_owned());
19 // Present the user with a message to setup remote deployment
20 - repo.remote("smbcloud", "deploy@api.smbcloud.xyz:git/foodandtravel")
20 + repo.remote("smbcloud", &format!("deploy@api.smbcloud.xyz:git/{}", repo_name))
21 .map_err(|e: git2::Error| {
22 spinner.stop_and_persist("😩", e.to_string());
23 anyhow!("Failed to setup remote deployment: {e}")
@@ -27,8 +27,6 @@ pub async fn remote_deployment_setup(repo: &Repository) -> Result<Remote> {
27 ));
28 }
29 };
30 - // Simulate some work
31 - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
30
31 spinner.stop_and_persist("🚀", "Remote deployment setup complete.".to_owned());
32
smbcloud-cli/src/deploy/mod.rs
+7 -4
@@ -2,7 +2,7 @@ mod git;
2 mod smb_config;
3
4 use std::{fs::File, io::BufReader};
5 -use crate::cli::CommandResult;
5 +use crate::{account::lib::protected_request, cli::CommandResult};
6 use anyhow::Result;
7 use console::style;
8 use git::remote_deployment_setup;
@@ -14,7 +14,8 @@ use spinners::Spinner;
14 use ssh2_config::{ParseRule, SshConfig};
15
16 pub async fn process_deploy(env: Environment) -> Result<CommandResult> {
17 - check_config(env).await?;
17 + protected_request(env).await?;
18 + let repo_name = check_config().await?;
19 println!("Deploying your app...");
20 let mut spinner = Spinner::new(
21 spinners::Spinners::SimpleDotsScrolling,
@@ -45,7 +46,7 @@ pub async fn process_deploy(env: Environment) -> Result<CommandResult> {
46 });
47 }
48 };
48 - let mut origin = remote_deployment_setup(&repo).await?;
49 + let origin = remote_deployment_setup(&repo, repo_name).await?;
50 let remote_url = match origin.url() {
51 Some(url) => url,
52 None => {
@@ -173,6 +174,7 @@ pub async fn process_deploy(env: Environment) -> Result<CommandResult> {
174 });
175 push_opts.remote_callbacks(callbacks);
176
177 + /*
178 match origin.push(&["refs/heads/main:refs/heads/main"], Some(&mut push_opts)) {
179 Ok(_) => {}
180 Err(e) => {
@@ -185,7 +187,8 @@ pub async fn process_deploy(env: Environment) -> Result<CommandResult> {
187 });
188 }
189 };
188 -
190 + */
191 +
192 Ok(CommandResult {
193 spinner,
194 symbol: "🚀".to_owned(),
smbcloud-cli/src/deploy/smb_config.rs
+24 -21
@@ -1,30 +1,33 @@
1 +use anyhow::Result;
2 use console::style;
2 -use smbcloud_networking::{environment::Environment, get_smb_token};
3 use spinners::Spinner;
4 -use anyhow::Result;
5 -use crate::cli::CommandResult;
4 +use std::{fs, path::Path};
5 +use toml::Value;
6
7 -pub(crate) async fn check_config(env: Environment) -> Result<CommandResult> {
7 +pub(crate) async fn check_config() -> Result<String> {
8 let mut spinner = Spinner::new(
9 spinners::Spinners::SimpleDotsScrolling,
10 style("Checking config...").green().bold().to_string(),
11 );
12
13 - let token = match get_smb_token(env).await {
14 - Ok(token) => token,
15 - Err(e) => {
16 - spinner.stop_and_persist("😩", e.to_string());
17 - return Ok(CommandResult {
18 - spinner,
19 - symbol: "😩".to_owned(),
20 - msg: "Failed to get SMB token.".to_owned(),
21 - });
22 - }
23 - };
13 + // Get .smb/config.toml file path in the current directory
14 + let config_path = Path::new(".smb/config.toml");
15 + if !config_path.exists() {
16 + spinner.stop_and_persist("😩", "No config file found.".to_owned());
17 + return Err(anyhow::anyhow!(
18 + "No config file found. Please run `smbcloud init` command."
19 + ));
20 + }
21 +
22 + // Parse toml file
23 + let config_content = fs::read_to_string(config_path)?;
24 + let config: Value = toml::from_str(&config_content)?;
25 +
26 + let repo_name = config
27 + .get("repo")
28 + .and_then(|repo| repo.get("name"))
29 + .and_then(|name| name.as_str())
30 + .ok_or_else(|| anyhow::anyhow!("Repo name not found in config file"))?;
31
25 - Ok(CommandResult {
26 - spinner,
27 - symbol: "✅".to_owned(),
28 - msg: format!("SMB Token: {}", token),
29 - })
30 -}
\ No newline at end of file
32 + Ok(repo_name.to_string())
33 +}
smbcloud-cli/src/lib.rs
+1
@@ -2,3 +2,4 @@ pub mod account;
2 pub mod cli;
3 pub mod deploy;
4 pub mod project;
5 +mod ui;
\ No newline at end of file
smbcloud-cli/src/main.rs
+4 -5
@@ -1,6 +1,7 @@
1 use anyhow::{anyhow, Result};
2 use clap::Parser;
3 use console::style;
4 +use smbcloud_cli::account::login::process_login;
5 use smbcloud_cli::cli::CommandResult;
6 use smbcloud_cli::project::init::process_project_init;
7 use smbcloud_cli::{
@@ -74,8 +75,7 @@ fn setup_logging(env: Environment, level: Option<EnvFilter>) -> Result<()> {
75 async fn main() {
76 match run().await {
77 Ok(result) => {
77 - let mut spinner = result.spinner;
78 - spinner.stop_and_persist(&result.symbol, result.msg);
78 + result.stop_and_persist();
79 std::process::exit(1);
80 }
81 Err(e) => {
@@ -111,10 +111,9 @@ async fn run() -> Result<CommandResult> {
111
112 match cli.command {
113 Commands::Account { command } => process_account(cli.environment, command).await,
114 + Commands::Login {} => process_login(cli.environment).await,
115 Commands::Project { command } => process_project(cli.environment, command).await,
115 - Commands::Init { name, description } => {
116 - process_project_init(cli.environment, name, description).await
117 - }
116 + Commands::Init { } => { process_project_init(cli.environment).await }
117 Commands::Deploy {} => process_deploy(cli.environment).await,
118 }
119 }
smbcloud-cli/src/project/init.rs
+46 -23
@@ -1,38 +1,61 @@
1 -use crate::cli::CommandResult;
2 -use anyhow::Result;
1 +use crate::{account::lib::protected_request, cli::CommandResult, ui::{succeed_message, succeed_symbol}};
2 +use anyhow::{anyhow, Result};
3 use console::style;
4 +use dialoguer::{theme::ColorfulTheme, Input};
5 use smbcloud_model::project::ProjectCreate;
6 use smbcloud_networking::environment::Environment;
7 use smbcloud_networking_project::create_project;
8 use spinners::Spinner;
9
9 -pub async fn process_project_init(
10 - env: Environment,
11 - name: Option<String>,
12 - description: Option<String>,
13 -) -> Result<CommandResult> {
14 - let mut spinner = Spinner::new(
10 +pub async fn process_project_init(env: Environment) -> Result<CommandResult> {
11 + protected_request(env).await?;
12 +
13 + let project_name = match Input::<String>::with_theme(&ColorfulTheme::default())
14 + .with_prompt("Project name")
15 + .interact()
16 + {
17 + Ok(project_name) => project_name,
18 + Err(_) => {
19 + let error = anyhow!("Invalid project name.");
20 + return Err(error);
21 + }
22 + };
23 + let description = match Input::<String>::with_theme(&ColorfulTheme::default())
24 + .with_prompt("Description")
25 + .interact()
26 + {
27 + Ok(description) => description,
28 + Err(_) => {
29 + let error = anyhow!("Invalid description.");
30 + return Err(error);
31 + }
32 + };
33 +
34 + let spinner = Spinner::new(
35 spinners::Spinners::SimpleDotsScrolling,
16 - style("Loading...").green().bold().to_string(),
36 + style("Creating a project...").green().bold().to_string(),
37 );
18 - let name = name.unwrap_or_else(|| "New Project".to_string());
19 - let description = description.unwrap_or_else(|| "No description".to_string());
20 - let project = ProjectCreate { name, description };
21 - match create_project(env, project).await {
22 - Ok(p) => {
23 - spinner.stop_and_persist("✅", "Done.".to_owned());
24 - Ok(CommandResult {
25 - spinner,
26 - symbol: "✅".to_owned(),
27 - msg: format!("Project {} has been created.", p.name),
28 - })
29 - }
38 +
39 + match create_project(
40 + env,
41 + ProjectCreate {
42 + name: project_name.clone(),
43 + description: description.clone(),
44 + },
45 + )
46 + .await
47 + {
48 + Ok(_) => Ok(CommandResult {
49 + spinner,
50 + symbol: succeed_symbol(),
51 + msg: succeed_message(&format!("{project_name} has been created.")),
52 + }),
53 Err(e) => {
54 println!("Error: {e:#?}");
55 Ok(CommandResult {
56 spinner,
34 - symbol: "😩".to_owned(),
35 - msg: "Failed to initiate a project.".to_string(),
57 + symbol: succeed_symbol(),
58 + msg: format!("Failed to create a project {project_name}."),
59 })
60 }
61 }
smbcloud-cli/src/project/mod.rs
+6 -48
@@ -6,62 +6,20 @@ use crate::cli::CommandResult;
6 use anyhow::{anyhow, Result};
7 use console::style;
8 use dialoguer::{theme::ColorfulTheme, Input};
9 +use init::process_project_init;
10 use log::debug;
10 -use smbcloud_model::{
11 - self,
12 - project::{Config, Project, ProjectCreate},
13 -};
11 +use smbcloud_model::
12 + project::{Config, Project}
13 +;
14 use smbcloud_networking::environment::Environment;
15 -use smbcloud_networking_project::{create_project, delete_project, get_all, get_project};
15 +use smbcloud_networking_project::{delete_project, get_all, get_project};
16 use spinners::Spinner;
17 use std::{fs::OpenOptions, io::Write};
18
19 pub async fn process_project(env: Environment, commands: Commands) -> Result<CommandResult> {
20 match commands {
21 Commands::New {} => {
22 - let project_name = Input::<String>::with_theme(&ColorfulTheme::default())
23 - .with_prompt("Project name")
24 - .interact()
25 - .unwrap();
26 - let description = Input::<String>::with_theme(&ColorfulTheme::default())
27 - .with_prompt("Description")
28 - .interact()
29 - .unwrap();
30 -
31 - let mut spinner = Spinner::new(
32 - spinners::Spinners::SimpleDotsScrolling,
33 - style("Creating a project...").green().bold().to_string(),
34 - );
35 -
36 - match create_project(
37 - env,
38 - ProjectCreate {
39 - name: project_name.clone(),
40 - description: description.clone(),
41 - },
42 - )
43 - .await
44 - {
45 - Ok(_) => {
46 - spinner.stop_and_persist("✅", "Done.".to_owned());
47 - Ok(CommandResult {
48 - spinner: Spinner::new(
49 - spinners::Spinners::SimpleDotsScrolling,
50 - style("Loading...").green().bold().to_string(),
51 - ),
52 - symbol: "✅".to_owned(),
53 - msg: format!("{project_name} has been created."),
54 - })
55 - }
56 - Err(e) => {
57 - println!("Error: {e:#?}");
58 - Ok(CommandResult {
59 - spinner,
60 - symbol: "😩".to_owned(),
61 - msg: format!("Failed to create a project {project_name}."),
62 - })
63 - }
64 - }
22 + process_project_init(env).await
23 }
24 Commands::List {} => {
25 let mut spinner = Spinner::new(
smbcloud-cli/src/ui/mod.rs new
+13
@@ -0,0 +1,13 @@
1 +use console::style;
2 +
3 +pub fn succeed_symbol() -> String {
4 + style("✔").green().to_string()
5 +}
6 +
7 +pub fn fail_symbol() -> String {
8 + style("✘").for_stderr().red().to_string()
9 +}
10 +
11 +pub fn succeed_message(message: &str) -> String {
12 + style(message).bold().white().to_string()
13 +}
\ No newline at end of file
smbcloud-networking/src/constants.rs
+1 -1
@@ -17,4 +17,4 @@ pub const PATH_USERS_SIGN_IN: &str = "v1/users/sign_in";
17 pub const PATH_LINK_GITHUB_ACCOUNT: &str = "v1/link_github_account";
18 pub const PATH_RESET_PASSWORD_INSTRUCTIONS: &str = "v1/resend_reset_password_instructions";
19 pub const PATH_RESEND_CONFIRMATION: &str = "v1/resend_confirmation";
20 -pub const PATH_AUTHORIZE: &str = "v1/authorize";
20 +pub const PATH_AUTHORIZE: &str = "v1/authorize";
\ No newline at end of file