From c7aeeec603c90934ccae261472f08b60d12c7c77 Mon Sep 17 00:00:00 2001
From: shmck <shawn.j.mckay@gmail.com>
Date: Thu, 16 Apr 2020 20:15:37 -0700
Subject: [PATCH 1/5] validate extension version

Signed-off-by: shmck <shawn.j.mckay@gmail.com>
---
 errors/GitProjectAlreadyExists.md |  6 +++---
 errors/GitRemoteAlreadyExists.md  |  5 +++++
 errors/UnmetExtensionVersion.md   |  3 +++
 errors/UnmetTutorialDependency.md |  2 --
 src/channel/index.ts              | 26 +++++++++++++++++++++++---
 typings/error.d.ts                | 11 +++++++----
 typings/tutorial.d.ts             |  5 +++++
 7 files changed, 46 insertions(+), 12 deletions(-)
 create mode 100644 errors/GitRemoteAlreadyExists.md
 create mode 100644 errors/UnmetExtensionVersion.md

diff --git a/errors/GitProjectAlreadyExists.md b/errors/GitProjectAlreadyExists.md
index f2e31560..f06258d4 100644
--- a/errors/GitProjectAlreadyExists.md
+++ b/errors/GitProjectAlreadyExists.md
@@ -1,5 +1,5 @@
-### Git Project Already Exists
+### Git Remote Already Exists
 
-CodeRoad requires an empty Git project.
+Have you started this tutorial before in this workspace? The Git remote already exists.
 
-Open a new workspace to start a tutorial.
+Consider deleting your `.git` folder and restarting.
diff --git a/errors/GitRemoteAlreadyExists.md b/errors/GitRemoteAlreadyExists.md
new file mode 100644
index 00000000..f2e31560
--- /dev/null
+++ b/errors/GitRemoteAlreadyExists.md
@@ -0,0 +1,5 @@
+### Git Project Already Exists
+
+CodeRoad requires an empty Git project.
+
+Open a new workspace to start a tutorial.
diff --git a/errors/UnmetExtensionVersion.md b/errors/UnmetExtensionVersion.md
new file mode 100644
index 00000000..688d4d42
--- /dev/null
+++ b/errors/UnmetExtensionVersion.md
@@ -0,0 +1,3 @@
+### Unmet Tutorial Dependency
+
+This tutorial requires a different version of CodeRoad.
diff --git a/errors/UnmetTutorialDependency.md b/errors/UnmetTutorialDependency.md
index c3524680..9803ae4d 100644
--- a/errors/UnmetTutorialDependency.md
+++ b/errors/UnmetTutorialDependency.md
@@ -1,5 +1,3 @@
 ### Unmet Tutorial Dependency
 
-### Unmet Tutorial Dependency
-
 Tutorial cannot reun because a dependency version doesn't match. Install the correct dependency and click "Check Again".
diff --git a/src/channel/index.ts b/src/channel/index.ts
index 59cbf41a..f99c8d6a 100644
--- a/src/channel/index.ts
+++ b/src/channel/index.ts
@@ -2,6 +2,7 @@ import * as T from 'typings'
 import * as TT from 'typings/tutorial'
 import * as E from 'typings/error'
 import * as vscode from 'vscode'
+import { satisfies } from 'semver'
 import saveCommit from '../actions/saveCommit'
 import setupActions from '../actions/setupActions'
 import solutionActions from '../actions/solutionActions'
