Skip to content

Commit 5700690

Browse files
authored
Add CLI tool to install/search plugins or launch app (vercel#2375)
1 parent bbb1cae commit 5700690

15 files changed

+790
-68
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# build output
22
dist
33
app/renderer
4+
bin/cli.*
45

56
# dependencies
67
node_modules

app/config.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ const _watch = function() {
1515
}
1616

1717
const onChange = () => {
18-
cfg = _import();
19-
notify('Configuration updated', 'Hyper configuration reloaded!');
20-
watchers.forEach(fn => fn());
21-
checkDeprecatedConfig();
18+
// Need to wait 100ms to ensure that write is complete
19+
setTimeout(() => {
20+
cfg = _import();
21+
notify('Configuration updated', 'Hyper configuration reloaded!');
22+
watchers.forEach(fn => fn());
23+
checkDeprecatedConfig();
24+
}, 100);
2225
};
2326

2427
if (process.platform === 'win32') {

app/config/paths.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const plugs = {
3535
cache: resolve(plugins, 'cache')
3636
};
3737
const yarn = resolve(__dirname, '../../bin/yarn-standalone.js');
38+
const cliScriptPath = resolve(__dirname, '../../bin/hyper');
3839

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

@@ -64,5 +65,6 @@ module.exports = {
6465
icon,
6566
defaultPlatformKeyPath,
6667
plugs,
67-
yarn
68+
yarn,
69+
cliScriptPath
6870
};

app/index.js

+8
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const config = require('./config');
6262
config.setup();
6363

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

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

@@ -98,6 +99,13 @@ if (isDev) {
9899
} else {
99100
//eslint-disable-next-line no-console
100101
console.log('running in prod mode');
102+
if (process.platform === 'win32') {
103+
//eslint-disable-next-line no-console
104+
addBinToUserPath().catch(err => console.error('Failed to add Hyper CLI path to user PATH', err));
105+
} else {
106+
//eslint-disable-next-line no-console
107+
addSymlink().catch(err => console.error('Failed to symlink Hyper CLI', err));
108+
}
101109
}
102110

103111
const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'index.html');

app/utils/cli-install.js

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
const pify = require('pify');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const Registry = require('winreg');
5+
6+
const {cliScriptPath} = require('../config/paths');
7+
8+
const lstat = pify(fs.lstat);
9+
const readlink = pify(fs.readlink);
10+
const unlink = pify(fs.unlink);
11+
const symlink = pify(fs.symlink);
12+
13+
const target = '/usr/local/bin/hyper';
14+
const source = cliScriptPath;
15+
16+
const checkInstall = () => {
17+
return lstat(target)
18+
.then(stat => stat.isSymbolicLink())
19+
.then(() => readlink(target))
20+
.then(link => link === source)
21+
.catch(err => {
22+
if (err.code === 'ENOENT') {
23+
return false;
24+
}
25+
throw err;
26+
});
27+
};
28+
29+
const createSymlink = () => {
30+
return unlink(target)
31+
.catch(err => {
32+
if (err.code === 'ENOENT') {
33+
return;
34+
}
35+
throw err;
36+
})
37+
.then(() => symlink(source, target));
38+
};
39+
40+
exports.addSymlink = () => {
41+
return checkInstall().then(isInstalled => {
42+
if (isInstalled) {
43+
return Promise.resolve();
44+
}
45+
return createSymlink();
46+
});
47+
};
48+
49+
exports.addBinToUserPath = () => {
50+
// Can't use pify because of param order of Registry.values callback
51+
return new Promise((resolve, reject) => {
52+
const envKey = new Registry({hive: 'HKCU', key: '\\Environment'});
53+
envKey.values((err, items) => {
54+
if (err) {
55+
reject(err);
56+
return;
57+
}
58+
// C:\Users\<user>\AppData\Local\hyper\app-<version>\resources\bin
59+
const binPath = path.dirname(cliScriptPath);
60+
// C:\Users\<user>\AppData\Local\hyper
61+
const basePath = path.resolve(binPath, '../../..');
62+
63+
const pathItem = items.find(item => item.name === 'Path');
64+
const pathParts = pathItem.value.split(';');
65+
const existingPath = pathParts.find(pathPart => pathPart === binPath);
66+
if (existingPath) {
67+
resolve();
68+
return;
69+
}
70+
71+
// Because version is in path we need to remove old path if present and add current path
72+
const newPathValue = pathParts
73+
.filter(pathPart => !pathPart.startsWith(basePath))
74+
.concat([binPath])
75+
.join(';');
76+
77+
envKey.set(pathItem.name, Registry.REG_SZ, newPathValue, error => {
78+
if (error) {
79+
reject(error);
80+
return;
81+
}
82+
resolve();
83+
});
84+
});
85+
});
86+
};

build/linux/after-install.tpl

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
3+
# Link to the CLI bootstrap
4+
ln -sf '/opt/${productFilename}/resources/bin/${executable}' '/usr/local/bin/${executable}'

build/linux/hyper

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env bash
2+
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/linux/bin/code.sh
3+
4+
# If root, ensure that --user-data-dir is specified
5+
if [ "$(id -u)" = "0" ]; then
6+
for i in $@
7+
do
8+
if [[ $i == --user-data-dir=* ]]; then
9+
DATA_DIR_SET=1
10+
fi
11+
done
12+
if [ -z $DATA_DIR_SET ]; then
13+
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
14+
exit 1
15+
fi
16+
fi
17+
18+
if [ ! -L $0 ]; then
19+
# if path is not a symlink, find relatively
20+
HYPER_PATH="$(dirname $0)/../.."
21+
else
22+
if which readlink >/dev/null; then
23+
# if readlink exists, follow the symlink and find relatively
24+
HYPER_PATH="$(dirname $(readlink -f $0))/../.."
25+
else
26+
# else use the standard install location
27+
HYPER_PATH="/opt/Hyper"
28+
fi
29+
fi
30+
31+
ELECTRON="$HYPER_PATH/hyper"
32+
CLI="$HYPER_PATH/resources/bin/cli.js"
33+
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
34+
exit $?

build/mac/hyper

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/darwin/bin/code.sh
3+
4+
function realpath() { /usr/bin/python -c "import os,sys; print os.path.realpath(sys.argv[1])" "$0"; }
5+
CONTENTS="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")"
6+
ELECTRON="$CONTENTS/MacOS/Hyper"
7+
CLI="$CONTENTS/Resources/bin/cli.js"
8+
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
9+
exit $?

