Skip to content

Commit 687f8d5

Browse files
committed
fix: show failed step in TUI progress display on error
When an operation errored, the step that failed was hidden — the rendering logic sliced it off as incomplete and only showed it during the running state. Extracted deriveStepDisplay() to compute completed, current, and failed steps from the steps array and status. All three TUI components now show the failed step with a red cross marker.
1 parent d0e4592 commit 687f8d5

5 files changed

Lines changed: 82 additions & 14 deletions

File tree

source/__tests__/utils.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import { featureDefinitions } from '../constants/config.js'
33
import {
4+
deriveStepDisplay,
45
getPackagesToRemove,
56
getPostInstallMessages,
67
isFeatureSelected,
@@ -114,3 +115,53 @@ describe('getPostInstallMessages', () => {
114115
expect(result).toEqual([])
115116
})
116117
})
118+
119+
describe('deriveStepDisplay', () => {
120+
it('shows all steps as completed when done', () => {
121+
const result = deriveStepDisplay(['Step 1', 'Step 2', 'Step 3'], 'done')
122+
123+
expect(result.completedSteps).toEqual(['Step 1', 'Step 2', 'Step 3'])
124+
expect(result.currentStep).toBeUndefined()
125+
expect(result.failedStep).toBeUndefined()
126+
})
127+
128+
it('shows last step as current when running', () => {
129+
const result = deriveStepDisplay(['Step 1', 'Step 2', 'Step 3'], 'running')
130+
131+
expect(result.completedSteps).toEqual(['Step 1', 'Step 2'])
132+
expect(result.currentStep).toBe('Step 3')
133+
expect(result.failedStep).toBeUndefined()
134+
})
135+
136+
it('shows last step as failed when error', () => {
137+
const result = deriveStepDisplay(['Step 1', 'Step 2', 'Step 3'], 'error')
138+
139+
expect(result.completedSteps).toEqual(['Step 1', 'Step 2'])
140+
expect(result.currentStep).toBeUndefined()
141+
expect(result.failedStep).toBe('Step 3')
142+
})
143+
144+
it('handles empty steps on error', () => {
145+
const result = deriveStepDisplay([], 'error')
146+
147+
expect(result.completedSteps).toEqual([])
148+
expect(result.currentStep).toBeUndefined()
149+
expect(result.failedStep).toBeUndefined()
150+
})
151+
152+
it('handles single step running', () => {
153+
const result = deriveStepDisplay(['Step 1'], 'running')
154+
155+
expect(result.completedSteps).toEqual([])
156+
expect(result.currentStep).toBe('Step 1')
157+
expect(result.failedStep).toBeUndefined()
158+
})
159+
160+
it('handles single step error', () => {
161+
const result = deriveStepDisplay(['Step 1'], 'error')
162+
163+
expect(result.completedSteps).toEqual([])
164+
expect(result.currentStep).toBeUndefined()
165+
expect(result.failedStep).toBe('Step 1')
166+
})
167+
})

source/components/steps/CloneRepo/CloneRepo.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Text } from 'ink'
22
import { type FC, useCallback, useEffect, useState } from 'react'
33
import { cloneRepo } from '../../../operations/index.js'
4+
import { deriveStepDisplay } from '../../../utils/utils.js'
45
import Divider from '../../Divider.js'
56

67
interface Props {
@@ -29,8 +30,7 @@ const CloneRepo: FC<Props> = ({ projectName, onCompletion }) => {
2930
})
3031
}, [projectName, onCompletion, handleProgress])
3132

32-
const completedSteps = status === 'done' ? steps : steps.slice(0, -1)
33-
const currentStep = status === 'running' ? steps.at(-1) : undefined
33+
const { completedSteps, currentStep, failedStep } = deriveStepDisplay(steps, status)
3434

3535
return (
3636
<>
@@ -45,11 +45,12 @@ const CloneRepo: FC<Props> = ({ projectName, onCompletion }) => {
4545
<Text dimColor>{'\u25CB'}</Text> {currentStep} <Text dimColor>Working...</Text>
4646
</Text>
4747
)}
48-
{status === 'error' && (
48+
{failedStep && (
4949
<Text>
50-
<Text color={'red'}>{'\u2717'}</Text> Failed to clone: {errorMessage}
50+
<Text color={'red'}>{'\u2717'}</Text> {failedStep} <Text color={'red'}>Error</Text>
5151
</Text>
5252
)}
53+
{status === 'error' && <Text color={'red'}>Failed to clone: {errorMessage}</Text>}
5354
</>
5455
)
5556
}