@@ -110,6 +111,25 @@ class Channel implements Channel {
       case 'EDITOR_TUTORIAL_CONFIG':
         try {
           const data: TT.Tutorial = action.payload.tutorial
+
+          // validate extension version
+          const expectedAppVersion = data.config?.appVersions?.coderoadVSCode
+          if (expectedAppVersion) {
+            const extension = vscode.extensions.getExtension('coderoad.coderoad')
+            if (extension) {
+              const currentAppVersion = extension.packageJSON.version
+              const satisfied = satisfies(currentAppVersion, expectedAppVersion)
+              if (!satisfied) {
+                const error: E.ErrorMessage = {
+                  type: 'UnmetExtensionVersion',
+                  message: `Expected CodeRoad v${expectedAppVersion}, but found ${currentAppVersion}`,
+                }
+                this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
+                return
+              }
+            }
+          }
+
           // setup tutorial config (save watcher, test runner, etc)
           await this.context.setTutorial(this.workspaceState, data)
 
@@ -121,7 +141,7 @@ class Channel implements Channel {
               const currentVersion: string | null = await version(dep.name)
               if (!currentVersion) {
                 // use a custom error message
-                const error = {
+                const error: E.ErrorMessage = {
                   type: 'MissingTutorialDependency',
                   message:
                     dep.message || `Process "${dep.name}" is required but not found. It may need to be installed`,
@@ -140,7 +160,7 @@ class Channel implements Channel {
               const satisfiedDependency = await compareVersions(currentVersion, dep.version)
 
               if (!satisfiedDependency) {
-                const error = {
+                const error: E.ErrorMessage = {
                   type: 'UnmetTutorialDependency',
                   message: `Expected ${dep.name} to have version ${dep.version}, but found version ${currentVersion}`,
                   actions: [
@@ -155,7 +175,7 @@ class Channel implements Channel {
               }
 
               if (satisfiedDependency !== true) {
-                const error = satisfiedDependency || {
+                const error: E.ErrorMessage = satisfiedDependency || {
                   type: 'UnknownError',
                   message: `Something went wrong comparing dependency for ${name}`,
                   actions: [
diff --git a/typings/error.d.ts b/typings/error.d.ts
index a72b175a..296ec727 100644
--- a/typings/error.d.ts
+++ b/typings/error.d.ts
@@ -1,13 +1,16 @@
 export type ErrorMessageView = 'FULL_PAGE' | 'NOTIFY' | 'NONE'
 
 export type ErrorMessageType =
-  | 'UnknownError'
-  | 'NoWorkspaceFound'
-  | 'GitNotFound'
-  | 'WorkspaceNotEmpty'
   | 'FailedToConnectToGitRepo'
+  | 'GitNotFound'
   | 'GitProjectAlreadyExists'
   | 'GitRemoteAlreadyExists'
+  | 'MissingTutorialDependency'
+  | 'NoWorkspaceFound'
+  | 'UnknownError'
+  | 'UnmetExtensionVersion'
+  | 'UnmetTutorialDependency'
+  | 'WorkspaceNotEmpty'
 
 export type ErrorAction = {
   label: string
diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts
index a604a85b..0813c594 100644
--- a/typings/tutorial.d.ts
+++ b/typings/tutorial.d.ts
@@ -1,6 +1,7 @@
 export type Maybe<T> = T | null
 
 export type TutorialConfig = {
+  appVersions: TutorialAppVersions
   testRunner: TutorialTestRunner
   repo: TutorialRepo
   dependencies?: TutorialDependency[]
@@ -64,3 +65,7 @@ export interface TutorialDependency {
   version: string
   message?: string
 }
+
+export interface TutorialAppVersions {
+  coderoadVSCode: string
+}

From 930d532b2a66500cd09a2c4d43e47df18ed71abd Mon Sep 17 00:00:00 2001
From: shmck <shawn.j.mckay@gmail.com>
Date: Thu, 16 Apr 2020 20:21:28 -0700
Subject: [PATCH 2/5] validate vscode version with tutorial required version

Signed-off-by: shmck <shawn.j.mckay@gmail.com>
---
 src/channel/index.ts  | 4 ++--
 typings/tutorial.d.ts | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/channel/index.ts b/src/channel/index.ts
index f99c8d6a..d43e9e8f 100644
--- a/src/channel/index.ts
+++ b/src/channel/index.ts
@@ -113,7 +113,7 @@ class Channel implements Channel {
           const data: TT.Tutorial = action.payload.tutorial
 
           // validate extension version
-          const expectedAppVersion = data.config?.appVersions?.coderoadVSCode
+          const expectedAppVersion = data.config?.appVersions?.vscode
           if (expectedAppVersion) {
             const extension = vscode.extensions.getExtension('coderoad.coderoad')
             if (extension) {
@@ -122,7 +122,7 @@ class Channel implements Channel {
               if (!satisfied) {
                 const error: E.ErrorMessage = {
                   type: 'UnmetExtensionVersion',
-                  message: `Expected CodeRoad v${expectedAppVersion}, but found ${currentAppVersion}`,
+                  message: `Expected CodeRoad v${expectedAppVersion}, but found v${currentAppVersion}`,
                 }
                 this.send({ type: 'TUTORIAL_CONFIGURE_FAIL', payload: { error } })
                 return
diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts
index 0813c594..b6c3303b 100644
--- a/typings/tutorial.d.ts
+++ b/typings/tutorial.d.ts
@@ -67,5 +67,5 @@ export interface TutorialDependency {
 }
 
 export interface TutorialAppVersions {
-  coderoadVSCode: string
+  vscode: string
 }

From de55619baa4df74b99c64c104e3ea212a8f8649d Mon Sep 17 00:00:00 2001
From: shmck <shawn.j.mckay@gmail.com>
Date: Thu, 16 Apr 2020 20:23:48 -0700
Subject: [PATCH 3/5] increment version for 0.3.0 release

Signed-off-by: shmck <shawn.j.mckay@gmail.com>
---
 CHANGELOG.md              | 6 +++++-
 package-lock.json         | 2 +-
 package.json              | 4 ++--
 web-app/package-lock.json | 2 +-
 web-app/package.json      | 4 ++--
 5 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac591636..09e9694b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,4 +48,8 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
 
 ## [0.2.4]
 
-- Support VSCode 1.39.2 
\ No newline at end of file
+- Support VSCode 1.39.2
+
+## [0.3.0]
+
+- Validate the extension version against the tutorial config version. This should allow us to manage breaking changes in tutorial schema in upcoming versions
diff --git a/package-lock.json b/package-lock.json
index a332ca23..426479f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "coderoad",
-  "version": "0.2.4",
+  "version": "0.3.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index 1e603ed9..c2b9f563 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "coderoad",
-  "version": "0.2.4",
+  "version": "0.3.0",
   "description": "Play interactive coding tutorials in your editor",
   "keywords": [
     "tutorial",
@@ -82,4 +82,4 @@
   },
   "preview": true,
   "publisher": "CodeRoad"
-}
\ No newline at end of file
+}
diff --git a/web-app/package-lock.json b/web-app/package-lock.json
index 5eaeb417..6f063a98 100644
--- a/web-app/package-lock.json
+++ b/web-app/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "coderoad-app",
-  "version": "0.2.4",
+  "version": "0.3.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/web-app/package.json b/web-app/package.json
index dbe79e39..089d719e 100644
--- a/web-app/package.json
+++ b/web-app/package.json
@@ -1,6 +1,6 @@
 {
   "name": "coderoad-app",
-  "version": "0.2.4",
+  "version": "0.3.0",
   "private": true,
   "scripts": {
     "build": "react-app-rewired build",
@@ -73,4 +73,4 @@
     "typescript": "^3.8.3",
     "typescript-eslint-parser": "^22.0.0"
   }
-}
\ No newline at end of file
+}

From 08d782083180a929c8564395c840f380e2d87585 Mon Sep 17 00:00:00 2001
From: shmck <shawn.j.mckay@gmail.com>
Date: Thu, 16 Apr 2020 17:53:39 -0700
Subject: [PATCH 4/5] allow for separate coderoad directory

Signed-off-by: shmck <shawn.j.mckay@gmail.com>
---
 src/actions/utils/runCommands.ts   |  2 +-
 src/editor/commands.ts             |  8 +++++++-
 src/services/dependencies/index.ts |  2 +-
 src/services/git/index.ts          | 18 +++++++++---------
 src/services/node/index.ts         | 11 ++++++++---
 src/services/testRunner/index.ts   |  9 +++------
 typings/tutorial.d.ts              |  6 ++++--
 7 files changed, 33 insertions(+), 23 deletions(-)

diff --git a/src/actions/utils/runCommands.ts b/src/actions/utils/runCommands.ts
index ca4c770d..543d586b 100644
--- a/src/actions/utils/runCommands.ts
+++ b/src/actions/utils/runCommands.ts
@@ -13,7 +13,7 @@ const runCommands = async (commands: string[], send: (action: T.Action) => void)
     send({ type: 'COMMAND_START', payload: { process: { ...process, status: 'RUNNING' } } })
     let result: { stdout: string; stderr: string }
     try {
-      result = await exec(command)
+      result = await exec({ command })
     } catch (error) {
       console.log(`Test failed: ${error.message}`)
       send({ type: 'COMMAND_FAIL', payload: { process: { ...process, status: 'FAIL' } } })
diff --git a/src/editor/commands.ts b/src/editor/commands.ts
index 5c356750..1242cd42 100644
--- a/src/editor/commands.ts
+++ b/src/editor/commands.ts
@@ -1,6 +1,7 @@
 import * as TT from 'typings/tutorial'
 import * as vscode from 'vscode'
 import createTestRunner, { Payload } from '../services/testRunner'
+import setupActions from 'actions/setupActions'
 import createWebView from '../webview'
 
 export const COMMANDS = {
@@ -47,7 +48,12 @@ export const createCommands = ({ extensionPath, workspaceState }: CreateCommandP
       // setup 1x1 horizontal layout
       webview.createOrShow()
     },
-    [COMMANDS.CONFIG_TEST_RUNNER]: (config: TT.TutorialTestRunner) => {
+    [COMMANDS.CONFIG_TEST_RUNNER]: (config: TT.TutorialTestRunnerConfig) => {
+      if (config.actions) {
+        // setup tutorial test runner commits
+        // assumes git already exists
+        setupActions(config.actions, webview.send)
+      }
       testRunner = createTestRunner(config, {
         onSuccess: (payload: Payload) => {
           // send test pass message back to client
diff --git a/src/services/dependencies/index.ts b/src/services/dependencies/index.ts
index 155c9f0c..332455b9 100644
--- a/src/services/dependencies/index.ts
+++ b/src/services/dependencies/index.ts
@@ -5,7 +5,7 @@ const semverRegex = /(?<=^v?|\sv?)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)
 
 export const version = async (name: string): Promise<string | null> => {
   try {
-    const { stdout, stderr } = await exec(`${name} --version`)
+    const { stdout, stderr } = await exec({ command: `${name} --version` })
     if (!stderr) {
       const match = stdout.match(semverRegex)
       if (match) {
diff --git a/src/services/git/index.ts b/src/services/git/index.ts
index f2dfc23f..beda0a4e 100644
--- a/src/services/git/index.ts
+++ b/src/services/git/index.ts
@@ -6,7 +6,7 @@ const gitOrigin = 'coderoad'
 
 const stashAllFiles = async (): Promise<never | void> => {
   // stash files including untracked (eg. newly created file)
-  const { stdout, stderr } = await exec(`git stash --include-untracked`)
+  const { stdout, stderr } = await exec({ command: `git stash --include-untracked` })
   if (stderr) {
     console.error(stderr)
     throw new Error('Error stashing files')
@@ -21,7 +21,7 @@ const cherryPickCommit = async (commit: string, count = 0): Promise<never | void
   try {
     // cherry-pick pulls commits from another branch
     // -X theirs merges and accepts incoming changes over existing changes
-    const { stdout } = await exec(`git cherry-pick -X theirs ${commit}`)
+    const { stdout } = await exec({ command: `git cherry-pick -X theirs ${commit}` })
     if (!stdout) {
       throw new Error('No cherry-pick output')
     }
@@ -47,7 +47,7 @@ export function loadCommit(commit: string): Promise<never | void> {
 */
 
 export async function saveCommit(message: string): Promise<never | void> {
-  const { stdout, stderr } = await exec(`git commit -am '${message}'`)
+  const { stdout, stderr } = await exec({ command: `git commit -am '${message}'` })
   if (stderr) {
     console.error(stderr)
     throw new Error('Error saving progress to Git')
@@ -58,7 +58,7 @@ export async function saveCommit(message: string): Promise<never | void> {
 export async function clear(): Promise<Error | void> {
   try {
     // commit progress to git
-    const { stderr } = await exec('git reset HEAD --hard && git clean -fd')
+    const { stderr } = await exec({ command: 'git reset HEAD --hard && git clean -fd' })
     if (!stderr) {
       return
     }
@@ -70,7 +70,7 @@ export async function clear(): Promise<Error | void> {
 }
 
 async function init(): Promise<Error | void> {
-  const { stderr } = await exec('git init')
+  const { stderr } = await exec({ command: 'git init' })
   if (stderr) {
     throw new Error('Error initializing Git')
   }
@@ -85,13 +85,13 @@ export async function initIfNotExists(): Promise<never | void> {
 
 export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise<never | void> {
   // check for git repo
-  const externalRepoExists = await exec(`git ls-remote --exit-code --heads ${repo.uri}`)
+  const externalRepoExists = await exec({ command: `git ls-remote --exit-code --heads ${repo.uri}` })
   if (externalRepoExists.stderr) {
     // no repo found or no internet connection
     throw new Error(externalRepoExists.stderr)
   }
   // check for git repo branch
-  const { stderr, stdout } = await exec(`git ls-remote --exit-code --heads ${repo.uri} ${repo.branch}`)
+  const { stderr, stdout } = await exec({ command: `git ls-remote --exit-code --heads ${repo.uri} ${repo.branch}` })
   if (stderr) {
     throw new Error(stderr)
   }
@@ -101,7 +101,7 @@ export async function checkRemoteConnects(repo: TT.TutorialRepo): Promise<never
 }
 
 export async function addRemote(repo: string): Promise<never | void> {
-  const { stderr } = await exec(`git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}`)
+  const { stderr } = await exec({ command: `git remote add ${gitOrigin} ${repo} && git fetch ${gitOrigin}` })
   if (stderr) {
     const alreadyExists = stderr.match(`${gitOrigin} already exists.`)
     const successfulNewBranch = stderr.match('new branch')
@@ -116,7 +116,7 @@ export async function addRemote(repo: string): Promise<never | void> {
 
 export async function checkRemoteExists(): Promise<boolean> {
   try {
-    const { stdout, stderr } = await exec('git remote -v')
+    const { stdout, stderr } = await exec({ command: 'git remote -v' })
     if (stderr) {
       return false
     }
diff --git a/src/services/node/index.ts b/src/services/node/index.ts
index 1dbae70b..0f8e5f74 100644
--- a/src/services/node/index.ts
+++ b/src/services/node/index.ts
@@ -6,9 +6,14 @@ import { WORKSPACE_ROOT } from '../../environment'
 
 const asyncExec = promisify(cpExec)
 
-export const exec = (cmd: string): Promise<{ stdout: string; stderr: string }> | never => {
-  return asyncExec(cmd, {
-    cwd: WORKSPACE_ROOT,
+interface ExecParams {
+  command: string
+  path?: string
+}
+
+export const exec = (params: ExecParams): Promise<{ stdout: string; stderr: string }> | never => {
+  return asyncExec(params.command, {
+    cwd: join(WORKSPACE_ROOT, params.path || ''),
   })
 }
 
diff --git a/src/services/testRunner/index.ts b/src/services/testRunner/index.ts
index b36d9575..16c097c8 100644
--- a/src/services/testRunner/index.ts
+++ b/src/services/testRunner/index.ts
@@ -1,3 +1,4 @@
+import { TutorialTestRunnerConfig } from 'typings/tutorial'
 import { exec } from '../node'
 import logger from '../logger'
 import parser from './parser'
@@ -17,14 +18,10 @@ interface Callbacks {
   onError(payload: Payload): void
 }
 
-interface TestRunnerConfig {
-  command: string
-}
-
 const failChannelName = 'CodeRoad (Tests)'
 const logChannelName = 'CodeRoad (Logs)'
 
-const createTestRunner = (config: TestRunnerConfig, callbacks: Callbacks) => {
+const createTestRunner = (config: TutorialTestRunnerConfig, callbacks: Callbacks) => {
   return async (payload: Payload, onSuccess?: () => void): Promise<void> => {
     const startTime = throttle()
     // throttle time early
@@ -39,7 +36,7 @@ const createTestRunner = (config: TestRunnerConfig, callbacks: Callbacks) => {
 
     let result: { stdout: string | undefined; stderr: string | undefined }
     try {
-      result = await exec(config.command)
+      result = await exec({ command: config.command, path: config.path })
     } catch (err) {
       result = { stdout: err.stdout, stderr: err.stack }
     }
diff --git a/typings/tutorial.d.ts b/typings/tutorial.d.ts
index b6c3303b..afe3021b 100644
--- a/typings/tutorial.d.ts
+++ b/typings/tutorial.d.ts
@@ -2,7 +2,7 @@ export type Maybe<T> = T | null
 
 export type TutorialConfig = {
   appVersions: TutorialAppVersions
-  testRunner: TutorialTestRunner
+  testRunner: TutorialTestRunnerConfig
   repo: TutorialRepo
   dependencies?: TutorialDependency[]
 }
@@ -51,8 +51,10 @@ export type StepActions = {
   watchers: string[]
 }
 
-export interface TutorialTestRunner {
+export interface TutorialTestRunnerConfig {
   command: string
+  path?: string
+  actions?: StepActions
 }
 
 export interface TutorialRepo {

From eaf27ffeee352c021c4355806ba99d21387b22af Mon Sep 17 00:00:00 2001
From: shmck <shawn.j.mckay@gmail.com>
Date: Thu, 16 Apr 2020 20:34:00 -0700
Subject: [PATCH 5/5] update changelog with testrunner path

Signed-off-by: shmck <shawn.j.mckay@gmail.com>
---
 CHANGELOG.md | 29 ++++++++++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09e9694b..1333258d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,4 +52,31 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
 
 ## [0.3.0]
 
-- Validate the extension version against the tutorial config version. This should allow us to manage breaking changes in tutorial schema in upcoming versions
+- Validate the extension version against the tutorial config version. This should allow us to manage breaking changes in tutorial schema in upcoming versions. See [node-semver](https://github.com/npm/node-semver#advanced-range-syntax) for possible version ranges and options.
+
+```json
+{
+"config": {
+  "appVersions": {
+    "vscode": "<0.2"
+  },
+}
+```
+
+- Configure the CodeRoad to load and run in a different directory. The example below will:
+  - load a commit and run npm install to setup the test runner in its own folder.
+  - run "npm test" in the \$ROOT/coderoad directory on save
+
+```json
+{
+"config": {
+  "testRunner": {
+    "command": "npm test",
+    "path": "coderoad",
+    "actions": {
+      "commits": ["a974aea"],
+      "commands": ["cd coderoad && npm install"]
+    }
+  },
+}
+```