Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/run-entrypoint.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./silence-sqlite-warning.js";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";

Expand Down
14 changes: 14 additions & 0 deletions bin/silence-sqlite-warning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// node:sqlite is a release candidate but still emits an ExperimentalWarning in every process that loads it.
// Built-ins load while the ES module graph links, before any app code runs, so this filter must be preloaded:
// run-entrypoint.js imports it first, and the agent daemon receives it through `--import`.
const emitWarning = process.emitWarning;

process.emitWarning = function (warning, ...args) {
// emitWarning(warning, type) and emitWarning(warning, { type }) are both valid call shapes.
const type = args[0]?.type ?? args[0];
const message = String(warning?.message ?? warning);

if (type === "ExperimentalWarning" && message.startsWith("SQLite")) return;

return emitWarning.call(process, warning, ...args);
};
165 changes: 165 additions & 0 deletions install/install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# DevSpace installer for Windows.
#
# powershell -c "irm https://raw.githubusercontent.com/Waishnav/devspace/main/install/install.ps1 | iex"
#
# Mirrors install.sh: uses a compatible Node already on PATH, otherwise downloads a checksum-verified Node 24 LTS
# for DevSpace alone, installs @waishnav/devspace into a user-owned npm prefix, writes `devspace` launchers that
# pin that Node, then runs `devspace init`. Rerun it to update. DevSpace itself also needs Git Bash on Windows.
#
# Environment overrides: DEVSPACE_VERSION (default: beta), DEVSPACE_INSTALL_DIR, DEVSPACE_BIN_DIR, DEVSPACE_NO_INIT=1,
# DEVSPACE_NO_MODIFY_PATH=1, DEVSPACE_PACKAGE_SPEC (see install.sh).

$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"

$Package = "@waishnav/devspace"
# v1.1 is the first release installable without install scripts, and it is still on the beta tag.
# Switch this to latest once v1.1 ships as stable.
$Version = if ($env:DEVSPACE_VERSION) { $env:DEVSPACE_VERSION } else { "beta" }
$PackageSpec = if ($env:DEVSPACE_PACKAGE_SPEC) { $env:DEVSPACE_PACKAGE_SPEC } else { "$Package@$Version" }
$InstallDir = if ($env:DEVSPACE_INSTALL_DIR) { $env:DEVSPACE_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA "devspace\install" }
$BinDir = if ($env:DEVSPACE_BIN_DIR) { $env:DEVSPACE_BIN_DIR } else { Join-Path $env:LOCALAPPDATA "devspace\bin" }
$NodeMajor = 24
# Must match package.json engines.node.
$NodeMin = "22.19.0"
$NodeMaxMajor = 27

function Test-CompatibleNode([string]$Node) {
$script = 'const [maj,min,patch]=process.versions.node.split(".").map(Number);const [a,b,c]=process.argv[1].split(".").map(Number);const ok=maj>a||(maj===a&&(min>b||(min===b&&patch>=c)));process.exit(ok&&maj<Number(process.argv[2])?0:1)'
& $Node -e $script $NodeMin $NodeMaxMajor *> $null
return $LASTEXITCODE -eq 0
}

function Install-Node {
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" { "x64" }
"ARM64" { "arm64" }
default { throw "Unsupported CPU architecture: $($env:PROCESSOR_ARCHITECTURE)" }
}
$dist = "https://nodejs.org/dist/latest-v$NodeMajor.x"
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("devspace-node-" + [Guid]::NewGuid())
New-Item -ItemType Directory -Path $tmp | Out-Null