build/win/hyper

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env bash
2+
# Deeply inspired by https://github.com/Microsoft/vscode/blob/1.17.0/resources/win/bin/code.sh
3+
4+
NAME="Hyper"
5+
HYPER_PATH="$(dirname "$(dirname "$(dirname "$(realpath "$0")")")")"
6+
ELECTRON="$HYPER_PATH/$NAME.exe"
7+
if grep -q Microsoft /proc/version; then
8+
echo "Warning! Due to WSL limitations, you can use CLI commands here. Please use Hyper CLI on cmd, PowerShell or GitBash/CygWin."
9+
echo "Please see: https://github.com/Microsoft/WSL/issues/1494"
10+
echo ""
11+
# If running under WSL don't pass cli.js to Electron as environment vars
12+
# cannot be transferred from WSL to Windows
13+
# See: https://github.com/Microsoft/BashOnWindows/issues/1363
14+
# https://github.com/Microsoft/BashOnWindows/issues/1494
15+
"$ELECTRON" "$@"
16+
exit $?
17+
fi
18+
if [ "$(expr substr $(uname -s) 1 9)" == "CYGWIN_NT" ]; then
19+
CLI=$(cygpath -m "$HYPER_PATH/resources/bin/cli.js")
20+
else
21+
CLI="$HYPER_PATH/resources/bin/cli.js"
22+
fi
23+
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"
24+
exit $?
25+

