Skip to content

Commit 9a6121a

Browse files
committed
feat: add non-interactive mode and route management options to CLI
1 parent cd2fc62 commit 9a6121a

File tree

8 files changed

+258
-38
lines changed

8 files changed

+258
-38
lines changed

packages/toolchain/cli/src/commands/actor/create.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ impl Opts {
200200
filter_tags: None,
201201
build_tags: Some(build_tags),
202202
version: self.version.clone(),
203+
skip_route_creation: None,
204+
keep_existing_routes: None,
205+
non_interactive: false,
203206
})
204207
.await?;
205208

packages/toolchain/cli/src/commands/deploy.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,19 @@ pub struct Opts {
1717
#[clap(long)]
1818
extra_tags: Option<String>,
1919

20-
#[clap(long, help = "Override the automatically generated version name")]
20+
/// Override the automatically generated version name
21+
#[clap(long)]
2122
version: Option<String>,
23+
24+
#[clap(long)]
25+
skip_route_creation: Option<bool>,
26+
27+
#[clap(long)]
28+
keep_existing_routes: Option<bool>,
29+
30+
/// Run in non-interactive mode (no prompts)
31+
#[clap(long)]
32+
non_interactive: bool,
2233
}
2334

2435
impl Opts {
@@ -47,6 +58,9 @@ impl Opts {
4758
filter_tags: filter_tags,
4859
build_tags: build_tags,
4960
version: self.version.clone(),
61+
skip_route_creation: self.skip_route_creation,
62+
keep_existing_routes: self.keep_existing_routes,
63+
non_interactive: self.non_interactive,
5064
})
5165
.await?;
5266

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use anyhow::*;
2+
use clap::Parser;
3+
use toolchain::{
4+
rivet_api::{apis, models},
5+
ToolchainCtx,
6+
};
7+
8+
/// Get information about a route endpoint
9+
#[derive(Parser, Clone)]
10+
pub struct Opts {
11+
/// Name of the route to retrieve information about
12+
function_name: String,
13+
14+
/// Specify the environment to get the route from (will prompt if not specified)
15+
#[clap(long, alias = "env", short = 'e')]
16+
environment: Option<String>,
17+
}
18+
19+
impl Opts {
20+
pub async fn execute(&self) -> Result<()> {
21+
let ctx = crate::util::login::load_or_login().await?;
22+
let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?;
23+
24+
// Get route information
25+
let route = get_route(&ctx, &env, &self.function_name).await?;
26+
27+
match route {
28+
Some(route) => {
29+
println!("https://{}{}", route.hostname, route.path);
30+
31+
Ok(())
32+
}
33+
None => Err(anyhow!(
34+
"Route '{}' not found in environment '{}'",
35+
self.function_name,
36+
env
37+
)),
38+
}
39+
}
40+
}
41+
42+
// Helper function to get route if it exists
43+
async fn get_route(
44+
ctx: &ToolchainCtx,
45+
env: &str,
46+
route_id: &str,
47+
) -> Result<Option<models::RoutesRoute>> {
48+
let routes_response = apis::routes_api::routes_list(
49+
&ctx.openapi_config_cloud,
50+
Some(&ctx.project.name_id.to_string()),
51+
Some(env),
52+
)
53+
.await?;
54+
55+
// Find route that matches the ID
56+
let matching_route = routes_response
57+
.routes
58+
.iter()
59+
.find(|route| route.id == *route_id)
60+
.cloned();
61+
62+
Ok(matching_route)
63+
}
64+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use anyhow::*;
2+
use clap::Parser;
3+
use toolchain::rivet_api::apis;
4+
5+
/// List all functions for an environment
6+
#[derive(Parser)]
7+
pub struct Opts {
8+
/// Specify the environment to list function for (will prompt if not specified)
9+
#[clap(long, alias = "env", short = 'e')]
10+
environment: Option<String>,
11+
}
12+
13+
impl Opts {
14+
pub async fn execute(&self) -> Result<()> {
15+
let ctx = crate::util::login::load_or_login().await?;
16+
let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?;
17+
18+
// Get routes
19+
let routes_response = apis::routes_api::routes_list(
20+
&ctx.openapi_config_cloud,
21+
Some(&ctx.project.name_id.to_string()),
22+
Some(&env),
23+
)
24+
.await?;
25+
26+
if routes_response.routes.is_empty() {
27+
println!("No routes found for environment '{}'", env);
28+
return Ok(());
29+
}
30+
31+
for route in routes_response.routes {
32+
println!("- {}: {}{}", route.id, route.hostname, route.path);
33+
}
34+
35+
Ok(())
36+
}
37+
}
38+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use anyhow::*;
2+
use clap::Parser;
3+
4+
pub mod endpoint;
5+
pub mod list;
6+
7+
/// Commands for managing routes
8+
#[derive(Parser)]
9+
pub enum SubCommand {
10+
/// List all routes
11+
List(list::Opts),
12+
/// Get information about a specific route endpoint
13+
Endpoint(endpoint::Opts),
14+
}
15+
16+
impl SubCommand {
17+
pub async fn execute(&self) -> Result<()> {
18+
match self {
19+
SubCommand::List(opts) => opts.execute().await,
20+
SubCommand::Endpoint(opts) => opts.execute().await,
21+
}
22+
}
23+
}