try {
Write-Host "Downloading Node $NodeMajor LTS for win-$arch..."
$sums = (Invoke-WebRequest -UseBasicParsing "$dist/SHASUMS256.txt").Content -split "`n"
$line = $sums | Where-Object { $_ -match "\s(node-v[\d.]+-win-$arch\.zip)$" } | Select-Object -First 1
if (-not $line) { throw "No Node $NodeMajor build is published for win-$arch." }
$expected, $file = $line.Trim() -split "\s+"
$zip = Join-Path $tmp $file
Invoke-WebRequest -UseBasicParsing "$dist/$file" -OutFile $zip
if ((Get-FileHash -Algorithm SHA256 $zip).Hash.ToLower() -ne $expected) {
throw "Checksum mismatch for $file; refusing to install it."
}

Expand-Archive -Path $zip -DestinationPath $tmp
$nodeDir = Join-Path $InstallDir "node"
if (Test-Path $nodeDir) { Remove-Item -Recurse -Force $nodeDir }
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
Move-Item (Join-Path $tmp ($file -replace "\.zip$", "")) $nodeDir
Write-Host "Installed $($file -replace '\.zip$', '') into $nodeDir"
return (Join-Path $nodeDir "node.exe")
} finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}

function Select-Node {
$managed = Join-Path $InstallDir "node\node.exe"
if ((Test-Path $managed) -and (Test-CompatibleNode $managed)) { return $managed }

$system = Get-Command node -ErrorAction SilentlyContinue
if ($system -and (Test-CompatibleNode $system.Source)) {
# Pin the real binary, since version managers switch what `node` points to but keep each version on disk.
# nvm-windows repoints a symlinked directory, which process.execPath may report unresolved, so follow it.
$dir = Get-Item -LiteralPath (Split-Path $system.Source)
if ($dir.LinkType -and $dir.Target) {
$target = @($dir.Target)[0]
if (-not [IO.Path]::IsPathRooted($target)) { $target = Join-Path (Split-Path $dir.FullName) $target }
return Join-Path $target (Split-Path $system.Source -Leaf)
}
return (& $system.Source -p "process.execPath")
}
if ($system) { Write-Host "Found Node $(& $system.Source --version), but DevSpace needs >=$NodeMin <$NodeMaxMajor." }

return Install-Node
}

