From f4a4a64f19b826f80a1c57a01bfd71f251f06028 Mon Sep 17 00:00:00 2001
From: HipsterBrown <headhipster@hipsterbrown.com>
Date: Sat, 28 Dec 2024 13:51:09 -0500
Subject: [PATCH] fix(setup,update): build SDK tools if not included in tagged
 release

---
 src/commands/scan.ts         |  11 +-
 src/toolbox/setup/linux.ts   |  72 ++++++++-----
 src/toolbox/setup/mac.ts     | 109 ++++++++++++--------
 src/toolbox/setup/windows.ts |  63 +++++++----
 src/toolbox/update/linux.ts  | 195 ++++++++++++++++++++---------------
 src/toolbox/update/mac.ts    | 157 ++++++++++++++++------------
 6 files changed, 361 insertions(+), 246 deletions(-)

diff --git a/src/commands/scan.ts b/src/commands/scan.ts
index ccc2c9b..9290022 100644
--- a/src/commands/scan.ts
+++ b/src/commands/scan.ts
@@ -31,17 +31,19 @@ const command = buildCommand({
       )
     }
 
-    spinner.start('Scanning for devices...')
-
     const hasPicotool = system.which('picotool') !== null
 
     if (hasPicotool) {
       try {
+        spinner.start('Found picotool, rebooting device before scanning')
         await system.exec('picotool reboot -fa')
         await sleep(1000)
+        spinner.stop()
       } catch {}
     }
 
+    spinner.start('Scanning for devices...')
+
     const ports = await SerialPort.list()
     const result: Array<
       [output: Buffer, port: string] | [output: undefined, port: string]