packages/toolchain/cli/src/commands/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod config;
44
pub mod deno;
55
pub mod deploy;
66
pub mod environment;
7+
pub mod function;
78
pub mod login;
89
pub mod logout;
910
pub mod metadata;
@@ -58,6 +59,14 @@ pub enum SubCommand {
5859
#[clap(subcommand)]
5960
subcommand: region::SubCommand,
6061
},
62+
/// Commands for managing routes
63+
#[clap(alias = "f", alias = "func", alias = "route")]
64+
Function {
65+
#[clap(subcommand)]
66+
subcommand: function::SubCommand,
67+
},
68+
/// Get information about a specific route endpoint
69+
Endpoint(function::endpoint::Opts),
6170
/// Commands for managing Rivet configuration
6271
Config {
6372
#[clap(subcommand)]
@@ -95,6 +104,8 @@ impl SubCommand {
95104
SubCommand::Actor { subcommand } => subcommand.execute().await,
96105
SubCommand::Build { subcommand } => subcommand.execute().await,
97106
SubCommand::Region { subcommand } => subcommand.execute().await,
107+
SubCommand::Function { subcommand } => subcommand.execute().await,
108+
SubCommand::Endpoint(opts) => opts.execute().await,
98109
SubCommand::Config { subcommand } => subcommand.execute().await,
99110
SubCommand::Metadata { subcommand } => subcommand.execute().await,
100111
SubCommand::Deno(opts) => opts.execute().await,

packages/toolchain/cli/src/util/deploy.rs

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ pub struct DeployOpts<'a> {
1919
pub filter_tags: Option<HashMap<String, String>>,
2020
pub build_tags: Option<HashMap<String, String>>,
2121
pub version: Option<String>,
22+
pub skip_route_creation: Option<bool>,
23+
pub keep_existing_routes: Option<bool>,
24+
pub non_interactive: bool,
2225
}
2326

2427
pub async fn deploy(opts: DeployOpts<'_>) -> Result<Vec<Uuid>> {
@@ -65,7 +68,16 @@ pub async fn deploy(opts: DeployOpts<'_>) -> Result<Vec<Uuid>> {
6568
.await?;
6669

6770
// Setup function routes
68-
setup_function_routes(opts.ctx, environment, &config, &opts.filter_tags).await?;
71+
setup_function_routes(
72+
opts.ctx,
73+
environment,
74+
&config,
75+
&opts.filter_tags,
76+
opts.skip_route_creation,
77+
opts.keep_existing_routes,
78+
opts.non_interactive,
79+
)
80+
.await?;
6981

7082
// Print summary
7183
print_summary(opts.ctx, environment);
@@ -78,20 +90,23 @@ async fn setup_function_routes(
7890
environment: &toolchain::project::environment::TEMPEnvironment,
7991
config: &config::Config,
8092
filter_tags: &Option<HashMap<String, String>>,
93+
skip_route_creation: Option<bool>,
94+
keep_existing_routes: Option<bool>,
95+
non_interactive: bool,
8196
) -> Result<()> {
82-
// Determine default hostname based on project & env
83-
let default_hostname = format!(
84-
"{}-{}.{}",
85-
ctx.project.name_id,
86-
environment.slug,
87-
ctx.bootstrap
88-
.domains
89-
.job
90-
.as_ref()
91-
.context("bootstrap.domains.job")?
92-
);
93-
9497
for (fn_name, function) in &config.functions {
98+
// Determine default hostname based on project & env
99+
let default_hostname = format!(
100+
"{}-{}-{fn_name}.{}",
101+
ctx.project.name_id,
102+
environment.slug,
103+
ctx.bootstrap
104+
.domains
105+
.job
106+
.as_ref()
107+
.context("bootstrap.domains.job")?
108+
);
109+
95110
// TODO: Convert this in to a shared fn
96111
// Filter out builds that match the tags
97112
if let Some(filter) = &filter_tags {
@@ -190,19 +205,40 @@ async fn setup_function_routes(
190205
];
191206

192207
println!();
193-
let choice = block_in_place(|| {
194-
Select::new(
195-
&format!(
196-
"Route configuration for '{fn_name}' has changed{}",
197-
changes_text
198-
),
199-
options.to_vec(),
200-
)
201-
.with_starting_cursor(0)
202-
.prompt()
203-
})?;
204208

205-
match choice.index {
209+
let choice_index = if non_interactive {
210+
// In non-interactive mode, use auto_sync_routes if provided, otherwise sync by default
211+
match keep_existing_routes {
212+
Some(true) => {
213+
println!("Skipping route sync for '{fn_name}' (non-interactive mode)");
214+
1 // Keep existing route
215+
}
216+
Some(false) => {
217+
println!("Auto-syncing route configuration for '{fn_name}' (non-interactive mode)");
218+
0 // Sync route with config
219+
}
220+
None => {
221+
println!("Auto-syncing route configuration for '{fn_name}' (non-interactive mode)");
222+
0 // Default to sync in non-interactive mode
223+
}
224+
}
225+
} else {
226+
// Interactive mode - prompt the user
227+
let choice = block_in_place(|| {
228+
Select::new(
229+
&format!(
230+
"Route configuration for '{fn_name}' has changed{}",
231+
changes_text
232+
),
233+
options.to_vec(),
234+
)
235+
.with_starting_cursor(0)
236+
.prompt()
237+
})?;
238+
choice.index
239+
};
240+
241+
match choice_index {
206242
0 => {
207243
// Update first matching route to match config
208244
let mut update_route_body = models::RoutesUpdateRouteBody {
@@ -268,17 +304,42 @@ async fn setup_function_routes(
268304
];
269305

270306
println!();
271-
let choice = block_in_place(|| {
272-
Select::new(
273-
&format!("Set up routing for function '{}':", fn_name),
274-
options.to_vec(),
275-
)
276-
.with_help_message("Routes can be manually created in the Rivet dashboard")
277-
.with_starting_cursor(0)
278-
.prompt()
279-
})?;
280-
281-
match choice.index {
307+
308+
let choice_index = if non_interactive {
309+
// In non-interactive mode, use auto_create_routes if provided, otherwise create by default
310+
match skip_route_creation {
311+
Some(true) => {
312+
println!("Skipping route creation for '{fn_name}' (non-interactive mode)");
313+
1 // Skip route creation
314+
}
315+
Some(false) => {
316+
println!(
317+
"Auto-creating route for function '{fn_name}' (non-interactive mode)"
318+
);
319+
0 // Create default route
320+
}
321+
None => {
322+
println!(
323+
"Auto-creating route for function '{fn_name}' (non-interactive mode)"
324+
);
325+
0 // Default to create in non-interactive mode
326+
}
327+
}
328+
} else {
329+
// Interactive mode - prompt the user
330+
let choice = block_in_place(|| {
331+
Select::new(
332+
&format!("Set up routing for function '{}':", fn_name),
333+
options.to_vec(),
334+
)
335+
.with_help_message("Routes can be manually created in the Rivet dashboard")
336+
.with_starting_cursor(0)
337+
.prompt()
338+
})?;
339+
choice.index
340+
};
341+
342+
match choice_index {
282343
0 => {
283344
// Create route with default settings
284345
create_function_route(

packages/toolchain/toolchain/src/toolchain_ctx.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,13 @@ pub fn cloud_config_from_env() -> Option<(String, String)> {
5050

5151
/// If the credentials already exist or loading credentials from env.
5252
pub async fn has_cloud_config() -> Result<bool> {
53-
if cloud_config_from_env().is_some() {
53+
if let Some((api_endpoint, token)) = cloud_config_from_env() {
54+
meta::mutate_project(&paths::data_dir()?, |meta| {
55+
if meta.cloud.is_none() {
56+
meta.cloud = Some(meta::Cloud::new(api_endpoint, token))
57+
}
58+
})
59+
.await?;
5460
Ok(true)
5561
} else {
5662
meta::read_project(&paths::data_dir()?, |x| x.cloud.is_some()).await

0 commit comments

Comments
 (0)