source/components/steps/FileCleanup.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type FC, useCallback, useEffect, useMemo, useState } from 'react'
33
import type { FeatureName } from '../../constants/config.js'
44
import { cleanupFiles } from '../../operations/index.js'
55
import type { InstallationType, MultiSelectItem } from '../../types/types.js'
6-
import { getProjectFolder } from '../../utils/utils.js'
6+
import { deriveStepDisplay, getProjectFolder } from '../../utils/utils.js'
77
import Divider from '../Divider.js'
88

99
interface Props {
@@ -40,8 +40,7 @@ const FileCleanup: FC<Props> = ({ onCompletion, installationConfig, projectName
4040
})
4141
}, [projectFolder, installationType, selectedFeatures, onCompletion, handleProgress])
4242

43-
const completedSteps = status === 'done' ? steps : steps.slice(0, -1)
44-
const currentStep = status === 'running' ? steps.at(-1) : undefined
43+
const { completedSteps, currentStep, failedStep } = deriveStepDisplay(steps, status)
4544

4645
return (
4746
<>
@@ -56,11 +55,12 @@ const FileCleanup: FC<Props> = ({ onCompletion, installationConfig, projectName
5655
<Text dimColor>{'\u25CB'}</Text> {currentStep} <Text dimColor>Working...</Text>
5756
</Text>
5857
)}
59-
{status === 'error' && (
58+
{failedStep && (
6059
<Text>
61-
<Text color={'red'}>{'\u2717'}</Text> Cleanup failed: {errorMessage}
60+
<Text color={'red'}>{'\u2717'}</Text> {failedStep} <Text color={'red'}>Error</Text>
6261
</Text>
6362
)}
63+
{status === 'error' && <Text color={'red'}>Cleanup failed: {errorMessage}</Text>}
6464
</>
6565
)
6666
}

source/components/steps/Install/Install.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { FeatureName } from '../../../constants/config.js'
44
import { createEnvFile } from '../../../operations/createEnvFile.js'
55
import { installPackages } from '../../../operations/installPackages.js'
66
import type { InstallationType, MultiSelectItem } from '../../../types/types.js'
7-
import { getProjectFolder } from '../../../utils/utils.js'
7+
import { deriveStepDisplay, getProjectFolder } from '../../../utils/utils.js'
88
import Divider from '../../Divider.js'
99

1010
interface Props {
@@ -51,8 +51,7 @@ const Install: FC<Props> = ({ projectName, onCompletion, installationConfig }) =
5151
})
5252
}, [projectFolder, installationType, selectedFeatures, onCompletion, handleProgress])
5353

54-
const completedSteps = status === 'done' ? steps : steps.slice(0, -1)
55-
const currentStep = status === 'running' ? steps.at(-1) : undefined
54+
const { completedSteps, currentStep, failedStep } = deriveStepDisplay(steps, status)
5655

5756
return (
5857
<>
@@ -67,11 +66,12 @@ const Install: FC<Props> = ({ projectName, onCompletion, installationConfig }) =
6766
<Text dimColor>{'\u25CB'}</Text> {currentStep} <Text dimColor>Working...</Text>
6867
</Text>
6968
)}
70-
{status === 'error' && (
69+
{failedStep && (
7170
<Text>
72-
<Text color={'red'}>{'\u2717'}</Text> Installation failed: {errorMessage}
71+
<Text color={'red'}>{'\u2717'}</Text> {failedStep} <Text color={'red'}>Error</Text>
7372
</Text>
7473
)}
74+
{status === 'error' && <Text color={'red'}>Installation failed: {errorMessage}</Text>}
7575
</>
7676
)
7777
}

source/utils/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,19 @@ export function getPostInstallMessages(
4545
export function projectDirectoryExists(projectName: string): boolean {
4646
return existsSync(getProjectFolder(projectName))
4747
}
48+
49+
type StepStatus = 'running' | 'done' | 'error'
50+
51+
type StepDisplay = {
52+
completedSteps: string[]
53+
currentStep: string | undefined
54+
failedStep: string | undefined
55+
}
56+
57+
export function deriveStepDisplay(steps: string[], status: StepStatus): StepDisplay {
58+
return {
59+
completedSteps: status === 'done' ? steps : steps.slice(0, -1),
60+
currentStep: status === 'running' ? steps.at(-1) : undefined,
61+
failedStep: status === 'error' ? steps.at(-1) : undefined,
62+
}
63+
}

0 commit comments

Comments
 (0)