1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "ncheap", version, about = "Namecheap registrar API CLI")]
pub struct Cli {
/// Emit machine-readable JSON on stdout
#[arg(long, global = true)]
pub json: bool,
/// Config profile to use (overrides NCHEAP_PROFILE and default_profile)
#[arg(long, global = true)]
pub profile: Option<String>,
/// Refuse before any API call unless the resolved profile has this
/// name (guards against a leaked NCHEAP_PROFILE switching accounts)
#[arg(long, global = true, value_name = "NAME")]
pub expect_profile: Option<String>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// Run every read-only safety check across the account (3 + 2N API calls)
Audit,
/// Preflight the environment: config, profile, gates, and a live auth check
Doctor,
/// Domain operations
Domains {
#[command(subcommand)]
command: DomainsCommand,
},
/// Account operations
Account {
#[command(subcommand)]
command: AccountCommand,
},
/// DNS operations
Dns {
#[command(subcommand)]
command: DnsCommand,
},
/// Domain privacy operations
Privacy {
#[command(subcommand)]
command: PrivacyCommand,
},
/// Domain transfer operations
Transfer {
#[command(subcommand)]
command: TransferCommand,
},
/// Call an allowlisted read-only API method directly, emitting raw XML
Raw {
/// API command, e.g. domains.getTldList ("namecheap." prefix optional)
command: String,
/// Method parameter, repeatable: --param Key=Value
#[arg(long = "param", value_name = "KEY=VALUE")]
params: Vec<String>,
},
}
impl Command {
/// The command name used in the JSON envelope; the single source of
/// truth so main's error path and success path cannot drift apart.
pub fn name(&self) -> &'static str {
match self {
Command::Audit => "audit",
Command::Doctor => "doctor",
Command::Domains { command } => match command {
DomainsCommand::List => "domains.list",
DomainsCommand::Check { .. } => "domains.check",
DomainsCommand::Lock { lock, unlock, .. } => {
if *lock || *unlock {
"domains.lock.set"
} else {
"domains.lock"
}
}
DomainsCommand::Info { .. } => "domains.info",
DomainsCommand::Contacts { set_from, .. } => {
if set_from.is_some() {
"domains.contacts.set"
} else {
"domains.contacts"
}
}
DomainsCommand::Register { .. } => "domains.register",
DomainsCommand::Renew { .. } => "domains.renew",
},
Command::Account { command } => match command {
AccountCommand::Balances { .. } => "account.balances",
AccountCommand::Pricing { .. } => "account.pricing",
},
Command::Dns { command } => match command {
DnsCommand::Get { .. } => "dns.get",
DnsCommand::Add { .. } => "dns.add",
DnsCommand::Remove { .. } => "dns.remove",
DnsCommand::Set { .. } => "dns.set",
DnsCommand::SetDefault { .. } => "dns.set_default",
},
Command::Privacy { command } => match command {
PrivacyCommand::List => "privacy.list",
PrivacyCommand::Enable { .. } => "privacy.enable",
PrivacyCommand::Disable { .. } => "privacy.disable",
},
Command::Transfer { command } => match command {
TransferCommand::Create { .. } => "transfer.create",
TransferCommand::Status { .. } => "transfer.status",
},
Command::Raw { .. } => "raw",
}
}
}
#[derive(Subcommand)]
pub enum TransferCommand {
/// Start an inbound transfer (mutating, charges money; price-guarded)
Create {
domain: String,
/// EPP/auth code from the current registrar (note: visible in
/// process listings and shell history)
#[arg(long)]
epp_code: String,
/// Ceiling on the live LISTED transfer price
#[arg(long)]
max_price: f64,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
/// Check the status of a transfer by its TransferID
Status { transfer_id: String },
}
#[derive(Subcommand)]
pub enum PrivacyCommand {
/// List all domain privacy subscriptions (auto-paginated)
List,
/// Enable domain privacy (mutating)
Enable {
domain: String,
/// Email address privacy emails are forwarded to (required, never defaulted)
#[arg(long)]
forward_to: String,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
/// Disable domain privacy (mutating)
Disable {
domain: String,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
}
#[derive(Subcommand)]
pub enum DnsCommand {
/// Show nameserver mode and host records for a domain
Get { domain: String },
/// Add one host record (mutating; setHosts is a full-zone rewrite)
Add {
domain: String,
/// Record type: A, AAAA, ALIAS, CAA, CNAME, MX, MXE, NS, TXT, URL, URL301, FRAME
#[arg(long = "type")]
record_type: String,
/// Host name ("@" for the apex, "www", ...)
#[arg(long)]
name: String,
/// Record value (IP, hostname, text — per record type)
#[arg(long)]
address: String,
/// TTL in seconds (60–60000; API default 1800)
#[arg(long)]
ttl: Option<u32>,
/// MX preference (required for MX records)
#[arg(long)]
mx_pref: Option<u32>,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
/// Remove matching host records (mutating; full-zone rewrite)
Remove {
domain: String,
/// Record type of the records to remove
#[arg(long = "type")]
record_type: String,
/// Host name of the records to remove
#[arg(long)]
name: String,
/// Only remove records with this exact value
#[arg(long)]
address: Option<String>,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
/// Point a domain at custom nameservers (mutating)
Set {
domain: String,
/// Nameserver hostnames (registries require at least two)
#[arg(required = true, num_args = 2..)]
nameservers: Vec<String>,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
/// Revert a domain to Namecheap default DNS (mutating)
SetDefault {
domain: String,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
}
#[derive(Subcommand)]
pub enum DomainsCommand {
/// List all domains in the account (auto-paginated)
List,
/// Check availability of one or more domains
Check {
/// Domains to check (the API caps one call at 50)
#[arg(required = true)]
domains: Vec<String>,
},
/// Show — or with --lock/--unlock, set — the registrar transfer lock
Lock {
domain: String,
/// Turn the transfer lock on (mutating)
#[arg(long, conflicts_with = "unlock")]
lock: bool,
/// Turn the transfer lock off (mutating)
#[arg(long)]
unlock: bool,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
/// Show registration, privacy, and DNS details for a domain
Info { domain: String },
/// Show domain contacts (PII redacted unless --full), or replace them
/// with another owned domain's via --set-from (mutating)
Contacts {
domain: String,
/// Show the actual contact fields
#[arg(long)]
full: bool,
/// Replace all four contact sets with this owned domain's (mutating)
#[arg(long, value_name = "DOMAIN", conflicts_with = "full")]
set_from: Option<String>,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
/// Register a domain (mutating, charges money; live price guard)
Register {
domain: String,
/// Registration period in years
#[arg(long, default_value_t = 1)]
years: u8,
/// Ceiling on the live LISTED price (the actual charge may add ICANN fees)
#[arg(long)]
max_price: f64,
/// Owned domain whose contacts are copied for the registration
#[arg(long)]
contacts_from: String,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
/// Renew a domain (mutating, charges money; live price guard)
Renew {
domain: String,
/// Renewal period in years
#[arg(long, default_value_t = 1)]
years: u8,
/// Ceiling on the live LISTED price (the actual charge may add ICANN fees)
#[arg(long)]
max_price: f64,
/// Confirm the mutation (required for non-interactive use)
#[arg(long)]
yes: bool,
},
}
#[derive(Subcommand)]
pub enum AccountCommand {
/// Show account balance summary (amounts redacted unless --full)
Balances {
/// Show exact balance amounts
#[arg(long)]
full: bool,
},
/// Show product pricing (response cached locally for 24h)
Pricing {
/// Product type (DOMAIN, SSLCERTIFICATE)
#[arg(long = "type", default_value = "DOMAIN")]
product_type: String,
/// Product category filter (e.g. DOMAINS)
#[arg(long)]
category: Option<String>,
/// Action filter (e.g. REGISTER, RENEW, TRANSFER)
#[arg(long)]
action: Option<String>,
/// Product name filter (e.g. com)
#[arg(long)]
product: Option<String>,
},
}