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
67 changes: 67 additions & 0 deletions rust/src/hosting/app/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use cli_engine::{CommandResult, CommandSpec, NextActionParam, RuntimeCommandSpec, Tier};
use serde_json::json;

use crate::hosting::common::{HostingAppOperation, client_err, make_client, parse_app_type};
use crate::next_action::next_action;
use crate::scopes::HOSTING_APPLICATION_CREATE as APP_CREATE;

#[derive(Debug, Clone, clap::Args)]
struct AppCreateArgs {
/// Application type (NODEJS).
#[arg(long = "app-type", value_name = "TYPE", value_parser = parse_app_type)]
app_type: String,

/// Human-readable display name (1–200 characters).
#[arg(long, value_name = "NAME")]
name: String,
}

pub(super) fn command() -> RuntimeCommandSpec {
RuntimeCommandSpec::new_typed_with_context::<AppCreateArgs, _, _, _>(
CommandSpec::from_args::<AppCreateArgs>("create", "Create a hosting application")
.with_long(
"Provision a new hosting application slot. Because no app ID exists \
until provisioning completes, this returns an operation ID. \
Poll `hosting operation get --operation-id <id>` until \
status is COMPLETED or FAILED. On COMPLETED, the operation's \
`application` field carries the created app; use `application.id` \
as the --app-id for all subsequent calls.",
)
.with_system("hosting")
.with_tier(Tier::Mutate)
.mutates(true)
.with_scopes(&[APP_CREATE])
.with_output_schema::<HostingAppOperation>(),
|ctx, args: AppCreateArgs| async move {
let app_type = args.app_type;
let name = args.name;
let client = make_client(&ctx, &[APP_CREATE]).await?;
let data = client
.create_app(&app_type, json!({ "name": name }))
.await
.map_err(client_err)?;

let operation_id = data
.get("operationId")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_owned();

let mut poll_action = next_action(
"hosting operation get --operation-id <operation-id>",
"Poll app provisioning status",
)
.with_param("operation-id", NextActionParam::required());

if !operation_id.is_empty() {
poll_action = next_action(
"hosting operation get --operation-id <operation-id>",
"Poll app provisioning status",
)
.with_param("operation-id", NextActionParam::value(operation_id));
}

Ok(CommandResult::new(data).with_next_actions(vec![poll_action]))
},
)
}
30 changes: 30 additions & 0 deletions rust/src/hosting/app/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use cli_engine::{CommandResult, CommandSpec, RuntimeCommandSpec, Tier};
use serde_json::json;

use crate::hosting::common::{AppIdArgs, client_err, make_client};
use crate::next_action::next_action;
use crate::scopes::HOSTING_APPLICATION_DELETE as APP_DELETE;

pub(super) fn command() -> RuntimeCommandSpec {
RuntimeCommandSpec::new_typed_with_context::<AppIdArgs, _, _, _>(
CommandSpec::from_args::<AppIdArgs>("delete", "Delete a hosting application")
.with_long("Permanently delete a hosting application and all its associated data.")
.with_system("hosting")
.with_tier(Tier::Destructive)
.mutates(true)
.with_scopes(&[APP_DELETE]),
|ctx, args: AppIdArgs| async move {
let app_id = args.app_id;
let client = make_client(&ctx, &[APP_DELETE]).await?;
client.delete_app(&app_id).await.map_err(client_err)?;
Ok(
CommandResult::new(json!({ "deleted": true, "appId": app_id })).with_next_actions(
vec![next_action(
"hosting app list --app-type <type>",
"List remaining applications",
)],
),
)
},
)
}
37 changes: 37 additions & 0 deletions rust/src/hosting/app/get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use cli_engine::{CommandResult, CommandSpec, NextActionParam, RuntimeCommandSpec, Tier};

use crate::hosting::common::{AppIdArgs, HostingApplication, client_err, make_client};
use crate::next_action::next_action;
use crate::scopes::HOSTING_APPLICATION_READ as APP_READ;

