run402 3.8.3 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -44,14 +44,14 @@ Commands:
44
44
  sites Deploy static sites
45
45
  cdn CloudFront CDN diagnostics (wait-fresh) for public asset URLs
46
46
  subdomains Manage custom subdomains (claim, list, delete)
47
- domains Manage custom domains (add, list, status, delete)
47
+ domains Manage ProjectDomain lifecycle (connect, check, repair, wait)
48
48
  apps Browse and manage the app marketplace
49
49
  ai AI translation and moderation tools
50
50
  image Generate AI images via x402 or MPP micropayments
51
51
  email Send template-based emails from your project
52
52
  message Send messages to Run402 developers
53
53
  auth Manage project user authentication (magic link, passwords, settings)
54
- sender-domain Manage custom email sender domain (register, status, remove)
54
+ sender-domain Removed; use domains connect/status/repair
55
55
  billing Email organizations, Stripe tier checkout, email packs
56
56
  contracts KMS signers ($0.04/day rental + $0.000005/sign)
57
57
  agent Manage agent identity (contact info)
package/lib/domains.mjs CHANGED
@@ -1,232 +1,402 @@
1
1
  import { resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
- import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs } from "./argparse.mjs";
4
+ import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs, parseIntegerFlag } from "./argparse.mjs";
5
5
 
6
- const HELP = `run402 domains — Manage custom domains
6
+ const HELP = `run402 domains — Manage ProjectDomain lifecycle
7
7
 
8
8
  Usage:
9
9
  run402 domains <subcommand> [args...]
10
10
 
11
11
  Subcommands:
12
- add <domain> <subdomain_name> [--project <id>] [--auth principal|service-key]
13
- list [--project <id>] [--auth principal|service-key]
14
- status <domain> [--project <id>] [--auth principal|service-key]
15
- delete <domain> --confirm [--project <id>] [--auth principal|service-key]
12
+ connect <domain> [--project <id>] [--web] [--email-send] [--email-receive] [...]
13
+ list [--project <id>] [--include-managed]
14
+ status <domain> [--project <id>]
15
+ dns <domain> [--project <id>] [--format json|bind]
16
+ check <domain> [--project <id>]
17
+ apply <domain> [--project <id>] [--authority auto|provider-connect|delegated-subdomain|hosted-zone]
18
+ repair <domain> [--project <id>]
19
+ test-receive <domain> --to <local-part|address> [--project <id>]
20
+ wait <domain> [--project <id>] [--until active|safe|receive-active] [--timeout-ms <n>] [--interval-ms <n>]
21
+ activate <domain> [--project <id>]
22
+ disconnect <domain> --confirm [--project <id>]
16
23
 
17
- Examples:
18
- run402 domains add example.com myapp
19
- run402 domains add example.com myapp --project prj_123
20
- run402 domains list --project prj_123 --auth service-key
21
- run402 domains list
22
- run402 domains status example.com
23
- run402 domains delete example.com --confirm
24
-
25
- Notes:
26
- - After adding a domain, configure DNS as shown in the response
27
- - Poll 'status' until the domain is active (DNS propagation ~60s)
28
- - The domain must CNAME to domains.run402.com (or ALIAS for apex domains)
24
+ Removed:
25
+ add, delete. Use connect/disconnect.
29
26
  `;
30
27
 
31
28
  const SUB_HELP = {
32
- add: `run402 domains addRegister a custom domain for a project
29
+ connect: `run402 domains connectconnect a ProjectDomain capability
33
30
 
34
31
  Usage:
35
- run402 domains add <domain> <subdomain_name> [--project <id>] [--auth principal|service-key]
36
-
37
- Arguments:
38
- <domain> Custom domain (e.g. example.com)
39
- <subdomain_name> Existing subdomain to map the custom domain to
40
-
41
- Options:
42
- --project <id> Project ID (defaults to the active project)
43
- --auth <mode> principal (default, server-authoritative) or service-key
44
- (uses local project-key cache)
45
-
46
- Notes:
47
- - After adding, configure DNS as shown in the response
48
- - Poll 'run402 domains status <domain>' until active
49
- - The domain must CNAME to domains.run402.com (or ALIAS for apex domains)
32
+ run402 domains connect <domain> --project <id> [--web] [--email-send] [--email-receive] [options]
50
33
 
51
34
  Examples:
52
- run402 domains add example.com myapp
53
- run402 domains add example.com myapp --project prj_abc123
35
+ run402 domains connect kysigned.com --project prj_123 --email-send --email-receive --mailbox-addresses primary --addresses info
36
+ run402 domains connect example.com --project prj_123 --web --web-target production
54
37
  `,
