From b788b83c9cfa6156c6efc084d908740ca9feeca8 Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Thu, 9 Apr 2026 06:16:02 +0200 Subject: [PATCH] fix: silence stray clack writes during init channel add addChannelInternal is invoked from `capgo init` with silent=true while the Ink onboarding UI owns stdout. Several helpers it transitively reaches still wrote directly to process.stdout via @clack/prompts, which broke the Ink frame and caused the 'Capgo OTA Onboarding' header to be repainted twice when the user picked 'production' in the channel step. Continuing the whack-a-mole pattern from #560, thread an opt-in silent flag through the offending helpers and gate every clack write with it: - getConfig, getLocalConfig, getRemoteConfig, createSupabaseClient now accept silent and propagate it through the call chain. - sendEvent always loads remote config silently and only logs telemetry failures when verbose is set, since it is invoked in fire-and-forget contexts (including init markStep -> markSnag). - addChannelInternal forwards its existing silent flag to getConfig, createSupabaseClient, and findUnknownVersion. No interceptor / monkey-patching: each fix is at the actual call site. --- src/channel/add.ts | 6 +++--- src/utils.ts | 38 +++++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/channel/add.ts b/src/channel/add.ts index 4e870a60..f429c20f 100644 --- a/src/channel/add.ts +++ b/src/channel/add.ts @@ -19,7 +19,7 @@ export async function addChannelInternal(channelId: string, appId: string, optio intro('Create channel') options.apikey = options.apikey || findSavedKey() - const extConfig = await getConfig().catch(() => undefined) + const extConfig = await getConfig(silent).catch(() => undefined) appId = getAppId(appId, extConfig?.config) if (!options.apikey) { @@ -34,7 +34,7 @@ export async function addChannelInternal(channelId: string, appId: string, optio throw new Error('Missing appId') } - const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon) + const supabase = await createSupabaseClient(options.apikey, options.supaHost, options.supaAnon, silent) await check2FAComplianceForApp(supabase, appId, silent) await verifyUser(supabase, options.apikey, ['write', 'all']) await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, OrganizationPerm.admin, silent, true) @@ -42,7 +42,7 @@ export async function addChannelInternal(channelId: string, appId: string, optio if (!silent) log.info(`Creating channel ${appId}#${channelId} to Capgo`) - const data = await findUnknownVersion(supabase, appId) + const data = await findUnknownVersion(supabase, appId, { silent }) if (!data) { if (!silent) log.error('Cannot find default version for channel creation, please contact Capgo support 🤨') diff --git a/src/utils.ts b/src/utils.ts index 746d1d17..9d35559c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -488,19 +488,21 @@ export async function getAllPackagesDependencies(f: string = findRoot(cwd()), fi return dependencies } -export async function getConfig() { +export async function getConfig(silent = false) { try { const extConfig = await loadConfig() if (!extConfig) { const message = 'No capacitor config file found, run `cap init` first' - log.error(message) + if (!silent) + log.error(message) throw new Error(message) } return extConfig } catch (err) { const message = `No capacitor config file found, run \`cap init\` first ${formatError(err)}` - log.error(message) + if (!silent) + log.error(message) throw new Error(message) } } @@ -527,9 +529,9 @@ export async function updateConfigUpdater(newConfig: any): Promise(config.supaHost, config.supaKey, { @@ -1395,7 +1401,9 @@ export async function sendEvent(capgkey: string, payload: TrackOptions & { notif if (verbose) { log.info(`Get remove config: for ${payload.event}`) } - const config = await getRemoteConfig() + // Always fetch remote config silently — sendEvent is telemetry and must + // not bypass an Ink-controlled stdout (e.g. during `capgo init`). + const config = await getRemoteConfig(true) if (verbose) { log.info(`Sending LogSnag event: ${JSON.stringify(payload)}`) } @@ -1421,7 +1429,7 @@ export async function sendEvent(capgkey: string, payload: TrackOptions & { notif const response = await fetchResponse.json() as { error?: string } - if (response.error) { + if (response.error && verbose) { log.error(`Failed to send LogSnag event: ${response.error}`) } }