# cmd.exe reads .cmd files in the console code page, so a launcher cannot embed non-ASCII text literally.
# Spell paths under the user's profile folders through their environment variables instead, which cmd expands
# with full Unicode, and keep any literal % from being expanded.
function ConvertTo-CmdPath([string]$Path) {
$prefix = ""
$rest = $Path
foreach ($name in @("LOCALAPPDATA", "APPDATA", "USERPROFILE")) {
$root = [Environment]::GetEnvironmentVariable($name)
if ($root -and $Path.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) {
$prefix = "%$name%"
$rest = $Path.Substring($root.Length)
break
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
if ($rest -match "[^\x00-\x7F]") {
throw "Cannot write a launcher for $Path because cmd.exe cannot read its non-ASCII characters. If it is a Node runtime, move that Node to an ASCII path or remove it from PATH so the installer downloads its own; otherwise set DEVSPACE_INSTALL_DIR and DEVSPACE_BIN_DIR to ASCII paths. Then rerun."
}
return $prefix + $rest.Replace("%", "%%")
}

$Node = Select-Node
$NodeDir = Split-Path $Node
$Npm = Join-Path $NodeDir "npm.cmd"
if (-not (Test-Path $Npm)) { throw "npm was not found next to $Node. Install npm for that Node and rerun." }

Write-Host "Installing $PackageSpec with Node $(& $Node --version)..."
$prefix = Join-Path $InstallDir "npm"
New-Item -ItemType Directory -Path $prefix -Force | Out-Null
# npm resolves `node` from PATH, so put the selected Node first.
# DevSpace and its dependencies ship prebuilt binaries, so no install scripts are needed.
$env:Path = "$NodeDir;$env:Path"
& $Npm install --global --prefix $prefix --ignore-scripts --no-fund --no-audit --no-update-notifier --loglevel=error $PackageSpec
if ($LASTEXITCODE -ne 0) { throw "npm install failed." }

$packageDir = Join-Path $prefix "node_modules\$Package"
if (-not (Test-Path (Join-Path $packageDir "bin\devspace.js"))) { throw "Install finished but $packageDir\bin\devspace.js is missing." }
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null

$cmdNode = ConvertTo-CmdPath $Node
foreach ($command in @("devspace", "devspace-agentd")) {
$cmdEntry = ConvertTo-CmdPath (Join-Path $packageDir "bin\$command.js")
# Pin the Node that installed DevSpace so a version manager switching `node` cannot break it.
# goto instead of an if (...) block: paths such as "Program Files (x86)" would close the block early.
$launcher = @(
"@echo off",
"rem Generated by the DevSpace installer. Rerun the installer to update.",
"if not exist `"$cmdNode`" goto missing",
"`"$cmdNode`" `"$cmdEntry`" %*",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
"exit /b %errorlevel%",
":missing",
"echo DevSpace's Node runtime is missing: `"$cmdNode`" 1>&2",
"echo Rerun the DevSpace installer. 1>&2",
"exit /b 1"
) -join "`r`n"
Set-Content -Path (Join-Path $BinDir "$command.cmd") -Value $launcher -Encoding ASCII
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if (($userPath -split ";") -notcontains $BinDir) {
if ($env:DEVSPACE_NO_MODIFY_PATH -eq "1") {
Write-Host "Add $BinDir to your PATH to run devspace."
} else {
[Environment]::SetEnvironmentVariable("Path", (@($BinDir, $userPath) | Where-Object { $_ }) -join ";", "User")
Write-Host "Added $BinDir to your user PATH (open a new terminal to pick it up)."
}
}
$env:Path = "$BinDir;$env:Path"

$devspace = Join-Path $BinDir "devspace.cmd"
$version = & $devspace --version
if ($LASTEXITCODE -ne 0) { throw "The installed launcher at $devspace did not run." }
Write-Host "DevSpace $version is installed at $devspace"

if ($env:DEVSPACE_NO_INIT -ne "1") {
Write-Host ""
& $devspace init
if ($LASTEXITCODE -ne 0) { Write-Host "Setup did not finish. Run ``devspace init`` to continue." }
}
206 changes: 206 additions & 0 deletions install/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/bin/sh
# DevSpace installer for macOS, Linux, and WSL.
#
# curl -fsSL https://raw.githubusercontent.com/Waishnav/devspace/main/install/install.sh | sh
#
# Uses a compatible Node already on PATH, otherwise downloads a checksum-verified Node 24 LTS for DevSpace alone.
# Installs @waishnav/devspace into a user-owned npm prefix (no sudo), writes `devspace` launchers that pin that
# Node, then runs `devspace init`. Rerun it to update.
#
# Environment overrides:
# DEVSPACE_VERSION npm version or dist-tag to install (default: beta)
# DEVSPACE_INSTALL_DIR runtime and package location (default: ~/.local/share/devspace/install)
# DEVSPACE_BIN_DIR launcher location (default: ~/.local/bin)
# DEVSPACE_NO_INIT=1 skip running `devspace init` afterwards
# DEVSPACE_NO_MODIFY_PATH=1 never edit shell profiles
# DEVSPACE_PACKAGE_SPEC npm spec to install instead, such as a local tarball (for testing)

set -eu

PACKAGE="@waishnav/devspace"
# v1.1 is the first release installable without install scripts, and it is still on the beta tag.
# Switch this to latest once v1.1 ships as stable.
VERSION="${DEVSPACE_VERSION:-beta}"
PACKAGE_SPEC="${DEVSPACE_PACKAGE_SPEC:-$PACKAGE@$VERSION}"
INSTALL_DIR="${DEVSPACE_INSTALL_DIR:-$HOME/.local/share/devspace/install}"
BIN_DIR="${DEVSPACE_BIN_DIR:-$HOME/.local/bin}"
NODE_MAJOR=24
# Must match package.json engines.node.
NODE_MIN="22.19.0"
NODE_MAX_MAJOR=27

info() { printf '%s\n' "$*"; }
fail() { printf 'error: %s\n' "$*" >&2; exit 1; }

need() { command -v "$1" >/dev/null 2>&1 || fail "'$1' is required but not installed."; }

# Single-quotes a value for POSIX sh, so generated files keep paths containing $, `, ", or spaces literal.
shell_quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

# Succeeds when the given `node` binary satisfies NODE_MIN <= version < NODE_MAX_MAJOR.
node_is_compatible() {
"$1" -e '
const [maj, min, patch] = process.versions.node.split(".").map(Number);
const [wantMaj, wantMin, wantPatch] = process.argv[1].split(".").map(Number);
const atLeast = maj > wantMaj || (maj === wantMaj && (min > wantMin || (min === wantMin && patch >= wantPatch)));
process.exit(atLeast && maj < Number(process.argv[2]) ? 0 : 1);
' "$NODE_MIN" "$NODE_MAX_MAJOR" >/dev/null 2>&1
}

sha256() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1
else fail "sha256sum or shasum is required to verify the Node download."
fi
}

detect_platform() {
case "$(uname -s)" in
Linux) os=linux ;;
Darwin) os=darwin ;;
MINGW* | MSYS* | CYGWIN*) fail "Use install.ps1 on Windows, or run this script inside WSL." ;;
*) fail "Unsupported OS: $(uname -s)" ;;
esac

