diff --git a/app/package.json b/app/package.json index 31cfadb6ace1..ae52b3eed48d 100644 --- a/app/package.json +++ b/app/package.json @@ -33,6 +33,7 @@ "react-dom": "17.0.1", "semver": "7.3.4", "shell-env": "3.0.1", + "sudo-prompt": "^9.2.1", "uuid": "8.3.2" }, "optionalDependencies": { diff --git a/app/session.ts b/app/session.ts index d92c88bfc8f2..9858213e8126 100644 --- a/app/session.ts +++ b/app/session.ts @@ -5,6 +5,8 @@ import {getDecoratedEnv} from './plugins'; import {productName, version} from './package.json'; import * as config from './config'; import {IPty, IWindowsPtyForkOptions, spawn as npSpawn} from 'node-pty'; +import {cliScriptPath} from './config/paths'; +import {dirname} from 'path'; const createNodePtyError = () => new Error( @@ -118,6 +120,14 @@ export default class Session extends EventEmitter { envFromConfig ); + // path to AppImage mount point is added to PATH environment variable automatically + // which conflicts with the cli + if (baseEnv['APPIMAGE'] && baseEnv['APPDIR']) { + baseEnv['PATH'] = [dirname(cliScriptPath)] + .concat((baseEnv['PATH'] || '').split(':').filter((val) => !val.startsWith(baseEnv['APPDIR']))) + .join(':'); + } + // Electron has a default value for process.env.GOOGLE_API_KEY // We don't want to leak this to the shell // See https://github.com/vercel/hyper/issues/696 diff --git a/app/utils/cli-install.ts b/app/utils/cli-install.ts index 1d83129e6168..ff98d55de4da 100644 --- a/app/utils/cli-install.ts +++ b/app/utils/cli-install.ts @@ -5,9 +5,13 @@ import notify from '../notify'; import {cliScriptPath, cliLinkPath} from '../config/paths'; import {Registry, loadRegistry} from './registry'; import type {ValueType} from 'native-reg'; +import sudoPrompt from 'sudo-prompt'; +import {clipboard, dialog} from 'electron'; +import {mkdirpSync} from 'fs-extra'; const readlink = pify(fs.readlink); const symlink = pify(fs.symlink); +const sudoExec = pify(sudoPrompt.exec, {multiArgs: true}); const checkInstall = () => { return readlink(cliLinkPath) @@ -20,15 +24,51 @@ const checkInstall = () => { }); }; -const addSymlink = () => { - return checkInstall().then((isInstalled) => { +const addSymlink = async (silent: boolean) => { + try { + const isInstalled = await checkInstall(); if (isInstalled) { console.log('Hyper CLI already in PATH'); - return Promise.resolve(); + return; } console.log('Linking HyperCLI'); - return symlink(cliScriptPath, cliLinkPath); - }); + if (!fs.existsSync(path.dirname(cliLinkPath))) { + try { + mkdirpSync(path.dirname(cliLinkPath)); + } catch (err) { + throw `Failed to create directory ${path.dirname(cliLinkPath)} - ${err}`; + } + } + await symlink(cliScriptPath, cliLinkPath); + } catch (err) { + // 'EINVAL' is returned by readlink, + // 'EEXIST' is returned by symlink + let error = + err.code === 'EEXIST' || err.code === 'EINVAL' + ? `File already exists: ${cliLinkPath}` + : `Symlink creation failed: ${err.code}`; + // Need sudo access to create symlink + if (err.code === 'EACCES' && !silent) { + const result = await dialog.showMessageBox({ + message: `You need to grant elevated privileges to add Hyper CLI to PATH +Or you can run +sudo ln -sf "${cliScriptPath}" "${cliLinkPath}"`, + type: 'info', + buttons: ['OK', 'Copy Command', 'Cancel'] + }); + if (result.response === 0) { + try { + await sudoExec(`ln -sf "${cliScriptPath}" "${cliLinkPath}"`, {name: 'Hyper'}); + return; + } catch (_error) { + error = _error[0]; + } + } else if (result.response === 1) { + clipboard.writeText(`sudo ln -sf "${cliScriptPath}" "${cliLinkPath}"`); + } + } + throw error; + } }; const addBinToUserPath = () => { @@ -89,35 +129,31 @@ const logNotify = (withNotification: boolean, title: string, body: string, detai withNotification && notify(title, body, details); }; -export const installCLI = (withNotification: boolean) => { +export const installCLI = async (withNotification: boolean) => { if (process.platform === 'win32') { - addBinToUserPath() - .then(() => - logNotify( - withNotification, - 'Hyper CLI installed', - 'You may need to restart your computer to complete this installation process.' - ) - ) - .catch((err) => - logNotify(withNotification, 'Hyper CLI installation failed', `Failed to add Hyper CLI path to user PATH ${err}`) + try { + await addBinToUserPath(); + logNotify( + withNotification, + 'Hyper CLI installed', + 'You may need to restart your computer to complete this installation process.' ); - } else if (process.platform === 'darwin') { - addSymlink() - .then(() => logNotify(withNotification, 'Hyper CLI installed', `Symlink created at ${cliLinkPath}`)) - .catch((err) => { - // 'EINVAL' is returned by readlink, - // 'EEXIST' is returned by symlink - const error = - err.code === 'EEXIST' || err.code === 'EINVAL' - ? `File already exists: ${cliLinkPath}` - : `Symlink creation failed: ${err.code}`; - - console.error(err); - logNotify(withNotification, 'Hyper CLI installation failed', error); - }); + } catch (err) { + logNotify(withNotification, 'Hyper CLI installation failed', `Failed to add Hyper CLI path to user PATH ${err}`); + } + } else if (process.platform === 'darwin' || process.platform === 'linux') { + // AppImages are mounted on run at a temporary path, don't create symlink + if (process.env['APPIMAGE']) { + console.log('Skipping CLI symlink creation as it is an AppImage install'); + return; + } + try { + await addSymlink(!withNotification); + logNotify(withNotification, 'Hyper CLI installed', `Symlink created at ${cliLinkPath}`); + } catch (error) { + logNotify(withNotification, 'Hyper CLI installation failed', `${error}`); + } } else { - withNotification && - notify('Hyper CLI installation', 'Command is added in PATH only at package installation. Please reinstall.'); + logNotify(withNotification, 'Hyper CLI installation failed', `Unsupported platform ${process.platform}`); } }; diff --git a/app/yarn.lock b/app/yarn.lock index bcd2a725cc5f..dd277a0e77b4 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -823,6 +823,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +sudo-prompt@^9.2.1: + version "9.2.1" + resolved "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd" + integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"