From 13a160fb795a69bad1edbb3fc5fd5c7c15396e03 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 15 Mar 2019 22:33:19 -0400 Subject: [PATCH] move shared CLI items to jslib --- package-lock.json | 24 +++- package.json | 2 + src/cli/commands/update.command.ts | 83 ++++++++++++ src/cli/models/response.ts | 43 ++++++ src/cli/models/response/baseResponse.ts | 3 + src/cli/models/response/listResponse.ts | 11 ++ src/cli/models/response/messageResponse.ts | 15 +++ src/cli/models/response/stringResponse.ts | 11 ++ src/cli/services/cliPlatformUtils.service.ts | 132 +++++++++++++++++++ src/cli/services/consoleLog.service.ts | 53 ++++++++ src/services/noopMessaging.service.ts | 7 + 11 files changed, 380 insertions(+), 4 deletions(-) create mode 100644 src/cli/commands/update.command.ts create mode 100644 src/cli/models/response.ts create mode 100644 src/cli/models/response/baseResponse.ts create mode 100644 src/cli/models/response/listResponse.ts create mode 100644 src/cli/models/response/messageResponse.ts create mode 100644 src/cli/models/response/stringResponse.ts create mode 100644 src/cli/services/cliPlatformUtils.service.ts create mode 100644 src/cli/services/consoleLog.service.ts create mode 100644 src/services/noopMessaging.service.ts diff --git a/package-lock.json b/package-lock.json index ad035477f0..40a167e3e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,6 +97,15 @@ "msgpack5": "^4.0.2" } }, + "@types/commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==", + "dev": true, + "requires": { + "commander": "*" + } + }, "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", @@ -1497,10 +1506,9 @@ } }, "commander": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", - "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", - "dev": true + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", + "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==" }, "compare-versions": { "version": "3.3.1", @@ -1558,6 +1566,14 @@ "spawn-command": "^0.0.2-1", "supports-color": "^3.2.3", "tree-kill": "^1.1.0" + }, + "dependencies": { + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "dev": true + } } }, "conf": { diff --git a/package.json b/package.json index 3d89b5696c..a5f7def7d1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\"" }, "devDependencies": { + "@types/commander": "^2.12.2", "@types/form-data": "^2.2.1", "@types/jasmine": "^2.8.8", "@types/lowdb": "^1.0.5", @@ -72,6 +73,7 @@ "@aspnet/signalr": "1.0.4", "@aspnet/signalr-protocol-msgpack": "1.0.4", "big-integer": "1.6.36", + "commander": "2.18.0", "core-js": "2.6.2", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "electron-log": "2.2.17", diff --git a/src/cli/commands/update.command.ts b/src/cli/commands/update.command.ts new file mode 100644 index 0000000000..600a6eb2bf --- /dev/null +++ b/src/cli/commands/update.command.ts @@ -0,0 +1,83 @@ +import * as program from 'commander'; +import * as fetch from 'node-fetch'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { Response } from '../models/response'; +import { MessageResponse } from '../models/response/messageResponse'; + +export class UpdateCommand { + inPkg: boolean = false; + + constructor(private platformUtilsService: PlatformUtilsService, private repoName: string, + private executableName: string) { + this.inPkg = !!(process as any).pkg; + } + + async run(cmd: program.Command): Promise { + const currentVersion = this.platformUtilsService.getApplicationVersion(); + + const response = await fetch.default('https://api.github.com/repos/bitwarden/' + + this.repoName + '/releases/latest'); + if (response.status === 200) { + const responseJson = await response.json(); + const res = new MessageResponse(null, null); + + const tagName: string = responseJson.tag_name; + if (tagName === ('v' + currentVersion)) { + res.title = 'No update available.'; + res.noColor = true; + return Response.success(res); + } + + let downloadUrl: string = null; + if (responseJson.assets != null) { + for (const a of responseJson.assets) { + const download: string = a.browser_download_url; + if (download == null) { + continue; + } + + if (download.indexOf('.zip') === -1) { + continue; + } + + if (process.platform === 'win32' && download.indexOf(this.executableName + '-windows') > -1) { + downloadUrl = download; + break; + } else if (process.platform === 'darwin' && download.indexOf(this.executableName + '-macos') > -1) { + downloadUrl = download; + break; + } else if (process.platform === 'linux' && download.indexOf(this.executableName + '-linux') > -1) { + downloadUrl = download; + break; + } + } + } + + res.title = 'A new version is available: ' + tagName; + if (downloadUrl == null) { + downloadUrl = 'https://github.com/bitwarden/' + this.repoName + '/releases'; + } else { + res.raw = downloadUrl; + } + res.message = ''; + if (responseJson.body != null && responseJson.body !== '') { + res.message = responseJson.body + '\n\n'; + } + + res.message += 'You can download this update at ' + downloadUrl; + + if (this.inPkg) { + res.message += '\n\nIf you installed this CLI through a package manager ' + + 'you should probably update using its update command instead.'; + } else { + res.message += '\n\nIf you installed this CLI through NPM ' + + 'you should update using `npm install -g @bitwarden/' + this.repoName + '`'; + } + return Response.success(res); + } else { + return Response.error('Error contacting update API: ' + response.status); + } + } +} diff --git a/src/cli/models/response.ts b/src/cli/models/response.ts new file mode 100644 index 0000000000..d361c3a2b6 --- /dev/null +++ b/src/cli/models/response.ts @@ -0,0 +1,43 @@ +import { BaseResponse } from './response/baseResponse'; + +export class Response { + static error(error: any): Response { + const res = new Response(); + res.success = false; + if (typeof (error) === 'string') { + res.message = error; + } else { + res.message = error.message != null ? error.message : error.toString(); + } + return res; + } + + static notFound(): Response { + return Response.error('Not found.'); + } + + static badRequest(message: string): Response { + return Response.error(message); + } + + static multipleResults(ids: string[]): Response { + let msg = 'More than one result was found. Try getting a specific object by `id` instead. ' + + 'The following objects were found:'; + ids.forEach((id) => { + msg += '\n' + id; + }); + return Response.error(msg); + } + + static success(data?: BaseResponse): Response { + const res = new Response(); + res.success = true; + res.data = data; + return res; + } + + success: boolean; + message: string; + errorCode: number; + data: BaseResponse; +} diff --git a/src/cli/models/response/baseResponse.ts b/src/cli/models/response/baseResponse.ts new file mode 100644 index 0000000000..9d8beca059 --- /dev/null +++ b/src/cli/models/response/baseResponse.ts @@ -0,0 +1,3 @@ +export interface BaseResponse { + object: string; +} diff --git a/src/cli/models/response/listResponse.ts b/src/cli/models/response/listResponse.ts new file mode 100644 index 0000000000..7995bd4fe8 --- /dev/null +++ b/src/cli/models/response/listResponse.ts @@ -0,0 +1,11 @@ +import { BaseResponse } from './baseResponse'; + +export class ListResponse implements BaseResponse { + object: string; + data: BaseResponse[]; + + constructor(data: BaseResponse[]) { + this.object = 'list'; + this.data = data; + } +} diff --git a/src/cli/models/response/messageResponse.ts b/src/cli/models/response/messageResponse.ts new file mode 100644 index 0000000000..448e3db795 --- /dev/null +++ b/src/cli/models/response/messageResponse.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from './baseResponse'; + +export class MessageResponse implements BaseResponse { + object: string; + title: string; + message: string; + raw: string; + noColor = false; + + constructor(title: string, message: string) { + this.object = 'message'; + this.title = title; + this.message = message; + } +} diff --git a/src/cli/models/response/stringResponse.ts b/src/cli/models/response/stringResponse.ts new file mode 100644 index 0000000000..b9a0f04412 --- /dev/null +++ b/src/cli/models/response/stringResponse.ts @@ -0,0 +1,11 @@ +import { BaseResponse } from './baseResponse'; + +export class StringResponse implements BaseResponse { + object: string; + data: string; + + constructor(data: string) { + this.object = 'string'; + this.data = data; + } +} diff --git a/src/cli/services/cliPlatformUtils.service.ts b/src/cli/services/cliPlatformUtils.service.ts new file mode 100644 index 0000000000..ad4f69b6fc --- /dev/null +++ b/src/cli/services/cliPlatformUtils.service.ts @@ -0,0 +1,132 @@ + +import { DeviceType } from '../../enums/deviceType'; + +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +export class CliPlatformUtilsService implements PlatformUtilsService { + identityClientId: string; + + private deviceCache: DeviceType = null; + + constructor(identityClientId: string, private packageJson: any) { + this.identityClientId = identityClientId; + } + + getDevice(): DeviceType { + if (!this.deviceCache) { + switch (process.platform) { + case 'win32': + this.deviceCache = DeviceType.WindowsDesktop; + break; + case 'darwin': + this.deviceCache = DeviceType.MacOsDesktop; + break; + case 'linux': + default: + this.deviceCache = DeviceType.LinuxDesktop; + break; + } + } + + return this.deviceCache; + } + + getDeviceString(): string { + const device = DeviceType[this.getDevice()].toLowerCase(); + return device.replace('desktop', ''); + } + + isFirefox() { + return false; + } + + isChrome() { + return false; + } + + isEdge() { + return false; + } + + isOpera() { + return false; + } + + isVivaldi() { + return false; + } + + isSafari() { + return false; + } + + isIE() { + return false; + } + + isMacAppStore() { + return false; + } + + analyticsId() { + return null as string; + } + + isViewOpen() { + return false; + } + + lockTimeout(): number { + return null; + } + + launchUri(uri: string, options?: any): void { + throw new Error('Not implemented.'); + } + + saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void { + throw new Error('Not implemented.'); + } + + getApplicationVersion(): string { + return this.packageJson.version; + } + + supportsU2f(win: Window) { + return false; + } + + supportsDuo(): boolean { + return false; + } + + showToast(type: 'error' | 'success' | 'warning' | 'info', title: string, text: string | string[], + options?: any): void { + throw new Error('Not implemented.'); + } + + showDialog(text: string, title?: string, confirmText?: string, cancelText?: string, type?: string): + Promise { + throw new Error('Not implemented.'); + } + + eventTrack(action: string, label?: string, options?: any) { + throw new Error('Not implemented.'); + } + + isDev(): boolean { + return process.env.BWCLI_ENV === 'development'; + } + + isSelfHost(): boolean { + return false; + } + + copyToClipboard(text: string, options?: any): void { + throw new Error('Not implemented.'); + } + + readFromClipboard(options?: any): Promise { + throw new Error('Not implemented.'); + } +} diff --git a/src/cli/services/consoleLog.service.ts b/src/cli/services/consoleLog.service.ts new file mode 100644 index 0000000000..5e1904790d --- /dev/null +++ b/src/cli/services/consoleLog.service.ts @@ -0,0 +1,53 @@ +import { LogLevelType } from '../../enums/logLevelType'; + +import { LogService as LogServiceAbstraction } from '../../abstractions/log.service'; + +export class ConsoleLogService implements LogServiceAbstraction { + constructor(private isDev: boolean, private filter: (level: LogLevelType) => boolean = null) { } + + debug(message: string) { + if (!this.isDev) { + return; + } + this.write(LogLevelType.Debug, message); + } + + info(message: string) { + this.write(LogLevelType.Info, message); + } + + warning(message: string) { + this.write(LogLevelType.Warning, message); + } + + error(message: string) { + this.write(LogLevelType.Error, message); + } + + write(level: LogLevelType, message: string) { + if (this.filter != null && this.filter(level)) { + return; + } + + switch (level) { + case LogLevelType.Debug: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Info: + // tslint:disable-next-line + console.log(message); + break; + case LogLevelType.Warning: + // tslint:disable-next-line + console.warn(message); + break; + case LogLevelType.Error: + // tslint:disable-next-line + console.error(message); + break; + default: + break; + } + } +} diff --git a/src/services/noopMessaging.service.ts b/src/services/noopMessaging.service.ts new file mode 100644 index 0000000000..c1a1443ca7 --- /dev/null +++ b/src/services/noopMessaging.service.ts @@ -0,0 +1,7 @@ +import { MessagingService } from '../abstractions/messaging.service'; + +export class NoopMessagingService implements MessagingService { + send(subscriber: string, arg: any = {}) { + // Do nothing... + } +}