pub(super) fn command() -> RuntimeCommandSpec {
RuntimeCommandSpec::new_typed_with_context::<AppIdArgs, _, _, _>(
CommandSpec::from_args::<AppIdArgs>("get", "Get a hosting application")
.with_long(
"Get details for a single hosting application by ID. \
The `urls` object exposes the reachable URLs for each environment \
variant (preview and publish).",
)
.with_system("hosting")
.with_tier(Tier::Read)
.with_scopes(&[APP_READ])
.with_output_schema::<HostingApplication>(),
|ctx, args: AppIdArgs| async move {
let app_id = args.app_id;
let client = make_client(&ctx, &[APP_READ]).await?;
let data = client.get_app(&app_id).await.map_err(client_err)?;
Ok(CommandResult::new(data).with_next_actions(vec![
next_action(
"hosting app status --app-id <app-id>",
"Get runtime status for this application",
)
.with_param("app-id", NextActionParam::value(app_id.clone())),
next_action(
"hosting deployment list --app-id <app-id>",
"List deployments for this application",
)
.with_param("app-id", NextActionParam::value(app_id)),
]))
},
)
}
80 changes: 80 additions & 0 deletions rust/src/hosting/app/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use cli_engine::{CommandResult, CommandSpec, NextActionParam, RuntimeCommandSpec, Tier};
use serde_json::{Value, json};

use crate::hosting::common::{
HostingAppSummary, client_err, make_client, next_page_token, parse_app_type,
};
use crate::next_action::next_action;
use crate::scopes::HOSTING_APPLICATION_READ as APP_READ;

#[derive(Debug, Clone, clap::Args)]
struct AppListArgs {
/// Application type (NODEJS).
#[arg(long = "app-type", value_name = "TYPE", value_parser = parse_app_type)]
app_type: String,

/// Maximum number of applications to return. Omit to return all.
#[arg(long, value_name = "N", value_parser = clap::value_parser!(u32).range(1..))]
limit: Option<u32>,
}

pub(super) fn command() -> RuntimeCommandSpec {
RuntimeCommandSpec::new_typed_with_context::<AppListArgs, _, _, _>(
CommandSpec::from_args::<AppListArgs>("list", "List hosting applications")
.with_long(
"List all hosting applications of a given type. Results are autopaginated — \
all pages are fetched and combined. Use --limit to cap the total returned.\n\
\n\
--app-type is required. Currently supported: NODEJS.",
)
.with_system("hosting")
.with_tier(Tier::Read)
.with_scopes(&[APP_READ])
.with_default_fields("id,name,status")
.with_output_schema::<HostingAppSummary>(),
|ctx, args: AppListArgs| async move {
let app_type = args.app_type;
let limit = args.limit;
let client = make_client(&ctx, &[APP_READ]).await?;

let mut all_items: Vec<Value> = Vec::new();
let mut page_token: Option<String> = None;

loop {
let page_limit = limit.map(|cap| {
let remaining = cap.saturating_sub(all_items.len() as u32);
remaining.min(100)
});

let response = client
.list_apps(&app_type, page_token.as_deref(), page_limit)
.await
.map_err(client_err)?;

if let Some(items) = response.get("items").and_then(|v| v.as_array()) {
all_items.extend(items.iter().cloned());
}

if limit.is_some_and(|cap| all_items.len() >= cap as usize) {
if let Some(cap) = limit {
all_items.truncate(cap as usize);
}
break;
}

match next_page_token(&response) {
Some(token) => page_token = Some(token),
None => break,
}
}

Ok(CommandResult::new(json!(all_items)).with_next_actions(vec![
next_action(
"hosting app get --app-id <app-id>",
"Get details for an application",
)
.with_param("app-id", NextActionParam::required()),
]))
},
)
}
29 changes: 29 additions & 0 deletions rust/src/hosting/app/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
mod create;
mod delete;
mod get;
mod list;
mod restart;
mod status;
mod update;

use cli_engine::{GroupSpec, RuntimeGroupSpec};

