From 64f24f1bb167c3d0cb3beca51fd300eaec21cc27 Mon Sep 17 00:00:00 2001 From: Adam Majmudar <64697628+adam-maj@users.noreply.github.com> Date: Mon, 5 Dec 2022 21:40:31 -0500 Subject: [PATCH] CLI auto-updates on every run (#441) --- .changeset/chilly-cows-pay.md | 5 + packages/cli/package.json | 3 +- packages/cli/src/cli/index.ts | 168 +++++++++++++++--- .../cli/src/helpers/detect-local-packages.ts | 103 +++++++++++ yarn.lock | 7 + 5 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 .changeset/chilly-cows-pay.md create mode 100644 packages/cli/src/helpers/detect-local-packages.ts diff --git a/.changeset/chilly-cows-pay.md b/.changeset/chilly-cows-pay.md new file mode 100644 index 0000000000..8b61feb9fe --- /dev/null +++ b/.changeset/chilly-cows-pay.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +CLI autoupdates on run diff --git a/packages/cli/package.json b/packages/cli/package.json index 887304d765..6fd6f4d827 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,6 +32,7 @@ "@thirdweb-dev/sdk": "*", "async-retry": "^1.3.3", "commander": "^9.1.0", + "detect-package-manager": "^2.0.1", "enquirer": "^2.3.6", "ethers": "^5.7.2", "got": "11.8.5", @@ -73,4 +74,4 @@ "sourcemap": true, "clean": true } -} +} \ No newline at end of file diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts index c5ebc0ae83..0a3dbc8477 100644 --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -2,37 +2,37 @@ import { detectExtensions } from "../common/feature-detector"; import { processProject } from "../common/processor"; import { cliVersion, pkg } from "../constants/urls"; -import { info, logger } from "../core/helpers/logger"; +import { info, logger, spinner } from "../core/helpers/logger"; import { twCreate } from "../create/command"; import { deploy } from "../deploy"; +import { findPackageInstallation } from "../helpers/detect-local-packages"; import { upload } from "../storage/command"; import { ThirdwebStorage } from "@thirdweb-dev/storage"; import chalk from "chalk"; +import { exec, spawn } from "child_process"; import { Command } from "commander"; import open from "open"; +import prompts from "prompts"; const main = async () => { + // eslint-disable-next-line turbo/no-undeclared-env-vars + const skipIntro = process.env.THIRDWEB_CLI_SKIP_INTRO === "true"; + const program = new Command(); - //yes this has to look like this, eliminates whitespace - console.info(` - $$\\ $$\\ $$\\ $$\\ $$\\ - $$ | $$ | \\__| $$ | $$ | -$$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$$$$\\ -\\_$$ _| $$ __$$\\ $$ |$$ __$$\\ $$ __$$ |$$ | $$ | $$ |$$ __$$\\ $$ __$$\\ - $$ | $$ | $$ |$$ |$$ | \\__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | - $$ |$$\\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | - \\$$$$ |$$ | $$ |$$ |$$ | \\$$$$$$$ |\\$$$$$\\$$$$ |\\$$$$$$$\\ $$$$$$$ | - \\____/ \\__| \\__|\\__|\\__| \\_______| \\_____\\____/ \\_______|\\_______/ `); - console.info(`\n 💎 thirdweb-cli v${cliVersion} 💎\n`); - import("update-notifier").then(({ default: updateNotifier }) => { - updateNotifier({ - pkg, - shouldNotifyInNpmScript: true, - // check every time while we're still building the CLI - updateCheckInterval: 0, - }).notify(); - }); + // yes this has to look like this, eliminates whitespace + if (!skipIntro) { + console.info(` + $$\\ $$\\ $$\\ $$\\ $$\\ + $$ | $$ | \\__| $$ | $$ | + $$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$$$$\\ + \\_$$ _| $$ __$$\\ $$ |$$ __$$\\ $$ __$$ |$$ | $$ | $$ |$$ __$$\\ $$ __$$\\ + $$ | $$ | $$ |$$ |$$ | \\__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | + $$ |$$\\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | + \\$$$$ |$$ | $$ |$$ |$$ | \\$$$$$$$ |\\$$$$$\\$$$$ |\\$$$$$$$\\ $$$$$$$ | + \\____/ \\__| \\__|\\__|\\__| \\_______| \\_____\\____/ \\_______|\\_______/ `); + console.info(`\n 💎 thirdweb-cli v${cliVersion} 💎\n`); + } program .name("thirdweb-cli") @@ -198,6 +198,134 @@ $$$$$$\\ $$$$$$$\\ $$\\ $$$$$$\\ $$$$$$$ |$$\\ $$\\ $$\\ $$$$$$\\ $$$$ await detectExtensions(options); }); + if (!skipIntro) { + const versionSpinner = spinner("Checking for updates..."); + await import("update-notifier").then( + async ({ default: updateNotifier }) => { + const notifier = updateNotifier({ + pkg, + shouldNotifyInNpmScript: true, + // check every time while we're still building the CLI + updateCheckInterval: 0, + }); + + const versionInfo = await notifier.fetchInfo(); + versionSpinner.stop(); + + if (versionInfo.type !== "latest") { + const res = await prompts({ + type: "toggle", + name: "upgrade", + message: `A new version of the CLI is available. Would you like to upgrade?`, + initial: true, + active: "yes", + inactive: "no", + }); + + if (res.upgrade) { + const updateSpinner = spinner( + `Upgrading CLI to version ${versionInfo.latest}...`, + ); + + const clonedEnvironment = { ...process.env }; + clonedEnvironment.THIRDWEB_CLI_SKIP_INTRO = "true"; + + const installation = await findPackageInstallation(); + + // If the package isn't installed anywhere, just defer to npx thirdweb@latest + if (!installation) { + updateSpinner.succeed( + `Now using CLI version ${versionInfo.latest}. Continuing execution...`, + ); + + await new Promise((done, failed) => { + const shell = spawn( + `npx --yes thirdweb@latest ${process.argv + .slice(2) + .join(" ")}`, + [], + { stdio: "inherit", shell: true, env: clonedEnvironment }, + ); + shell.on("close", (code) => { + if (code === 0) { + done(""); + } else { + failed(); + } + }); + }); + + return process.exit(0); + } + + // Otherwise, get the correct command based on package manager and local vs. global + let command = ""; + switch (installation.packageManager) { + case "npm": + command = installation.isGlobal + ? `npm install -g thirdweb` + : `npm install thirdweb`; + break; + case "yarn": + command = installation.isGlobal + ? `yarn global add thirdweb` + : `yarn add thirdweb`; + break; + case "pnpm": + command = installation.isGlobal + ? `pnpm add -g thirdweb@latest` + : `pnpm add thirdweb@latest`; + break; + default: + console.error( + `Could not detect package manager in use, aborting automatic upgrade.\nIf you want to upgrade the CLI, please do it manually with your package manager.`, + ); + process.exit(1); + } + + await new Promise((done, failed) => { + exec(command, (err, stdout, stderr) => { + if (err) { + failed(err); + return; + } + + done({ stdout, stderr }); + }); + }); + + updateSpinner.succeed( + `Successfully upgraded CLI to version ${versionInfo.latest}. Continuing execution...`, + ); + + // If the package is installed globally with yarn or pnpm, then npx won't recognize it + // So we need to make sure to run the command directly + const executionCommand = + !installation.isGlobal || installation.packageManager === "npm" + ? `npx thirdweb` + : `thirdweb`; + await new Promise((done, failed) => { + const shell = spawn( + `${executionCommand} ${process.argv.slice(2).join(" ")}`, + [], + { stdio: "inherit", shell: true, env: clonedEnvironment }, + ); + shell.on("close", (code) => { + if (code === 0) { + done(""); + } else { + failed(); + } + }); + }); + + process.exit(0); + } + } + }, + ); + } + await program.parseAsync(); }; diff --git a/packages/cli/src/helpers/detect-local-packages.ts b/packages/cli/src/helpers/detect-local-packages.ts new file mode 100644 index 0000000000..f9f9a0fe5c --- /dev/null +++ b/packages/cli/src/helpers/detect-local-packages.ts @@ -0,0 +1,103 @@ +import { exec } from "child_process"; + +type PackageManager = "npm" | "yarn" | "pnpm"; + +interface Installation { + packageManager: PackageManager; + isGlobal: boolean; +} + +export async function findPackageInstallation(): Promise< + Installation | undefined +> { + const isLocal = await isInstalledLocally(); + if (isLocal) { + const packageManager = await import("detect-package-manager").then( + ({ detect }) => { + return detect(); + }, + ); + + return { packageManager, isGlobal: false }; + } + + const isGlobalNpm = await isInstalledGloballyWithNpm(); + if (isGlobalNpm) { + return { packageManager: "npm", isGlobal: true }; + } + + const isGlobalYarn = await isInstalledGloballyWithYarn(); + if (isGlobalYarn) { + return { packageManager: "yarn", isGlobal: true }; + } + + const isGlobalPnpm = await isInstalledGloballyWithPnpm(); + if (isGlobalPnpm) { + return { packageManager: "pnpm", isGlobal: true }; + } + + return undefined; +} + +// For LOCAL packages, npm ls picks up things installed by yarn and pnpm too +async function isInstalledLocally(): Promise { + const packages = await detectPackages("npm ls --depth=0"); + return containsThirdweb(packages || []); +} + +// But for GLOBAL packages, we need to use a separate command for each one +async function isInstalledGloballyWithNpm(): Promise { + const packages = await detectPackages("npm ls -g --depth=0"); + return containsThirdweb(packages || []); +} + +async function isInstalledGloballyWithYarn(): Promise { + const packages = await detectPackages("yarn --cwd `yarn global dir` list"); + return containsThirdweb(packages || []); +} + +async function isInstalledGloballyWithPnpm(): Promise { + const packages: string[] = await new Promise((resolve) => { + exec(`pnpm list -g --depth=0`, (err, stdout) => { + var pkgs: string[] = []; + pkgs = stdout.split("dependencies:")[1]?.trim().split("\n") || []; + pkgs = pkgs.map((pkg) => pkg.replace(" ", "@")); + resolve(pkgs); + }); + }); + return containsThirdweb(packages || []); +} + +function containsThirdweb(packages: string[]) { + return packages.some((pkg) => pkg.match(/^thirdweb/)); +} + +async function detectPackages(cmd: string): Promise { + return new Promise((resolve) => { + exec(cmd, (err, stdout) => { + var packages: string[] = []; + packages = stdout.split("\n"); + // We're one short on '-' to support both yarn and npm + packages = packages.filter(function (item) { + if (item.match(/^├─.+/g) !== null) { + return true; + } + if (item.match(/^└─.+/g) !== null) { + return true; + } + return undefined; + }); + packages = packages + .map(function (item) { + if (item.match(/^├─.+/g) !== null) { + return item.replace(/(^├──\s|^├─\s)/g, ""); + } + if (item.match(/^└─.+/g) !== null) { + return item.replace(/(^└──\s|^├─\s)/g, ""); + } + }) + .filter((item) => !!item) as string[]; + resolve(packages); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index ffae57f3ea..b904bc6d18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9764,6 +9764,13 @@ detect-node@2.1.0, detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +detect-package-manager@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-package-manager/-/detect-package-manager-2.0.1.tgz#6b182e3ae5e1826752bfef1de9a7b828cffa50d8" + integrity sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A== + dependencies: + execa "^5.1.1" + detect-port-alt@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275"