Skip to content

Commit 562f5fa

Browse files
refactor(cli): restructure commands and main (#128)
This cleans up the CLI main file and move commands under their own module.
1 parent 542483b commit 562f5fa

File tree

18 files changed

+409
-391
lines changed

18 files changed

+409
-391
lines changed

linkup-cli/src/completion.rs renamed to linkup-cli/src/commands/completion.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@ use clap_complete::{generate, Generator, Shell};
55

66
use crate::{Cli, CliError};
77

8-
pub fn completion(shell: &Option<Shell>) -> Result<(), CliError> {
9-
if let Some(shell) = shell {
8+
#[derive(clap::Args)]
9+
pub struct Args {
10+
#[arg(long, value_enum)]
11+
shell: Option<Shell>,
12+
}
13+
14+
pub fn completion(args: &Args) -> Result<(), CliError> {
15+
if let Some(shell) = &args.shell {
1016
let mut cmd = Cli::command();
1117
print_completions(shell, &mut cmd);
1218
}
19+
1320
Ok(())
1421
}
1522
fn print_completions<G: Generator + Clone>(gen: &G, cmd: &mut Command) {

linkup-cli/src/health.rs renamed to linkup-cli/src/commands/health.rs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@ use serde::Serialize;
1010

1111
use crate::{linkup_dir_path, local_config::LocalState, services, CliError};
1212

13+
#[derive(clap::Args)]
14+
pub struct Args {
15+
// Output status in JSON format
16+
#[arg(long)]
17+
json: bool,
18+
}
19+
20+
pub fn health(args: &Args) -> Result<(), CliError> {
21+
let health = Health::load()?;
22+
23+
let health = if args.json {
24+
serde_json::to_string_pretty(&health).unwrap()
25+
} else {
26+
format!("{}", health)
27+
};
28+
29+
println!("{}", health);
30+
31+
Ok(())
32+
}
33+
1334
#[derive(Debug, Serialize)]
1435
struct System {
1536
os_name: String,
@@ -262,17 +283,3 @@ impl Display for Health {
262283
Ok(())
263284
}
264285
}
265-
266-
pub fn health(json: bool) -> Result<(), CliError> {
267-
let health = Health::load()?;
268-
269-
let health = if json {
270-
serde_json::to_string_pretty(&health).unwrap()
271-
} else {
272-
format!("{}", health)
273-
};
274-
275-
println!("{}", health);
276-
277-
Ok(())
278-
}

linkup-cli/src/commands/local.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use crate::{
2+
local_config::{upload_state, LocalState, ServiceTarget},
3+
CliError,
4+
};
5+
6+
#[derive(clap::Args)]
7+
pub struct Args {
8+
service_names: Vec<String>,
9+
10+
#[arg(
11+
short,
12+
long,
13+
help = "Route all the services to local. Cannot be used with SERVICE_NAMES.",
14+
conflicts_with = "service_names"
15+
)]
16+
all: bool,
17+
}
18+
19+
pub async fn local(args: &Args) -> Result<(), CliError> {
20+
if args.service_names.is_empty() && !args.all {
21+
return Err(CliError::NoSuchService(
22+
"No service names provided".to_string(),
23+
));
24+
}
25+
26+
let mut state = LocalState::load()?;
27+
28+
if args.all {
29+
for service in state.services.iter_mut() {
30+
service.current = ServiceTarget::Local;
31+
}
32+
} else {
33+
for service_name in &args.service_names {
34+
let service = state
35+
.services
36+
.iter_mut()
37+
.find(|s| s.name.as_str() == service_name)
38+
.ok_or_else(|| CliError::NoSuchService(service_name.to_string()))?;
39+
service.current = ServiceTarget::Local;
40+
}
41+
}
42+
43+
state.save()?;
44+
upload_state(&state).await?;
45+
46+
if args.all {
47+
println!("Linkup is routing all traffic to the local servers");
48+
} else {
49+
println!(
50+
"Linkup is routing {} traffic to the local server",
51+
args.service_names.join(", ")
52+
);
53+
}
54+
55+
Ok(())
56+
}

linkup-cli/src/local_dns.rs renamed to linkup-cli/src/commands/local_dns.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,32 @@ use std::{
33
process::{Command, Stdio},
44
};
55

6+
use clap::Subcommand;
7+
68
use crate::{
79
local_config::{config_path, get_config},
810
services, CliError, Result, LINKUP_CF_TLS_API_ENV_VAR,
911
};
1012

13+
#[derive(clap::Args)]
14+
pub struct Args {
15+
#[clap(subcommand)]
16+
pub subcommand: LocalDNSSubcommand,
17+
}
18+
19+
#[derive(Subcommand)]
20+
pub enum LocalDNSSubcommand {
21+
Install,
22+
Uninstall,
23+
}
24+
25+
pub fn local_dns(args: &Args, config: &Option<String>) -> Result<()> {
26+
match args.subcommand {
27+
LocalDNSSubcommand::Install => install(config),
28+
LocalDNSSubcommand::Uninstall => uninstall(config),
29+
}
30+
}
31+
1132
pub fn install(config_arg: &Option<String>) -> Result<()> {
1233
if std::env::var(LINKUP_CF_TLS_API_ENV_VAR).is_err() {
1334
println!("local-dns uses Cloudflare to enable https through local certificates.");

linkup-cli/src/commands/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
pub mod completion;
2+
pub mod health;
3+
pub mod local;
4+
pub mod local_dns;
5+
pub mod preview;
6+
pub mod remote;
7+
pub mod reset;
8+
pub mod server;
9+
pub mod start;
10+
pub mod status;
11+
pub mod stop;
12+
13+
pub use {completion::completion, completion::Args as CompletionArgs};
14+
pub use {health::health, health::Args as HealthArgs};
15+
pub use {local::local, local::Args as LocalArgs};
16+
pub use {local_dns::local_dns, local_dns::Args as LocalDnsArgs};
17+
pub use {preview::preview, preview::Args as PreviewArgs};
18+
pub use {remote::remote, remote::Args as RemoteArgs};
19+
pub use {reset::reset, reset::Args as ResetArgs};
20+
pub use {server::server, server::Args as ServerArgs};
21+
pub use {start::start, start::Args as StartArgs};
22+
pub use {status::status, status::Args as StatusArgs};
23+
pub use {stop::stop, stop::Args as StopArgs};

linkup-cli/src/preview.rs renamed to linkup-cli/src/commands/preview.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,34 @@
1+
use crate::commands::status::{format_state_domains, SessionStatus};
12
use crate::local_config::{config_path, get_config};
2-
use crate::status::{format_state_domains, SessionStatus};
33
use crate::worker_client::WorkerClient;
44
use crate::CliError;
5+
use clap::builder::ValueParser;
56
use linkup::CreatePreviewRequest;
67

7-
pub async fn preview(
8-
config: &Option<String>,
9-
services: &[(String, String)],
8+
#[derive(clap::Args)]
9+
pub struct Args {
10+
#[arg(
11+
help = "<service>=<url> pairs to preview.",
12+
value_parser = ValueParser::new(parse_services_tuple),
13+
required = true,
14+
num_args = 1..,
15+
)]
16+
services: Vec<(String, String)>,
17+
18+
#[arg(long, help = "Print the request body instead of sending it.")]
1019
print_request: bool,
11-
) -> Result<(), CliError> {
20+
}
21+
22+
pub async fn preview(args: &Args, config: &Option<String>) -> Result<(), CliError> {
1223
let config_path = config_path(config)?;
1324
let input_config = get_config(&config_path)?;
1425
let create_preview_request: CreatePreviewRequest =
15-
input_config.create_preview_request(services);
26+
input_config.create_preview_request(&args.services);
1627
let url = input_config.linkup.remote.clone();
1728
let create_req_json = serde_json::to_string(&create_preview_request)
1829
.map_err(|e| CliError::LoadConfig(url.to_string(), e.to_string()))?;
1930

20-
if print_request {
31+
if args.print_request {
2132
println!("{}", create_req_json);
2233
return Ok(());
2334
}

linkup-cli/src/commands/remote.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use crate::{
2+
local_config::{upload_state, LocalState, ServiceTarget},
3+
CliError,
4+
};
5+
6+
#[derive(clap::Args)]
7+
pub struct Args {
8+
service_names: Vec<String>,
9+
10+
#[arg(
11+
short,
12+
long,
13+
help = "Route all the services to remote. Cannot be used with SERVICE_NAMES.",
14+
conflicts_with = "service_names"
15+
)]
16+
all: bool,
17+
}
18+
19+
pub async fn remote(args: &Args) -> Result<(), CliError> {
20+
if args.service_names.is_empty() && !args.all {
21+
return Err(CliError::NoSuchService(
22+
"No service names provided".to_string(),
23+
));
24+
}
25+
26+
let mut state = LocalState::load()?;
27+
28+
if args.all {
29+
for service in state.services.iter_mut() {
30+
service.current = ServiceTarget::Remote;
31+
}
32+
} else {
33+
for service_name in &args.service_names {
34+
let service = state
35+
.services
36+
.iter_mut()
37+
.find(|s| s.name.as_str() == service_name)
38+
.ok_or_else(|| CliError::NoSuchService(service_name.to_string()))?;
39+
service.current = ServiceTarget::Remote;
40+
}
41+
}
42+
43+
state.save()?;
44+
upload_state(&state).await?;
45+
46+
if args.all {
47+
println!("Linkup is routing all traffic to the remote servers");
48+
} else {
49+
println!(
50+
"Linkup is routing {} traffic to the remote server",
51+
args.service_names.join(", ")
52+
);
53+
}
54+
55+
Ok(())
56+
}

linkup-cli/src/commands/reset.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use crate::{commands, local_config::LocalState, CliError};
2+
3+
#[derive(clap::Args)]
4+
pub struct Args {}
5+
6+
pub async fn reset(_args: &Args) -> Result<(), CliError> {
7+
let _ = LocalState::load()?;
8+
9+
commands::stop(&commands::StopArgs {}, false)?;
10+
commands::start(&commands::StartArgs { no_tunnel: false }, false, &None).await?;
11+
12+
Ok(())
13+
}

linkup-cli/src/commands/server.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use std::fs;
2+
3+
use crate::CliError;
4+
5+
#[derive(clap::Args)]
6+
pub struct Args {
7+
#[arg(long)]
8+
pidfile: String,
9+
}
10+
11+
pub async fn server(args: &Args) -> Result<(), CliError> {
12+
let pid = std::process::id();
13+
fs::write(&args.pidfile, pid.to_string())?;
14+
15+
let res = linkup_local_server::start_server().await;
16+
17+
if let Err(pid_file_err) = fs::remove_file(&args.pidfile) {
18+
eprintln!("Failed to remove pidfile: {}", pid_file_err);
19+
}
20+
21+
res.map_err(|e| e.into())
22+
}

linkup-cli/src/start.rs renamed to linkup-cli/src/commands/start.rs

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,35 @@ use colored::Colorize;
1212
use crossterm::{cursor, ExecutableCommand};
1313

1414
use crate::{
15+
commands::status::{format_state_domains, SessionStatus},
1516
env_files::write_to_env_file,
1617
local_config::{config_path, config_to_state, get_config},
1718
services::{self, BackgroundService},
18-
status::{format_state_domains, SessionStatus},
1919
};
2020
use crate::{local_config::LocalState, CliError};
2121

2222
const LOADING_CHARS: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
2323

24-
pub struct StartArgs<'a> {
25-
/// Path to the Linkup config to be used as base in case `fresh_state` argument is `true`.
26-
pub config_arg: &'a Option<String>,
27-
28-
/// If there should not be a Cloudflare tunnel.
24+
#[derive(clap::Args)]
25+
pub struct Args {
26+
#[clap(
27+
short,
28+
long,
29+
help = "Start linkup in partial mode without a tunnel. Not all requests will succeed."
30+
)]
2931
pub no_tunnel: bool,
30-
31-
/// Boolean representing if should refresh the state to what is defined on `config_arg`.
32-
pub fresh_state: bool,
3332
}
3433

35-
impl<'a> Default for StartArgs<'a> {
36-
fn default() -> Self {
37-
Self {
38-
config_arg: &None,
39-
no_tunnel: false,
40-
fresh_state: false,
41-
}
42-
}
43-
}
44-
45-
pub async fn start<'a>(args: StartArgs<'_>) -> Result<(), CliError> {
34+
pub async fn start<'a>(
35+
args: &Args,
36+
fresh_state: bool,
37+
config_arg: &Option<String>,
38+
) -> Result<(), CliError> {
4639
env_logger::init();
4740

48-
let mut state = if args.fresh_state {
41+
let mut state = if fresh_state {
4942
let is_paid = services::CloudflareTunnel::use_paid_tunnels();
50-
let state = load_and_save_state(args.config_arg, args.no_tunnel, is_paid)?;
43+
let state = load_and_save_state(config_arg, args.no_tunnel, is_paid)?;
5144
set_linkup_env(state.clone())?;
5245

5346
state

0 commit comments

Comments
 (0)