From 88e5c781263bc4339e5f2f6ea6e4412768b60c28 Mon Sep 17 00:00:00 2001 From: maayan Date: Thu, 26 Sep 2024 18:32:02 -0400 Subject: [PATCH] refactor, add specific command flags --- .github/workflows/run-bin-script-on-mac.yaml | 5 +- .gitignore | 3 +- CONTRIBUTING.md | 19 +- bin/aptos | 201 ------------------- bin/aptos.ts | 32 +++ bin/tasks/install.ts | 56 ++++++ bin/tasks/run.ts | 28 +++ bin/tasks/update.ts | 33 +++ bin/utils/aptosExecutableIsAvailable.ts | 14 ++ bin/utils/brewOperations.ts | 23 +++ bin/utils/consts.ts | 3 + bin/utils/execSyncShell.ts | 8 + bin/utils/getAptosCliLatestVersion.ts | 14 ++ bin/utils/getLocalBinPath.ts | 26 +++ bin/utils/getUserOs.ts | 18 ++ bin/utils/ghOperations.ts | 20 ++ bin/utils/parseCommandOptions.ts | 19 ++ bin/utils/versions.ts | 9 + package.json | 33 ++- tsconfig.json | 18 ++ 20 files changed, 372 insertions(+), 210 deletions(-) delete mode 100755 bin/aptos create mode 100755 bin/aptos.ts create mode 100644 bin/tasks/install.ts create mode 100644 bin/tasks/run.ts create mode 100644 bin/tasks/update.ts create mode 100644 bin/utils/aptosExecutableIsAvailable.ts create mode 100644 bin/utils/brewOperations.ts create mode 100644 bin/utils/consts.ts create mode 100644 bin/utils/execSyncShell.ts create mode 100644 bin/utils/getAptosCliLatestVersion.ts create mode 100644 bin/utils/getLocalBinPath.ts create mode 100644 bin/utils/getUserOs.ts create mode 100644 bin/utils/ghOperations.ts create mode 100644 bin/utils/parseCommandOptions.ts create mode 100644 bin/utils/versions.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/run-bin-script-on-mac.yaml b/.github/workflows/run-bin-script-on-mac.yaml index 7021b0b..d123b38 100644 --- a/.github/workflows/run-bin-script-on-mac.yaml +++ b/.github/workflows/run-bin-script-on-mac.yaml @@ -15,4 +15,7 @@ jobs: uses: actions/checkout@v3 - name: Run the bin script on Mac - run: node bin/aptos + run: | + npm install + npm run build + node ./dist/aptos.js diff --git a/.gitignore b/.gitignore index a7905f3..ae7a4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/aptos-cli* package-lock.json -node_modules \ No newline at end of file +node_modules +dist \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2a45b8..3183af9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,19 @@ # Contributing -## Testing -To test changes to the script, do this: +Clone this repo: + +``` +git clone git@github.com:aptos-labs/aptos-cli.git +``` + +Install dependencies: + +``` +npm install +``` + +Run the bin file + ``` -pnpm install -./bin/aptos +npm run dev ``` diff --git a/bin/aptos b/bin/aptos deleted file mode 100755 index 17c71b6..0000000 --- a/bin/aptos +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env node - -// On MacOS we install the CLI with brew. There are two main reasons for this: -// 1. Brew builds the CLI for the native CPU architecture for the user, which -// eliminates any issues arising from using x86 binaries on ARM machines. -// 2. Brew handles dependency management for us. This isn't relevant right now but -// might become necessary later if we reintroduce OpenSSL as a dep for the CLI. -// -// On Linux and Windows we just query the GH API for the latest CLI release and -// download and extract that. - -const { execSync, spawn } = require("child_process"); -const fs = require("fs"); -const os = require("os"); - -const PNAME = "aptos-cli"; -const GH_CLI_DOWNLOAD_URL = - "https://github.com/aptos-labs/aptos-core/releases/download"; - -// Wrapper around execSync that uses the shell. -const execSyncShell = (command, options) => { - return execSync(command, { shell: true, ...options }); -}; - -// Determine what OS we're running on. -const getOS = () => { - const platform = os.platform(); - switch (platform) { - case "darwin": - return "MacOS"; - case "linux": - return "Ubuntu"; - case "win32": - return "Windows"; - default: - throw new Error(`Unsupported OS ${platform}`); - } -}; - -// Only works on Unix systems. This is fine because we only need to check for brew on -// MacOS. -const executableIsAvailable = (name) => { - try { - execSyncShell(`which ${name}`, { encoding: "utf8" }); - return true; - } catch (error) { - return false; - } -}; - -// Query the GitHub API to find the latest CLI release. We assume that the CLI is in -// the last 100 releases so we don't paginate through the releases. -const getLatestVersionGh = async () => { - const prefix = `${PNAME}-v`; - const response = await ( - await fetch( - "https://api.github.com/repos/aptos-labs/aptos-core/releases?per_page=100" - ) - ).json(); - for (release of response) { - if (release["tag_name"].startsWith(`${prefix}`)) { - return release.tag_name.replace(`${prefix}`, ""); - } - } - throw "Could not determine latest version of Aptos CLI"; -}; - -// Use brew to find the latest version of the CLI. Make sure to confirm that brew -// is installed before calling this function. -const getLatestVersionBrew = () => { - const out = JSON.parse( - execSyncShell("brew info --json aptos", { encoding: "utf8" }) - ); - return out[0].versions.stable; -}; - -// Determine the latest version of the CLI. -const getLatestVersion = async () => { - if (getOS() === "MacOS") { - return getLatestVersionBrew(); - } else { - return getLatestVersionGh(); - } -}; - -// Determine the current SSL version -const getCurrentOpenSSLVersion = () => { - const out = execSyncShell("openssl version", { encoding: "utf8" }); - return out.split(" ")[1].trim(); -}; - -// Based on the installation path of the aptos formula, determine the path where the -// CLI should be installed. -const getCliPathBrew = () => { - const directory = execSyncShell("brew --prefix aptos", { encoding: "utf8" }) - .toString() - .trim(); - return `${directory}/bin/aptos`; -}; - -// Install or update the CLI. -const installCli = (os, path, latestCLIVersion) => { - console.log(`Downloading aptos CLI version ${latestCLIVersion}`); - if (os === "Windows") { - const url = `${GH_CLI_DOWNLOAD_URL}/${PNAME}-v${latestCLIVersion}/${PNAME}-${latestCLIVersion}-${os}-x86_64.zip`; - // Download the zip file, extract it, and move the binary to the correct location. - execSync( - `powershell -Command "if (!(Test-Path -Path 'C:\\tmp')) { New-Item -ItemType Directory -Path 'C:\\tmp' } ; Invoke-RestMethod -Uri ${url} -OutFile C:\\tmp\\aptos.zip; Expand-Archive -Path C:\\tmp\\aptos.zip -DestinationPath C:\\tmp -Force; Move-Item -Path C:\\tmp\\aptos.exe -Destination ${path}"` - ); - } else if (os === "MacOS") { - // Install the CLI with brew. - execSyncShell("brew install aptos"); - // Get the path of the CLI. - path = getCliPathBrew(); - } else { - // On Linux, we check what version of OpenSSL we're working with to figure out - // which binary to download. - let osVersion = "x86_64"; - let opensSslVersion = "1.0.0"; - try { - opensSslVersion = getCurrentOpenSSLVersion(); - } catch (error) { - console.log( - "Could not determine OpenSSL version, assuming older version (1.x.x)" - ); - } - - if (opensSslVersion.startsWith("3.")) { - osVersion = "22.04-x86_64"; - } - console.log(`Downloading CLI binary ${os}-${osVersion}`); - const url = `${GH_CLI_DOWNLOAD_URL}/${PNAME}-v${latestCLIVersion}/${PNAME}-${latestCLIVersion}-${os}-${osVersion}.zip`; - // Download the zip file, extract it, and move the binary to the correct location. - execSync( - `curl -L -o /tmp/aptos.zip ${url}; unzip -o -q /tmp/aptos.zip -d /tmp; mv /tmp/aptos ${path};` - ); - } -}; - -const main = async () => { - const os = getOS(); - - let path; - if (os === "MacOS") { - // Confirm brew is installed. - const brewInstalled = executableIsAvailable("brew"); - if (!brewInstalled) { - throw "Please install brew to continue: https://brew.sh/"; - } - try { - path = getCliPathBrew(); - } catch (e) { - path = ""; - } - } else if (os === "Windows") { - path = `${__dirname}\\${PNAME}.exe`; - } else { - path = `${__dirname}/${PNAME}`; - } - - // Look up the latest version. - const latestVersion = await getLatestVersion(); - - // If binary does not exist, download it. - if (!fs.existsSync(path)) { - console.log("CLI not installed"); - // Install the latest version. - installCli(os, path, latestVersion); - } else { - // Get the current version of the CLI. - const currentVersion = execSyncShell(`${path} --version`, { - encoding: "utf8", - }) - .trim() - .split(" ")[1]; - console.log( - `Previously installed CLI version ${currentVersion}, checking for updates` - ); - // Check if the installed version is the latest version. - if (currentVersion !== latestVersion) { - console.log(`A newer version of the CLI is available: ${latestVersion}`); - installCli(os, path, latestVersion); - } else { - console.log(`CLI is up to date`); - } - } - - // Spawn a child process to execute the binary with the provided arguments. - if (os === "Windows") { - spawn(path, process.argv.slice(2), { - stdio: "inherit", - shell: true, - }); - } else { - spawn(path, process.argv.slice(2), { - stdio: "inherit", - }); - } -}; - -main().catch(console.error); diff --git a/bin/aptos.ts b/bin/aptos.ts new file mode 100755 index 0000000..7a55742 --- /dev/null +++ b/bin/aptos.ts @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +// On MacOS we install the CLI with brew. There are two main reasons for this: +// 1. Brew builds the CLI for the native CPU architecture for the user, which +// eliminates any issues arising from using x86 binaries on ARM machines. +// 2. Brew handles dependency management for us. This isn't relevant right now but +// might become necessary later if we reintroduce OpenSSL as a dep for the CLI. +// +// On Linux and Windows we just query the GH API for the latest CLI release and +// download and extract that. + +import { program } from "commander"; + +import { parseCommandOptions } from "./utils/parseCommandOptions.js"; + +program + .name("") + .description("") + .option("-i, --install", "install the latest version of the CLI") + .option("-u, --update", "update the CLI to the latest version"); + +program.parse(); + +const main = async () => { + const options = { + install: program.opts().install, + update: program.opts().update, + }; + await parseCommandOptions(options); +}; + +main().catch(console.error); diff --git a/bin/tasks/install.ts b/bin/tasks/install.ts new file mode 100644 index 0000000..096a1d4 --- /dev/null +++ b/bin/tasks/install.ts @@ -0,0 +1,56 @@ +import { execSync } from "child_process"; +import { existsSync } from "fs"; + +import { getCliPathBrew } from "../utils/brewOperations.js"; +import { GH_CLI_DOWNLOAD_URL, PNAME } from "../utils/consts.js"; +import { execSyncShell } from "../utils/execSyncShell.js"; +import { getCurrentOpenSSLVersion } from "../utils/versions.js"; +import { getOS } from "../utils/getUserOs.js"; +import { getLatestVersion } from "../utils/getAptosCliLatestVersion.js"; +import { getLocalBinPath } from "../utils/getLocalBinPath.js"; + +// Install the CLI. +export const installCli = async () => { + const path = getLocalBinPath(); + if (existsSync(path)) { + console.log("Aptos CLI is already installed"); + return; + } + // Look up the latest version. + const latestCLIVersion = await getLatestVersion(); + console.log(`Downloading aptos CLI version ${latestCLIVersion}`); + const os = getOS(); + + if (os === "Windows") { + const url = `${GH_CLI_DOWNLOAD_URL}/${PNAME}-v${latestCLIVersion}/${PNAME}-${latestCLIVersion}-${os}-x86_64.zip`; + // Download the zip file, extract it, and move the binary to the correct location. + execSync( + `powershell -Command "if (!(Test-Path -Path 'C:\\tmp')) { New-Item -ItemType Directory -Path 'C:\\tmp' } ; Invoke-RestMethod -Uri ${url} -OutFile C:\\tmp\\aptos.zip; Expand-Archive -Path C:\\tmp\\aptos.zip -DestinationPath C:\\tmp -Force; Move-Item -Path C:\\tmp\\aptos.exe -Destination ${path}"` + ); + } else if (os === "MacOS") { + // Install the CLI with brew. + execSyncShell("brew install aptos", { encoding: "utf8" }); + } else { + // On Linux, we check what version of OpenSSL we're working with to figure out + // which binary to download. + let osVersion = "x86_64"; + let opensSslVersion = "1.0.0"; + try { + opensSslVersion = getCurrentOpenSSLVersion(); + } catch (error) { + console.log( + "Could not determine OpenSSL version, assuming older version (1.x.x)" + ); + } + + if (opensSslVersion.startsWith("3.")) { + osVersion = "22.04-x86_64"; + } + console.log(`Downloading CLI binary ${os}-${osVersion}`); + const url = `${GH_CLI_DOWNLOAD_URL}/${PNAME}-v${latestCLIVersion}/${PNAME}-${latestCLIVersion}-${os}-${osVersion}.zip`; + // Download the zip file, extract it, and move the binary to the correct location. + execSync( + `curl -L -o /tmp/aptos.zip ${url}; unzip -o -q /tmp/aptos.zip -d /tmp; mv /tmp/aptos ${path};` + ); + } +}; diff --git a/bin/tasks/run.ts b/bin/tasks/run.ts new file mode 100644 index 0000000..9bda482 --- /dev/null +++ b/bin/tasks/run.ts @@ -0,0 +1,28 @@ +import { spawn } from "child_process"; +import { existsSync } from "fs"; + +import { getOS } from "../utils/getUserOs.js"; +import { getLocalBinPath } from "../utils/getLocalBinPath.js"; + +export const runCLI = async () => { + const path = getLocalBinPath(); + if (!existsSync(path)) { + console.log( + "Aptos CLI not installed, run `npx aptos --install` to install" + ); + return; + } + const os = getOS(); + + // Spawn a child process to execute the binary with the provided arguments. + if (os === "Windows") { + spawn(path, process.argv.slice(2), { + stdio: "inherit", + shell: true, + }); + } else { + spawn(path, process.argv.slice(2), { + stdio: "inherit", + }); + } +}; diff --git a/bin/tasks/update.ts b/bin/tasks/update.ts new file mode 100644 index 0000000..80f99fc --- /dev/null +++ b/bin/tasks/update.ts @@ -0,0 +1,33 @@ +import { existsSync } from "fs"; + +import { execSyncShell } from "../utils/execSyncShell.js"; +import { getLatestVersion } from "../utils/getAptosCliLatestVersion.js"; +import { installCli } from "./install.js"; +import { getLocalBinPath } from "../utils/getLocalBinPath.js"; + +export const updateCli = async () => { + const path = getLocalBinPath(); + if (!existsSync(path)) { + console.log( + "Aptos CLI not installed, run `npx aptos --install` to install" + ); + return; + } + // Look up the latest version. + const latestVersion = await getLatestVersion(); + // Get the current version of the CLI. + const currentVersion = execSyncShell(`${path} --version`, { + encoding: "utf8", + }) + .trim() + .split(" ")[1]; + // Check if the installed version is the latest version. + if (currentVersion !== latestVersion) { + console.log( + `A newer version of the CLI is available: ${latestVersion}, installing...` + ); + installCli(); + } else { + console.log(`CLI is up to date`); + } +}; diff --git a/bin/utils/aptosExecutableIsAvailable.ts b/bin/utils/aptosExecutableIsAvailable.ts new file mode 100644 index 0000000..3da6345 --- /dev/null +++ b/bin/utils/aptosExecutableIsAvailable.ts @@ -0,0 +1,14 @@ +import { execSyncShell } from "./execSyncShell.js"; + +/** + * Only works on Unix systems. This is fine because we only need to check for brew on + * MacOS. + */ +export const executableIsAvailable = (name) => { + try { + execSyncShell(`which ${name}`, { encoding: "utf8" }); + return true; + } catch (error) { + return false; + } +}; diff --git a/bin/utils/brewOperations.ts b/bin/utils/brewOperations.ts new file mode 100644 index 0000000..c38d5dd --- /dev/null +++ b/bin/utils/brewOperations.ts @@ -0,0 +1,23 @@ +import { execSyncShell } from "./execSyncShell.js"; + +/** + * Based on the installation path of the aptos formula, determine the path where the + * CLI should be installed. + */ +export const getCliPathBrew = () => { + const directory = execSyncShell("brew --prefix aptos", { encoding: "utf8" }) + .toString() + .trim(); + return `${directory}/bin/aptos`; +}; + +/** + * Use brew to find the latest version of the CLI. Make sure to confirm that brew + * is installed before calling this function. + */ +export const getLatestVersionBrew = () => { + const out = JSON.parse( + execSyncShell("brew info --json aptos", { encoding: "utf8" }) + ); + return out[0].versions.stable; +}; diff --git a/bin/utils/consts.ts b/bin/utils/consts.ts new file mode 100644 index 0000000..fa87bee --- /dev/null +++ b/bin/utils/consts.ts @@ -0,0 +1,3 @@ +export const PNAME = "aptos-cli"; +export const GH_CLI_DOWNLOAD_URL = + "https://github.com/aptos-labs/aptos-core/releases/download"; diff --git a/bin/utils/execSyncShell.ts b/bin/utils/execSyncShell.ts new file mode 100644 index 0000000..48338e3 --- /dev/null +++ b/bin/utils/execSyncShell.ts @@ -0,0 +1,8 @@ +import { execSync } from "child_process"; + +/** + * Wrapper around execSync that uses the shell. + */ +export const execSyncShell = (command, options) => { + return execSync(command, { shell: true, ...options }); +}; diff --git a/bin/utils/getAptosCliLatestVersion.ts b/bin/utils/getAptosCliLatestVersion.ts new file mode 100644 index 0000000..374a6e2 --- /dev/null +++ b/bin/utils/getAptosCliLatestVersion.ts @@ -0,0 +1,14 @@ +import { getLatestVersionBrew } from "./brewOperations.js"; +import { getOS } from "./getUserOs.js"; +import { getLatestVersionGh } from "./ghOperations.js"; + +/** + * Determine the latest version of the CLI. + */ +export const getLatestVersion = async () => { + if (getOS() === "MacOS") { + return getLatestVersionBrew(); + } else { + return getLatestVersionGh(); + } +}; diff --git a/bin/utils/getLocalBinPath.ts b/bin/utils/getLocalBinPath.ts new file mode 100644 index 0000000..de6c748 --- /dev/null +++ b/bin/utils/getLocalBinPath.ts @@ -0,0 +1,26 @@ +import { executableIsAvailable } from "./aptosExecutableIsAvailable.js"; +import { getCliPathBrew } from "./brewOperations.js"; +import { PNAME } from "./consts.js"; +import { getOS } from "./getUserOs.js"; + +export const getLocalBinPath = () => { + let path; + const os = getOS(); + if (os === "MacOS") { + // Confirm brew is installed. + const brewInstalled = executableIsAvailable("brew"); + if (!brewInstalled) { + throw "Please install brew to continue: https://brew.sh/"; + } + try { + path = getCliPathBrew(); + } catch (e) { + path = ""; + } + } else if (os === "Windows") { + path = `${__dirname}\\${PNAME}.exe`; + } else { + path = `${__dirname}/${PNAME}`; + } + return path; +}; diff --git a/bin/utils/getUserOs.ts b/bin/utils/getUserOs.ts new file mode 100644 index 0000000..f1f3a1f --- /dev/null +++ b/bin/utils/getUserOs.ts @@ -0,0 +1,18 @@ +import { platform } from "os"; + +/** + * Determine what OS we're running on. + */ +export const getOS = () => { + const osPlatform = platform(); + switch (osPlatform) { + case "darwin": + return "MacOS"; + case "linux": + return "Ubuntu"; + case "win32": + return "Windows"; + default: + throw new Error(`Unsupported OS ${osPlatform}`); + } +}; diff --git a/bin/utils/ghOperations.ts b/bin/utils/ghOperations.ts new file mode 100644 index 0000000..5dc7dd3 --- /dev/null +++ b/bin/utils/ghOperations.ts @@ -0,0 +1,20 @@ +import { PNAME } from "./consts.js"; + +/** + * Query the GitHub API to find the latest CLI release. We assume that the CLI is in + * the last 100 releases so we don't paginate through the releases. + */ +export const getLatestVersionGh = async () => { + const prefix = `${PNAME}-v`; + const response = await ( + await fetch( + "https://api.github.com/repos/aptos-labs/aptos-core/releases?per_page=100" + ) + ).json(); + for (const release of response) { + if (release["tag_name"].startsWith(`${prefix}`)) { + return release.tag_name.replace(`${prefix}`, ""); + } + } + throw "Could not determine latest version of Aptos CLI"; +}; diff --git a/bin/utils/parseCommandOptions.ts b/bin/utils/parseCommandOptions.ts new file mode 100644 index 0000000..82cd36a --- /dev/null +++ b/bin/utils/parseCommandOptions.ts @@ -0,0 +1,19 @@ +import { installCli } from "../tasks/install.js"; +import { runCLI } from "../tasks/run.js"; +import { updateCli } from "../tasks/update.js"; + +export const parseCommandOptions = async (options) => { + // if `--install` flag is set, only install the cli and dont run it + if (options.install) { + return installCli(); + } + + // if `--update` flag is set, update the cli and dont run it + if (options.update) { + return updateCli(); + } + + // if no flags are set, install and run the cli + await installCli(); + await runCLI(); +}; diff --git a/bin/utils/versions.ts b/bin/utils/versions.ts new file mode 100644 index 0000000..556d193 --- /dev/null +++ b/bin/utils/versions.ts @@ -0,0 +1,9 @@ +import { execSyncShell } from "./execSyncShell.js"; + +/** + * Determine the current SSL version + */ +export const getCurrentOpenSSLVersion = () => { + const out = execSyncShell("openssl version", { encoding: "utf8" }); + return out.split(" ")[1].trim(); +}; diff --git a/package.json b/package.json index 599dd41..ef1ac99 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,35 @@ "version": "0.2.0", "description": "Aptos CLI available from npmjs", "bin": { - "aptos": "bin/aptos" + "aptos": "./dist/aptos" }, - "author": "", - "license": "ISC" + "type": "module", + "author": "aptoslabs.com", + "keywords": [ + "aptos node cli", + "aptos" + ], + "repository": { + "type": "git", + "url": "https://github.com/aptos-labs/aptos-cli" + }, + "bugs": { + "url": "https://github.com/aptos-labs/aptos-cli/issues" + }, + "license": "ISC", + "scripts": { + "clean": "rm -rf dist", + "build": "npm run clean && tsc", + "dev": "npm run build && node ./dist/aptos.js" + }, + "files": [ + "bin" + ], + "dependencies": { + "commander": "^12.1.0" + }, + "devDependencies": { + "@types/node": "^20.5.0", + "typescript": "^5.6.2" + } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a7d0248 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "noImplicitAny": false, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "declaration": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "target": "ES2022", + "module": "ES2020" + }, + "include": ["./bin"], + "exclude": ["node_modules"] +}