mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-17 09:59:58 +00:00
made cli handling extensible - fixes #3763
This commit is contained in:
parent
8fb2bc1ba0
commit
fa07fdcb64
@ -19,7 +19,6 @@
|
||||
"devDependencies": {
|
||||
"@electron/remote": "1.1.0",
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"@types/winston": "^2.3.6",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^4.1.3",
|
||||
@ -32,7 +31,6 @@
|
||||
"ng2-dnd": "^5.0.2",
|
||||
"ngx-perfect-scrollbar": "^10.1.0",
|
||||
"readable-stream": "3.6.0",
|
||||
"shell-escape": "^0.2.0",
|
||||
"uuid": "^8.0.0",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
|
12
terminus-core/src/api/cli.ts
Normal file
12
terminus-core/src/api/cli.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface CLIEvent {
|
||||
argv: any
|
||||
cwd: string
|
||||
secondInstance: boolean
|
||||
}
|
||||
|
||||
export abstract class CLIHandler {
|
||||
priority: number
|
||||
firstMatchOnly: boolean
|
||||
|
||||
abstract handle (event: CLIEvent): Promise<boolean>
|
||||
}
|
@ -9,6 +9,7 @@ export { HotkeyProvider, HotkeyDescription } from './hotkeyProvider'
|
||||
export { Theme } from './theme'
|
||||
export { TabContextMenuItemProvider } from './tabContextMenuProvider'
|
||||
export { SelectorOption } from './selector'
|
||||
export { CLIHandler, CLIEvent } from './cli'
|
||||
|
||||
export { AppService } from '../services/app.service'
|
||||
export { ConfigService } from '../services/config.service'
|
||||
|
20
terminus-core/src/cli.ts
Normal file
20
terminus-core/src/cli.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { HostAppService } from './services/hostApp.service'
|
||||
import { CLIHandler, CLIEvent } from './api/cli'
|
||||
|
||||
@Injectable()
|
||||
export class LastCLIHandler extends CLIHandler {
|
||||
firstMatchOnly = true
|
||||
priority = -999
|
||||
|
||||
constructor (private hostApp: HostAppService) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handle (event: CLIEvent): Promise<boolean> {
|
||||
if (event.secondInstance) {
|
||||
this.hostApp.newWindow()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ import { FastHtmlBindDirective } from './directives/fastHtmlBind.directive'
|
||||
import { HotkeyProvider } from './api/hotkeyProvider'
|
||||
import { ConfigProvider } from './api/configProvider'
|
||||
import { Theme } from './api/theme'
|
||||
import { CLIHandler } from './api/cli'
|
||||
import { TabContextMenuItemProvider } from './api/tabContextMenuProvider'
|
||||
import { TabRecoveryProvider } from './api/tabRecovery'
|
||||
|
||||
@ -37,6 +38,7 @@ import { StandardTheme, StandardCompactTheme, PaperTheme } from './theme'
|
||||
import { CoreConfigProvider } from './config'
|
||||
import { AppHotkeyProvider } from './hotkeys'
|
||||
import { TaskCompletionContextMenu, CommonOptionsContextMenu, TabManagementContextMenu } from './tabContextMenu'
|
||||
import { LastCLIHandler } from './cli'
|
||||
|
||||
import 'perfect-scrollbar/css/perfect-scrollbar.css'
|
||||
import 'ng2-dnd/bundles/style.css'
|
||||
@ -51,6 +53,7 @@ const PROVIDERS = [
|
||||
{ provide: TabContextMenuItemProvider, useClass: TabManagementContextMenu, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: TaskCompletionContextMenu, multi: true },
|
||||
{ provide: TabRecoveryProvider, useClass: SplitTabRecoveryProvider, multi: true },
|
||||
{ provide: CLIHandler, useClass: LastCLIHandler, multi: true },
|
||||
{ provide: PERFECT_SCROLLBAR_CONFIG, useValue: { suppressScrollX: true } },
|
||||
]
|
||||
|
||||
|
@ -1,11 +1,9 @@
|
||||
import type { BrowserWindow, TouchBar, MenuItemConstructorOptions } from 'electron'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'mz/fs'
|
||||
import shellEscape from 'shell-escape'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { Injectable, NgZone, EventEmitter } from '@angular/core'
|
||||
import { Injectable, NgZone, EventEmitter, Injector } from '@angular/core'
|
||||
import { ElectronService } from './electron.service'
|
||||
import { Logger, LogService } from './log.service'
|
||||
import { CLIHandler } from '../api/cli'
|
||||
import { isWindowsBuild, WIN_BUILD_FLUENT_BG_SUPPORTED } from '../utils'
|
||||
|
||||
/* eslint-disable block-scoped-var */
|
||||
@ -42,11 +40,6 @@ export class HostAppService {
|
||||
isPortable = !!process.env.PORTABLE_EXECUTABLE_FILE
|
||||
|
||||
private preferencesMenu = new Subject<void>()
|
||||
private secondInstance = new Subject<void>()
|
||||
private cliOpenDirectory = new Subject<string>()
|
||||
private cliRunCommand = new Subject<string[]>()
|
||||
private cliPaste = new Subject<string>()
|
||||
private cliOpenProfile = new Subject<string>()
|
||||
private configChangeBroadcast = new Subject<void>()
|
||||
private windowCloseRequest = new Subject<void>()
|
||||
private windowMoved = new Subject<void>()
|
||||
@ -61,31 +54,6 @@ export class HostAppService {
|
||||
*/
|
||||
get preferencesMenu$ (): Observable<void> { return this.preferencesMenu }
|
||||
|
||||
/**
|
||||
* Fired when a second instance of Terminus is launched
|
||||
*/
|
||||
get secondInstance$ (): Observable<void> { return this.secondInstance }
|
||||
|
||||
/**
|
||||
* Fired for the `terminus open` CLI command
|
||||
*/
|
||||
get cliOpenDirectory$ (): Observable<string> { return this.cliOpenDirectory }
|
||||
|
||||
/**
|
||||
* Fired for the `terminus run` CLI command
|
||||
*/
|
||||
get cliRunCommand$ (): Observable<string[]> { return this.cliRunCommand }
|
||||
|
||||
/**
|
||||
* Fired for the `terminus paste` CLI command
|
||||
*/
|
||||
get cliPaste$ (): Observable<string> { return this.cliPaste }
|
||||
|
||||
/**
|
||||
* Fired for the `terminus profile` CLI command
|
||||
*/
|
||||
get cliOpenProfile$ (): Observable<string> { return this.cliOpenProfile }
|
||||
|
||||
/**
|
||||
* Fired when another window modified the config file
|
||||
*/
|
||||
@ -107,6 +75,7 @@ export class HostAppService {
|
||||
private constructor (
|
||||
private zone: NgZone,
|
||||
private electron: ElectronService,
|
||||
injector: Injector,
|
||||
log: LogService,
|
||||
) {
|
||||
this.logger = log.create('hostApp')
|
||||
@ -159,28 +128,18 @@ export class HostAppService {
|
||||
|
||||
electron.ipcRenderer.on('cli', (_$event, argv: any, cwd: string, secondInstance: boolean) => this.zone.run(async () => {
|
||||
this.logger.info('CLI arguments received:', argv)
|
||||
const op = argv._[0]
|
||||
const opAsPath = op ? path.resolve(cwd, op) : null
|
||||
if (op === 'open') {
|
||||
this.cliOpenDirectory.next(path.resolve(cwd, argv.directory))
|
||||
} else if (op === 'run') {
|
||||
this.cliRunCommand.next(argv.command)
|
||||
} else if (op === 'paste') {
|
||||
let text = argv.text
|
||||
if (argv.escape) {
|
||||
text = shellEscape([text])
|
||||
}
|
||||
this.cliPaste.next(text)
|
||||
} else if (op === 'profile') {
|
||||
this.cliOpenProfile.next(argv.profileName)
|
||||
} else if (secondInstance && op === undefined) {
|
||||
this.newWindow()
|
||||
} else if (opAsPath && (await fs.lstat(opAsPath)).isDirectory()) {
|
||||
this.cliOpenDirectory.next(opAsPath)
|
||||
}
|
||||
|
||||
if (secondInstance) {
|
||||
this.secondInstance.next()
|
||||
const cliHandlers = injector.get(CLIHandler) as unknown as CLIHandler[]
|
||||
cliHandlers.sort((a, b) => b.priority - a.priority)
|
||||
|
||||
let handled = false
|
||||
for (const handler of cliHandlers) {
|
||||
if (handled && handler.firstMatchOnly) {
|
||||
continue
|
||||
}
|
||||
if (await handler.handle({ argv, cwd, secondInstance })) {
|
||||
handled = true
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
|
@ -26,11 +26,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.5.tgz#74deebbbcb1e86634dbf10a5b5e8798626f5a597"
|
||||
integrity sha512-iotVxtCCsPLRAvxMFFgxL8HD2l4mAZ2Oin7/VJ2ooWO0VOK4EGOGmZWZn1uCq7RofR3I/1IOSjCHlFT71eVK0Q==
|
||||
|
||||
"@types/shell-escape@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/shell-escape/-/shell-escape-0.2.0.tgz#cd2f0df814388599dd07196dcc510de2669d1ed2"
|
||||
integrity sha512-7kUdtJtUylvyISJbe9FMcvMTjRdP0EvNDO1WbT0lT22k/IPBiPRTpmWaKu5HTWLCGLQRWVHrzVHZktTDvvR23g==
|
||||
|
||||
"@types/winston@^2.3.6":
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/winston/-/winston-2.4.4.tgz#48cc744b7b42fad74b9a2e8490e0112bd9a3d08d"
|
||||
@ -410,11 +405,6 @@ shallow-clone@^3.0.0:
|
||||
dependencies:
|
||||
kind-of "^6.0.2"
|
||||
|
||||
shell-escape@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133"
|
||||
integrity sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM=
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||
|
@ -22,12 +22,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/deep-equal": "^1.0.0",
|
||||
"@types/shell-escape": "^0.2.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"dataurl": "0.1.0",
|
||||
"deep-equal": "2.0.5",
|
||||
"mz": "^2.6.0",
|
||||
"ps-node": "^0.1.6",
|
||||
"runes": "^0.4.2",
|
||||
"shell-escape": "^0.2.0",
|
||||
"slugify": "^1.4.0",
|
||||
"utils-decorators": "^1.8.1",
|
||||
"xterm": "^4.9.0-beta.7",
|
||||
|
135
terminus-terminal/src/cli.ts
Normal file
135
terminus-terminal/src/cli.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import * as path from 'path'
|
||||
import * as fs from 'mz/fs'
|
||||
import shellEscape from 'shell-escape'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { CLIHandler, CLIEvent, HostAppService, AppService, ConfigService } from 'terminus-core'
|
||||
import { TerminalTabComponent } from './components/terminalTab.component'
|
||||
import { TerminalService } from './services/terminal.service'
|
||||
|
||||
@Injectable()
|
||||
export class TerminalCLIHandler extends CLIHandler {
|
||||
firstMatchOnly = true
|
||||
priority = 0
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private config: ConfigService,
|
||||
private hostApp: HostAppService,
|
||||
private terminal: TerminalService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handle (event: CLIEvent): Promise<boolean> {
|
||||
const op = event.argv._[0]
|
||||
|
||||
if (op === 'open') {
|
||||
this.handleOpenDirectory(path.resolve(event.cwd, event.argv.directory))
|
||||
} else if (op === 'run') {
|
||||
this.handleRunCommand(event.argv.command)
|
||||
} else if (op === 'paste') {
|
||||
let text = event.argv.text
|
||||
if (event.argv.escape) {
|
||||
text = shellEscape([text])
|
||||
}
|
||||
this.handlePaste(text)
|
||||
} else if (op === 'profile') {
|
||||
this.handleOpenProfile(event.argv.profileName)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private async handleOpenDirectory (directory: string) {
|
||||
if (directory.length > 1 && (directory.endsWith('/') || directory.endsWith('\\'))) {
|
||||
directory = directory.substring(0, directory.length - 1)
|
||||
}
|
||||
if (await fs.exists(directory)) {
|
||||
if ((await fs.stat(directory)).isDirectory()) {
|
||||
this.terminal.openTab(undefined, directory)
|
||||
this.hostApp.bringToFront()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleRunCommand (command: string[]) {
|
||||
this.terminal.openTab({
|
||||
name: '',
|
||||
sessionOptions: {
|
||||
command: command[0],
|
||||
args: command.slice(1),
|
||||
},
|
||||
}, null, true)
|
||||
this.hostApp.bringToFront()
|
||||
}
|
||||
|
||||
private handleOpenProfile (profileName: string) {
|
||||
const profile = this.config.store.terminal.profiles.find(x => x.name === profileName)
|
||||
if (!profile) {
|
||||
console.error('Requested profile', profileName, 'not found')
|
||||
return
|
||||
}
|
||||
this.terminal.openTabWithOptions(profile.sessionOptions)
|
||||
this.hostApp.bringToFront()
|
||||
}
|
||||
|
||||
private handlePaste (text: string) {
|
||||
if (this.app.activeTab instanceof TerminalTabComponent && this.app.activeTab.session) {
|
||||
this.app.activeTab.sendInput(text)
|
||||
this.hostApp.bringToFront()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class OpenPathCLIHandler extends CLIHandler {
|
||||
firstMatchOnly = true
|
||||
priority = -100
|
||||
|
||||
constructor (
|
||||
private terminal: TerminalService,
|
||||
private hostApp: HostAppService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handle (event: CLIEvent): Promise<boolean> {
|
||||
const op = event.argv._[0]
|
||||
const opAsPath = op ? path.resolve(event.cwd, op) : null
|
||||
|
||||
if (opAsPath && (await fs.lstat(opAsPath)).isDirectory()) {
|
||||
this.terminal.openTab(undefined, opAsPath)
|
||||
this.hostApp.bringToFront()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AutoOpenTabCLIHandler extends CLIHandler {
|
||||
firstMatchOnly = true
|
||||
priority = -1000
|
||||
|
||||
constructor (
|
||||
private app: AppService,
|
||||
private config: ConfigService,
|
||||
private terminal: TerminalService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handle (event: CLIEvent): Promise<boolean> {
|
||||
if (!event.secondInstance && this.config.store.terminal.autoOpen) {
|
||||
this.app.ready$.subscribe(() => {
|
||||
this.terminal.openTab()
|
||||
})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
import * as fs from 'mz/fs'
|
||||
|
||||
import { NgModule } from '@angular/core'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastrModule } from 'ngx-toastr'
|
||||
|
||||
import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, AppService, ConfigService, TabContextMenuItemProvider, ElectronService } from 'terminus-core'
|
||||
import TerminusCorePlugin, { HostAppService, ToolbarButtonProvider, TabRecoveryProvider, ConfigProvider, HotkeysService, HotkeyProvider, TabContextMenuItemProvider, CLIHandler } from 'terminus-core'
|
||||
import { SettingsTabProvider } from 'terminus-settings'
|
||||
|
||||
import { AppearanceSettingsTabComponent } from './components/appearanceSettingsTab.component'
|
||||
@ -57,6 +55,7 @@ import { hterm } from './frontends/hterm'
|
||||
import { Frontend } from './frontends/frontend'
|
||||
import { HTermFrontend } from './frontends/htermFrontend'
|
||||
import { XTermFrontend, XTermWebGLFrontend } from './frontends/xtermFrontend'
|
||||
import { AutoOpenTabCLIHandler, OpenPathCLIHandler, TerminalCLIHandler } from './cli'
|
||||
|
||||
/** @hidden */
|
||||
@NgModule({
|
||||
@ -100,6 +99,10 @@ import { XTermFrontend, XTermWebGLFrontend } from './frontends/xtermFrontend'
|
||||
{ provide: TabContextMenuItemProvider, useClass: SaveAsProfileContextMenu, multi: true },
|
||||
{ provide: TabContextMenuItemProvider, useClass: LegacyContextMenu, multi: true },
|
||||
|
||||
{ provide: CLIHandler, useClass: TerminalCLIHandler, multi: true },
|
||||
{ provide: CLIHandler, useClass: OpenPathCLIHandler, multi: true },
|
||||
{ provide: CLIHandler, useClass: AutoOpenTabCLIHandler, multi: true },
|
||||
|
||||
// For WindowsDefaultShellProvider
|
||||
PowerShellCoreShellProvider,
|
||||
WSLShellProvider,
|
||||
@ -133,13 +136,10 @@ import { XTermFrontend, XTermWebGLFrontend } from './frontends/xtermFrontend'
|
||||
})
|
||||
export default class TerminalModule { // eslint-disable-line @typescript-eslint/no-extraneous-class
|
||||
private constructor (
|
||||
app: AppService,
|
||||
config: ConfigService,
|
||||
hotkeys: HotkeysService,
|
||||
terminal: TerminalService,
|
||||
hostApp: HostAppService,
|
||||
dockMenu: DockMenuService,
|
||||
electron: ElectronService,
|
||||
) {
|
||||
const events = [
|
||||
{
|
||||
@ -165,18 +165,6 @@ export default class TerminalModule { // eslint-disable-line @typescript-eslint/
|
||||
hotkeys.emitKeyEvent(nativeEvent)
|
||||
}
|
||||
})
|
||||
if (config.store.terminal.autoOpen) {
|
||||
let argv = electron.process.argv
|
||||
if (argv[0].includes('node')) {
|
||||
argv = argv.slice(1)
|
||||
}
|
||||
|
||||
if (require('yargs/yargs')(argv.slice(1)).parse()._[0] !== 'open'){
|
||||
app.ready$.subscribe(() => {
|
||||
terminal.openTab()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hotkeys.matchedHotkey.subscribe(async (hotkey) => {
|
||||
if (hotkey === 'new-tab') {
|
||||
@ -193,46 +181,6 @@ export default class TerminalModule { // eslint-disable-line @typescript-eslint/
|
||||
}
|
||||
})
|
||||
|
||||
hostApp.cliOpenDirectory$.subscribe(async directory => {
|
||||
if (directory.length > 1 && (directory.endsWith('/') || directory.endsWith('\\'))) {
|
||||
directory = directory.substring(0, directory.length - 1)
|
||||
}
|
||||
if (await fs.exists(directory)) {
|
||||
if ((await fs.stat(directory)).isDirectory()) {
|
||||
terminal.openTab(undefined, directory)
|
||||
hostApp.bringToFront()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
hostApp.cliRunCommand$.subscribe(async command => {
|
||||
terminal.openTab({
|
||||
name: '',
|
||||
sessionOptions: {
|
||||
command: command[0],
|
||||
args: command.slice(1),
|
||||
},
|
||||
}, null, true)
|
||||
hostApp.bringToFront()
|
||||
})
|
||||
|
||||
hostApp.cliPaste$.subscribe(text => {
|
||||
if (app.activeTab instanceof TerminalTabComponent && app.activeTab.session) {
|
||||
app.activeTab.sendInput(text)
|
||||
hostApp.bringToFront()
|
||||
}
|
||||
})
|
||||
|
||||
hostApp.cliOpenProfile$.subscribe(async profileName => {
|
||||
const profile = config.store.terminal.profiles.find(x => x.name === profileName)
|
||||
if (!profile) {
|
||||
console.error('Requested profile', profileName, 'not found')
|
||||
return
|
||||
}
|
||||
terminal.openTabWithOptions(profile.sessionOptions)
|
||||
hostApp.bringToFront()
|
||||
})
|
||||
|
||||
dockMenu.update()
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.1.tgz#71cfabb247c22bcc16d536111f50c0ed12476b03"
|
||||
integrity sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==
|
||||
|
||||
"@types/shell-escape@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/shell-escape/-/shell-escape-0.2.0.tgz#cd2f0df814388599dd07196dcc510de2669d1ed2"
|
||||
integrity sha512-7kUdtJtUylvyISJbe9FMcvMTjRdP0EvNDO1WbT0lT22k/IPBiPRTpmWaKu5HTWLCGLQRWVHrzVHZktTDvvR23g==
|
||||
|
||||
ansi-colors@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||
@ -430,6 +435,11 @@ runes@^0.4.2:
|
||||
resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.3.tgz#32f7738844bc767b65cc68171528e3373c7bb355"
|
||||
integrity sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg==
|
||||
|
||||
shell-escape@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shell-escape/-/shell-escape-0.2.0.tgz#68fd025eb0490b4f567a027f0bf22480b5f84133"
|
||||
integrity sha1-aP0CXrBJC09WegJ/C/IkgLX4QTM=
|
||||
|
||||
side-channel@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
||||
|
Loading…
x
Reference in New Issue
Block a user