@@ -9,11 +9,12 @@ import tutorialConfig from '../actions/tutorialConfig'
99import { COMMANDS } from '../editor/commands'
1010import logger from '../services/logger'
1111import Context from './context'
12- import { version as gitVersion } from '../services/git '
12+ import { version , compareVersions } from '../services/dependencies '
1313import { openWorkspace , checkWorkspaceEmpty } from '../services/workspace'
1414import { readFile } from 'fs'
1515import { join } from 'path'
1616import { promisify } from 'util'
17+ import { compare } from 'semver'
1718
1819const readFileAsync = promisify ( readFile )
1920
@@ -94,6 +95,63 @@ class Channel implements Channel {
9495// setup tutorial config (save watcher, test runner, etc)
9596await this . context . setTutorial ( this . workspaceState , data )
9697
98+ // validate dependencies
99+ const dependencies = data . config . dependencies
100+ if ( dependencies && dependencies . length ) {
101+ for ( const dep of dependencies ) {
102+ // check dependency is installed
103+ const currentVersion :string | null = await version ( dep . name )
104+ if ( ! currentVersion ) {
105+ // use a custom error message
106+ const error = {
107+ type :'MissingTutorialDependency' ,
108+ message :dep . message || `Process "${ dep . name } " is required but not found. It may need to be installed` ,
109+ actions :[
110+ {
111+ label :'Check Again' ,
112+ transition :'TRY_AGAIN' ,
113+ } ,
114+ ] ,
115+ }
116+ this . send ( { type :'TUTORIAL_CONFIGURE_FAIL' , payload :{ error} } )
117+ return
118+ }
119+
120+ // check dependency version
121+ const satisfiedDependency = await compareVersions ( currentVersion , dep . version )
122+
123+ if ( ! satisfiedDependency ) {
124+ const error = {
125+ type :'UnmetTutorialDependency' ,
126+ message :`Expected${ dep . name } to have version${ dep . version } , but found version${ currentVersion } ` ,
127+ actions :[
128+ {
129+ label :'Check Again' ,
130+ transition :'TRY_AGAIN' ,
131+ } ,
132+ ] ,
133+ }
134+ this . send ( { type :'TUTORIAL_CONFIGURE_FAIL' , payload :{ error} } )
135+ return
136+ }
137+
138+ if ( satisfiedDependency !== true ) {
139+ const error = satisfiedDependency || {
140+ type :'UnknownError' ,
141+ message :`Something went wrong comparing dependency for${ name } ` ,
142+ actions :[
143+ {
144+ label :'Try Again' ,
145+ transition :'TRY_AGAIN' ,
146+ } ,
147+ ] ,
148+ }
149+ this . send ( { type :'TUTORIAL_CONFIGURE_FAIL' , payload :{ error} } )
150+ return
151+ }
152+ }
153+ }
154+
97155const error :E . ErrorMessage | void = await tutorialConfig ( { config :data . config } ) . catch ( ( error :Error ) => ( {
98156type :'UnknownError' ,
99157message :`Location: tutorial config.\n\n${ error . message } ` ,
@@ -144,7 +202,7 @@ class Channel implements Channel {
144202}
145203// 2. check Git is installed.
146204// Should wait for workspace before running otherwise requires access to root folder
147- const isGitInstalled = await gitVersion ( )
205+ const isGitInstalled = await version ( 'git' )
148206if ( ! isGitInstalled ) {
149207const error :E . ErrorMessage = {
150208type :'GitNotFound' ,
@@ -197,7 +255,7 @@ class Channel implements Channel {
197255
198256if ( errorMarkdown ) {
199257// add a clearer error message for the user
200- error . message = `${ errorMarkdown } \n${ error . message } `
258+ error . message = `${ errorMarkdown } \n\n ${ error . message } `
201259}
202260}
203261