Skip to content

Commit

Permalink
Add CLI tool to install/search plugins or launch app (#2375)
Browse files Browse the repository at this point in the history
  • Loading branch information
chabou authored Jan 9, 2018
1 parent bbb1cae commit 5700690
Show file tree
Hide file tree
Showing 15 changed files with 790 additions and 68 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# build output
dist
app/renderer
bin/cli.*

# dependencies
node_modules
Expand Down
11 changes: 7 additions & 4 deletions app/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ const _watch = function() {
}

const onChange = () => {
cfg = _import();
notify('Configuration updated', 'Hyper configuration reloaded!');
watchers.forEach(fn => fn());
checkDeprecatedConfig();
// Need to wait 100ms to ensure that write is complete
setTimeout(() => {
cfg = _import();
notify('Configuration updated', 'Hyper configuration reloaded!');
watchers.forEach(fn => fn());
checkDeprecatedConfig();
}, 100);
};

if (process.platform === 'win32') {
Expand Down
4 changes: 3 additions & 1 deletion app/config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const plugs = {
cache: resolve(plugins, 'cache')
};
const yarn = resolve(__dirname, '../../bin/yarn-standalone.js');
const cliScriptPath = resolve(__dirname, '../../bin/hyper');

const icon = resolve(__dirname, '../static/icon96x96.png');

Expand Down Expand Up @@ -64,5 +65,6 @@ module.exports = {
icon,
defaultPlatformKeyPath,
plugs,
yarn
yarn,
cliScriptPath
};
8 changes: 8 additions & 0 deletions app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const config = require('./config');
config.setup();

const plugins = require('./plugins');
const {addSymlink, addBinToUserPath} = require('./utils/cli-install');

const AppMenu = require('./menus/menu');

Expand Down Expand Up @@ -98,6 +99,13 @@ if (isDev) {
} else {
//eslint-disable-next-line no-console
console.log('running in prod mode');
if (process.platform === 'win32') {
//eslint-disable-next-line no-console
addBinToUserPath().catch(err => console.error('Failed to add Hyper CLI path to user PATH', err));
} else {
//eslint-disable-next-line no-console
addSymlink().catch(err => console.error('Failed to symlink Hyper CLI', err));
}
}

const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'index.html');
Expand Down
86 changes: 86 additions & 0 deletions app/utils/cli-install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const pify = require('pify');
const fs = require('fs');
const path = require('path');
const Registry = require('winreg');

const {cliScriptPath} = require('../config/paths');

const lstat = pify(fs.lstat);
const readlink = pify(fs.readlink);
const unlink = pify(fs.unlink);
const symlink = pify(fs.symlink);

const target = '/usr/local/bin/hyper';
const source = cliScriptPath;

const checkInstall = () => {
return lstat(target)
.then(stat => stat.isSymbolicLink())
.then(() => readlink(target))
.then(link => link === source)
.catch(err => {
if (err.code === 'ENOENT') {
return false;
}
throw err;
});
};

const createSymlink = () => {
return unlink(target)
.catch(err => {
if (err.code === 'ENOENT') {
return;
}
throw err;
})
.then(() => symlink(source, target));
};

exports.addSymlink = () => {
return checkInstall().then(isInstalled => {
if (isInstalled) {
return Promise.resolve();
}
return createSymlink();
});
};

exports.addBinToUserPath = () => {
// Can't use pify because of param order of Registry.values callback
return new Promise((resolve, reject) => {
const envKey = new Registry({hive: 'HKCU', key: '\\Environment'});
envKey.values((err, items) => {
if (err) {
reject(err);
return;
}
// C:\Users\<user>\AppData\Local\hyper\app-<version>\resources\bin
const binPath = path.dirname(cliScriptPath);
// C:\Users\<user>\AppData\Local\hyper
const basePath = path.resolve(binPath, '../../..');

const pathItem = items.find(item => item.name === 'Path');
const pathParts = pathItem.value.split(';');
const existingPath = pathParts.find(pathPart => pathPart === binPath);
if (existingPath) {
resolve();
return;
}

// Because version is in path we need to remove old path if present and add current path
const newPathValue = pathParts
.filter(pathPart => !pathPart.startsWith(basePath))
.concat([binPath])
.join(';');

envKey.set(pathItem.name, Registry.REG_SZ, newPathValue, error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
};
4 changes: 4 additions & 0 deletions build/linux/after-install.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

# Link to the CLI bootstrap
ln -sf '/opt/${productFilename}/resources/bin/${executable}' '/usr/local/bin/${executable}'
34 changes: 34 additions & 0 deletions build/linux/hyper
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/linux/bin/code.sh

# If root, ensure that --user-data-dir is specified
if [ "$(id -u)" = "0" ]; then
for i in $@
do
if [[ $i == --user-data-dir=* ]]; then
DATA_DIR_SET=1
fi
done
if [ -z $DATA_DIR_SET ]; then
echo "It is recommended to start hyper as a normal user. To run as root, you must specify an alternate user data directory with the --user-data-dir argument." 1>&2
exit 1
fi
fi

if [ ! -L $0 ]; then
# if path is not a symlink, find relatively
HYPER_PATH="$(dirname $0)/../.."
else
if which readlink >/dev/null; then
# if readlink exists, follow the symlink and find relatively
HYPER_PATH="$(dirname $(readlink -f $0))/../.."
else
# else use the standard install location
HYPER_PATH="/opt/Hyper"
fi
fi

ELECTRON="$HYPER_PATH/hyper"
CLI="$HYPER_PATH/resources/bin/cli.js"
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
exit $?
9 changes: 9 additions & 0 deletions build/mac/hyper
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/darwin/bin/code.sh

function realpath() { /usr/bin/python -c "import os,sys; print os.path.realpath(sys.argv[1])" "$0"; }
CONTENTS="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")"
ELECTRON="$CONTENTS/MacOS/Hyper"
CLI="$CONTENTS/Resources/bin/cli.js"
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
exit $?
25 changes: 25 additions & 0 deletions build/win/hyper
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/win/bin/code.sh

NAME="Hyper"
HYPER_PATH="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")"
ELECTRON="$HYPER_PATH/$NAME.exe"
if grep -q Microsoft /proc/version; then
echo "Warning! Due to WSL limitations, you can use CLI commands here. Please use Hyper CLI on cmd, PowerShell or GitBash/CygWin."
echo "Please see: https://github.com/Microsoft/WSL/issues/1494"
echo ""
# If running under WSL don't pass cli.js to Electron as environment vars
# cannot be transferred from WSL to Windows
# See: https://github.com/Microsoft/BashOnWindows/issues/1363
# https://github.com/Microsoft/BashOnWindows/issues/1494
"$ELECTRON" "$@"
exit $?
fi
if [ "$(expr substr $(uname -s) 1 9)" == "CYGWIN_NT" ]; then
CLI=$(cygpath -m "$HYPER_PATH/resources/bin/cli.js")
else
CLI="$HYPER_PATH/resources/bin/cli.js"
fi
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
exit $?

5 changes: 5 additions & 0 deletions build/win/hyper.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@echo off
setlocal
set ELECTRON_RUN_AS_NODE=1
call "%~dp0..\..\Hyper.exe" "%~dp0..\..\resources\bin\cli.js" %*
endlocal
112 changes: 112 additions & 0 deletions cli/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const fs = require('fs');
const os = require('os');
const npmName = require('npm-name');
const pify = require('pify');
const recast = require('recast');

const fileName = `${os.homedir()}/.hyper.js`;

let fileContents;
let parsedFile;
let plugins;
let localPlugins;

try {
fileContents = fs.readFileSync(fileName, 'utf8');

parsedFile = recast.parse(fileContents);

const properties = parsedFile.program.body[0].expression.right.properties;
plugins = properties.find(property => {
return property.key.name === 'plugins';
}).value.elements;

localPlugins = properties.find(property => {
return property.key.name === 'localPlugins';
}).value.elements;
} catch (err) {
if (err.code !== 'ENOENT') {
// ENOENT === !exists()
throw err;
}
}

function exists() {
return fileContents !== undefined;
}

function isInstalled(plugin, locally) {
const array = locally ? localPlugins : plugins;
if (array && Array.isArray(array)) {
return array.find(entry => entry.value === plugin) !== undefined;
}
return false;
}

function save() {
return pify(fs.writeFile)(fileName, recast.print(parsedFile).code, 'utf8');
}

function existsOnNpm(plugin) {
plugin = plugin.split('#')[0].split('@')[0];
return npmName(plugin).then(unavailable => {
if (unavailable) {
const err = new Error(`${plugin} not found on npm`);
err.code = 'NOT_FOUND_ON_NPM';
throw err;
}
});
}

function install(plugin, locally) {
const array = locally ? localPlugins : plugins;
return new Promise((resolve, reject) => {
existsOnNpm(plugin)
.then(() => {
if (isInstalled(plugin, locally)) {
return reject(`${plugin} is already installed`);
}

array.push(recast.types.builders.literal(plugin));
save()
.then(resolve)
.catch(err => reject(err));
})
.catch(err => {
if (err.code === 'NOT_FOUND_ON_NPM') {
reject(err.message);
} else {
reject(err);
}
});
});
}

function uninstall(plugin) {
return new Promise((resolve, reject) => {
if (!isInstalled(plugin)) {
return reject(`${plugin} is not installed`);
}

const index = plugins.findIndex(entry => entry.value === plugin);
plugins.splice(index, 1);
save()
.then(resolve)
.catch(err => reject(err));
});
}

function list() {
if (Array.isArray(plugins)) {
return plugins.map(plugin => plugin.value).join('\n');
}
return false;
}

module.exports.configPath = fileName;
module.exports.exists = exists;
module.exports.existsOnNpm = existsOnNpm;
module.exports.isInstalled = isInstalled;
module.exports.install = install;
module.exports.uninstall = uninstall;
module.exports.list = list;
Loading

0 comments on commit 5700690

Please sign in to comment.