Skip to content
4 changes: 4 additions & 0 deletions .agents/skills/gddy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ gddy dns delete example.com --type A --name www

`gddy payment-methods add` opens the browser to the account's payment-methods page — no card data is ever handled by the CLI itself. Purchases fail (403/422) without a valid payment method or sufficient account balance; check the error's `code` field, not just the HTTP status, to tell that apart from other failures.

## Hosting

Use the `hosting app restart` command after creating, updating or deleting an application's secrets.

## Global flags

Use the `--debug` flag for verbose output, including the full HTTP request/response. Use `--dry-run` to see what would happen without making any changes. Humans see human-formatted output; use `--human` to see what they see in an interactive TTY. Non-TTY output uses `--json` formatting by default. Use `--toon` if you understand that format to save tokens. Use `--env` to override the default `prod` environment, or set it permanently with `gddy env set <env>`.
Expand Down
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 \
`app` field carries the created app; use `app.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