@@ -112,7 +112,7 @@ import {
112112 renderGlobalCaddyCompose ,
113113 renderGlobalCoreDnsConfig ,
114114} from "../templates.ts" ;
115- import { display } from "../ui/display.ts" ;
115+ import { type DisplayStatusItem , display } from "../ui/display.ts" ;
116116import { getFzfPath } from "../ui/fzf.ts" ;
117117import { getGumPath } from "../ui/gum.ts" ;
118118import {
@@ -609,7 +609,7 @@ const handleDoctor: CommandHandlerFor<typeof doctorSpec> = async ({
609609
610610 await renderDoctorSummary ( results ) ;
611611 emitSlowChecksNote ( results ) ;
612- renderMacNote ( results ) ;
612+ await renderMacNote ( results ) ;
613613 if ( ! args . options . fix ) {
614614 await renderRecoveryGuidance ( results ) ;
615615 }
@@ -3104,11 +3104,18 @@ async function ensureIngressNetwork(): Promise<{
31043104 }
31053105
31063106 if ( ingress . exists ) {
3107- await run ( [ "docker" , "network" , "rm" , DEFAULT_INGRESS_NETWORK ] , {
3108- stdin : "inherit" ,
3109- } ) ;
3107+ const removed = await exec (
3108+ [ "docker" , "network" , "rm" , DEFAULT_INGRESS_NETWORK ] ,
3109+ {
3110+ stdin : "ignore" ,
3111+ }
3112+ ) ;
3113+ if ( removed . exitCode !== 0 ) {
3114+ note ( commandFailureDetail ( { result : removed } ) , "runtime repair" ) ;
3115+ return ingress ;
3116+ }
31103117 }
3111- await run (
3118+ const created = await exec (
31123119 [
31133120 "docker" ,
31143121 "network" ,
@@ -3119,8 +3126,11 @@ async function ensureIngressNetwork(): Promise<{
31193126 "--gateway" ,
31203127 DEFAULT_INGRESS_GATEWAY ,
31213128 ] ,
3122- { stdin : "inherit " }
3129+ { stdin : "ignore " }
31233130 ) ;
3131+ if ( created . exitCode !== 0 ) {
3132+ note ( commandFailureDetail ( { result : created } ) , "runtime repair" ) ;
3133+ }
31243134
31253135 return await inspectDockerNetwork ( DEFAULT_INGRESS_NETWORK ) ;
31263136}
@@ -3131,9 +3141,15 @@ async function ensureLoggingNetwork(): Promise<void> {
31313141 return ;
31323142 }
31333143
3134- await run ( [ "docker" , "network" , "create" , DEFAULT_LOGGING_NETWORK ] , {
3135- stdin : "inherit" ,
3136- } ) ;
3144+ const created = await exec (
3145+ [ "docker" , "network" , "create" , DEFAULT_LOGGING_NETWORK ] ,
3146+ {
3147+ stdin : "ignore" ,
3148+ }
3149+ ) ;
3150+ if ( created . exitCode !== 0 ) {
3151+ note ( commandFailureDetail ( { result : created } ) , "runtime repair" ) ;
3152+ }
31373153}
31383154
31393155async function maybeStartGlobalCaddyCompose ( opts : {
@@ -3143,7 +3159,7 @@ async function maybeStartGlobalCaddyCompose(opts: {
31433159 return ;
31443160 }
31453161
3146- await run (
3162+ const started = await exec (
31473163 [
31483164 "docker" ,
31493165 "compose" ,
@@ -3155,9 +3171,27 @@ async function maybeStartGlobalCaddyCompose(opts: {
31553171 ] ,
31563172 {
31573173 cwd : dirname ( opts . paths . caddyCompose ) ,
3158- stdin : "inherit " ,
3174+ stdin : "ignore " ,
31593175 }
31603176 ) ;
3177+ if ( started . exitCode !== 0 ) {
3178+ note ( commandFailureDetail ( { result : started } ) , "runtime repair" ) ;
3179+ return ;
3180+ }
3181+ note ( "CoreDNS and Caddy are ready." , "runtime repair" ) ;
3182+ }
3183+
3184+ function commandFailureDetail ( input : {
3185+ readonly result : {
3186+ readonly exitCode : number ;
3187+ readonly stdout : string ;
3188+ readonly stderr : string ;
3189+ } ;
3190+ } ) : string {
3191+ const detail = input . result . stderr . trim ( ) || input . result . stdout . trim ( ) ;
3192+ return detail . length > 0
3193+ ? `Repair command failed: ${ detail } `
3194+ : `Repair command failed (exit ${ input . result . exitCode } ).` ;
31613195}
31623196
31633197async function maybeExportCaddyCaCert ( opts : {
@@ -3562,26 +3596,85 @@ function isHiddenDoctorCheck(result: RecoveryCheckResult): boolean {
35623596 return result . name === "tickets git" ;
35633597}
35643598
3599+ export function buildDoctorSummaryStatusItems ( input : {
3600+ readonly results : readonly RecoveryCheckResult [ ] ;
3601+ } ) : readonly DisplayStatusItem [ ] {
3602+ const items : DisplayStatusItem [ ] = [ ] ;
3603+
3604+ for ( const group of DOCTOR_SUMMARY_GROUPS ) {
3605+ const members = input . results . filter ( ( result ) =>
3606+ group . checks . has ( result . name )
3607+ ) ;
3608+ if ( members . length === 0 ) {
3609+ continue ;
3610+ }
3611+ items . push ( buildDoctorSummaryStatusItem ( { title : group . title , members } ) ) ;
3612+ }
3613+
3614+ const ungrouped = input . results . filter (
3615+ ( result ) =>
3616+ ! (
3617+ isHiddenDoctorCheck ( result ) ||
3618+ DOCTOR_SUMMARY_GROUPS . some ( ( group ) => group . checks . has ( result . name ) )
3619+ )
3620+ ) ;
3621+ if ( ungrouped . length > 0 ) {
3622+ items . push (
3623+ buildDoctorSummaryStatusItem ( {
3624+ title : "Other checks" ,
3625+ members : ungrouped ,
3626+ } )
3627+ ) ;
3628+ }
3629+
3630+ return items ;
3631+ }
3632+
3633+ function buildDoctorSummaryStatusItem ( input : {
3634+ readonly title : string ;
3635+ readonly members : readonly RecoveryCheckResult [ ] ;
3636+ } ) : DisplayStatusItem {
3637+ const issues = input . members . filter (
3638+ ( result ) => result . status !== "ok" && ! isIgnorableDoctorSummaryIssue ( result )
3639+ ) ;
3640+ const errorCount = issues . filter (
3641+ ( result ) => result . status === "error"
3642+ ) . length ;
3643+ const warningCount = issues . length - errorCount ;
3644+ let status : DisplayStatusItem [ "status" ] = "ok" ;
3645+ if ( errorCount > 0 ) {
3646+ status = "error" ;
3647+ } else if ( warningCount > 0 ) {
3648+ status = "warn" ;
3649+ }
3650+
3651+ const issueCounts = [
3652+ errorCount > 0 ? `${ errorCount } error${ errorCount === 1 ? "" : "s" } ` : null ,
3653+ warningCount > 0
3654+ ? `${ warningCount } warning${ warningCount === 1 ? "" : "s" } `
3655+ : null ,
3656+ ] . filter ( ( part ) : part is string => part !== null ) ;
3657+
3658+ return {
3659+ label : input . title ,
3660+ status,
3661+ meta :
3662+ issueCounts . length > 0
3663+ ? issueCounts . join ( ", " )
3664+ : `${ input . members . length } checks` ,
3665+ detail :
3666+ issues . length > 0
3667+ ? summarizeDoctorGroupIssues ( { title : input . title , issues } )
3668+ : undefined ,
3669+ } ;
3670+ }
3671+
35653672async function renderDoctorSummary (
35663673 results : readonly TimedCheckResult [ ]
35673674) : Promise < void > {
3568- const summaryLines = buildDoctorSummaryLines ( { results } ) ;
3569- let tone : "error" | "warn" | "info" = "info" ;
3570- if ( results . some ( ( result ) => result . status === "error" ) ) {
3571- tone = "error" ;
3572- } else if (
3573- results . some (
3574- ( result ) =>
3575- result . status === "warn" && ! isIgnorableDoctorSummaryIssue ( result )
3576- )
3577- ) {
3578- tone = "warn" ;
3579- }
3580-
3581- await display . panel ( {
3675+ await display . statusList ( {
35823676 title : "Doctor summary" ,
3583- tone,
3584- lines : summaryLines ,
3677+ items : buildDoctorSummaryStatusItems ( { results } ) ,
35853678 } ) ;
35863679}
35873680
@@ -3649,23 +3742,34 @@ function summarizeDoctorIssue(issue: RecoveryCheckResult): string {
36493742 return `${ issue . name } : ${ issue . message } ` ;
36503743}
36513744
3652- function renderMacNote ( results : readonly RecoveryCheckResult [ ] ) : void {
3653- const dnsNeedsSetup = results . some (
3745+ async function renderMacNote (
3746+ results : readonly TimedCheckResult [ ]
3747+ ) : Promise < void > {
3748+ const hasResolverIssue = results . some (
36543749 ( result ) =>
3655- DOCTOR_SUMMARY_GROUPS . find (
3656- ( group ) => group . title === "Resolver & DNS"
3657- ) ?. checks . has ( result . name ) && result . status !== "ok"
3750+ result . status !== "ok" &&
3751+ ( result . name . startsWith ( "resolver:" ) ||
3752+ result . name . startsWith ( "dnsmasq.conf:" ) )
36583753 ) ;
3659- if ( isMac ( ) && dnsNeedsSetup ) {
3660- note (
3661- [
3662- "macOS tip:" ,
3663- `- wildcard DNS: /etc/resolver/${ DEFAULT_PROJECT_TLD } + dnsmasq address=/.${ DEFAULT_PROJECT_TLD } /<reachable-ingress-target>` ,
3664- `- OAuth alias DNS: /etc/resolver/${ DEFAULT_OAUTH_ALIAS_ROOT } + dnsmasq address=/.${ DEFAULT_OAUTH_ALIAS_ROOT } /<reachable-ingress-target>` ,
3665- ] . join ( "\n" ) ,
3666- "doctor"
3667- ) ;
3754+ if ( ! ( isMac ( ) && hasResolverIssue ) ) {
3755+ return ;
36683756 }
3757+
3758+ await display . statusList ( {
3759+ title : "macOS resolver setup" ,
3760+ items : [
3761+ {
3762+ label : `*.${ DEFAULT_PROJECT_TLD } ` ,
3763+ status : "info" ,
3764+ detail : `/etc/resolver/${ DEFAULT_PROJECT_TLD } with dnsmasq address=/.${ DEFAULT_PROJECT_TLD } /<reachable-ingress-target>` ,
3765+ } ,
3766+ {
3767+ label : `*.${ DEFAULT_OAUTH_ALIAS_ROOT } ` ,
3768+ status : "info" ,
3769+ detail : `/etc/resolver/${ DEFAULT_OAUTH_ALIAS_ROOT } with dnsmasq address=/.${ DEFAULT_OAUTH_ALIAS_ROOT } /<reachable-ingress-target>` ,
3770+ } ,
3771+ ] ,
3772+ } ) ;
36693773}
36703774
36713775async function resolvePreferredMacHostDnsTarget ( ) : Promise < string > {
0 commit comments