@@ -50,10 +52,7 @@ const command = buildCommand({
         .filter((port) => port.serialNumber !== undefined)
         .map(async (port) => {
           try {
-            if (
-              port.manufacturer?.includes('Raspberry Pi') === true &&
-              hasPicotool
-            ) {
+            if (port.vendorId === '2e8a' && hasPicotool) {
               const device = await findBySerialNumber(port.serialNumber ?? '')
               const bus = String(device?.busNumber)
               const address = String(device?.deviceAddress)
diff --git a/src/toolbox/setup/linux.ts b/src/toolbox/setup/linux.ts
index f7a81ca..01149f5 100644
--- a/src/toolbox/setup/linux.ts
+++ b/src/toolbox/setup/linux.ts
@@ -1,7 +1,7 @@
 import os from 'os'
 import { promisify } from 'util'
 import { chmod } from 'fs'
-import { filesystem, print, system } from 'gluegun'
+import { filesystem, print, system, prompt } from 'gluegun'
 import {
   INSTALL_DIR,
   INSTALL_PATH,
@@ -43,6 +43,7 @@ export default async function ({
     'lin',
   )
 
+  let buildTools = false
   const spinner = print.spin()
   spinner.start('Beginning setup...')
 
@@ -73,38 +74,59 @@ export default async function ({
     if (release !== undefined && (branch === undefined || branch === null)) {
       spinner.start('Getting latest Moddable-OpenSource/moddable release')
       const remoteRelease = await fetchRelease(release)
+
+      if (remoteRelease.assets.length === 0) {
+        print.warning(
+          `Moddable release ${release} does not have any pre-built assets.`,
+        )
+        buildTools = await prompt.confirm(
+          'Would you like to continue setting up and build the SDK locally?',
+          true,
+        )
+
+        if (!buildTools) {
+          print.info(
+            'Please select another release version with pre-built assets: https://github.com/Moddable-OpenSource/moddable/releases',
+          )
+          process.exit(0)
+        }
+      }
+
       await system.spawn(
         `git clone ${sourceRepo} ${INSTALL_PATH} --depth 1 --branch ${remoteRelease.tag_name} --single-branch`,
       )
 
-      filesystem.dir(BIN_PATH)
-      filesystem.dir(DEBUG_BIN_PATH)
-
-      const isArm = os.arch() === 'arm64'
-      const assetName = isArm
-        ? 'moddable-tools-lin64arm.zip'
-        : 'moddable-tools-lin64.zip'
-      spinner.info('Downloading release tools')
-      await downloadReleaseTools({
-        writePath: BIN_PATH,
-        assetName,
-        release: remoteRelease,
-      })
-      const tools = filesystem.list(BIN_PATH) ?? []
-      await Promise.all(
-        tools.map(async (tool) => {
-          await chmodPromise(filesystem.resolve(BIN_PATH, tool), 0o751)
-          await filesystem.copyAsync(
-            filesystem.resolve(BIN_PATH, tool),
-            filesystem.resolve(DEBUG_BIN_PATH, tool),
-          )
-        }),
-      )
+      if (!buildTools) {
+        filesystem.dir(BIN_PATH)
+        filesystem.dir(DEBUG_BIN_PATH)
+
+        const isArm = os.arch() === 'arm64'
+        const assetName = isArm
+          ? 'moddable-tools-lin64arm.zip'
+          : 'moddable-tools-lin64.zip'
+        spinner.info('Downloading release tools')
+        await downloadReleaseTools({
+          writePath: BIN_PATH,
+          assetName,
+          release: remoteRelease,
+        })
+        const tools = filesystem.list(BIN_PATH) ?? []
+        await Promise.all(
+          tools.map(async (tool) => {
+            await chmodPromise(filesystem.resolve(BIN_PATH, tool), 0o751)
+            await filesystem.copyAsync(
+              filesystem.resolve(BIN_PATH, tool),
+              filesystem.resolve(DEBUG_BIN_PATH, tool),
+            )
+          }),
+        )
+      }
     } else {
       spinner.start(`Cloning ${sourceRepo} repo`)
       await system.spawn(
         `git clone ${sourceRepo} ${INSTALL_PATH} --depth 1 --branch ${branch} --single-branch`,
       )
+      buildTools = true
     }
     spinner.succeed()
   }
@@ -117,7 +139,7 @@ export default async function ({
   await upsert(EXPORTS_FILE_PATH, `export PATH="${BIN_PATH}:$PATH"`)
 
   // 5. Build the Moddable command line tools, simulator, and debugger from the command line:
-  if (typeof branch === 'string') {
+  if (buildTools) {
     spinner.start('Building platform tooling')
     await system.exec('make', { cwd: BUILD_DIR, stdout: process.stdout })
     spinner.succeed()
diff --git a/src/toolbox/setup/mac.ts b/src/toolbox/setup/mac.ts
index 55c3e5a..f62ed5d 100644
--- a/src/toolbox/setup/mac.ts
+++ b/src/toolbox/setup/mac.ts
@@ -1,4 +1,4 @@
-import { print, filesystem, system } from 'gluegun'
+import { print, filesystem, system, prompt } from 'gluegun'
 import os from 'os'
 import { promisify } from 'util'
 import { chmod } from 'fs'
@@ -56,6 +56,8 @@ export default async function ({
     process.exit(1)
   }
 
+  let buildTools = false
+
   const spinner = print.spin()
   spinner.start('Beginning setup...')
 
@@ -76,62 +78,83 @@ export default async function ({
       if (release !== undefined && (branch === undefined || branch === null)) {
         spinner.start('Getting latest Moddable-OpenSource/moddable release')
         const remoteRelease = await fetchRelease(release)
+
+        if (remoteRelease.assets.length === 0) {
+          print.warning(
+            `Moddable release ${release} does not have any pre-built assets.`,
+          )
+          buildTools = await prompt.confirm(
+            'Would you like to continue setting up and build the SDK locally?',
+            true,
+          )
+
+          if (!buildTools) {
+            print.info(
+              'Please select another release version with pre-built assets: https://github.com/Moddable-OpenSource/moddable/releases',
+            )
+            process.exit(0)
+          }
+        }
+
         await system.spawn(
           `git clone ${sourceRepo} ${INSTALL_PATH} --depth 1 --branch ${remoteRelease.tag_name} --single-branch`,
         )
 
-        filesystem.dir(BIN_PATH)
-        filesystem.dir(DEBUG_BIN_PATH)
-
-        spinner.info('Downloading release tools')
-        try {
-          const universalAssetName = `moddable-tools-macuniversal.zip`
-          await downloadReleaseTools({
-            writePath: BIN_PATH,
-            assetName: universalAssetName,
-            release: remoteRelease,
-          })
-        } catch (error: unknown) {
-          if (error instanceof MissingReleaseAssetError) {
-            const isArm = os.arch() === 'arm64'
-            const assetName = isArm
-              ? 'moddable-tools-mac64arm.zip'
-              : 'moddable-tools-mac64.zip'
+        if (!buildTools) {
+          filesystem.dir(BIN_PATH)
+          filesystem.dir(DEBUG_BIN_PATH)
+
+          spinner.info('Downloading release tools')
+          try {
+            const universalAssetName = `moddable-tools-macuniversal.zip`
             await downloadReleaseTools({
               writePath: BIN_PATH,
-              assetName,
+              assetName: universalAssetName,
               release: remoteRelease,
             })
-          } else {
-            throw error as Error
-          }
-        }
-        const tools = filesystem.list(BIN_PATH) ?? []
-        await Promise.all(
-          tools.map(async (tool) => {
-            if (tool.endsWith('.app')) {
-              const mainPath = filesystem.resolve(
-                BIN_PATH,
-                tool,
-                'Contents',
-                'MacOS',
-                'main',
-              )
-              await chmodPromise(mainPath, 0o751)
+          } catch (error: unknown) {
+            if (error instanceof MissingReleaseAssetError) {
+              const isArm = os.arch() === 'arm64'
+              const assetName = isArm
+                ? 'moddable-tools-mac64arm.zip'
+                : 'moddable-tools-mac64.zip'
+              await downloadReleaseTools({
+                writePath: BIN_PATH,
+                assetName,
+                release: remoteRelease,
+              })
             } else {
-              await chmodPromise(filesystem.resolve(BIN_PATH, tool), 0o751)
+              throw error as Error
             }
-            await filesystem.copyAsync(
-              filesystem.resolve(BIN_PATH, tool),
-              filesystem.resolve(DEBUG_BIN_PATH, tool),
-            )
-          }),
-        )
+          }
+          const tools = filesystem.list(BIN_PATH) ?? []
+          await Promise.all(
+            tools.map(async (tool) => {
+              if (tool.endsWith('.app')) {
+                const mainPath = filesystem.resolve(
+                  BIN_PATH,
+                  tool,
+                  'Contents',
+                  'MacOS',
+                  'main',
+                )
+                await chmodPromise(mainPath, 0o751)
+              } else {
+                await chmodPromise(filesystem.resolve(BIN_PATH, tool), 0o751)
+              }
+              await filesystem.copyAsync(
+                filesystem.resolve(BIN_PATH, tool),
+                filesystem.resolve(DEBUG_BIN_PATH, tool),
+              )
+            }),
+          )
+        }
       } else {
         spinner.start(`Cloning ${sourceRepo} repo`)
         await system.spawn(
           `git clone ${sourceRepo} ${INSTALL_PATH} --depth 1 --branch ${branch} --single-branch`,
         )
+        buildTools = true
       }
       spinner.succeed()
     } catch (error) {
@@ -148,7 +171,7 @@ export default async function ({
   await upsert(EXPORTS_FILE_PATH, `export PATH="${BIN_PATH}:$PATH"`)
 
   // 3. cd into makefiles dir for platform, run `make`
-  if (branch !== undefined || branch !== null) {
+  if (buildTools) {
     try {
       spinner.start('Building platform tooling')
       await system.exec('make', { cwd: BUILD_DIR, stdout: process.stdout })
diff --git a/src/toolbox/setup/windows.ts b/src/toolbox/setup/windows.ts
index f02ed37..2073e01 100644
--- a/src/toolbox/setup/windows.ts
+++ b/src/toolbox/setup/windows.ts
@@ -212,6 +212,7 @@ export default async function ({
     spinner.fail(`Error setting up install directory: ${String(error)}`)
     process.exit(1)
   }
+  let buildTools = false
 
   if (filesystem.exists(INSTALL_PATH) !== false) {
     spinner.info('Moddable repo already installed')
@@ -220,39 +221,59 @@ export default async function ({
       if (release !== undefined && (branch === undefined || branch === null)) {
         spinner.start(`Getting latest Moddable-OpenSource/moddable release`)
         const remoteRelease = await fetchRelease(release)
+
+        if (remoteRelease.assets.length === 0) {
+          print.warning(
+            `Moddable release ${release} does not have any pre-built assets.`,
+          )
+          buildTools = await prompt.confirm(
+            'Would you like to continue setting up and build the SDK locally?',
+            true,
+          )
+
+          if (!buildTools) {
+            print.info(
+              'Please select another release version with pre-built assets: https://github.com/Moddable-OpenSource/moddable/releases',
+            )
+            process.exit(0)
+          }
+        }
+
         await system.spawn(
           `git clone ${sourceRepo} ${INSTALL_PATH} --depth 1 --branch ${remoteRelease.tag_name} --single-branch`,
         )
 
-        filesystem.dir(BIN_PATH)
-        filesystem.dir(DEBUG_BIN_PATH)
+        if (!buildTools) {
+          filesystem.dir(BIN_PATH)
+          filesystem.dir(DEBUG_BIN_PATH)
 
-        spinner.info('Downloading release tools')
+          spinner.info('Downloading release tools')
 
-        const assetName = `moddable-tools-win64.zip`
-        await downloadReleaseTools({
-          writePath: BIN_PATH,
-          assetName,
-          release: remoteRelease,
-        })
+          const assetName = `moddable-tools-win64.zip`
+          await downloadReleaseTools({
+            writePath: BIN_PATH,
+            assetName,
+            release: remoteRelease,
+          })
 
-        const tools = filesystem.list(BIN_PATH) ?? []
-        await Promise.all(
-          tools.map(async (tool) => {
-            await filesystem.copyAsync(
-              filesystem.resolve(BIN_PATH, tool),
-              filesystem.resolve(DEBUG_BIN_PATH, tool),
-            )
-          }),
-        )
-
-        spinner.succeed()
+          const tools = filesystem.list(BIN_PATH) ?? []
+          await Promise.all(
+            tools.map(async (tool) => {
+              await filesystem.copyAsync(
+                filesystem.resolve(BIN_PATH, tool),
+                filesystem.resolve(DEBUG_BIN_PATH, tool),
+              )
+            }),
+          )
+        }
       } else {
         spinner.start(`Cloning ${sourceRepo} repo`)
         await system.spawn(
           `git clone ${sourceRepo} ${INSTALL_PATH} --depth 1 --branch ${branch} --single-branch`,
         )
+        buildTools = true
       }
+      spinner.succeed()
     } catch (error) {
       spinner.fail(`Error cloning moddable repo: ${String(error)}`)
       process.exit(1)
@@ -271,7 +292,7 @@ export default async function ({
   }
 
   // 3. build tools if needed
-  if (typeof branch === 'string') {
+  if (buildTools) {
     try {
       spinner.start(`Building Moddable SDK tools`)
       await system.exec(`build.bat`, { cwd: BUILD_DIR, stdout: process.stdout })
diff --git a/src/toolbox/update/linux.ts b/src/toolbox/update/linux.ts
index 1f95ebe..29c10fb 100644
--- a/src/toolbox/update/linux.ts
+++ b/src/toolbox/update/linux.ts
@@ -1,7 +1,7 @@
 import os from 'os'
 import { promisify } from 'util'
 import { chmod } from 'fs'
-import { print, system, filesystem } from 'gluegun'
+import { print, system, filesystem, prompt } from 'gluegun'
 import { INSTALL_PATH, MODDABLE_REPO, XSBUG_LOG_PATH } from '../setup/constants'
 import type { SetupArgs } from '../setup/types'
 import {
@@ -32,6 +32,7 @@ export default async function ({ branch, release }: SetupArgs): Promise<void> {
     'makefiles',
     'lin',
   )
+  let rebuildTools = false
 
   if (release !== undefined && (branch === undefined || branch === null)) {
     // get tag for current repo
@@ -46,98 +47,117 @@ export default async function ({ branch, release }: SetupArgs): Promise<void> {
       process.exit(0)
     }
 
+    if (remoteRelease.assets.length === 0) {
+      print.warning(
+        `Moddable release ${release} does not have any pre-built assets.`,
+      )
+      rebuildTools = await prompt.confirm(
+        'Would you like to continue updating and build the SDK locally?',
+        false,
+      )
+
+      if (!rebuildTools) {
+        print.info(
+          'Please select another release version with pre-built assets: https://github.com/Moddable-OpenSource/moddable/releases',
+        )
+        process.exit(0)
+      }
+    }
+
     const spinner = print.spin()
     spinner.start('Updating Moddable SDK!')
 
-    const BIN_PATH = filesystem.resolve(
-      INSTALL_PATH,
-      'build',
-      'bin',
-      'lin',
-      'release',
-    )
-    const DEBUG_BIN_PATH = filesystem.resolve(
-      INSTALL_PATH,
-      'build',
-      'bin',
-      'lin',
-      'debug',
-    )
-
     filesystem.remove(process.env.MODDABLE)
     await system.spawn(
       `git clone ${MODDABLE_REPO} ${INSTALL_PATH} --depth 1 --branch ${remoteRelease.tag_name} --single-branch`,
     )
 
-    filesystem.dir(BIN_PATH)
-    filesystem.dir(DEBUG_BIN_PATH)
-
-    const isArm = os.arch() === 'arm64'
-    const assetName = isArm
-      ? 'moddable-tools-lin64arm.zip'
-      : 'moddable-tools-lin64.zip'
-
-    spinner.info('Downloading release tools')
-    await downloadReleaseTools({
-      writePath: BIN_PATH,
-      assetName,
-      release: remoteRelease,
-    })
-
-    spinner.info('Updating tool permissions')
-    const tools = filesystem.list(BIN_PATH) ?? []
-    await Promise.all(
-      tools.map(async (tool) => {
-        await chmodPromise(filesystem.resolve(BIN_PATH, tool), 0o751)
-        await filesystem.copyAsync(
-          filesystem.resolve(BIN_PATH, tool),
-          filesystem.resolve(DEBUG_BIN_PATH, tool),
-        )
-      }),
-    )
-
-    spinner.info('Reinstalling simulator')
-    filesystem.dir(
-      filesystem.resolve(
-        BUILD_DIR,
-        '..',
-        '..',
-        'tmp',
-        'lin',
-        'debug',
-        'simulator',
-      ),
-    )
-    await system.exec(
-      `mcconfig -m -p x-lin ${filesystem.resolve(
+    if (!rebuildTools) {
+      const BIN_PATH = filesystem.resolve(
         INSTALL_PATH,
-        'tools',
-        'xsbug',
-        'manifest.json',
-      )}`,
-      { process },
-    )
-    await system.exec(
-      `mcconfig -m -p x-lin ${filesystem.resolve(
+        'build',
+        'bin',
+        'lin',
+        'release',
+      )
+      const DEBUG_BIN_PATH = filesystem.resolve(
         INSTALL_PATH,
-        'tools',
-        'mcsim',
-        'manifest.json',
-      )}`,
-      { process },
-    )
-    await execWithSudo('make install', {
-      cwd: BUILD_DIR,
-      stdout: process.stdout,
-    })
-    if (system.which('npm') !== null) {
-      spinner.start('Installing xsbug-log dependencies')
-      await system.exec('npm install', { cwd: XSBUG_LOG_PATH })
-      spinner.succeed()
+        'build',
+        'bin',
+        'lin',
+        'debug',
+      )
+
+      filesystem.dir(BIN_PATH)
+      filesystem.dir(DEBUG_BIN_PATH)
+
+      const isArm = os.arch() === 'arm64'
+      const assetName = isArm
+        ? 'moddable-tools-lin64arm.zip'
+        : 'moddable-tools-lin64.zip'
+
+      spinner.info('Downloading release tools')
+      await downloadReleaseTools({
+        writePath: BIN_PATH,
+        assetName,
+        release: remoteRelease,
+      })
+
+      spinner.info('Updating tool permissions')
+      const tools = filesystem.list(BIN_PATH) ?? []
+      await Promise.all(
+        tools.map(async (tool) => {
+          await chmodPromise(filesystem.resolve(BIN_PATH, tool), 0o751)
+          await filesystem.copyAsync(
+            filesystem.resolve(BIN_PATH, tool),
+            filesystem.resolve(DEBUG_BIN_PATH, tool),
+          )
+        }),
+      )
+
+      spinner.info('Reinstalling simulator')
+      filesystem.dir(
+        filesystem.resolve(
+          BUILD_DIR,
+          '..',
+          '..',
+          'tmp',
+          'lin',
+          'debug',
+          'simulator',
+        ),
+      )
+      await system.exec(
+        `mcconfig -m -p x-lin ${filesystem.resolve(
+          INSTALL_PATH,
+          'tools',
+          'xsbug',
+          'manifest.json',
+        )}`,
+        { process },
+      )
+      await system.exec(
+        `mcconfig -m -p x-lin ${filesystem.resolve(
+          INSTALL_PATH,
+          'tools',
+          'mcsim',
+          'manifest.json',
+        )}`,
+        { process },
+      )
+      await execWithSudo('make install', {
+        cwd: BUILD_DIR,
+        stdout: process.stdout,
+      })
+      if (system.which('npm') !== null) {
+        spinner.start('Installing xsbug-log dependencies')
+        await system.exec('npm install', { cwd: XSBUG_LOG_PATH })
+        spinner.succeed()
+      }
+      spinner.succeed(
+        'Moddable SDK successfully updated! Start the xsbug.app and run the "helloworld example": xs-dev run --example helloworld',
+      )
     }
-    spinner.succeed(
-      'Moddable SDK successfully updated! Start the xsbug.app and run the "helloworld example": xs-dev run --example helloworld',
-    )
   }
 
   if (typeof branch === 'string') {
@@ -157,16 +177,21 @@ export default async function ({ branch, release }: SetupArgs): Promise<void> {
     const spinner = print.spin()
     spinner.start('Updating Moddable SDK!')
 
-    spinner.start('Stashing any unsaved changes before committing')
+    spinner.info('Stashing any unsaved changes before committing')
     await system.exec('git stash', { cwd: process.env.MODDABLE })
     await system.exec(`git pull origin ${branch}`, {
       cwd: process.env.MODDABLE,
     })
-
-    await system.exec('rm -rf build/{tmp,bin}', { cwd: process.env.MODDABLE })
+    rebuildTools = true
     spinner.succeed()
+  }
 
+  if (rebuildTools) {
+    const spinner = print.spin()
     spinner.start('Rebuilding platform tools')
+
+    await system.exec('rm -rf build/{tmp,bin}', { cwd: process.env.MODDABLE })
+
     await system.exec('make', {
       cwd: BUILD_DIR,
       stdout: process.stdout,
diff --git a/src/toolbox/update/mac.ts b/src/toolbox/update/mac.ts
index 4fbcf5c..29e4a54 100644
--- a/src/toolbox/update/mac.ts
+++ b/src/toolbox/update/mac.ts
@@ -1,7 +1,7 @@
 import os from 'os'
 import { promisify } from 'util'
 import { chmod } from 'fs'
-import { print, system, filesystem } from 'gluegun'
+import { print, system, filesystem, prompt } from 'gluegun'
 import { INSTALL_PATH, MODDABLE_REPO, XSBUG_LOG_PATH } from '../setup/constants'
 import {
   fetchRelease,
@@ -27,6 +27,8 @@ export default async function ({ branch, release }: SetupArgs): Promise<void> {
     process.exit(1)
   }
 
+  let rebuildTools = false
+
   if (release !== undefined && (branch === undefined || branch === null)) {
     // get tag for current repo
     const currentTag: string = await system.exec('git tag', {
@@ -40,85 +42,104 @@ export default async function ({ branch, release }: SetupArgs): Promise<void> {
       process.exit(0)
     }
 
+    if (remoteRelease.assets.length === 0) {
+      print.warning(
+        `Moddable release ${release} does not have any pre-built assets.`,
+      )
+      rebuildTools = await prompt.confirm(
+        'Would you like to continue updating and build the SDK locally?',
+        false,
+      )
+
+      if (!rebuildTools) {
+        print.info(
+          'Please select another release version with pre-built assets: https://github.com/Moddable-OpenSource/moddable/releases',
+        )
+        process.exit(0)
+      }
+    }
+
     const spinner = print.spin()
     spinner.start('Updating Moddable SDK!')
 
-    const BIN_PATH = filesystem.resolve(
-      INSTALL_PATH,
-      'build',
-      'bin',
-      'mac',
-      'release',
-    )
-    const DEBUG_BIN_PATH = filesystem.resolve(
-      INSTALL_PATH,
-      'build',
-      'bin',
-      'mac',
-      'debug',
-    )
-
     filesystem.remove(process.env.MODDABLE)
     await system.spawn(
       `git clone ${MODDABLE_REPO} ${INSTALL_PATH} --depth 1 --branch ${remoteRelease.tag_name} --single-branch`,
     )
 
-    filesystem.dir(BIN_PATH)
-    filesystem.dir(DEBUG_BIN_PATH)
-
-    try {
-      const universalAssetName = `moddable-tools-macuniversal.zip`
-      await downloadReleaseTools({
-        writePath: BIN_PATH,
-        assetName: universalAssetName,
-        release: remoteRelease,
-      })
-    } catch (error: unknown) {
-      if (error instanceof MissingReleaseAssetError) {
-        const isArm = os.arch() === 'arm64'
-        const assetName = isArm
-          ? 'moddable-tools-mac64arm.zip'
-          : 'moddable-tools-mac64.zip'
+    if (!rebuildTools) {
+      const BIN_PATH = filesystem.resolve(
+        INSTALL_PATH,
+        'build',
+        'bin',
+        'mac',
+        'release',
+      )
+      const DEBUG_BIN_PATH = filesystem.resolve(
+        INSTALL_PATH,
+        'build',
+        'bin',
+        'mac',
+        'debug',
+      )
+
+      filesystem.dir(BIN_PATH)
+      filesystem.dir(DEBUG_BIN_PATH)
+
+      try {
+        const universalAssetName = `moddable-tools-macuniversal.zip`
         await downloadReleaseTools({
           writePath: BIN_PATH,
-          assetName,
+          assetName: universalAssetName,
           release: remoteRelease,
         })
-      } else {
-        throw error as Error
+      } catch (error: unknown) {
+        if (error instanceof MissingReleaseAssetError) {
+          const isArm = os.arch() === 'arm64'
+          const assetName = isArm
+            ? 'moddable-tools-mac64arm.zip'
+            : 'moddable-tools-mac64.zip'
+          await downloadReleaseTools({
+            writePath: BIN_PATH,
+            assetName,
+            release: remoteRelease,
+          })
+        } else {
+          throw error as Error
+        }
       }
-    }
 
-    spinner.info('Updating tool permissions')
-    const tools = filesystem.list(BIN_PATH) ?? []
-    await Promise.all(
-      tools.map(async (tool) => {
-        if (tool.endsWith('.app')) {
-          const mainPath = filesystem.resolve(
-            BIN_PATH,
-            tool,
-            'Contents',
-            'MacOS',
-            'main',
+      spinner.info('Updating tool permissions')
+      const tools = filesystem.list(BIN_PATH) ?? []
+      await Promise.all(
+        tools.map(async (tool) => {
+          if (tool.endsWith('.app')) {
+            const mainPath = filesystem.resolve(
+              BIN_PATH,
+              tool,
+              'Contents',
+              'MacOS',
+              'main',
+            )
+            await chmodPromise(mainPath, 0o751)
+          } else {
+            await chmodPromise(filesystem.resolve(BIN_PATH, tool), 0o751)
+          }
+          await filesystem.copyAsync(
+            filesystem.resolve(BIN_PATH, tool),
+            filesystem.resolve(DEBUG_BIN_PATH, tool),
           )
-          await chmodPromise(mainPath, 0o751)
-        } else {
-          await chmodPromise(filesystem.resolve(BIN_PATH, tool), 0o751)
-        }
-        await filesystem.copyAsync(
-          filesystem.resolve(BIN_PATH, tool),
-          filesystem.resolve(DEBUG_BIN_PATH, tool),
-        )
-      }),
-    )
-    if (system.which('npm') !== null) {
-      spinner.start('Installing xsbug-log dependencies')
-      await system.exec('npm install', { cwd: XSBUG_LOG_PATH })
-      spinner.succeed()
+        }),
+      )
+      if (system.which('npm') !== null) {
+        spinner.start('Installing xsbug-log dependencies')
+        await system.exec('npm install', { cwd: XSBUG_LOG_PATH })
+        spinner.succeed()
+      }
+      spinner.succeed(
+        'Moddable SDK successfully updated! Start the xsbug.app and run the "helloworld example": xs-dev run --example helloworld',
+      )
     }
-    spinner.succeed(
-      'Moddable SDK successfully updated! Start the xsbug.app and run the "helloworld example": xs-dev run --example helloworld',
-    )
   }
 
   if (typeof branch === 'string') {
@@ -135,6 +156,8 @@ export default async function ({ branch, release }: SetupArgs): Promise<void> {
       process.exit(0)
     }
 
+    rebuildTools = true
+
     const spinner = print.spin()
     spinner.start('Updating Moddable SDK!')
 
@@ -143,7 +166,10 @@ export default async function ({ branch, release }: SetupArgs): Promise<void> {
     await system.exec(`git pull origin ${branch}`, {
       cwd: process.env.MODDABLE,
     })
-
+    spinner.succeed()
+  }
+  if (rebuildTools) {
+    const spinner = print.spin()
     const BUILD_DIR = filesystem.resolve(
       process.env.MODDABLE ?? '',
       'build',
@@ -156,7 +182,6 @@ export default async function ({ branch, release }: SetupArgs): Promise<void> {
     )
     filesystem.remove(BUILD_DIR)
     filesystem.remove(TMP_DIR)
-    spinner.succeed()
 
     spinner.start('Rebuilding platform tools')
     // install release assets or build