Skip to content
Merged
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
6 changes: 4 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,10 +539,12 @@ List all custom domains and their status (active/pending).
### `void domain status`

```
void domain status <hostname> [--project <name>]
void domain status <hostname> [--project <name>] [--verbose]
```

Check verification and SSL status for a specific domain. Shows hostname, status, SSL status, and any verification errors.
Check verification and SSL status for a specific domain. Prints a rolled-up state (`awaiting_dns`, `verifying_dns`, `issuing_cert`, `deploying_cert`, `awaiting_deployment`, `active`, `error`, or `pending`) with a one-line diagnostic explaining what Cloudflare is doing and any user action required. When DNS records are still pending, the command also prints the exact TXT records to add at your DNS provider.

Pass `--verbose` to additionally print the raw multi-line status breakdown (DB status, SSL status, ownership state, verification errors) underneath the rollup.

Project resolution for domain commands follows the same order as deploy (`--project`, `VOID_PROJECT`, linked project).

Expand Down
2 changes: 2 additions & 0 deletions packages/void/src/cli/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ export type DomainResponse = {
export type DomainStatusResponse = DomainResponse & {
ssl_status: string;
verification_errors?: Array<string>;
cf_status?: string;
awaiting_deployment?: boolean;
};

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/void/src/cli/domain-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type DomainArgs = {
subcommand: 'add' | 'delete' | 'list' | 'status';
hostname?: string;
projectSlug?: string;
verbose?: boolean;
};

export function parseDomainArgs(args: Array<string>): DomainArgs {
Expand All @@ -23,11 +24,14 @@ export function parseDomainArgs(args: Array<string>): DomainArgs {

let hostname: string | undefined;
let projectSlug: string | undefined;
let verbose = false;

const rest = args.slice(1);
for (let i = 0; i < rest.length; i++) {
if (rest[i] === '--project' && rest[i + 1]) {
projectSlug = rest[++i];
} else if (rest[i] === '--verbose') {
verbose = true;
} else if (!hostname) {
hostname = rest[i];
}
Expand All @@ -37,5 +41,5 @@ export function parseDomainArgs(args: Array<string>): DomainArgs {
throw new Error(`domain: Usage: void domain ${subcommand} <hostname> [--project <name>].`);
}

return { subcommand, hostname, projectSlug };
return { subcommand, hostname, projectSlug, verbose };
}
138 changes: 118 additions & 20 deletions packages/void/src/cli/domain.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { log, note } from '@clack/prompts';
import { getToken } from './auth.ts';
import { PlatformClient } from './client.ts';
import { type DomainStatusResponse, PlatformClient } from './client.ts';
import type { DomainArgs } from './domain-args.ts';
import { pc } from './output.ts';
import { readProjectConfig } from './project.ts';
Expand Down Expand Up @@ -43,7 +43,7 @@ export async function runDomainCommand(root: string, args: DomainArgs): Promise<
case 'list':
return domainList(client, projectId!);
case 'status':
return domainStatus(client, projectId!, args.hostname!);
return domainStatus(client, projectId!, args.hostname!, args.verbose === true);
}
}

Expand All @@ -55,16 +55,18 @@ async function domainAdd(
const domain = await client.addDomain(projectId, hostname);
log.success(`Domain ${hostname} added (status: ${domain.status})`);

const sections: Array<string> = [];
// Named locals make the section order explicit (CNAME -> ownership -> SSL),
// rather than relying on the order of conditional pushes onto an array.
let cnameSection: string | undefined;
let ownershipSection: string | undefined;
let sslSection: string | undefined;

if (domain.cname_target) {
sections.push(`CNAME (traffic)\n ${hostname} → ${domain.cname_target}`);
cnameSection = `CNAME (traffic)\n ${hostname} → ${domain.cname_target}`;
}

if (domain.ownership_verification) {
sections.push(
`TXT (ownership verification)\n ${domain.ownership_verification.name} → ${domain.ownership_verification.value}`,
);
ownershipSection = `TXT (ownership verification)\n ${domain.ownership_verification.name} → ${domain.ownership_verification.value}`;
}

const sslRecords = (domain.ssl_validation_records ?? []).filter(
Expand All @@ -73,14 +75,23 @@ async function domainAdd(
);
if (sslRecords.length > 0) {
const lines = sslRecords.map((r) => ` ${r.txt_name} → ${r.txt_value}`).join('\n');
sections.push(`TXT (SSL validation)\n${lines}`);
sslSection = `TXT (SSL validation)\n${lines}`;
} else {
// Deadline-miss path: API short-poll didn't see SSL records before timeout.
// Tell the user they'll surface in `domain status` instead of silently
// omitting the section.
sslSection = `SSL validation records will appear in \`void domain status\` shortly.`;
}

const summary = `Add the records above to your DNS, then run \`void domain status ${hostname}\` to check progress.`;

const sections = [cnameSection, ownershipSection, sslSection, summary].filter(
(s) => s !== undefined,
);

if (sections.length > 0) {
note(sections.join('\n\n'), 'DNS Setup');
}

log.info('Run `void domain status ' + hostname + '` to check verification progress.');
}

async function domainRemove(
Expand Down Expand Up @@ -114,10 +125,86 @@ async function domainList(client: PlatformClient, projectId: string): Promise<vo
note(lines.join('\n'), 'Custom Domains');
}

export type RolledUpStatus = {
state:
| 'error'
| 'awaiting_deployment'
| 'awaiting_dns'
| 'verifying_dns'
| 'issuing_cert'
| 'deploying_cert'
| 'active'
| 'pending';
diagnostic: string;
};

export function computeRollup(status: DomainStatusResponse): RolledUpStatus {
// Priority order matters: `error` and `awaiting_deployment` describe
// user-actionable conditions that should override CF's state-machine view.
if (status.verification_errors && status.verification_errors.length > 0) {
return {
state: 'error',
diagnostic: `Verification failed: ${status.verification_errors[0]}`,
};
}

if (status.awaiting_deployment === true) {
return {
state: 'awaiting_deployment',
diagnostic:
'Your project has no live deployment yet. Run `void deploy` to bring the domain online.',
};
}

if (status.cf_status === 'pending' && status.ownership_verification) {
return {
state: 'awaiting_dns',
diagnostic:
'Cloudflare has not yet observed your _cf-custom-hostname TXT record. Confirm it is published at the host shown below.',
};
}
Comment thread
cursor[bot] marked this conversation as resolved.

if (status.cf_status === 'pending_validation') {
return {
state: 'verifying_dns',
diagnostic: 'DNS records found, Cloudflare is validating ownership.',
};
}

if (
status.cf_status === 'pending_issuance' ||
(status.cf_status === 'active' && status.ssl_status !== 'active')
) {
return {
state: 'issuing_cert',
diagnostic: 'Cloudflare is issuing the SSL certificate (typically <5 minutes).',
};
}

if (status.cf_status === 'pending_deployment') {
return {
state: 'deploying_cert',
diagnostic: 'SSL certificate issued; Cloudflare is propagating it to its edge.',
};
}

// `active` is keyed off the DB status, but only reached after the per-field
// signals above have already surfaced cert-renewal / re-validation windows
// (e.g. `issuing_cert` catches `cf_status === 'active' && ssl_status !== 'active'`
// even when the DB row says active). Reaching this branch means CF and SSL
// are both healthy.
if (status.status === 'active' && status.ssl_status === 'active') {
return { state: 'active', diagnostic: 'Live.' };
}
Comment thread
cursor[bot] marked this conversation as resolved.

return { state: 'pending', diagnostic: 'Verification pending. Cloudflare is processing.' };
}

async function domainStatus(
client: PlatformClient,
projectId: string,
hostname: string,
verbose: boolean,
): Promise<void> {
const domains = await client.listDomains(projectId);
const domain = domains.find((d) => d.hostname === hostname);
Expand All @@ -127,18 +214,29 @@ async function domainStatus(
}

const status = await client.getDomainStatus(projectId, domain.id);
const lines = [
`Hostname: ${status.hostname}`,
`Status: ${status.status}`,
`SSL Status: ${status.ssl_status}`,
const rollup = computeRollup(status);

const rollupLines = [
`Hostname: ${status.hostname}`,
`Status: ${rollup.state} — ${rollup.diagnostic}`,
];
if (status.ownership_verification) {
lines.push(`Ownership: pending`);
}
if (status.verification_errors && status.verification_errors.length > 0) {
lines.push(`Errors: ${status.verification_errors.join(', ')}`);

if (verbose) {
// Visual separator: blank line + "Details:" header so the raw breakdown
// reads as a distinct section under the rollup, not just a continuation.
// `Hostname:` is already shown by the rollup above, so it's omitted here.
rollupLines.push('', 'Details:');
rollupLines.push(` Status: ${status.status}`);
rollupLines.push(` SSL Status: ${status.ssl_status}`);
if (status.ownership_verification) {
rollupLines.push(` Ownership: pending`);
}
if (status.verification_errors && status.verification_errors.length > 0) {
rollupLines.push(` Errors: ${status.verification_errors.join(', ')}`);
}
}
note(lines.join('\n'), 'Domain Status');

note(rollupLines.join('\n'), 'Domain Status');

// Gate on actual validation states, not the aggregate `status.status`.
// Once the DB row is flipped to `active`, the API keeps returning
Expand Down
16 changes: 12 additions & 4 deletions packages/void/src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,18 @@ const HELP_TREE: HelpTree = {
'void domain delete <hostname> [options]',
),
list: withProjectOption('List custom domains and status.', 'void domain list [options]'),
status: withProjectOption(
'Check domain verification status.',
'void domain status <hostname> [options]',
),
status: {
summary:
'Check domain verification status. Prints a rolled-up state with a one-line diagnostic.',
usage: ['void domain status <hostname> [options]'],
options: [
{ command: '--project <name>', description: 'Project name override' },
{
command: '--verbose',
description: 'Also print the raw multi-line status breakdown',
},
],
},
},
},
db: {
Expand Down
Loading