├── .gitattributes ├── SHA256SUM ├── .github └── workflows │ ├── publish.yml │ └── ci.yml ├── install_test.sh ├── deno.json ├── shell-setup ├── deno.jsonc ├── src │ ├── test │ │ ├── rc_files_test.ts │ │ └── common.ts │ ├── util.ts │ ├── environment.ts │ ├── rc_files.ts │ ├── shell.ts │ └── main.ts ├── deno.lock └── bundled.esm.js ├── LICENSE ├── install_test.ps1 ├── install.ps1 ├── install.sh ├── README.md └── install_test.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | shell-setup/bundled.esm.js -diff 4 | -------------------------------------------------------------------------------- /SHA256SUM: -------------------------------------------------------------------------------- 1 | 47ab042defa51f0afe82a3374219e45280dcc9b51b3cbbdec5e1bda6ca277a43 install.sh 2 | bb2fb3df6b8a7f55eed0d155231d46f15f02ac0541239206358b070711699684 install.ps1 3 | -------------------------------------------------------------------------------- /.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@v2 19 | 20 | - name: Publish to JSR on tag 21 | run: | 22 | cd shell-setup 23 | deno run -A jsr:@david/publish-on-tag@0.1.3 24 | -------------------------------------------------------------------------------- /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 | if [ "$OS" != "Windows_NT" ]; then 11 | ~/.deno/bin/dx -h 12 | fi 13 | 14 | # Test that we can install a specific version at a custom location. 15 | rm -rf ~/deno-1.15.0 16 | export DENO_INSTALL="$HOME/deno-1.15.0" 17 | ./install.sh v1.15.0 18 | ~/deno-1.15.0/bin/deno --version | grep 1.15.0 19 | 20 | # Test that we can install at a relative custom location. 21 | export DENO_INSTALL="." 22 | ./install.sh v1.46.0 23 | bin/deno --version | grep 1.46.0 24 | -------------------------------------------------------------------------------- /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 | "imports": { 26 | "@std/assert": "jsr:@std/assert@^1.0.8", 27 | "@david/dax": "jsr:@david/dax@^0.44", 28 | "@sigma/pty-ffi": "jsr:@sigma/pty-ffi@^0.26" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /shell-setup/deno.jsonc: -------------------------------------------------------------------------------- 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 bundle ./src/main.ts -o ./bundled.esm.js" 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 | // dev deps 18 | "@types/node": "npm:@types/node@*" 19 | }, 20 | "publish": { 21 | "exclude": [ 22 | "./bundle.ts", 23 | "./src/test/*.ts" 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /shell-setup/src/test/rc_files_test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "./common.ts"; 2 | import { RcBackups, updateRcFile } from "../rc_files.ts"; 3 | import { assertEquals } from "@std/assert"; 4 | 5 | test("updateRcFile includes trailing newline", async ({ fileStore }) => { 6 | for ( 7 | const existingContent of [ 8 | "echo 'existing content'", 9 | "echo 'existing content'\n", 10 | ] 11 | ) { 12 | await fileStore.mkdir("/test/backups/", { recursive: true }); 13 | await fileStore.writeTextFile( 14 | "/test/home/.bashrc", 15 | existingContent, 16 | ); 17 | const backups = new RcBackups("/test/backups/"); 18 | await updateRcFile("/test/home/.bashrc", "install deno", backups); 19 | const contents = await fileStore.readTextFile("/test/home/.bashrc"); 20 | assertEquals(contents, "echo 'existing content'\ninstall deno\n"); 21 | 22 | const backupsContents = await fileStore.readTextFile( 23 | "/test/backups/.bashrc.bak", 24 | ); 25 | assertEquals(backupsContents, existingContent); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | Get-ChildItem -Force ~\.deno\bin 11 | ~\.deno\bin\dx.cmd -h 12 | 13 | # Test that we can install a specific version at a custom location. 14 | Remove-Item "~\deno-1.0.0" -Recurse -Force -ErrorAction SilentlyContinue 15 | $env:DENO_INSTALL = "$Home\deno-1.0.0" 16 | $v = "1.0.0"; .\install.ps1 17 | $DenoVersion = ~\deno-1.0.0\bin\deno.exe --version 18 | if (!($DenoVersion -like '*1.0.0*')) { 19 | throw $DenoVersion 20 | } 21 | 22 | # Test that we can install at a relative custom location. 23 | Remove-Item "bin" -Recurse -Force -ErrorAction SilentlyContinue 24 | $env:DENO_INSTALL = "." 25 | $v = "1.1.0"; .\install.ps1 26 | $DenoVersion = bin\deno.exe --version 27 | if (!($DenoVersion -like '*1.1.0*')) { 28 | throw $DenoVersion 29 | } 30 | 31 | # Test that the old temp file installer still works. 32 | Remove-Item "~\deno-1.0.1" -Recurse -Force -ErrorAction SilentlyContinue 33 | $env:DENO_INSTALL = "$Home\deno-1.0.1" 34 | $v = $null; .\install.ps1 v1.0.1 35 | $DenoVersion = ~\deno-1.0.1\bin\deno.exe --version 36 | if (!($DenoVersion -like '*1.0.1*')) { 37 | throw $DenoVersion 38 | } 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | $versionCheck = "const [major, minor] = Deno.version.deno.split('.').map(Number); if (major < 2 || (major === 2 && minor < 6)) Deno.exit(1);" 51 | & $DenoExe eval $versionCheck 52 | if ($LASTEXITCODE -eq 0) { 53 | & $DenoExe x --install-alias 54 | Write-Output 'Installed dx alias, if this conflicts with an existing command, you can remove it with `Remove-Item $(Get-Command dx).Path` and choose a new name with `dx --install-alias `' 55 | } 56 | 57 | Write-Output "Deno was installed successfully to ${DenoExe}" 58 | Write-Output "Run 'deno --help' to get started" 59 | Write-Output "Stuck? Join our Discord https://discord.gg/deno" 60 | -------------------------------------------------------------------------------- /.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@v2 55 | 56 | - name: deno lint 57 | run: deno lint 58 | 59 | - name: check fmt 60 | run: deno fmt --check 61 | 62 | - name: check bundled file up to date 63 | run: | 64 | cd shell-setup 65 | deno task bundle 66 | if ! git --no-pager diff --exit-code ./bundled.esm.js; then 67 | echo 'Bundled script is out of date, update it with `cd shell-setup; deno task bundle`'. 68 | exit 1 69 | fi 70 | - name: integration tests 71 | if: matrix.os != 'windows-latest' 72 | run: deno test -A --permit-no-files 73 | 74 | - name: dry run publishing 75 | run: deno publish --dry-run 76 | -------------------------------------------------------------------------------- /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 _environmentImpl = { 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 | async pathExists(path: string): Promise { 36 | const info = await tryStat(path); 37 | return info !== undefined; 38 | }, 39 | mkdir: Deno.mkdir, 40 | homeDir: getHomeDir(), 41 | findCmd: which, 42 | getEnv(name: string): string | undefined { 43 | return Deno.env.get(name); 44 | }, 45 | async runCmd( 46 | cmd: string, 47 | args?: string[], 48 | ): Promise { 49 | return await new Deno.Command(cmd, { 50 | args, 51 | stderr: "piped", 52 | stdout: "piped", 53 | stdin: "null", 54 | }).output(); 55 | }, 56 | }; 57 | 58 | export type Environment = typeof _environmentImpl; 59 | 60 | function makeWrapper() { 61 | const wrapperEnv: Partial = {}; 62 | for (const keyString in _environmentImpl) { 63 | const key = keyString as keyof Environment; 64 | if (typeof _environmentImpl[key] === "function") { 65 | // deno-lint-ignore no-explicit-any 66 | wrapperEnv[key] = function (...args: any[]) { 67 | // deno-lint-ignore no-explicit-any 68 | return (_environmentImpl[key] as any)(...args); 69 | // deno-lint-ignore no-explicit-any 70 | } as any; 71 | } 72 | } 73 | Object.defineProperty(wrapperEnv, "homeDir", { 74 | get: () => _environmentImpl.homeDir, 75 | }); 76 | return wrapperEnv as Environment; 77 | } 78 | 79 | export const environment: Environment = makeWrapper(); 80 | -------------------------------------------------------------------------------- /shell-setup/src/rc_files.ts: -------------------------------------------------------------------------------- 1 | import { environment } from "./environment.ts"; 2 | import { basename, dirname, join } from "@std/path"; 3 | import { 4 | ensureEndsWith, 5 | ensureExists, 6 | ensureStartsWith, 7 | info, 8 | withContext, 9 | } from "./util.ts"; 10 | import type { UpdateRcFile } from "./shell.ts"; 11 | 12 | /** A little class to manage backing up shell rc files */ 13 | export class RcBackups { 14 | backedUp = new Set(); 15 | constructor(public backupDir: string) {} 16 | 17 | async add(path: string, contents: string): Promise { 18 | if (this.backedUp.has(path)) { 19 | return; 20 | } 21 | const dest = join(this.backupDir, basename(path)) + `.bak`; 22 | info( 23 | `backing '${path}' up to '${dest}'`, 24 | ); 25 | await environment.writeTextFile(dest, contents); 26 | this.backedUp.add(path); 27 | } 28 | } 29 | 30 | /** Updates an rc file (e.g. `.bashrc`) with a command string. 31 | * If the file already contains the command, it will not be updated. 32 | * @param rc - path to the rc file 33 | * @param command - either the command to append, or an object with commands to prepend and/or append 34 | * @param backups - manager for rc file backups 35 | */ 36 | export async function updateRcFile( 37 | rc: string, 38 | command: string | UpdateRcFile, 39 | backups: RcBackups, 40 | ): Promise { 41 | let prepend = ""; 42 | let append = ""; 43 | if (typeof command === "string") { 44 | append = command; 45 | } else { 46 | prepend = command.prepend ?? ""; 47 | append = command.append ?? ""; 48 | } 49 | if (!prepend && !append) { 50 | return false; 51 | } 52 | 53 | let contents: string | undefined; 54 | try { 55 | contents = await environment.readTextFile(rc); 56 | if (prepend) { 57 | if (contents.includes(prepend)) { 58 | // nothing to prepend 59 | prepend = ""; 60 | } else { 61 | // always add a newline 62 | prepend = ensureEndsWith(prepend, "\n"); 63 | } 64 | } 65 | if (append) { 66 | if (contents.includes(append)) { 67 | // nothing to append 68 | append = ""; 69 | } else if (!contents.endsWith("\n")) { 70 | // add new line to start + end 71 | append = ensureEndsWith(ensureStartsWith(append, "\n"), "\n"); 72 | } else { 73 | append = ensureEndsWith(append, "\n"); 74 | } 75 | } 76 | } catch (_error) { 77 | prepend = prepend ? ensureEndsWith(prepend, "\n") : prepend; 78 | append = append ? ensureEndsWith(append, "\n") : append; 79 | } 80 | if (!prepend && !append) { 81 | return false; 82 | } 83 | 84 | if (contents !== undefined) { 85 | await backups.add(rc, contents); 86 | } 87 | 88 | await ensureExists(dirname(rc)); 89 | 90 | try { 91 | await environment.writeTextFile(rc, prepend + (contents ?? "") + append, { 92 | create: true, 93 | }); 94 | 95 | return true; 96 | } catch (error) { 97 | if ( 98 | error instanceof Deno.errors.PermissionDenied || 99 | // deno-lint-ignore no-explicit-any 100 | error instanceof (Deno.errors as any).NotCapable 101 | ) { 102 | return false; 103 | } 104 | throw withContext(`Failed to update shell rc file: ${rc}`, error); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /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 | if $exe eval 'const [major, minor] = Deno.version.deno.split(".").map(Number); if (major < 2 || (major === 2 && minor < 6)) Deno.exit(1)'; then 88 | "$exe" x --install-alias 89 | # shellcheck disable=SC2016 90 | echo 'Installed dx alias, if this conflicts with an existing command, you can remove it with `rm $(which dx)` and choose a new name with `dx --install-alias `' 91 | fi 92 | echo "Deno was installed successfully to $exe" 93 | 94 | run_shell_setup() { 95 | $exe run -A --reload jsr:@deno/installer-shell-setup/bundled "$deno_install" "$@" 96 | } 97 | 98 | # If stdout is a terminal, see if we can run shell setup script (which includes interactive prompts) 99 | if { [ -z "$CI" ] && [ -t 1 ]; } || $should_run_shell_setup; then 100 | if $exe eval 'const [major, minor] = Deno.version.deno.split(".").map(Number); if (major < 1 || (major === 1 && minor < 42)) Deno.exit(1)'; then 101 | if $should_run_shell_setup; then 102 | run_shell_setup -y "$@" # doublely sure to pass -y to run_shell_setup in this case 103 | else 104 | if [ -t 0 ]; then 105 | run_shell_setup "$@" 106 | else 107 | # This script is probably running piped into sh, so we don't have direct access to stdin. 108 | # Instead, explicitly connect /dev/tty to stdin 109 | run_shell_setup "$@" /dev/null; then 115 | echo "Run 'deno --help' to get started" 116 | else 117 | echo "Run '$exe --help' to get started" 118 | fi 119 | echo 120 | echo "Stuck? Join our Discord https://discord.gg/deno" 121 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /install_test.ts: -------------------------------------------------------------------------------- 1 | import $, { Path } from "@david/dax"; 2 | import { Pty } from "@sigma/pty-ffi"; 3 | import { assert, assertEquals, assertStringIncludes } from "@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 | const shellOutput = await runInBash( 13 | [`cat "${installScript.toString()}" | sh -s -- -y v2.0.0-rc.6`], 14 | { env, cwd: tempDir }, 15 | ); 16 | console.log(shellOutput); 17 | 18 | assertStringIncludes(shellOutput, "Deno was added to the PATH"); 19 | 20 | const deno = installDir.join("bin/deno"); 21 | assert(await deno.exists()); 22 | 23 | // Check that it's on the PATH now, and that it's the correct version. 24 | const output = await new Deno.Command("bash", { 25 | args: ["-i", "-c", "deno --version"], 26 | env, 27 | }).output(); 28 | const stdout = new TextDecoder().decode(output.stdout).trim(); 29 | 30 | const versionRe = /deno (\d+\.\d+\.\d+\S*)/; 31 | const match = stdout.match(versionRe); 32 | 33 | assert(match !== null); 34 | assertEquals(match[1], "2.0.0-rc.6"); 35 | }, 36 | ); 37 | 38 | Deno.test( 39 | { name: "install no modify path", ignore: Deno.build.os === "windows" }, 40 | async () => { 41 | await using testEnv = await TestEnv.setup(); 42 | const { env, tempDir, installScript, installDir } = testEnv; 43 | await testEnv.homeDir.join(".bashrc").ensureFile(); 44 | 45 | const shellOutput = await runInBash( 46 | [`cat "${installScript.toString()}" | sh -s -- -y v2.0.0-rc.6 --no-modify-path`], 47 | { env, cwd: tempDir }, 48 | ); 49 | 50 | assert( 51 | !shellOutput.includes("Deno was added to the PATH"), 52 | `Unexpected output, shouldn't have added to the PATH:\n${shellOutput}`, 53 | ); 54 | 55 | const deno = installDir.join("bin/deno"); 56 | assert(await deno.exists()); 57 | }, 58 | ); 59 | 60 | class TestEnv implements AsyncDisposable, Disposable { 61 | #tempDir: Path; 62 | private constructor( 63 | tempDir: Path, 64 | public homeDir: Path, 65 | public installDir: Path, 66 | public installScript: Path, 67 | public env: Record, 68 | ) { 69 | this.#tempDir = tempDir; 70 | } 71 | get tempDir() { 72 | return this.#tempDir; 73 | } 74 | static async setup({ env = {} }: { env?: Record } = {}) { 75 | const tempDir = $.path(await Deno.makeTempDir()); 76 | const homeDir = await tempDir.join("home").ensureDir(); 77 | const installDir = tempDir.join(".deno"); 78 | 79 | const tempSetup = tempDir.join("shell-setup.js"); 80 | await $.path(resolve("./shell-setup/bundled.esm.js")).copyFile(tempSetup); 81 | 82 | // Copy the install script to a temp location, and modify it to 83 | // run the shell setup script from the local source instead of JSR. 84 | const contents = await Deno.readTextFile(resolve("./install.sh")); 85 | const contentsLocal = contents.replaceAll( 86 | "jsr:@deno/installer-shell-setup/bundled", 87 | tempSetup.toString(), 88 | ); 89 | if (contents === contentsLocal) { 90 | throw new Error("Failed to point installer at local source"); 91 | } 92 | const installScript = tempDir.join("install.sh"); 93 | await installScript.writeText(contentsLocal); 94 | 95 | await Deno.chmod(installScript.toString(), 0o755); 96 | 97 | // Ensure that the necessary binaries are in the PATH. 98 | // It's not perfect, but the idea is to keep the test environment 99 | // as clean as possible to make it less host dependent. 100 | const needed = ["bash", "unzip", "cat", "sh"]; 101 | const binPaths = await Promise.all(needed.map((n) => $.which(n))); 102 | const searchPaths = new Set( 103 | binPaths.map((p, i) => { 104 | if (p === undefined) { 105 | throw new Error(`missing dependency: ${needed[i]}`); 106 | } 107 | return $.path(p).parentOrThrow().toString(); 108 | }), 109 | ); 110 | const newEnv = { 111 | HOME: homeDir.toString(), 112 | XDG_CONFIG_HOME: homeDir.toString(), 113 | DENO_INSTALL: installDir.toString(), 114 | PATH: searchPaths.values().toArray().join(":"), 115 | ZDOTDIR: homeDir.toString(), 116 | SHELL: "/bin/bash", 117 | CI: "", 118 | }; 119 | Object.assign(newEnv, env); 120 | return new TestEnv(tempDir, homeDir, installDir, installScript, newEnv); 121 | } 122 | async [Symbol.asyncDispose]() { 123 | await this.#tempDir.remove({ recursive: true }); 124 | } 125 | [Symbol.dispose]() { 126 | this.#tempDir.removeSync({ recursive: true }); 127 | } 128 | } 129 | 130 | async function runInBash( 131 | commands: string[], 132 | options: { cwd?: Path; env: Record }, 133 | ): Promise { 134 | const { cwd, env } = options; 135 | const bash = await $.which("bash") ?? "bash"; 136 | const pty = new Pty({ 137 | env: Object.entries(env), 138 | cmd: bash, 139 | args: [], 140 | }); 141 | if (cwd) { 142 | await pty.write(`cd "${cwd.toString()}"\n`); 143 | } 144 | 145 | for (const command of commands) { 146 | await pty.write(command + "\n"); 147 | } 148 | await pty.write("exit\n"); 149 | let output = ""; 150 | while (true) { 151 | const { data, done } = await pty.read(); 152 | output += data; 153 | if (done) { 154 | break; 155 | } 156 | } 157 | pty.close(); 158 | return output; 159 | } 160 | 161 | function resolve(s: string): URL { 162 | return new URL(import.meta.resolve(s)); 163 | } 164 | -------------------------------------------------------------------------------- /shell-setup/deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "specifiers": { 4 | "jsr:@david/console-static-text@0.3": "0.3.0", 5 | "jsr:@david/dax@0.44": "0.44.1", 6 | "jsr:@david/path@0.2": "0.2.0", 7 | "jsr:@david/which@~0.4.1": "0.4.1", 8 | "jsr:@denosaurs/plug@1.0.5": "1.0.5", 9 | "jsr:@nathanwhit/promptly@~0.1.2": "0.1.2", 10 | "jsr:@sigma/pty-ffi@0.26": "0.26.4", 11 | "jsr:@std/assert@0.214": "0.214.0", 12 | "jsr:@std/assert@^1.0.8": "1.0.16", 13 | "jsr:@std/bytes@^1.0.5": "1.0.6", 14 | "jsr:@std/cli@^1.0.6": "1.0.24", 15 | "jsr:@std/encoding@0.214": "0.214.0", 16 | "jsr:@std/fmt@0.214": "0.214.0", 17 | "jsr:@std/fmt@1": "1.0.8", 18 | "jsr:@std/fmt@^1.0.2": "1.0.8", 19 | "jsr:@std/fs@0.214": "0.214.0", 20 | "jsr:@std/fs@1": "1.0.20", 21 | "jsr:@std/fs@^1.0.20": "1.0.20", 22 | "jsr:@std/internal@^1.0.12": "1.0.12", 23 | "jsr:@std/io@0.225": "0.225.2", 24 | "jsr:@std/path@0.214": "0.214.0", 25 | "jsr:@std/path@1": "1.1.3", 26 | "jsr:@std/path@^1.0.4": "1.1.3", 27 | "jsr:@std/path@^1.1.3": "1.1.3", 28 | "npm:@types/node@*": "24.10.2" 29 | }, 30 | "jsr": { 31 | "@david/console-static-text@0.3.0": { 32 | "integrity": "2dfb46ecee525755f7989f94ece30bba85bd8ffe3e8666abc1bf926e1ee0698d" 33 | }, 34 | "@david/dax@0.44.1": { 35 | "integrity": "200cbb2f85d833e657acd50dfceb92aa6c5330a48dd36e85e4e9f6a3a457609c", 36 | "dependencies": [ 37 | "jsr:@david/console-static-text", 38 | "jsr:@david/path", 39 | "jsr:@david/which", 40 | "jsr:@std/fmt@1", 41 | "jsr:@std/fs@^1.0.20", 42 | "jsr:@std/io", 43 | "jsr:@std/path@1" 44 | ] 45 | }, 46 | "@david/path@0.2.0": { 47 | "integrity": "f2d7aa7f02ce5a55e27c09f9f1381794acb09d328f8d3c8a2e3ab3ffc294dccd", 48 | "dependencies": [ 49 | "jsr:@std/fs@1", 50 | "jsr:@std/path@1" 51 | ] 52 | }, 53 | "@david/which@0.4.1": { 54 | "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" 55 | }, 56 | "@denosaurs/plug@1.0.5": { 57 | "integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf", 58 | "dependencies": [ 59 | "jsr:@std/encoding", 60 | "jsr:@std/fmt@0.214", 61 | "jsr:@std/fs@0.214", 62 | "jsr:@std/path@0.214" 63 | ] 64 | }, 65 | "@nathanwhit/promptly@0.1.2": { 66 | "integrity": "f434ebd37b103e2b9c5569578fb531c855c39980d1b186c0f508aaefe4060d06", 67 | "dependencies": [ 68 | "jsr:@std/fmt@^1.0.2" 69 | ] 70 | }, 71 | "@sigma/pty-ffi@0.26.4": { 72 | "integrity": "94c7e81bed356a166591a9b4aa79b5e39b430270b97a5647575d84d39094c6f9", 73 | "dependencies": [ 74 | "jsr:@denosaurs/plug" 75 | ] 76 | }, 77 | "@std/assert@0.214.0": { 78 | "integrity": "55d398de76a9828fd3b1aa653f4dba3eee4c6985d90c514865d2be9bd082b140" 79 | }, 80 | "@std/assert@1.0.16": { 81 | "integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532", 82 | "dependencies": [ 83 | "jsr:@std/internal" 84 | ] 85 | }, 86 | "@std/bytes@1.0.6": { 87 | "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" 88 | }, 89 | "@std/cli@1.0.24": { 90 | "integrity": "b655a5beb26aa94f98add6bc8889f5fb9bc3ee2cc3fc954e151201f4c4200a5e", 91 | "dependencies": [ 92 | "jsr:@std/internal" 93 | ] 94 | }, 95 | "@std/encoding@0.214.0": { 96 | "integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5" 97 | }, 98 | "@std/fmt@0.214.0": { 99 | "integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4" 100 | }, 101 | "@std/fmt@1.0.8": { 102 | "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 103 | }, 104 | "@std/fs@0.214.0": { 105 | "integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea", 106 | "dependencies": [ 107 | "jsr:@std/assert@0.214", 108 | "jsr:@std/path@0.214" 109 | ] 110 | }, 111 | "@std/fs@1.0.20": { 112 | "integrity": "e953206aae48d46ee65e8783ded459f23bec7dd1f3879512911c35e5484ea187", 113 | "dependencies": [ 114 | "jsr:@std/internal", 115 | "jsr:@std/path@^1.1.3" 116 | ] 117 | }, 118 | "@std/internal@1.0.12": { 119 | "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 120 | }, 121 | "@std/io@0.225.2": { 122 | "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7", 123 | "dependencies": [ 124 | "jsr:@std/bytes" 125 | ] 126 | }, 127 | "@std/path@0.214.0": { 128 | "integrity": "d5577c0b8d66f7e8e3586d864ebdf178bb326145a3611da5a51c961740300285", 129 | "dependencies": [ 130 | "jsr:@std/assert@0.214" 131 | ] 132 | }, 133 | "@std/path@1.1.3": { 134 | "integrity": "b015962d82a5e6daea980c32b82d2c40142149639968549c649031a230b1afb3", 135 | "dependencies": [ 136 | "jsr:@std/internal" 137 | ] 138 | } 139 | }, 140 | "npm": { 141 | "@types/node@24.10.2": { 142 | "integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==", 143 | "dependencies": [ 144 | "undici-types" 145 | ] 146 | }, 147 | "undici-types@7.16.0": { 148 | "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" 149 | } 150 | }, 151 | "workspace": { 152 | "dependencies": [ 153 | "jsr:@david/dax@0.44", 154 | "jsr:@sigma/pty-ffi@0.26", 155 | "jsr:@std/assert@^1.0.8" 156 | ], 157 | "members": { 158 | "shell-setup": { 159 | "dependencies": [ 160 | "jsr:@david/which@~0.4.1", 161 | "jsr:@nathanwhit/promptly@~0.1.2", 162 | "jsr:@std/cli@^1.0.6", 163 | "jsr:@std/path@^1.0.4", 164 | "npm:@types/node@*" 165 | ] 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /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/main.ts: -------------------------------------------------------------------------------- 1 | import { environment } from "./environment.ts"; 2 | import { 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 | Zsh, 15 | } from "./shell.ts"; 16 | import { ensureExists, warn } from "./util.ts"; 17 | import { RcBackups, updateRcFile } from "./rc_files.ts"; 18 | const { 19 | readTextFile, 20 | runCmd, 21 | writeTextFile, 22 | } = environment; 23 | 24 | type CompletionWriteResult = "fail" | "success" | null; 25 | 26 | /** Write completion files to the appropriate locations for all supported shells */ 27 | async function writeCompletionFiles( 28 | availableShells: UnixShell[], 29 | ): Promise { 30 | const written = new Set(); 31 | const results: CompletionWriteResult[] = []; 32 | 33 | const decoder = new TextDecoder(); 34 | 35 | for (const shell of availableShells) { 36 | if (!shell.supportsCompletion) { 37 | results.push(null); 38 | continue; 39 | } 40 | 41 | try { 42 | const completionFilePath = await shell.completionsFilePath?.(); 43 | if (!completionFilePath) { 44 | results.push(null); 45 | continue; 46 | } 47 | await ensureExists(dirname(completionFilePath)); 48 | // deno completions 49 | const output = await runCmd(Deno.execPath(), ["completions", shell.name]); 50 | if (!output.success) { 51 | throw new Error( 52 | `deno completions subcommand failed, stderr was: ${ 53 | decoder.decode(output.stderr) 54 | }`, 55 | ); 56 | } 57 | const completionFileContents = decoder.decode(output.stdout); 58 | if (!completionFileContents) { 59 | warn(`Completions were empty, skipping ${shell.name}`); 60 | results.push("fail"); 61 | continue; 62 | } 63 | let currentContents = null; 64 | try { 65 | currentContents = await readTextFile(completionFilePath); 66 | } catch (error) { 67 | if (!(error instanceof Deno.errors.NotFound)) { 68 | throw error; 69 | } else { 70 | // nothing 71 | } 72 | } 73 | if (currentContents !== completionFileContents) { 74 | if (currentContents !== null) { 75 | warn( 76 | `an existing completion file for deno already exists at ${completionFilePath}, but is out of date. overwriting with new contents`, 77 | ); 78 | } 79 | await writeTextFile(completionFilePath, completionFileContents); 80 | } 81 | results.push("success"); 82 | written.add(completionFilePath); 83 | } catch (error) { 84 | warn(`Failed to install completions for ${shell.name}: ${error}`); 85 | results.push("fail"); 86 | continue; 87 | } 88 | } 89 | return results; 90 | } 91 | 92 | /** Write commands necessary to set up completions to shell rc files */ 93 | async function writeCompletionRcCommands( 94 | availableShells: UnixShell[], 95 | backups: RcBackups, 96 | ) { 97 | for (const shell of availableShells) { 98 | if (!shell.supportsCompletion) continue; 99 | 100 | const rcCmd = await shell.completionsSourceString?.(); 101 | if (!rcCmd) continue; 102 | 103 | for (const rc of await shell.rcsToUpdate()) { 104 | await updateRcFile(rc, rcCmd, backups); 105 | } 106 | } 107 | } 108 | 109 | /** Write the files setting up the PATH vars (and potentially others in the future) for all shells */ 110 | async function writeEnvFiles(availableShells: UnixShell[], installDir: string) { 111 | const written = new Array(); 112 | 113 | let i = 0; 114 | while (i < availableShells.length) { 115 | const shell = availableShells[i]; 116 | const script = (shell.envScript ?? shEnvScript)(installDir); 117 | 118 | if (!written.some((s) => s.equals(script))) { 119 | if (await script.write(installDir)) { 120 | written.push(script); 121 | } else { 122 | continue; 123 | } 124 | } 125 | 126 | i++; 127 | } 128 | } 129 | 130 | /** Write the commands necessary to source the env file (which sets up the path). 131 | * Up until this point, we have not modified any shell config files. 132 | */ 133 | async function addToPath( 134 | availableShells: UnixShell[], 135 | installDir: string, 136 | backups: RcBackups, 137 | ) { 138 | for (const shell of availableShells) { 139 | const sourceCmd = await (shell.sourceString ?? shSourceString)(installDir); 140 | 141 | for (const rc of await shell.rcsToUpdate()) { 142 | await updateRcFile(rc, sourceCmd, backups); 143 | } 144 | } 145 | } 146 | 147 | // Update this when adding support for a new shell 148 | const shells: UnixShell[] = [ 149 | new Posix(), 150 | new Bash(), 151 | new Zsh(), 152 | new Fish(), 153 | ]; 154 | 155 | async function getAvailableShells(): Promise { 156 | const present = []; 157 | for (const shell of shells) { 158 | try { 159 | if (await shell.exists()) { 160 | present.push(shell); 161 | } 162 | } catch (_e) { 163 | continue; 164 | } 165 | } 166 | return present; 167 | } 168 | 169 | interface SetupOpts { 170 | skipPrompts: boolean; 171 | noModifyPath: boolean; 172 | } 173 | 174 | async function setupShells( 175 | installDir: string, 176 | backupDir: string, 177 | opts: SetupOpts, 178 | ) { 179 | const { 180 | skipPrompts, 181 | noModifyPath, 182 | } = opts; 183 | const availableShells = await getAvailableShells(); 184 | 185 | await writeEnvFiles(availableShells, installDir); 186 | 187 | const backups = new RcBackups(backupDir); 188 | 189 | if ( 190 | (skipPrompts && !noModifyPath) || (!skipPrompts && 191 | await confirm(`Edit shell configs to add deno to the PATH?`, { 192 | default: true, 193 | })) 194 | ) { 195 | await ensureExists(backupDir); 196 | await addToPath(availableShells, installDir, backups); 197 | console.log( 198 | "\nDeno was added to the PATH.\nYou may need to restart your shell for it to become available.\n", 199 | ); 200 | } 201 | 202 | const shellsWithCompletion = availableShells.filter((s) => 203 | s.supportsCompletion !== false 204 | ); 205 | const selected = skipPrompts ? [] : await multiSelect( 206 | { 207 | message: `Set up completions?`, 208 | options: shellsWithCompletion.map((s) => { 209 | const maybeNotes = typeof s.supportsCompletion === "string" 210 | ? ` (${s.supportsCompletion})` 211 | : ""; 212 | return s.name + 213 | maybeNotes; 214 | }), 215 | }, 216 | ); 217 | const completionsToSetup = selected.map((idx) => shellsWithCompletion[idx]); 218 | 219 | if ( 220 | completionsToSetup.length > 0 221 | ) { 222 | await ensureExists(backupDir); 223 | const results = await writeCompletionFiles(completionsToSetup); 224 | await writeCompletionRcCommands( 225 | completionsToSetup.filter((_s, i) => results[i] !== "fail"), 226 | backups, 227 | ); 228 | } 229 | } 230 | 231 | function printHelp() { 232 | console.log(`\n 233 | Setup script for installing deno 234 | 235 | Options: 236 | -y, --yes 237 | Skip interactive prompts and accept defaults 238 | --no-modify-path 239 | Don't add deno to the PATH environment variable 240 | -h, --help 241 | Print help\n`); 242 | } 243 | 244 | async function main() { 245 | if (Deno.args.length === 0) { 246 | throw new Error( 247 | "Expected the deno install directory as the first argument", 248 | ); 249 | } 250 | 251 | const args = parseArgs(Deno.args.slice(1), { 252 | boolean: ["yes", "no-modify-path", "help"], 253 | alias: { 254 | "yes": "y", 255 | "help": "h", 256 | }, 257 | default: { 258 | yes: false, 259 | "no-modify-path": false, 260 | }, 261 | unknown: (arg: string) => { 262 | if (arg.startsWith("-")) { 263 | printHelp(); 264 | console.error(`Unknown flag ${arg}. Shell will not be configured`); 265 | Deno.exit(1); 266 | } 267 | return false; 268 | }, 269 | }); 270 | 271 | if (args.help) { 272 | printHelp(); 273 | return; 274 | } 275 | 276 | if ( 277 | Deno.build.os === "windows" || (!args.yes && !(Deno.stdin.isTerminal() && 278 | Deno.stdout.isTerminal())) 279 | ) { 280 | // the powershell script already handles setting up the path 281 | return; 282 | } 283 | 284 | const installDir = Deno.args[0].trim(); 285 | 286 | const backupDir = join(installDir, ".shellRcBackups"); 287 | 288 | try { 289 | await setupShells(installDir, backupDir, { 290 | skipPrompts: args.yes, 291 | noModifyPath: args["no-modify-path"], 292 | }); 293 | } catch (_e) { 294 | warn( 295 | `Failed to configure your shell environments, you may need to manually add deno to your PATH environment variable. 296 | 297 | Manually add the directory to your $HOME/.bashrc (or similar)": 298 | export DENO_INSTALL="${installDir}" 299 | export PATH="${installDir}/bin:$PATH"\n`, 300 | ); 301 | } 302 | } 303 | 304 | if (import.meta.main) { 305 | await main(); 306 | } 307 | -------------------------------------------------------------------------------- /shell-setup/src/test/common.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file require-await 2 | 3 | import { _environmentImpl, type Environment } from "../environment.ts"; 4 | 5 | const testNameRegex = /^##test##(.+)$/; 6 | 7 | type Callsite = { 8 | getFunctionName(): string | null; 9 | getFileName(): string | null; 10 | getLineNumber(): number; 11 | getColumnNumber(): number; 12 | }; 13 | 14 | function getCallsites(): Callsite[] { 15 | const old = (Error as Any).prepareStackTrace; 16 | let callsites: Any[] = []; 17 | (Error as Any).prepareStackTrace = (_: Error, stack: Any[]) => { 18 | callsites = stack; 19 | }; 20 | const err: { stack?: Any } = {}; 21 | Error.captureStackTrace(err); 22 | err.stack; 23 | (Error as Any).prepareStackTrace = old; 24 | callsites.shift(); 25 | return callsites; 26 | } 27 | 28 | function getTestName(): string { 29 | const callsites = getCallsites(); 30 | const index = callsites.findIndex((callsite) => { 31 | const functionName = callsite.getFunctionName(); 32 | return functionName !== null && testNameRegex.test(functionName); 33 | }); 34 | if (index === -1) { 35 | throw new Error("Could not find test name"); 36 | } 37 | const callsite = callsites[index]; 38 | const match = testNameRegex.exec(callsite.getFunctionName()!); 39 | if (match === null) { 40 | throw new Error("Could not find test name"); 41 | } 42 | return match[1]; 43 | } 44 | 45 | class PerTestStore { 46 | #store: Map; 47 | #constr: new () => T; 48 | 49 | constructor(constr: new () => T) { 50 | this.#store = new Map(); 51 | this.#constr = constr; 52 | } 53 | 54 | registerTest(testName: string) { 55 | this.#store.set(testName, new this.#constr()); 56 | return this.#store.get(testName)!; 57 | } 58 | 59 | currentTest(): T { 60 | const testName = getTestName(); 61 | if (!this.#store.has(testName)) { 62 | this.#store.set(testName, new this.#constr()); 63 | } 64 | return this.#store.get(testName)!; 65 | } 66 | } 67 | 68 | class InMemoryStore { 69 | #store: Map; 70 | 71 | constructor() { 72 | this.#store = new Map(); 73 | } 74 | 75 | get(path: string): string | undefined { 76 | return this.#store.get(path); 77 | } 78 | 79 | set(path: string, contents: string) { 80 | this.#store.set(path, contents); 81 | } 82 | 83 | has(path: string): boolean { 84 | return this.#store.has(path); 85 | } 86 | 87 | toString(): string { 88 | return JSON.stringify(Object.fromEntries(this.#store)); 89 | } 90 | } 91 | 92 | const fsFunctions = [ 93 | "isExistingDir", 94 | "isExistingFile", 95 | "mkdir", 96 | "pathExists", 97 | "readTextFile", 98 | "writeTextFile", 99 | ] as const; 100 | 101 | type FsFunctions = typeof fsFunctions[number]; 102 | 103 | // abstract class FsNode { 104 | // } 105 | 106 | type DirNode = { 107 | type: "dir"; 108 | entries: Map; 109 | }; 110 | 111 | function newDirNode(entries: Map = new Map()): DirNode { 112 | return { 113 | type: "dir", 114 | entries, 115 | }; 116 | } 117 | 118 | function newFileNode(contents: string = ""): FileNode { 119 | return { 120 | type: "file", 121 | contents, 122 | }; 123 | } 124 | 125 | type FileNode = { 126 | type: "file"; 127 | contents: string; 128 | }; 129 | 130 | type FsNode = DirNode | FileNode; 131 | 132 | function assertNever(never: never): never { 133 | throw new Error(`unreachable: ${never}`); 134 | } 135 | 136 | class InMemoryFs implements 137 | Pick< 138 | Environment, 139 | FsFunctions 140 | > { 141 | root: DirNode; 142 | 143 | constructor() { 144 | this.root = newDirNode(); 145 | this.mkdir("/test/home", { 146 | recursive: true, 147 | }); 148 | } 149 | 150 | [Symbol.dispose]() { 151 | this.reset(); 152 | } 153 | 154 | reset() { 155 | this.root = newDirNode(); 156 | this.mkdir("/test/home", { 157 | recursive: true, 158 | }); 159 | } 160 | 161 | #findNode(path: string): FsNode | undefined { 162 | const parts = path.replace(/\/$/, "").replace(/^\//, "").split("/"); 163 | let current: FsNode = this.root; 164 | for (const part of parts) { 165 | if (current.type !== "dir") { 166 | return undefined; 167 | } 168 | const node = current.entries.get(part); 169 | if (!node) { 170 | return undefined; 171 | } 172 | current = node; 173 | } 174 | return current; 175 | } 176 | 177 | #findFileOrParentDir(path: string): FsNode | undefined { 178 | const parts = path.replace(/\/$/, "").replace(/^\//, "").split("/"); 179 | let current: FsNode = this.root; 180 | for (const part of parts) { 181 | if (current.type !== "dir") { 182 | return undefined; 183 | } 184 | const node = current.entries.get(part); 185 | if (!node) { 186 | return current; 187 | } 188 | current = node; 189 | } 190 | if (current.type === "dir") { 191 | throw new Deno.errors.IsADirectory(); 192 | } 193 | return current; 194 | } 195 | 196 | exists(path: string): boolean { 197 | return this.#findNode(path) !== undefined; 198 | } 199 | 200 | async isExistingFile(path: string): Promise { 201 | return this.#findNode(path)?.type === "file"; 202 | } 203 | 204 | async isExistingDir(path: string): Promise { 205 | return this.#findNode(path)?.type === "dir"; 206 | } 207 | 208 | fileInfo(path: string): { isFile: boolean; isDirectory: boolean } { 209 | const node = this.#findNode(path); 210 | let isFile = false, isDirectory = false; 211 | switch (node?.type) { 212 | case "dir": { 213 | isDirectory = true; 214 | break; 215 | } 216 | case "file": { 217 | isFile = true; 218 | break; 219 | } 220 | case undefined: { 221 | throw new Deno.errors.NotFound(); 222 | } 223 | default: { 224 | assertNever(node); 225 | } 226 | } 227 | return { isFile: isFile, isDirectory: isDirectory }; 228 | } 229 | 230 | async readTextFile( 231 | path: string | URL, 232 | _options?: Deno.ReadFileOptions, 233 | ): Promise { 234 | const node = this.#findNode(path.toString()); 235 | if (node?.type !== "file") { 236 | throw new Deno.errors.NotFound(); 237 | } 238 | return node.contents; 239 | } 240 | 241 | async writeTextFile( 242 | path: string | URL, 243 | contents: string | ReadableStream, 244 | options?: Deno.WriteFileOptions, 245 | ): Promise { 246 | const { 247 | append = false, 248 | create = true, 249 | createNew = false, 250 | } = options ?? {}; 251 | const node = this.#findFileOrParentDir(path.toString()); 252 | if (createNew && node?.type === "file") { 253 | throw new Deno.errors.AlreadyExists(); 254 | } 255 | if (!create && node?.type === "dir") { 256 | throw new Deno.errors.NotFound(); 257 | } 258 | if (!node) { 259 | throw new Deno.errors.NotFound(); 260 | } 261 | 262 | let fileNode: FsNode; 263 | if (node.type === "dir") { 264 | fileNode = newFileNode(""); 265 | node.entries.set(path.toString().split("/").pop()!, fileNode); 266 | } else { 267 | fileNode = node; 268 | } 269 | let newContents = ""; 270 | if (typeof contents === "string") { 271 | newContents = append ? fileNode.contents + contents : contents; 272 | } else { 273 | for await (const chunk of contents) { 274 | newContents += chunk; 275 | } 276 | } 277 | fileNode.contents = newContents; 278 | } 279 | 280 | async pathExists(path: string): Promise { 281 | return this.exists(path); 282 | } 283 | 284 | async mkdir(path: string | URL, options?: Deno.MkdirOptions): Promise { 285 | const { recursive = false } = options ?? {}; 286 | path = path.toString().replace(/\/$/, "").replace(/^\//, ""); 287 | const parts = path.split("/"); 288 | let current: FsNode = this.root; 289 | for (let i = 0; i < parts.length; i++) { 290 | const part = parts[i]; 291 | if (current.type !== "dir") { 292 | throw new Deno.errors.NotADirectory(); 293 | } 294 | if (!current.entries.has(part)) { 295 | if (i === parts.length - 1 || recursive) { 296 | current.entries.set(part, newDirNode()); 297 | } else { 298 | throw new Deno.errors.NotFound(); 299 | } 300 | } 301 | current = current.entries.get(part)!; 302 | } 303 | } 304 | 305 | tree(): string { 306 | let output = "/\n"; 307 | const walk = (node: FsNode, indent: string) => { 308 | if (node.type === "dir") { 309 | for (const [name, child] of node.entries) { 310 | console.log("child", name, child); 311 | if (child.type === "dir") { 312 | output += `${indent}${name}/\n`; 313 | walk(child, indent + " "); 314 | } else { 315 | output += `${indent}${name}\n`; 316 | } 317 | } 318 | } 319 | }; 320 | walk(this.root, " "); 321 | return output.trimEnd(); 322 | } 323 | } 324 | 325 | class FileStore extends PerTestStore { 326 | constructor() { 327 | super(InMemoryFs); 328 | } 329 | } 330 | 331 | class EnvStore extends PerTestStore { 332 | constructor() { 333 | super(InMemoryStore); 334 | } 335 | } 336 | 337 | const mockEnvironment = ( 338 | fileStore: FileStore, 339 | envVars: EnvStore, 340 | ): Environment => { 341 | const fsMocksSetup: Partial> = {}; 342 | for (const fn of fsFunctions) { 343 | (fsMocksSetup as Any)[fn] = async (...args: unknown[]) => { 344 | return await ((fileStore.currentTest()[fn] as Any)(...args)); 345 | }; 346 | } 347 | const fsMocks = fsMocksSetup as Pick; 348 | return { 349 | ...fsMocks, 350 | homeDir: "/test/home", 351 | async findCmd(command: string) { 352 | return await which(command, { 353 | stat: async (path) => { 354 | return fileStore.currentTest().fileInfo(path); 355 | }, 356 | env: (name) => envVars.currentTest().get(name), 357 | os: Deno.build.os, 358 | }); 359 | }, 360 | runCmd(_cmd: string, _args?: string[]): Promise { 361 | throw new Error("Not implemented"); 362 | }, 363 | getEnv(name: string): string | undefined { 364 | return envVars.currentTest().get(name); 365 | }, 366 | }; 367 | }; 368 | 369 | function setupMockEnvironment() { 370 | const fileStore = new FileStore(); 371 | const envVars = new EnvStore(); 372 | const env = mockEnvironment(fileStore, envVars); 373 | Object.assign(_environmentImpl, env); 374 | return { fileStore, envVars }; 375 | } 376 | 377 | const globalTestEnv = setupMockEnvironment(); 378 | 379 | import { which } from "@david/which"; 380 | 381 | // deno-lint-ignore no-explicit-any 382 | type Any = any; 383 | 384 | const allTestNames = new Set(); 385 | 386 | export function test( 387 | name: string, 388 | body: (testEnv: { 389 | fileStore: InMemoryFs; 390 | envVars: InMemoryStore; 391 | }) => void | Promise, 392 | ) { 393 | if (allTestNames.has(name)) { 394 | throw new Error( 395 | `Duplicate test name already exists: ${name}. Choose a unique name.`, 396 | ); 397 | } 398 | allTestNames.add(name); 399 | 400 | const fullTestBody = async () => { 401 | await body({ 402 | fileStore: globalTestEnv.fileStore.registerTest(name), 403 | envVars: globalTestEnv.envVars.registerTest(name), 404 | }); 405 | }; 406 | Object.defineProperty(fullTestBody, "name", { value: name }); 407 | Object.defineProperty(body, "name", { value: `##test##${name}` }); 408 | Deno.test(name, fullTestBody); 409 | } 410 | -------------------------------------------------------------------------------- /shell-setup/bundled.esm.js: -------------------------------------------------------------------------------- 1 | // deno: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({ 93 | name: "read", 94 | path 95 | })).state == "granted") { 96 | return; 97 | } 98 | throw error; 99 | } 100 | } 101 | var _environmentImpl = { 102 | writeTextFile: Deno.writeTextFile, 103 | readTextFile: Deno.readTextFile, 104 | async isExistingFile(path) { 105 | const info2 = await tryStat(path); 106 | return info2?.isFile ?? false; 107 | }, 108 | async isExistingDir(path) { 109 | const info2 = await tryStat(path); 110 | return info2?.isDirectory ?? false; 111 | }, 112 | async pathExists(path) { 113 | const info2 = await tryStat(path); 114 | return info2 !== void 0; 115 | }, 116 | mkdir: Deno.mkdir, 117 | homeDir: getHomeDir(), 118 | findCmd: which, 119 | getEnv(name) { 120 | return Deno.env.get(name); 121 | }, 122 | async runCmd(cmd, args) { 123 | return await new Deno.Command(cmd, { 124 | args, 125 | stderr: "piped", 126 | stdout: "piped", 127 | stdin: "null" 128 | }).output(); 129 | } 130 | }; 131 | function makeWrapper() { 132 | const wrapperEnv = {}; 133 | for (const keyString in _environmentImpl) { 134 | const key = keyString; 135 | if (typeof _environmentImpl[key] === "function") { 136 | wrapperEnv[key] = function(...args) { 137 | return _environmentImpl[key](...args); 138 | }; 139 | } 140 | } 141 | Object.defineProperty(wrapperEnv, "homeDir", { 142 | get: () => _environmentImpl.homeDir 143 | }); 144 | return wrapperEnv; 145 | } 146 | var environment = makeWrapper(); 147 | 148 | // deno:https://jsr.io/@std/internal/1.0.12/_os.ts 149 | function checkWindows() { 150 | const global = globalThis; 151 | const os = global.Deno?.build?.os; 152 | return typeof os === "string" ? os === "windows" : global.navigator?.platform?.startsWith("Win") ?? global.process?.platform?.startsWith("win") ?? false; 153 | } 154 | 155 | // deno:https://jsr.io/@std/internal/1.0.12/os.ts 156 | var isWindows = checkWindows(); 157 | 158 | // deno:https://jsr.io/@std/path/1.1.3/_common/assert_path.ts 159 | function assertPath(path) { 160 | if (typeof path !== "string") { 161 | throw new TypeError(`Path must be a string, received "${JSON.stringify(path)}"`); 162 | } 163 | } 164 | 165 | // deno:https://jsr.io/@std/path/1.1.3/_common/basename.ts 166 | function stripSuffix(name, suffix) { 167 | if (suffix.length >= name.length) { 168 | return name; 169 | } 170 | const lenDiff = name.length - suffix.length; 171 | for (let i = suffix.length - 1; i >= 0; --i) { 172 | if (name.charCodeAt(lenDiff + i) !== suffix.charCodeAt(i)) { 173 | return name; 174 | } 175 | } 176 | return name.slice(0, -suffix.length); 177 | } 178 | function lastPathSegment(path, isSep, start = 0) { 179 | let matchedNonSeparator = false; 180 | let end = path.length; 181 | for (let i = path.length - 1; i >= start; --i) { 182 | if (isSep(path.charCodeAt(i))) { 183 | if (matchedNonSeparator) { 184 | start = i + 1; 185 | break; 186 | } 187 | } else if (!matchedNonSeparator) { 188 | matchedNonSeparator = true; 189 | end = i + 1; 190 | } 191 | } 192 | return path.slice(start, end); 193 | } 194 | function assertArgs(path, suffix) { 195 | assertPath(path); 196 | if (path.length === 0) return path; 197 | if (typeof suffix !== "string") { 198 | throw new TypeError(`Suffix must be a string, received "${JSON.stringify(suffix)}"`); 199 | } 200 | } 201 | 202 | // deno:https://jsr.io/@std/path/1.1.3/_common/from_file_url.ts 203 | function assertArg(url) { 204 | url = url instanceof URL ? url : new URL(url); 205 | if (url.protocol !== "file:") { 206 | throw new TypeError(`URL must be a file URL: received "${url.protocol}"`); 207 | } 208 | return url; 209 | } 210 | 211 | // deno:https://jsr.io/@std/path/1.1.3/posix/from_file_url.ts 212 | function fromFileUrl(url) { 213 | url = assertArg(url); 214 | return decodeURIComponent(url.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, "%25")); 215 | } 216 | 217 | // deno:https://jsr.io/@std/path/1.1.3/_common/strip_trailing_separators.ts 218 | function stripTrailingSeparators(segment, isSep) { 219 | if (segment.length <= 1) { 220 | return segment; 221 | } 222 | let end = segment.length; 223 | for (let i = segment.length - 1; i > 0; i--) { 224 | if (isSep(segment.charCodeAt(i))) { 225 | end = i; 226 | } else { 227 | break; 228 | } 229 | } 230 | return segment.slice(0, end); 231 | } 232 | 233 | // deno:https://jsr.io/@std/path/1.1.3/_common/constants.ts 234 | var CHAR_UPPERCASE_A = 65; 235 | var CHAR_LOWERCASE_A = 97; 236 | var CHAR_UPPERCASE_Z = 90; 237 | var CHAR_LOWERCASE_Z = 122; 238 | var CHAR_DOT = 46; 239 | var CHAR_FORWARD_SLASH = 47; 240 | var CHAR_BACKWARD_SLASH = 92; 241 | var CHAR_COLON = 58; 242 | 243 | // deno:https://jsr.io/@std/path/1.1.3/posix/_util.ts 244 | function isPosixPathSeparator(code2) { 245 | return code2 === CHAR_FORWARD_SLASH; 246 | } 247 | 248 | // deno:https://jsr.io/@std/path/1.1.3/posix/basename.ts 249 | function basename(path, suffix = "") { 250 | if (path instanceof URL) { 251 | path = fromFileUrl(path); 252 | } 253 | assertArgs(path, suffix); 254 | const lastSegment = lastPathSegment(path, isPosixPathSeparator); 255 | const strippedSegment = stripTrailingSeparators(lastSegment, isPosixPathSeparator); 256 | return suffix ? stripSuffix(strippedSegment, suffix) : strippedSegment; 257 | } 258 | 259 | // deno:https://jsr.io/@std/path/1.1.3/windows/_util.ts 260 | function isPosixPathSeparator2(code2) { 261 | return code2 === CHAR_FORWARD_SLASH; 262 | } 263 | function isPathSeparator(code2) { 264 | return code2 === CHAR_FORWARD_SLASH || code2 === CHAR_BACKWARD_SLASH; 265 | } 266 | function isWindowsDeviceRoot(code2) { 267 | return code2 >= CHAR_LOWERCASE_A && code2 <= CHAR_LOWERCASE_Z || code2 >= CHAR_UPPERCASE_A && code2 <= CHAR_UPPERCASE_Z; 268 | } 269 | 270 | // deno:https://jsr.io/@std/path/1.1.3/windows/from_file_url.ts 271 | function fromFileUrl2(url) { 272 | url = assertArg(url); 273 | let path = decodeURIComponent(url.pathname.replace(/\//g, "\\").replace(/%(?![0-9A-Fa-f]{2})/g, "%25")).replace(/^\\*([A-Za-z]:)(\\|$)/, "$1\\"); 274 | if (url.hostname !== "") { 275 | path = `\\\\${url.hostname}${path}`; 276 | } 277 | return path; 278 | } 279 | 280 | // deno:https://jsr.io/@std/path/1.1.3/windows/basename.ts 281 | function basename2(path, suffix = "") { 282 | if (path instanceof URL) { 283 | path = fromFileUrl2(path); 284 | } 285 | assertArgs(path, suffix); 286 | let start = 0; 287 | if (path.length >= 2) { 288 | const drive = path.charCodeAt(0); 289 | if (isWindowsDeviceRoot(drive)) { 290 | if (path.charCodeAt(1) === CHAR_COLON) start = 2; 291 | } 292 | } 293 | const lastSegment = lastPathSegment(path, isPathSeparator, start); 294 | const strippedSegment = stripTrailingSeparators(lastSegment, isPathSeparator); 295 | return suffix ? stripSuffix(strippedSegment, suffix) : strippedSegment; 296 | } 297 | 298 | // deno:https://jsr.io/@std/path/1.1.3/basename.ts 299 | function basename3(path, suffix = "") { 300 | return isWindows ? basename2(path, suffix) : basename(path, suffix); 301 | } 302 | 303 | // deno:https://jsr.io/@std/path/1.1.3/_common/dirname.ts 304 | function assertArg2(path) { 305 | assertPath(path); 306 | if (path.length === 0) return "."; 307 | } 308 | 309 | // deno:https://jsr.io/@std/path/1.1.3/posix/dirname.ts 310 | function dirname(path) { 311 | if (path instanceof URL) { 312 | path = fromFileUrl(path); 313 | } 314 | assertArg2(path); 315 | let end = -1; 316 | let matchedNonSeparator = false; 317 | for (let i = path.length - 1; i >= 1; --i) { 318 | if (isPosixPathSeparator(path.charCodeAt(i))) { 319 | if (matchedNonSeparator) { 320 | end = i; 321 | break; 322 | } 323 | } else { 324 | matchedNonSeparator = true; 325 | } 326 | } 327 | if (end === -1) { 328 | return isPosixPathSeparator(path.charCodeAt(0)) ? "/" : "."; 329 | } 330 | return stripTrailingSeparators(path.slice(0, end), isPosixPathSeparator); 331 | } 332 | 333 | // deno:https://jsr.io/@std/path/1.1.3/windows/dirname.ts 334 | function dirname2(path) { 335 | if (path instanceof URL) { 336 | path = fromFileUrl2(path); 337 | } 338 | assertArg2(path); 339 | const len = path.length; 340 | let rootEnd = -1; 341 | let end = -1; 342 | let matchedSlash = true; 343 | let offset = 0; 344 | const code2 = path.charCodeAt(0); 345 | if (len > 1) { 346 | if (isPathSeparator(code2)) { 347 | rootEnd = offset = 1; 348 | if (isPathSeparator(path.charCodeAt(1))) { 349 | let j = 2; 350 | let last = j; 351 | for (; j < len; ++j) { 352 | if (isPathSeparator(path.charCodeAt(j))) break; 353 | } 354 | if (j < len && j !== last) { 355 | last = j; 356 | for (; j < len; ++j) { 357 | if (!isPathSeparator(path.charCodeAt(j))) break; 358 | } 359 | if (j < len && j !== last) { 360 | last = j; 361 | for (; j < len; ++j) { 362 | if (isPathSeparator(path.charCodeAt(j))) break; 363 | } 364 | if (j === len) { 365 | return path; 366 | } 367 | if (j !== last) { 368 | rootEnd = offset = j + 1; 369 | } 370 | } 371 | } 372 | } 373 | } else if (isWindowsDeviceRoot(code2)) { 374 | if (path.charCodeAt(1) === CHAR_COLON) { 375 | rootEnd = offset = 2; 376 | if (len > 2) { 377 | if (isPathSeparator(path.charCodeAt(2))) rootEnd = offset = 3; 378 | } 379 | } 380 | } 381 | } else if (isPathSeparator(code2)) { 382 | return path; 383 | } 384 | for (let i = len - 1; i >= offset; --i) { 385 | if (isPathSeparator(path.charCodeAt(i))) { 386 | if (!matchedSlash) { 387 | end = i; 388 | break; 389 | } 390 | } else { 391 | matchedSlash = false; 392 | } 393 | } 394 | if (end === -1) { 395 | if (rootEnd === -1) return "."; 396 | else end = rootEnd; 397 | } 398 | return stripTrailingSeparators(path.slice(0, end), isPosixPathSeparator2); 399 | } 400 | 401 | // deno:https://jsr.io/@std/path/1.1.3/dirname.ts 402 | function dirname3(path) { 403 | return isWindows ? dirname2(path) : dirname(path); 404 | } 405 | 406 | // deno:https://jsr.io/@std/path/1.1.3/_common/normalize.ts 407 | function assertArg4(path) { 408 | assertPath(path); 409 | if (path.length === 0) return "."; 410 | } 411 | 412 | // deno:https://jsr.io/@std/path/1.1.3/_common/normalize_string.ts 413 | function normalizeString(path, allowAboveRoot, separator, isPathSeparator2) { 414 | let res = ""; 415 | let lastSegmentLength = 0; 416 | let lastSlash = -1; 417 | let dots = 0; 418 | let code2; 419 | for (let i = 0; i <= path.length; ++i) { 420 | if (i < path.length) code2 = path.charCodeAt(i); 421 | else if (isPathSeparator2(code2)) break; 422 | else code2 = CHAR_FORWARD_SLASH; 423 | if (isPathSeparator2(code2)) { 424 | if (lastSlash === i - 1 || dots === 1) { 425 | } else if (lastSlash !== i - 1 && dots === 2) { 426 | if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== CHAR_DOT || res.charCodeAt(res.length - 2) !== CHAR_DOT) { 427 | if (res.length > 2) { 428 | const lastSlashIndex = res.lastIndexOf(separator); 429 | if (lastSlashIndex === -1) { 430 | res = ""; 431 | lastSegmentLength = 0; 432 | } else { 433 | res = res.slice(0, lastSlashIndex); 434 | lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); 435 | } 436 | lastSlash = i; 437 | dots = 0; 438 | continue; 439 | } else if (res.length === 2 || res.length === 1) { 440 | res = ""; 441 | lastSegmentLength = 0; 442 | lastSlash = i; 443 | dots = 0; 444 | continue; 445 | } 446 | } 447 | if (allowAboveRoot) { 448 | if (res.length > 0) res += `${separator}..`; 449 | else res = ".."; 450 | lastSegmentLength = 2; 451 | } 452 | } else { 453 | if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); 454 | else res = path.slice(lastSlash + 1, i); 455 | lastSegmentLength = i - lastSlash - 1; 456 | } 457 | lastSlash = i; 458 | dots = 0; 459 | } else if (code2 === CHAR_DOT && dots !== -1) { 460 | ++dots; 461 | } else { 462 | dots = -1; 463 | } 464 | } 465 | return res; 466 | } 467 | 468 | // deno:https://jsr.io/@std/path/1.1.3/posix/normalize.ts 469 | function normalize(path) { 470 | if (path instanceof URL) { 471 | path = fromFileUrl(path); 472 | } 473 | assertArg4(path); 474 | const isAbsolute3 = isPosixPathSeparator(path.charCodeAt(0)); 475 | const trailingSeparator = isPosixPathSeparator(path.charCodeAt(path.length - 1)); 476 | path = normalizeString(path, !isAbsolute3, "/", isPosixPathSeparator); 477 | if (path.length === 0 && !isAbsolute3) path = "."; 478 | if (path.length > 0 && trailingSeparator) path += "/"; 479 | if (isAbsolute3) return `/${path}`; 480 | return path; 481 | } 482 | 483 | // deno:https://jsr.io/@std/path/1.1.3/posix/join.ts 484 | function join(path, ...paths) { 485 | if (path === void 0) return "."; 486 | if (path instanceof URL) { 487 | path = fromFileUrl(path); 488 | } 489 | paths = path ? [ 490 | path, 491 | ...paths 492 | ] : paths; 493 | paths.forEach((path2) => assertPath(path2)); 494 | const joined = paths.filter((path2) => path2.length > 0).join("/"); 495 | return joined === "" ? "." : normalize(joined); 496 | } 497 | 498 | // deno:https://jsr.io/@std/path/1.1.3/windows/normalize.ts 499 | function normalize2(path) { 500 | if (path instanceof URL) { 501 | path = fromFileUrl2(path); 502 | } 503 | assertArg4(path); 504 | const len = path.length; 505 | let rootEnd = 0; 506 | let device; 507 | let isAbsolute3 = false; 508 | const code2 = path.charCodeAt(0); 509 | if (len > 1) { 510 | if (isPathSeparator(code2)) { 511 | isAbsolute3 = true; 512 | if (isPathSeparator(path.charCodeAt(1))) { 513 | let j = 2; 514 | let last = j; 515 | for (; j < len; ++j) { 516 | if (isPathSeparator(path.charCodeAt(j))) break; 517 | } 518 | if (j < len && j !== last) { 519 | const firstPart = path.slice(last, j); 520 | last = j; 521 | for (; j < len; ++j) { 522 | if (!isPathSeparator(path.charCodeAt(j))) break; 523 | } 524 | if (j < len && j !== last) { 525 | last = j; 526 | for (; j < len; ++j) { 527 | if (isPathSeparator(path.charCodeAt(j))) break; 528 | } 529 | if (j === len) { 530 | return `\\\\${firstPart}\\${path.slice(last)}\\`; 531 | } else if (j !== last) { 532 | device = `\\\\${firstPart}\\${path.slice(last, j)}`; 533 | rootEnd = j; 534 | } 535 | } 536 | } 537 | } else { 538 | rootEnd = 1; 539 | } 540 | } else if (isWindowsDeviceRoot(code2)) { 541 | if (path.charCodeAt(1) === CHAR_COLON) { 542 | device = path.slice(0, 2); 543 | rootEnd = 2; 544 | if (len > 2) { 545 | if (isPathSeparator(path.charCodeAt(2))) { 546 | isAbsolute3 = true; 547 | rootEnd = 3; 548 | } 549 | } 550 | } 551 | } 552 | } else if (isPathSeparator(code2)) { 553 | return "\\"; 554 | } 555 | let tail; 556 | if (rootEnd < len) { 557 | tail = normalizeString(path.slice(rootEnd), !isAbsolute3, "\\", isPathSeparator); 558 | } else { 559 | tail = ""; 560 | } 561 | if (tail.length === 0 && !isAbsolute3) tail = "."; 562 | if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) { 563 | tail += "\\"; 564 | } 565 | if (device === void 0) { 566 | if (isAbsolute3) { 567 | if (tail.length > 0) return `\\${tail}`; 568 | else return "\\"; 569 | } 570 | return tail; 571 | } else if (isAbsolute3) { 572 | if (tail.length > 0) return `${device}\\${tail}`; 573 | else return `${device}\\`; 574 | } 575 | return device + tail; 576 | } 577 | 578 | // deno:https://jsr.io/@std/path/1.1.3/windows/join.ts 579 | function join2(path, ...paths) { 580 | if (path instanceof URL) { 581 | path = fromFileUrl2(path); 582 | } 583 | paths = path ? [ 584 | path, 585 | ...paths 586 | ] : paths; 587 | paths.forEach((path2) => assertPath(path2)); 588 | paths = paths.filter((path2) => path2.length > 0); 589 | if (paths.length === 0) return "."; 590 | let needsReplace = true; 591 | let slashCount = 0; 592 | const firstPart = paths[0]; 593 | if (isPathSeparator(firstPart.charCodeAt(0))) { 594 | ++slashCount; 595 | const firstLen = firstPart.length; 596 | if (firstLen > 1) { 597 | if (isPathSeparator(firstPart.charCodeAt(1))) { 598 | ++slashCount; 599 | if (firstLen > 2) { 600 | if (isPathSeparator(firstPart.charCodeAt(2))) ++slashCount; 601 | else { 602 | needsReplace = false; 603 | } 604 | } 605 | } 606 | } 607 | } 608 | let joined = paths.join("\\"); 609 | if (needsReplace) { 610 | for (; slashCount < joined.length; ++slashCount) { 611 | if (!isPathSeparator(joined.charCodeAt(slashCount))) break; 612 | } 613 | if (slashCount >= 2) joined = `\\${joined.slice(slashCount)}`; 614 | } 615 | return normalize2(joined); 616 | } 617 | 618 | // deno:https://jsr.io/@std/path/1.1.3/join.ts 619 | function join3(path, ...paths) { 620 | return isWindows ? join2(path, ...paths) : join(path, ...paths); 621 | } 622 | 623 | // deno:https://jsr.io/@std/fmt/1.0.8/colors.ts 624 | var { Deno: Deno2 } = globalThis; 625 | var noColor = typeof Deno2?.noColor === "boolean" ? Deno2.noColor : false; 626 | var enabled = !noColor; 627 | function code(open, close) { 628 | return { 629 | open: `\x1B[${open.join(";")}m`, 630 | close: `\x1B[${close}m`, 631 | regexp: new RegExp(`\\x1b\\[${close}m`, "g") 632 | }; 633 | } 634 | function run(str, code2) { 635 | return enabled ? `${code2.open}${str.replace(code2.regexp, code2.open)}${code2.close}` : str; 636 | } 637 | function bold(str) { 638 | return run(str, code([ 639 | 1 640 | ], 22)); 641 | } 642 | function italic(str) { 643 | return run(str, code([ 644 | 3 645 | ], 23)); 646 | } 647 | function blue(str) { 648 | return run(str, code([ 649 | 34 650 | ], 39)); 651 | } 652 | var ANSI_PATTERN = new RegExp([ 653 | "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", 654 | "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TXZcf-nq-uy=><~]))" 655 | ].join("|"), "g"); 656 | function stripAnsiCode(string) { 657 | return string.replace(ANSI_PATTERN, ""); 658 | } 659 | 660 | // deno:https://jsr.io/@nathanwhit/promptly/0.1.2/mod.ts 661 | var encoder = new TextEncoder(); 662 | var decoder = new TextDecoder(); 663 | var Key = /* @__PURE__ */ function(Key2) { 664 | Key2[Key2["Up"] = 0] = "Up"; 665 | Key2[Key2["Down"] = 1] = "Down"; 666 | Key2[Key2["Left"] = 2] = "Left"; 667 | Key2[Key2["Right"] = 3] = "Right"; 668 | Key2[Key2["Enter"] = 4] = "Enter"; 669 | Key2[Key2["Space"] = 5] = "Space"; 670 | Key2[Key2["Backspace"] = 6] = "Backspace"; 671 | return Key2; 672 | }(Key || {}); 673 | async function* readKeys() { 674 | loop: while (true) { 675 | const buf = new Uint8Array(8); 676 | const byteCount = await Deno.stdin.read(buf); 677 | if (byteCount == null) { 678 | break; 679 | } else if (byteCount === 3) { 680 | if (buf[0] === 27 && buf[1] === 91) { 681 | switch (buf[2]) { 682 | // ESC[A -> cursor up 683 | case 65: 684 | yield Key.Up; 685 | continue; 686 | // ESC[B -> cursor down 687 | case 66: 688 | yield Key.Down; 689 | continue; 690 | // ESC[C -> cursor right 691 | case 67: 692 | yield Key.Right; 693 | continue; 694 | // ESC[D -> cursor left 695 | case 68: 696 | yield Key.Left; 697 | continue; 698 | } 699 | } 700 | } else if (byteCount === 1) { 701 | const c = buf[0]; 702 | switch (c) { 703 | case 3: 704 | break loop; 705 | case 13: 706 | yield Key.Enter; 707 | continue; 708 | case 32: 709 | yield Key.Space; 710 | continue; 711 | case 127: 712 | yield Key.Backspace; 713 | continue; 714 | } 715 | } 716 | const text = stripAnsiCode(decoder.decode(buf.subarray(0, byteCount ?? 0))); 717 | if (text.length > 0) { 718 | yield text; 719 | } 720 | } 721 | } 722 | function writeAll(writer, buf) { 723 | let pos = 0; 724 | while (pos < buf.byteLength) { 725 | pos += writer.writeSync(buf.subarray(pos)); 726 | } 727 | } 728 | var CursorDir = /* @__PURE__ */ function(CursorDir2) { 729 | CursorDir2[CursorDir2["Up"] = 0] = "Up"; 730 | CursorDir2[CursorDir2["Down"] = 1] = "Down"; 731 | CursorDir2[CursorDir2["Left"] = 2] = "Left"; 732 | CursorDir2[CursorDir2["Right"] = 3] = "Right"; 733 | CursorDir2[CursorDir2["Column"] = 4] = "Column"; 734 | return CursorDir2; 735 | }(CursorDir || {}); 736 | var charCodes = (...cs) => { 737 | const map = /* @__PURE__ */ Object.create(null); 738 | for (let i = 0; i < cs.length; i++) { 739 | const c = cs[i]; 740 | map[c.charAt(0)] = c.charCodeAt(0); 741 | } 742 | return map; 743 | }; 744 | function assertUnreachable(_x) { 745 | throw new Error("unreachable"); 746 | } 747 | var codes = charCodes("A", "B", "C", "D", "G", "0", "K"); 748 | function moveCursor(writer, dir, n) { 749 | const seq = [ 750 | 27, 751 | 91 752 | ]; 753 | if (n != void 0) { 754 | seq.push(...encoder.encode(n.toString())); 755 | } 756 | switch (dir) { 757 | case CursorDir.Up: 758 | seq.push(codes.A); 759 | break; 760 | case CursorDir.Down: 761 | seq.push(codes.B); 762 | break; 763 | case CursorDir.Left: 764 | seq.push(codes.D); 765 | break; 766 | case CursorDir.Right: 767 | seq.push(codes.C); 768 | break; 769 | case CursorDir.Column: 770 | seq.push(codes.G); 771 | break; 772 | default: 773 | assertUnreachable(dir); 774 | } 775 | const buf = new Uint8Array(seq); 776 | writeAll(writer, buf); 777 | } 778 | function eraseToEnd(writer) { 779 | writeAll(writer, new Uint8Array([ 780 | 27, 781 | 91, 782 | codes[0], 783 | codes.K 784 | ])); 785 | } 786 | function hideCursor(writer) { 787 | writeAll(writer, encoder.encode("\x1B[?25l")); 788 | } 789 | function showCursor(writer) { 790 | writeAll(writer, encoder.encode("\x1B[?25h")); 791 | } 792 | var lastPromise = Promise.resolve(); 793 | function ensureSingleSelection(action) { 794 | const currentLastPromise = lastPromise; 795 | const currentPromise = (async () => { 796 | try { 797 | await currentLastPromise; 798 | } catch { 799 | } 800 | hideCursor(Deno.stdout); 801 | try { 802 | Deno.stdin.setRaw(true); 803 | try { 804 | return await action(); 805 | } finally { 806 | Deno.stdin.setRaw(false); 807 | } 808 | } finally { 809 | showCursor(Deno.stdout); 810 | } 811 | })(); 812 | lastPromise = currentPromise; 813 | return currentPromise; 814 | } 815 | function clearRow(writer) { 816 | moveCursor(writer, CursorDir.Column); 817 | eraseToEnd(writer); 818 | } 819 | var row = 0; 820 | function writeLines(writer, lines) { 821 | while (row > 0) { 822 | clearRow(writer); 823 | moveCursor(writer, CursorDir.Up); 824 | row--; 825 | } 826 | clearRow(writer); 827 | for (const [i, line] of lines.entries()) { 828 | moveCursor(writer, CursorDir.Column); 829 | let suffix = ""; 830 | if (i < lines.length - 1) { 831 | suffix = "\n"; 832 | row++; 833 | } 834 | writer.writeSync(encoder.encode(line + suffix)); 835 | } 836 | moveCursor(writer, CursorDir.Column); 837 | } 838 | function createSelection(options) { 839 | row = 0; 840 | return ensureSingleSelection(async () => { 841 | writeLines(Deno.stdout, options.render()); 842 | for await (const key of readKeys()) { 843 | const keyResult = options.onKey(key); 844 | if (keyResult != null) { 845 | writeLines(Deno.stdout, []); 846 | if (options.noClear) { 847 | writeLines(Deno.stdout, options.render()); 848 | console.log(); 849 | } 850 | return keyResult; 851 | } 852 | writeLines(Deno.stdout, options.render()); 853 | } 854 | writeLines(Deno.stdout, []); 855 | return void 0; 856 | }); 857 | } 858 | function resultOrExit(result) { 859 | if (result == null) { 860 | Deno.exit(120); 861 | } else { 862 | return result; 863 | } 864 | } 865 | async function multiSelect(options) { 866 | const result = await maybeMultiSelect(options); 867 | return resultOrExit(result); 868 | } 869 | function maybeMultiSelect(options) { 870 | const state = { 871 | title: options.message, 872 | activeIndex: 0, 873 | items: options.options.map((option) => { 874 | if (typeof option === "string") { 875 | option = { 876 | text: option 877 | }; 878 | } 879 | return { 880 | selected: option.selected ?? false, 881 | text: option.text 882 | }; 883 | }), 884 | hasCompleted: false 885 | }; 886 | const { selected = "[x]", unselected = "[ ]", pointer = ">", listBullet = "-", messageStyle = (s) => bold(blue(s)) } = options.styling ?? {}; 887 | const style = { 888 | selected, 889 | unselected, 890 | pointer, 891 | listBullet, 892 | messageStyle 893 | }; 894 | return createSelection({ 895 | message: options.message, 896 | noClear: options.noClear, 897 | render: () => renderMultiSelect(state, style), 898 | onKey: (key) => { 899 | switch (key) { 900 | case Key.Up: 901 | case "k": 902 | if (state.activeIndex === 0) { 903 | state.activeIndex = state.items.length - 1; 904 | } else { 905 | state.activeIndex--; 906 | } 907 | break; 908 | case Key.Down: 909 | case "j": 910 | state.activeIndex = (state.activeIndex + 1) % state.items.length; 911 | break; 912 | case Key.Space: { 913 | const item = state.items[state.activeIndex]; 914 | item.selected = !item.selected; 915 | break; 916 | } 917 | case Key.Enter: 918 | state.hasCompleted = true; 919 | return state.items.map((value, index) => [ 920 | value, 921 | index 922 | ]).filter(([value]) => value.selected).map(([, index]) => index); 923 | } 924 | } 925 | }); 926 | } 927 | function renderMultiSelect(state, style) { 928 | const items = []; 929 | items.push(style.messageStyle(state.title)); 930 | if (state.hasCompleted) { 931 | if (state.items.some((i) => i.selected)) { 932 | for (const item of state.items) { 933 | if (item.selected) { 934 | items.push(`${" ".repeat(style.pointer.length + style.selected.length - style.listBullet.length - 2)}${style.listBullet} ${item.text}`); 935 | } 936 | } 937 | } else { 938 | items.push(italic(" ")); 939 | } 940 | } else { 941 | for (const [i, item] of state.items.entries()) { 942 | const prefix = i === state.activeIndex ? `${style.pointer} ` : `${" ".repeat(style.pointer.length + 1)}`; 943 | items.push(`${prefix}${item.selected ? style.selected : style.unselected} ${item.text}`); 944 | } 945 | } 946 | return items; 947 | } 948 | async function confirm(optsOrMessage, options) { 949 | const result = await maybeConfirm(optsOrMessage, options); 950 | return resultOrExit(result); 951 | } 952 | function maybeConfirm(optsOrMessage, options) { 953 | const opts = typeof optsOrMessage === "string" ? { 954 | message: optsOrMessage, 955 | ...options 956 | } : optsOrMessage; 957 | return innerConfirm(opts); 958 | } 959 | function innerConfirm(options) { 960 | const { messageStyle = (s) => bold(blue(s)) } = options.styling ?? {}; 961 | const style = { 962 | messageStyle 963 | }; 964 | const state = { 965 | title: options.message, 966 | default: options.default, 967 | inputText: "", 968 | hasCompleted: false 969 | }; 970 | return createSelection({ 971 | message: options.message, 972 | noClear: options.noClear, 973 | render: () => renderConfirm(state, style), 974 | onKey: (key) => { 975 | switch (key) { 976 | case "Y": 977 | case "y": 978 | state.inputText = "Y"; 979 | break; 980 | case "N": 981 | case "n": 982 | state.inputText = "N"; 983 | break; 984 | case Key.Backspace: 985 | state.inputText = ""; 986 | break; 987 | case Key.Enter: 988 | if (state.inputText.length === 0) { 989 | if (state.default == null) { 990 | return void 0; 991 | } 992 | state.inputText = state.default ? "Y" : "N"; 993 | } 994 | state.hasCompleted = true; 995 | return state.inputText === "Y" ? true : state.inputText === "N" ? false : state.default; 996 | } 997 | } 998 | }); 999 | } 1000 | function renderConfirm(state, style) { 1001 | return [ 1002 | style.messageStyle(state.title) + " " + (state.hasCompleted ? "" : state.default == null ? "(Y/N) " : state.default ? "(Y/n) " : "(y/N) ") + state.inputText + (state.hasCompleted ? "" : "\u2588") 1003 | ]; 1004 | } 1005 | 1006 | // deno:https://jsr.io/@std/cli/1.0.24/parse_args.ts 1007 | var FLAG_REGEXP = /^(?:-(?:(?-)(?no-)?)?)(?.+?)(?:=(?.+?))?$/s; 1008 | var LETTER_REGEXP = /[A-Za-z]/; 1009 | var NUMBER_REGEXP = /-?\d+(\.\d*)?(e-?\d+)?$/; 1010 | var HYPHEN_REGEXP = /^(-|--)[^-]/; 1011 | var VALUE_REGEXP = /=(?.+)/; 1012 | var FLAG_NAME_REGEXP = /^--[^=]+$/; 1013 | var SPECIAL_CHAR_REGEXP = /\W/; 1014 | var NON_WHITESPACE_REGEXP = /\S/; 1015 | function isNumber(string) { 1016 | return NON_WHITESPACE_REGEXP.test(string) && Number.isFinite(Number(string)); 1017 | } 1018 | function setNested(object, keys, value, collect = false) { 1019 | keys = [ 1020 | ...keys 1021 | ]; 1022 | const key = keys.pop(); 1023 | keys.forEach((key2) => object = object[key2] ??= {}); 1024 | if (collect) { 1025 | const v = object[key]; 1026 | if (Array.isArray(v)) { 1027 | v.push(value); 1028 | return; 1029 | } 1030 | value = v ? [ 1031 | v, 1032 | value 1033 | ] : [ 1034 | value 1035 | ]; 1036 | } 1037 | object[key] = value; 1038 | } 1039 | function hasNested(object, keys) { 1040 | for (const key of keys) { 1041 | const value = object[key]; 1042 | if (!Object.hasOwn(object, key)) return false; 1043 | object = value; 1044 | } 1045 | return true; 1046 | } 1047 | function aliasIsBoolean(aliasMap, booleanSet, key) { 1048 | const set = aliasMap.get(key); 1049 | if (set === void 0) return false; 1050 | for (const alias of set) if (booleanSet.has(alias)) return true; 1051 | return false; 1052 | } 1053 | function isBooleanString(value) { 1054 | return value === "true" || value === "false"; 1055 | } 1056 | function parseBooleanString(value) { 1057 | return value !== "false"; 1058 | } 1059 | function parseArgs(args, options) { 1060 | const { "--": doubleDash = false, alias = {}, boolean = false, default: defaults = {}, stopEarly = false, string = [], collect = [], negatable = [], unknown: unknownFn = (i) => i } = options ?? {}; 1061 | const aliasMap = /* @__PURE__ */ new Map(); 1062 | const booleanSet = /* @__PURE__ */ new Set(); 1063 | const stringSet = /* @__PURE__ */ new Set(); 1064 | const collectSet = /* @__PURE__ */ new Set(); 1065 | const negatableSet = /* @__PURE__ */ new Set(); 1066 | let allBools = false; 1067 | if (alias) { 1068 | for (const [key, value] of Object.entries(alias)) { 1069 | if (value === void 0) { 1070 | throw new TypeError("Alias value must be defined"); 1071 | } 1072 | const aliases = Array.isArray(value) ? value : [ 1073 | value 1074 | ]; 1075 | aliasMap.set(key, new Set(aliases)); 1076 | aliases.forEach((alias2) => aliasMap.set(alias2, /* @__PURE__ */ new Set([ 1077 | key, 1078 | ...aliases.filter((it) => it !== alias2) 1079 | ]))); 1080 | } 1081 | } 1082 | if (boolean) { 1083 | if (typeof boolean === "boolean") { 1084 | allBools = boolean; 1085 | } else { 1086 | const booleanArgs = Array.isArray(boolean) ? boolean : [ 1087 | boolean 1088 | ]; 1089 | for (const key of booleanArgs.filter(Boolean)) { 1090 | booleanSet.add(key); 1091 | aliasMap.get(key)?.forEach((al) => { 1092 | booleanSet.add(al); 1093 | }); 1094 | } 1095 | } 1096 | } 1097 | if (string) { 1098 | const stringArgs = Array.isArray(string) ? string : [ 1099 | string 1100 | ]; 1101 | for (const key of stringArgs.filter(Boolean)) { 1102 | stringSet.add(key); 1103 | aliasMap.get(key)?.forEach((al) => stringSet.add(al)); 1104 | } 1105 | } 1106 | if (collect) { 1107 | const collectArgs = Array.isArray(collect) ? collect : [ 1108 | collect 1109 | ]; 1110 | for (const key of collectArgs.filter(Boolean)) { 1111 | collectSet.add(key); 1112 | aliasMap.get(key)?.forEach((al) => collectSet.add(al)); 1113 | } 1114 | } 1115 | if (negatable) { 1116 | const negatableArgs = Array.isArray(negatable) ? negatable : [ 1117 | negatable 1118 | ]; 1119 | for (const key of negatableArgs.filter(Boolean)) { 1120 | negatableSet.add(key); 1121 | aliasMap.get(key)?.forEach((alias2) => negatableSet.add(alias2)); 1122 | } 1123 | } 1124 | const argv = { 1125 | _: [] 1126 | }; 1127 | function setArgument(key, value, arg, collect2) { 1128 | if (!booleanSet.has(key) && !stringSet.has(key) && !aliasMap.has(key) && !collectSet.has(key) && !(allBools && FLAG_NAME_REGEXP.test(arg)) && unknownFn?.(arg, key, value) === false) { 1129 | return; 1130 | } 1131 | if (typeof value === "string" && !stringSet.has(key)) { 1132 | value = isNumber(value) ? Number(value) : value; 1133 | } 1134 | const collectable = collect2 && collectSet.has(key); 1135 | setNested(argv, key.split("."), value, collectable); 1136 | aliasMap.get(key)?.forEach((key2) => { 1137 | setNested(argv, key2.split("."), value, collectable); 1138 | }); 1139 | } 1140 | let notFlags = []; 1141 | const index = args.indexOf("--"); 1142 | if (index !== -1) { 1143 | notFlags = args.slice(index + 1); 1144 | args = args.slice(0, index); 1145 | } 1146 | argsLoop: for (let i = 0; i < args.length; i++) { 1147 | const arg = args[i]; 1148 | const groups = arg.match(FLAG_REGEXP)?.groups; 1149 | if (groups) { 1150 | const { doubleDash: doubleDash2, negated } = groups; 1151 | let key = groups.key; 1152 | let value = groups.value; 1153 | if (doubleDash2) { 1154 | if (value) { 1155 | if (booleanSet.has(key)) value = parseBooleanString(value); 1156 | setArgument(key, value, arg, true); 1157 | continue; 1158 | } 1159 | if (negated) { 1160 | if (negatableSet.has(key)) { 1161 | setArgument(key, false, arg, false); 1162 | continue; 1163 | } 1164 | key = `no-${key}`; 1165 | } 1166 | const next = args[i + 1]; 1167 | if (next) { 1168 | if (!booleanSet.has(key) && !allBools && !next.startsWith("-") && (!aliasMap.has(key) || !aliasIsBoolean(aliasMap, booleanSet, key))) { 1169 | value = next; 1170 | i++; 1171 | setArgument(key, value, arg, true); 1172 | continue; 1173 | } 1174 | if (isBooleanString(next)) { 1175 | value = parseBooleanString(next); 1176 | i++; 1177 | setArgument(key, value, arg, true); 1178 | continue; 1179 | } 1180 | } 1181 | value = stringSet.has(key) ? "" : true; 1182 | setArgument(key, value, arg, true); 1183 | continue; 1184 | } 1185 | const letters = arg.slice(1, -1).split(""); 1186 | for (const [j, letter] of letters.entries()) { 1187 | const next = arg.slice(j + 2); 1188 | if (next === "-") { 1189 | setArgument(letter, next, arg, true); 1190 | continue; 1191 | } 1192 | if (LETTER_REGEXP.test(letter)) { 1193 | const groups2 = VALUE_REGEXP.exec(next)?.groups; 1194 | if (groups2) { 1195 | setArgument(letter, groups2.value, arg, true); 1196 | continue argsLoop; 1197 | } 1198 | if (NUMBER_REGEXP.test(next)) { 1199 | setArgument(letter, next, arg, true); 1200 | continue argsLoop; 1201 | } 1202 | } 1203 | if (letters[j + 1]?.match(SPECIAL_CHAR_REGEXP)) { 1204 | setArgument(letter, arg.slice(j + 2), arg, true); 1205 | continue argsLoop; 1206 | } 1207 | setArgument(letter, stringSet.has(letter) ? "" : true, arg, true); 1208 | } 1209 | key = arg.slice(-1); 1210 | if (key === "-") continue; 1211 | const nextArg = args[i + 1]; 1212 | if (nextArg) { 1213 | if (!HYPHEN_REGEXP.test(nextArg) && !booleanSet.has(key) && (!aliasMap.has(key) || !aliasIsBoolean(aliasMap, booleanSet, key))) { 1214 | setArgument(key, nextArg, arg, true); 1215 | i++; 1216 | continue; 1217 | } 1218 | if (isBooleanString(nextArg)) { 1219 | const value2 = parseBooleanString(nextArg); 1220 | setArgument(key, value2, arg, true); 1221 | i++; 1222 | continue; 1223 | } 1224 | } 1225 | setArgument(key, stringSet.has(key) ? "" : true, arg, true); 1226 | continue; 1227 | } 1228 | if (unknownFn?.(arg) !== false) { 1229 | argv._.push(stringSet.has("_") || !isNumber(arg) ? arg : Number(arg)); 1230 | } 1231 | if (stopEarly) { 1232 | argv._.push(...args.slice(i + 1)); 1233 | break; 1234 | } 1235 | } 1236 | for (const [key, value] of Object.entries(defaults)) { 1237 | const keys = key.split("."); 1238 | if (!hasNested(argv, keys)) { 1239 | setNested(argv, keys, value); 1240 | aliasMap.get(key)?.forEach((key2) => setNested(argv, key2.split("."), value)); 1241 | } 1242 | } 1243 | for (const key of booleanSet.keys()) { 1244 | const keys = key.split("."); 1245 | if (!hasNested(argv, keys)) { 1246 | const value = collectSet.has(key) ? [] : false; 1247 | setNested(argv, keys, value); 1248 | } 1249 | } 1250 | for (const key of stringSet.keys()) { 1251 | const keys = key.split("."); 1252 | if (!hasNested(argv, keys) && collectSet.has(key)) { 1253 | setNested(argv, keys, []); 1254 | } 1255 | } 1256 | if (doubleDash) { 1257 | argv["--"] = notFlags; 1258 | } else { 1259 | argv._.push(...notFlags); 1260 | } 1261 | return argv; 1262 | } 1263 | 1264 | // src/util.ts 1265 | var { isExistingDir, mkdir } = environment; 1266 | function withContext(ctx, error) { 1267 | return new Error(ctx, { 1268 | cause: error 1269 | }); 1270 | } 1271 | async function filterAsync(arr, pred) { 1272 | const filtered = await Promise.all(arr.map((v) => pred(v))); 1273 | return arr.filter((_, i) => filtered[i]); 1274 | } 1275 | function withEnvVar(name, f) { 1276 | const value = environment.getEnv(name); 1277 | return f(value); 1278 | } 1279 | function shellEnvContains(s) { 1280 | return withEnvVar("SHELL", (sh) => sh !== void 0 && sh.includes(s)); 1281 | } 1282 | function warn(s) { 1283 | console.error(`%cwarning%c: ${s}`, "color: yellow", "color: inherit"); 1284 | } 1285 | function info(s) { 1286 | console.error(`%cinfo%c: ${s}`, "color: green", "color: inherit"); 1287 | } 1288 | async function ensureExists(dirPath) { 1289 | if (!await isExistingDir(dirPath)) { 1290 | await mkdir(dirPath, { 1291 | recursive: true 1292 | }); 1293 | } 1294 | } 1295 | function ensureEndsWith(s, suffix) { 1296 | if (!s.endsWith(suffix)) { 1297 | return s + suffix; 1298 | } 1299 | return s; 1300 | } 1301 | function ensureStartsWith(s, prefix) { 1302 | if (!s.startsWith(prefix)) { 1303 | return prefix + s; 1304 | } 1305 | return s; 1306 | } 1307 | 1308 | // src/shell.ts 1309 | var { isExistingFile, writeTextFile, homeDir, findCmd, runCmd, getEnv, pathExists } = environment; 1310 | var ShellScript = class { 1311 | name; 1312 | contents; 1313 | constructor(name, contents) { 1314 | this.name = name; 1315 | this.contents = contents; 1316 | } 1317 | equals(other) { 1318 | return this.name === other.name && this.contents === other.contents; 1319 | } 1320 | async write(denoInstallDir) { 1321 | const envFilePath = join3(denoInstallDir, this.name); 1322 | try { 1323 | await writeTextFile(envFilePath, this.contents); 1324 | return true; 1325 | } catch (error) { 1326 | if (error instanceof Deno.errors.PermissionDenied) { 1327 | return false; 1328 | } 1329 | throw withContext(`Failed to write ${this.name} file to ${envFilePath}`, error); 1330 | } 1331 | } 1332 | }; 1333 | var shEnvScript = (installDir) => new ShellScript("env", `#!/bin/sh 1334 | # deno shell setup; adapted from rustup 1335 | # affix colons on either side of $PATH to simplify matching 1336 | case ":\${PATH}:" in 1337 | *:"${installDir}/bin":*) 1338 | ;; 1339 | *) 1340 | # Prepending path in case a system-installed deno executable needs to be overridden 1341 | export PATH="${installDir}/bin:$PATH" 1342 | ;; 1343 | esac 1344 | `); 1345 | var shSourceString = (installDir) => { 1346 | return `. "${installDir}/env"`; 1347 | }; 1348 | var Posix = class { 1349 | name = "sh"; 1350 | supportsCompletion = false; 1351 | exists() { 1352 | return true; 1353 | } 1354 | rcfiles() { 1355 | return [ 1356 | join3(homeDir, ".profile") 1357 | ]; 1358 | } 1359 | rcsToUpdate() { 1360 | return this.rcfiles(); 1361 | } 1362 | }; 1363 | var Bash = class { 1364 | name = "bash"; 1365 | get supportsCompletion() { 1366 | if (Deno.build.os === "darwin") { 1367 | return "not recommended on macOS"; 1368 | } 1369 | return true; 1370 | } 1371 | async exists() { 1372 | return (await this.rcsToUpdate()).length > 0; 1373 | } 1374 | rcfiles() { 1375 | return [ 1376 | ".bash_profile", 1377 | ".bash_login", 1378 | ".bashrc" 1379 | ].map((rc) => join3(homeDir, rc)); 1380 | } 1381 | rcsToUpdate() { 1382 | return filterAsync(this.rcfiles(), isExistingFile); 1383 | } 1384 | completionsFilePath() { 1385 | const USER = Deno.env.get("USER"); 1386 | if (USER === "root") { 1387 | return "/usr/local/etc/bash_completion.d/deno.bash"; 1388 | } 1389 | return join3(homeDir, ".local/share/bash-completion/completions/deno.bash"); 1390 | } 1391 | completionsSourceString() { 1392 | return `source ${this.completionsFilePath()}`; 1393 | } 1394 | }; 1395 | var Zsh = class { 1396 | name = "zsh"; 1397 | supportsCompletion = true; 1398 | async exists() { 1399 | if (shellEnvContains("zsh") || await findCmd("zsh")) { 1400 | return true; 1401 | } 1402 | return false; 1403 | } 1404 | async getZshDotDir() { 1405 | let zshDotDir; 1406 | if (shellEnvContains("zsh")) { 1407 | zshDotDir = getEnv("ZDOTDIR"); 1408 | } else { 1409 | const output = await runCmd("zsh", [ 1410 | "-c", 1411 | "echo -n $ZDOTDIR" 1412 | ]); 1413 | const stdout = new TextDecoder().decode(output.stdout).trim(); 1414 | zshDotDir = stdout.length > 0 ? stdout : void 0; 1415 | } 1416 | return zshDotDir; 1417 | } 1418 | async rcfiles() { 1419 | const zshDotDir = await this.getZshDotDir(); 1420 | return [ 1421 | zshDotDir, 1422 | homeDir 1423 | ].map((dir) => dir ? join3(dir, ".zshrc") : void 0).filter((dir) => dir !== void 0); 1424 | } 1425 | async rcsToUpdate() { 1426 | let out = await filterAsync(await this.rcfiles(), isExistingFile); 1427 | if (out.length === 0) { 1428 | out = await this.rcfiles(); 1429 | } 1430 | return out; 1431 | } 1432 | async completionsFilePath() { 1433 | let zshDotDir = await this.getZshDotDir(); 1434 | if (!zshDotDir) { 1435 | zshDotDir = join3(homeDir, ".zsh"); 1436 | } 1437 | return join3(zshDotDir, "completions", "_deno.zsh"); 1438 | } 1439 | async completionsSourceString() { 1440 | const filePath = await this.completionsFilePath(); 1441 | const completionDir = dirname3(filePath); 1442 | const fpathSetup = `# Add deno completions to search path 1443 | if [[ ":$FPATH:" != *":${completionDir}:"* ]]; then export FPATH="${completionDir}:$FPATH"; fi`; 1444 | const zshDotDir = await this.getZshDotDir() ?? homeDir; 1445 | let append; 1446 | if ((await filterAsync([ 1447 | ".zcompdump", 1448 | ".oh_my_zsh", 1449 | ".zprezto" 1450 | ], (f) => pathExists(join3(zshDotDir, f)))).length == 0) { 1451 | append = "# Initialize zsh completions (added by deno install script)\nautoload -Uz compinit\ncompinit"; 1452 | } 1453 | return { 1454 | prepend: fpathSetup, 1455 | append 1456 | }; 1457 | } 1458 | }; 1459 | var Fish = class { 1460 | name = "fish"; 1461 | supportsCompletion = true; 1462 | async exists() { 1463 | if (shellEnvContains("fish") || await findCmd("fish")) { 1464 | return true; 1465 | } 1466 | return false; 1467 | } 1468 | fishConfigDir() { 1469 | const first = withEnvVar("XDG_CONFIG_HOME", (p) => { 1470 | if (!p) return; 1471 | return join3(p, "fish"); 1472 | }); 1473 | return first ?? join3(homeDir, ".config", "fish"); 1474 | } 1475 | rcfiles() { 1476 | const conf = "conf.d/deno.fish"; 1477 | return [ 1478 | join3(this.fishConfigDir(), conf) 1479 | ]; 1480 | } 1481 | rcsToUpdate() { 1482 | return this.rcfiles(); 1483 | } 1484 | envScript(installDir) { 1485 | const fishEnv = ` 1486 | # deno shell setup 1487 | if not contains "${installDir}/bin" $PATH 1488 | # prepend to path to take precedence over potential package manager deno installations 1489 | set -x PATH "${installDir}/bin" $PATH 1490 | end 1491 | `; 1492 | return new ShellScript("env.fish", fishEnv); 1493 | } 1494 | sourceString(installDir) { 1495 | return `source "${installDir}/env.fish"`; 1496 | } 1497 | completionsFilePath() { 1498 | return join3(this.fishConfigDir(), "completions", "deno.fish"); 1499 | } 1500 | }; 1501 | 1502 | // src/rc_files.ts 1503 | var RcBackups = class { 1504 | backupDir; 1505 | backedUp; 1506 | constructor(backupDir) { 1507 | this.backupDir = backupDir; 1508 | this.backedUp = /* @__PURE__ */ new Set(); 1509 | } 1510 | async add(path, contents) { 1511 | if (this.backedUp.has(path)) { 1512 | return; 1513 | } 1514 | const dest = join3(this.backupDir, basename3(path)) + `.bak`; 1515 | info(`backing '${path}' up to '${dest}'`); 1516 | await environment.writeTextFile(dest, contents); 1517 | this.backedUp.add(path); 1518 | } 1519 | }; 1520 | async function updateRcFile(rc, command, backups) { 1521 | let prepend = ""; 1522 | let append = ""; 1523 | if (typeof command === "string") { 1524 | append = command; 1525 | } else { 1526 | prepend = command.prepend ?? ""; 1527 | append = command.append ?? ""; 1528 | } 1529 | if (!prepend && !append) { 1530 | return false; 1531 | } 1532 | let contents; 1533 | try { 1534 | contents = await environment.readTextFile(rc); 1535 | if (prepend) { 1536 | if (contents.includes(prepend)) { 1537 | prepend = ""; 1538 | } else { 1539 | prepend = ensureEndsWith(prepend, "\n"); 1540 | } 1541 | } 1542 | if (append) { 1543 | if (contents.includes(append)) { 1544 | append = ""; 1545 | } else if (!contents.endsWith("\n")) { 1546 | append = ensureEndsWith(ensureStartsWith(append, "\n"), "\n"); 1547 | } else { 1548 | append = ensureEndsWith(append, "\n"); 1549 | } 1550 | } 1551 | } catch (_error) { 1552 | prepend = prepend ? ensureEndsWith(prepend, "\n") : prepend; 1553 | append = append ? ensureEndsWith(append, "\n") : append; 1554 | } 1555 | if (!prepend && !append) { 1556 | return false; 1557 | } 1558 | if (contents !== void 0) { 1559 | await backups.add(rc, contents); 1560 | } 1561 | await ensureExists(dirname3(rc)); 1562 | try { 1563 | await environment.writeTextFile(rc, prepend + (contents ?? "") + append, { 1564 | create: true 1565 | }); 1566 | return true; 1567 | } catch (error) { 1568 | if (error instanceof Deno.errors.PermissionDenied || // deno-lint-ignore no-explicit-any 1569 | error instanceof Deno.errors.NotCapable) { 1570 | return false; 1571 | } 1572 | throw withContext(`Failed to update shell rc file: ${rc}`, error); 1573 | } 1574 | } 1575 | 1576 | // src/main.ts 1577 | var { readTextFile, runCmd: runCmd2, writeTextFile: writeTextFile2 } = environment; 1578 | async function writeCompletionFiles(availableShells) { 1579 | const written = /* @__PURE__ */ new Set(); 1580 | const results = []; 1581 | const decoder2 = new TextDecoder(); 1582 | for (const shell of availableShells) { 1583 | if (!shell.supportsCompletion) { 1584 | results.push(null); 1585 | continue; 1586 | } 1587 | try { 1588 | const completionFilePath = await shell.completionsFilePath?.(); 1589 | if (!completionFilePath) { 1590 | results.push(null); 1591 | continue; 1592 | } 1593 | await ensureExists(dirname3(completionFilePath)); 1594 | const output = await runCmd2(Deno.execPath(), [ 1595 | "completions", 1596 | shell.name 1597 | ]); 1598 | if (!output.success) { 1599 | throw new Error(`deno completions subcommand failed, stderr was: ${decoder2.decode(output.stderr)}`); 1600 | } 1601 | const completionFileContents = decoder2.decode(output.stdout); 1602 | if (!completionFileContents) { 1603 | warn(`Completions were empty, skipping ${shell.name}`); 1604 | results.push("fail"); 1605 | continue; 1606 | } 1607 | let currentContents = null; 1608 | try { 1609 | currentContents = await readTextFile(completionFilePath); 1610 | } catch (error) { 1611 | if (!(error instanceof Deno.errors.NotFound)) { 1612 | throw error; 1613 | } else { 1614 | } 1615 | } 1616 | if (currentContents !== completionFileContents) { 1617 | if (currentContents !== null) { 1618 | warn(`an existing completion file for deno already exists at ${completionFilePath}, but is out of date. overwriting with new contents`); 1619 | } 1620 | await writeTextFile2(completionFilePath, completionFileContents); 1621 | } 1622 | results.push("success"); 1623 | written.add(completionFilePath); 1624 | } catch (error) { 1625 | warn(`Failed to install completions for ${shell.name}: ${error}`); 1626 | results.push("fail"); 1627 | continue; 1628 | } 1629 | } 1630 | return results; 1631 | } 1632 | async function writeCompletionRcCommands(availableShells, backups) { 1633 | for (const shell of availableShells) { 1634 | if (!shell.supportsCompletion) continue; 1635 | const rcCmd = await shell.completionsSourceString?.(); 1636 | if (!rcCmd) continue; 1637 | for (const rc of await shell.rcsToUpdate()) { 1638 | await updateRcFile(rc, rcCmd, backups); 1639 | } 1640 | } 1641 | } 1642 | async function writeEnvFiles(availableShells, installDir) { 1643 | const written = new Array(); 1644 | let i = 0; 1645 | while (i < availableShells.length) { 1646 | const shell = availableShells[i]; 1647 | const script = (shell.envScript ?? shEnvScript)(installDir); 1648 | if (!written.some((s) => s.equals(script))) { 1649 | if (await script.write(installDir)) { 1650 | written.push(script); 1651 | } else { 1652 | continue; 1653 | } 1654 | } 1655 | i++; 1656 | } 1657 | } 1658 | async function addToPath(availableShells, installDir, backups) { 1659 | for (const shell of availableShells) { 1660 | const sourceCmd = await (shell.sourceString ?? shSourceString)(installDir); 1661 | for (const rc of await shell.rcsToUpdate()) { 1662 | await updateRcFile(rc, sourceCmd, backups); 1663 | } 1664 | } 1665 | } 1666 | var shells = [ 1667 | new Posix(), 1668 | new Bash(), 1669 | new Zsh(), 1670 | new Fish() 1671 | ]; 1672 | async function getAvailableShells() { 1673 | const present = []; 1674 | for (const shell of shells) { 1675 | try { 1676 | if (await shell.exists()) { 1677 | present.push(shell); 1678 | } 1679 | } catch (_e) { 1680 | continue; 1681 | } 1682 | } 1683 | return present; 1684 | } 1685 | async function setupShells(installDir, backupDir, opts) { 1686 | const { skipPrompts, noModifyPath } = opts; 1687 | const availableShells = await getAvailableShells(); 1688 | await writeEnvFiles(availableShells, installDir); 1689 | const backups = new RcBackups(backupDir); 1690 | if (skipPrompts && !noModifyPath || !skipPrompts && await confirm(`Edit shell configs to add deno to the PATH?`, { 1691 | default: true 1692 | })) { 1693 | await ensureExists(backupDir); 1694 | await addToPath(availableShells, installDir, backups); 1695 | console.log("\nDeno was added to the PATH.\nYou may need to restart your shell for it to become available.\n"); 1696 | } 1697 | const shellsWithCompletion = availableShells.filter((s) => s.supportsCompletion !== false); 1698 | const selected = skipPrompts ? [] : await multiSelect({ 1699 | message: `Set up completions?`, 1700 | options: shellsWithCompletion.map((s) => { 1701 | const maybeNotes = typeof s.supportsCompletion === "string" ? ` (${s.supportsCompletion})` : ""; 1702 | return s.name + maybeNotes; 1703 | }) 1704 | }); 1705 | const completionsToSetup = selected.map((idx) => shellsWithCompletion[idx]); 1706 | if (completionsToSetup.length > 0) { 1707 | await ensureExists(backupDir); 1708 | const results = await writeCompletionFiles(completionsToSetup); 1709 | await writeCompletionRcCommands(completionsToSetup.filter((_s, i) => results[i] !== "fail"), backups); 1710 | } 1711 | } 1712 | function printHelp() { 1713 | console.log(` 1714 | 1715 | Setup script for installing deno 1716 | 1717 | Options: 1718 | -y, --yes 1719 | Skip interactive prompts and accept defaults 1720 | --no-modify-path 1721 | Don't add deno to the PATH environment variable 1722 | -h, --help 1723 | Print help 1724 | `); 1725 | } 1726 | async function main() { 1727 | if (Deno.args.length === 0) { 1728 | throw new Error("Expected the deno install directory as the first argument"); 1729 | } 1730 | const args = parseArgs(Deno.args.slice(1), { 1731 | boolean: [ 1732 | "yes", 1733 | "no-modify-path", 1734 | "help" 1735 | ], 1736 | alias: { 1737 | "yes": "y", 1738 | "help": "h" 1739 | }, 1740 | default: { 1741 | yes: false, 1742 | "no-modify-path": false 1743 | }, 1744 | unknown: (arg) => { 1745 | if (arg.startsWith("-")) { 1746 | printHelp(); 1747 | console.error(`Unknown flag ${arg}. Shell will not be configured`); 1748 | Deno.exit(1); 1749 | } 1750 | return false; 1751 | } 1752 | }); 1753 | if (args.help) { 1754 | printHelp(); 1755 | return; 1756 | } 1757 | if (Deno.build.os === "windows" || !args.yes && !(Deno.stdin.isTerminal() && Deno.stdout.isTerminal())) { 1758 | return; 1759 | } 1760 | const installDir = Deno.args[0].trim(); 1761 | const backupDir = join3(installDir, ".shellRcBackups"); 1762 | try { 1763 | await setupShells(installDir, backupDir, { 1764 | skipPrompts: args.yes, 1765 | noModifyPath: args["no-modify-path"] 1766 | }); 1767 | } catch (_e) { 1768 | warn(`Failed to configure your shell environments, you may need to manually add deno to your PATH environment variable. 1769 | 1770 | Manually add the directory to your $HOME/.bashrc (or similar)": 1771 | export DENO_INSTALL="${installDir}" 1772 | export PATH="${installDir}/bin:$PATH" 1773 | `); 1774 | } 1775 | } 1776 | if (import.meta.main) { 1777 | await main(); 1778 | } 1779 | --------------------------------------------------------------------------------