summaryrefslogtreecommitdiff
path: root/installer/mod.ts
diff options
context:
space:
mode:
Diffstat (limited to 'installer/mod.ts')
-rw-r--r--installer/mod.ts270
1 files changed, 270 insertions, 0 deletions
diff --git a/installer/mod.ts b/installer/mod.ts
new file mode 100644
index 000000000..aff01f361
--- /dev/null
+++ b/installer/mod.ts
@@ -0,0 +1,270 @@
+#!/usr/bin/env deno --allow-all
+
+const {
+ args,
+ env,
+ readDirSync,
+ mkdirSync,
+ writeFile,
+ exit,
+ stdin,
+ stat,
+ readAll,
+ run,
+ remove
+} = Deno;
+import * as path from "../fs/path.ts";
+
+const encoder = new TextEncoder();
+const decoder = new TextDecoder("utf-8");
+
+enum Permission {
+ Read,
+ Write,
+ Net,
+ Env,
+ Run,
+ All
+}
+
+function getPermissionFromFlag(flag: string): Permission | undefined {
+ switch (flag) {
+ case "--allow-read":
+ return Permission.Read;
+ case "--allow-write":
+ return Permission.Write;
+ case "--allow-net":
+ return Permission.Net;
+ case "--allow-env":
+ return Permission.Env;
+ case "--allow-run":
+ return Permission.Run;
+ case "--allow-all":
+ return Permission.All;
+ case "-A":
+ return Permission.All;
+ }
+}
+
+function getFlagFromPermission(perm: Permission): string {
+ switch (perm) {
+ case Permission.Read:
+ return "--allow-read";
+ case Permission.Write:
+ return "--allow-write";
+ case Permission.Net:
+ return "--allow-net";
+ case Permission.Env:
+ return "--allow-env";
+ case Permission.Run:
+ return "--allow-run";
+ case Permission.All:
+ return "--allow-all";
+ }
+ return "";
+}
+
+async function readCharacter(): Promise<string> {
+ const byteArray = new Uint8Array(1024);
+ await stdin.read(byteArray);
+ const line = decoder.decode(byteArray);
+ return line[0];
+}
+
+async function yesNoPrompt(message: string): Promise<boolean> {
+ console.log(`${message} [yN]`);
+ const input = await readCharacter();
+ console.log();
+ return input === "y" || input === "Y";
+}
+
+function createDirIfNotExists(path: string): void {
+ try {
+ readDirSync(path);
+ } catch (e) {
+ mkdirSync(path, true);
+ }
+}
+
+function checkIfExistsInPath(path: string): boolean {
+ const { PATH } = env();
+
+ const paths = (PATH as string).split(":");
+
+ return paths.includes(path);
+}
+
+function getInstallerDir(): string {
+ const { HOME } = env();
+
+ if (!HOME) {
+ throw new Error("$HOME is not defined.");
+ }
+
+ return path.join(HOME, ".deno", "bin");
+}
+
+// TODO: fetch doesn't handle redirects yet - once it does this function
+// can be removed
+async function fetchWithRedirects(
+ url: string,
+ redirectLimit: number = 10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): Promise<any> {
+ // TODO: `Response` is not exposed in global so 'any'
+ const response = await fetch(url);
+
+ if (response.status === 301 || response.status === 302) {
+ if (redirectLimit > 0) {
+ const redirectUrl = response.headers.get("location")!;
+ return await fetchWithRedirects(redirectUrl, redirectLimit - 1);
+ }
+ }
+
+ return response;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+async function fetchModule(url: string): Promise<any> {
+ const response = await fetchWithRedirects(url);
+
+ if (response.status !== 200) {
+ // TODO: show more debug information like status and maybe body
+ throw new Error(`Failed to get remote script ${url}.`);
+ }
+
+ const body = await readAll(response.body);
+ return decoder.decode(body);
+}
+
+function showHelp(): void {
+ console.log(`deno installer
+ Install remote or local script as executables.
+
+USAGE:
+ deno https://deno.land/std/installer/mod.ts EXE_NAME SCRIPT_URL [FLAGS...]
+
+ARGS:
+ EXE_NAME Name for executable
+ SCRIPT_URL Local or remote URL of script to install
+ [FLAGS...] List of flags for script, both Deno permission and script specific flag can be used.
+ `);
+}
+
+export async function install(
+ moduleName: string,
+ moduleUrl: string,
+ flags: string[]
+): Promise<void> {
+ const installerDir = getInstallerDir();
+ createDirIfNotExists(installerDir);
+
+ const FILE_PATH = path.join(installerDir, moduleName);
+
+ let fileInfo;
+ try {
+ fileInfo = await stat(FILE_PATH);
+ } catch (e) {
+ // pass
+ }
+
+ if (fileInfo) {
+ const msg = `⚠️ ${moduleName} is already installed, do you want to overwrite it?`;
+ if (!(await yesNoPrompt(msg))) {
+ return;
+ }
+ }
+
+ // ensure script that is being installed exists
+ if (moduleUrl.startsWith("http")) {
+ // remote module
+ console.log(`Downloading: ${moduleUrl}\n`);
+ await fetchModule(moduleUrl);
+ } else {
+ // assume that it's local file
+ moduleUrl = path.resolve(moduleUrl);
+ console.log(`Looking for: ${moduleUrl}\n`);
+ await stat(moduleUrl);
+ }
+
+ const grantedPermissions: Permission[] = [];
+ const scriptArgs: string[] = [];
+
+ for (const flag of flags) {
+ const permission = getPermissionFromFlag(flag);
+ if (permission === undefined) {
+ scriptArgs.push(flag);
+ } else {
+ grantedPermissions.push(permission);
+ }
+ }
+
+ const commands = [
+ "deno",
+ ...grantedPermissions.map(getFlagFromPermission),
+ moduleUrl,
+ ...scriptArgs,
+ "$@"
+ ];
+
+ // TODO: add windows Version
+ const template = `#/bin/sh\n${commands.join(" ")}`;
+ await writeFile(FILE_PATH, encoder.encode(template));
+
+ const makeExecutable = run({ args: ["chmod", "+x", FILE_PATH] });
+ const { code } = await makeExecutable.status();
+ makeExecutable.close();
+
+ if (code !== 0) {
+ throw new Error("Failed to make file executable");
+ }
+
+ console.log(`✅ Successfully installed ${moduleName}.`);
+ // TODO: add Windows version
+ if (!checkIfExistsInPath(installerDir)) {
+ console.log("\nℹ️ Add ~/.deno/bin to PATH");
+ console.log(
+ " echo 'export PATH=\"$HOME/.deno/bin:$PATH\"' >> ~/.bashrc # change this to your shell"
+ );
+ }
+}
+
+export async function uninstall(moduleName: string): Promise<void> {
+ const installerDir = getInstallerDir();
+ const FILE_PATH = path.join(installerDir, moduleName);
+
+ try {
+ await stat(FILE_PATH);
+ } catch (e) {
+ if (e instanceof Deno.DenoError && e.kind === Deno.ErrorKind.NotFound) {
+ throw new Error(`ℹ️ ${moduleName} not found`);
+ }
+ }
+
+ await remove(FILE_PATH);
+ console.log(`ℹ️ Uninstalled ${moduleName}`);
+}
+
+async function main(): Promise<void> {
+ if (args.length < 3) {
+ return showHelp();
+ }
+
+ if (["-h", "--help"].includes(args[1])) {
+ return showHelp();
+ }
+
+ const moduleName = args[1];
+ const moduleUrl = args[2];
+ const flags = args.slice(3);
+ try {
+ await install(moduleName, moduleUrl, flags);
+ } catch (e) {
+ console.log(e);
+ exit(1);
+ }
+}
+
+if (import.meta.main) {
+ main();
+}