case "$(uname -m)" in
x86_64 | amd64) arch=x64 ;;
arm64 | aarch64) arch=arm64 ;;
*) fail "Unsupported CPU architecture: $(uname -m)" ;;
esac

# Rosetta reports x64 on Apple silicon; prefer the native build.
if [ "$os" = darwin ] && [ "$arch" = x64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then
arch=arm64
fi

if [ "$os" = linux ] && { [ -f /etc/alpine-release ] || ldd --version 2>&1 | grep -qi musl; }; then
musl=1
else
musl=0
fi
}

install_node() {
[ "$musl" = 0 ] || fail "nodejs.org has no musl build. Install Node $NODE_MIN+ with your package manager (apk add nodejs npm) and rerun."
need curl
need tar

dist="https://nodejs.org/dist/latest-v$NODE_MAJOR.x"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

info "Downloading Node $NODE_MAJOR LTS for $os-$arch..."
curl -fsSL "$dist/SHASUMS256.txt" -o "$tmp/SHASUMS256.txt"
file=$(awk -v suffix="-$os-$arch.tar.gz" 'substr($2, length($2) - length(suffix) + 1) == suffix { print $2; exit }' "$tmp/SHASUMS256.txt")
[ -n "$file" ] || fail "No Node $NODE_MAJOR build is published for $os-$arch."
expected=$(awk -v name="$file" '$2 == name { print $1; exit }' "$tmp/SHASUMS256.txt")

curl -fsSL "$dist/$file" -o "$tmp/$file"
[ "$(sha256 "$tmp/$file")" = "$expected" ] || fail "Checksum mismatch for $file; refusing to install it."

rm -rf "$INSTALL_DIR/node"
mkdir -p "$INSTALL_DIR/node"
tar -xzf "$tmp/$file" -C "$INSTALL_DIR/node" --strip-components=1
NODE="$INSTALL_DIR/node/bin/node"
info "Installed ${file%.tar.gz} into $INSTALL_DIR/node"
}

