├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── LICENSE ├── README.md ├── SHA256SUM ├── deno.json ├── install.ps1 ├── install.sh ├── install_test.ps1 ├── install_test.sh ├── install_test.ts └── shell-setup ├── bundle.ts ├── bundled.esm.js ├── deno.json ├── deno.lock └── src ├── environment.ts ├── main.ts ├── shell.ts └── util.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | shell-setup/bundled.esm.js -diff 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, windows-latest, macOS-latest] 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: lint 18 | if: matrix.os == 'macOS-latest' 19 | run: | 20 | brew install shfmt shellcheck 21 | shfmt -d . 22 | shellcheck -s sh *.sh 23 | 24 | - name: verify checksum 25 | if: matrix.os == 'ubuntu-latest' 26 | shell: bash 27 | run: | 28 | if ! shasum -a 256 -c SHA256SUM; then 29 | echo 'Checksum verification failed.' 30 | echo 'If the installer has been updated intentionally, update the checksum with:' 31 | echo 'shasum -a 256 install.{sh,ps1} > SHA256SUM' 32 | exit 1 33 | fi 34 | - name: tests shell 35 | shell: bash 36 | run: ./install_test.sh 37 | 38 | - name: tests powershell 39 | if: matrix.os == 'windows-latest' 40 | shell: powershell 41 | run: ./install_test.ps1 42 | 43 | - name: tests powershell core 44 | if: matrix.os == 'windows-latest' 45 | shell: pwsh 46 | run: ./install_test.ps1 47 | 48 | check-js: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v3 53 | 54 | - uses: denoland/setup-deno@v1 55 | with: 56 | deno-version: rc 57 | 58 | - name: deno lint 59 | run: deno lint 60 | 61 | - name: check fmt 62 | run: deno fmt --check 63 | 64 | - name: check bundled file up to date 65 | run: | 66 | cd shell-setup 67 | deno task bundle 68 | if ! git --no-pager diff --exit-code ./bundled.esm.js; then 69 | echo 'Bundled script is out of date, update it with `cd shell-setup; deno task bundle`'. 70 | exit 1 71 | fi 72 | - name: integration tests 73 | if: matrix.os != 'windows-latest' 74 | run: deno test -A --permit-no-files 75 | 76 | - name: dry run publishing 77 | run: deno publish --dry-run 78 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: rc 21 | 22 | - name: Publish to JSR on tag 23 | run: | 24 | cd shell-setup 25 | deno run -A jsr:@david/publish-on-tag@0.1.3 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2018-2024 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deno_install 2 | 3 | **One-line commands to install Deno on your system.** 4 | 5 | [![Build Status](https://github.com/denoland/deno_install/workflows/ci/badge.svg?branch=master)](https://github.com/denoland/deno_install/actions) 6 | 7 | ## Install Latest Version 8 | 9 | **With Shell:** 10 | 11 | ```sh 12 | curl -fsSL https://deno.land/install.sh | sh 13 | ``` 14 | 15 | **With PowerShell:** 16 | 17 | ```powershell 18 | irm https://deno.land/install.ps1 | iex 19 | ``` 20 | 21 | ## Install Specific Version 22 | 23 | **With Shell:** 24 | 25 | ```sh 26 | curl -fsSL https://deno.land/install.sh | sh -s v1.0.0 27 | ``` 28 | 29 | **With PowerShell:** 30 | 31 | ```powershell 32 | $v="1.0.0"; irm https://deno.land/install.ps1 | iex 33 | ``` 34 | 35 | ## Install via Package Manager 36 | 37 | **With 38 | [winget](https://github.com/microsoft/winget-pkgs/tree/master/manifests/d/DenoLand/Deno):** 39 | 40 | ```powershell 41 | winget install deno 42 | ``` 43 | 44 | **With 45 | [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/deno.json):** 46 | 47 | ```powershell 48 | scoop install deno 49 | ``` 50 | 51 | **With [Homebrew](https://formulae.brew.sh/formula/deno):** 52 | 53 | ```sh 54 | brew install deno 55 | ``` 56 | 57 | **With [Macports](https://ports.macports.org/port/deno/summary):** 58 | 59 | ```sh 60 | sudo port install deno 61 | ``` 62 | 63 | **With [Chocolatey](https://chocolatey.org/packages/deno):** 64 | 65 | ```powershell 66 | choco install deno 67 | ``` 68 | 69 | **With [Snap](https://snapcraft.io/deno):** 70 | 71 | ```sh 72 | sudo snap install deno 73 | ``` 74 | 75 | **With [Pacman](https://www.archlinux.org/pacman/):** 76 | 77 | ```sh 78 | pacman -S deno 79 | ``` 80 | 81 | **With [Zypper](https://software.opensuse.org/package/deno):** 82 | 83 | ```sh 84 | zypper install deno 85 | ``` 86 | 87 | **Build and install from source using [Cargo](https://lib.rs/crates/deno):** 88 | 89 | ```sh 90 | # Install the Protobuf compiler 91 | apt install -y protobuf-compiler # Linux 92 | brew install protobuf # macOS 93 | 94 | # Build and install Deno 95 | cargo install deno 96 | ``` 97 | 98 | ## Install and Manage Multiple Versions 99 | 100 | **With [asdf](https://asdf-vm.com) and 101 | [asdf-deno](https://github.com/asdf-community/asdf-deno):** 102 | 103 | ```sh 104 | asdf plugin add deno 105 | 106 | # Get latest version of deno available 107 | DENO_LATEST=$(asdf latest deno) 108 | 109 | asdf install deno $DENO_LATEST 110 | 111 | # Activate globally with: 112 | asdf global deno $DENO_LATEST 113 | 114 | # Activate locally in the current folder with: 115 | asdf local deno $DENO_LATEST 116 | 117 | #====================================================== 118 | # If you want to install specific version of deno then use that version instead 119 | # of DENO_LATEST variable example 120 | asdf install deno 1.0.0 121 | 122 | # Activate globally with: 123 | asdf global deno 1.0.0 124 | 125 | # Activate locally in the current folder with: 126 | asdf local deno 1.0.0 127 | ``` 128 | 129 | **With 130 | [Scoop](https://github.com/lukesampson/scoop/wiki/Switching-Ruby-And-Python-Versions):** 131 | 132 | ```sh 133 | # Install a specific version of deno: 134 | scoop install deno@1.0.0 135 | 136 | # Switch to v1.0.0 137 | scoop reset deno@1.0.0 138 | 139 | # Switch to the latest version 140 | scoop reset deno 141 | ``` 142 | 143 | ## Environment Variables 144 | 145 | - `DENO_INSTALL` - The directory in which to install Deno. This defaults to 146 | `$HOME/.deno`. The executable is placed in `$DENO_INSTALL/bin`. One 147 | application of this is a system-wide installation: 148 | 149 | **With Shell (`/usr/local`):** 150 | 151 | ```sh 152 | curl -fsSL https://deno.land/install.sh | sudo DENO_INSTALL=/usr/local sh 153 | ``` 154 | 155 | **With PowerShell (`C:\Program Files\deno`):** 156 | 157 | ```powershell 158 | # Run as administrator: 159 | $env:DENO_INSTALL = "C:\Program Files\deno" 160 | irm https://deno.land/install.ps1 | iex 161 | ``` 162 | 163 | ## Verification 164 | 165 | As an additional layer of security, you can verify the integrity of the shell 166 | installer against the provided checksums. 167 | 168 | ```sh 169 | curl -fLso install.sh https://deno.land/install.sh 170 | ``` 171 | 172 | Verify the SHA256 checksum of the installer: 173 | 174 | ```sh 175 | curl -s https://raw.githubusercontent.com/denoland/deno_install/master/SHA256SUM | sha256sum --check --ignore-missing 176 | ``` 177 | 178 | ## Compatibility 179 | 180 | - The Shell installer can be used on Windows with 181 | [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about), 182 | [MSYS](https://www.msys2.org) or equivalent set of tools. 183 | 184 | ## Known Issues 185 | 186 | ### either unzip or 7z is required 187 | 188 | To decompress the `deno` archive, one of either 189 | [`unzip`](https://linux.die.net/man/1/unzip) or 190 | [`7z`](https://linux.die.net/man/1/7z) must be available on the target system. 191 | 192 | ```sh 193 | $ curl -fsSL https://deno.land/install.sh | sh 194 | Error: either unzip or 7z is required to install Deno (see: https://github.com/denoland/deno_install#either-unzip-or-7z-is-required ). 195 | ``` 196 | 197 | **When does this issue occur?** 198 | 199 | During the `install.sh` process, `unzip` or `7z` is used to extract the zip 200 | archive. 201 | 202 | **How can this issue be fixed?** 203 | 204 | You can install unzip via `brew install unzip` on MacOS or 205 | `sudo apt-get install unzip -y` on Linux. 206 | -------------------------------------------------------------------------------- /SHA256SUM: -------------------------------------------------------------------------------- 1 | 83f19ea13f6d7884f4dc0e2f92e7e08e7589204138d9b6edfcd53c3f07b3273b install.sh 2 | c564142dfb209500330c09c39176141f1d13e7e897ee2d8e8715a3dcdf8b324c install.ps1 3 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspace": [ 3 | "./shell-setup" 4 | ], 5 | "lint": { 6 | "exclude": [ 7 | "./shell-setup/bundled.esm.js" 8 | ], 9 | "rules": { 10 | "exclude": [ 11 | "no-slow-types" 12 | ] 13 | } 14 | }, 15 | "tasks": { 16 | "bundle": "cd shell-setup && deno task bundle" 17 | }, 18 | "lock": { "path": "./shell-setup/deno.lock", "frozen": true }, 19 | "fmt": { 20 | "exclude": [ 21 | "./shell-setup/bundled.esm.js", 22 | ".github" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | # Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 3 | # TODO(everyone): Keep this script simple and easily auditable. 4 | 5 | $ErrorActionPreference = 'Stop' 6 | 7 | if ($v) { 8 | $Version = "v${v}" 9 | } 10 | if ($Args.Length -eq 1) { 11 | $Version = $Args.Get(0) 12 | } 13 | 14 | $DenoInstall = $env:DENO_INSTALL 15 | $BinDir = if ($DenoInstall) { 16 | "${DenoInstall}\bin" 17 | } else { 18 | "${Home}\.deno\bin" 19 | } 20 | 21 | $DenoZip = "$BinDir\deno.zip" 22 | $DenoExe = "$BinDir\deno.exe" 23 | $Target = 'x86_64-pc-windows-msvc' 24 | 25 | $Version = if (!$Version) { 26 | curl.exe --ssl-revoke-best-effort -s "https://dl.deno.land/release-latest.txt" 27 | } else { 28 | $Version 29 | } 30 | 31 | $DownloadUrl = "https://dl.deno.land/release/${Version}/deno-${Target}.zip" 32 | 33 | if (!(Test-Path $BinDir)) { 34 | New-Item $BinDir -ItemType Directory | Out-Null 35 | } 36 | 37 | curl.exe --ssl-revoke-best-effort -Lo $DenoZip $DownloadUrl 38 | 39 | tar.exe xf $DenoZip -C $BinDir 40 | 41 | Remove-Item $DenoZip 42 | 43 | $User = [System.EnvironmentVariableTarget]::User 44 | $Path = [System.Environment]::GetEnvironmentVariable('Path', $User) 45 | if (!(";${Path};".ToLower() -like "*;${BinDir};*".ToLower())) { 46 | [System.Environment]::SetEnvironmentVariable('Path', "${Path};${BinDir}", $User) 47 | $Env:Path += ";${BinDir}" 48 | } 49 | 50 | Write-Output "Deno was installed successfully to ${DenoExe}" 51 | Write-Output "Run 'deno --help' to get started" 52 | Write-Output "Stuck? Join our Discord https://discord.gg/deno" 53 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2019 the Deno authors. All rights reserved. MIT license. 3 | # TODO(everyone): Keep this script simple and easily auditable. 4 | 5 | set -e 6 | 7 | if ! command -v unzip >/dev/null && ! command -v 7z >/dev/null; then 8 | echo "Error: either unzip or 7z is required to install Deno (see: https://github.com/denoland/deno_install#either-unzip-or-7z-is-required )." 1>&2 9 | exit 1 10 | fi 11 | 12 | if [ "$OS" = "Windows_NT" ]; then 13 | target="x86_64-pc-windows-msvc" 14 | else 15 | case $(uname -sm) in 16 | "Darwin x86_64") target="x86_64-apple-darwin" ;; 17 | "Darwin arm64") target="aarch64-apple-darwin" ;; 18 | "Linux aarch64") target="aarch64-unknown-linux-gnu" ;; 19 | *) target="x86_64-unknown-linux-gnu" ;; 20 | esac 21 | fi 22 | 23 | print_help_and_exit() { 24 | echo "Setup script for installing deno 25 | 26 | Options: 27 | -y, --yes 28 | Skip interactive prompts and accept defaults 29 | --no-modify-path 30 | Don't add deno to the PATH environment variable 31 | -h, --help 32 | Print help 33 | " 34 | echo "Note: Deno was not installed" 35 | exit 0 36 | } 37 | 38 | # Initialize variables 39 | should_run_shell_setup=false 40 | 41 | # Simple arg parsing - look for help flag, otherwise 42 | # ignore args starting with '-' and take the first 43 | # positional arg as the deno version to install 44 | for arg in "$@"; do 45 | case "$arg" in 46 | "-h") 47 | print_help_and_exit 48 | ;; 49 | "--help") 50 | print_help_and_exit 51 | ;; 52 | "-y") 53 | should_run_shell_setup=true 54 | ;; 55 | "--yes") 56 | should_run_shell_setup=true 57 | ;; 58 | "-"*) ;; 59 | *) 60 | if [ -z "$deno_version" ]; then 61 | deno_version="$arg" 62 | fi 63 | ;; 64 | esac 65 | done 66 | if [ -z "$deno_version" ]; then 67 | deno_version="$(curl -s https://dl.deno.land/release-latest.txt)" 68 | fi 69 | 70 | deno_uri="https://dl.deno.land/release/${deno_version}/deno-${target}.zip" 71 | deno_install="${DENO_INSTALL:-$HOME/.deno}" 72 | bin_dir="$deno_install/bin" 73 | exe="$bin_dir/deno" 74 | 75 | if [ ! -d "$bin_dir" ]; then 76 | mkdir -p "$bin_dir" 77 | fi 78 | 79 | curl --fail --location --progress-bar --output "$exe.zip" "$deno_uri" 80 | if command -v unzip >/dev/null; then 81 | unzip -d "$bin_dir" -o "$exe.zip" 82 | else 83 | 7z x -o"$bin_dir" -y "$exe.zip" 84 | fi 85 | chmod +x "$exe" 86 | rm "$exe.zip" 87 | 88 | echo "Deno was installed successfully to $exe" 89 | 90 | run_shell_setup() { 91 | $exe run -A --reload jsr:@deno/installer-shell-setup/bundled "$deno_install" "$@" 92 | } 93 | 94 | # If stdout is a terminal, see if we can run shell setup script (which includes interactive prompts) 95 | if { [ -z "$CI" ] && [ -t 1 ]; } || $should_run_shell_setup; then 96 | if $exe eval 'const [major, minor] = Deno.version.deno.split("."); if (major < 2 && minor < 42) Deno.exit(1)'; then 97 | if $should_run_shell_setup; then 98 | run_shell_setup -y "$@" # doublely sure to pass -y to run_shell_setup in this case 99 | else 100 | if [ -t 0 ]; then 101 | run_shell_setup "$@" 102 | else 103 | # This script is probably running piped into sh, so we don't have direct access to stdin. 104 | # Instead, explicitly connect /dev/tty to stdin 105 | run_shell_setup "$@" /dev/null; then 111 | echo "Run 'deno --help' to get started" 112 | else 113 | echo "Run '$exe --help' to get started" 114 | fi 115 | echo 116 | echo "Stuck? Join our Discord https://discord.gg/deno" 117 | -------------------------------------------------------------------------------- /install_test.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | $ErrorActionPreference = 'Stop' 4 | 5 | # Test that we can install the latest version at the default location. 6 | Remove-Item "~\.deno" -Recurse -Force -ErrorAction SilentlyContinue 7 | $env:DENO_INSTALL = "" 8 | $v = $null; .\install.ps1 9 | ~\.deno\bin\deno.exe --version 10 | 11 | # Test that we can install a specific version at a custom location. 12 | Remove-Item "~\deno-1.0.0" -Recurse -Force -ErrorAction SilentlyContinue 13 | $env:DENO_INSTALL = "$Home\deno-1.0.0" 14 | $v = "1.0.0"; .\install.ps1 15 | $DenoVersion = ~\deno-1.0.0\bin\deno.exe --version 16 | if (!($DenoVersion -like '*1.0.0*')) { 17 | throw $DenoVersion 18 | } 19 | 20 | # Test that we can install at a relative custom location. 21 | Remove-Item "bin" -Recurse -Force -ErrorAction SilentlyContinue 22 | $env:DENO_INSTALL = "." 23 | $v = "1.1.0"; .\install.ps1 24 | $DenoVersion = bin\deno.exe --version 25 | if (!($DenoVersion -like '*1.1.0*')) { 26 | throw $DenoVersion 27 | } 28 | 29 | # Test that the old temp file installer still works. 30 | Remove-Item "~\deno-1.0.1" -Recurse -Force -ErrorAction SilentlyContinue 31 | $env:DENO_INSTALL = "$Home\deno-1.0.1" 32 | $v = $null; .\install.ps1 v1.0.1 33 | $DenoVersion = ~\deno-1.0.1\bin\deno.exe --version 34 | if (!($DenoVersion -like '*1.0.1*')) { 35 | throw $DenoVersion 36 | } 37 | -------------------------------------------------------------------------------- /install_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Test that we can install the latest version at the default location. 6 | rm -f ~/.deno/bin/deno 7 | unset DENO_INSTALL 8 | sh ./install.sh 9 | ~/.deno/bin/deno --version 10 | 11 | # Test that we can install a specific version at a custom location. 12 | rm -rf ~/deno-1.15.0 13 | export DENO_INSTALL="$HOME/deno-1.15.0" 14 | ./install.sh v1.15.0 15 | ~/deno-1.15.0/bin/deno --version | grep 1.15.0 16 | 17 | # Test that we can install at a relative custom location. 18 | export DENO_INSTALL="." 19 | ./install.sh v1.46.0 20 | bin/deno --version | grep 1.46.0 21 | -------------------------------------------------------------------------------- /install_test.ts: -------------------------------------------------------------------------------- 1 | import $, { Path } from "jsr:@david/dax"; 2 | import { Pty } from "jsr:@sigma/pty-ffi"; 3 | import { assert, assertEquals, assertStringIncludes } from "jsr:@std/assert"; 4 | 5 | Deno.test( 6 | { name: "install skip prompts", ignore: Deno.build.os === "windows" }, 7 | async () => { 8 | await using testEnv = await TestEnv.setup(); 9 | const { env, tempDir, installScript, installDir } = testEnv; 10 | await testEnv.homeDir.join(".bashrc").ensureFile(); 11 | 12 | console.log("installscript contents", await installScript.readText()); 13 | 14 | const shellOutput = await runInBash( 15 | [`cat "${installScript.toString()}" | sh -s -- -y v2.0.0-rc.6`], 16 | { env, cwd: tempDir }, 17 | ); 18 | console.log(shellOutput); 19 | 20 | assertStringIncludes(shellOutput, "Deno was added to the PATH"); 21 | 22 | const deno = installDir.join("bin/deno"); 23 | assert(await deno.exists()); 24 | 25 | // Check that it's on the PATH now, and that it's the correct version. 26 | const output = await new Deno.Command("bash", { 27 | args: ["-i", "-c", "deno --version"], 28 | env, 29 | }).output(); 30 | const stdout = new TextDecoder().decode(output.stdout).trim(); 31 | 32 | const versionRe = /deno (\d+\.\d+\.\d+\S*)/; 33 | const match = stdout.match(versionRe); 34 | 35 | assert(match !== null); 36 | assertEquals(match[1], "2.0.0-rc.6"); 37 | }, 38 | ); 39 | 40 | Deno.test( 41 | { name: "install no modify path", ignore: Deno.build.os === "windows" }, 42 | async () => { 43 | await using testEnv = await TestEnv.setup(); 44 | const { env, tempDir, installScript, installDir } = testEnv; 45 | await testEnv.homeDir.join(".bashrc").ensureFile(); 46 | 47 | const shellOutput = await runInBash( 48 | [`cat "${installScript.toString()}" | sh -s -- -y v2.0.0-rc.6 --no-modify-path`], 49 | { env, cwd: tempDir }, 50 | ); 51 | 52 | assert( 53 | !shellOutput.includes("Deno was added to the PATH"), 54 | `Unexpected output, shouldn't have added to the PATH:\n${shellOutput}`, 55 | ); 56 | 57 | const deno = installDir.join("bin/deno"); 58 | assert(await deno.exists()); 59 | }, 60 | ); 61 | 62 | class TestEnv implements AsyncDisposable, Disposable { 63 | #tempDir: Path; 64 | private constructor( 65 | tempDir: Path, 66 | public homeDir: Path, 67 | public installDir: Path, 68 | public installScript: Path, 69 | public env: Record, 70 | ) { 71 | this.#tempDir = tempDir; 72 | } 73 | get tempDir() { 74 | return this.#tempDir; 75 | } 76 | static async setup({ env = {} }: { env?: Record } = {}) { 77 | const tempDir = $.path(await Deno.makeTempDir()); 78 | const homeDir = await tempDir.join("home").ensureDir(); 79 | const installDir = tempDir.join(".deno"); 80 | 81 | const tempSetup = tempDir.join("shell-setup.js"); 82 | await $.path(resolve("./shell-setup/bundled.esm.js")).copyFile(tempSetup); 83 | 84 | // Copy the install script to a temp location, and modify it to 85 | // run the shell setup script from the local source instead of JSR. 86 | const contents = await Deno.readTextFile(resolve("./install.sh")); 87 | const contentsLocal = contents.replaceAll( 88 | "jsr:@deno/installer-shell-setup/bundled", 89 | tempSetup.toString(), 90 | ); 91 | if (contents === contentsLocal) { 92 | throw new Error("Failed to point installer at local source"); 93 | } 94 | const installScript = tempDir.join("install.sh"); 95 | await installScript.writeText(contentsLocal); 96 | 97 | await Deno.chmod(installScript.toString(), 0o755); 98 | 99 | // Ensure that the necessary binaries are in the PATH. 100 | // It's not perfect, but the idea is to keep the test environment 101 | // as clean as possible to make it less host dependent. 102 | const needed = ["bash", "unzip", "cat", "sh"]; 103 | const binPaths = await Promise.all(needed.map((n) => $.which(n))); 104 | const searchPaths = new Set( 105 | binPaths.map((p, i) => { 106 | if (p === undefined) { 107 | throw new Error(`missing dependency: ${needed[i]}`); 108 | } 109 | return $.path(p).parentOrThrow().toString(); 110 | }), 111 | ); 112 | const newEnv = { 113 | HOME: homeDir.toString(), 114 | XDG_CONFIG_HOME: homeDir.toString(), 115 | DENO_INSTALL: installDir.toString(), 116 | PATH: searchPaths.values().toArray().join(":"), 117 | ZDOTDIR: homeDir.toString(), 118 | SHELL: "/bin/bash", 119 | CI: "", 120 | }; 121 | Object.assign(newEnv, env); 122 | return new TestEnv(tempDir, homeDir, installDir, installScript, newEnv); 123 | } 124 | async [Symbol.asyncDispose]() { 125 | await this.#tempDir.remove({ recursive: true }); 126 | } 127 | [Symbol.dispose]() { 128 | this.#tempDir.removeSync({ recursive: true }); 129 | } 130 | } 131 | 132 | async function runInBash( 133 | commands: string[], 134 | options: { cwd?: Path; env: Record }, 135 | ): Promise { 136 | const { cwd, env } = options; 137 | const bash = await $.which("bash") ?? "bash"; 138 | const pty = new Pty({ 139 | env: Object.entries(env), 140 | cmd: bash, 141 | args: [], 142 | }); 143 | if (cwd) { 144 | await pty.write(`cd "${cwd.toString()}"\n`); 145 | } 146 | 147 | for (const command of commands) { 148 | await pty.write(command + "\n"); 149 | } 150 | await pty.write("exit\n"); 151 | let output = ""; 152 | while (true) { 153 | const { data, done } = await pty.read(); 154 | output += data; 155 | if (done) { 156 | break; 157 | } 158 | } 159 | pty.close(); 160 | return output; 161 | } 162 | 163 | function resolve(s: string): URL { 164 | return new URL(import.meta.resolve(s)); 165 | } 166 | -------------------------------------------------------------------------------- /shell-setup/bundle.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from "npm:esbuild"; 2 | 3 | import { fromFileUrl } from "@std/path/from-file-url"; 4 | import { denoPlugins } from "jsr:@luca/esbuild-deno-loader"; 5 | 6 | const result = await esbuild.build({ 7 | plugins: denoPlugins({ 8 | configPath: fromFileUrl(import.meta.resolve("./deno.json")), 9 | lockPath: fromFileUrl(import.meta.resolve("./deno.lock")), 10 | }), 11 | entryPoints: ["./src/main.ts"], 12 | outfile: "./bundled.esm.js", 13 | bundle: true, 14 | format: "esm", 15 | }); 16 | 17 | if (result.errors.length || result.warnings.length) { 18 | console.error(`Errors: ${result.errors}, warnings: ${result.warnings}`); 19 | } 20 | 21 | await esbuild.stop(); 22 | -------------------------------------------------------------------------------- /shell-setup/bundled.esm.js: -------------------------------------------------------------------------------- 1 | // https://jsr.io/@david/which/0.4.1/mod.ts 2 | var RealEnvironment = class { 3 | env(key) { 4 | return Deno.env.get(key); 5 | } 6 | stat(path) { 7 | return Deno.stat(path); 8 | } 9 | statSync(path) { 10 | return Deno.statSync(path); 11 | } 12 | get os() { 13 | return Deno.build.os; 14 | } 15 | }; 16 | async function which(command, environment2 = new RealEnvironment()) { 17 | const systemInfo = getSystemInfo(command, environment2); 18 | if (systemInfo == null) { 19 | return void 0; 20 | } 21 | for (const pathItem of systemInfo.pathItems) { 22 | const filePath = pathItem + command; 23 | if (systemInfo.pathExts) { 24 | environment2.requestPermission?.(pathItem); 25 | for (const pathExt of systemInfo.pathExts) { 26 | const filePath2 = pathItem + command + pathExt; 27 | if (await pathMatches(environment2, filePath2)) { 28 | return filePath2; 29 | } 30 | } 31 | } else if (await pathMatches(environment2, filePath)) { 32 | return filePath; 33 | } 34 | } 35 | return void 0; 36 | } 37 | async function pathMatches(environment2, path) { 38 | try { 39 | const result = await environment2.stat(path); 40 | return result.isFile; 41 | } catch (err) { 42 | if (err instanceof Deno.errors.PermissionDenied) { 43 | throw err; 44 | } 45 | return false; 46 | } 47 | } 48 | function getSystemInfo(command, environment2) { 49 | const isWindows2 = environment2.os === "windows"; 50 | const envValueSeparator = isWindows2 ? ";" : ":"; 51 | const path = environment2.env("PATH"); 52 | const pathSeparator = isWindows2 ? "\\" : "/"; 53 | if (path == null) { 54 | return void 0; 55 | } 56 | return { 57 | pathItems: splitEnvValue(path).map((item) => normalizeDir(item)), 58 | pathExts: getPathExts(), 59 | isNameMatch: isWindows2 ? (a, b) => a.toLowerCase() === b.toLowerCase() : (a, b) => a === b 60 | }; 61 | function getPathExts() { 62 | if (!isWindows2) { 63 | return void 0; 64 | } 65 | const pathExtText = environment2.env("PATHEXT") ?? ".EXE;.CMD;.BAT;.COM"; 66 | const pathExts = splitEnvValue(pathExtText); 67 | const lowerCaseCommand = command.toLowerCase(); 68 | for (const pathExt of pathExts) { 69 | if (lowerCaseCommand.endsWith(pathExt.toLowerCase())) { 70 | return void 0; 71 | } 72 | } 73 | return pathExts; 74 | } 75 | function splitEnvValue(value) { 76 | return value.split(envValueSeparator).map((item) => item.trim()).filter((item) => item.length > 0); 77 | } 78 | function normalizeDir(dirPath) { 79 | if (!dirPath.endsWith(pathSeparator)) { 80 | dirPath += pathSeparator; 81 | } 82 | return dirPath; 83 | } 84 | } 85 | 86 | // src/environment.ts 87 | import { homedir as getHomeDir } from "node:os"; 88 | async function tryStat(path) { 89 | try { 90 | return await Deno.stat(path); 91 | } catch (error) { 92 | if (error instanceof Deno.errors.NotFound || error instanceof Deno.errors.PermissionDenied && (await Deno.permissions.query({ name: "read", path })).state == "granted") { 93 | return; 94 | } 95 | throw error; 96 | } 97 | } 98 | var environment = { 99 | writeTextFile: Deno.writeTextFile, 100 | readTextFile: Deno.readTextFile, 101 | async isExistingFile(path) { 102 | const info2 = await tryStat(path); 103 | return info2?.isFile ?? false; 104 | }, 105 | async isExistingDir(path) { 106 | const info2 = await tryStat(path); 107 | return info2?.isDirectory ?? false; 108 | }, 109 | pathExists(path) { 110 | return tryStat(path).then((info2) => info2 !== void 0); 111 | }, 112 | mkdir: Deno.mkdir, 113 | homeDir: getHomeDir(), 114 | findCmd: which, 115 | getEnv(name) { 116 | return Deno.env.get(name); 117 | }, 118 | async runCmd(cmd, args) { 119 | return await new Deno.Command(cmd, { 120 | args, 121 | stderr: "piped", 122 | stdout: "piped", 123 | stdin: "null" 124 | }).output(); 125 | } 126 | }; 127 | 128 | // https://jsr.io/@std/path/1.0.6/_os.ts 129 | var isWindows = globalThis.Deno?.build.os === "windows" || globalThis.navigator?.platform?.startsWith("Win") || globalThis.process?.platform?.startsWith("win") || false; 130 | 131 | // https://jsr.io/@std/path/1.0.6/_common/assert_path.ts 132 | function assertPath(path) { 133 | if (typeof path !== "string") { 134 | throw new TypeError( 135 | `Path must be a string, received "${JSON.stringify(path)}"` 136 | ); 137 | } 138 | } 139 | 140 | // https://jsr.io/@std/path/1.0.6/_common/basename.ts 141 | function stripSuffix(name, suffix) { 142 | if (suffix.length >= name.length) { 143 | return name; 144 | } 145 | const lenDiff = name.length - suffix.length; 146 | for (let i = suffix.length - 1; i >= 0; --i) { 147 | if (name.charCodeAt(lenDiff + i) !== suffix.charCodeAt(i)) { 148 | return name; 149 | } 150 | } 151 | return name.slice(0, -suffix.length); 152 | } 153 | function lastPathSegment(path, isSep, start = 0) { 154 | let matchedNonSeparator = false; 155 | let end = path.length; 156 | for (let i = path.length - 1; i >= start; --i) { 157 | if (isSep(path.charCodeAt(i))) { 158 | if (matchedNonSeparator) { 159 | start = i + 1; 160 | break; 161 | } 162 | } else if (!matchedNonSeparator) { 163 | matchedNonSeparator = true; 164 | end = i + 1; 165 | } 166 | } 167 | return path.slice(start, end); 168 | } 169 | function assertArgs(path, suffix) { 170 | assertPath(path); 171 | if (path.length === 0) return path; 172 | if (typeof suffix !== "string") { 173 | throw new TypeError( 174 | `Suffix must be a string, received "${JSON.stringify(suffix)}"` 175 | ); 176 | } 177 | } 178 | 179 | // https://jsr.io/@std/path/1.0.6/_common/strip_trailing_separators.ts 180 | function stripTrailingSeparators(segment, isSep) { 181 | if (segment.length <= 1) { 182 | return segment; 183 | } 184 | let end = segment.length; 185 | for (let i = segment.length - 1; i > 0; i--) { 186 | if (isSep(segment.charCodeAt(i))) { 187 | end = i; 188 | } else { 189 | break; 190 | } 191 | } 192 | return segment.slice(0, end); 193 | } 194 | 195 | // https://jsr.io/@std/path/1.0.6/_common/constants.ts 196 | var CHAR_UPPERCASE_A = 65; 197 | var CHAR_LOWERCASE_A = 97; 198 | var CHAR_UPPERCASE_Z = 90; 199 | var CHAR_LOWERCASE_Z = 122; 200 | var CHAR_DOT = 46; 201 | var CHAR_FORWARD_SLASH = 47; 202 | var CHAR_BACKWARD_SLASH = 92; 203 | var CHAR_COLON = 58; 204 | 205 | // https://jsr.io/@std/path/1.0.6/posix/_util.ts 206 | function isPosixPathSeparator(code2) { 207 | return code2 === CHAR_FORWARD_SLASH; 208 | } 209 | 210 | // https://jsr.io/@std/path/1.0.6/posix/basename.ts 211 | function basename(path, suffix = "") { 212 | assertArgs(path, suffix); 213 | const lastSegment = lastPathSegment(path, isPosixPathSeparator); 214 | const strippedSegment = stripTrailingSeparators( 215 | lastSegment, 216 | isPosixPathSeparator 217 | ); 218 | return suffix ? stripSuffix(strippedSegment, suffix) : strippedSegment; 219 | } 220 | 221 | // https://jsr.io/@std/path/1.0.6/windows/_util.ts 222 | function isPosixPathSeparator2(code2) { 223 | return code2 === CHAR_FORWARD_SLASH; 224 | } 225 | function isPathSeparator(code2) { 226 | return code2 === CHAR_FORWARD_SLASH || code2 === CHAR_BACKWARD_SLASH; 227 | } 228 | function isWindowsDeviceRoot(code2) { 229 | return code2 >= CHAR_LOWERCASE_A && code2 <= CHAR_LOWERCASE_Z || code2 >= CHAR_UPPERCASE_A && code2 <= CHAR_UPPERCASE_Z; 230 | } 231 | 232 | // https://jsr.io/@std/path/1.0.6/windows/basename.ts 233 | function basename2(path, suffix = "") { 234 | assertArgs(path, suffix); 235 | let start = 0; 236 | if (path.length >= 2) { 237 | const drive = path.charCodeAt(0); 238 | if (isWindowsDeviceRoot(drive)) { 239 | if (path.charCodeAt(1) === CHAR_COLON) start = 2; 240 | } 241 | } 242 | const lastSegment = lastPathSegment(path, isPathSeparator, start); 243 | const strippedSegment = stripTrailingSeparators(lastSegment, isPathSeparator); 244 | return suffix ? stripSuffix(strippedSegment, suffix) : strippedSegment; 245 | } 246 | 247 | // https://jsr.io/@std/path/1.0.6/basename.ts 248 | function basename3(path, suffix = "") { 249 | return isWindows ? basename2(path, suffix) : basename(path, suffix); 250 | } 251 | 252 | // https://jsr.io/@std/path/1.0.6/_common/dirname.ts 253 | function assertArg(path) { 254 | assertPath(path); 255 | if (path.length === 0) return "."; 256 | } 257 | 258 | // https://jsr.io/@std/path/1.0.6/posix/dirname.ts 259 | function dirname(path) { 260 | assertArg(path); 261 | let end = -1; 262 | let matchedNonSeparator = false; 263 | for (let i = path.length - 1; i >= 1; --i) { 264 | if (isPosixPathSeparator(path.charCodeAt(i))) { 265 | if (matchedNonSeparator) { 266 | end = i; 267 | break; 268 | } 269 | } else { 270 | matchedNonSeparator = true; 271 | } 272 | } 273 | if (end === -1) { 274 | return isPosixPathSeparator(path.charCodeAt(0)) ? "/" : "."; 275 | } 276 | return stripTrailingSeparators( 277 | path.slice(0, end), 278 | isPosixPathSeparator 279 | ); 280 | } 281 | 282 | // https://jsr.io/@std/path/1.0.6/windows/dirname.ts 283 | function dirname2(path) { 284 | assertArg(path); 285 | const len = path.length; 286 | let rootEnd = -1; 287 | let end = -1; 288 | let matchedSlash = true; 289 | let offset = 0; 290 | const code2 = path.charCodeAt(0); 291 | if (len > 1) { 292 | if (isPathSeparator(code2)) { 293 | rootEnd = offset = 1; 294 | if (isPathSeparator(path.charCodeAt(1))) { 295 | let j = 2; 296 | let last = j; 297 | for (; j < len; ++j) { 298 | if (isPathSeparator(path.charCodeAt(j))) break; 299 | } 300 | if (j < len && j !== last) { 301 | last = j; 302 | for (; j < len; ++j) { 303 | if (!isPathSeparator(path.charCodeAt(j))) break; 304 | } 305 | if (j < len && j !== last) { 306 | last = j; 307 | for (; j < len; ++j) { 308 | if (isPathSeparator(path.charCodeAt(j))) break; 309 | } 310 | if (j === len) { 311 | return path; 312 | } 313 | if (j !== last) { 314 | rootEnd = offset = j + 1; 315 | } 316 | } 317 | } 318 | } 319 | } else if (isWindowsDeviceRoot(code2)) { 320 | if (path.charCodeAt(1) === CHAR_COLON) { 321 | rootEnd = offset = 2; 322 | if (len > 2) { 323 | if (isPathSeparator(path.charCodeAt(2))) rootEnd = offset = 3; 324 | } 325 | } 326 | } 327 | } else if (isPathSeparator(code2)) { 328 | return path; 329 | } 330 | for (let i = len - 1; i >= offset; --i) { 331 | if (isPathSeparator(path.charCodeAt(i))) { 332 | if (!matchedSlash) { 333 | end = i; 334 | break; 335 | } 336 | } else { 337 | matchedSlash = false; 338 | } 339 | } 340 | if (end === -1) { 341 | if (rootEnd === -1) return "."; 342 | else end = rootEnd; 343 | } 344 | return stripTrailingSeparators(path.slice(0, end), isPosixPathSeparator2); 345 | } 346 | 347 | // https://jsr.io/@std/path/1.0.6/dirname.ts 348 | function dirname3(path) { 349 | return isWindows ? dirname2(path) : dirname(path); 350 | } 351 | 352 | // https://jsr.io/@std/path/1.0.6/_common/normalize.ts 353 | function assertArg4(path) { 354 | assertPath(path); 355 | if (path.length === 0) return "."; 356 | } 357 | 358 | // https://jsr.io/@std/path/1.0.6/_common/normalize_string.ts 359 | function normalizeString(path, allowAboveRoot, separator, isPathSeparator2) { 360 | let res = ""; 361 | let lastSegmentLength = 0; 362 | let lastSlash = -1; 363 | let dots = 0; 364 | let code2; 365 | for (let i = 0; i <= path.length; ++i) { 366 | if (i < path.length) code2 = path.charCodeAt(i); 367 | else if (isPathSeparator2(code2)) break; 368 | else code2 = CHAR_FORWARD_SLASH; 369 | if (isPathSeparator2(code2)) { 370 | if (lastSlash === i - 1 || dots === 1) { 371 | } else if (lastSlash !== i - 1 && dots === 2) { 372 | if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== CHAR_DOT || res.charCodeAt(res.length - 2) !== CHAR_DOT) { 373 | if (res.length > 2) { 374 | const lastSlashIndex = res.lastIndexOf(separator); 375 | if (lastSlashIndex === -1) { 376 | res = ""; 377 | lastSegmentLength = 0; 378 | } else { 379 | res = res.slice(0, lastSlashIndex); 380 | lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); 381 | } 382 | lastSlash = i; 383 | dots = 0; 384 | continue; 385 | } else if (res.length === 2 || res.length === 1) { 386 | res = ""; 387 | lastSegmentLength = 0; 388 | lastSlash = i; 389 | dots = 0; 390 | continue; 391 | } 392 | } 393 | if (allowAboveRoot) { 394 | if (res.length > 0) res += `${separator}..`; 395 | else res = ".."; 396 | lastSegmentLength = 2; 397 | } 398 | } else { 399 | if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); 400 | else res = path.slice(lastSlash + 1, i); 401 | lastSegmentLength = i - lastSlash - 1; 402 | } 403 | lastSlash = i; 404 | dots = 0; 405 | } else if (code2 === CHAR_DOT && dots !== -1) { 406 | ++dots; 407 | } else { 408 | dots = -1; 409 | } 410 | } 411 | return res; 412 | } 413 | 414 | // https://jsr.io/@std/path/1.0.6/posix/normalize.ts 415 | function normalize(path) { 416 | assertArg4(path); 417 | const isAbsolute3 = isPosixPathSeparator(path.charCodeAt(0)); 418 | const trailingSeparator = isPosixPathSeparator( 419 | path.charCodeAt(path.length - 1) 420 | ); 421 | path = normalizeString(path, !isAbsolute3, "/", isPosixPathSeparator); 422 | if (path.length === 0 && !isAbsolute3) path = "."; 423 | if (path.length > 0 && trailingSeparator) path += "/"; 424 | if (isAbsolute3) return `/${path}`; 425 | return path; 426 | } 427 | 428 | // https://jsr.io/@std/path/1.0.6/posix/join.ts 429 | function join(...paths) { 430 | if (paths.length === 0) return "."; 431 | paths.forEach((path) => assertPath(path)); 432 | const joined = paths.filter((path) => path.length > 0).join("/"); 433 | return joined === "" ? "." : normalize(joined); 434 | } 435 | 436 | // https://jsr.io/@std/path/1.0.6/windows/normalize.ts 437 | function normalize2(path) { 438 | assertArg4(path); 439 | const len = path.length; 440 | let rootEnd = 0; 441 | let device; 442 | let isAbsolute3 = false; 443 | const code2 = path.charCodeAt(0); 444 | if (len > 1) { 445 | if (isPathSeparator(code2)) { 446 | isAbsolute3 = true; 447 | if (isPathSeparator(path.charCodeAt(1))) { 448 | let j = 2; 449 | let last = j; 450 | for (; j < len; ++j) { 451 | if (isPathSeparator(path.charCodeAt(j))) break; 452 | } 453 | if (j < len && j !== last) { 454 | const firstPart = path.slice(last, j); 455 | last = j; 456 | for (; j < len; ++j) { 457 | if (!isPathSeparator(path.charCodeAt(j))) break; 458 | } 459 | if (j < len && j !== last) { 460 | last = j; 461 | for (; j < len; ++j) { 462 | if (isPathSeparator(path.charCodeAt(j))) break; 463 | } 464 | if (j === len) { 465 | return `\\\\${firstPart}\\${path.slice(last)}\\`; 466 | } else if (j !== last) { 467 | device = `\\\\${firstPart}\\${path.slice(last, j)}`; 468 | rootEnd = j; 469 | } 470 | } 471 | } 472 | } else { 473 | rootEnd = 1; 474 | } 475 | } else if (isWindowsDeviceRoot(code2)) { 476 | if (path.charCodeAt(1) === CHAR_COLON) { 477 | device = path.slice(0, 2); 478 | rootEnd = 2; 479 | if (len > 2) { 480 | if (isPathSeparator(path.charCodeAt(2))) { 481 | isAbsolute3 = true; 482 | rootEnd = 3; 483 | } 484 | } 485 | } 486 | } 487 | } else if (isPathSeparator(code2)) { 488 | return "\\"; 489 | } 490 | let tail; 491 | if (rootEnd < len) { 492 | tail = normalizeString( 493 | path.slice(rootEnd), 494 | !isAbsolute3, 495 | "\\", 496 | isPathSeparator 497 | ); 498 | } else { 499 | tail = ""; 500 | } 501 | if (tail.length === 0 && !isAbsolute3) tail = "."; 502 | if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) { 503 | tail += "\\"; 504 | } 505 | if (device === void 0) { 506 | if (isAbsolute3) { 507 | if (tail.length > 0) return `\\${tail}`; 508 | else return "\\"; 509 | } 510 | return tail; 511 | } else if (isAbsolute3) { 512 | if (tail.length > 0) return `${device}\\${tail}`; 513 | else return `${device}\\`; 514 | } 515 | return device + tail; 516 | } 517 | 518 | // https://jsr.io/@std/path/1.0.6/windows/join.ts 519 | function join2(...paths) { 520 | paths.forEach((path) => assertPath(path)); 521 | paths = paths.filter((path) => path.length > 0); 522 | if (paths.length === 0) return "."; 523 | let needsReplace = true; 524 | let slashCount = 0; 525 | const firstPart = paths[0]; 526 | if (isPathSeparator(firstPart.charCodeAt(0))) { 527 | ++slashCount; 528 | const firstLen = firstPart.length; 529 | if (firstLen > 1) { 530 | if (isPathSeparator(firstPart.charCodeAt(1))) { 531 | ++slashCount; 532 | if (firstLen > 2) { 533 | if (isPathSeparator(firstPart.charCodeAt(2))) ++slashCount; 534 | else { 535 | needsReplace = false; 536 | } 537 | } 538 | } 539 | } 540 | } 541 | let joined = paths.join("\\"); 542 | if (needsReplace) { 543 | for (; slashCount < joined.length; ++slashCount) { 544 | if (!isPathSeparator(joined.charCodeAt(slashCount))) break; 545 | } 546 | if (slashCount >= 2) joined = `\\${joined.slice(slashCount)}`; 547 | } 548 | return normalize2(joined); 549 | } 550 | 551 | // https://jsr.io/@std/path/1.0.6/join.ts 552 | function join3(...paths) { 553 | return isWindows ? join2(...paths) : join(...paths); 554 | } 555 | 556 | // https://jsr.io/@std/fmt/1.0.2/colors.ts 557 | var { Deno: Deno2 } = globalThis; 558 | var noColor = typeof Deno2?.noColor === "boolean" ? Deno2.noColor : false; 559 | var enabled = !noColor; 560 | function code(open, close) { 561 | return { 562 | open: `\x1B[${open.join(";")}m`, 563 | close: `\x1B[${close}m`, 564 | regexp: new RegExp(`\\x1b\\[${close}m`, "g") 565 | }; 566 | } 567 | function run(str, code2) { 568 | return enabled ? `${code2.open}${str.replace(code2.regexp, code2.open)}${code2.close}` : str; 569 | } 570 | function bold(str) { 571 | return run(str, code([1], 22)); 572 | } 573 | function italic(str) { 574 | return run(str, code([3], 23)); 575 | } 576 | function blue(str) { 577 | return run(str, code([34], 39)); 578 | } 579 | var ANSI_PATTERN = new RegExp( 580 | [ 581 | "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", 582 | "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TXZcf-nq-uy=><~]))" 583 | ].join("|"), 584 | "g" 585 | ); 586 | function stripAnsiCode(string) { 587 | return string.replace(ANSI_PATTERN, ""); 588 | } 589 | 590 | // https://jsr.io/@nathanwhit/promptly/0.1.2/mod.ts 591 | var encoder = new TextEncoder(); 592 | var decoder = new TextDecoder(); 593 | async function* readKeys() { 594 | loop: while (true) { 595 | const buf = new Uint8Array(8); 596 | const byteCount = await Deno.stdin.read(buf); 597 | if (byteCount == null) { 598 | break; 599 | } else if (byteCount === 3) { 600 | if (buf[0] === 27 && buf[1] === 91) { 601 | switch (buf[2]) { 602 | // ESC[A -> cursor up 603 | case 65: 604 | yield 0 /* Up */; 605 | continue; 606 | // ESC[B -> cursor down 607 | case 66: 608 | yield 1 /* Down */; 609 | continue; 610 | // ESC[C -> cursor right 611 | case 67: 612 | yield 3 /* Right */; 613 | continue; 614 | // ESC[D -> cursor left 615 | case 68: 616 | yield 2 /* Left */; 617 | continue; 618 | } 619 | } 620 | } else if (byteCount === 1) { 621 | const c = buf[0]; 622 | switch (c) { 623 | case 3: 624 | break loop; 625 | case 13: 626 | yield 4 /* Enter */; 627 | continue; 628 | case 32: 629 | yield 5 /* Space */; 630 | continue; 631 | case 127: 632 | yield 6 /* Backspace */; 633 | continue; 634 | } 635 | } 636 | const text = stripAnsiCode(decoder.decode(buf.subarray(0, byteCount ?? 0))); 637 | if (text.length > 0) { 638 | yield text; 639 | } 640 | } 641 | } 642 | function writeAll(writer, buf) { 643 | let pos = 0; 644 | while (pos < buf.byteLength) { 645 | pos += writer.writeSync(buf.subarray(pos)); 646 | } 647 | } 648 | var charCodes = (...cs) => { 649 | const map = /* @__PURE__ */ Object.create(null); 650 | for (let i = 0; i < cs.length; i++) { 651 | const c = cs[i]; 652 | map[c.charAt(0)] = c.charCodeAt(0); 653 | } 654 | return map; 655 | }; 656 | function assertUnreachable(_x) { 657 | throw new Error("unreachable"); 658 | } 659 | var codes = charCodes("A", "B", "C", "D", "G", "0", "K"); 660 | function moveCursor(writer, dir, n) { 661 | const seq = [27, 91]; 662 | if (n != void 0) { 663 | seq.push(...encoder.encode(n.toString())); 664 | } 665 | switch (dir) { 666 | case 0 /* Up */: 667 | seq.push(codes.A); 668 | break; 669 | case 1 /* Down */: 670 | seq.push(codes.B); 671 | break; 672 | case 2 /* Left */: 673 | seq.push(codes.D); 674 | break; 675 | case 3 /* Right */: 676 | seq.push(codes.C); 677 | break; 678 | case 4 /* Column */: 679 | seq.push(codes.G); 680 | break; 681 | default: 682 | assertUnreachable(dir); 683 | } 684 | const buf = new Uint8Array(seq); 685 | writeAll(writer, buf); 686 | } 687 | function eraseToEnd(writer) { 688 | writeAll(writer, new Uint8Array([27, 91, codes[0], codes.K])); 689 | } 690 | function hideCursor(writer) { 691 | writeAll(writer, encoder.encode("\x1B[?25l")); 692 | } 693 | function showCursor(writer) { 694 | writeAll(writer, encoder.encode("\x1B[?25h")); 695 | } 696 | var lastPromise = Promise.resolve(); 697 | function ensureSingleSelection(action) { 698 | const currentLastPromise = lastPromise; 699 | const currentPromise = (async () => { 700 | try { 701 | await currentLastPromise; 702 | } catch { 703 | } 704 | hideCursor(Deno.stdout); 705 | try { 706 | Deno.stdin.setRaw(true); 707 | try { 708 | return await action(); 709 | } finally { 710 | Deno.stdin.setRaw(false); 711 | } 712 | } finally { 713 | showCursor(Deno.stdout); 714 | } 715 | })(); 716 | lastPromise = currentPromise; 717 | return currentPromise; 718 | } 719 | function clearRow(writer) { 720 | moveCursor(writer, 4 /* Column */); 721 | eraseToEnd(writer); 722 | } 723 | var row = 0; 724 | function writeLines(writer, lines) { 725 | while (row > 0) { 726 | clearRow(writer); 727 | moveCursor(writer, 0 /* Up */); 728 | row--; 729 | } 730 | clearRow(writer); 731 | for (const [i, line] of lines.entries()) { 732 | moveCursor(writer, 4 /* Column */); 733 | let suffix = ""; 734 | if (i < lines.length - 1) { 735 | suffix = "\n"; 736 | row++; 737 | } 738 | writer.writeSync( 739 | encoder.encode(line + suffix) 740 | ); 741 | } 742 | moveCursor(writer, 4 /* Column */); 743 | } 744 | function createSelection(options) { 745 | row = 0; 746 | return ensureSingleSelection(async () => { 747 | writeLines(Deno.stdout, options.render()); 748 | for await (const key of readKeys()) { 749 | const keyResult = options.onKey(key); 750 | if (keyResult != null) { 751 | writeLines(Deno.stdout, []); 752 | if (options.noClear) { 753 | writeLines(Deno.stdout, options.render()); 754 | console.log(); 755 | } 756 | return keyResult; 757 | } 758 | writeLines(Deno.stdout, options.render()); 759 | } 760 | writeLines(Deno.stdout, []); 761 | return void 0; 762 | }); 763 | } 764 | function resultOrExit(result) { 765 | if (result == null) { 766 | Deno.exit(120); 767 | } else { 768 | return result; 769 | } 770 | } 771 | async function multiSelect(options) { 772 | const result = await maybeMultiSelect(options); 773 | return resultOrExit(result); 774 | } 775 | function maybeMultiSelect(options) { 776 | const state = { 777 | title: options.message, 778 | activeIndex: 0, 779 | items: options.options.map((option) => { 780 | if (typeof option === "string") { 781 | option = { 782 | text: option 783 | }; 784 | } 785 | return { 786 | selected: option.selected ?? false, 787 | text: option.text 788 | }; 789 | }), 790 | hasCompleted: false 791 | }; 792 | const { 793 | selected = "[x]", 794 | unselected = "[ ]", 795 | pointer = ">", 796 | listBullet = "-", 797 | messageStyle = (s) => bold(blue(s)) 798 | } = options.styling ?? {}; 799 | const style = { 800 | selected, 801 | unselected, 802 | pointer, 803 | listBullet, 804 | messageStyle 805 | }; 806 | return createSelection({ 807 | message: options.message, 808 | noClear: options.noClear, 809 | render: () => renderMultiSelect(state, style), 810 | onKey: (key) => { 811 | switch (key) { 812 | case 0 /* Up */: 813 | case "k": 814 | if (state.activeIndex === 0) { 815 | state.activeIndex = state.items.length - 1; 816 | } else { 817 | state.activeIndex--; 818 | } 819 | break; 820 | case 1 /* Down */: 821 | case "j": 822 | state.activeIndex = (state.activeIndex + 1) % state.items.length; 823 | break; 824 | case 5 /* Space */: { 825 | const item = state.items[state.activeIndex]; 826 | item.selected = !item.selected; 827 | break; 828 | } 829 | case 4 /* Enter */: 830 | state.hasCompleted = true; 831 | return state.items.map((value, index) => [value, index]).filter(([value]) => value.selected).map(([, index]) => index); 832 | } 833 | } 834 | }); 835 | } 836 | function renderMultiSelect(state, style) { 837 | const items = []; 838 | items.push(style.messageStyle(state.title)); 839 | if (state.hasCompleted) { 840 | if (state.items.some((i) => i.selected)) { 841 | for (const item of state.items) { 842 | if (item.selected) { 843 | items.push( 844 | `${" ".repeat( 845 | style.pointer.length + style.selected.length - style.listBullet.length - 2 846 | )}${style.listBullet} ${item.text}` 847 | ); 848 | } 849 | } 850 | } else { 851 | items.push(italic(" ")); 852 | } 853 | } else { 854 | for (const [i, item] of state.items.entries()) { 855 | const prefix = i === state.activeIndex ? `${style.pointer} ` : `${" ".repeat(style.pointer.length + 1)}`; 856 | items.push( 857 | `${prefix}${item.selected ? style.selected : style.unselected} ${item.text}` 858 | ); 859 | } 860 | } 861 | return items; 862 | } 863 | async function confirm(optsOrMessage, options) { 864 | const result = await maybeConfirm(optsOrMessage, options); 865 | return resultOrExit(result); 866 | } 867 | function maybeConfirm(optsOrMessage, options) { 868 | const opts = typeof optsOrMessage === "string" ? { message: optsOrMessage, ...options } : optsOrMessage; 869 | return innerConfirm(opts); 870 | } 871 | function innerConfirm(options) { 872 | const { 873 | messageStyle = (s) => bold(blue(s)) 874 | } = options.styling ?? {}; 875 | const style = { 876 | messageStyle 877 | }; 878 | const state = { 879 | title: options.message, 880 | default: options.default, 881 | inputText: "", 882 | hasCompleted: false 883 | }; 884 | return createSelection({ 885 | message: options.message, 886 | noClear: options.noClear, 887 | render: () => renderConfirm(state, style), 888 | onKey: (key) => { 889 | switch (key) { 890 | case "Y": 891 | case "y": 892 | state.inputText = "Y"; 893 | break; 894 | case "N": 895 | case "n": 896 | state.inputText = "N"; 897 | break; 898 | case 6 /* Backspace */: 899 | state.inputText = ""; 900 | break; 901 | case 4 /* Enter */: 902 | if (state.inputText.length === 0) { 903 | if (state.default == null) { 904 | return void 0; 905 | } 906 | state.inputText = state.default ? "Y" : "N"; 907 | } 908 | state.hasCompleted = true; 909 | return state.inputText === "Y" ? true : state.inputText === "N" ? false : state.default; 910 | } 911 | } 912 | }); 913 | } 914 | function renderConfirm(state, style) { 915 | return [ 916 | style.messageStyle(state.title) + " " + (state.hasCompleted ? "" : state.default == null ? "(Y/N) " : state.default ? "(Y/n) " : "(y/N) ") + state.inputText + (state.hasCompleted ? "" : "\u2588") 917 | ]; 918 | } 919 | 920 | // https://jsr.io/@std/cli/1.0.6/parse_args.ts 921 | var FLAG_REGEXP = /^(?:-(?:(?-)(?no-)?)?)(?.+?)(?:=(?.+?))?$/s; 922 | var LETTER_REGEXP = /[A-Za-z]/; 923 | var NUMBER_REGEXP = /-?\d+(\.\d*)?(e-?\d+)?$/; 924 | var HYPHEN_REGEXP = /^(-|--)[^-]/; 925 | var VALUE_REGEXP = /=(?.+)/; 926 | var FLAG_NAME_REGEXP = /^--[^=]+$/; 927 | var SPECIAL_CHAR_REGEXP = /\W/; 928 | var NON_WHITESPACE_REGEXP = /\S/; 929 | function isNumber(string) { 930 | return NON_WHITESPACE_REGEXP.test(string) && Number.isFinite(Number(string)); 931 | } 932 | function setNested(object, keys, value, collect = false) { 933 | keys = [...keys]; 934 | const key = keys.pop(); 935 | keys.forEach((key2) => object = object[key2] ??= {}); 936 | if (collect) { 937 | const v = object[key]; 938 | if (Array.isArray(v)) { 939 | v.push(value); 940 | return; 941 | } 942 | value = v ? [v, value] : [value]; 943 | } 944 | object[key] = value; 945 | } 946 | function hasNested(object, keys) { 947 | for (const key of keys) { 948 | const value = object[key]; 949 | if (!Object.hasOwn(object, key)) return false; 950 | object = value; 951 | } 952 | return true; 953 | } 954 | function aliasIsBoolean(aliasMap, booleanSet, key) { 955 | const set = aliasMap.get(key); 956 | if (set === void 0) return false; 957 | for (const alias of set) if (booleanSet.has(alias)) return true; 958 | return false; 959 | } 960 | function isBooleanString(value) { 961 | return value === "true" || value === "false"; 962 | } 963 | function parseBooleanString(value) { 964 | return value !== "false"; 965 | } 966 | function parseArgs(args, options) { 967 | const { 968 | "--": doubleDash = false, 969 | alias = {}, 970 | boolean = false, 971 | default: defaults = {}, 972 | stopEarly = false, 973 | string = [], 974 | collect = [], 975 | negatable = [], 976 | unknown: unknownFn = (i) => i 977 | } = options ?? {}; 978 | const aliasMap = /* @__PURE__ */ new Map(); 979 | const booleanSet = /* @__PURE__ */ new Set(); 980 | const stringSet = /* @__PURE__ */ new Set(); 981 | const collectSet = /* @__PURE__ */ new Set(); 982 | const negatableSet = /* @__PURE__ */ new Set(); 983 | let allBools = false; 984 | if (alias) { 985 | for (const [key, value] of Object.entries(alias)) { 986 | if (value === void 0) { 987 | throw new TypeError("Alias value must be defined"); 988 | } 989 | const aliases = Array.isArray(value) ? value : [value]; 990 | aliasMap.set(key, new Set(aliases)); 991 | aliases.forEach( 992 | (alias2) => aliasMap.set( 993 | alias2, 994 | /* @__PURE__ */ new Set([key, ...aliases.filter((it) => it !== alias2)]) 995 | ) 996 | ); 997 | } 998 | } 999 | if (boolean) { 1000 | if (typeof boolean === "boolean") { 1001 | allBools = boolean; 1002 | } else { 1003 | const booleanArgs = Array.isArray(boolean) ? boolean : [boolean]; 1004 | for (const key of booleanArgs.filter(Boolean)) { 1005 | booleanSet.add(key); 1006 | aliasMap.get(key)?.forEach((al) => { 1007 | booleanSet.add(al); 1008 | }); 1009 | } 1010 | } 1011 | } 1012 | if (string) { 1013 | const stringArgs = Array.isArray(string) ? string : [string]; 1014 | for (const key of stringArgs.filter(Boolean)) { 1015 | stringSet.add(key); 1016 | aliasMap.get(key)?.forEach((al) => stringSet.add(al)); 1017 | } 1018 | } 1019 | if (collect) { 1020 | const collectArgs = Array.isArray(collect) ? collect : [collect]; 1021 | for (const key of collectArgs.filter(Boolean)) { 1022 | collectSet.add(key); 1023 | aliasMap.get(key)?.forEach((al) => collectSet.add(al)); 1024 | } 1025 | } 1026 | if (negatable) { 1027 | const negatableArgs = Array.isArray(negatable) ? negatable : [negatable]; 1028 | for (const key of negatableArgs.filter(Boolean)) { 1029 | negatableSet.add(key); 1030 | aliasMap.get(key)?.forEach((alias2) => negatableSet.add(alias2)); 1031 | } 1032 | } 1033 | const argv = { _: [] }; 1034 | function setArgument(key, value, arg, collect2) { 1035 | if (!booleanSet.has(key) && !stringSet.has(key) && !aliasMap.has(key) && !(allBools && FLAG_NAME_REGEXP.test(arg)) && unknownFn?.(arg, key, value) === false) { 1036 | return; 1037 | } 1038 | if (typeof value === "string" && !stringSet.has(key)) { 1039 | value = isNumber(value) ? Number(value) : value; 1040 | } 1041 | const collectable = collect2 && collectSet.has(key); 1042 | setNested(argv, key.split("."), value, collectable); 1043 | aliasMap.get(key)?.forEach((key2) => { 1044 | setNested(argv, key2.split("."), value, collectable); 1045 | }); 1046 | } 1047 | let notFlags = []; 1048 | const index = args.indexOf("--"); 1049 | if (index !== -1) { 1050 | notFlags = args.slice(index + 1); 1051 | args = args.slice(0, index); 1052 | } 1053 | argsLoop: 1054 | for (let i = 0; i < args.length; i++) { 1055 | const arg = args[i]; 1056 | const groups = arg.match(FLAG_REGEXP)?.groups; 1057 | if (groups) { 1058 | const { doubleDash: doubleDash2, negated } = groups; 1059 | let key = groups.key; 1060 | let value = groups.value; 1061 | if (doubleDash2) { 1062 | if (value) { 1063 | if (booleanSet.has(key)) value = parseBooleanString(value); 1064 | setArgument(key, value, arg, true); 1065 | continue; 1066 | } 1067 | if (negated) { 1068 | if (negatableSet.has(key)) { 1069 | setArgument(key, false, arg, false); 1070 | continue; 1071 | } 1072 | key = `no-${key}`; 1073 | } 1074 | const next = args[i + 1]; 1075 | if (next) { 1076 | if (!booleanSet.has(key) && !allBools && !next.startsWith("-") && (!aliasMap.has(key) || !aliasIsBoolean(aliasMap, booleanSet, key))) { 1077 | value = next; 1078 | i++; 1079 | setArgument(key, value, arg, true); 1080 | continue; 1081 | } 1082 | if (isBooleanString(next)) { 1083 | value = parseBooleanString(next); 1084 | i++; 1085 | setArgument(key, value, arg, true); 1086 | continue; 1087 | } 1088 | } 1089 | value = stringSet.has(key) ? "" : true; 1090 | setArgument(key, value, arg, true); 1091 | continue; 1092 | } 1093 | const letters = arg.slice(1, -1).split(""); 1094 | for (const [j, letter] of letters.entries()) { 1095 | const next = arg.slice(j + 2); 1096 | if (next === "-") { 1097 | setArgument(letter, next, arg, true); 1098 | continue; 1099 | } 1100 | if (LETTER_REGEXP.test(letter)) { 1101 | const groups2 = VALUE_REGEXP.exec(next)?.groups; 1102 | if (groups2) { 1103 | setArgument(letter, groups2.value, arg, true); 1104 | continue argsLoop; 1105 | } 1106 | if (NUMBER_REGEXP.test(next)) { 1107 | setArgument(letter, next, arg, true); 1108 | continue argsLoop; 1109 | } 1110 | } 1111 | if (letters[j + 1]?.match(SPECIAL_CHAR_REGEXP)) { 1112 | setArgument(letter, arg.slice(j + 2), arg, true); 1113 | continue argsLoop; 1114 | } 1115 | setArgument(letter, stringSet.has(letter) ? "" : true, arg, true); 1116 | } 1117 | key = arg.slice(-1); 1118 | if (key === "-") continue; 1119 | const nextArg = args[i + 1]; 1120 | if (nextArg) { 1121 | if (!HYPHEN_REGEXP.test(nextArg) && !booleanSet.has(key) && (!aliasMap.has(key) || !aliasIsBoolean(aliasMap, booleanSet, key))) { 1122 | setArgument(key, nextArg, arg, true); 1123 | i++; 1124 | continue; 1125 | } 1126 | if (isBooleanString(nextArg)) { 1127 | const value2 = parseBooleanString(nextArg); 1128 | setArgument(key, value2, arg, true); 1129 | i++; 1130 | continue; 1131 | } 1132 | } 1133 | setArgument(key, stringSet.has(key) ? "" : true, arg, true); 1134 | continue; 1135 | } 1136 | if (unknownFn?.(arg) !== false) { 1137 | argv._.push( 1138 | stringSet.has("_") || !isNumber(arg) ? arg : Number(arg) 1139 | ); 1140 | } 1141 | if (stopEarly) { 1142 | argv._.push(...args.slice(i + 1)); 1143 | break; 1144 | } 1145 | } 1146 | for (const [key, value] of Object.entries(defaults)) { 1147 | const keys = key.split("."); 1148 | if (!hasNested(argv, keys)) { 1149 | setNested(argv, keys, value); 1150 | aliasMap.get(key)?.forEach( 1151 | (key2) => setNested(argv, key2.split("."), value) 1152 | ); 1153 | } 1154 | } 1155 | for (const key of booleanSet.keys()) { 1156 | const keys = key.split("."); 1157 | if (!hasNested(argv, keys)) { 1158 | const value = collectSet.has(key) ? [] : false; 1159 | setNested(argv, keys, value); 1160 | } 1161 | } 1162 | for (const key of stringSet.keys()) { 1163 | const keys = key.split("."); 1164 | if (!hasNested(argv, keys) && collectSet.has(key)) { 1165 | setNested(argv, keys, []); 1166 | } 1167 | } 1168 | if (doubleDash) { 1169 | argv["--"] = notFlags; 1170 | } else { 1171 | argv._.push(...notFlags); 1172 | } 1173 | return argv; 1174 | } 1175 | 1176 | // src/util.ts 1177 | var { isExistingDir, mkdir } = environment; 1178 | function withContext(ctx, error) { 1179 | return new Error(ctx, { cause: error }); 1180 | } 1181 | async function filterAsync(arr, pred) { 1182 | const filtered = await Promise.all(arr.map((v) => pred(v))); 1183 | return arr.filter((_, i) => filtered[i]); 1184 | } 1185 | function withEnvVar(name, f) { 1186 | const value = environment.getEnv(name); 1187 | return f(value); 1188 | } 1189 | function shellEnvContains(s) { 1190 | return withEnvVar("SHELL", (sh) => sh !== void 0 && sh.includes(s)); 1191 | } 1192 | function warn(s) { 1193 | console.error(`%cwarning%c: ${s}`, "color: yellow", "color: inherit"); 1194 | } 1195 | function info(s) { 1196 | console.error(`%cinfo%c: ${s}`, "color: green", "color: inherit"); 1197 | } 1198 | async function ensureExists(dirPath) { 1199 | if (!await isExistingDir(dirPath)) { 1200 | await mkdir(dirPath, { 1201 | recursive: true 1202 | }); 1203 | } 1204 | } 1205 | function ensureEndsWith(s, suffix) { 1206 | if (!s.endsWith(suffix)) { 1207 | return s + suffix; 1208 | } 1209 | return s; 1210 | } 1211 | function ensureStartsWith(s, prefix) { 1212 | if (!s.startsWith(prefix)) { 1213 | return prefix + s; 1214 | } 1215 | return s; 1216 | } 1217 | 1218 | // src/shell.ts 1219 | var { 1220 | isExistingFile, 1221 | writeTextFile, 1222 | homeDir, 1223 | findCmd, 1224 | runCmd, 1225 | getEnv, 1226 | pathExists 1227 | } = environment; 1228 | var ShellScript = class { 1229 | constructor(name, contents) { 1230 | this.name = name; 1231 | this.contents = contents; 1232 | } 1233 | equals(other) { 1234 | return this.name === other.name && this.contents === other.contents; 1235 | } 1236 | async write(denoInstallDir) { 1237 | const envFilePath = join3(denoInstallDir, this.name); 1238 | try { 1239 | await writeTextFile(envFilePath, this.contents); 1240 | return true; 1241 | } catch (error) { 1242 | if (error instanceof Deno.errors.PermissionDenied) { 1243 | return false; 1244 | } 1245 | throw withContext( 1246 | `Failed to write ${this.name} file to ${envFilePath}`, 1247 | error 1248 | ); 1249 | } 1250 | } 1251 | }; 1252 | var shEnvScript = (installDir) => new ShellScript( 1253 | "env", 1254 | `#!/bin/sh 1255 | # deno shell setup; adapted from rustup 1256 | # affix colons on either side of $PATH to simplify matching 1257 | case ":\${PATH}:" in 1258 | *:"${installDir}/bin":*) 1259 | ;; 1260 | *) 1261 | # Prepending path in case a system-installed deno executable needs to be overridden 1262 | export PATH="${installDir}/bin:$PATH" 1263 | ;; 1264 | esac 1265 | ` 1266 | ); 1267 | var shSourceString = (installDir) => { 1268 | return `. "${installDir}/env"`; 1269 | }; 1270 | var Posix = class { 1271 | name = "sh"; 1272 | supportsCompletion = false; 1273 | exists() { 1274 | return true; 1275 | } 1276 | rcfiles() { 1277 | return [join3(homeDir, ".profile")]; 1278 | } 1279 | rcsToUpdate() { 1280 | return this.rcfiles(); 1281 | } 1282 | }; 1283 | var Bash = class { 1284 | name = "bash"; 1285 | get supportsCompletion() { 1286 | if (Deno.build.os === "darwin") { 1287 | return "not recommended on macOS"; 1288 | } 1289 | return true; 1290 | } 1291 | async exists() { 1292 | return (await this.rcsToUpdate()).length > 0; 1293 | } 1294 | rcfiles() { 1295 | return [".bash_profile", ".bash_login", ".bashrc"].map((rc) => join3(homeDir, rc)); 1296 | } 1297 | rcsToUpdate() { 1298 | return filterAsync(this.rcfiles(), isExistingFile); 1299 | } 1300 | completionsFilePath() { 1301 | const USER = Deno.env.get("USER"); 1302 | if (USER === "root") { 1303 | return "/usr/local/etc/bash_completion.d/deno.bash"; 1304 | } 1305 | return join3(homeDir, ".local/share/bash-completion/completions/deno.bash"); 1306 | } 1307 | completionsSourceString() { 1308 | return `source ${this.completionsFilePath()}`; 1309 | } 1310 | }; 1311 | var Zsh = class { 1312 | name = "zsh"; 1313 | supportsCompletion = true; 1314 | async exists() { 1315 | if (shellEnvContains("zsh") || await findCmd("zsh")) { 1316 | return true; 1317 | } 1318 | return false; 1319 | } 1320 | async getZshDotDir() { 1321 | let zshDotDir; 1322 | if (shellEnvContains("zsh")) { 1323 | zshDotDir = getEnv("ZDOTDIR"); 1324 | } else { 1325 | const output = await runCmd("zsh", [ 1326 | "-c", 1327 | "echo -n $ZDOTDIR" 1328 | ]); 1329 | const stdout = new TextDecoder().decode(output.stdout).trim(); 1330 | zshDotDir = stdout.length > 0 ? stdout : void 0; 1331 | } 1332 | return zshDotDir; 1333 | } 1334 | async rcfiles() { 1335 | const zshDotDir = await this.getZshDotDir(); 1336 | return [zshDotDir, homeDir].map( 1337 | (dir) => dir ? join3(dir, ".zshrc") : void 0 1338 | ).filter((dir) => dir !== void 0); 1339 | } 1340 | async rcsToUpdate() { 1341 | let out = await filterAsync( 1342 | await this.rcfiles(), 1343 | isExistingFile 1344 | ); 1345 | if (out.length === 0) { 1346 | out = await this.rcfiles(); 1347 | } 1348 | return out; 1349 | } 1350 | async completionsFilePath() { 1351 | let zshDotDir = await this.getZshDotDir(); 1352 | if (!zshDotDir) { 1353 | zshDotDir = join3(homeDir, ".zsh"); 1354 | } 1355 | return join3(zshDotDir, "completions", "_deno.zsh"); 1356 | } 1357 | async completionsSourceString() { 1358 | const filePath = await this.completionsFilePath(); 1359 | const completionDir = dirname3(filePath); 1360 | const fpathSetup = `# Add deno completions to search path 1361 | if [[ ":$FPATH:" != *":${completionDir}:"* ]]; then export FPATH="${completionDir}:$FPATH"; fi`; 1362 | const zshDotDir = await this.getZshDotDir() ?? homeDir; 1363 | let append; 1364 | if ((await filterAsync( 1365 | [".zcompdump", ".oh_my_zsh", ".zprezto"], 1366 | (f) => pathExists(join3(zshDotDir, f)) 1367 | )).length == 0) { 1368 | append = "# Initialize zsh completions (added by deno install script)\nautoload -Uz compinit\ncompinit"; 1369 | } 1370 | return { 1371 | prepend: fpathSetup, 1372 | append 1373 | }; 1374 | } 1375 | }; 1376 | var Fish = class { 1377 | name = "fish"; 1378 | supportsCompletion = true; 1379 | async exists() { 1380 | if (shellEnvContains("fish") || await findCmd("fish")) { 1381 | return true; 1382 | } 1383 | return false; 1384 | } 1385 | fishConfigDir() { 1386 | const first = withEnvVar("XDG_CONFIG_HOME", (p) => { 1387 | if (!p) return; 1388 | return join3(p, "fish"); 1389 | }); 1390 | return first ?? join3(homeDir, ".config", "fish"); 1391 | } 1392 | rcfiles() { 1393 | const conf = "conf.d/deno.fish"; 1394 | return [join3(this.fishConfigDir(), conf)]; 1395 | } 1396 | rcsToUpdate() { 1397 | return this.rcfiles(); 1398 | } 1399 | envScript(installDir) { 1400 | const fishEnv = ` 1401 | # deno shell setup 1402 | if not contains "${installDir}/bin" $PATH 1403 | # prepend to path to take precedence over potential package manager deno installations 1404 | set -x PATH "${installDir}/bin" $PATH 1405 | end 1406 | `; 1407 | return new ShellScript("env.fish", fishEnv); 1408 | } 1409 | sourceString(installDir) { 1410 | return `source "${installDir}/env.fish"`; 1411 | } 1412 | completionsFilePath() { 1413 | return join3(this.fishConfigDir(), "completions", "deno.fish"); 1414 | } 1415 | // no further config needed for completions 1416 | }; 1417 | 1418 | // src/main.ts 1419 | var { 1420 | readTextFile, 1421 | runCmd: runCmd2, 1422 | writeTextFile: writeTextFile2 1423 | } = environment; 1424 | async function writeCompletionFiles(availableShells) { 1425 | const written = /* @__PURE__ */ new Set(); 1426 | const results = []; 1427 | const decoder2 = new TextDecoder(); 1428 | for (const shell of availableShells) { 1429 | if (!shell.supportsCompletion) { 1430 | results.push(null); 1431 | continue; 1432 | } 1433 | try { 1434 | const completionFilePath = await shell.completionsFilePath?.(); 1435 | if (!completionFilePath) { 1436 | results.push(null); 1437 | continue; 1438 | } 1439 | await ensureExists(dirname3(completionFilePath)); 1440 | const output = await runCmd2(Deno.execPath(), ["completions", shell.name]); 1441 | if (!output.success) { 1442 | throw new Error( 1443 | `deno completions subcommand failed, stderr was: ${decoder2.decode(output.stderr)}` 1444 | ); 1445 | } 1446 | const completionFileContents = decoder2.decode(output.stdout); 1447 | if (!completionFileContents) { 1448 | warn(`Completions were empty, skipping ${shell.name}`); 1449 | results.push("fail"); 1450 | continue; 1451 | } 1452 | let currentContents = null; 1453 | try { 1454 | currentContents = await readTextFile(completionFilePath); 1455 | } catch (error) { 1456 | if (!(error instanceof Deno.errors.NotFound)) { 1457 | throw error; 1458 | } else { 1459 | } 1460 | } 1461 | if (currentContents !== completionFileContents) { 1462 | if (currentContents !== null) { 1463 | warn( 1464 | `an existing completion file for deno already exists at ${completionFilePath}, but is out of date. overwriting with new contents` 1465 | ); 1466 | } 1467 | await writeTextFile2(completionFilePath, completionFileContents); 1468 | } 1469 | results.push("success"); 1470 | written.add(completionFilePath); 1471 | } catch (error) { 1472 | warn(`Failed to install completions for ${shell.name}: ${error}`); 1473 | results.push("fail"); 1474 | continue; 1475 | } 1476 | } 1477 | return results; 1478 | } 1479 | var Backups = class { 1480 | constructor(backupDir) { 1481 | this.backupDir = backupDir; 1482 | } 1483 | backedUp = /* @__PURE__ */ new Set(); 1484 | async add(path, contents) { 1485 | if (this.backedUp.has(path)) { 1486 | return; 1487 | } 1488 | const dest = join3(this.backupDir, basename3(path)) + `.bak`; 1489 | info( 1490 | `backing '${path}' up to '${dest}'` 1491 | ); 1492 | await Deno.writeTextFile(dest, contents); 1493 | this.backedUp.add(path); 1494 | } 1495 | }; 1496 | async function writeCompletionRcCommands(availableShells, backups) { 1497 | for (const shell of availableShells) { 1498 | if (!shell.supportsCompletion) continue; 1499 | const rcCmd = await shell.completionsSourceString?.(); 1500 | if (!rcCmd) continue; 1501 | for (const rc of await shell.rcsToUpdate()) { 1502 | await updateRcFile(rc, rcCmd, backups); 1503 | } 1504 | } 1505 | } 1506 | async function writeEnvFiles(availableShells, installDir) { 1507 | const written = new Array(); 1508 | let i = 0; 1509 | while (i < availableShells.length) { 1510 | const shell = availableShells[i]; 1511 | const script = (shell.envScript ?? shEnvScript)(installDir); 1512 | if (!written.some((s) => s.equals(script))) { 1513 | if (await script.write(installDir)) { 1514 | written.push(script); 1515 | } else { 1516 | continue; 1517 | } 1518 | } 1519 | i++; 1520 | } 1521 | } 1522 | async function updateRcFile(rc, command, backups) { 1523 | let prepend = ""; 1524 | let append = ""; 1525 | if (typeof command === "string") { 1526 | append = command; 1527 | } else { 1528 | prepend = command.prepend ?? ""; 1529 | append = command.append ?? ""; 1530 | } 1531 | if (!prepend && !append) { 1532 | return false; 1533 | } 1534 | let contents; 1535 | try { 1536 | contents = await readTextFile(rc); 1537 | if (prepend) { 1538 | if (contents.includes(prepend)) { 1539 | prepend = ""; 1540 | } else { 1541 | prepend = ensureEndsWith(prepend, "\n"); 1542 | } 1543 | } 1544 | if (append) { 1545 | if (contents.includes(append)) { 1546 | append = ""; 1547 | } else if (!contents.endsWith("\n")) { 1548 | append = ensureStartsWith(append, "\n"); 1549 | } 1550 | } 1551 | } catch (_error) { 1552 | prepend = prepend ? ensureEndsWith(prepend, "\n") : prepend; 1553 | } 1554 | if (!prepend && !append) { 1555 | return false; 1556 | } 1557 | if (contents !== void 0) { 1558 | await backups.add(rc, contents); 1559 | } 1560 | await ensureExists(dirname3(rc)); 1561 | try { 1562 | await writeTextFile2(rc, prepend + (contents ?? "") + append, { 1563 | create: true 1564 | }); 1565 | return true; 1566 | } catch (error) { 1567 | if (error instanceof Deno.errors.PermissionDenied || // deno-lint-ignore no-explicit-any 1568 | error instanceof Deno.errors.NotCapable) { 1569 | return false; 1570 | } 1571 | throw withContext(`Failed to update shell rc file: ${rc}`, error); 1572 | } 1573 | } 1574 | async function addToPath(availableShells, installDir, backups) { 1575 | for (const shell of availableShells) { 1576 | const sourceCmd = await (shell.sourceString ?? shSourceString)(installDir); 1577 | for (const rc of await shell.rcsToUpdate()) { 1578 | await updateRcFile(rc, sourceCmd, backups); 1579 | } 1580 | } 1581 | } 1582 | var shells = [ 1583 | new Posix(), 1584 | new Bash(), 1585 | new Zsh(), 1586 | new Fish() 1587 | ]; 1588 | async function getAvailableShells() { 1589 | const present = []; 1590 | for (const shell of shells) { 1591 | try { 1592 | if (await shell.exists()) { 1593 | present.push(shell); 1594 | } 1595 | } catch (_e) { 1596 | continue; 1597 | } 1598 | } 1599 | return present; 1600 | } 1601 | async function setupShells(installDir, backupDir, opts) { 1602 | const { 1603 | skipPrompts, 1604 | noModifyPath 1605 | } = opts; 1606 | const availableShells = await getAvailableShells(); 1607 | await writeEnvFiles(availableShells, installDir); 1608 | const backups = new Backups(backupDir); 1609 | if (skipPrompts && !noModifyPath || !skipPrompts && await confirm(`Edit shell configs to add deno to the PATH?`, { 1610 | default: true 1611 | })) { 1612 | await ensureExists(backupDir); 1613 | await addToPath(availableShells, installDir, backups); 1614 | console.log( 1615 | "\nDeno was added to the PATH.\nYou may need to restart your shell for it to become available.\n" 1616 | ); 1617 | } 1618 | const shellsWithCompletion = availableShells.filter( 1619 | (s) => s.supportsCompletion !== false 1620 | ); 1621 | const selected = skipPrompts ? [] : await multiSelect( 1622 | { 1623 | message: `Set up completions?`, 1624 | options: shellsWithCompletion.map((s) => { 1625 | const maybeNotes = typeof s.supportsCompletion === "string" ? ` (${s.supportsCompletion})` : ""; 1626 | return s.name + maybeNotes; 1627 | }) 1628 | } 1629 | ); 1630 | const completionsToSetup = selected.map((idx) => shellsWithCompletion[idx]); 1631 | if (completionsToSetup.length > 0) { 1632 | await ensureExists(backupDir); 1633 | const results = await writeCompletionFiles(completionsToSetup); 1634 | await writeCompletionRcCommands( 1635 | completionsToSetup.filter((_s, i) => results[i] !== "fail"), 1636 | backups 1637 | ); 1638 | } 1639 | } 1640 | function printHelp() { 1641 | console.log(` 1642 | 1643 | Setup script for installing deno 1644 | 1645 | Options: 1646 | -y, --yes 1647 | Skip interactive prompts and accept defaults 1648 | --no-modify-path 1649 | Don't add deno to the PATH environment variable 1650 | -h, --help 1651 | Print help 1652 | `); 1653 | } 1654 | async function main() { 1655 | if (Deno.args.length === 0) { 1656 | throw new Error( 1657 | "Expected the deno install directory as the first argument" 1658 | ); 1659 | } 1660 | const args = parseArgs(Deno.args.slice(1), { 1661 | boolean: ["yes", "no-modify-path", "help"], 1662 | alias: { 1663 | "yes": "y", 1664 | "help": "h" 1665 | }, 1666 | default: { 1667 | yes: false, 1668 | "no-modify-path": false 1669 | }, 1670 | unknown: (arg) => { 1671 | if (arg.startsWith("-")) { 1672 | printHelp(); 1673 | console.error(`Unknown flag ${arg}. Shell will not be configured`); 1674 | Deno.exit(1); 1675 | } 1676 | return false; 1677 | } 1678 | }); 1679 | if (args.help) { 1680 | printHelp(); 1681 | return; 1682 | } 1683 | if (Deno.build.os === "windows" || !args.yes && !(Deno.stdin.isTerminal() && Deno.stdout.isTerminal())) { 1684 | return; 1685 | } 1686 | const installDir = Deno.args[0].trim(); 1687 | const backupDir = join3(installDir, ".shellRcBackups"); 1688 | try { 1689 | await setupShells(installDir, backupDir, { 1690 | skipPrompts: args.yes, 1691 | noModifyPath: args["no-modify-path"] 1692 | }); 1693 | } catch (_e) { 1694 | warn( 1695 | `Failed to configure your shell environments, you may need to manually add deno to your PATH environment variable. 1696 | 1697 | Manually add the directory to your $HOME/.bashrc (or similar)": 1698 | export DENO_INSTALL="${installDir}" 1699 | export PATH="${installDir}/bin:$PATH" 1700 | ` 1701 | ); 1702 | } 1703 | } 1704 | if (import.meta.main) { 1705 | await main(); 1706 | } 1707 | -------------------------------------------------------------------------------- /shell-setup/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deno/installer-shell-setup", 3 | "version": "0.0.0", 4 | "exports": { 5 | ".": "./src/main.ts", 6 | "./bundled": "./bundled.esm.js" 7 | }, 8 | "tasks": { 9 | "bundle": "deno run -A ./bundle.ts" 10 | }, 11 | "license": "../LICENSE", 12 | "imports": { 13 | "@david/which": "jsr:@david/which@^0.4.1", 14 | "@nathanwhit/promptly": "jsr:@nathanwhit/promptly@^0.1.2", 15 | "@std/cli": "jsr:@std/cli@^1.0.6", 16 | "@std/path": "jsr:@std/path@^1.0.4", 17 | "@types/node": "npm:@types/node@*" 18 | }, 19 | "publish": { 20 | "exclude": ["./bundle.ts"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shell-setup/deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@david/dax@*": "0.42.0", 5 | "jsr:@david/path@0.2": "0.2.0", 6 | "jsr:@david/which@~0.4.1": "0.4.1", 7 | "jsr:@denosaurs/plug@1.0.5": "1.0.5", 8 | "jsr:@luca/esbuild-deno-loader@*": "0.10.3", 9 | "jsr:@nathanwhit/promptly@~0.1.2": "0.1.2", 10 | "jsr:@sigma/pty-ffi@*": "0.26.2", 11 | "jsr:@std/assert@*": "0.221.0", 12 | "jsr:@std/assert@0.214": "0.214.0", 13 | "jsr:@std/assert@0.221": "0.221.0", 14 | "jsr:@std/assert@~0.213.1": "0.213.1", 15 | "jsr:@std/bytes@0.221": "0.221.0", 16 | "jsr:@std/cli@^1.0.6": "1.0.6", 17 | "jsr:@std/encoding@0.213": "0.213.1", 18 | "jsr:@std/encoding@0.214": "0.214.0", 19 | "jsr:@std/fmt@0.214": "0.214.0", 20 | "jsr:@std/fmt@0.221": "0.221.0", 21 | "jsr:@std/fmt@1": "1.0.2", 22 | "jsr:@std/fmt@^1.0.2": "1.0.2", 23 | "jsr:@std/fmt@~0.213.1": "0.213.1", 24 | "jsr:@std/fs@0.214": "0.214.0", 25 | "jsr:@std/fs@1": "1.0.4", 26 | "jsr:@std/io@0.221": "0.221.0", 27 | "jsr:@std/jsonc@0.213": "0.213.1", 28 | "jsr:@std/path@0.213": "0.213.1", 29 | "jsr:@std/path@0.214": "0.214.0", 30 | "jsr:@std/path@1": "1.0.6", 31 | "jsr:@std/path@^1.0.4": "1.0.6", 32 | "jsr:@std/path@^1.0.6": "1.0.6", 33 | "jsr:@std/streams@0.221": "0.221.0", 34 | "npm:@types/node@*": "22.5.4", 35 | "npm:esbuild@*": "0.23.1" 36 | }, 37 | "jsr": { 38 | "@david/dax@0.42.0": { 39 | "integrity": "0c547c9a20577a6072b90def194c159c9ddab82280285ebfd8268a4ebefbd80b", 40 | "dependencies": [ 41 | "jsr:@david/path", 42 | "jsr:@david/which", 43 | "jsr:@std/fmt@1", 44 | "jsr:@std/fs@1", 45 | "jsr:@std/io", 46 | "jsr:@std/path@1", 47 | "jsr:@std/streams" 48 | ] 49 | }, 50 | "@david/path@0.2.0": { 51 | "integrity": "f2d7aa7f02ce5a55e27c09f9f1381794acb09d328f8d3c8a2e3ab3ffc294dccd", 52 | "dependencies": [ 53 | "jsr:@std/fs@1", 54 | "jsr:@std/path@1" 55 | ] 56 | }, 57 | "@david/which@0.4.1": { 58 | "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" 59 | }, 60 | "@denosaurs/plug@1.0.5": { 61 | "integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf", 62 | "dependencies": [ 63 | "jsr:@std/encoding@0.214", 64 | "jsr:@std/fmt@0.214", 65 | "jsr:@std/fs@0.214", 66 | "jsr:@std/path@0.214" 67 | ] 68 | }, 69 | "@luca/esbuild-deno-loader@0.10.3": { 70 | "integrity": "32fc93f7e7f78060234fd5929a740668aab1c742b808c6048b57f9aaea514921", 71 | "dependencies": [ 72 | "jsr:@std/encoding@0.213", 73 | "jsr:@std/jsonc", 74 | "jsr:@std/path@0.213" 75 | ] 76 | }, 77 | "@nathanwhit/promptly@0.1.2": { 78 | "integrity": "f434ebd37b103e2b9c5569578fb531c855c39980d1b186c0f508aaefe4060d06", 79 | "dependencies": [ 80 | "jsr:@std/fmt@^1.0.2" 81 | ] 82 | }, 83 | "@sigma/pty-ffi@0.26.2": { 84 | "integrity": "1f75f765eceddf051a2c7f064ba12070fb35f74bf8b4e31d8b2734961735f823", 85 | "dependencies": [ 86 | "jsr:@denosaurs/plug" 87 | ] 88 | }, 89 | "@std/assert@0.213.1": { 90 | "integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe", 91 | "dependencies": [ 92 | "jsr:@std/fmt@~0.213.1" 93 | ] 94 | }, 95 | "@std/assert@0.214.0": { 96 | "integrity": "55d398de76a9828fd3b1aa653f4dba3eee4c6985d90c514865d2be9bd082b140" 97 | }, 98 | "@std/assert@0.221.0": { 99 | "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a", 100 | "dependencies": [ 101 | "jsr:@std/fmt@0.221" 102 | ] 103 | }, 104 | "@std/bytes@0.221.0": { 105 | "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" 106 | }, 107 | "@std/cli@1.0.6": { 108 | "integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d" 109 | }, 110 | "@std/encoding@0.213.1": { 111 | "integrity": "fcbb6928713dde941a18ca5db88ca1544d0755ec8fb20fe61e2dc8144b390c62" 112 | }, 113 | "@std/encoding@0.214.0": { 114 | "integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5" 115 | }, 116 | "@std/fmt@0.213.1": { 117 | "integrity": "a06d31777566d874b9c856c10244ac3e6b660bdec4c82506cd46be052a1082c3" 118 | }, 119 | "@std/fmt@0.214.0": { 120 | "integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4" 121 | }, 122 | "@std/fmt@0.221.0": { 123 | "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" 124 | }, 125 | "@std/fmt@1.0.2": { 126 | "integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7" 127 | }, 128 | "@std/fs@0.214.0": { 129 | "integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea", 130 | "dependencies": [ 131 | "jsr:@std/assert@0.214", 132 | "jsr:@std/path@0.214" 133 | ] 134 | }, 135 | "@std/fs@1.0.4": { 136 | "integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c", 137 | "dependencies": [ 138 | "jsr:@std/path@^1.0.6" 139 | ] 140 | }, 141 | "@std/io@0.221.0": { 142 | "integrity": "faf7f8700d46ab527fa05cc6167f4b97701a06c413024431c6b4d207caa010da", 143 | "dependencies": [ 144 | "jsr:@std/assert@0.221", 145 | "jsr:@std/bytes" 146 | ] 147 | }, 148 | "@std/jsonc@0.213.1": { 149 | "integrity": "5578f21aa583b7eb7317eed077ffcde47b294f1056bdbb9aacec407758637bfe", 150 | "dependencies": [ 151 | "jsr:@std/assert@~0.213.1" 152 | ] 153 | }, 154 | "@std/path@0.213.1": { 155 | "integrity": "f187bf278a172752e02fcbacf6bd78a335ed320d080a7ed3a5a59c3e88abc673", 156 | "dependencies": [ 157 | "jsr:@std/assert@~0.213.1" 158 | ] 159 | }, 160 | "@std/path@0.214.0": { 161 | "integrity": "d5577c0b8d66f7e8e3586d864ebdf178bb326145a3611da5a51c961740300285", 162 | "dependencies": [ 163 | "jsr:@std/assert@0.214" 164 | ] 165 | }, 166 | "@std/path@1.0.6": { 167 | "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" 168 | }, 169 | "@std/streams@0.221.0": { 170 | "integrity": "47f2f74634b47449277c0ee79fe878da4424b66bd8975c032e3afdca88986e61", 171 | "dependencies": [ 172 | "jsr:@std/io" 173 | ] 174 | } 175 | }, 176 | "npm": { 177 | "@esbuild/aix-ppc64@0.23.1": { 178 | "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==" 179 | }, 180 | "@esbuild/android-arm64@0.23.1": { 181 | "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==" 182 | }, 183 | "@esbuild/android-arm@0.23.1": { 184 | "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==" 185 | }, 186 | "@esbuild/android-x64@0.23.1": { 187 | "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==" 188 | }, 189 | "@esbuild/darwin-arm64@0.23.1": { 190 | "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==" 191 | }, 192 | "@esbuild/darwin-x64@0.23.1": { 193 | "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==" 194 | }, 195 | "@esbuild/freebsd-arm64@0.23.1": { 196 | "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==" 197 | }, 198 | "@esbuild/freebsd-x64@0.23.1": { 199 | "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==" 200 | }, 201 | "@esbuild/linux-arm64@0.23.1": { 202 | "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==" 203 | }, 204 | "@esbuild/linux-arm@0.23.1": { 205 | "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==" 206 | }, 207 | "@esbuild/linux-ia32@0.23.1": { 208 | "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==" 209 | }, 210 | "@esbuild/linux-loong64@0.23.1": { 211 | "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==" 212 | }, 213 | "@esbuild/linux-mips64el@0.23.1": { 214 | "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==" 215 | }, 216 | "@esbuild/linux-ppc64@0.23.1": { 217 | "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==" 218 | }, 219 | "@esbuild/linux-riscv64@0.23.1": { 220 | "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==" 221 | }, 222 | "@esbuild/linux-s390x@0.23.1": { 223 | "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==" 224 | }, 225 | "@esbuild/linux-x64@0.23.1": { 226 | "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==" 227 | }, 228 | "@esbuild/netbsd-x64@0.23.1": { 229 | "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==" 230 | }, 231 | "@esbuild/openbsd-arm64@0.23.1": { 232 | "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==" 233 | }, 234 | "@esbuild/openbsd-x64@0.23.1": { 235 | "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==" 236 | }, 237 | "@esbuild/sunos-x64@0.23.1": { 238 | "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==" 239 | }, 240 | "@esbuild/win32-arm64@0.23.1": { 241 | "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==" 242 | }, 243 | "@esbuild/win32-ia32@0.23.1": { 244 | "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==" 245 | }, 246 | "@esbuild/win32-x64@0.23.1": { 247 | "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==" 248 | }, 249 | "@types/node@22.5.4": { 250 | "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", 251 | "dependencies": [ 252 | "undici-types" 253 | ] 254 | }, 255 | "esbuild@0.23.1": { 256 | "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", 257 | "dependencies": [ 258 | "@esbuild/aix-ppc64", 259 | "@esbuild/android-arm", 260 | "@esbuild/android-arm64", 261 | "@esbuild/android-x64", 262 | "@esbuild/darwin-arm64", 263 | "@esbuild/darwin-x64", 264 | "@esbuild/freebsd-arm64", 265 | "@esbuild/freebsd-x64", 266 | "@esbuild/linux-arm", 267 | "@esbuild/linux-arm64", 268 | "@esbuild/linux-ia32", 269 | "@esbuild/linux-loong64", 270 | "@esbuild/linux-mips64el", 271 | "@esbuild/linux-ppc64", 272 | "@esbuild/linux-riscv64", 273 | "@esbuild/linux-s390x", 274 | "@esbuild/linux-x64", 275 | "@esbuild/netbsd-x64", 276 | "@esbuild/openbsd-arm64", 277 | "@esbuild/openbsd-x64", 278 | "@esbuild/sunos-x64", 279 | "@esbuild/win32-arm64", 280 | "@esbuild/win32-ia32", 281 | "@esbuild/win32-x64" 282 | ] 283 | }, 284 | "undici-types@6.19.8": { 285 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" 286 | } 287 | }, 288 | "workspace": { 289 | "members": { 290 | "shell-setup": { 291 | "dependencies": [ 292 | "jsr:@david/which@~0.4.1", 293 | "jsr:@nathanwhit/promptly@~0.1.2", 294 | "jsr:@std/cli@^1.0.6", 295 | "jsr:@std/path@^1.0.4", 296 | "npm:@types/node@*" 297 | ] 298 | } 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /shell-setup/src/environment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A collection of functions that interact with the environment, to allow 3 | * for potentially mocking in tests in the future. 4 | */ 5 | import { which } from "@david/which"; 6 | import { homedir as getHomeDir } from "node:os"; 7 | 8 | async function tryStat(path: string): Promise { 9 | try { 10 | return await Deno.stat(path); 11 | } catch (error) { 12 | if ( 13 | error instanceof Deno.errors.NotFound || 14 | (error instanceof Deno.errors.PermissionDenied && 15 | (await Deno.permissions.query({ name: "read", path })).state == 16 | "granted") 17 | ) { 18 | return; 19 | } 20 | throw error; 21 | } 22 | } 23 | 24 | export const environment = { 25 | writeTextFile: Deno.writeTextFile, 26 | readTextFile: Deno.readTextFile, 27 | async isExistingFile(path: string): Promise { 28 | const info = await tryStat(path); 29 | return info?.isFile ?? false; 30 | }, 31 | async isExistingDir(path: string): Promise { 32 | const info = await tryStat(path); 33 | return info?.isDirectory ?? false; 34 | }, 35 | pathExists(path: string): Promise { 36 | return tryStat(path).then((info) => info !== undefined); 37 | }, 38 | mkdir: Deno.mkdir, 39 | homeDir: getHomeDir(), 40 | findCmd: which, 41 | getEnv(name: string): string | undefined { 42 | return Deno.env.get(name); 43 | }, 44 | async runCmd( 45 | cmd: string, 46 | args?: string[], 47 | ): Promise { 48 | return await new Deno.Command(cmd, { 49 | args, 50 | stderr: "piped", 51 | stdout: "piped", 52 | stdin: "null", 53 | }).output(); 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /shell-setup/src/main.ts: -------------------------------------------------------------------------------- 1 | import { environment } from "./environment.ts"; 2 | import { basename, dirname, join } from "@std/path"; 3 | import { confirm, multiSelect } from "@nathanwhit/promptly"; 4 | import { parseArgs } from "@std/cli/parse-args"; 5 | 6 | import { 7 | Bash, 8 | Fish, 9 | Posix, 10 | type ShellScript, 11 | shEnvScript, 12 | shSourceString, 13 | type UnixShell, 14 | type UpdateRcFile, 15 | Zsh, 16 | } from "./shell.ts"; 17 | import { 18 | ensureEndsWith, 19 | ensureExists, 20 | ensureStartsWith, 21 | info, 22 | warn, 23 | withContext, 24 | } from "./util.ts"; 25 | const { 26 | readTextFile, 27 | runCmd, 28 | writeTextFile, 29 | } = environment; 30 | 31 | type CompletionWriteResult = "fail" | "success" | null; 32 | 33 | /** Write completion files to the appropriate locations for all supported shells */ 34 | async function writeCompletionFiles( 35 | availableShells: UnixShell[], 36 | ): Promise { 37 | const written = new Set(); 38 | const results: CompletionWriteResult[] = []; 39 | 40 | const decoder = new TextDecoder(); 41 | 42 | for (const shell of availableShells) { 43 | if (!shell.supportsCompletion) { 44 | results.push(null); 45 | continue; 46 | } 47 | 48 | try { 49 | const completionFilePath = await shell.completionsFilePath?.(); 50 | if (!completionFilePath) { 51 | results.push(null); 52 | continue; 53 | } 54 | await ensureExists(dirname(completionFilePath)); 55 | // deno completions 56 | const output = await runCmd(Deno.execPath(), ["completions", shell.name]); 57 | if (!output.success) { 58 | throw new Error( 59 | `deno completions subcommand failed, stderr was: ${ 60 | decoder.decode(output.stderr) 61 | }`, 62 | ); 63 | } 64 | const completionFileContents = decoder.decode(output.stdout); 65 | if (!completionFileContents) { 66 | warn(`Completions were empty, skipping ${shell.name}`); 67 | results.push("fail"); 68 | continue; 69 | } 70 | let currentContents = null; 71 | try { 72 | currentContents = await readTextFile(completionFilePath); 73 | } catch (error) { 74 | if (!(error instanceof Deno.errors.NotFound)) { 75 | throw error; 76 | } else { 77 | // nothing 78 | } 79 | } 80 | if (currentContents !== completionFileContents) { 81 | if (currentContents !== null) { 82 | warn( 83 | `an existing completion file for deno already exists at ${completionFilePath}, but is out of date. overwriting with new contents`, 84 | ); 85 | } 86 | await writeTextFile(completionFilePath, completionFileContents); 87 | } 88 | results.push("success"); 89 | written.add(completionFilePath); 90 | } catch (error) { 91 | warn(`Failed to install completions for ${shell.name}: ${error}`); 92 | results.push("fail"); 93 | continue; 94 | } 95 | } 96 | return results; 97 | } 98 | 99 | /** A little class to manage backing up shell rc files */ 100 | class Backups { 101 | backedUp = new Set(); 102 | constructor(public backupDir: string) {} 103 | 104 | async add(path: string, contents: string): Promise { 105 | if (this.backedUp.has(path)) { 106 | return; 107 | } 108 | const dest = join(this.backupDir, basename(path)) + `.bak`; 109 | info( 110 | `backing '${path}' up to '${dest}'`, 111 | ); 112 | await Deno.writeTextFile(dest, contents); 113 | this.backedUp.add(path); 114 | } 115 | } 116 | 117 | /** Write commands necessary to set up completions to shell rc files */ 118 | async function writeCompletionRcCommands( 119 | availableShells: UnixShell[], 120 | backups: Backups, 121 | ) { 122 | for (const shell of availableShells) { 123 | if (!shell.supportsCompletion) continue; 124 | 125 | const rcCmd = await shell.completionsSourceString?.(); 126 | if (!rcCmd) continue; 127 | 128 | for (const rc of await shell.rcsToUpdate()) { 129 | await updateRcFile(rc, rcCmd, backups); 130 | } 131 | } 132 | } 133 | 134 | /** Write the files setting up the PATH vars (and potentially others in the future) for all shells */ 135 | async function writeEnvFiles(availableShells: UnixShell[], installDir: string) { 136 | const written = new Array(); 137 | 138 | let i = 0; 139 | while (i < availableShells.length) { 140 | const shell = availableShells[i]; 141 | const script = (shell.envScript ?? shEnvScript)(installDir); 142 | 143 | if (!written.some((s) => s.equals(script))) { 144 | if (await script.write(installDir)) { 145 | written.push(script); 146 | } else { 147 | continue; 148 | } 149 | } 150 | 151 | i++; 152 | } 153 | } 154 | 155 | /** Updates an rc file (e.g. `.bashrc`) with a command string. 156 | * If the file already contains the command, it will not be updated. 157 | * @param rc - path to the rc file 158 | * @param command - either the command to append, or an object with commands to prepend and/or append 159 | * @param backups - manager for rc file backups 160 | */ 161 | async function updateRcFile( 162 | rc: string, 163 | command: string | UpdateRcFile, 164 | backups: Backups, 165 | ): Promise { 166 | let prepend = ""; 167 | let append = ""; 168 | if (typeof command === "string") { 169 | append = command; 170 | } else { 171 | prepend = command.prepend ?? ""; 172 | append = command.append ?? ""; 173 | } 174 | if (!prepend && !append) { 175 | return false; 176 | } 177 | 178 | let contents: string | undefined; 179 | try { 180 | contents = await readTextFile(rc); 181 | if (prepend) { 182 | if (contents.includes(prepend)) { 183 | // nothing to prepend 184 | prepend = ""; 185 | } else { 186 | // always add a newline 187 | prepend = ensureEndsWith(prepend, "\n"); 188 | } 189 | } 190 | if (append) { 191 | if (contents.includes(append)) { 192 | // nothing to append 193 | append = ""; 194 | } else if (!contents.endsWith("\n")) { 195 | // add new line to start 196 | append = ensureStartsWith(append, "\n"); 197 | } 198 | } 199 | } catch (_error) { 200 | prepend = prepend ? ensureEndsWith(prepend, "\n") : prepend; 201 | } 202 | if (!prepend && !append) { 203 | return false; 204 | } 205 | 206 | if (contents !== undefined) { 207 | await backups.add(rc, contents); 208 | } 209 | 210 | await ensureExists(dirname(rc)); 211 | 212 | try { 213 | await writeTextFile(rc, prepend + (contents ?? "") + append, { 214 | create: true, 215 | }); 216 | 217 | return true; 218 | } catch (error) { 219 | if ( 220 | error instanceof Deno.errors.PermissionDenied || 221 | // deno-lint-ignore no-explicit-any 222 | error instanceof (Deno.errors as any).NotCapable 223 | ) { 224 | return false; 225 | } 226 | throw withContext(`Failed to update shell rc file: ${rc}`, error); 227 | } 228 | } 229 | 230 | /** Write the commands necessary to source the env file (which sets up the path). 231 | * Up until this point, we have not modified any shell config files. 232 | */ 233 | async function addToPath( 234 | availableShells: UnixShell[], 235 | installDir: string, 236 | backups: Backups, 237 | ) { 238 | for (const shell of availableShells) { 239 | const sourceCmd = await (shell.sourceString ?? shSourceString)(installDir); 240 | 241 | for (const rc of await shell.rcsToUpdate()) { 242 | await updateRcFile(rc, sourceCmd, backups); 243 | } 244 | } 245 | } 246 | 247 | // Update this when adding support for a new shell 248 | const shells: UnixShell[] = [ 249 | new Posix(), 250 | new Bash(), 251 | new Zsh(), 252 | new Fish(), 253 | ]; 254 | 255 | async function getAvailableShells(): Promise { 256 | const present = []; 257 | for (const shell of shells) { 258 | try { 259 | if (await shell.exists()) { 260 | present.push(shell); 261 | } 262 | } catch (_e) { 263 | continue; 264 | } 265 | } 266 | return present; 267 | } 268 | 269 | interface SetupOpts { 270 | skipPrompts: boolean; 271 | noModifyPath: boolean; 272 | } 273 | 274 | async function setupShells( 275 | installDir: string, 276 | backupDir: string, 277 | opts: SetupOpts, 278 | ) { 279 | const { 280 | skipPrompts, 281 | noModifyPath, 282 | } = opts; 283 | const availableShells = await getAvailableShells(); 284 | 285 | await writeEnvFiles(availableShells, installDir); 286 | 287 | const backups = new Backups(backupDir); 288 | 289 | if ( 290 | (skipPrompts && !noModifyPath) || (!skipPrompts && 291 | await confirm(`Edit shell configs to add deno to the PATH?`, { 292 | default: true, 293 | })) 294 | ) { 295 | await ensureExists(backupDir); 296 | await addToPath(availableShells, installDir, backups); 297 | console.log( 298 | "\nDeno was added to the PATH.\nYou may need to restart your shell for it to become available.\n", 299 | ); 300 | } 301 | 302 | const shellsWithCompletion = availableShells.filter((s) => 303 | s.supportsCompletion !== false 304 | ); 305 | const selected = skipPrompts ? [] : await multiSelect( 306 | { 307 | message: `Set up completions?`, 308 | options: shellsWithCompletion.map((s) => { 309 | const maybeNotes = typeof s.supportsCompletion === "string" 310 | ? ` (${s.supportsCompletion})` 311 | : ""; 312 | return s.name + 313 | maybeNotes; 314 | }), 315 | }, 316 | ); 317 | const completionsToSetup = selected.map((idx) => shellsWithCompletion[idx]); 318 | 319 | if ( 320 | completionsToSetup.length > 0 321 | ) { 322 | await ensureExists(backupDir); 323 | const results = await writeCompletionFiles(completionsToSetup); 324 | await writeCompletionRcCommands( 325 | completionsToSetup.filter((_s, i) => results[i] !== "fail"), 326 | backups, 327 | ); 328 | } 329 | } 330 | 331 | function printHelp() { 332 | console.log(`\n 333 | Setup script for installing deno 334 | 335 | Options: 336 | -y, --yes 337 | Skip interactive prompts and accept defaults 338 | --no-modify-path 339 | Don't add deno to the PATH environment variable 340 | -h, --help 341 | Print help\n`); 342 | } 343 | 344 | async function main() { 345 | if (Deno.args.length === 0) { 346 | throw new Error( 347 | "Expected the deno install directory as the first argument", 348 | ); 349 | } 350 | 351 | const args = parseArgs(Deno.args.slice(1), { 352 | boolean: ["yes", "no-modify-path", "help"], 353 | alias: { 354 | "yes": "y", 355 | "help": "h", 356 | }, 357 | default: { 358 | yes: false, 359 | "no-modify-path": false, 360 | }, 361 | unknown: (arg: string) => { 362 | if (arg.startsWith("-")) { 363 | printHelp(); 364 | console.error(`Unknown flag ${arg}. Shell will not be configured`); 365 | Deno.exit(1); 366 | } 367 | return false; 368 | }, 369 | }); 370 | 371 | if (args.help) { 372 | printHelp(); 373 | return; 374 | } 375 | 376 | if ( 377 | Deno.build.os === "windows" || (!args.yes && !(Deno.stdin.isTerminal() && 378 | Deno.stdout.isTerminal())) 379 | ) { 380 | // the powershell script already handles setting up the path 381 | return; 382 | } 383 | 384 | const installDir = Deno.args[0].trim(); 385 | 386 | const backupDir = join(installDir, ".shellRcBackups"); 387 | 388 | try { 389 | await setupShells(installDir, backupDir, { 390 | skipPrompts: args.yes, 391 | noModifyPath: args["no-modify-path"], 392 | }); 393 | } catch (_e) { 394 | warn( 395 | `Failed to configure your shell environments, you may need to manually add deno to your PATH environment variable. 396 | 397 | Manually add the directory to your $HOME/.bashrc (or similar)": 398 | export DENO_INSTALL="${installDir}" 399 | export PATH="${installDir}/bin:$PATH"\n`, 400 | ); 401 | } 402 | } 403 | 404 | if (import.meta.main) { 405 | await main(); 406 | } 407 | -------------------------------------------------------------------------------- /shell-setup/src/shell.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shell-specific handling. Largely adapted from rustup 3 | * (https://github.com/rust-lang/rustup/blob/ccc668ccf852b7f37a4072150a6dd2aac5844d38/src/cli/self_update/shell.rs) 4 | */ 5 | 6 | import { environment } from "./environment.ts"; 7 | import { join } from "@std/path/join"; 8 | import { dirname } from "@std/path/dirname"; 9 | import { 10 | filterAsync, 11 | shellEnvContains, 12 | withContext, 13 | withEnvVar, 14 | } from "./util.ts"; 15 | const { 16 | isExistingFile, 17 | writeTextFile, 18 | homeDir, 19 | findCmd, 20 | runCmd, 21 | getEnv, 22 | pathExists, 23 | } = environment; 24 | 25 | /** A shell script, for instance an `env` file. Abstraction adapted from 26 | * rustup (see above) 27 | */ 28 | export class ShellScript { 29 | constructor(public name: string, public contents: string) {} 30 | 31 | equals(other: ShellScript): boolean { 32 | return this.name === other.name && this.contents === other.contents; 33 | } 34 | 35 | async write(denoInstallDir: string): Promise { 36 | const envFilePath = join(denoInstallDir, this.name); 37 | try { 38 | await writeTextFile(envFilePath, this.contents); 39 | return true; 40 | } catch (error) { 41 | if (error instanceof Deno.errors.PermissionDenied) { 42 | return false; 43 | } 44 | throw withContext( 45 | `Failed to write ${this.name} file to ${envFilePath}`, 46 | error, 47 | ); 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * An env script to set up the PATH, suitable for `sh` compatible shells. 54 | */ 55 | export const shEnvScript = (installDir: string) => 56 | new ShellScript( 57 | "env", 58 | `#!/bin/sh 59 | # deno shell setup; adapted from rustup 60 | # affix colons on either side of $PATH to simplify matching 61 | case ":\${PATH}:" in 62 | *:"${installDir}/bin":*) 63 | ;; 64 | *) 65 | # Prepending path in case a system-installed deno executable needs to be overridden 66 | export PATH="${installDir}/bin:$PATH" 67 | ;; 68 | esac 69 | `, 70 | ); 71 | 72 | /** 73 | * A command for `sh` compatible shells to source the env file. 74 | */ 75 | export const shSourceString = (installDir: string) => { 76 | return `. "${installDir}/env"`; 77 | }; 78 | 79 | export type MaybePromise = Promise | T; 80 | 81 | export type UpdateRcFile = { prepend?: string; append?: string }; 82 | 83 | /** Abstraction of a Unix-y shell. */ 84 | export interface UnixShell { 85 | name: string; 86 | /** Does deno support completions for the shell? If a string, implies true 87 | * and the string will appear to the user as a note when prompting for completion install 88 | */ 89 | supportsCompletion: boolean | string; 90 | /** Does the shell exist on the system? */ 91 | exists(): MaybePromise; 92 | /** List of potential config files for the shell */ 93 | rcfiles(): MaybePromise; 94 | /** List of config files to update */ 95 | rcsToUpdate(): MaybePromise; 96 | /** Script to set up env vars (PATH, and potentially others in the future) */ 97 | envScript?(installDir: string): ShellScript; 98 | /** Command to source the env script */ 99 | sourceString?(installDir: string): MaybePromise; 100 | /** Path to write completions to */ 101 | completionsFilePath?(): MaybePromise; 102 | /** Command to source the completion file */ 103 | completionsSourceString?(): MaybePromise; 104 | } 105 | 106 | export class Posix implements UnixShell { 107 | name = "sh"; 108 | supportsCompletion = false; 109 | exists(): boolean { 110 | return true; 111 | } 112 | rcfiles(): string[] { 113 | return [join(homeDir, ".profile")]; 114 | } 115 | rcsToUpdate(): string[] { 116 | return this.rcfiles(); 117 | } 118 | } 119 | 120 | export class Bash implements UnixShell { 121 | name = "bash"; 122 | get supportsCompletion() { 123 | if (Deno.build.os === "darwin") { 124 | return "not recommended on macOS"; 125 | } 126 | return true; 127 | } 128 | async exists(): Promise { 129 | return (await this.rcsToUpdate()).length > 0; 130 | } 131 | rcfiles(): string[] { 132 | return [".bash_profile", ".bash_login", ".bashrc"] 133 | .map((rc) => join(homeDir, rc)); 134 | } 135 | rcsToUpdate(): Promise { 136 | return filterAsync(this.rcfiles(), isExistingFile); 137 | } 138 | completionsFilePath(): string { 139 | const USER = Deno.env.get("USER"); 140 | if (USER === "root") { 141 | return "/usr/local/etc/bash_completion.d/deno.bash"; 142 | } 143 | return join(homeDir, ".local/share/bash-completion/completions/deno.bash"); 144 | } 145 | completionsSourceString(): string { 146 | return `source ${this.completionsFilePath()}`; 147 | } 148 | } 149 | 150 | export class Zsh implements UnixShell { 151 | name = "zsh"; 152 | supportsCompletion = true; 153 | async exists(): Promise { 154 | if ( 155 | shellEnvContains("zsh") || (await findCmd("zsh")) 156 | ) { 157 | return true; 158 | } 159 | return false; 160 | } 161 | async getZshDotDir(): Promise { 162 | let zshDotDir; 163 | if ( 164 | shellEnvContains("zsh") 165 | ) { 166 | zshDotDir = getEnv("ZDOTDIR"); 167 | } else { 168 | const output = await runCmd("zsh", [ 169 | "-c", 170 | "echo -n $ZDOTDIR", 171 | ]); 172 | const stdout = new TextDecoder().decode(output.stdout).trim(); 173 | zshDotDir = stdout.length > 0 ? stdout : undefined; 174 | } 175 | 176 | return zshDotDir; 177 | } 178 | async rcfiles(): Promise { 179 | const zshDotDir = await this.getZshDotDir(); 180 | return [zshDotDir, homeDir].map((dir) => 181 | dir ? join(dir, ".zshrc") : undefined 182 | ).filter((dir) => dir !== undefined); 183 | } 184 | async rcsToUpdate(): Promise { 185 | let out = await filterAsync( 186 | await this.rcfiles(), 187 | isExistingFile, 188 | ); 189 | if (out.length === 0) { 190 | out = await this.rcfiles(); 191 | } 192 | return out; 193 | } 194 | async completionsFilePath(): Promise { 195 | let zshDotDir = await this.getZshDotDir(); 196 | if (!zshDotDir) { 197 | zshDotDir = join(homeDir, ".zsh"); 198 | } 199 | return join(zshDotDir, "completions", "_deno.zsh"); 200 | } 201 | async completionsSourceString(): Promise { 202 | const filePath = await this.completionsFilePath(); 203 | const completionDir = dirname(filePath); 204 | const fpathSetup = 205 | `# Add deno completions to search path\nif [[ ":$FPATH:" != *":${completionDir}:"* ]]; then export FPATH="${completionDir}:$FPATH"; fi`; 206 | 207 | const zshDotDir = (await this.getZshDotDir()) ?? homeDir; 208 | // try to figure out whether the user already has `compinit` being called 209 | 210 | let append: string | undefined; 211 | if ( 212 | (await filterAsync( 213 | [".zcompdump", ".oh_my_zsh", ".zprezto"], 214 | (f) => pathExists(join(zshDotDir, f)), 215 | )).length == 0 216 | ) { 217 | append = 218 | "# Initialize zsh completions (added by deno install script)\nautoload -Uz compinit\ncompinit"; 219 | } 220 | return { 221 | prepend: fpathSetup, 222 | append, 223 | }; 224 | } 225 | } 226 | 227 | export class Fish implements UnixShell { 228 | name = "fish"; 229 | supportsCompletion = true; 230 | async exists(): Promise { 231 | if ( 232 | shellEnvContains("fish") || 233 | (await findCmd("fish")) 234 | ) { 235 | return true; 236 | } 237 | return false; 238 | } 239 | 240 | fishConfigDir(): string { 241 | const first = withEnvVar("XDG_CONFIG_HOME", (p) => { 242 | if (!p) return; 243 | return join(p, "fish"); 244 | }); 245 | return first ?? join(homeDir, ".config", "fish"); 246 | } 247 | 248 | rcfiles(): string[] { 249 | // XDG_CONFIG_HOME/fish/conf.d or ~/.config/fish/conf.d 250 | const conf = "conf.d/deno.fish"; 251 | return [join(this.fishConfigDir(), conf)]; 252 | } 253 | 254 | rcsToUpdate(): string[] { 255 | return this.rcfiles(); 256 | } 257 | 258 | envScript(installDir: string): ShellScript { 259 | const fishEnv = ` 260 | # deno shell setup 261 | if not contains "${installDir}/bin" $PATH 262 | # prepend to path to take precedence over potential package manager deno installations 263 | set -x PATH "${installDir}/bin" $PATH 264 | end 265 | `; 266 | return new ShellScript("env.fish", fishEnv); 267 | } 268 | 269 | sourceString(installDir: string): MaybePromise { 270 | return `source "${installDir}/env.fish"`; 271 | } 272 | 273 | completionsFilePath(): string { 274 | return join(this.fishConfigDir(), "completions", "deno.fish"); 275 | } 276 | 277 | // no further config needed for completions 278 | } 279 | -------------------------------------------------------------------------------- /shell-setup/src/util.ts: -------------------------------------------------------------------------------- 1 | import { environment } from "./environment.ts"; 2 | const { isExistingDir, mkdir } = environment; 3 | 4 | export function withContext(ctx: string, error?: unknown) { 5 | return new Error(ctx, { cause: error }); 6 | } 7 | 8 | export async function filterAsync( 9 | arr: T[], 10 | pred: (v: T) => Promise, 11 | ): Promise { 12 | const filtered = await Promise.all(arr.map((v) => pred(v))); 13 | return arr.filter((_, i) => filtered[i]); 14 | } 15 | 16 | export function withEnvVar( 17 | name: string, 18 | f: (value: string | undefined) => T, 19 | ): T { 20 | const value = environment.getEnv(name); 21 | return f(value); 22 | } 23 | 24 | export function shellEnvContains(s: string): boolean { 25 | return withEnvVar("SHELL", (sh) => sh !== undefined && sh.includes(s)); 26 | } 27 | 28 | export function warn(s: string) { 29 | console.error(`%cwarning%c: ${s}`, "color: yellow", "color: inherit"); 30 | } 31 | 32 | export function info(s: string) { 33 | console.error(`%cinfo%c: ${s}`, "color: green", "color: inherit"); 34 | } 35 | 36 | export async function ensureExists(dirPath: string): Promise { 37 | if (!await isExistingDir(dirPath)) { 38 | await mkdir(dirPath, { 39 | recursive: true, 40 | }); 41 | } 42 | } 43 | 44 | export function ensureEndsWith(s: string, suffix: string): string { 45 | if (!s.endsWith(suffix)) { 46 | return s + suffix; 47 | } 48 | return s; 49 | } 50 | 51 | export function ensureStartsWith(s: string, prefix: string): string { 52 | if (!s.startsWith(prefix)) { 53 | return prefix + s; 54 | } 55 | return s; 56 | } 57 | --------------------------------------------------------------------------------