diff --git a/.gitattributes b/.gitattributes index b2d0ca9..186daf8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ * text=auto *.sh text eol=lf +shell-setup/bundled.esm.js -diff diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b76428..d6bcd19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,9 @@ jobs: shell: bash run: | if ! shasum -a 256 -c SHA256SUM; then - echo 'Checksum verification failed' + echo 'Checksum verification failed.' + echo 'If the installer has been updated intentionally, update the checksum with:' + echo 'shasum -a 256 install.{sh,ps1} > SHA256SUM' exit 1 fi - name: tests shell @@ -67,3 +69,6 @@ jobs: echo 'Bundled script is out of date, update it with `cd shell-setup; deno task bundle`'. exit 1 fi + - name: integration tests + if: matrix.os != 'windows-latest' + run: deno test -A --permit-no-files diff --git a/SHA256SUM b/SHA256SUM index bbaee35..02f4c82 100644 --- a/SHA256SUM +++ b/SHA256SUM @@ -1,2 +1,2 @@ -e0497676dfe693ba9a69e808ac308f6fdaffb3979eecc2c69fd9d24e84df9cee install.sh +9f3677714f300baa745dfd6078d2a95f0d77d19ea0a67b44b93c804e183e55f0 install.sh 0e7618d4055b21fe1fe7915ebbe27814f2f6f178cb739d1cc2a0d729da1c9d58 install.ps1 diff --git a/deno.json b/deno.json index f966b8e..0673b1f 100644 --- a/deno.json +++ b/deno.json @@ -12,10 +12,14 @@ ] } }, - "lock": "./shell-setup/deno.lock", + "tasks": { + "bundle": "cd shell-setup && deno task bundle" + }, + "lock": { "path": "./shell-setup/deno.lock", "frozen": true }, "fmt": { "exclude": [ - "./shell-setup/bundled.esm.js" + "./shell-setup/bundled.esm.js", + ".github" ] } } diff --git a/install.sh b/install.sh index cf0c602..de1e344 100755 --- a/install.sh +++ b/install.sh @@ -20,10 +20,42 @@ else esac fi -if [ $# -eq 0 ]; then +print_help_and_exit() { + echo "Setup script for installing deno + +Options: + -y, --yes + Skip interactive prompts and accept defaults + --no-modify-path + Don't add deno to the PATH environment variable + -h, --help + Print help +" + echo "Note: Deno was not installed" + exit 0 +} + +# Simple arg parsing - look for help flag, otherwise +# ignore args starting with '-' and take the first +# positional arg as the deno version to install +for arg in "$@"; do + case "$arg" in + "-h") + print_help_and_exit + ;; + "--help") + print_help_and_exit + ;; + "-"*) ;; + *) + if [ -z "$deno_version" ]; then + deno_version="$arg" + fi + ;; + esac +done +if [ -z "$deno_version" ]; then deno_version="$(curl -s https://dl.deno.land/release-latest.txt)" -else - deno_version=$1 fi deno_uri="https://dl.deno.land/release/${deno_version}/deno-${target}.zip" @@ -47,17 +79,17 @@ rm "$exe.zip" echo "Deno was installed successfully to $exe" run_shell_setup() { - $exe run -A --reload jsr:@deno/installer-shell-setup/bundled "$deno_install" - + $exe run -A --reload jsr:@deno/installer-shell-setup/bundled "$deno_install" "$@" } + # If stdout is a terminal, see if we can run shell setup script (which includes interactive prompts) if [ -z "$CI" ] && [ -t 1 ] && $exe eval 'const [major, minor] = Deno.version.deno.split("."); if (major < 2 && minor < 42) Deno.exit(1)'; then if [ -t 0 ]; then - run_shell_setup + run_shell_setup "$@" else # This script is probably running piped into sh, so we don't have direct access to stdin. # Instead, explicitly connect /dev/tty to stdin - run_shell_setup /dev/null; then diff --git a/install_test.ts b/install_test.ts new file mode 100644 index 0000000..c8bbe65 --- /dev/null +++ b/install_test.ts @@ -0,0 +1,165 @@ +import $, { Path } from "jsr:@david/dax"; +import { Pty } from "jsr:@sigma/pty-ffi"; +import { assert, assertEquals, assertStringIncludes } from "jsr:@std/assert"; + +Deno.test( + { name: "install skip prompts", ignore: Deno.build.os === "windows" }, + async () => { + await using testEnv = await TestEnv.setup(); + const { env, tempDir, installScript, installDir } = testEnv; + await testEnv.homeDir.join(".bashrc").ensureFile(); + + console.log("installscript contents", await installScript.readText()); + + const shellOutput = await runInBash( + [`cat "${installScript.toString()}" | sh -s -- -y v2.0.0-rc.6`], + { env, cwd: tempDir }, + ); + console.log(shellOutput); + + assertStringIncludes(shellOutput, "Deno was added to the PATH"); + + const deno = installDir.join("bin/deno"); + assert(await deno.exists()); + + // Check that it's on the PATH now, and that it's the correct version. + const output = await new Deno.Command("bash", { + args: ["-i", "-c", "deno --version"], + env, + }).output(); + const stdout = new TextDecoder().decode(output.stdout).trim(); + + const versionRe = /deno (\d+\.\d+\.\d+\S*)/; + const match = stdout.match(versionRe); + + assert(match !== null); + assertEquals(match[1], "2.0.0-rc.6"); + }, +); + +Deno.test( + { name: "install no modify path", ignore: Deno.build.os === "windows" }, + async () => { + await using testEnv = await TestEnv.setup(); + const { env, tempDir, installScript, installDir } = testEnv; + await testEnv.homeDir.join(".bashrc").ensureFile(); + + const shellOutput = await runInBash( + [`cat "${installScript.toString()}" | sh -s -- -y v2.0.0-rc.6 --no-modify-path`], + { env, cwd: tempDir }, + ); + + assert( + !shellOutput.includes("Deno was added to the PATH"), + `Unexpected output, shouldn't have added to the PATH:\n${shellOutput}`, + ); + + const deno = installDir.join("bin/deno"); + assert(await deno.exists()); + }, +); + +class TestEnv implements AsyncDisposable, Disposable { + #tempDir: Path; + private constructor( + tempDir: Path, + public homeDir: Path, + public installDir: Path, + public installScript: Path, + public env: Record, + ) { + this.#tempDir = tempDir; + } + get tempDir() { + return this.#tempDir; + } + static async setup({ env = {} }: { env?: Record } = {}) { + const tempDir = $.path(await Deno.makeTempDir()); + const homeDir = await tempDir.join("home").ensureDir(); + const installDir = tempDir.join(".deno"); + + const tempSetup = tempDir.join("shell-setup.js"); + await $.path(resolve("./shell-setup/bundled.esm.js")).copyFile(tempSetup); + + // Copy the install script to a temp location, and modify it to + // run the shell setup script from the local source instead of JSR. + const contents = await Deno.readTextFile(resolve("./install.sh")); + const contentsLocal = contents.replaceAll( + "jsr:@deno/installer-shell-setup/bundled", + tempSetup.toString(), + ); + if (contents === contentsLocal) { + throw new Error("Failed to point installer at local source"); + } + const installScript = tempDir.join("install.sh"); + await installScript.writeText(contentsLocal); + + await Deno.chmod(installScript.toString(), 0o755); + + // Ensure that the necessary binaries are in the PATH. + // It's not perfect, but the idea is to keep the test environment + // as clean as possible to make it less host dependent. + const needed = ["bash", "unzip", "cat", "sh"]; + const binPaths = await Promise.all(needed.map((n) => $.which(n))); + const searchPaths = new Set( + binPaths.map((p, i) => { + if (p === undefined) { + throw new Error(`missing dependency: ${needed[i]}`); + } + return $.path(p).parentOrThrow().toString(); + }), + ); + const newEnv = { + HOME: homeDir.toString(), + XDG_CONFIG_HOME: homeDir.toString(), + DENO_INSTALL: installDir.toString(), + PATH: searchPaths.values().toArray().join(":"), + ZDOTDIR: homeDir.toString(), + SHELL: "/bin/bash", + CI: "", + }; + Object.assign(newEnv, env); + return new TestEnv(tempDir, homeDir, installDir, installScript, newEnv); + } + async [Symbol.asyncDispose]() { + await this.#tempDir.remove({ recursive: true }); + } + [Symbol.dispose]() { + this.#tempDir.removeSync({ recursive: true }); + } +} + +async function runInBash( + commands: string[], + options: { cwd?: Path; env: Record }, +): Promise { + const { cwd, env } = options; + const bash = await $.which("bash") ?? "bash"; + const pty = new Pty({ + env: Object.entries(env), + cmd: bash, + args: [], + }); + if (cwd) { + await pty.write(`cd "${cwd.toString()}"\n`); + } + + for (const command of commands) { + await pty.write(command + "\n"); + } + await pty.write("exit\n"); + let output = ""; + while (true) { + const { data, done } = await pty.read(); + output += data; + if (done) { + break; + } + } + pty.close(); + return output; +} + +function resolve(s: string): URL { + return new URL(import.meta.resolve(s)); +} diff --git a/shell-setup/bundle.ts b/shell-setup/bundle.ts index 8adb217..40f8ef9 100644 --- a/shell-setup/bundle.ts +++ b/shell-setup/bundle.ts @@ -14,6 +14,8 @@ const result = await esbuild.build({ format: "esm", }); -console.log(result.outputFiles); +if (result.errors.length || result.warnings.length) { + console.error(`Errors: ${result.errors}, warnings: ${result.warnings}`); +} await esbuild.stop(); diff --git a/shell-setup/bundled.esm.js b/shell-setup/bundled.esm.js index 06b7d6c..4457748 100644 --- a/shell-setup/bundled.esm.js +++ b/shell-setup/bundled.esm.js @@ -917,6 +917,262 @@ function renderConfirm(state, style) { ]; } +// https://jsr.io/@std/cli/1.0.6/parse_args.ts +var FLAG_REGEXP = /^(?:-(?:(?-)(?no-)?)?)(?.+?)(?:=(?.+?))?$/s; +var LETTER_REGEXP = /[A-Za-z]/; +var NUMBER_REGEXP = /-?\d+(\.\d*)?(e-?\d+)?$/; +var HYPHEN_REGEXP = /^(-|--)[^-]/; +var VALUE_REGEXP = /=(?.+)/; +var FLAG_NAME_REGEXP = /^--[^=]+$/; +var SPECIAL_CHAR_REGEXP = /\W/; +var NON_WHITESPACE_REGEXP = /\S/; +function isNumber(string) { + return NON_WHITESPACE_REGEXP.test(string) && Number.isFinite(Number(string)); +} +function setNested(object, keys, value, collect = false) { + keys = [...keys]; + const key = keys.pop(); + keys.forEach((key2) => object = object[key2] ??= {}); + if (collect) { + const v = object[key]; + if (Array.isArray(v)) { + v.push(value); + return; + } + value = v ? [v, value] : [value]; + } + object[key] = value; +} +function hasNested(object, keys) { + for (const key of keys) { + const value = object[key]; + if (!Object.hasOwn(object, key)) return false; + object = value; + } + return true; +} +function aliasIsBoolean(aliasMap, booleanSet, key) { + const set = aliasMap.get(key); + if (set === void 0) return false; + for (const alias of set) if (booleanSet.has(alias)) return true; + return false; +} +function isBooleanString(value) { + return value === "true" || value === "false"; +} +function parseBooleanString(value) { + return value !== "false"; +} +function parseArgs(args, options) { + const { + "--": doubleDash = false, + alias = {}, + boolean = false, + default: defaults = {}, + stopEarly = false, + string = [], + collect = [], + negatable = [], + unknown: unknownFn = (i) => i + } = options ?? {}; + const aliasMap = /* @__PURE__ */ new Map(); + const booleanSet = /* @__PURE__ */ new Set(); + const stringSet = /* @__PURE__ */ new Set(); + const collectSet = /* @__PURE__ */ new Set(); + const negatableSet = /* @__PURE__ */ new Set(); + let allBools = false; + if (alias) { + for (const [key, value] of Object.entries(alias)) { + if (value === void 0) { + throw new TypeError("Alias value must be defined"); + } + const aliases = Array.isArray(value) ? value : [value]; + aliasMap.set(key, new Set(aliases)); + aliases.forEach( + (alias2) => aliasMap.set( + alias2, + /* @__PURE__ */ new Set([key, ...aliases.filter((it) => it !== alias2)]) + ) + ); + } + } + if (boolean) { + if (typeof boolean === "boolean") { + allBools = boolean; + } else { + const booleanArgs = Array.isArray(boolean) ? boolean : [boolean]; + for (const key of booleanArgs.filter(Boolean)) { + booleanSet.add(key); + aliasMap.get(key)?.forEach((al) => { + booleanSet.add(al); + }); + } + } + } + if (string) { + const stringArgs = Array.isArray(string) ? string : [string]; + for (const key of stringArgs.filter(Boolean)) { + stringSet.add(key); + aliasMap.get(key)?.forEach((al) => stringSet.add(al)); + } + } + if (collect) { + const collectArgs = Array.isArray(collect) ? collect : [collect]; + for (const key of collectArgs.filter(Boolean)) { + collectSet.add(key); + aliasMap.get(key)?.forEach((al) => collectSet.add(al)); + } + } + if (negatable) { + const negatableArgs = Array.isArray(negatable) ? negatable : [negatable]; + for (const key of negatableArgs.filter(Boolean)) { + negatableSet.add(key); + aliasMap.get(key)?.forEach((alias2) => negatableSet.add(alias2)); + } + } + const argv = { _: [] }; + function setArgument(key, value, arg, collect2) { + if (!booleanSet.has(key) && !stringSet.has(key) && !aliasMap.has(key) && !(allBools && FLAG_NAME_REGEXP.test(arg)) && unknownFn?.(arg, key, value) === false) { + return; + } + if (typeof value === "string" && !stringSet.has(key)) { + value = isNumber(value) ? Number(value) : value; + } + const collectable = collect2 && collectSet.has(key); + setNested(argv, key.split("."), value, collectable); + aliasMap.get(key)?.forEach((key2) => { + setNested(argv, key2.split("."), value, collectable); + }); + } + let notFlags = []; + const index = args.indexOf("--"); + if (index !== -1) { + notFlags = args.slice(index + 1); + args = args.slice(0, index); + } + argsLoop: + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + const groups = arg.match(FLAG_REGEXP)?.groups; + if (groups) { + const { doubleDash: doubleDash2, negated } = groups; + let key = groups.key; + let value = groups.value; + if (doubleDash2) { + if (value) { + if (booleanSet.has(key)) value = parseBooleanString(value); + setArgument(key, value, arg, true); + continue; + } + if (negated) { + if (negatableSet.has(key)) { + setArgument(key, false, arg, false); + continue; + } + key = `no-${key}`; + } + const next = args[i + 1]; + if (next) { + if (!booleanSet.has(key) && !allBools && !next.startsWith("-") && (!aliasMap.has(key) || !aliasIsBoolean(aliasMap, booleanSet, key))) { + value = next; + i++; + setArgument(key, value, arg, true); + continue; + } + if (isBooleanString(next)) { + value = parseBooleanString(next); + i++; + setArgument(key, value, arg, true); + continue; + } + } + value = stringSet.has(key) ? "" : true; + setArgument(key, value, arg, true); + continue; + } + const letters = arg.slice(1, -1).split(""); + for (const [j, letter] of letters.entries()) { + const next = arg.slice(j + 2); + if (next === "-") { + setArgument(letter, next, arg, true); + continue; + } + if (LETTER_REGEXP.test(letter)) { + const groups2 = VALUE_REGEXP.exec(next)?.groups; + if (groups2) { + setArgument(letter, groups2.value, arg, true); + continue argsLoop; + } + if (NUMBER_REGEXP.test(next)) { + setArgument(letter, next, arg, true); + continue argsLoop; + } + } + if (letters[j + 1]?.match(SPECIAL_CHAR_REGEXP)) { + setArgument(letter, arg.slice(j + 2), arg, true); + continue argsLoop; + } + setArgument(letter, stringSet.has(letter) ? "" : true, arg, true); + } + key = arg.slice(-1); + if (key === "-") continue; + const nextArg = args[i + 1]; + if (nextArg) { + if (!HYPHEN_REGEXP.test(nextArg) && !booleanSet.has(key) && (!aliasMap.has(key) || !aliasIsBoolean(aliasMap, booleanSet, key))) { + setArgument(key, nextArg, arg, true); + i++; + continue; + } + if (isBooleanString(nextArg)) { + const value2 = parseBooleanString(nextArg); + setArgument(key, value2, arg, true); + i++; + continue; + } + } + setArgument(key, stringSet.has(key) ? "" : true, arg, true); + continue; + } + if (unknownFn?.(arg) !== false) { + argv._.push( + stringSet.has("_") || !isNumber(arg) ? arg : Number(arg) + ); + } + if (stopEarly) { + argv._.push(...args.slice(i + 1)); + break; + } + } + for (const [key, value] of Object.entries(defaults)) { + const keys = key.split("."); + if (!hasNested(argv, keys)) { + setNested(argv, keys, value); + aliasMap.get(key)?.forEach( + (key2) => setNested(argv, key2.split("."), value) + ); + } + } + for (const key of booleanSet.keys()) { + const keys = key.split("."); + if (!hasNested(argv, keys)) { + const value = collectSet.has(key) ? [] : false; + setNested(argv, keys, value); + } + } + for (const key of stringSet.keys()) { + const keys = key.split("."); + if (!hasNested(argv, keys) && collectSet.has(key)) { + setNested(argv, keys, []); + } + } + if (doubleDash) { + argv["--"] = notFlags; + } else { + argv._.push(...notFlags); + } + return argv; +} + // src/util.ts var { isExistingDir, mkdir } = environment; function withContext(ctx, error) { @@ -1290,7 +1546,6 @@ async function updateRcFile(rc, command, backups) { } } catch (_error) { prepend = prepend ? ensureEndsWith(prepend, "\n") : prepend; - append = append ? ensureStartsWith(append, "\n") : append; } if (!prepend && !append) { return false; @@ -1339,11 +1594,15 @@ async function getAvailableShells() { } return present; } -async function setupShells(installDir, backupDir) { +async function setupShells(installDir, backupDir, opts) { + const { + skipPrompts, + noModifyPath + } = opts; const availableShells = await getAvailableShells(); await writeEnvFiles(availableShells, installDir); const backups = new Backups(backupDir); - if (await confirm(`Edit shell configs to add deno to the PATH?`, { + if (skipPrompts && !noModifyPath || !skipPrompts && await confirm(`Edit shell configs to add deno to the PATH?`, { default: true })) { await ensureExists(backupDir); @@ -1355,7 +1614,7 @@ async function setupShells(installDir, backupDir) { const shellsWithCompletion = availableShells.filter( (s) => s.supportsCompletion !== false ); - const selected = await multiSelect( + const selected = skipPrompts ? [] : await multiSelect( { message: `Set up completions?`, options: shellsWithCompletion.map((s) => { @@ -1374,19 +1633,59 @@ async function setupShells(installDir, backupDir) { ); } } +function printHelp() { + console.log(` + +Setup script for installing deno + +Options: + -y, --yes + Skip interactive prompts and accept defaults + --no-modify-path + Don't add deno to the PATH environment variable + -h, --help + Print help +`); +} async function main() { - if (Deno.build.os === "windows" || !Deno.stdin.isTerminal()) { - return; - } if (Deno.args.length === 0) { throw new Error( "Expected the deno install directory as the first argument" ); } + const args = parseArgs(Deno.args.slice(1), { + boolean: ["yes", "no-modify-path", "help"], + alias: { + "yes": "y", + "help": "h" + }, + default: { + yes: false, + "no-modify-path": false + }, + unknown: (arg) => { + if (arg.startsWith("-")) { + printHelp(); + console.error(`Unknown flag ${arg}. Shell will not be configured`); + Deno.exit(1); + } + return false; + } + }); + if (args.help) { + printHelp(); + return; + } + if (Deno.build.os === "windows" || !args.yes && !(Deno.stdin.isTerminal() && Deno.stdout.isTerminal())) { + return; + } const installDir = Deno.args[0].trim(); const backupDir = join3(installDir, ".shellRcBackups"); try { - await setupShells(installDir, backupDir); + await setupShells(installDir, backupDir, { + skipPrompts: args.yes, + noModifyPath: args["no-modify-path"] + }); } catch (_e) { warn( `Failed to configure your shell environments, you may need to manually add deno to your PATH environment variable. diff --git a/shell-setup/deno.json b/shell-setup/deno.json index 1c13703..41b93a9 100644 --- a/shell-setup/deno.json +++ b/shell-setup/deno.json @@ -10,11 +10,12 @@ }, "license": "../LICENSE", "imports": { - "@nathanwhit/promptly": "jsr:@nathanwhit/promptly@^0.1.2", "@david/which": "jsr:@david/which@^0.4.1", + "@nathanwhit/promptly": "jsr:@nathanwhit/promptly@^0.1.2", + "@std/cli": "jsr:@std/cli@^1.0.6", "@std/path": "jsr:@std/path@^1.0.4" }, - "exclude": [ - "./bundle.ts" - ] + "publish": { + "exclude": ["./bundle.ts"] + } } diff --git a/shell-setup/deno.lock b/shell-setup/deno.lock index c4acd60..b27dc85 100644 --- a/shell-setup/deno.lock +++ b/shell-setup/deno.lock @@ -1,25 +1,74 @@ { "version": "4", "specifiers": { + "jsr:@david/dax@*": "0.42.0", + "jsr:@david/path@0.2": "0.2.0", "jsr:@david/which@~0.4.1": "0.4.1", + "jsr:@denosaurs/plug@1.0.5": "1.0.5", "jsr:@luca/esbuild-deno-loader@*": "0.10.3", "jsr:@nathanwhit/promptly@~0.1.2": "0.1.2", + "jsr:@sigma/pty-ffi@*": "0.26.2", + "jsr:@std/assert@*": "0.221.0", + "jsr:@std/assert@0.214": "0.214.0", + "jsr:@std/assert@0.221": "0.221.0", "jsr:@std/assert@~0.213.1": "0.213.1", + "jsr:@std/bytes@0.221": "0.221.0", + "jsr:@std/cli@^1.0.6": "1.0.6", "jsr:@std/encoding@0.213": "0.213.1", + "jsr:@std/encoding@0.214": "0.214.0", + "jsr:@std/fmt@0.214": "0.214.0", + "jsr:@std/fmt@0.221": "0.221.0", + "jsr:@std/fmt@1": "1.0.2", "jsr:@std/fmt@^1.0.2": "1.0.2", + "jsr:@std/fmt@~0.213.1": "0.213.1", + "jsr:@std/fs@0.214": "0.214.0", + "jsr:@std/fs@1": "1.0.4", + "jsr:@std/io@0.221": "0.221.0", "jsr:@std/jsonc@0.213": "0.213.1", "jsr:@std/path@0.213": "0.213.1", + "jsr:@std/path@0.214": "0.214.0", + "jsr:@std/path@1": "1.0.6", "jsr:@std/path@^1.0.4": "1.0.6", + "jsr:@std/path@^1.0.6": "1.0.6", + "jsr:@std/streams@0.221": "0.221.0", "npm:esbuild@*": "0.23.1" }, "jsr": { + "@david/dax@0.42.0": { + "integrity": "0c547c9a20577a6072b90def194c159c9ddab82280285ebfd8268a4ebefbd80b", + "dependencies": [ + "jsr:@david/path", + "jsr:@david/which", + "jsr:@std/fmt@1", + "jsr:@std/fs@1", + "jsr:@std/io", + "jsr:@std/path@1", + "jsr:@std/streams" + ] + }, + "@david/path@0.2.0": { + "integrity": "f2d7aa7f02ce5a55e27c09f9f1381794acb09d328f8d3c8a2e3ab3ffc294dccd", + "dependencies": [ + "jsr:@std/fs@1", + "jsr:@std/path@1" + ] + }, "@david/which@0.4.1": { "integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e" }, + "@denosaurs/plug@1.0.5": { + "integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf", + "dependencies": [ + "jsr:@std/encoding@0.214", + "jsr:@std/fmt@0.214", + "jsr:@std/fs@0.214", + "jsr:@std/path@0.214" + ] + }, "@luca/esbuild-deno-loader@0.10.3": { "integrity": "32fc93f7e7f78060234fd5929a740668aab1c742b808c6048b57f9aaea514921", "dependencies": [ - "jsr:@std/encoding", + "jsr:@std/encoding@0.213", "jsr:@std/jsonc", "jsr:@std/path@0.213" ] @@ -27,32 +76,100 @@ "@nathanwhit/promptly@0.1.2": { "integrity": "f434ebd37b103e2b9c5569578fb531c855c39980d1b186c0f508aaefe4060d06", "dependencies": [ - "jsr:@std/fmt" + "jsr:@std/fmt@^1.0.2" + ] + }, + "@sigma/pty-ffi@0.26.2": { + "integrity": "1f75f765eceddf051a2c7f064ba12070fb35f74bf8b4e31d8b2734961735f823", + "dependencies": [ + "jsr:@denosaurs/plug" ] }, "@std/assert@0.213.1": { - "integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe" + "integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe", + "dependencies": [ + "jsr:@std/fmt@~0.213.1" + ] + }, + "@std/assert@0.214.0": { + "integrity": "55d398de76a9828fd3b1aa653f4dba3eee4c6985d90c514865d2be9bd082b140" + }, + "@std/assert@0.221.0": { + "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a", + "dependencies": [ + "jsr:@std/fmt@0.221" + ] + }, + "@std/bytes@0.221.0": { + "integrity": "64a047011cf833890a4a2ab7293ac55a1b4f5a050624ebc6a0159c357de91966" + }, + "@std/cli@1.0.6": { + "integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d" }, "@std/encoding@0.213.1": { "integrity": "fcbb6928713dde941a18ca5db88ca1544d0755ec8fb20fe61e2dc8144b390c62" }, + "@std/encoding@0.214.0": { + "integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5" + }, + "@std/fmt@0.213.1": { + "integrity": "a06d31777566d874b9c856c10244ac3e6b660bdec4c82506cd46be052a1082c3" + }, + "@std/fmt@0.214.0": { + "integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4" + }, + "@std/fmt@0.221.0": { + "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" + }, "@std/fmt@1.0.2": { "integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7" }, + "@std/fs@0.214.0": { + "integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea", + "dependencies": [ + "jsr:@std/assert@0.214", + "jsr:@std/path@0.214" + ] + }, + "@std/fs@1.0.4": { + "integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c", + "dependencies": [ + "jsr:@std/path@^1.0.6" + ] + }, + "@std/io@0.221.0": { + "integrity": "faf7f8700d46ab527fa05cc6167f4b97701a06c413024431c6b4d207caa010da", + "dependencies": [ + "jsr:@std/assert@0.221", + "jsr:@std/bytes" + ] + }, "@std/jsonc@0.213.1": { "integrity": "5578f21aa583b7eb7317eed077ffcde47b294f1056bdbb9aacec407758637bfe", "dependencies": [ - "jsr:@std/assert" + "jsr:@std/assert@~0.213.1" ] }, "@std/path@0.213.1": { "integrity": "f187bf278a172752e02fcbacf6bd78a335ed320d080a7ed3a5a59c3e88abc673", "dependencies": [ - "jsr:@std/assert" + "jsr:@std/assert@~0.213.1" + ] + }, + "@std/path@0.214.0": { + "integrity": "d5577c0b8d66f7e8e3586d864ebdf178bb326145a3611da5a51c961740300285", + "dependencies": [ + "jsr:@std/assert@0.214" ] }, "@std/path@1.0.6": { "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" + }, + "@std/streams@0.221.0": { + "integrity": "47f2f74634b47449277c0ee79fe878da4424b66bd8975c032e3afdca88986e61", + "dependencies": [ + "jsr:@std/io" + ] } }, "npm": { @@ -164,6 +281,7 @@ "dependencies": [ "jsr:@david/which@~0.4.1", "jsr:@nathanwhit/promptly@~0.1.2", + "jsr:@std/cli@^1.0.6", "jsr:@std/path@^1.0.4" ] } diff --git a/shell-setup/src/main.ts b/shell-setup/src/main.ts index 9e8b005..548eac2 100644 --- a/shell-setup/src/main.ts +++ b/shell-setup/src/main.ts @@ -1,6 +1,7 @@ import { environment } from "./environment.ts"; import { basename, dirname, join } from "@std/path"; import { confirm, multiSelect } from "@nathanwhit/promptly"; +import { parseArgs } from "@std/cli/parse-args"; import { Bash, @@ -197,7 +198,6 @@ async function updateRcFile( } } catch (_error) { prepend = prepend ? ensureEndsWith(prepend, "\n") : prepend; - append = append ? ensureStartsWith(append, "\n") : append; } if (!prepend && !append) { return false; @@ -266,7 +266,20 @@ async function getAvailableShells(): Promise { return present; } -async function setupShells(installDir: string, backupDir: string) { +interface SetupOpts { + skipPrompts: boolean; + noModifyPath: boolean; +} + +async function setupShells( + installDir: string, + backupDir: string, + opts: SetupOpts, +) { + const { + skipPrompts, + noModifyPath, + } = opts; const availableShells = await getAvailableShells(); await writeEnvFiles(availableShells, installDir); @@ -274,9 +287,10 @@ async function setupShells(installDir: string, backupDir: string) { const backups = new Backups(backupDir); if ( - await confirm(`Edit shell configs to add deno to the PATH?`, { - default: true, - }) + (skipPrompts && !noModifyPath) || (!skipPrompts && + await confirm(`Edit shell configs to add deno to the PATH?`, { + default: true, + })) ) { await ensureExists(backupDir); await addToPath(availableShells, installDir, backups); @@ -288,7 +302,7 @@ async function setupShells(installDir: string, backupDir: string) { const shellsWithCompletion = availableShells.filter((s) => s.supportsCompletion !== false ); - const selected = await multiSelect( + const selected = skipPrompts ? [] : await multiSelect( { message: `Set up completions?`, options: shellsWithCompletion.map((s) => { @@ -314,24 +328,68 @@ async function setupShells(installDir: string, backupDir: string) { } } -async function main() { - if (Deno.build.os === "windows" || !Deno.stdin.isTerminal()) { - // the powershell script already handles setting up the path - return; - } +function printHelp() { + console.log(`\n +Setup script for installing deno + +Options: + -y, --yes + Skip interactive prompts and accept defaults + --no-modify-path + Don't add deno to the PATH environment variable + -h, --help + Print help\n`); +} +async function main() { if (Deno.args.length === 0) { throw new Error( "Expected the deno install directory as the first argument", ); } + const args = parseArgs(Deno.args.slice(1), { + boolean: ["yes", "no-modify-path", "help"], + alias: { + "yes": "y", + "help": "h", + }, + default: { + yes: false, + "no-modify-path": false, + }, + unknown: (arg: string) => { + if (arg.startsWith("-")) { + printHelp(); + console.error(`Unknown flag ${arg}. Shell will not be configured`); + Deno.exit(1); + } + return false; + }, + }); + + if (args.help) { + printHelp(); + return; + } + + if ( + Deno.build.os === "windows" || (!args.yes && !(Deno.stdin.isTerminal() && + Deno.stdout.isTerminal())) + ) { + // the powershell script already handles setting up the path + return; + } + const installDir = Deno.args[0].trim(); const backupDir = join(installDir, ".shellRcBackups"); try { - await setupShells(installDir, backupDir); + await setupShells(installDir, backupDir, { + skipPrompts: args.yes, + noModifyPath: args["no-modify-path"], + }); } catch (_e) { warn( `Failed to configure your shell environments, you may need to manually add deno to your PATH environment variable.