55
- list: `run402 domains list — List custom domains for a project
38
+ list: `run402 domains list — list project domains
56
39
 
57
40
  Usage:
58
- run402 domains list [--project <id>] [--auth principal|service-key]
59
-
60
- Arguments:
61
- <id> Project ID (defaults to the active project)
62
-
63
- Options:
64
- --auth <mode> principal (default, server-authoritative) or service-key
65
- (uses local project-key cache)
41
+ run402 domains list --project <id>
42
+ `,
43
+ status: `run402 domains status — show one ProjectDomain aggregate
66
44
 
67
- Examples:
68
- run402 domains list
69
- run402 domains list --project prj_abc123
45
+ Usage:
46
+ run402 domains status <domain> --project <id>
70
47
  `,
71
- status: `run402 domains statusCheck DNS/SSL status of a custom domain
48
+ dns: `run402 domains dnsprint required DNS records
72
49
 
73
50
  Usage:
74
- run402 domains status <domain> [--project <id>] [--auth principal|service-key]
51
+ run402 domains dns <domain> --project <id> [--format json|bind]
52
+ `,
53
+ check: `run402 domains check — refresh ProjectDomain observations and checks
75
54
 
76
- Arguments:
77
- <domain> Custom domain to check
55
+ Usage:
56
+ run402 domains check <domain> --project <id>
57
+ `,
58
+ apply: `run402 domains apply — apply safe provider-managed changes
78
59
 
79
- Options:
80
- --project <id> Project ID (defaults to the active project)
81
- --auth <mode> principal (default, server-authoritative) or service-key
82
- (uses local project-key cache)
60
+ Usage:
61
+ run402 domains apply <domain> --project <id> [--authority auto|provider-connect|delegated-subdomain|hosted-zone]
62
+ `,
63
+ repair: `run402 domains repair — repair Run402-owned domain infrastructure
83
64
 
84
- Examples:
85
- run402 domains status example.com
86
- run402 domains status example.com --project prj_abc123
65
+ Usage:
66
+ run402 domains repair <domain> --project <id>
87
67
  `,
88
- delete: `run402 domains deleteRelease a custom domain
68
+ "test-receive": `run402 domains test-receivecreate an inbound receive test token
89
69
 
90
70
  Usage:
91
- run402 domains delete <domain> --confirm [--project <id>] [--auth principal|service-key]
71
+ run402 domains test-receive <domain> --project <id> --to <local-part|address>
72
+ `,
73
+ wait: `run402 domains wait — poll until a ProjectDomain is ready
92
74
 
93
- Arguments:
94
- <domain> Custom domain to release
75
+ Usage:
76
+ run402 domains wait <domain> --project <id> [--until active|safe|receive-active] [--timeout-ms <n>] [--interval-ms <n>]
77
+ `,
78
+ activate: `run402 domains activate — activate custom mailbox addresses
95
79
 
96
- Options:
97
- --confirm Required: releasing detaches the domain from this
98
- project and clears its DNS/SSL configuration
99
- (irreversible)
100
- --project <id> Project ID (defaults to the active project)
101
- --auth <mode> principal (default, server-authoritative) or service-key
102
- (uses local project-key cache)
80
+ Usage:
81
+ run402 domains activate <domain> --project <id>
82
+ `,
83
+ disconnect: `run402 domains disconnect — disconnect a ProjectDomain
103
84
 