pub(super) fn group() -> RuntimeGroupSpec {
RuntimeGroupSpec::new(
GroupSpec::new(
"app",
"Create, inspect, update, and delete hosting applications",
)
.with_long(
"Work with hosting applications. Use --app-type on list and create to \
specify the product type (currently NODEJS).",
),
)
.with_command(list::command())
.with_command(get::command())
.with_command(create::command())
.with_command(update::command())
.with_command(delete::command())
.with_command(status::command())
.with_command(restart::command())
}
47 changes: 47 additions & 0 deletions rust/src/hosting/app/restart.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use cli_engine::{CommandResult, CommandSpec, NextActionParam, RuntimeCommandSpec, Tier};

use crate::hosting::common::{client_err, make_client};
use crate::next_action::next_action;
use crate::scopes::HOSTING_DEPLOYMENT_EXECUTE as DEPLOY_EXECUTE;

#[derive(Debug, Clone, clap::Args)]
struct AppRestartArgs {
/// Application ID.
#[arg(long = "app-id", value_name = "APP_ID")]
app_id: String,

/// Environment to restart (PREVIEW or PUBLISH).
#[arg(long, value_name = "VARIANT", value_parser = ["PREVIEW", "PUBLISH"])]
variant: String,
}

pub(super) fn command() -> RuntimeCommandSpec {
RuntimeCommandSpec::new_typed_with_context::<AppRestartArgs, _, _, _>(
CommandSpec::from_args::<AppRestartArgs>("restart", "Restart an application environment")
.with_long(
"Restart the PREVIEW or PUBLISH environment of a hosting application. \
Check `hosting app status` after restarting to confirm the environment \
returns to ACTIVE.",
)
.with_system("hosting")
.with_tier(Tier::Mutate)
.mutates(true)
.with_scopes(&[DEPLOY_EXECUTE]),
|ctx, args: AppRestartArgs| async move {
let app_id = args.app_id;
let variant = args.variant;
let client = make_client(&ctx, &[DEPLOY_EXECUTE]).await?;
let data = client
.restart_app(&app_id, &variant)
.await
.map_err(client_err)?;
Ok(CommandResult::new(data).with_next_actions(vec![
next_action(
"hosting app status --app-id <app-id>",
"Check environment status after restart",
)
.with_param("app-id", NextActionParam::value(app_id)),
]))
},
)
}
39 changes: 39 additions & 0 deletions rust/src/hosting/app/status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use cli_engine::{CommandResult, CommandSpec, NextActionParam, RuntimeCommandSpec, Tier};

use crate::hosting::common::{AppIdArgs, HostingApplicationStatus, client_err, make_client};
use crate::next_action::next_action;
use crate::scopes::HOSTING_APPLICATION_READ as APP_READ;

pub(super) fn command() -> RuntimeCommandSpec {
RuntimeCommandSpec::new_typed_with_context::<AppIdArgs, _, _, _>(
CommandSpec::from_args::<AppIdArgs>("status", "Get application runtime status")
.with_long(
"Get the runtime status of a hosting application's environments \
(preview and publish). Use this to check whether an environment \
is ACTIVE, IDLE, or in a transitional state after a restart or deployment. \
The `variants` array contains one entry per environment (PREVIEW and PUBLISH) \
with the runtime status for that environment.",
)
.with_system("hosting")
.with_tier(Tier::Read)
.with_scopes(&[APP_READ])
.with_output_schema::<HostingApplicationStatus>(),
|ctx, args: AppIdArgs| async move {
let app_id = args.app_id;
let client = make_client(&ctx, &[APP_READ]).await?;
let data = client.get_app_status(&app_id).await.map_err(client_err)?;
Ok(CommandResult::new(data).with_next_actions(vec![
next_action(
"hosting deployment list --app-id <app-id>",
"List deployments for this application",
)
.with_param("app-id", NextActionParam::value(app_id.clone())),
next_action(
"hosting app restart --app-id <app-id> --variant <variant>",
"Restart an environment",
)
.with_param("app-id", NextActionParam::value(app_id)),
]))
},
)
}
Loading
Loading