select_node() {
if [ -x "$INSTALL_DIR/node/bin/node" ] && node_is_compatible "$INSTALL_DIR/node/bin/node"; then
NODE="$INSTALL_DIR/node/bin/node"
elif command -v node >/dev/null 2>&1 && node_is_compatible "$(command -v node)"; then
# Pin the real binary: version managers (nvm, fnm, mise, asdf, Volta) switch what `node` points to but
# keep every installed version on disk. Homebrew and Nix delete old versions on upgrade instead, so for
# them keep the stable link that `command -v` found.
NODE=$(node -p process.execPath)
case "$NODE" in
*/Cellar/* | /nix/store/*) NODE=$(command -v node) ;;
esac
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
else
if command -v node >/dev/null 2>&1; then
info "Found Node $(node --version), but DevSpace needs >=$NODE_MIN <$NODE_MAX_MAJOR."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
fi
install_node
fi

NODE_BIN_DIR=$(dirname "$NODE")
[ -x "$NODE_BIN_DIR/npm" ] || fail "npm was not found next to $NODE. Install npm for that Node and rerun."
}

install_package() {
info "Installing $PACKAGE_SPEC with Node $("$NODE" --version)..."
mkdir -p "$INSTALL_DIR/npm"
# npm's shebang resolves `node` from PATH, so put the selected Node first.
# DevSpace and its dependencies ship prebuilt binaries, so no install scripts are needed.
PATH="$NODE_BIN_DIR:$PATH" "$NODE_BIN_DIR/npm" install --global --prefix "$INSTALL_DIR/npm" \
--ignore-scripts --no-fund --no-audit --no-update-notifier --loglevel=error "$PACKAGE_SPEC"
}

write_launchers() {
package_dir="$INSTALL_DIR/npm/lib/node_modules/$PACKAGE"
[ -f "$package_dir/bin/devspace.js" ] || fail "Install finished but $package_dir/bin/devspace.js is missing."
mkdir -p "$BIN_DIR"

for command in devspace devspace-agentd; do
launcher="$BIN_DIR/$command"
# Pin the Node that installed DevSpace so a version manager switching `node` cannot break it.
cat >"$launcher" <<EOF
#!/bin/sh
# Generated by the DevSpace installer. Rerun the installer to update.
node=$(shell_quote "$NODE")
if [ ! -x "\$node" ]; then
echo "DevSpace's Node runtime (\$node) is missing. Rerun the DevSpace installer." >&2
exit 1
fi
exec "\$node" $(shell_quote "$package_dir/bin/$command.js") "\$@"
EOF
chmod 755 "$launcher"
done
}

ensure_path() {
case ":$PATH:" in *":$BIN_DIR:"*) return ;; esac
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

if [ "${DEVSPACE_NO_MODIFY_PATH:-0}" = 1 ]; then
info "Add $BIN_DIR to your PATH to run devspace."
return
fi

line="export PATH=$(shell_quote "$BIN_DIR"):\"\$PATH\""

case "$(basename "${SHELL:-sh}")" in
zsh) profile="${ZDOTDIR:-$HOME}/.zshrc" ;;
bash) if [ "$os" = darwin ]; then profile="$HOME/.bash_profile"; else profile="$HOME/.bashrc"; fi ;;
fish) profile="$HOME/.config/fish/config.fish"
# fish single quotes escape only backslash and the quote itself.
line="fish_add_path '$(printf '%s' "$BIN_DIR" | sed "s/[\\\\']/\\\\&/g")'" ;;
*) profile="$HOME/.profile" ;;
esac

if ! grep -Fqs "$line" "$profile"; then
mkdir -p "$(dirname "$profile")"
printf '\n# DevSpace\n%s\n' "$line" >>"$profile"
info "Added $BIN_DIR to PATH in $profile (open a new shell to pick it up)."
fi
}

run_init() {
if [ "${DEVSPACE_NO_INIT:-0}" = 1 ]; then return; fi

# Under `curl | sh` stdin is the script itself, so setup reads answers from the terminal.
if [ -r /dev/tty ] && [ -w /dev/tty ]; then
info ""
"$BIN_DIR/devspace" init </dev/tty >/dev/tty 2>&1 || info "Setup did not finish. Run \`devspace init\` to continue."
else
info "No terminal available; run \`devspace init\` to finish setup."
fi
}

detect_platform
select_node
install_package
write_launchers
ensure_path
version=$("$BIN_DIR/devspace" --version) || fail "The installed launcher at $BIN_DIR/devspace did not run."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
info "DevSpace $version is installed at $BIN_DIR/devspace"
run_init
Loading
Loading