Visual Studio Code Integrated Terminal Environment Shell Scripts

Posted Sunday, June 16, 2024 by Sri. Tagged TOOL

After getting an M1 Macbook Pro, many of the old NodeJS projects that pre-date this CPU started acting weird; any version of node that predated v14 did not have reliable library support, and it's only v16 and beyond that it's necessary. In the cases I was testing a Node v10 project, many installation errors would occur.

So, there are two cases I wanted to check for automatically:

  • what is the architecture? is it ARM or X86?
  • what is the node version?
  • as a bonus, does the node version match what's in .nvmrc if one exists?

With the help of GitHub Copilot looking up shell syntax, I made a sh script that also works on zsh and bash variants. It runs whenever you open an integrated terminal in Visual Studio Code.

If the current node version matches what's in .nvmrc, you'll see something like this:

Successful Node version match output

If there is no match between versions, you'll see something like this:

Successful Node version match output

Here's the shell script, which is installed in .vscode folder of the workspace. Note that it is sourced from within Visual Studio Code's terminal profile, so it's not shebanged with #!/bin/sh and doesn't need to be set as executable.

vs_env (shell script)

# vs_env - Sri's Visual Studio Code Environment Shell Script
#
# The following script is invoked from inside a Visual Studio Code integrated terminal
# and is located in a workspace .vscode folder. Accompanying changes must be made to
# the .code-workspace file to enable this script to run. As an example, these settings
# in the .code-workspace file will first force x86 mode on an ARM-based Mac through
# Rosetta 2, and then run the script vs_env script
#
# "terminal.integrated.profiles.osx": {
# "x86 macos": {
# "path": "/usr/bin/arch",
# "args": [
# "-arch",
# "x86_64",
# "${env:SHELL}",
# "-i",
# "-c",
# "export VSCODE_TERM='x86 shell';source ${workspaceFolder}/.vscode/vs_env; exec ${env:SHELL}"
# ]
# }
# },
# "terminal.integrated.defaultProfile.osx": "x86 macos",
#
# For more information on terminal profiles in vscode, see
# https://code.visualstudio.com/docs/terminal/profiles

# ANSI Terminal Colors
ALRT="\033[33;1m" # yellow
INFO="\033[34;1m" # blue
NRML="\033[0m" # normal
BOLD="\033[1m" # normal bold

# Check if NVM_DIR is defined (this is set by nvm on startup)
if [ -z "$NVM_DIR" ]; then
printf "\n"
printf "vsenv: ${ALRT}NVM does not appear to be installed${NRML}\n"
printf " Does your ${INFO}~/.zshrc${NRML} have ${INFO}export NVM_DIR${NRML} lines?\n"
printf "\n"
printf " If you haven't yet installed nvm, please follow the instructions\n"
printf " at https://github.com/nvm-sh/nvm to install it."
printf " If you are using 'bash' as your default shell, you can copy\n"
printf " these lines to your .zshrc file so nvm will also work in zsh.\n"
return
fi

# Check if shell is opening inside a VSCODE integrated terminal
# is NVM is installed, there is a .nvmrc file and a .vscode directory?
if [ -n "$NVM_DIR" ] && [ -s "./.nvmrc" ] && [ -d "./.vscode" ]; then
NVM_RC=$(cat ./.nvmrc)
REQ_VERSION=$(nvm version $NVM_RC)
if [ "$REQ_VERSION" = "N/A" ]; then
REQ_VERSION="$NVM_RC (requires nvm install)"
fi

CUR_VERSION=$(nvm current)
NODE_LABEL="$NVM_RC"
if [ "$NVM_RC" != "$REQ_VERSION" ]; then
NODE_LABEL="$NODE_LABEL ($REQ_VERSION)"
fi

printf "\n"
printf "vsenv: VISUAL STUDIO CODE INTEGRATED TERMINAL DETECTED\n"
printf " detected node version is ${INFO}$NODE_LABEL${NRML}\n"
ARCH=$(uname -m)
printf " detected architecture is ${INFO}$ARCH${NRML} (can override in code-workspace)\n"

