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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ __pycache__
# Claude Code local runtime state (per-machine, not for commit)
**/.claude/scheduled_tasks.lock
**/.claude/scheduled_tasks.json
.cursor/
69 changes: 67 additions & 2 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ url = "2"
uuid = { version = "1", features = ["v4"] }
zip = { version = "8.6.0", default-features = false, features = ["deflate-flate2"] }
iso_currency = "0.5.3"
dialoguer = "0.12.0"
console = "0.16.4"
indicatif = "0.18.6"

[dev-dependencies]
httpmock = "0.8"
Expand Down
8 changes: 4 additions & 4 deletions rust/src/config/settings_form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,10 @@ fn validate_field(field: &SettingsFormV1Field, errors: &mut Vec<String>, path: &
}
match field {
SettingsFormV1Field::Select { options, .. }
| SettingsFormV1Field::MultiSelect { options, .. } => {
if options.is_empty() {
errors.push(format!("{path}.options must contain at least one option"));
}
| SettingsFormV1Field::MultiSelect { options, .. }
if options.is_empty() =>
{
errors.push(format!("{path}.options must contain at least one option"));
}
SettingsFormV1Field::ListGroup { item, .. } => {
if !is_field_name(&item.id_field) {
Expand Down
23 changes: 20 additions & 3 deletions rust/src/domain/available.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use serde_json::json;

use domains_client::types;

use super::common::{api_error, format_money, make_client, period_label, validate_domain_name};
use super::common::{
api_error, format_money, make_client, period_label, term_for_period, validate_domain_name,
};
use crate::next_action::next_action;
use crate::output_schema::output_schema;
use crate::scopes::DOMAINS_READ;
Expand Down Expand Up @@ -159,14 +161,29 @@ pub(super) fn command() -> RuntimeCommandSpec {

let cmd = CommandResult::new(result);
if body.available.unwrap_or(false) {
// If interactive, offer to continue directly into registration.
let price_1yr = term_for_period(&prices, 1)
.and_then(|t| t.price.as_ref())
.and_then(format_money);
let currency_str = shared_currency(&prices);
if let Some(wizard_result) =
super::register::bridge::offer_registration_from_available(
&ctx,
&resolved_domain,
price_1yr,
currency_str,
)
.await?
{
return Ok(wizard_result);
}

Ok(cmd.with_next_actions(vec![
next_action("domain quote <domain>", "Price a registration")
.with_param("domain", NextActionParam::value(resolved_domain)),
]))
} else {
Ok(cmd.with_next_actions(vec![
// `domain suggest` accepts a seed domain, so the domain just
// checked as taken is a valid query to copy/paste directly.
next_action("domain suggest <query>", "Find alternatives")
.with_param("query", NextActionParam::value(resolved_domain)),
]))
Expand Down
94 changes: 94 additions & 0 deletions rust/src/domain/guides/domain-register.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
summary: Interactive domain registration wizard — guided step-by-step domain purchase
---

# Interactive domain registration with `gddy domain register`

The `register` command is an interactive wizard that walks you through
the entire domain registration process in a single session:

```
gddy domain register
```

## How it works

The wizard guides you through 5 steps:

1. **Discovery** — Search for a domain or enter one directly. If taken, view
suggestions and pick an alternative.
2. **Options** — Choose registration period (1–10 years), WHOIS privacy, and
auto-renewal. Optionally set custom nameservers.
3. **Contacts** — Use your account default contacts, load saved contacts from
`contacts.toml`, or enter new ones interactively.
4. **Review & Confirm** — See the full order summary (price, renewal, agreements)
and explicitly consent before any charge is made.
5. **Register** — Submit the registration and wait for the registry to confirm.

You can go back to a previous step at any point. Pressing Ctrl+C at any time
cancels the wizard — **no charges are made until you explicitly confirm in
Step 4**.

## Non-interactive mode

For scripts and CI, pass all options as flags:

```
gddy domain register example.com \
--period 2 \
--privacy true \
--auto-renew true \
--agree \
--confirm
```

Required flags in non-interactive mode:
- Domain name (positional argument)
- `--agree` — consent to legal agreements
- `--confirm` — authorize the purchase

## Contacts

The wizard checks for saved contacts at `~/.config/gddy/contacts.toml`.
If found, you can reuse them without re-entering details each time.

To create a starter contacts file:
```
gddy domain contacts init
```

When you enter contacts manually during the wizard, you'll be offered to save
them for future registrations.

## Payment

A valid payment method (credit card or Good-as-Gold balance) must be on file.
If the quote fails with a payment error, the wizard will offer to open the
GoDaddy payment methods page in your browser.

## Entry from other commands

When running interactively, related commands offer to continue into registration:

- `gddy domain available example.com` — if available, asks "Would you like to register?"
- `gddy domain suggest "keywords"` — after results, asks "Would you like to register one?"
- `gddy domain quote example.com` — after pricing, asks "Would you like to purchase now?"

## Examples

```
# Full interactive wizard
gddy domain register

# Start with a specific domain (skips discovery search)
gddy domain register example.com

# Non-interactive for scripts
gddy domain register example.com --period 1 --agree --confirm

# With custom nameservers
gddy domain register example.com \
--nameserver ns1.example.net \
--nameserver ns2.example.net \
--agree --confirm
```
24 changes: 16 additions & 8 deletions rust/src/domain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod nameservers;
mod operation;
mod purchase;
mod quote;
mod register;
mod suggest;

// Shared with the `dns` module, which builds the same Domains API client and
Expand All @@ -46,13 +47,13 @@ pub fn module() -> Module {
\n\
• list / get — your existing domains and their details\n\
• available / suggest — find a name to register\n\
quote — price a registration and see required agreements\n\
purchase — register a new domain (charges your account)\n\
register — interactive wizard: discover, configure, and buy\n\
quote / purchase — scripted two-step registration (quote then buy)\n\
• nameservers set — point a domain at custom nameservers\n\
• operation status — check on an async operation (e.g. a pending purchase)\n\
\n\
Reads need the `domains.domain:read` scope; purchase also needs\n\
`domains.domain:create`, and `nameservers set` needs\n\
Reads need the `domains.domain:read` scope; purchase/register also\n\
need `domains.domain:create`, and `nameservers set` needs\n\
`domains.nameserver:update`. Manage a domain's DNS with `gddy dns`.",
),
)
Expand All @@ -63,14 +64,21 @@ pub fn module() -> Module {
.with_command(agreements::command())
.with_command(quote::command())
.with_command(purchase::command())
.with_command(register::command())
.with_group(nameservers::group())
.with_group(contacts::group())
.with_group(operation::group())
})
.with_guides_from_markdown([(
"domain-purchase.md",
include_bytes!("guides/domain-purchase.md").as_slice(),
)])
.with_guides_from_markdown([
(
"domain-purchase.md",
include_bytes!("guides/domain-purchase.md").as_slice(),
),
(
"domain-register.md",
include_bytes!("guides/domain-register.md").as_slice(),
),
])
}

#[cfg(test)]
Expand Down
23 changes: 23 additions & 0 deletions rust/src/domain/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,29 @@ pub(super) fn command() -> RuntimeCommandSpec {
// find it. Warn so the user knows to re-quote on this host.
tracing::warn!(error = %e, "could not cache the quote for purchase");
}
// If interactive, offer to purchase directly.
let quote_price = view
.get("price")
.and_then(|v| v.as_str())
.map(str::to_owned);
let quote_currency = view
.get("currency")
.and_then(|v| v.as_str())
.map(str::to_owned);
if let Some(wizard_result) =
super::register::bridge::offer_registration_from_quote(
&ctx,
&domain,
&token,
quote_price,
quote_currency,
period,
)
.await?
{
return Ok(wizard_result);
}

next_actions.push(
next_action(
"domain purchase --quote-token <quote-token> --agree --confirm",
Expand Down
Loading
Loading