@@ -2,16 +2,17 @@ import { Box } from 'ink'
22import { type FC , type ReactNode , useCallback , useMemo , useState } from 'react'
33import MainTitle from './components/MainTitle.js'
44import CloneRepo from './components/steps/CloneRepo/CloneRepo.js'
5+ import Confirmation from './components/steps/Confirmation.js'
56import FileCleanup from './components/steps/FileCleanup.js'
67import Install from './components/steps/Install/Install.js'
78import InstallationMode from './components/steps/InstallationMode.js'
89import OptionalPackages from './components/steps/OptionalPackages.js'
910import PostInstall from './components/steps/PostInstall.js'
1011import ProjectName from './components/steps/ProjectName.js'
1112import StackSelection from './components/steps/StackSelection.js'
12- import type { Stack } from './constants/config.js'
13+ import type { FeatureName , Stack } from './constants/config.js'
1314import type { InstallationSelectItem , MultiSelectItem } from './types/types.js'
14- import { canShowStep } from './utils/utils.js'
15+ import { canShowStep , describeInstallPlan } from './utils/utils.js'
1516
1617interface Props {
1718 preselectedStack ?: Stack
@@ -23,6 +24,9 @@ const App: FC<Props> = ({ preselectedStack }) => {
2324 const [ currentStep , setCurrentStep ] = useState ( 1 )
2425 const [ setupType , setSetupType ] = useState < InstallationSelectItem | undefined > ( )
2526 const [ selectedFeatures , setSelectedFeatures ] = useState < Array < MultiSelectItem > | undefined > ( )
27+ // Bumped when the user cancels at the confirmation step; re-keys every step so they re-mount
28+ // fresh for a clean re-run of the wizard.
29+ const [ attempt , setAttempt ] = useState ( 0 )
2630
2731 const finishStep = useCallback ( ( ) => setCurrentStep ( ( prevStep ) => prevStep + 1 ) , [ ] )
2832 const onSelectStack = useCallback ( ( value : Stack ) => setStack ( value ) , [ ] )
@@ -32,14 +36,32 @@ const App: FC<Props> = ({ preselectedStack }) => {
3236 [ ] ,
3337 )
3438
39+ // Confirmation "No": discard the answers and return to the first question. No disk work has
40+ // happened yet, so this is a clean restart.
41+ const restart = useCallback ( ( ) => {
42+ setProjectName ( '' )
43+ setSetupType ( undefined )
44+ setSelectedFeatures ( undefined )
45+ setStack ( preselectedStack )
46+ setCurrentStep ( 1 )
47+ setAttempt ( ( prev ) => prev + 1 )
48+ } , [ preselectedStack ] )
49+
3550 const skipFeatures = setupType ?. value === 'full'
3651
52+ const mode = setupType ?. value ?? 'full'
53+ const planFeatures = selectedFeatures ?. map ( ( item ) => item . value as FeatureName ) ?? [ ]
54+ const planSummary =
55+ stack === undefined ? '' : describeInstallPlan ( stack , projectName , mode , planFeatures )
56+
3757 const steps : Array < ReactNode > = useMemo ( ( ) => {
58+ // Questions first (no disk writes), operations last. This way an interrupt while answering
59+ // leaves nothing behind, and all cloning/installing happens only after the confirmation.
3860 const orderedSteps : Array < ReactNode > = [
3961 < ProjectName
4062 onCompletion = { finishStep }
4163 onSubmit = { setProjectName }
42- key = { ' project-name' }
64+ key = { ` project-name- ${ attempt } ` }
4365 /> ,
4466 ]
4567
@@ -48,7 +70,7 @@ const App: FC<Props> = ({ preselectedStack }) => {
4870 < StackSelection
4971 onCompletion = { finishStep }
5072 onSelect = { onSelectStack }
51- key = { ' stack-selection' }
73+ key = { ` stack-selection- ${ attempt } ` }
5274 /> ,
5375 )
5476 }
@@ -57,20 +79,12 @@ const App: FC<Props> = ({ preselectedStack }) => {
5779 return orderedSteps
5880 }
5981
60- orderedSteps . push (
61- < CloneRepo
62- stack = { stack }
63- onCompletion = { finishStep }
64- projectName = { projectName }
65- key = { 'clone-repo' }
66- /> ,
67- )
68-
82+ // --- remaining questions (need the stack) ---
6983 orderedSteps . push (
7084 < InstallationMode
7185 onCompletion = { finishStep }
7286 onSelect = { onSelectSetupType }
73- key = { ' installation-mode' }
87+ key = { ` installation-mode- ${ attempt } ` }
7488 /> ,
7589 )
7690
@@ -80,7 +94,26 @@ const App: FC<Props> = ({ preselectedStack }) => {
8094 onCompletion = { finishStep }
8195 onSubmit = { onSelectSelectedFeatures }
8296 skip = { skipFeatures }
83- key = { 'optional-packages' }
97+ key = { `optional-packages-${ attempt } ` }
98+ /> ,
99+ )
100+
101+ orderedSteps . push (
102+ < Confirmation
103+ summary = { planSummary }
104+ onConfirm = { finishStep }
105+ onCancel = { restart }
106+ key = { `confirmation-${ attempt } ` }
107+ /> ,
108+ )
109+
110+ // --- operations (disk writes) ---
111+ orderedSteps . push (
112+ < CloneRepo
113+ stack = { stack }
114+ onCompletion = { finishStep }
115+ projectName = { projectName }
116+ key = { `clone-repo-${ attempt } ` }
84117 /> ,
85118 )
86119
@@ -93,7 +126,7 @@ const App: FC<Props> = ({ preselectedStack }) => {
93126 } }
94127 onCompletion = { finishStep }
95128 projectName = { projectName }
96- key = { ' install' }
129+ key = { ` install- ${ attempt } ` }
97130 /> ,
98131 )
99132
@@ -106,7 +139,7 @@ const App: FC<Props> = ({ preselectedStack }) => {
106139 } }
107140 onCompletion = { finishStep }
108141 projectName = { projectName }
109- key = { ' file-cleanup' }
142+ key = { ` file-cleanup- ${ attempt } ` }
110143 /> ,
111144 )
112145
@@ -118,7 +151,7 @@ const App: FC<Props> = ({ preselectedStack }) => {
118151 installationType : setupType ?. value ,
119152 selectedFeatures : selectedFeatures ,
120153 } }
121- key = { ' post-install' }
154+ key = { ` post-install- ${ attempt } ` }
122155 /> ,
123156 )
124157
@@ -134,6 +167,9 @@ const App: FC<Props> = ({ preselectedStack }) => {
134167 skipFeatures ,
135168 stack ,
136169 preselectedStack ,
170+ attempt ,
171+ planSummary ,
172+ restart ,
137173 ] )
138174
139175 return (
0 commit comments