Skip to content

Commit b6d7752

Browse files
fix: prevent @clack/prompts stdout writes from corrupting Ink onboarding UI (#560)
During init, functions like findProjectType() and checkAlerts() wrote directly to stdout via @clack/prompts while the Ink terminal UI had control, causing garbled rendering and a broken header banner. - Split checkAlerts into checkVersionStatus (pure data) + checkAlerts (clack UI) - Init flow now uses checkVersionStatus and renders an Ink Alert for outdated CLI - findProjectType accepts { quiet: true } to suppress clack logging during init - loginInternal skips clack output when called with silent=true from init
1 parent 89a6669 commit b6d7752

File tree

6 files changed

+65
-21
lines changed

6 files changed

+65
-21
lines changed

src/api/update.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,29 @@ import { log } from '@clack/prompts'
22
import pack from '../../package.json'
33
import { getLatestVersion } from '../utils/latest-version'
44

5-
export async function checkAlerts() {
5+
export interface VersionCheckResult {
6+
currentVersion: string
7+
latestVersion: string
8+
isOutdated: boolean
9+
majorVersion: string
10+
}
11+
12+
export async function checkVersionStatus(): Promise<VersionCheckResult> {
613
const latest = await getLatestVersion('@capgo/cli') ?? ''
7-
const major = latest?.split('.')[0]
8-
if (latest !== pack.version) {
9-
log.warning(`🚨 You are using @capgo/cli@${pack.version} it's not the latest version.
10-
Please use @capgo/cli@${latest}" or @capgo/cli@${major} to keep up to date with the latest features and bug fixes.`,
14+
const major = latest?.split('.')[0] ?? ''
15+
return {
16+
currentVersion: pack.version,
17+
latestVersion: latest,
18+
isOutdated: !!latest && latest !== pack.version,
19+
majorVersion: major,
20+
}
21+
}
22+
23+
export async function checkAlerts() {
24+
const { isOutdated, currentVersion, latestVersion, majorVersion } = await checkVersionStatus()
25+
if (isOutdated) {
26+
log.warning(`🚨 You are using @capgo/cli@${currentVersion} it's not the latest version.
27+
Please use @capgo/cli@${latestVersion}" or @capgo/cli@${majorVersion} to keep up to date with the latest features and bug fixes.`,
1128
)
1229
}
13-
// check if the app use old encryption key and if so alert the user it not secure enough and it should migrate on v2
1430
}

src/init/command.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { cwd, env, exit, platform, stdin, stdout } from 'node:process'
88
import { canParse, format, increment, lessThan, parse } from '@std/semver'
99
import tmp from 'tmp'
1010
import { checkAppIdsExist, completePendingOnboardingApp, listPendingOnboardingApps } from '../api/app'
11-
import { checkAlerts } from '../api/update'
11+
import { checkVersionStatus } from '../api/update'
1212
import { addAppInternal } from '../app/add'
1313
import { markSnag, waitLog } from '../app/debug'
1414
import { canUseFilePicker, openPackageJsonPicker } from '../build/onboarding/file-picker'
@@ -21,7 +21,7 @@ import { doLoginExists, loginInternal } from '../login'
2121
import { showReplicationProgress } from '../replicationProgress'
2222
import { createSupabaseClient, findBuildCommandForProjectType, findMainFile, findMainFileForProjectType, findProjectType, findRoot, findSavedKey, formatError, getAllPackagesDependencies, getAppId, getBundleVersion, getConfig, getInstalledVersion, getLocalConfig, getNativeProjectResetAdvice, getPackageScripts, getPMAndCommand, PACKNAME, projectIsMonorepo, updateConfigbyKey, updateConfigUpdater, validateIosUpdaterSync, verifyUser } from '../utils'
2323
import { cancel as pCancel, confirm as pConfirm, intro as pIntro, isCancel as pIsCancel, log as pLog, outro as pOutro, select as pSelect, spinner as pSpinner, text as pText } from './prompts'
24-
import { stopInitInkSession } from './runtime'
24+
import { setInitVersionWarning, stopInitInkSession } from './runtime'
2525
import { formatInitResumeMessage, initOnboardingSteps, renderInitOnboardingComplete, renderInitOnboardingFrame, renderInitOnboardingWelcome } from './ui'
2626

2727
interface SuperOptions extends Options {
@@ -329,7 +329,7 @@ async function ensureWorkspaceReadyForInit(initialAppId?: string): Promise<strin
329329
const nearestCapacitorConfig = findNearestCapacitorConfig(currentDir)
330330
const nearestPackageJson = findNearestPackageJson(currentDir)
331331
const projectDir = nearestCapacitorConfig?.dir || (nearestPackageJson ? dirname(nearestPackageJson) : currentDir)
332-
const projectType = await findProjectType()
332+
const projectType = await findProjectType({ quiet: true })
333333
const frameworkKind = getFrameworkKind(projectType)
334334

335335
if (nearestCapacitorConfig?.dir === currentDir) {
@@ -2261,7 +2261,10 @@ export async function initApp(apikeyCommand: string, appId: string, options: Sup
22612261
pIntro('Capgo onboarding')
22622262
renderInitOnboardingWelcome(initOnboardingSteps.length)
22632263
appId = await ensureWorkspaceReadyForInit(appId) ?? appId
2264-
await checkAlerts()
2264+
const versionStatus = await checkVersionStatus()
2265+
if (versionStatus.isOutdated) {
2266+
setInitVersionWarning(versionStatus.currentVersion, versionStatus.latestVersion, versionStatus.majorVersion)
2267+
}
22652268

22662269
let extConfig: Awaited<ReturnType<typeof getConfig>> | undefined
22672270
if (!options.supaAnon || !options.supaHost) {
@@ -2300,7 +2303,7 @@ export async function initApp(apikeyCommand: string, appId: string, options: Sup
23002303
if (!doLoginExists() || apikeyCommand) {
23012304
log.start(`Running: ${pm.runner} @capgo/cli@latest login ***`)
23022305
try {
2303-
await loginInternal(options.apikey, options, false)
2306+
await loginInternal(options.apikey, options, true)
23042307
log.stop('Login Done ✅')
23052308
}
23062309
catch (error) {

src/init/runtime.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,18 @@ export interface InitLogEntry {
5959
tone: InitLogTone
6060
}
6161

62+
export interface InitVersionWarning {
63+
currentVersion: string
64+
latestVersion: string
65+
majorVersion: string
66+
}
67+
6268
export interface InitRuntimeState {
6369
screen?: InitScreen
6470
logs: InitLogEntry[]
6571
spinner?: string
6672
prompt?: PromptRequest
73+
versionWarning?: InitVersionWarning
6774
}
6875

6976
let state: InitRuntimeState = {
@@ -200,6 +207,13 @@ export function requestInitSelect(message: string, options: SelectPromptOption[]
200207
})
201208
}
202209

210+
export function setInitVersionWarning(currentVersion: string, latestVersion: string, majorVersion: string) {
211+
updateState(current => ({
212+
...current,
213+
versionWarning: { currentVersion, latestVersion, majorVersion },
214+
}))
215+
}
216+
203217
function updatePromptError(error?: string) {
204218
updateState((current) => {
205219
if (current.prompt?.kind !== 'text')

src/init/ui/app.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { InitRuntimeState } from '../runtime'
2+
import { Alert } from '@inkjs/ui'
23
import { Box, Text, useStdout } from 'ink'
34
import React, { useEffect, useState } from 'react'
45
import { CurrentStepSection, InitHeader, ProgressSection, PromptArea, ScreenIntro, SpinnerArea } from './components'
@@ -29,6 +30,14 @@ export default function InitInkApp({ getSnapshot, subscribe, updatePromptError }
2930
<Box flexDirection="column" padding={1} width={columns}>
3031
<InitHeader />
3132

33+
{snapshot.versionWarning && (
34+
<Box marginTop={1} width={contentWidth}>
35+
<Alert variant="warning">
36+
You are using @capgo/cli@{snapshot.versionWarning.currentVersion} — update to @capgo/cli@{snapshot.versionWarning.latestVersion} or @capgo/cli@{snapshot.versionWarning.majorVersion}
37+
</Alert>
38+
</Box>
39+
)}
40+
3241
{screen?.introLines?.length || screen?.title
3342
? <ScreenIntro screen={screen} />
3443
: null}

src/login.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export async function loginInternal(apikey: string, options: Options, silent = f
3939
throw new Error('Missing API key')
4040
}
4141

42-
await checkAlerts()
42+
if (!silent)
43+
await checkAlerts()
4344
// write in file .capgo the apikey in home directory
4445
const { local } = options
4546

src/utils.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,7 @@ export function getContentType(filename: string): string {
951951
return mimeTypes[ext] || 'application/octet-stream'
952952
}
953953

954-
export async function findProjectType() {
954+
export async function findProjectType(options?: { quiet?: boolean }) {
955955
// for nuxtjs check if nuxt.config.js exists
956956
// for nextjs check if next.config.js exists
957957
// for angular check if angular.json exists
@@ -960,6 +960,7 @@ export async function findProjectType() {
960960
// for react check if package.json exists and react is in dependencies
961961
const pwd = cwd()
962962
let isTypeScript = false
963+
const quiet = options?.quiet ?? false
963964

964965
// Check for TypeScript configuration file
965966
const tsConfigPath = resolve(pwd, 'tsconfig.json')
@@ -970,39 +971,39 @@ export async function findProjectType() {
970971
for await (const f of getFiles(pwd)) {
971972
// find number of folder in path after pwd
972973
if (f.includes('angular.json')) {
973-
log.info('Found angular project')
974+
if (!quiet) log.info('Found angular project')
974975
return isTypeScript ? 'angular-ts' : 'angular-js'
975976
}
976977
if (f.includes('nuxt.config.js') || f.includes('nuxt.config.ts')) {
977-
log.info('Found nuxtjs project')
978+
if (!quiet) log.info('Found nuxtjs project')
978979
return isTypeScript ? 'nuxtjs-ts' : 'nuxtjs-js'
979980
}
980981
if (f.includes('next.config.js') || f.includes('next.config.mjs')) {
981-
log.info('Found nextjs project')
982+
if (!quiet) log.info('Found nextjs project')
982983
return isTypeScript ? 'nextjs-ts' : 'nextjs-js'
983984
}
984985
if (f.includes('svelte.config.js')) {
985-
log.info('Found sveltekit project')
986+
if (!quiet) log.info('Found sveltekit project')
986987
return isTypeScript ? 'sveltekit-ts' : 'sveltekit-js'
987988
}
988989
if (f.includes('rolluconfig.js')) {
989-
log.info('Found svelte project')
990+
if (!quiet) log.info('Found svelte project')
990991
return isTypeScript ? 'svelte-ts' : 'svelte-js'
991992
}
992993
if (f.includes('vue.config.js')) {
993-
log.info('Found vue project')
994+
if (!quiet) log.info('Found vue project')
994995
return isTypeScript ? 'vue-ts' : 'vue-js'
995996
}
996997
if (f.includes(PACKNAME)) {
997998
const folder = dirname(f)
998999
const dependencies = await getAllPackagesDependencies(folder)
9991000
if (dependencies) {
10001001
if (dependencies.get('react')) {
1001-
log.info('Found react project')
1002+
if (!quiet) log.info('Found react project')
10021003
return isTypeScript ? 'react-ts' : 'react-js'
10031004
}
10041005
if (dependencies.get('vue')) {
1005-
log.info('Found vue project')
1006+
if (!quiet) log.info('Found vue project')
10061007
return isTypeScript ? 'vue-ts' : 'vue-js'
10071008
}
10081009
}

0 commit comments

Comments
 (0)