104
- Examples:
105
- run402 domains delete example.com --confirm
85
+ Usage:
86
+ run402 domains disconnect <domain> --project <id> --confirm
106
87
  `,
107
88
  };
108
89
 
109
- function parseProjectFlag(args, extraKnown = []) {
110
- const parsedArgs = normalizeArgv(args);
111
- const valueFlags = ["--project", "--auth"];
112
- assertKnownFlags(parsedArgs, [...valueFlags, ...extraKnown, "--help", "-h"], valueFlags);
113
- const auth = flagValue(parsedArgs, "--auth") ?? "principal";
114
- if (auth !== "principal" && auth !== "service-key") {
90
+ const CONNECT_VALUE_FLAGS = [
91
+ "--project",
92
+ "--web-target",
93
+ "--receive-strategy",
94
+ "--mail-subdomain",
95
+ "--mailbox-addresses",
96
+ "--addresses",
97
+ "--activation",
98
+ "--authority",
99
+ "--confirm-mx-takeover",
100
+ ];
101
+ const COMMON_VALUE_FLAGS = ["--project"];
102
+
103
+ function print(data) {
104
+ console.log(JSON.stringify(data, null, 2));
105
+ }
106
+
107
+ function removed(command, replacement) {
108
+ fail({
109
+ code: "COMMAND_REMOVED",
110
+ message: `${command} has been removed. Use ${replacement}.`,
111
+ details: { command, replacement },
112
+ next_actions: [{ type: "use_replacement_command", command: replacement }],
113
+ });
114
+ }
115
+
116
+ function parseCommon(args, extraKnown = [], valueFlags = COMMON_VALUE_FLAGS) {
117
+ const parsed = normalizeArgv(args);
118
+ assertKnownFlags(parsed, [...valueFlags, ...extraKnown, "--help", "-h"], valueFlags);
119
+ return {
120
+ parsed,
121
+ projectId: resolveProjectId(flagValue(parsed, "--project")),
122
+ rest: positionalArgs(parsed, valueFlags),
123
+ };
124
+ }
125
+
126
+ function normalizeReceiveStrategy(value) {
127
+ if (!value) return "auto";
128
+ const map = {
129
+ auto: "auto",
130
+ "outbound-only": "outbound_only",
131
+ outbound_only: "outbound_only",
132
+ forwarding: "forwarding_mode",
133
+ "forwarding-mode": "forwarding_mode",
134
+ forwarding_mode: "forwarding_mode",
135
+ subdomain: "subdomain_mode",
136
+ "subdomain-mode": "subdomain_mode",
137
+ subdomain_mode: "subdomain_mode",
138
+ "full-mx": "full_receive_takeover",
139
+ "full-receive-takeover": "full_receive_takeover",
140
+ full_receive_takeover: "full_receive_takeover",
141
+ };
142
+ const normalized = map[value];
143
+ if (!normalized) {
115
144
  fail({
116
145
  code: "BAD_FLAG",
117
- message: "--auth must be one of: principal, service-key.",
118
- details: { flag: "--auth", value: auth, allowed: ["principal", "service-key"] },
146
+ message: "--receive-strategy must be one of: auto, outbound-only, forwarding, subdomain, full-mx.",
147
+ details: { flag: "--receive-strategy", value },
119
148
  });
120
149
  }
121
- return {
122
- project: flagValue(parsedArgs, "--project"),
123
- authMode: auth === "service-key" ? "service_key" : "principal",
124
- rest: positionalArgs(parsedArgs, valueFlags),
125
- args: parsedArgs,
126
- };
150
+ return normalized;
127
151
  }
128
152
 
129
- async function add(args) {
130
- const { project, authMode, rest } = parseProjectFlag(args);
131
- const domain = rest[0];
132
- const subdomainName = rest[1];
133
- if (!domain || !subdomainName) {
153
+ function parseMailboxAddressMode(value) {
154
+ if (!value) return null;
155
+ const allowed = ["primary", "alias", "managed", "none"];
156
+ if (value.includes(",") || !allowed.includes(value)) {
157
+ fail({
158
+ code: "BAD_FLAG",
159
+ message: "--mailbox-addresses must be one of: primary, alias, managed, none.",
160
+ hint: "Use `--mailbox-addresses primary --addresses info,legal`, not `--mailbox-addresses info,legal`.",
161
+ details: { flag: "--mailbox-addresses", value, replacement_flag: "--addresses" },
162
+ });
163
+ }
164
+ return value;
165
+ }
166
+
167
+ function parseAddresses(value) {
168
+ if (!value) return [];
169
+ return value.split(",").map((part) => part.trim()).filter(Boolean);
170
+ }
171
+
172
+ function desiredFromConnectFlags(domain, parsed) {
173
+ const desired = {};
174
+ const web = parsed.includes("--web") || flagValue(parsed, "--web-target") || parsed.includes("--primary-web") || parsed.includes("--web-alias");
175
+ if (web) {
176
+ desired.web = {
177
+ enabled: true,
178
+ target: flagValue(parsed, "--web-target") ?? "production",
179
+ role: parsed.includes("--web-alias") ? "alias" : "primary",
180
+ };
181
+ }
182
+
183
+ const emailSend = parsed.includes("--email-send");
184
+ const emailReceive = parsed.includes("--email-receive");
185
+ const mailboxMode = parseMailboxAddressMode(flagValue(parsed, "--mailbox-addresses"));
186
+ if (emailSend || emailReceive || mailboxMode) {
187
+ const email = {};
188
+ if (emailSend) email.send = { enabled: true };
189
+ if (emailReceive) {
190
+ const receive = {
191
+ enabled: true,
192
+ strategy: normalizeReceiveStrategy(flagValue(parsed, "--receive-strategy")),
193
+ };
194
+ const mailSubdomain = flagValue(parsed, "--mail-subdomain");
195
+ if (mailSubdomain) receive.mail_subdomain = mailSubdomain;
196
+ const mxFingerprint = flagValue(parsed, "--confirm-mx-takeover");
197
+ if (mxFingerprint) receive.observed_mx_fingerprint = mxFingerprint;
198
+ email.receive = receive;
199
+ }
200
+ if (mailboxMode) {
201
+ const locals = parseAddresses(flagValue(parsed, "--addresses"));
202
+ if ((mailboxMode === "primary" || mailboxMode === "alias") && locals.length === 0) {
203
+ fail({
204
+ code: "BAD_FLAG",
205
+ message: "--addresses is required when --mailbox-addresses is primary or alias.",
206
+ details: { flag: "--addresses", domain },
207
+ });
208
+ }
209
+ const create = parsed.includes("--create-mailboxes")
210
+ ? true
211
+ : parsed.includes("--no-create-mailboxes") ? false : false;
212
+ email.mailbox_addresses = {
213
+ mode: mailboxMode,
214
+ addresses: locals.map((local) => ({
215
+ local_part: local,
216
+ mailbox_slug: local,
217
+ create_mailbox: create,
218
+ })),
219
+ };
220
+ }
221
+ const activation = flagValue(parsed, "--activation");
222
+ if (activation) {
223
+ if (!["automatic_when_ready", "manual"].includes(activation)) {
224
+ fail({
225
+ code: "BAD_FLAG",
226
+ message: "--activation must be automatic_when_ready or manual.",
227
+ details: { flag: "--activation", value: activation },
228
+ });
229
+ }
230
+ email.activation = activation;
231
+ }
232
+ desired.email = email;
233
+ }
234
+
235
+ if (!desired.web && !desired.email) {
134
236
  fail({
135
237
  code: "BAD_USAGE",
136
- message: "Missing <domain> and/or <subdomain_name>.",
137
- hint: "run402 domains add <domain> <subdomain_name> [--project <id>]",
238
+ message: "No desired domain capability selected.",
239
+ hint: "Use --web, --email-send, --email-receive, and/or --mailbox-addresses.",
138
240
  });
139
241
  }
140
- if (rest.length > 2) {
141
- fail({ code: "BAD_USAGE", message: `Unexpected argument for domains add: ${rest[2]}` });
242
+ return desired;
243
+ }
244
+
245
+ async function connect(args) {
246
+ const parsed = normalizeArgv(args);
247
+ assertKnownFlags(parsed, [
248
+ ...CONNECT_VALUE_FLAGS,
249
+ "--web",
250
+ "--primary-web",
251
+ "--web-alias",
252
+ "--email-send",
253
+ "--email-receive",
254
+ "--create-mailboxes",
255
+ "--no-create-mailboxes",
256
+ "--help",
257
+ "-h",
258
+ ], CONNECT_VALUE_FLAGS);
259
+ const rest = positionalArgs(parsed, CONNECT_VALUE_FLAGS);
260
+ const domain = rest[0];
261
+ if (!domain) fail({ code: "BAD_USAGE", message: "Missing <domain>.", hint: "run402 domains connect <domain> --project <id> --web" });
262
+ if (rest.length > 1) fail({ code: "BAD_USAGE", message: `Unexpected argument for domains connect: ${rest[1]}` });
263
+ if (parsed.includes("--create-mailboxes") && parsed.includes("--no-create-mailboxes")) {
264
+ fail({ code: "BAD_FLAG", message: "Choose only one of --create-mailboxes or --no-create-mailboxes." });
265
+ }
266
+ const authority = flagValue(parsed, "--authority");
267
+ if (authority && !["auto", "manual-dns", "provider-connect", "delegated-subdomain", "hosted-zone"].includes(authority)) {
268
+ fail({ code: "BAD_FLAG", message: "--authority must be one of: auto, manual-dns, provider-connect, delegated-subdomain, hosted-zone.", details: { flag: "--authority", value: authority } });
142
269
  }
143
- const projectId = resolveProjectId(project);
270
+ const projectId = resolveProjectId(flagValue(parsed, "--project"));
144
271
  try {
145
- const data = await getSdk().domains.add(projectId, { domain, subdomainName, authMode });
146
- console.log(JSON.stringify(data, null, 2));
272
+ print(await getSdk().domains.ensure(projectId, domain, { desired: desiredFromConnectFlags(domain, parsed) }));
147
273
  } catch (err) {
148
274
  reportSdkError(err);
149
275
  }
150
276
  }
151
277
 
152
278
  async function list(args) {
153
- const argList = Array.isArray(args) ? args : [];
154
- const { project, authMode, rest } = parseProjectFlag(argList);
155
- if (rest.length > 0) {
156
- fail({
157
- code: "BAD_USAGE",
158
- message: `Unexpected argument for domains list: ${rest[0]}`,
159
- hint: "Use `run402 domains list --project <id>`.",
160
- });
161
- }
162
- const projectId = resolveProjectId(project);
279
+ const { projectId, rest } = parseCommon(args, ["--include-managed"]);
280
+ if (rest.length > 0) fail({ code: "BAD_USAGE", message: `Unexpected argument for domains list: ${rest[0]}` });
163
281
  try {
164
- const data = await getSdk().domains.list(projectId, { authMode });
165
- console.log(JSON.stringify(data, null, 2));
282
+ print(await getSdk().domains.list(projectId));
166
283
  } catch (err) {
167
284
  reportSdkError(err);
168
285
  }
169
286
  }
170
287
 
171
288
  async function status(args) {
172
- const { project, authMode, rest } = parseProjectFlag(args);
289
+ const { projectId, rest } = parseCommon(args);
173
290
  const domain = rest[0];
174
- if (!domain) {
175
- fail({
176
- code: "BAD_USAGE",
177
- message: "Missing <domain>.",
178
- hint: "run402 domains status <domain> [--project <id>]",
179
- });
180
- }
181
- if (rest.length > 1) {
182
- fail({ code: "BAD_USAGE", message: `Unexpected argument for domains status: ${rest[1]}` });
183
- }
184
- const projectId = resolveProjectId(project);
291
+ if (!domain) fail({ code: "BAD_USAGE", message: "Missing <domain>.", hint: "run402 domains status <domain> [--project <id>]" });
292
+ if (rest.length > 1) fail({ code: "BAD_USAGE", message: `Unexpected argument for domains status: ${rest[1]}` });
185
293
  try {
186
- const data = await getSdk().domains.status(projectId, domain, { authMode });
187
- console.log(JSON.stringify(data, null, 2));
294
+ print(await getSdk().domains.get(projectId, domain));
188
295
  } catch (err) {
189
296
  reportSdkError(err);
190
297
  }
191
298
  }
192
299
 
193
- async function deleteDomain(args) {
194
- const { project, authMode, rest, args: parsedArgs } = parseProjectFlag(args, ["--confirm"]);
300
+ async function dns(args) {
301
+ const parsed = normalizeArgv(args);
302
+ const valueFlags = ["--project", "--format"];
303
+ assertKnownFlags(parsed, [...valueFlags, "--help", "-h"], valueFlags);
304
+ const rest = positionalArgs(parsed, valueFlags);
195
305
  const domain = rest[0];
196
- if (!domain) {
197
- fail({
198
- code: "BAD_USAGE",
199
- message: "Missing <domain>.",
200
- hint: "run402 domains delete <domain> --confirm [--project <id>]",
201
- });
306
+ if (!domain) fail({ code: "BAD_USAGE", message: "Missing <domain>.", hint: "run402 domains dns <domain> [--format json|bind]" });
307
+ const format = flagValue(parsed, "--format") ?? "json";
308
+ if (!["json", "bind"].includes(format)) fail({ code: "BAD_FLAG", message: "--format must be json or bind.", details: { flag: "--format", value: format } });
309
+ const projectId = resolveProjectId(flagValue(parsed, "--project"));
310
+ try {
311
+ const data = await getSdk().domains.get(projectId, domain);
312
+ if (format === "bind") {
313
+ console.log(data.dns_records.map((record) => record.bind).filter(Boolean).join("\n"));
314
+ } else {
315
+ print({ domain: data.domain, dns_records: data.dns_records, checks: data.checks, next_action: data.next_action });
316
+ }
317
+ } catch (err) {
318
+ reportSdkError(err);
202
319
  }
203
- if (rest.length > 1) {
204
- fail({ code: "BAD_USAGE", message: `Unexpected argument for domains delete: ${rest[1]}` });
320
+ }
321
+
322
+ async function action(name, args) {
323
+ const extraValue = name === "test-receive" ? ["--to"] : name === "wait" ? ["--until", "--timeout-ms", "--interval-ms"] : name === "apply" ? ["--authority"] : [];
324
+ const parsed = normalizeArgv(args);
325
+ const valueFlags = ["--project", ...extraValue];
326
+ assertKnownFlags(parsed, [...valueFlags, "--help", "-h"], valueFlags);
327
+ const rest = positionalArgs(parsed, valueFlags);
328
+ const domain = rest[0];
329
+ if (!domain) fail({ code: "BAD_USAGE", message: `Missing <domain>.`, hint: `run402 domains ${name} <domain> [--project <id>]` });
330
+ if (rest.length > 1) fail({ code: "BAD_USAGE", message: `Unexpected argument for domains ${name}: ${rest[1]}` });
331
+ const projectId = resolveProjectId(flagValue(parsed, "--project"));
332
+ try {
333
+ const sdk = getSdk().domains;
334
+ if (name === "check") return print(await sdk.check(projectId, domain));
335
+ if (name === "apply") return print(await sdk.apply(projectId, domain));
336
+ if (name === "repair") return print(await sdk.repair(projectId, domain));
337
+ if (name === "activate") return print(await sdk.activate(projectId, domain));
338
+ if (name === "test-receive") {
339
+ const to = flagValue(parsed, "--to");
340
+ if (!to) fail({ code: "BAD_FLAG", message: "--to is required for domains test-receive.", details: { flag: "--to" } });
341
+ return print(await sdk.testReceive(projectId, domain, to));
342
+ }
343
+ if (name === "wait") {
344
+ const until = flagValue(parsed, "--until") ?? "active";
345
+ if (!["active", "safe", "receive-active"].includes(until)) fail({ code: "BAD_FLAG", message: "--until must be active, safe, or receive-active.", details: { flag: "--until", value: until } });
346
+ const timeoutMs = parseIntegerFlag("--timeout-ms", flagValue(parsed, "--timeout-ms"), { min: 1, def: 120000 });
347
+ const intervalMs = parseIntegerFlag("--interval-ms", flagValue(parsed, "--interval-ms"), { min: 1, def: 5000 });
348
+ return print(await sdk.wait(projectId, domain, { until, timeoutMs, intervalMs }));
349
+ }
350
+ } catch (err) {
351
+ reportSdkError(err);
205
352
  }
206
- if (!parsedArgs.includes("--confirm")) {
353
+ }
354
+
355
+ async function disconnect(args) {
356
+ const parsed = normalizeArgv(args);
357
+ const valueFlags = ["--project"];
358
+ assertKnownFlags(parsed, [...valueFlags, "--confirm", "--help", "-h"], valueFlags);
359
+ const rest = positionalArgs(parsed, valueFlags);
360
+ const domain = rest[0];
361
+ if (!domain) fail({ code: "BAD_USAGE", message: "Missing <domain>.", hint: "run402 domains disconnect <domain> --confirm [--project <id>]" });
362
+ if (!parsed.includes("--confirm")) {
207
363
  fail({
208
364
  code: "CONFIRMATION_REQUIRED",
209
- message: `Destructive: releasing custom domain '${domain}' detaches it from this project and clears its DNS/SSL configuration. This is irreversible. Re-run with --confirm to proceed.`,
365
+ message: `Disconnecting ${domain} removes the desired custom binding. Re-run with --confirm to proceed.`,
210
366
  details: { domain },
211
367
  });
212
368
  }
213
- const projectId = resolveProjectId(project);
369
+ const projectId = resolveProjectId(flagValue(parsed, "--project"));
214
370
  try {
215
- await getSdk().domains.remove(domain, { projectId, authMode });
216
- console.log(JSON.stringify({ domain, project_id: projectId, released: true }));
371
+ print(await getSdk().domains.disconnect(projectId, domain));
217
372
  } catch (err) {
218
373
  reportSdkError(err);
219
374
  }
220
375
  }
221
376
 
222
377
  export async function run(sub, args) {
223
- if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
224
- if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
378
+ if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
379
+ if ((args ?? []).includes("--help") || (args ?? []).includes("-h")) {
380
+ const help = SUB_HELP[sub];
381
+ if (help) {
382
+ console.log(help);
383
+ process.exit(0);
384
+ }
385
+ }
225
386
  switch (sub) {
226
- case "add": await add(args); break;
227
- case "list": await list(args); break;
387
+ case "connect": await connect(args); break;
388
+ case "list": await list(args); break;
228
389
  case "status": await status(args); break;
229
- case "delete": await deleteDomain(args); break;
390
+ case "dns": await dns(args); break;
391
+ case "check": await action("check", args); break;
392
+ case "apply": await action("apply", args); break;
393
+ case "repair": await action("repair", args); break;
394
+ case "test-receive": await action("test-receive", args); break;
395
+ case "wait": await action("wait", args); break;
396
+ case "activate": await action("activate", args); break;
397
+ case "disconnect": await disconnect(args); break;
398
+ case "add": removed("run402 domains add", "run402 domains connect <domain> --project <id> --web"); break;
399
+ case "delete": removed("run402 domains delete", "run402 domains disconnect <domain> --project <id> --confirm"); break;
230
400
  default:
231
401
  fail({ code: "UNKNOWN_SUBCOMMAND", message: `Unknown domains subcommand: ${sub}`, hint: "Run `run402 domains --help` for usage.", details: { command: "domains", subcommand: sub } });
232
402
  }