mirror of
https://github.com/ershisan99/go-npm.git
synced 2025-12-16 20:59:28 +00:00
Merge pull request #1 from RecuencoJones/bugfix/add-windows-ext-replace-opt
ADD win_ext to URL template variables
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.idea
|
||||
node_modules
|
||||
coverage
|
||||
bin
|
||||
|
||||
4
.travis.yml
Normal file
4
.travis.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "8"
|
||||
- "10"
|
||||
13
README.md
13
README.md
@@ -1,16 +1,16 @@
|
||||
## Go NPM
|
||||
### Distribute cross-platform Go binaries via NPM
|
||||
|
||||
Applications written in Golang are portable - you can easily cross-compile binaries that work on Windows, Mac, and Linux. But how do you distribute the binaries to customers? When you publish new releases, how do they update the binary?
|
||||
Applications written in Golang are portable - you can easily cross-compile binaries that work on Windows, Mac, and Linux. But how do you distribute the binaries to customers? When you publish new releases, how do they update the binary?
|
||||
|
||||
**Use NPM to distribute cross-platform Go binaries**
|
||||
|
||||
## Kidding me! Why NPM?
|
||||
* **Cross-platform**: NPM is the only popular package manager that works cross-platform.
|
||||
* **Lower barier to entry**: Most developers have NPM installed already.
|
||||
* **Cross-platform**: NPM is the only popular package manager that works cross-platform.
|
||||
* **Lower barier to entry**: Most developers have NPM installed already.
|
||||
* **Pain free publishing**: It just takes one command to publish - `npm publish`
|
||||
* **Dead simple install & update story**: `npm install/update -g your-awesome-app`
|
||||
* **Adds $PATH**: NPM will automatically add your binary location to $PATH and generate .cmd file for Windows. Your app just works after installation!
|
||||
* **Adds $PATH**: NPM will automatically add your binary location to $PATH and generate .cmd file for Windows. Your app just works after installation!
|
||||
|
||||
## Okay, tell me how?
|
||||
### 1. Publish your binaries
|
||||
@@ -76,7 +76,7 @@ You need to tell `go-npm` where to download the binaries from, and where to inst
|
||||
"url": "https://github.com/user/my-go-package/releases/download/v{{version}}/myGoPackage_{{version}}_{{platform}}_{{arch}}.tar.gz"
|
||||
```
|
||||
|
||||
* *name*: Name of the command users will use to run your binary.
|
||||
* *name*: Name of the command users will use to run your binary.
|
||||
* *path*: Temporary path where binaries will be downloaded to
|
||||
* *url*: HTTP Web server where binaries are hosted.
|
||||
|
||||
@@ -84,6 +84,7 @@ Following variables are available to customize the URL:
|
||||
* `{{version}}`: Version number read from `package.json` file. When you publish your package to NPM, it will use this version number. Ex: 0.0.1
|
||||
* `{{platform}}`: `$GOOS` value for the platform
|
||||
* `{{arch}}`: `$GOARCH` value for the architecture
|
||||
* `{{ win_ext }}`: optional `.exe` extension for windows assets.
|
||||
|
||||
If you use `goreleaser` to publish your modules, it will automatically set the right architecture & platform in your URL.
|
||||
|
||||
@@ -106,4 +107,4 @@ To Update:
|
||||
|
||||
---
|
||||
|
||||
With ❤️ to the community by [Sanath Kumar Ramesh](http://twitter.com/sanathkr_)
|
||||
With ❤️ to the community by [Sanath Kumar Ramesh](http://twitter.com/sanathkr_)
|
||||
|
||||
91
__test__/actions/install.spec.js
Normal file
91
__test__/actions/install.spec.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const { EventEmitter } = require('events');
|
||||
const request = require('request');
|
||||
const common = require('../../src/common');
|
||||
const install = require('../../src/actions/install');
|
||||
const move = require('../../src/assets/move');
|
||||
const untar = require('../../src/assets/untar');
|
||||
const verifyAndPlaceCallback = require('../../src/assets/binary');
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('mkdirp');
|
||||
jest.mock('request');
|
||||
jest.mock('../../src/common');
|
||||
jest.mock('../../src/assets/move');
|
||||
jest.mock('../../src/assets/untar');
|
||||
jest.mock('../../src/assets/binary');
|
||||
|
||||
describe('install()', () => {
|
||||
|
||||
let callback, requestEvents;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
callback = jest.fn();
|
||||
|
||||
requestEvents = new EventEmitter();
|
||||
});
|
||||
|
||||
it('should call callback with error if package.json did not return value' , () => {
|
||||
common.parsePackageJson.mockReturnValueOnce(undefined);
|
||||
|
||||
install(callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith('Invalid inputs');
|
||||
});
|
||||
|
||||
it('should call callback with error on request error', () => {
|
||||
request.mockReturnValueOnce(requestEvents);
|
||||
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
|
||||
|
||||
install(callback);
|
||||
|
||||
requestEvents.emit('error');
|
||||
|
||||
expect(callback).toHaveBeenCalledWith('Error downloading from URL: http://url');
|
||||
});
|
||||
|
||||
it('should call callback with error on response with status code different than 200', () => {
|
||||
request.mockReturnValueOnce(requestEvents);
|
||||
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
|
||||
|
||||
install(callback);
|
||||
|
||||
requestEvents.emit('response', { statusCode: 404 });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith('Error downloading binary. HTTP Status Code: 404');
|
||||
});
|
||||
|
||||
it('should pick move strategy if url is an uncompressed binary', () => {
|
||||
request.mockReturnValueOnce(requestEvents);
|
||||
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url' });
|
||||
|
||||
install(callback);
|
||||
|
||||
requestEvents.emit('response', { statusCode: 200 });
|
||||
|
||||
expect(move).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pick untar strategy if url ends with .tar.gz', () => {
|
||||
request.mockReturnValueOnce(requestEvents);
|
||||
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url.tar.gz' });
|
||||
|
||||
install(callback);
|
||||
|
||||
requestEvents.emit('response', { statusCode: 200 });
|
||||
|
||||
expect(untar).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call verifyAndPlaceCallback on success', () => {
|
||||
request.mockReturnValueOnce(requestEvents);
|
||||
common.parsePackageJson.mockReturnValueOnce({ url: 'http://url', binName: 'command', binPath: './bin' });
|
||||
move.mockImplementationOnce(({ onSuccess }) => onSuccess());
|
||||
|
||||
install(callback);
|
||||
|
||||
requestEvents.emit('response', { statusCode: 200 });
|
||||
|
||||
expect(verifyAndPlaceCallback).toHaveBeenCalledWith('command', './bin', callback);
|
||||
});
|
||||
});
|
||||
59
__test__/actions/uninstall.spec.js
Normal file
59
__test__/actions/uninstall.spec.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const fs = require('fs');
|
||||
const common = require('../../src/common');
|
||||
const uninstall = require('../../src/actions/uninstall');
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../src/common');
|
||||
|
||||
describe('uninstall()', () => {
|
||||
|
||||
let callback;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
callback = jest.fn();
|
||||
|
||||
common.parsePackageJson.mockReturnValueOnce({ binName: 'command' });
|
||||
});
|
||||
|
||||
it('should call callback with error if binary not found', () => {
|
||||
const error = new Error();
|
||||
|
||||
common.getInstallationPath.mockImplementationOnce((cb) => cb(error));
|
||||
|
||||
uninstall(callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should call unlinkSync with binary and installation path', () => {
|
||||
|
||||
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
|
||||
|
||||
uninstall(callback);
|
||||
|
||||
expect(fs.unlinkSync).toHaveBeenCalledWith('bin/command');
|
||||
});
|
||||
|
||||
it('should call callback on success', () => {
|
||||
|
||||
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
|
||||
|
||||
uninstall(callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('should call callback regardless of errors on unlink', () => {
|
||||
|
||||
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, './bin'));
|
||||
|
||||
fs.unlinkSync.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
uninstall(callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
51
__test__/assets/binary.spec.js
Normal file
51
__test__/assets/binary.spec.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const fs = require('fs');
|
||||
const common = require('../../src/common');
|
||||
const verifyAndPlaceBinary = require('../../src/assets/binary');
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../src/common');
|
||||
|
||||
describe('verifyAndPlaceBinary()', () => {
|
||||
let callback;
|
||||
|
||||
beforeEach(() => {
|
||||
callback = jest.fn();
|
||||
});
|
||||
|
||||
it('should call callback with error if binary downloaded differs from config', () => {
|
||||
fs.existsSync.mockReturnValueOnce(false);
|
||||
|
||||
verifyAndPlaceBinary('command', './bin', callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith('Downloaded binary does not contain the binary specified in configuration - command');
|
||||
});
|
||||
|
||||
it('should call callback with error if installation path cannot be found', () => {
|
||||
const error = new Error();
|
||||
|
||||
fs.existsSync.mockReturnValueOnce(true);
|
||||
common.getInstallationPath.mockImplementationOnce((cb) => cb(error));
|
||||
|
||||
verifyAndPlaceBinary('command', './bin', callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should call callback with null on success', () => {
|
||||
fs.existsSync.mockReturnValueOnce(true);
|
||||
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, '/usr/local/bin'));
|
||||
|
||||
verifyAndPlaceBinary('command', './bin', callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('should move the binary to installation directory', () => {
|
||||
fs.existsSync.mockReturnValueOnce(true);
|
||||
common.getInstallationPath.mockImplementationOnce((cb) => cb(null, '/usr/local/bin'));
|
||||
|
||||
verifyAndPlaceBinary('command', './bin', callback);
|
||||
|
||||
expect(fs.renameSync).toHaveBeenCalledWith('bin/command', '/usr/local/bin/command');
|
||||
});
|
||||
});
|
||||
49
__test__/assets/move.spec.js
Normal file
49
__test__/assets/move.spec.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const { EventEmitter } = require('events');
|
||||
const fs = require('fs');
|
||||
const move = require('../../src/assets/move');
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
describe('move()', () => {
|
||||
|
||||
let streamEvents, pipe, onSuccess, onError;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
streamEvents = new EventEmitter();
|
||||
|
||||
pipe = jest.fn();
|
||||
onSuccess = jest.fn();
|
||||
onError = jest.fn();
|
||||
createWriteStream = jest.fn();
|
||||
|
||||
fs.createWriteStream.mockReturnValueOnce(streamEvents);
|
||||
});
|
||||
|
||||
it('should download resource to given binPath', () => {
|
||||
|
||||
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||
|
||||
expect(fs.createWriteStream).toHaveBeenCalledWith('bin/command');
|
||||
});
|
||||
|
||||
it('should call onSuccess on stream closed', () => {
|
||||
|
||||
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||
|
||||
streamEvents.emit('close');
|
||||
|
||||
expect(onSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onError with error on write stream error', () => {
|
||||
|
||||
const error = new Error();
|
||||
|
||||
move({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||
|
||||
streamEvents.emit('error', error);
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
65
__test__/assets/untar.spec.js
Normal file
65
__test__/assets/untar.spec.js
Normal file
@@ -0,0 +1,65 @@
|
||||
const { EventEmitter } = require('events');
|
||||
const zlib = require('zlib');
|
||||
const tar = require('tar');
|
||||
const untar = require('../../src/assets/untar');
|
||||
|
||||
jest.mock('zlib');
|
||||
jest.mock('tar', () => ({
|
||||
Extract: jest.fn()
|
||||
}));
|
||||
|
||||
describe('untar()', () => {
|
||||
|
||||
let ungzEvents, untarEvents, pipe, onSuccess, onError;
|
||||
|
||||
beforeEach(() => {
|
||||
ungzEvents = new EventEmitter();
|
||||
untarEvents = new EventEmitter();
|
||||
|
||||
pipe = jest.fn();
|
||||
onSuccess = jest.fn();
|
||||
onError = jest.fn();
|
||||
|
||||
pipe.mockReturnValueOnce({ pipe });
|
||||
tar.Extract.mockReturnValueOnce(untarEvents);
|
||||
zlib.createGunzip.mockReturnValueOnce(ungzEvents);
|
||||
});
|
||||
|
||||
it('should download resource and untar to given binPath', () => {
|
||||
|
||||
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||
|
||||
expect(tar.Extract).toHaveBeenCalledWith({ path: './bin' });
|
||||
});
|
||||
|
||||
it('should call onSuccess on untar end', () => {
|
||||
|
||||
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||
|
||||
untarEvents.emit('end');
|
||||
|
||||
expect(onSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onError with error on ungz error', () => {
|
||||
|
||||
const error = new Error();
|
||||
|
||||
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||
|
||||
ungzEvents.emit('error', error);
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should call onError with error on untar error', () => {
|
||||
|
||||
const error = new Error();
|
||||
|
||||
untar({ opts: { binPath: './bin', binName: 'command' }, req: { pipe }, onSuccess, onError });
|
||||
|
||||
untarEvents.emit('error', error);
|
||||
|
||||
expect(onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
40
__test__/cli.spec.js
Normal file
40
__test__/cli.spec.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const cli = require('../src/cli');
|
||||
const install = require('../src/actions/install');
|
||||
|
||||
jest.mock('../src/actions/install');
|
||||
|
||||
describe('cli()', () => {
|
||||
let exit;
|
||||
|
||||
beforeEach(() => {
|
||||
exit = jest.fn();
|
||||
});
|
||||
|
||||
it('should exit with error if not enough args are supplied', () => {
|
||||
cli({ argv: [], exit });
|
||||
|
||||
expect(exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should exit with error if command does not exist', () => {
|
||||
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'command' ], exit });
|
||||
|
||||
expect(exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should exit with error if command returns error', () => {
|
||||
install.mockImplementationOnce((cb) => cb(new Error()));
|
||||
|
||||
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'install' ], exit });
|
||||
|
||||
expect(exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should exit with success if command runs fine', () => {
|
||||
install.mockImplementationOnce((cb) => cb(null));
|
||||
|
||||
cli({ argv: [ '/usr/local/bin/node', 'index.js', 'install' ], exit });
|
||||
|
||||
expect(exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
});
|
||||
123
__test__/common.spec.js
Normal file
123
__test__/common.spec.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const fs = require('fs');
|
||||
const childProcess = require('child_process');
|
||||
const common = require('../src/common');
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('child_process');
|
||||
jest.mock('mkdirp');
|
||||
|
||||
describe('common', () => {
|
||||
describe('getInstallationPath()', () => {
|
||||
let callback, env;
|
||||
|
||||
beforeEach(() => {
|
||||
callback = jest.fn();
|
||||
|
||||
env = { ...process.env };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = env;
|
||||
});
|
||||
|
||||
it('should get binaries path from `npm bin`', () => {
|
||||
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(null, '/usr/local/bin'));
|
||||
|
||||
common.getInstallationPath(callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(null, '/usr/local/bin');
|
||||
});
|
||||
|
||||
it('should get binaries path from env', () => {
|
||||
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(new Error()));
|
||||
|
||||
process.env.npm_config_prefix = '/usr/local';
|
||||
|
||||
common.getInstallationPath(callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(null, '/usr/local/bin');
|
||||
});
|
||||
|
||||
it('should call callback with error if binaries path is not found', () => {
|
||||
childProcess.exec.mockImplementationOnce((_cmd, cb) => cb(new Error()));
|
||||
|
||||
process.env.npm_config_prefix = undefined;
|
||||
|
||||
common.getInstallationPath(callback);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(new Error('Error finding binary installation directory'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsePackageJson()', () => {
|
||||
let _process;
|
||||
|
||||
beforeEach(() => {
|
||||
_process = { ...global.process };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.process = _process;
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
it('should return if architecture is unsupported', () => {
|
||||
process.arch = 'mips';
|
||||
|
||||
expect(common.parsePackageJson()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return if platform is unsupported', () => {
|
||||
process.platform = 'amiga';
|
||||
|
||||
expect(common.parsePackageJson()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return if package.json does not exist', () => {
|
||||
fs.existsSync.mockReturnValueOnce(false);
|
||||
|
||||
expect(common.parsePackageJson()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable replacement', () => {
|
||||
it('should append .exe extension on windows platform', () => {
|
||||
fs.existsSync.mockReturnValueOnce(true);
|
||||
fs.readFileSync.mockReturnValueOnce(JSON.stringify({
|
||||
version: '1.0.0',
|
||||
goBinary: {
|
||||
name: 'command',
|
||||
path: './bin',
|
||||
url: 'https://github.com/foo/bar/releases/v{{version}}/assets/command{{win_ext}}'
|
||||
}
|
||||
}));
|
||||
|
||||
process.platform = 'win32';
|
||||
|
||||
expect(common.parsePackageJson()).toMatchObject({
|
||||
binName: 'command.exe',
|
||||
url: 'https://github.com/foo/bar/releases/v1.0.0/assets/command.exe'
|
||||
});
|
||||
});
|
||||
|
||||
it('should not append .exe extension on platform different than windows', () => {
|
||||
fs.existsSync.mockReturnValueOnce(true);
|
||||
fs.readFileSync.mockReturnValueOnce(JSON.stringify({
|
||||
version: '1.0.0',
|
||||
goBinary: {
|
||||
name: 'command',
|
||||
path: './bin',
|
||||
url: 'https://github.com/foo/bar/releases/v{{version}}/assets/command{{win_ext}}'
|
||||
}
|
||||
}));
|
||||
|
||||
process.platform = 'darwin';
|
||||
|
||||
expect(common.parsePackageJson()).toMatchObject({
|
||||
binName: 'command',
|
||||
url: 'https://github.com/foo/bar/releases/v1.0.0/assets/command'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
const rewire = require('rewire');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
|
||||
describe('go-npm', function() {
|
||||
let mod;
|
||||
|
||||
beforeEach(function() {
|
||||
mod = rewire('../src/index.js');
|
||||
});
|
||||
|
||||
describe('Resource handling strategies', function() {
|
||||
|
||||
describe('untarStrategy()', function() {
|
||||
|
||||
let untarStrategy, ungzEvents, untarEvents, pipe, callback, zlib, createGunzip, tar;
|
||||
|
||||
beforeEach(function() {
|
||||
untarStrategy = mod.__get__('untarStrategy');
|
||||
zlib = mod.__get__('zlib');
|
||||
tar = mod.__get__('tar');
|
||||
ungzEvents = new EventEmitter();
|
||||
untarEvents = new EventEmitter();
|
||||
|
||||
createGunzip = jest.fn();
|
||||
pipe = jest.fn();
|
||||
callback = jest.fn();
|
||||
|
||||
pipe.mockReturnValueOnce({ pipe });
|
||||
createGunzip.mockReturnValueOnce(ungzEvents);
|
||||
jest.spyOn(tar, 'Extract').mockReturnValueOnce(untarEvents);
|
||||
|
||||
// jest.spyOn not working on read-only properties
|
||||
Object.defineProperty(zlib, 'createGunzip', { value: createGunzip });
|
||||
});
|
||||
|
||||
it('should download resource and untar to given binPath', function() {
|
||||
|
||||
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
||||
|
||||
expect(tar.Extract).toHaveBeenCalledWith({ path: './bin' });
|
||||
});
|
||||
|
||||
it('should call verifyAndPlaceBinary on untar end', function() {
|
||||
|
||||
const verifyAndPlaceBinary = jest.fn();
|
||||
|
||||
mod.__set__('verifyAndPlaceBinary', verifyAndPlaceBinary);
|
||||
|
||||
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
||||
|
||||
untarEvents.emit('end');
|
||||
|
||||
expect(verifyAndPlaceBinary).toHaveBeenCalledWith('command', './bin', callback);
|
||||
});
|
||||
|
||||
it('should call callback with error on ungz error', function() {
|
||||
|
||||
const error = new Error();
|
||||
|
||||
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
||||
|
||||
ungzEvents.emit('error', error);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should call callback with error on untar error', function() {
|
||||
|
||||
const error = new Error();
|
||||
|
||||
untarStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
||||
|
||||
untarEvents.emit('error', error);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveStrategy()', function() {
|
||||
|
||||
let moveStrategy, streamEvents, pipe, callback, fs;
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
moveStrategy = mod.__get__('moveStrategy');
|
||||
fs = mod.__get__('fs');
|
||||
streamEvents = new EventEmitter();
|
||||
|
||||
pipe = jest.fn();
|
||||
callback = jest.fn();
|
||||
|
||||
jest.spyOn(fs, 'createWriteStream').mockReturnValueOnce(streamEvents);
|
||||
});
|
||||
|
||||
it('should download resource to given binPath', function() {
|
||||
|
||||
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
||||
|
||||
expect(fs.createWriteStream).toHaveBeenCalledWith('bin/command');
|
||||
});
|
||||
|
||||
it('should call verifyAndPlaceBinary on stream closed', function() {
|
||||
|
||||
const verifyAndPlaceBinary = jest.fn();
|
||||
|
||||
mod.__set__('verifyAndPlaceBinary', verifyAndPlaceBinary);
|
||||
|
||||
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
||||
|
||||
streamEvents.emit('close');
|
||||
|
||||
expect(verifyAndPlaceBinary).toHaveBeenCalledWith('command', './bin', callback);
|
||||
});
|
||||
|
||||
it('should call callback with error on write stream error', function() {
|
||||
|
||||
const error = new Error();
|
||||
|
||||
moveStrategy({ binPath: './bin', binName: 'command' }, { pipe }, callback);
|
||||
|
||||
streamEvents.emit('error', error);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,6 @@
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"jest": "^24.5.0",
|
||||
"rewire": "^4.0.1"
|
||||
"jest": "^24.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
52
src/actions/install.js
Normal file
52
src/actions/install.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const mkdirp = require('mkdirp');
|
||||
const request = require('request');
|
||||
const { parsePackageJson } = require('../common');
|
||||
const verifyAndPlaceBinary = require('../assets/binary');
|
||||
|
||||
/**
|
||||
* Select a resource handling strategy based on given options.
|
||||
*/
|
||||
function getStrategy({ url }) {
|
||||
|
||||
if (url.endsWith('.tar.gz')) {
|
||||
return require('../assets/untar');
|
||||
} else {
|
||||
return require('../assets/move');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the configuration from application's package.json,
|
||||
* validates properties, downloads the binary, untars, and stores at
|
||||
* ./bin in the package's root. NPM already has support to install binary files
|
||||
* specific locations when invoked with "npm install -g"
|
||||
*
|
||||
* See: https://docs.npmjs.com/files/package.json#bin
|
||||
*/
|
||||
function install(callback) {
|
||||
|
||||
const opts = parsePackageJson();
|
||||
if (!opts) return callback('Invalid inputs');
|
||||
|
||||
mkdirp.sync(opts.binPath);
|
||||
|
||||
console.log('Downloading from URL: ' + opts.url);
|
||||
|
||||
const req = request({ uri: opts.url });
|
||||
|
||||
req.on('error', () => callback('Error downloading from URL: ' + opts.url));
|
||||
req.on('response', (res) => {
|
||||
if (res.statusCode !== 200) return callback('Error downloading binary. HTTP Status Code: ' + res.statusCode);
|
||||
|
||||
const strategy = getStrategy(opts);
|
||||
|
||||
strategy({
|
||||
opts,
|
||||
req,
|
||||
onSuccess: () => verifyAndPlaceBinary(opts.binName, opts.binPath, callback),
|
||||
onError: callback
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = install;
|
||||
24
src/actions/uninstall.js
Normal file
24
src/actions/uninstall.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { join } = require('path');
|
||||
const { unlinkSync } = require('fs');
|
||||
const { parsePackageJson, getInstallationPath } = require('../common');
|
||||
|
||||
function uninstall(callback) {
|
||||
|
||||
const { binName } = parsePackageJson();
|
||||
|
||||
getInstallationPath((err, installationPath) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
try {
|
||||
unlinkSync(join(installationPath, binName));
|
||||
} catch(ex) {
|
||||
// Ignore errors when deleting the file.
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = uninstall;
|
||||
25
src/assets/binary.js
Normal file
25
src/assets/binary.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const { join } = require('path');
|
||||
const { existsSync, renameSync, chmodSync } = require('fs');
|
||||
const { getInstallationPath } = require('../common');
|
||||
|
||||
function verifyAndPlaceBinary(binName, binPath, callback) {
|
||||
if (!existsSync(join(binPath, binName))) {
|
||||
return callback(`Downloaded binary does not contain the binary specified in configuration - ${binName}`);
|
||||
}
|
||||
|
||||
getInstallationPath((err, installationPath) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Move the binary file and make sure it is executable
|
||||
renameSync(join(binPath, binName), join(installationPath, binName));
|
||||
chmodSync(join(installationPath, binName), '755');
|
||||
|
||||
console.log('Placed binary on', join(installationPath, binName));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = verifyAndPlaceBinary;
|
||||
17
src/assets/move.js
Normal file
17
src/assets/move.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { join } = require('path');
|
||||
const { createWriteStream } = require('fs');
|
||||
|
||||
/**
|
||||
* Move strategy for binary resources without compression.
|
||||
*/
|
||||
function move({ opts, req, onSuccess, onError }) {
|
||||
|
||||
const stream = createWriteStream(join(opts.binPath, opts.binName));
|
||||
|
||||
stream.on('error', onError);
|
||||
stream.on('close', onSuccess);
|
||||
|
||||
req.pipe(stream);
|
||||
}
|
||||
|
||||
module.exports = move;
|
||||
25
src/assets/untar.js
Normal file
25
src/assets/untar.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const tar = require('tar');
|
||||
const zlib = require('zlib');
|
||||
|
||||
/**
|
||||
* Unzip strategy for resources using `.tar.gz`.
|
||||
*
|
||||
* First we will Un-GZip, then we will untar. So once untar is completed,
|
||||
* binary is downloaded into `binPath`. Verify the binary and call it good.
|
||||
*/
|
||||
function untar({ opts, req, onSuccess, onError }) {
|
||||
|
||||
const ungz = zlib.createGunzip();
|
||||
const untar = tar.Extract({ path: opts.binPath });
|
||||
|
||||
ungz.on('error', onError);
|
||||
untar.on('error', onError);
|
||||
|
||||
// First we will Un-GZip, then we will untar. So once untar is completed,
|
||||
// binary is downloaded into `binPath`. Verify the binary and call it good
|
||||
untar.on('end', onSuccess);
|
||||
|
||||
req.pipe(ungz).pipe(untar);
|
||||
}
|
||||
|
||||
module.exports = untar;
|
||||
28
src/cli.js
Normal file
28
src/cli.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const actions = {
|
||||
install: (callback) => require('./actions/install')(callback),
|
||||
uninstall: (callback) => require('./actions/uninstall')(callback)
|
||||
};
|
||||
|
||||
// Parse command line arguments and call the right action
|
||||
module.exports = ({ argv, exit }) => {
|
||||
if (argv && argv.length > 2) {
|
||||
const cmd = argv[2];
|
||||
|
||||
if (!actions[cmd]) {
|
||||
console.log('Invalid command to go-npm. `install` and `uninstall` are the only supported commands');
|
||||
exit(1);
|
||||
} else {
|
||||
actions[cmd]((err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
exit(1);
|
||||
} else {
|
||||
exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('No command supplied. `install` and `uninstall` are the only supported commands');
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
130
src/common.js
Normal file
130
src/common.js
Normal file
@@ -0,0 +1,130 @@
|
||||
const { join } = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const { existsSync, readFileSync } = require('fs');
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
// Mapping from Node's `process.arch` to Golang's `$GOARCH`
|
||||
const ARCH_MAPPING = {
|
||||
ia32: '386',
|
||||
x64: 'amd64',
|
||||
arm: 'arm'
|
||||
};
|
||||
|
||||
// Mapping between Node's `process.platform` to Golang's
|
||||
const PLATFORM_MAPPING = {
|
||||
darwin: 'darwin',
|
||||
linux: 'linux',
|
||||
win32: 'windows',
|
||||
freebsd: 'freebsd'
|
||||
};
|
||||
|
||||
function getInstallationPath(callback) {
|
||||
|
||||
// `npm bin` will output the path where binary files should be installed
|
||||
exec('npm bin', (err, stdout, stderr) => {
|
||||
|
||||
let dir = null;
|
||||
if (err || stderr || !stdout || stdout.length === 0) {
|
||||
|
||||
// We couldn't infer path from `npm bin`. Let's try to get it from
|
||||
// Environment variables set by NPM when it runs.
|
||||
// npm_config_prefix points to NPM's installation directory where `bin` folder is available
|
||||
// Ex: /Users/foo/.nvm/versions/node/v4.3.0
|
||||
const env = process.env;
|
||||
|
||||
if (env && env.npm_config_prefix) {
|
||||
dir = join(env.npm_config_prefix, 'bin');
|
||||
} else {
|
||||
return callback(new Error('Error finding binary installation directory'));
|
||||
}
|
||||
} else {
|
||||
dir = stdout.trim();
|
||||
}
|
||||
|
||||
mkdirp.sync(dir);
|
||||
|
||||
callback(null, dir);
|
||||
});
|
||||
}
|
||||
|
||||
function validateConfiguration({ version, goBinary }) {
|
||||
|
||||
if (!version) {
|
||||
return "'version' property must be specified";
|
||||
}
|
||||
|
||||
if (!goBinary || typeof (goBinary) !== 'object') {
|
||||
return "'goBinary' property must be defined and be an object";
|
||||
}
|
||||
|
||||
if (!goBinary.name) {
|
||||
return "'name' property is necessary";
|
||||
}
|
||||
|
||||
if (!goBinary.path) {
|
||||
return "'path' property is necessary";
|
||||
}
|
||||
|
||||
if (!goBinary.url) {
|
||||
return "'url' property is required";
|
||||
}
|
||||
}
|
||||
|
||||
function parsePackageJson() {
|
||||
if (!(process.arch in ARCH_MAPPING)) {
|
||||
console.error('Installation is not supported for this architecture: ' + process.arch);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(process.platform in PLATFORM_MAPPING)) {
|
||||
console.error('Installation is not supported for this platform: ' + process.platform);
|
||||
return
|
||||
}
|
||||
|
||||
const packageJsonPath = join('.', 'package.json');
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
console.error('Unable to find package.json. ' +
|
||||
'Please run this script at root of the package you want to be installed');
|
||||
return
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath));
|
||||
const error = validateConfiguration(packageJson);
|
||||
|
||||
if (error && error.length > 0) {
|
||||
console.error('Invalid package.json: ' + error);
|
||||
return
|
||||
}
|
||||
|
||||
// We have validated the config. It exists in all its glory
|
||||
const binPath = packageJson.goBinary.path;
|
||||
let binName = packageJson.goBinary.name;
|
||||
let url = packageJson.goBinary.url;
|
||||
let version = packageJson.version;
|
||||
|
||||
if (version[0] === 'v') version = version.substr(1); // strip the 'v' if necessary v0.0.1 => 0.0.1
|
||||
|
||||
// Binary name on Windows has .exe suffix
|
||||
if (process.platform === 'win32') {
|
||||
binName += '.exe'
|
||||
|
||||
url = url.replace(/{{win_ext}}/g, '.exe');
|
||||
} else {
|
||||
url = url.replace(/{{win_ext}}/g, '');
|
||||
}
|
||||
|
||||
// Interpolate variables in URL, if necessary
|
||||
url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]);
|
||||
url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
|
||||
url = url.replace(/{{version}}/g, version);
|
||||
url = url.replace(/{{bin_name}}/g, binName);
|
||||
|
||||
return {
|
||||
binName,
|
||||
binPath,
|
||||
url,
|
||||
version
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { parsePackageJson, getInstallationPath };
|
||||
260
src/index.js
260
src/index.js
@@ -1,261 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
"use strict"
|
||||
|
||||
const request = require('request'),
|
||||
path = require('path'),
|
||||
tar = require('tar'),
|
||||
zlib = require('zlib'),
|
||||
mkdirp = require('mkdirp'),
|
||||
fs = require('fs'),
|
||||
exec = require('child_process').exec;
|
||||
|
||||
// Mapping from Node's `process.arch` to Golang's `$GOARCH`
|
||||
const ARCH_MAPPING = {
|
||||
"ia32": "386",
|
||||
"x64": "amd64",
|
||||
"arm": "arm"
|
||||
};
|
||||
|
||||
// Mapping between Node's `process.platform` to Golang's
|
||||
const PLATFORM_MAPPING = {
|
||||
"darwin": "darwin",
|
||||
"linux": "linux",
|
||||
"win32": "windows",
|
||||
"freebsd": "freebsd"
|
||||
};
|
||||
|
||||
function getInstallationPath(callback) {
|
||||
|
||||
// `npm bin` will output the path where binary files should be installed
|
||||
exec("npm bin", function(err, stdout, stderr) {
|
||||
|
||||
let dir = null;
|
||||
if (err || stderr || !stdout || stdout.length === 0) {
|
||||
|
||||
// We couldn't infer path from `npm bin`. Let's try to get it from
|
||||
// Environment variables set by NPM when it runs.
|
||||
// npm_config_prefix points to NPM's installation directory where `bin` folder is available
|
||||
// Ex: /Users/foo/.nvm/versions/node/v4.3.0
|
||||
let env = process.env;
|
||||
if (env && env.npm_config_prefix) {
|
||||
dir = path.join(env.npm_config_prefix, "bin");
|
||||
}
|
||||
} else {
|
||||
dir = stdout.trim();
|
||||
}
|
||||
|
||||
mkdirp.sync(dir);
|
||||
|
||||
callback(null, dir);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function verifyAndPlaceBinary(binName, binPath, callback) {
|
||||
if (!fs.existsSync(path.join(binPath, binName))) return callback(`Downloaded binary does not contain the binary specified in configuration - ${binName}`);
|
||||
|
||||
getInstallationPath(function(err, installationPath) {
|
||||
if (err) return callback("Error getting binary installation path from `npm bin`");
|
||||
|
||||
// Move the binary file and make sure it is executable
|
||||
fs.renameSync(path.join(binPath, binName), path.join(installationPath, binName));
|
||||
fs.chmodSync(path.join(installationPath, binName), "755");
|
||||
|
||||
console.log("Placed binary on", path.join(installationPath, binName));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function validateConfiguration(packageJson) {
|
||||
|
||||
if (!packageJson.version) {
|
||||
return "'version' property must be specified";
|
||||
}
|
||||
|
||||
if (!packageJson.goBinary || typeof(packageJson.goBinary) !== "object") {
|
||||
return "'goBinary' property must be defined and be an object";
|
||||
}
|
||||
|
||||
if (!packageJson.goBinary.name) {
|
||||
return "'name' property is necessary";
|
||||
}
|
||||
|
||||
if (!packageJson.goBinary.path) {
|
||||
return "'path' property is necessary";
|
||||
}
|
||||
|
||||
if (!packageJson.goBinary.url) {
|
||||
return "'url' property is required";
|
||||
}
|
||||
|
||||
// if (!packageJson.bin || typeof(packageJson.bin) !== "object") {
|
||||
// return "'bin' property of package.json must be defined and be an object";
|
||||
// }
|
||||
}
|
||||
|
||||
function parsePackageJson() {
|
||||
if (!(process.arch in ARCH_MAPPING)) {
|
||||
console.error("Installation is not supported for this architecture: " + process.arch);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(process.platform in PLATFORM_MAPPING)) {
|
||||
console.error("Installation is not supported for this platform: " + process.platform);
|
||||
return
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(".", "package.json");
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
console.error("Unable to find package.json. " +
|
||||
"Please run this script at root of the package you want to be installed");
|
||||
return
|
||||
}
|
||||
|
||||
let packageJson = JSON.parse(fs.readFileSync(packageJsonPath));
|
||||
let error = validateConfiguration(packageJson);
|
||||
if (error && error.length > 0) {
|
||||
console.error("Invalid package.json: " + error);
|
||||
return
|
||||
}
|
||||
|
||||
// We have validated the config. It exists in all its glory
|
||||
let binName = packageJson.goBinary.name;
|
||||
let binPath = packageJson.goBinary.path;
|
||||
let url = packageJson.goBinary.url;
|
||||
let version = packageJson.version;
|
||||
if (version[0] === 'v') version = version.substr(1); // strip the 'v' if necessary v0.0.1 => 0.0.1
|
||||
|
||||
// Binary name on Windows has .exe suffix
|
||||
if (process.platform === "win32") {
|
||||
binName += ".exe"
|
||||
}
|
||||
|
||||
// Interpolate variables in URL, if necessary
|
||||
url = url.replace(/{{arch}}/g, ARCH_MAPPING[process.arch]);
|
||||
url = url.replace(/{{platform}}/g, PLATFORM_MAPPING[process.platform]);
|
||||
url = url.replace(/{{version}}/g, version);
|
||||
url = url.replace(/{{bin_name}}/g, binName);
|
||||
|
||||
return {
|
||||
binName: binName,
|
||||
binPath: binPath,
|
||||
url: url,
|
||||
version: version
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzip strategy for resources using `.tar.gz`.
|
||||
*
|
||||
* First we will Un-GZip, then we will untar. So once untar is completed,
|
||||
* binary is downloaded into `binPath`. Verify the binary and call it good.
|
||||
*/
|
||||
function untarStrategy(opts, req, callback) {
|
||||
|
||||
const ungz = zlib.createGunzip();
|
||||
const untar = tar.Extract({path: opts.binPath});
|
||||
|
||||
ungz.on('error', callback);
|
||||
untar.on('error', callback);
|
||||
|
||||
// First we will Un-GZip, then we will untar. So once untar is completed,
|
||||
// binary is downloaded into `binPath`. Verify the binary and call it good
|
||||
untar.on('end', verifyAndPlaceBinary.bind(null, opts.binName, opts.binPath, callback));
|
||||
|
||||
req.pipe(ungz).pipe(untar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move strategy for binary resources without compression.
|
||||
*/
|
||||
function moveStrategy(opts, req, callback) {
|
||||
|
||||
const stream = fs.createWriteStream(path.join(opts.binPath, opts.binName));
|
||||
|
||||
stream.on('error', callback);
|
||||
stream.on('close', verifyAndPlaceBinary.bind(null, opts.binName, opts.binPath, callback));
|
||||
|
||||
req.pipe(stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a resource handling strategy based on given options.
|
||||
*/
|
||||
function getStrategy(opts) {
|
||||
|
||||
if (opts.url.endsWith('.tar.gz')) {
|
||||
return untarStrategy;
|
||||
} else {
|
||||
return moveStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the configuration from application's package.json,
|
||||
* validates properties, downloads the binary, untars, and stores at
|
||||
* ./bin in the package's root. NPM already has support to install binary files
|
||||
* specific locations when invoked with "npm install -g"
|
||||
*
|
||||
* See: https://docs.npmjs.com/files/package.json#bin
|
||||
*/
|
||||
const INVALID_INPUT = "Invalid inputs";
|
||||
function install(callback) {
|
||||
|
||||
const opts = parsePackageJson();
|
||||
if (!opts) return callback(INVALID_INPUT);
|
||||
|
||||
const strategy = getStrategy(opts);
|
||||
mkdirp.sync(opts.binPath);
|
||||
|
||||
console.log("Downloading from URL: " + opts.url);
|
||||
let req = request({uri: opts.url});
|
||||
req.on('error', callback.bind(null, "Error downloading from URL: " + opts.url));
|
||||
req.on('response', function(res) {
|
||||
if (res.statusCode !== 200) return callback("Error downloading binary. HTTP Status Code: " + res.statusCode);
|
||||
|
||||
strategy(opts, req, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function uninstall(callback) {
|
||||
|
||||
let opts = parsePackageJson();
|
||||
getInstallationPath(function(err, installationPath) {
|
||||
if (err) callback("Error finding binary installation directory");
|
||||
|
||||
try {
|
||||
fs.unlinkSync(path.join(installationPath, opts.binName));
|
||||
} catch(ex) {
|
||||
// Ignore errors when deleting the file.
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Parse command line arguments and call the right method
|
||||
let actions = {
|
||||
"install": install,
|
||||
"uninstall": uninstall
|
||||
};
|
||||
|
||||
let argv = process.argv;
|
||||
if (argv && argv.length > 2) {
|
||||
let cmd = process.argv[2];
|
||||
if (!actions[cmd]) {
|
||||
console.log("Invalid command to go-npm. `install` and `uninstall` are the only supported commands");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
actions[cmd](function(err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
require('./cli')(process);
|
||||
|
||||
Reference in New Issue
Block a user