build/win/hyper.cmd

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@echo off
2+
setlocal
3+
set ELECTRON_RUN_AS_NODE=1
4+
call "%~dp0..\..\Hyper.exe" "%~dp0..\..\resources\bin\cli.js" %*
5+
endlocal

cli/api.js

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const fs = require('fs');
2+
const os = require('os');
3+
const npmName = require('npm-name');
4+
const pify = require('pify');
5+
const recast = require('recast');
6+
7+
const fileName = `${os.homedir()}/.hyper.js`;
8+
9+
let fileContents;
10+
let parsedFile;
11+
let plugins;
12+
let localPlugins;
13+
14+
try {
15+
fileContents = fs.readFileSync(fileName, 'utf8');
16+
17+
parsedFile = recast.parse(fileContents);
18+
19+
const properties = parsedFile.program.body[0].expression.right.properties;
20+
plugins = properties.find(property => {
21+
return property.key.name === 'plugins';
22+
}).value.elements;
23+
24+
localPlugins = properties.find(property => {
25+
return property.key.name === 'localPlugins';
26+
}).value.elements;
27+
} catch (err) {
28+
if (err.code !== 'ENOENT') {
29+
// ENOENT === !exists()
30+
throw err;
31+
}
32+
}
33+
34+
function exists() {
35+
return fileContents !== undefined;
36+
}
37+
38+
function isInstalled(plugin, locally) {
39+
const array = locally ? localPlugins : plugins;
40+
if (array && Array.isArray(array)) {
41+
return array.find(entry => entry.value === plugin) !== undefined;
42+
}
43+
return false;
44+
}
45+
46+
function save() {
47+
return pify(fs.writeFile)(fileName, recast.print(parsedFile).code, 'utf8');
48+
}
49+
50+
function existsOnNpm(plugin) {
51+
plugin = plugin.split('#')[0].split('@')[0];
52+
return npmName(plugin).then(unavailable => {
53+
if (unavailable) {
54+
const err = new Error(`${plugin} not found on npm`);
55+
err.code = 'NOT_FOUND_ON_NPM';
56+
throw err;
57+
}
58+
});
59+
}
60+
61+
function install(plugin, locally) {
62+
const array = locally ? localPlugins : plugins;
63+
return new Promise((resolve, reject) => {
64+
existsOnNpm(plugin)
65+
.then(() => {
66+
if (isInstalled(plugin, locally)) {
67+
return reject(`${plugin} is already installed`);
68+
}
69+
70+
array.push(recast.types.builders.literal(plugin));
71+
save()
72+
.then(resolve)
73+
.catch(err => reject(err));
74+
})
75+
.catch(err => {
76+
if (err.code === 'NOT_FOUND_ON_NPM') {
77+
reject(err.message);
78+
} else {
79+
reject(err);
80+
}
81+
});
82+
});
83+
}
84+
85+
function uninstall(plugin) {
86+
return new Promise((resolve, reject) => {
87+
if (!isInstalled(plugin)) {
88+
return reject(`${plugin} is not installed`);
89+
}
90+
91+
const index = plugins.findIndex(entry => entry.value === plugin);
92+
plugins.splice(index, 1);
93+
save()
94+
.then(resolve)
95+
.catch(err => reject(err));
96+
});
97+
}
98+
99+
function list() {
100+
if (Array.isArray(plugins)) {
101+
return plugins.map(plugin => plugin.value).join('\n');
102+
}
103+
return false;
104+
}
105+
106+
module.exports.configPath = fileName;
107+
module.exports.exists = exists;
108+
module.exports.existsOnNpm = existsOnNpm;
109+
module.exports.isInstalled = isInstalled;
110+
module.exports.install = install;
111+
module.exports.uninstall = uninstall;
112+
module.exports.list = list;

0 commit comments

Comments
 (0)