# check if node binary is in the path
if ! command -v node &> /dev/null; then
printf "\n"
printf "vsenv: ${ALRT}The node binary can not be found! That is weird!${NRML}\n"
printf " This is a possible incompatibility with your shell environment.\n"
printf " Contact your friendly neighborhood developer for help.\n"
printf " your shell: $SHELL\n"
printf " your path: $PATH\n"
printf " your nvm dir: $NVM_DIR\n"
return
else
printf "\n"
printf "vsenv: node binary found at ${INFO}$(command -v node)${NRML}\n"
fi

if [ "$CUR_VERSION" != "$REQ_VERSION" ]; then
printf "\n"
printf "vsenv: ** WARNING **\n"
printf " This shell is using version ${INFO}$CUR_VERSION${NRML}, "
printf "not the ${ALRT}specified ${INFO}$REQ_VERSION${NRML} in .nvmrc\n"
printf " Type ${ALRT}nvm use${NRML} to use .nvmrc version. "
printf "You may need to run ${ALRT}npm ci${NRML} again.\n"
printf " If you want to use this version as default, "
printf "type ${ALRT}nvm alias default $REQ_VERSION${NRML}\n"
fi
else
printf "\n"
printf "vsenv: ${ALRT}Missing .nvmrc file or .vscode folder${NRML}\n"
printf " This script is intended to be used within a VSCODE integrated terminal opened "
printf "as a Code Workspace.\n"
printf " The project folder should contain a .nvmrc file and a .vscode folder.\n"
fi

# for bash and zsh shells: if ./ is not in PATH, add it to end of PATH
if [[ $PATH != *":./"* ]]; then
export PATH=$PATH:./
printf "\n"
printf "vsenv: adding './' to end of PATH for easier CLI execution in _ur directory!\n"
printf " (applies only to this shell)\n"
fi

printf "\n"

The script is copied into the .vscode folder, and the .code-workspace project sets Integrated Terminal Profiles that can be invoked whenever a new integrated terminal window works. This does not affect regular terminals outside of Visual Studio Code.

Here are some examples using the terminal profiles to invoke the vs_env script. The choice of profile is at the bottom, and can be applied per-platform.

required .code-workspace settings

    "terminal.integrated.profiles.osx": {
"x86 macos": {
"path": "/usr/bin/arch",
"args": [
"-arch",
"x86_64",
"${env:SHELL}",
"-i",
"-c",
"export VSCODE_TERM='x86 shell';source ${workspaceFolder}/.vscode/vs_env; exec ${env:SHELL}"
]
},
"arm64 macos": {
"path": "/usr/bin/arch",
"args": [
"-arch",
"arm64",
"${env:SHELL}",
"-i",
"-c",
"export VSCODE_TERM='arm64 shell'; cd ${workspaceFolder}; source .vscode/vs_env; exec ${env:SHELL}"
]
},
"generic macos": {
"path": "${env:SHELL}",
"args": [
"-i",
"-c",
"export VSCODE_TERM='generic bash'; cd ${workspaceFolder}; source .vscode/vs_env; exec ${env:SHELL}"
]
}
},
"terminal.integrated.profiles.linux": {
"generic bash": {
"path": "/bin/bash",
"args": [
"-i",
"-c",
"export VSCODE_TERM='generic bash'; cd ${workspaceFolder}; source .vscode/vs_env; exec ${env:SHELL}"
]
}
},
// this line will force use of the selected profile
// use "x86 macos" to force Rosetta x86 emulation in terminal
"terminal.integrated.defaultProfile.osx": "generic macos",
"terminal.integrated.defaultProfile.linux": "generic bash"
}

In addition to checking versions, the script attempts to...

  • to detects whether nvm is installed
  • whether the node binary is actually a reachable commant; this sometimes happens in weird configurations
  • adds ./ to the PATH inside the integrated terminal only because I don't like typing ./ in front of commands

Notes

People may find this kind of scaffolding excessive, but node version management is a weird thing when you first encounter it and it is a major point of confusion.

My development prioritiesI like an easy CLONE AND GO approach, which reminds me of my ancient Apple II days where "booting" into the environment was practically instantaneous. My list of dev priorities are detailed in the URSYS Wiki are a bit unusual, given the nature of my work. I like standardized environments that use free tools and have few peer dependencies, so eager developers without NodeJS experience with Node-related server administration can jump right in.